Pyppeteer入門及中文教程

參考:

  1. 爬蟲神器 Pyppeteer 介紹及爬取某商城實戰(zhàn)
  2. Pyppeteer:比selenium更高效的爬蟲界的新神器
  3. https://pyppeteer.github.io/pyppeteer/reference.html#pyppeteer.page.Page.querySelector
  4. https://www.w3cschool.cn/puppeteer/puppeteer-yi2c37sc.html

因為有些網(wǎng)頁是可以檢測到是否是使用了selenium皂吮。并且selenium所謂的保護機制不允許跨域cookies保存以及登錄的時候必須先打開網(wǎng)頁然后后加載cookies再刷新的方式很不友好檩小。

所以采用谷歌chrome官方無頭框架puppeteer的python版本pyppeteer

1. Pyppeteer 簡介

1.1 Chrome 瀏覽器和 Chromium 瀏覽器

在 Pyppetter 中歇父,實際上它背后也是有一個類似 Chrome 瀏覽器的 Chromium 瀏覽器在執(zhí)行一些動作進行網(wǎng)頁渲染,首先說下 Chrome 瀏覽器和 Chromium 瀏覽器的淵源甥桂。

Chromium 是谷歌為了研發(fā) Chrome 而啟動的項目避咆,是完全開源的痒芝。二者基于相同的源代碼構(gòu)建赃春,Chrome 所有的新功能都會先在 Chromium 上實現(xiàn),待驗證穩(wěn)定后才會移植束昵,因此 Chromium 的版本更新頻率更高拔稳,也會包含很多新的功能,但作為一款獨立的瀏覽器锹雏,Chromium 的用戶群體要小眾得多巴比。兩款瀏覽器“同根同源”,它們有著同樣的 Logo,但配色不同轻绞,Chrome 由藍紅綠黃四種顏色組成采记,而 Chromium 由不同深度的藍色構(gòu)成。

Pyppeteer 就是依賴于 Chromium 這個瀏覽器來運行的政勃。那么有了 Pyppeteer 之后唧龄,我們就可以免去那些繁瑣的環(huán)境配置等問題。如果第一次運行的時候奸远,Chromium 瀏覽器沒有安裝既棺,那么程序會幫我們自動安裝和配置,就免去了繁瑣的環(huán)境配置等工作懒叛。另外 Pyppeteer 是基于 Python 的新特性 async 實現(xiàn)的丸冕,所以它的一些執(zhí)行也支持異步操作,效率相對于 Selenium 來說也提高了薛窥。

注意:本來chrome就問題多多胖烛,puppeteer也是各種坑,加上pyppeteer是基于前者的改編python版本诅迷,也就是產(chǎn)生了只要前兩個有一個有bug佩番,那么pyppeteer就會原封不動的繼承下來,本來這沒什么罢杉,但是現(xiàn)在遇到的問題就是pyppeteer這個項目從18年9月份之后就沒更新過了趟畏,前兩者都在不斷的更新迭代,而pyppeteer一直不更新屑那,導致很多bug根本沒人修復拱镐。

1.2 asyncio

asyncio是Python的一個異步協(xié)程庫,自3.4版本引入的標準庫持际,直接內(nèi)置了對異步IO的支持,號稱是Python最有野心的庫哗咆,官網(wǎng)上有非常詳細的介紹:

2. Pyppeteer快速上手

2.1 安裝

在第一次使用pyppeteer的時候也會自動下載并安裝chromium瀏覽器蜘欲,效果是一樣的∩渭恚總的來說姥份,pyppeteer比起selenium省去了driver配置的環(huán)節(jié)。

當然年碘,出于某種原因澈歉,也可能會出現(xiàn)chromium自動安裝無法順利完成的情況,這時可以考慮手動安裝:首先屿衅,從下列網(wǎng)址中找到自己系統(tǒng)的對應版本埃难,下載chromium壓縮包;

2.2 初始化設置
import asyncio, time
from pyppeteer import launch

async def main():
    browser = await launch(headless=False, dumpio=True, autoClose=False,
                           args=['--no-sandbox', '--window-size=1920,1080', '--disable-infobars'])   # 進入有頭模式
    page = await browser.newPage()           # 打開新的標簽頁
    await page.setViewport({'width': 1920, 'height': 1080})      # 頁面大小一致
    await page.goto('https://www.baidu.com/?tn=99669880_hao_pg') # 訪問主頁

    # evaluate()是執(zhí)行js的方法,js逆向時如果需要在瀏覽器環(huán)境下執(zhí)行js代碼的話可以利用這個方法
    # js為設置webdriver的值涡尘,防止網(wǎng)站檢測
    await page.evaluate('''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }''')
    # await page.screenshot({'path': './1.jpg'})   # 截圖保存路徑

    page_text = await page.content()   # 獲取網(wǎng)頁源碼
    print(page_text)
    time.sleep(1)
asyncio.get_event_loop().run_until_complete(main()) #調(diào)用

參數(shù)參考:Pyppeteer:比selenium更高效的爬蟲界的新神器

launch可接收的參數(shù)非常多忍弛,其中

ignoreHTTPSErrors(bool):是否忽略 HTTPS 錯誤。默認為 False
headless指定瀏覽器是否以無頭模式運行考抄,默認是True细疚。
args 指定給瀏覽器實例傳遞的參數(shù),
--disable-infobars 代表關閉瀏覽上方的“Chrome 正受到自動測試軟件的控制”川梅,
--window-size=1920,1080是設置瀏覽器的顯示大小疯兼,
--no-sandbox 是 在 docker 里使用時需要加入的參數(shù)。
關閉提示條:”Chrome 正受到自動測試軟件的控制”贫途,這個提示條有點煩镇防,那咋關閉呢?這時候就需要用到 args 參數(shù)了潮饱,禁用操作如下:browser = await launch(headless=False, args=['--disable-infobars'])

其他很多參數(shù)可以參考puppeteer的文檔https://zhaoqize.github.io/puppeteer-api-zh_CN/#?product=Puppeteer&version=v2.1.1&show=api-class-puppetee

繞過 webdriver 檢測

檢測地址:https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html

import asyncio
from pyppeteer import launch

# 測試檢測webdriver
async def main():
    browser = await launch(headless=False, args=['--disable-infobars'])
    page = await browser.newPage()
    await page.setUserAgent("Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5")
    await page.setViewport(viewport={'width': 1536, 'height': 768})
    await page.goto('https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html')
    await asyncio.sleep(25)
    await browser.close()
asyncio.get_event_loop().run_until_complete(main())

Pyppeteer 開啟 Chromium 照樣還是能被檢測到 WebDriver 的存在


無論是 selenium 的 execute_script() 方法来氧,還是 pyppeteer 的 evaluate() 方法執(zhí)行下面代碼都能臨時修改瀏覽器屬性中的 webdriver 屬性,當頁面刷新或者跳轉(zhuǎn)之后該值就會原形畢露香拉。

await page.evaluate('''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }''')

但是 pyppeteer 的最底層是封裝的puppeteer啦扬,是 js 庫,是和網(wǎng)站源碼交互最深的方式凫碌。

在 pyppeteer 中提供了一個方法:evaluateOnNewDocument()扑毡,該方法是將一段 js 代碼加載到頁面文檔中,當發(fā)生頁面導航盛险、頁面內(nèi)嵌框架導航的時候加載的 js 代碼會自動執(zhí)行瞄摊,那么當頁面刷新的時候該 js 也會執(zhí)行,這樣就保證了修改網(wǎng)站的屬性持久化的目的苦掘。

await page.evaluateOnNewDocument('() =>{ Object.defineProperties(navigator,'
                                     '{ webdriver:{ get: () => false } }) }') 
2.3 基本使用

支持的選擇器有

# 在頁面內(nèi)執(zhí)行 document.querySelector换帜。如果沒有元素匹配指定選擇器,返回值是 None
J = querySelector
# 在頁面內(nèi)執(zhí)行 document.querySelector鹤啡,然后把匹配到的元素作為第一個參數(shù)傳給 pageFunction
Jeval = querySelectorEval
# 在頁面內(nèi)執(zhí)行 document.querySelectorAll惯驼。如果沒有元素匹配指定選擇器,返回值是 []
JJ = querySelectorAll
# 在頁面內(nèi)執(zhí)行 Array.from(document.querySelectorAll(selector))递瑰,然后把匹配到的元素數(shù)組作為第一個參數(shù)傳給 pageFunction
JJeval = querySelectorAllEval
# XPath表達式
Jx = xpath

參考:https://www.cnblogs.com/zhang-zi-yi/p/10820813.html


import asyncio
from pyppeteer import launch


async def main():
    # headless參數(shù)設為False祟牲,則變成有頭模式
    # Pyppeteer支持字典和關鍵字傳參,Puppeteer只支持字典傳參
    
    # 指定引擎路徑
    # exepath = r'C:\Users\Administrator\AppData\Local\pyppeteer\pyppeteer\local-chromium\575458\chrome-win32/chrome.exe'
    # browser = await launch({'executablePath': exepath, 'headless': False, 'slowMo': 30})
    
    browser = await launch(
        # headless=False,
        {'headless': False}
    )

    page = await browser.newPage()

    # 設置頁面視圖大小
    await page.setViewport(viewport={'width': 1280, 'height': 800})

    # 是否啟用JS抖部,enabled設為False说贝,則無渲染效果
    await page.setJavaScriptEnabled(enabled=True)
    # 超時間見 1000 毫秒
    res = await page.goto('https://www.toutiao.com/', options={'timeout': 1000})
    resp_headers = res.headers  # 響應頭
    resp_status = res.status  # 響應狀態(tài)
    
    # 等待
    await asyncio.sleep(2)
    # 第二種方法,在while循環(huán)里強行查詢某元素進行等待
    while not await page.querySelector('.t'):
        pass
    # 滾動到頁面底部
    await page.evaluate('window.scrollBy(0, document.body.scrollHeight)')

    await asyncio.sleep(2)
    # 截圖 保存圖片
    await page.screenshot({'path': 'toutiao.png'})

    # 打印頁面cookies
    print(await page.cookies())

    """  打印頁面文本 """
    # 獲取所有 html 內(nèi)容
    print(await page.content())

    # 在網(wǎng)頁上執(zhí)行js 腳本
    dimensions = await page.evaluate(pageFunction='''() => {
            return {
                width: document.documentElement.clientWidth,  // 頁面寬度
                height: document.documentElement.clientHeight,  // 頁面高度
                deviceScaleFactor: window.devicePixelRatio,  // 像素比 1.0000000149011612
            }
        }''', force_expr=False)  # force_expr=False  執(zhí)行的是函數(shù)
    print(dimensions)

    #  只獲取文本  執(zhí)行 js 腳本  force_expr  為 True 則執(zhí)行的是表達式
    content = await page.evaluate(pageFunction='document.body.textContent', force_expr=True)
    print(content)

    # 打印當前頁標題
    print(await page.title())

    # 抓取新聞內(nèi)容  可以使用 xpath 表達式
    """
    # Pyppeteer 三種解析方式
    Page.querySelector()  # 選擇器
    Page.querySelectorAll()
    Page.xpath()  # xpath  表達式
    # 簡寫方式為:
    Page.J(), Page.JJ(), and Page.Jx()
    """
    element = await page.querySelector(".feed-infinite-wrapper > ul>li")  # 紙抓取一個
    print(element)
    # 獲取所有文本內(nèi)容  執(zhí)行 js
    content = await page.evaluate('(element) => element.textContent', element)
    print(content)

    # elements = await page.xpath('//div[@class="title-box"]/a')
    elements = await page.querySelectorAll(".title-box a")
    for item in elements:
        print(await item.getProperty('textContent'))
        # <pyppeteer.execution_context.JSHandle object at 0x000002220E7FE518>

        # 獲取文本
        title_str = await (await item.getProperty('textContent')).jsonValue()

        # 獲取鏈接
        title_link = await (await item.getProperty('href')).jsonValue()
        print(title_str)
        print(title_link)

    # 關閉瀏覽器
    await browser.close()


asyncio.get_event_loop().run_until_complete(main())




2.4 ascyncio 同步與異步執(zhí)行 Pyppeteer

參考:python爬蟲神器Pyppeteer入門及使用

以天天基金網(wǎng)中的開放式基金凈值數(shù)據(jù) 基金列表頁(下圖)前50支基金的近20個交易日的凈值數(shù)據(jù) 為例慎颗。


1.同步

基本思路是新建一個browser瀏覽器和一個頁面page乡恕,依次訪問每個基金的凈值數(shù)據(jù)頁面并爬取數(shù)據(jù)言询。核心代碼如下:

get_data()函數(shù) 用于凈值數(shù)據(jù)頁面解析和數(shù)據(jù)的轉(zhuǎn)化,
get_all_codes()函數(shù) 用于獲取全部開放式基金的基金代碼(共6000余個)几颜。
雖然程序也使用了async/await的結(jié)構(gòu)倍试,但是對多個基金的凈值數(shù)據(jù)獲取都是在callurl_and_getdata()函數(shù)中順序執(zhí)行的,之所以這樣寫是因為pyppeteer中的方法都是coroutine對象蛋哭,必須以這種形式構(gòu)建程序县习。

為了排除打開瀏覽器的耗時干擾,我們僅統(tǒng)計訪問頁面和數(shù)據(jù)抓取的用時谆趾,其結(jié)果為:12.08秒躁愿。

2. 異步

主要是把對fundlist的循環(huán)運行改裝成async的task對象


3. 獲取標簽的文本、值

# 獲取a標簽
title_elements = await page.Jx('//*[@class="result c-container "]/h3/a')
 
for item in title_elements:
    # 獲取文本:方法一沪蓬,通過getProperty方法獲取
    title_str1 = await (await item.getProperty('textContent')).jsonValue()
    print(title_str1)
 
    # 獲取文本:方法二彤钟,通過evaluate方法獲取
    title_str2 = await page.evaluate('item => item.textContent', item)
    print(title_str2)
 
    # 獲取鏈接:通過getProperty方法獲取
    title_link = await (await item.getProperty('href')).jsonValue()

常見的bug

1. pyppeteer.errors.NetworkError: Protocol error Network.getCookies: Target close

方法1:控制訪問指定url之后await page.goto(url),會遇到上面的錯誤跷叉,如果這時候使用了sleep之類的延時也會出現(xiàn)這個錯誤或者類似的time out逸雹。
這個問題是puppeteer的bug,但是對方已經(jīng)修復了云挟,而pyppeteer遲遲沒更新梆砸,就只能靠自己了,搜了很多人的文章园欣,例如:https://github.com/miyakogi/pyppeteer/issues/171 帖世,但是我按照這個并沒有成功。
也有人增加一個函數(shù)沸枯,但調(diào)用這個參數(shù)依然沒解決問題日矫。

async def scroll_page(page):
    cur_dist = 0
    height = await page.evaluate("() => document.body.scrollHeight")
    while True:
        if cur_dist < height:
            await page.evaluate("window.scrollBy(0, 500);")
            await asyncio.sleep(0.1)
            cur_dist += 500
        else:
            break

方法2:可以把python第三方庫websockets版本7.0改為6.0就可以了,親測可用绑榴。

pip uninstall websockets #卸載websockets
pip install websockets==6.0 #指定安裝6.0版本
2. chromium瀏覽器多開頁面卡死問題

方法:解決這個問題的方法就是瀏覽器初始化的時候添加’dumpio’:True哪轿。

3. 瀏覽器窗口很大,內(nèi)容顯示很小

上面的問題是需要設置瀏覽器顯示大小彭沼,默認就是無法正常顯示缔逛。可以看到頁面左側(cè)右側(cè)都是空白姓惑,網(wǎng)站內(nèi)容并沒有完整鋪滿chrome.


browser = await launch({'headless': False,'dumpio':True, 'autoClose':False,'args': ['--no-sandbox', '--window-size=1366,850']})
await page.setViewport({'width':1366,'height':768})

方法:通過上面設置Windows-size和Viewport大小來實現(xiàn)網(wǎng)頁完整顯示。

但是對于那種向下無限加載的長網(wǎng)頁這種情況如果瀏覽器是可見狀態(tài)會顯示不全按脚,針對這種情況的解決方法就是復制當前網(wǎng)頁新開一個標簽頁粘貼進去就正常了

4. Execution context was destroyed, most likely because of a navigation.

因為頁面發(fā)生了跳轉(zhuǎn)導致 page 丟失
方法:

// 在登錄頁跳轉(zhuǎn)之后添加
await page.waitForNavigation(); // 等待頁面跳轉(zhuǎn)
5. 登錄出現(xiàn) 滑塊 和cookies獲取
import asyncio
from pyppeteer import launch

async def main():
    browser = await launch({'headless': False, 'args': ['--disable-infobars', '--window-size=1920,1080']})
    page = await browser.newPage()
    await page.setViewport({'width': 1920, 'height': 1080})
    await page.goto('https://login.taobao.com/member/login.jhtml')
    await page.evaluate('''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }''')
    await page.waitForSelector('#J_QRCodeLogin > div.login-links > a.forget-pwd.J_Quick2Static', {'timeout': 3000})
    await page.click('#J_QRCodeLogin > div.login-links > a.forget-pwd.J_Quick2Static')
    await page.type('#TPL_username_1', '')  # 賬號
    await page.type('#TPL_password_1', '')  # 密碼
    await asyncio.sleep(5)
    slider = await page.Jeval('#nocaptcha', 'node => node.style')  # 是否有滑塊于毙,ps:試了好多次都沒出滑塊
    if slider:
        print('出現(xiàn)滑塊')
    await page.click('#J_SubmitStatic')
    await asyncio.sleep(5)
    cookie = await page.cookies()
    print(cookie)
    await browser.close()

asyncio.get_event_loop().run_until_complete(main())
6. pyppeteer.errors.TimeoutError: Navigation Timeout Exceeded: 30000 ms exceeded

由于點擊事件執(zhí)行很快已跳轉(zhuǎn)到新的頁面,導致程序運行到導航等待的時候辅搬,一直處于新的頁面等待觸發(fā)唯沮,直到30秒超時報錯脖旱,所以,正確的做法應該是把點擊和導航等待視為一個整體進行操作介蛉,以下為兩種正確的寫法萌庆,了解協(xié)程并發(fā)的朋友應該知道,在此不做詳細說明


圖片.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末币旧,一起剝皮案震驚了整個濱河市践险,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吹菱,老刑警劉巖巍虫,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鳍刷,居然都是意外死亡占遥,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進店門输瓜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瓦胎,“玉大人,你說我怎么就攤上這事尤揣∩Π。” “怎么了?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵芹缔,是天一觀的道長坯癣。 經(jīng)常有香客問我,道長最欠,這世上最難降的妖魔是什么示罗? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮芝硬,結(jié)果婚禮上蚜点,老公的妹妹穿的比我還像新娘。我一直安慰自己拌阴,他們只是感情好绍绘,可當我...
    茶點故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著迟赃,像睡著了一般陪拘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纤壁,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天左刽,我揣著相機與錄音,去河邊找鬼酌媒。 笑死欠痴,一個胖子當著我的面吹牛迄靠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播喇辽,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼掌挚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了菩咨?” 一聲冷哼從身側(cè)響起吠式,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎旦委,沒想到半個月后奇徒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡缨硝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年摩钙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片查辩。...
    茶點故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡胖笛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宜岛,到底是詐尸還是另有隱情长踊,我是刑警寧澤,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布萍倡,位于F島的核電站身弊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏列敲。R本人自食惡果不足惜阱佛,卻給世界環(huán)境...
    茶點故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望戴而。 院中可真熱鬧凑术,春花似錦、人聲如沸所意。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扶踊。三九已至泄鹏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秧耗,已是汗流浹背命满。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绣版,地道東北人胶台。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像杂抽,于是被迫代替她去往敵國和親诈唬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,700評論 2 345