當今大數據的時代,網絡爬蟲已經成為了獲取數據的一個重要手段。
隨著互聯網的發展,前端技術也在不斷變化,數據的加載方式也不再是單純的服務端渲染了。現在你可以看到很多網站的數據可能都是通過接口的形式傳輸的,或者即使不是接口那也是一些 JSON 的數據,然後經過 JavaScript 渲染得出來的。
這時,如果你還用 requests 來爬取內容,那就不管用了。因為 requests 爬取下來的只能是服務器端網頁的源碼,這和瀏覽器渲染以後的頁面內容是不一樣的。因為,真正的數據是經過 JavaScript 執行後,渲染出來的,數據來源可能是 Ajax,也可能是頁面裡的某些 Data,或者是一些 ifame 頁面等。不過,大多數情況下極有可能是 Ajax 接口獲取的。
所以,很多情況我們需要分析 Ajax請求,分析這些接口的調用方式,通過抓包工具或者瀏覽器的“開發者工具”,找到數據的請求鏈接,然後再用程序來模擬。但是,抓包分析流的方式,也存在一定的缺點。
一是:因為有些接口帶著加密參數,比如 token、sign 等等,模擬難度較大;
二是:抓包的方式只適合量小的情況。如果有一百、一千個,甚至五千、一萬個網站要處理時,該如何處理?還一個一個分析數據流?一個一個去抓包嗎?
基於以上的兩個嚴重的缺點,那有沒有一種簡單粗暴的方法,既不需要分析數據流,不需要抓包,又適合大批量的網站採集呢?這時 Puppeteer、Pyppeteer、Selenium、Splash 等自動化框架出現了。使用這些框架獲取HTML源碼,這樣我們爬取到的源代碼就是JavaScript 渲染以後的真正的網頁代碼,數據自然就好提取了。同時,也就繞過分析 Ajax 和一些 JavaScript 邏輯的過程。這種方式就做到了可見即可爬,難度也不大,同時適合大批量的採集。由於是模擬瀏覽器,一些法律方面的問題可以繞過。畢竟,爬蟲有風險啊! 哈哈....
Selenium,作為一款知名的Web自動化測試框架,支持大部分主流瀏覽器,提供了功能豐富的API接口,常常被我們用作爬蟲工具來使用。然而selenium的缺點也很明顯,比如速度太慢、對版本配置要求嚴苛,最麻煩是經常要更新對應的驅動。
由於Selenium流行已久,現在稍微有點反爬的網站都會對selenium和webdriver進行識別,網站只需要在前端js添加一下判斷腳本,很容易就可以判斷出是真人訪問還是webdriver。雖然也可以通過中間代理的方式進行js注入屏蔽webdriver檢測,但是webdriver對瀏覽器的模擬操作(輸入、點擊等等)都會留下webdriver的標記,同樣會被識別出來,要繞過這種檢測,只有重新編譯webdriver,麻煩自不必說,難度不是一般大。
由於Selenium具有這些嚴重的缺點。pyperteer成為了爬蟲界的又一新星。相比於selenium具有異步加載、速度快、具備有界面/無界面模式、偽裝性更強不易被識別為機器人,同時可以偽裝手機平板等終端;雖然支持的瀏覽器比較單一,但在安裝配置的便利性和運行效率方面都要遠勝selenium。
pyppeteer無疑為防爬牆撕開了一道大口子,針對selenium的淘寶、美團、文書網等網站,目前可通過該庫使用selenium的思路繼續突破,毫不費勁。
01.Pyppeteer簡介
Pyppeteer其實是Puppeteer的Python版本,下面簡單介紹下Pyppeteer的兩大特點,chromium瀏覽器和asyncio框架:
1).chromium
Chromium是一款獨立的瀏覽器,是Google為發展自家的瀏覽器Google Chrome而開啟的計劃,相當於Chrome的實驗版,Chromium的穩定性不如Chrome但是功能更加豐富,而且更新速度很快,通常每隔數小時就有新的開發版本發佈
2).asyncio
syncio是Python的一個異步協程庫,自3.4版本引入的標準庫,直接內置了對異步IO的支持,號稱是Python最有野心的庫,官網上有非常詳細的介紹:
02.安裝與使用
1).極簡安裝
使用pip install pyppeteer命令就能完成pyppeteer庫的安裝,至於chromium瀏覽器,只需要一條pyppeteer-install命令就會自動下載對應的最新版本chromium瀏覽器到pyppeteer的默認位置。
如果不運行pyppeteer-install命令,在第一次使用pyppeteer的時候也會自動下載並安裝chromium瀏覽器,效果是一樣的。總的來說,pyppeteer比起selenium省去了driver配置的環節。
當然,出於某種原因,也可能會出現chromium自動安裝無法順利完成的情況,這時可以考慮手動安裝:首先,從下列網址中找到自己系統的對應版本,下載chromium壓縮包;
然後,將壓縮包放到pyppeteer的指定目錄下解壓縮,windows系統的默認目錄。其他系統下的默認目錄可以參照下面這幅圖:
2).使用
安裝完後就來試試效果。一起來看下面這段代碼,在main函數中,先是建立一個瀏覽器對象,然後打開新的標籤頁,訪問百度主頁,對當前頁面截圖並保存為“example.png”,最後關閉瀏覽器。前文也提到過,pyppeteer是基於asyncio構建的,所以在使用的時候需要用到async/await結構
現在網站或系統的開發,逐漸趨於前後端分離,這樣數據的傳入就需要通過接口的方式進行傳輸。所以Ajax、動態渲染數據採集逐漸成為常態,Pyppeteer的使用會越來越多。基於方便、便與管理的考量,需要整理Pyppeteer的工具類,提供給團隊使用,下面是我在工作中整理的一個簡單的工具類,共大家參考,由於內容有點多,大家可以去我WX(crawler-small-gun),那裡有完整的工具類。
一部分工具類代碼:
import asyncio, tkinter, traceback
import base64, time, random
from pyppeteer import launch
from com.fy.utils.http.UserAgentUtils import UserAgentUtils
from com.fy.utils.hash.HashUtils import Hash_Utils
from com.fy.utils.file.FileUtils import File_Utils
class PyppeteerBrowser:
def __init__(self):
self.hash = Hash_Utils()
self.url = None
self.ua = UserAgentUtils()
#"""使用tkinter獲取屏幕大小""")--OK--
def screen_size(self):
tk = tkinter.Tk()
width = tk.winfo_screenwidth()
height = tk.winfo_screenheight()
tk.quit()
return width, height
#構造一個瀏覽器對象; --OK--; 如果需要每次初始化新的瀏覽器對象,則userDataDir路徑必須不同,否則,始終是在第一次初始化的瀏覽器對象上進行操作,且容易出異常;
async def getbrowser(self, headless=False, userDataDir=None):
'''
參數:
•ignoreHTTPSErrors(bool):是否忽略 HTTPS 錯誤。默認為 False
•headless(bool):是否在無頭模式下運行瀏覽器。默認為 True除非appMode或devtools選項True
•executablePath (str):運行 Chromium 或 Chrome 可執行文件的路徑,而不是默認捆綁的 Chromium。如果指定之後就不需要使用默認的 Chromium 了,可以指定為已有的 Chrome 或 Chromium。
•slowMo (int | float):通過傳入指定的時間,可以減緩 Pyppeteer 的一些模擬操作。 (按指定的毫秒數減慢 pyppeteer 操作。)
•args (List [str]):傳遞給瀏覽器進程的附加參數(標誌)。
•dumpio(bool):是否管道瀏覽器進程 stdout 和 stderr 進入process.stdout和process.stderr。默認為False。為 True時,可以解決chromium瀏覽器多開頁面卡死問題。
•userDataDir (str):用戶數據目錄的路徑。即用戶數據文件夾,即可以保留一些個性化配置和操作記錄。(比如登錄信息等;可以在以後打開時自動登錄;)
•env(dict):指定瀏覽器可見的環境變量。默認與 python 進程相同。
•devtools(bool):是否為每個選項卡自動打開 DevTools 面板。如果是此選項True,headless則將設置該選項 False。
•logLevel(int | str):用於打印日誌的日誌級別。默認值與根記錄器相同。
•autoClose(bool):腳本完成時自動關閉瀏覽器進程。默認為True。
•loop(asyncio.AbstractEventLoop):事件循環(實驗)。
•args:常用的有['--no-sandbox','--disable-gpu', '--disable-setuid-sandbox','--window-size=1440x900']
•dumpio: 不知道為什麼,如果不加 dumpio=True 有時會出現瀏覽器卡頓
•autoClose:默認就好,不過如果你需要保持瀏覽器狀態,可以不關閉,下次直接連接這個已存在的瀏覽器
ignoreDefaultArgs (bool): 不使用 Pyppeteer 的默認參數,如果使用了這個參數,那麼最好通過 args 參數來設定一些參數,否則可能會出現一些意想不到的問題。這個參數相對比較危險,慎用。
handleSIGINT (bool): 是否響應 SIGINT 信號,也就是可以使用 Ctrl + C 來終止瀏覽器程序,默認是 True。
handleSIGTERM (bool): 是否響應 SIGTERM 信號,一般是 kill 命令,默認是 True。
handleSIGHUP (bool): 是否響應 SIGHUP 信號,即掛起信號,比如終端退出操作,默認是 True。
launch_kwargs = {
# 控制是否為無頭模式
"headless": False,
# chrome啟動命令行參數
"args": [
# 瀏覽器代理 配合某些中間人代理使用
"--proxy-server=http://127.0.0.1:8008",
# 最大化窗口
"--start-maximized",
# 取消沙盒模式 沙盒模式下權限太小
"--no-sandbox",
# 不顯示信息欄 比如 chrome正在受到自動測試軟件的控制 ...
"--disable-infobars",
# log等級設置 在某些不是那麼完整的系統裡 如果使用默認的日誌等級 可能會出現一大堆的warning信息
"--log-level=3",
# 設置UA
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
],
# 用戶數據保存目錄 這個最好也自己指定一個目錄
# 如果不指定的話,chrome會自動新建一個臨時目錄使用,在瀏覽器退出的時候會自動刪除臨時目錄
# 在刪除的時候可能會刪除失敗(不知道為什麼會出現權限問題,我用的windows) 導致瀏覽器退出失敗
# 然後chrome進程就會一直沒有退出 CPU就會狂飆到99%
"userDataDir": "",
}
'''
print("構造瀏覽器對象開始...")
args = [ "--start-maximized", '--no-sandbox', "--disable-infobars" , "--log-level=3"]
parameters = {}
if userDataDir == None:
parameters = {'headless': headless, #是否打開瀏覽器;False:打開瀏覽器;True:進程中運行;
'args': args,
'dumpio': True #'dumpio': True:解決chromium瀏覽器多開頁面卡死問題。
}
else:
parameters = {'headless': headless, #是否打開瀏覽器;False:打開瀏覽器;True:進程中運行;
'args': args,
"userDataDir": userDataDir,
'dumpio': True #'dumpio': True:解決chromium瀏覽器多開頁面卡死問題。
}
#注意:同一個用戶目錄(userDataDir)不能被兩個chrome進程使用,如果你要多開,記得分別指定用戶目錄。否則會報編碼錯誤。
self.browser = await launch(parameters)
self.page = await self.browser.newPage()#在此瀏覽器上創建新頁面並返回其對象
width, height = self.screen_size()
# 設置網頁可視區域大小
await self.page.setViewport({
"width": width,
"height": height
})
# 是否啟用JS,enabled設為False,則無渲染效果
await self.page.setJavaScriptEnabled(enabled=True)
#設置請求頭userAgent
await self.page.setUserAgent(self.ua.getheaders())
await self.preventCheckWebdriver(self.page)
print("構造瀏覽器對象完畢....", self.page)
#獲取當前操作的界面
async def getPage(self):
return self.page
#獲取當前page對象的鏈接;
async def getCurUrl(self, page):
if page == None:
page = self.page
return page.url
#打開一個新的界面;)--OK--
async def getnewpage(self):
return await self.browser.newPage()
#獲取當前操作的界面重新加載
async def reload(self):
await self.page.reload()
#當前操作界面返回
async def goBack(self):
await self.page.goBack()
#獲取當前操作的界面的URL
async def getPageUrl(self):
await self.page.url()
#打開連接;--OK--
async def open(self, url, timeout=60):
try:
if url == None:
print("當前傳入的【url】不能為空,參數錯誤!!")
self.url = url
print("打開網頁:" + (url))
self.res = await self.page.goto(url, options={'timeout':int(timeout * 1000)})#打開連接;
await asyncio.sleep(1)#強行等待3秒
status = self.res.status
curUrl = self.page.url
await self.preventCheckWebdriver(self.page)
return status, curUrl
except:return 404, None
閱讀更多 採集小鋼炮 的文章