使用 Selenium 抓取 Google 趨勢的熱門搜索排行榜

本文以 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 use time.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策肝,寫了不少深度技術文章肛捍,值得一看。

參考與引用

  1. http://stackoverflow.com/questions/27934945/selenium-move-to-element-does-not-always-mouse-hover
  2. http://blog.likewise.org/2015/04/scrolling-to-an-element-with-the-python-bindings-for-selenium-webdriver/
  3. http://selenium-python.readthedocs.io/waits.html
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末之众,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子依许,更是在濱河造成了極大的恐慌棺禾,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峭跳,死亡現(xiàn)場離奇詭異膘婶,居然都是意外死亡,警方通過查閱死者的電腦和手機蛀醉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門悬襟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拯刁,你說我怎么就攤上這事脊岳。” “怎么了垛玻?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵割捅,是天一觀的道長。 經(jīng)常有香客問我帚桩,道長亿驾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任账嚎,我火速辦了婚禮莫瞬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘郭蕉。我一直安慰自己疼邀,他們只是感情好,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布恳不。 她就那樣靜靜地躺著檩小,像睡著了一般。 火紅的嫁衣襯著肌膚如雪烟勋。 梳的紋絲不亂的頭發(fā)上规求,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天筐付,我揣著相機與錄音,去河邊找鬼阻肿。 笑死瓦戚,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的丛塌。 我是一名探鬼主播较解,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼赴邻!你這毒婦竟也來了印衔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤姥敛,失蹤者是張志新(化名)和其女友劉穎奸焙,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彤敛,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡与帆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了墨榄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片玄糟。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖袄秩,靈堂內(nèi)的尸體忽然破棺而出阵翎,到底是詐尸還是另有隱情,我是刑警寧澤播揪,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布贮喧,位于F島的核電站,受9級特大地震影響猪狈,放射性物質(zhì)發(fā)生泄漏箱沦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一雇庙、第九天 我趴在偏房一處隱蔽的房頂上張望谓形。 院中可真熱鬧,春花似錦疆前、人聲如沸寒跳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽童太。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間书释,已是汗流浹背翘贮。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留爆惧,地道東北人狸页。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像扯再,于是被迫代替她去往敵國和親芍耘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容