網路爬蟲之使用pyppeteer替代selenium完美繞過webdriver檢測
1引言
曾經使用模擬瀏覽器操作(selenium + webdriver)來寫爬蟲,但是稍微有點反爬的網站都會對selenium和webdriver進行識別,網站只需要在前端js新增一下判斷指令碼,很容易就可以判斷出是真人訪問還是webdriver。雖然也可以通過中間代理的方式進行js注入遮蔽webdriver檢測,但是webdriver對瀏覽器的模擬操作(輸入、點選等等)都會留下webdriver的標記,同樣會被識別出來,要繞過這種檢測,只有重新編譯webdriver,麻煩自不必說,難度不是一般大。
作為selenium+webdriver的優秀替代,pyppeteer就是一個很好的選擇。
2 手動安裝
通過pip使用豆瓣源加速安裝pyppeteer:
pip install -i https://pypi.douban.com/simple pypeteer
按照官方手冊,先來感受一下:
import asyncio from pyppeteer import launch async def main(): browser = await launch(headless=False) page = await browser.newPage() await page.goto('http://www.baidu.com/') await asyncio.sleep(100) await browser.close() asyncio.get_event_loop().run_until_complete(main())
pyppeteer第一次執行時,會自動下載chromium瀏覽器,時間可能會有些長。不過,我第一次執行時,直接報錯:
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)
嘗試多種方法無果,無奈只能手動下載,但手動下載的方法網上資料也幾乎沒有,讓我來做這個先行者吧。
上面程式碼執行雖然報錯,但是控制檯前兩行卻提供了很有用的資訊:
[W:pyppeteer.chromium_downloader] start chromium download. Download may take a few minutes.
可以看到,下載功能是由pyppeteer.chromium_downloader模組完成的,那麼我們進入這個模組檢視原始碼。
在這個模組原始碼中,我們可以看到downloadURLs、chromiumExecutable等變數,很明顯指的就是下載連結和chromium的可執行檔案路徑。我們重點關注一下可執行檔案路徑
chromiumExecutable: chromiumExecutable = { 'linux': DOWNLOADS_FOLDER / REVISION / 'chrome-linux' / 'chrome', 'mac': (DOWNLOADS_FOLDER / REVISION / 'chrome-mac' / 'Chromium.app' / 'Contents' / 'MacOS' / 'Chromium'), 'win32': DOWNLOADS_FOLDER / REVISION / 'chrome-win32' / 'chrome.exe', 'win64': DOWNLOADS_FOLDER / REVISION / 'chrome-win32' / 'chrome.exe', }
可見,無論在哪個平臺下,chromiumExecutable都是由是4個部分組成,其中 DOWNLOADS_FOLDER 和 REVISION是定義好的變數:
DOWNLOADS_FOLDER = Path(__pyppeteer_home__) / 'local-chromium'
進一步檢視可以發現:
__pyppeteer_home__ = os.environ.get('PYPPETEER_HOME', AppDirs('pyppeteer').user_data_dir) REVISION = os.environ.get('PYPPETEER_CHROMIUM_REVISION', __chromium_revision__)
所以,DOWNLOADS_FOLDER 和 REVISION都是讀取對應環境變數設定好的值,如果沒有設定,就使用預設值。我們來輸出一下,看看預設值:
import pyppeteer.chromium_downloader print('預設版本是:{}'.format(pyppeteer.__chromium_revision__)) print('可執行檔案預設路徑:{}'.format(pyppeteer.chromium_downloader.chromiumExecutable.get('win64'))) print('win64平臺下載連結為:{}'.format(pyppeteer.chromium_downloader.downloadURLs.get('win64')))
輸出結果如下:
預設版本是:575458 可執行檔案預設路徑:C:\Users\Administrator\AppData\Local\pyppeteer\pyppeteer\local-chromium\575458\chrome-win32\chrome.exe win64平臺下載連結為:https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/575458/chrome-win32.zip
在使用上面程式碼的時候,你可以將win64換成你的平臺就好了,有了上面的下載連結,這個時候就可以先開始下載著chromium瀏覽器(有些慢),然後繼續往下看。
對於版本,沒什麼好說的,是用預設的就好了。但是,對於chromium的可執行檔案路徑,預設是在C盤,對於有C盤潔癖的我,咋看咋不舒服,那就改了吧。從上面的分析中我們可以知道,C:\Users\Administrator\AppData\Local\pyppeteer\pyppeteer這一部分讀取的是環境變數或者預設值,所以,我們可以通過配置環境變數改這一部分(或者也可以直接改原始碼,讀取環境變數那一行,直接設為固定值),通過os.environ新增PYPPETEER_HOME這一變數值,例如我想把我的chromium放在D盤的Program Files資料夾下:import os os.environ['PYPPETEER_HOME'] = 'D:\Program Files' import pyppeteer.chromium_downloader print('預設版本是:{}'.format(pyppeteer.__chromium_revision__)) print('可執行檔案預設路徑:{}'.format(pyppeteer.chromium_downloader.chromiumExecutable.get('win64'))) print('win64平臺下載連結為:{}'.format(pyppeteer.chromium_downloader.downloadURLs.get('win64')))
輸出如下:
預設版本是:575458 可執行檔案預設路徑:D:\Program Files\local-chromium\575458\chrome-win32\chrome.exe win64平臺下載連結為:https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/575458/chrome-win32.zip
特別提醒:上面設定環境變數的那一行,必須在匯入pyppeteer這一行千米,否則設定無效。
上面這種方法你需要在每次使用pypeeteer之前通過這行程式碼設定一下,實在麻煩,所以,我還是更願意直接在windows系統裡面新增這個變數:
雖然我們把環境變數設定為D:\Program Files,但是層層資料夾之後,才到真正的可執行檔案chrome.exe,下載好的壓縮包解壓後,所有檔案都在名為chrome-win的資料夾中,所以,我們需要在D:\Program Files建立local-chromium\575458這兩個資料夾(575458是上面的版本號,記得修改為你的版本號),然後將解壓得到的chrome-win資料夾,重新命名為chrome-win32,然後直接拷貝進去就好,整個安裝過程就完成了。
再來試試最初(最上面)的程式碼,你會看到,已經可以成功執行。
我相信,大多數閱讀這篇博文的讀者都是用pyppeteer來開發爬蟲(別說維護世界和平,我不信),那麼接下來,重點來說說爬蟲中要用到的一些主要操作。
3 主要操作
3.1 開啟瀏覽器
開啟瀏覽器是通過pyppeteer.launcher.launch(options: dict = None, **kwargs) 方法,執行該函式後,會得到一個pyppeteer.browser.Browser例項,也就是說瀏覽器物件例項。launch方法是必須使用的方法,所以,詳細學學它的引數,你也直接閱讀官方文件,因為我也是直接翻譯的:
- ignoreHTTPSErrors (bool): 是否HTTPS錯誤,某人情況下為False.
- headless (bool): 是否以無頭模式(無介面模式)執行,預設為True,為True時是不會彈出可視介面的,所以,上面程式碼執行時設定headless=False。注意,下面還有個devtools引數,表示是否出現開啟除錯視窗,如果devtools設定為True,headless就算設定為False也會彈出可視介面。
- executablePath (str): Chromium或Chrome瀏覽器的可執行檔案路徑,如果設定,則使用設定的這個路徑,不使用預設設定.
- slowMo (int|float): 設定這個引數可以延遲pyppeteer的操作,單位是毫秒.
- args (List[str]): 要傳遞給瀏覽器程序的一些其他引數.
- ignoreDefaultArgs (bool): 如果有些引數你不想使用預設值,那麼,通過這個引數設定,不過,孩子,最好別用,有危險(電腦會爆炸).
- handleSIGINT (bool): 是否響應 SIGINT 訊號,是否允許通過快捷鍵Ctrl+C來終止瀏覽器程序,預設值為True,也就是允許.
- handleSIGTERM (bool): 是否響應 SIGTERM 訊號,也就是說kill命令關閉瀏覽器,,預設值為True,也就是允許.
- handleSIGHUP (bool): 是否響應 SIGHUP 訊號,即掛起訊號,預設值為True,也就是允許.
- dumpio (bool): 是要將瀏覽器程序的輸出傳遞給process.stdout 和 process.stderr 物件,預設為False不傳遞。
- userDataDir (str): 使用者資料檔案目錄.
- env (dict): 以字典的形式傳遞給瀏覽器環境變數.
- devtools (bool): 是否開啟除錯視窗,上面介紹headless引數是說過,預設值為False不開啟.
- logLevel (int|str): 日誌級別,預設和 root logger 物件的級別相同.
- autoClose (bool): 當所有操作都執行完後,是否自動關閉瀏覽器,預設True,自動關閉.
- loop (asyncio.AbstractEventLoop): 時間迴圈。
- appMode (bool): Deprecated.
開啟瀏覽器操作簡單,看引數就行,不多介紹。
3.2 調整視窗大小
如果你運行了上面的程式碼,你會發現,開啟的頁面只在視窗左上角一小塊顯示,看著很彆扭,這是因為pyppeteer預設視窗大小是800*600,所以,調整一下吧。調整視窗大小通過方法實現,看下面程式碼,最大化視窗:
import asyncio from pyppeteer import launch def screen_size(): """使用tkinter獲取螢幕大小""" import tkinter tk = tkinter.Tk() width = tk.winfo_screenwidth() height = tk.winfo_screenheight() tk.quit() return width, height async def main(): browser = await launch(headless=False) page = await browser.newPage() width, height = screen_size() await page.setViewport({ # 最大化視窗 "width": width, "height": height }) await page.goto('http://www.baidu.com/') await asyncio.sleep(100) await browser.close() asyncio.get_event_loop().run_until_complete(main())
3.3 設定userAgent
常規操作,不多說,上程式碼:
import asyncio from pyppeteer import launch async def main(): browser = await launch(headless=False) page = await browser.newPage() # 設定請求頭userAgent await page.setUserAgent('Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Mobile Safari/537.36') await page.goto('http://www.baidu.com/') await asyncio.sleep(100) await browser.close() asyncio.get_event_loop().run_until_complete(main())
3.4 執行js指令碼
有時候,為了達成某些目的(例如遮蔽網站原有js),我們不可避免得需要執行一些js指令碼。執行js指令碼通過evaluate方法。如下所示,我們通過js來修改window.navigator.webdriver屬性的值,由此繞過網站對webdriver的檢測:
import asyncio from pyppeteer import launch async def main(): js1 = '''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }''' js2 = '''() => { alert ( window.navigator.webdriver ) }''' browser = await launch({'headless':False, 'args':['--no-sandbox'],}) page = await browser.newPage() await page.goto('https://h5.ele.me/login/') await page.evaluate(js1) await page.evaluate(js2) asyncio.get_event_loop().run_until_complete(main())
在上面程式碼中,通過page.evalute方法執行了兩段js指令碼,第一段指令碼將webdriver的屬性值設為false,第二段程式碼在此讀取 webdriver屬性值,輸出為false。
3.5 模擬操作
pyppeteer提供了Keyboard和Mouse兩個類來實現模擬操作,前者是用來實現鍵盤模擬,後者實現滑鼠模擬(還有其他觸屏之類的就不說了)。
主要來說說輸入和點選:
import os os.environ['PYPPETEER_HOME'] = 'D:\Program Files' import asyncio from pyppeteer import launch async def main(): browser = await launch(headless=False, args=['--disable-infobars']) page = await browser.newPage() await page.goto('https://h5.ele.me/login/') await page.type('form section input', '12345678999') # 模擬鍵盤輸入手機號 await page.click('form section button') # 模擬滑鼠點選獲取驗證碼 await asyncio.sleep(200) await browser.close() asyncio.get_event_loop().run_until_complete(main())
上面的模擬操作中,無論是模擬鍵盤輸入還是滑鼠點選定位都是通過css選擇器,似乎pyppeteer的type和click直接模擬操作定位都只能通過css選擇器(或者是我在官方文件中沒找到方法),當然,要間接通過xpath先定位,然後再模擬操作也是可以的。下一小節中模擬登陸外賣平臺就是用這種方法,不過,這種方法要麻煩一些,不推薦。
3.6 某電商平臺模擬登陸
我曾經用selenium + chrome 實現了模擬登陸這個電商平臺,但是實在是有些麻煩,繞過對webdriver的檢測不難,但是,通過webdriver對瀏覽器的每一步操作都會留下特殊的痕跡,會被平臺識別,這個必須通過重新編譯chrome的webdriver才能實現,麻煩得讓人想哭。不說了,都是淚,下面直接上用pyppeteer實現的程式碼:
import os os.environ['PYPPETEER_HOME'] = 'D:\Program Files' import asyncio from pyppeteer import launch def screen_size(): """使用tkinter獲取螢幕大小""" import tkinter tk = tkinter.Tk() width = tk.winfo_screenwidth() height = tk.winfo_screenheight() tk.quit() return width, height async def main(): js1 = '''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }''' js2 = '''() => { alert ( window.navigator.webdriver ) }''' browser = await launch({'headless':False, 'args':['--no-sandbox'],}) page = await browser.newPage() width, height = screen_size() await page.setViewport({ # 最大化視窗 "width": width, "height": height }) await page.goto('https://h5.ele.me/login/') await page.evaluate(js1) await page.evaluate(js2) input_sjh = await page.xpath('//form/section[1]/input[1]') click_yzm = await page.xpath('//form/section[1]/button[1]') input_yzm = await page.xpath('//form/section[2]/input[1]') but = await page.xpath('//form/section[2]/input[1]') print(input_sjh) await input_sjh[0].type('*****手機號********') await click_yzm[0].click() ya = input('請輸入驗證碼:') await input_yzm[0].type(str(ya)) await but[0].click() await asyncio.sleep(3) await page.goto('https://www.ele.me/home/') await asyncio.sleep(100) await browser.close() asyncio.get_event_loop().run_until_complete(main())
登入時,由於等待時間過長(我猜的)導致出現以下錯誤:
pyppeteer.errors.NetworkError: Protocol Error (Runtime.callFunctionOn): Session closed. Most likely the page has been closed.
在github上找到了解決方法,似乎只能改原始碼,找到pyppeteer包下的connection.py模組,在其43行和44行改為下面這樣:
self._ws = websockets.client.connect( # self._url, max_size=None, loop=self._loop) self._url, max_size=None, loop=self._loop, ping_interval=None, ping_timeout=None)
再次執行就沒問題了。可以成功繞過官方對webdriver的檢測,登入成功,諸位可以自己嘗試一下。
4 總結
當使用selenium+webdriver寫爬蟲被檢測到時,pyppeteer是你得不二選擇,幾乎所有能在人工操作瀏覽器進行的操作通過pyppeteer都能實現,且能完美避開官方對webdriver的檢測。pyppeteer涉及的使用方法還很多,本文只介紹了常用方法的很小很小一部分,需要一說的是,pyppeteer的中文資料真的很少,多看看官方文件吧。
參考:
https://mp.weixin.qq.com/s?__biz=MzIzNzA4NDk3Nw==&mid=2457737358&idx=1&sn=fb88904cac67300130cabbc72bc4a650&chksm=ff44b0d0c83339c6496cabf8e09e8a9e0316df1032ef7523ba6ab7f4f6a4bea1cd4c02eb7d7b&mpshare=1&scene=1&srcid=&key=076402fec4624ccbe758d20c86fbbfabff1a1de62190662a69bb6decd76681b07d9b48c371a99b1237702740a0181d36410e1af661dad8732cc0c65b9f772fb3f988ce1840a07037579a9d134d7ad57d&ascene=1&uin=MjU5MjA4OTg0NA%3D%3D&devicetype=Windows+10&version=62060739&lang=zh_CN&pass_ticket=gFs%2B1sVN%2FxqIhOn1175cxFLsbS1MTzKJWqgpBIPWD9ilQcFn2fWqjXZ1AlHSt0fh https://miyakogi.github.io/pyppeteer/reference.ht