本文以 Google 趨勢為例,總結(jié)在抓取全動態(tài)網(wǎng)頁信息時遇到的幾個問題及對應的解決方法抒倚。包括如何等待動態(tài)獲取的內(nèi)容加載完成褐着,以及當搜索到的對象不在可視范圍內(nèi)不可被點擊等。
注意:本文內(nèi)容具有時效性托呕,只保證在撰寫當時是正確可用的含蓉,Google 的網(wǎng)站更新變化后频敛,代碼的抓取結(jié)果不可預測。
另外馅扣,業(yè)余實習僧斟赚,非專業(yè)碼農(nóng),純屬給自己寫備忘錄差油,技術層面難登大雅之堂拗军,見諒。
背景
Google 趨勢熱門搜索排行榜 是個有趣的網(wǎng)頁蓄喇,如字面所示发侵,它提供了全球各地在指定歷史年月的熱門搜索的關鍵字榜單,按排名算每個種類提供最多的 10 個公罕,若有并列則向后順延器紧。通過分析上面的數(shù)據(jù),可以對網(wǎng)絡流行趨勢和社會熱點有一個大概的把握楼眷。
網(wǎng)頁本身不提供內(nèi)容下載通道铲汪,手動整理相當?shù)托В谑亲匀欢坏膽四蔷淅显挕苡么a解決的問題不要復制粘貼——好吧罐柳,這只是我一家之言掌腰。
這是個全動態(tài)渲染的網(wǎng)頁,禁用 JavaScript 后一片空白张吉,查看源代碼發(fā)現(xiàn)其中 80% 的部分是 JS 腳本齿梁,HTML 只占很少一部分。用傳統(tǒng)的抓取靜態(tài)網(wǎng)頁解析 HTML 標簽的辦法無法獲取其中的內(nèi)容肮蛹,需要專門的處理手段勺择。
最著名的莫過于使用 Selenium WebDriver 引擎來驅(qū)動實體瀏覽器對網(wǎng)頁進行解析,然后從瀏覽器的結(jié)果中提取信息伦忠。
關于如何上手使用這一框架的教程一大堆省核,你轉(zhuǎn)我的我轉(zhuǎn)你的,搜索一大片所獲得的還是寫差不多的內(nèi)容昆码,不是很具體和詳細气忠。我在實際使用過程中遇到了兩個大坑,因為很少有人給出簡單有效的解決辦法赋咽,所以花了不少時間才得以解決【稍耄現(xiàn)在把個人經(jīng)驗總結(jié)于此,以來日后自己忘了可以回查脓匿,二來如果有幸能幫助到有同樣困惑的人淘钟,也算好事一樁。
準備工作
環(huán)境:Python 3.5陪毡、Selenium 3.0.2日月、ChromeDriver袱瓮,具體配置方法從略缤骨。
Google 趨勢的熱門搜索排行榜的地址是https://trends.google.com/trends/topcharts
爱咬,在其后用#
作為分隔來添加參數(shù),geo
表示 地區(qū)绊起,date
表示 時間(年月)精拟,不同參數(shù)用&
隔開,例如查詢 美國 2016 年 9 月 的排行榜虱歪,就在 URL 后添加#geo=US&date=201609
蜂绎。這是基本的 URL 約定,不再贅述笋鄙。
添加引用
from selenium import webdriver
定義網(wǎng)頁引擎并打開指定頁面
driver = webdriver.Chrome()
driver.get("https://trends.google.com/trends/topcharts#geo=US&date=201611")
解決等待頁面內(nèi)容加載的問題
抓取內(nèi)容需要等待目標元素被加載后才可以進行师枣,否則會引起無法定位元素的異常。在靜態(tài)網(wǎng)頁中萧落,頁面加載結(jié)束后所有的內(nèi)容就都已經(jīng)存在在瀏覽器中践美,但是在動態(tài)加載的網(wǎng)頁中,頁面加載完畢后找岖,動態(tài)加載的元素不一定已經(jīng)被獲取陨倡,需要確保目標元素已經(jīng)完成加載后在進行抓取操作。
在網(wǎng)上查詢解決方案時大多為很雞肋的“硬方法”许布, 即人為將程序暫停一段時間兴革,等待頁面加載完成。
import time
driver = webdriver.Chrome()
driver.get("https://trends.google.com/trends/topcharts#geo=US&date=201611")
# Wait for completion.
time.sleep(3)
# Extract information.
這樣做弊病很多蜜唾,一方面由于網(wǎng)絡環(huán)境的不確定性杂曲,程序無法確保在規(guī)定等待時間結(jié)束后目標元素已經(jīng)加載完成;另一方面如果在指定時間內(nèi)就已經(jīng)加載完成袁余,則會造成不必要的時間浪費擎勘。無論哪一種都不是理想的解決思路。
應該使用 Selenium 框架提供的官方解決方案泌霍,由檢測目標元素的可見性確定加載是否完成货抄,阻塞程序然后再進行下一步的處理。
alecxe, MrE - StakOverflow
You need to do this step by step checking the visibility of the elements you are going to interact with using Explicit Waits, do not usetime.sleep()
- it is not reliable and error-prone.
為此朱转,新增引用
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
指定第一個要查找的目標元素蟹地,在這里也就是 Google 趨勢頁面上的一個分類的名字,用 XPath 來定位藤为,并且使用官方提供的“等待直到”方法來等待目標元素加載完成
xpath = '//*[@id="djs-trending"]/div/a/div[1]/div/span'
element = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.XPATH, xpath)))
如此怪与,程序在尋找目標元素時會被阻塞,直到在瀏覽器中能夠找到該元素缅疟,也即該元素加載完成分别,即可恢復執(zhí)行遍愿,進行后面的操作。
實際上耘斩,除了 顯式等待 以外還有 隱式等待 可以使用沼填,這兩點的用法在 官方文檔 中有詳細的說明。比起顯式等待來說括授,隱式等待更有“一勞永逸”的效果坞笙,只要進行如下設置
# Set timeout to 10 seconds.
driver.implicitly_wait(10)
即可在后續(xù)的操作中的每一步都進行加載完成與否的檢驗,比顯式等待要清爽得多荚虚。
解決目標元素不在可視范圍內(nèi)無法點擊的問題
個別時候薛夜,并不是任何時候,在獲取到目標元素后版述,對其發(fā)送點擊事件或者鍵盤事件時梯澜,會提示元素無法接收該事件,事件會被其他元素攔截或者找不到該對象渴析。在確定無疑不是新彈出的上層元素將其覆蓋的情況下晚伙,這可能是因為目標元素沒有出現(xiàn)在瀏覽器可見范圍內(nèi)而導致的。
并不清楚背后的原理檬某,但是解決思路簡單暴力——將目標元素滾動到可視范圍內(nèi)來撬腾。可以通過對可接受事件的元素發(fā)送按鍵事件來模擬向下滾動恢恼,也可以通過 JS 來實現(xiàn)民傻。最為精準而安全的措施是直接將對象滾動到可視范圍的最頂端,類似頁面內(nèi)書簽的定位
# Scroll element to the top edge of the view.
driver.execute_script("return arguments[0].scrollIntoView();", element)
而后再進行鍵鼠事件操作即可场斑。
完整代碼
# Get top 10 keywords in https://trends.google.com/trends/topcharts
# Boss Ox / 2017.02.20 / Beijing @ByteDance
import threading
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# Settings.
URL = 'https://trends.google.com/trends/topcharts#geo=US&date='
SaveFolder = r'F:\Project\Python\GoogleTrends' + '\\'
ConcurrentNumber = 5
# Date to fetch.
Dates = [
'201611',
'201610',
'201609',
'201608',
'201607',
'201606',
'201605',
'201604',
'201603',
'201602',
'201601'
]
# Genres to fetch, acquired by category text in page, by XPath.
Genres = [
'//*[@id="djs-trending"]/div/a/div[1]/div/span',
'//*[@id="people-trending"]/div/a/div[1]/div/span',
'//*[@id="authors-trending"]/div/a/div[1]/div/span',
'//*[@id="childrens_tv_programs-trending"]/div/a/div[1]/div/span',
'//*[@id="animals-trending"]/div/a/div[1]/div/span',
'//*[@id="countries-trending"]/div/a/div[1]/div/span',
'//*[@id="books-trending"]/div/a/div[1]/div/span',
'//*[@id="cities-trending"]/div/a/div[1]/div/span',
'//*[@id="celestial_objects-trending"]/div/a/div[1]/div/span',
'//*[@id="whiskey-top"]/div/a/div[1]/div/span',
'//*[@id="fast_food_restaurants-trending"]/div/a/div[1]/div/span',
'//*[@id="governmental_bodies-top"]/div/a/div[1]/div/span',
'//*[@id="politicians-trending"]/div/a/div[1]/div/span',
'//*[@id="fashion_labels-top"]/div/a/div[1]/div/span',
'//*[@id="baseball_players-trending"]/div/a/div[1]/div/span',
'//*[@id="baseball_teams-trending"]/div/a/div[1]/div/span',
'//*[@id="songs-top"]/div/a/div[1]/div/span',
'//*[@id="automobile_models-trending"]/div/a/div[1]/div/span',
'//*[@id="auto_companies-top"]/div/a/div[1]/div/span',
'//*[@id="games-top"]/div/a/div[1]/div/span',
'//*[@id="actors-trending"]/div/a/div[1]/div/span',
'//*[@id="dog_breeds-trending"]/div/a/div[1]/div/span',
'//*[@id="sports_teams-trending"]/div/a/div[1]/div/span',
'//*[@id="films-trending"]/div/a/div[1]/div/span',
'//*[@id="tv_shows-trending"]/div/a/div[1]/div/span',
'//*[@id="reality_shows-trending"]/div/a/div[1]/div/span',
'//*[@id="scientists-trending"]/div/a/div[1]/div/span',
'//*[@id="basketball_players-trending"]/div/a/div[1]/div/span',
'//*[@id="basketball_teams-top"]/div/a/div[1]/div/span',
'//*[@id="us_governors-top"]/div/a/div[1]/div/span',
'//*[@id="foods-top"]/div/a/div[1]/div/span',
'//*[@id="energy_companies-top"]/div/a/div[1]/div/span',
'//*[@id="medicines-top"]/div/a/div[1]/div/span',
'//*[@id="soccer_players-trending"]/div/a/div[1]/div/span',
'//*[@id="soccer_teams-trending"]/div/a/div[1]/div/span',
'//*[@id="sports_cars-trending"]/div/a/div[1]/div/span',
'//*[@id="programming_languages-top"]/div/a/div[1]/div/span',
'//*[@id="athletes-trending"]/div/a/div[1]/div/span',
'//*[@id="financial_companies-top"]/div/a/div[1]/div/span',
'//*[@id="retail_companies-top"]/div/a/div[1]/div/span',
'//*[@id="teen_pop_artists-trending"]/div/a/div[1]/div/span',
'//*[@id="musicians-trending"]/div/a/div[1]/div/span',
'//*[@id="beverages-top"]/div/a/div[1]/div/span',
'//*[@id="colleges_universities-trending"]/div/a/div[1]/div/span',
'//*[@id="cocktails-top"]/div/a/div[1]/div/span'
]
# Fetch information in each genre on date.
def getTrendsOnDate(month):
url = URL + month
driver = webdriver.Chrome() # PhantomJS can fail extracting second item. DKW.
results = {}
try:
for genre in Genres:
# Load page.
driver.get(url)
# Wait for completion.
element = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.XPATH, genre)))
# Find genre.
element = driver.find_element_by_xpath(genre)
if element != None:
# Get genre text.
genre_text = element.text
# Scroll down to element
driver.execute_script('return arguments[0].scrollIntoView();', element)
# Open genre sub-page.
element.click()
# Wait for completion.
first_item_xpath = '/html/body/div[23]/div[2]/div/div[1]/div/span/div/span[1]/div/a'
WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.XPATH, first_item_xpath)))
# Extract information of top 10 items.
items = []
for i in range(1, 11):
item_xpath = '/html/body/div[23]/div[2]/div/div[%d]/div/span/div/span[1]/div/a' % (i)
element = driver.find_element_by_xpath(item_xpath)
items.append(element.text)
# Store results.
results[genre_text] = items
else:
# Genre not found, skip this genre.
pass
except:
# Anything wrong happens, just output what we have till now.
pass
# Output results.
outputResults(month, results)
# Close driver.
driver.quit()
def outputResults(month, results):
filename = SaveFolder + month + '.txt'
try:
with open(filename, 'w', encoding= 'utf_8_sig') as file:
for result in results:
for item in results[result]:
line = '%s\t%s'%(result, item)
file.writelines(line + '\n')
print('[ %s ] Completed.'%(month))
except Exception as e:
print('[ %s ] Error on writing file %s.\n %s'%(month, filename, e.args))
# Program Entrance.
while len(Dates) > 0:
# Get data on target date.
target = Dates.pop()
print('[ %s ] task started.' % (target))
task = threading.Thread(target= getTrendsOnDate, args= {target, })
task.start()
# Limit concurrent thread number.
while threading.activeCount() > ConcurrentNumber:
time.sleep(0.2)
總結(jié)
這段代碼還有很多待完善的地方漓踢,比如巨大的方法應該被拆分重構(gòu),對頁面的解析容錯度較小漏隐,性能有待優(yōu)化喧半,以及采用 PhantomJS 引擎時莫名其妙的信息丟失問題等。但是秉承著“先實現(xiàn)功能解決問題青责,再花精力想如何做好”的觀念挺据,有了能用的工具我就挺開心的了哈哈哈。
雖然 Python 解釋器的 GIL 機制使多線程性能大打折扣脖隶,但聊勝于無扁耐,多開之后的執(zhí)行效率還是有明顯提升的。
一句心得:多花時間研究官方文檔产阱。
一點題外話:新學期剛開始婉称,選了一門“計算社會學”課程作為選修,成功以經(jīng)濟學院學生身份打入信息學院內(nèi)部,課后閑聊竟偶遇在 Programmer at RUC 群里認識的好友王暗,也是緣分悔据。比起我這三天打魚兩天曬網(wǎng)的懶散人士,人家對計算機科學學習的興趣可是濃厚多了俗壹,談起我沒學過的數(shù)據(jù)結(jié)構(gòu)和算法科汗,真是慚愧不如。同學簡書賬號 CarbonCheney策肝,寫了不少深度技術文章肛捍,值得一看。