參考:
- 爬蟲神器 Pyppeteer 介紹及爬取某商城實戰(zhàn)
- Pyppeteer:比selenium更高效的爬蟲界的新神器
- https://pyppeteer.github.io/pyppeteer/reference.html#pyppeteer.page.Page.querySelector
- 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壓縮包;
- 'linux': 'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/575458/chrome-linux.zip'
- 'mac': 'https://storage.googleapis.com/chromium-browser-snapshots/Mac/575458/chrome-mac.zip'
- 'win32': 'https://storage.googleapis.com/chromium-browser-snapshots/Win/575458/chrome-win32.zip'
- 'win64': 'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/575458/chrome-win32.zip'
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ā)的朋友應該知道,在此不做詳細說明