近年來,隨著大數(shù)據(jù)、人工智能、機器學(xué)習(xí)等技術(shù)的興起,Python 語言也越來越為人們所喜愛。但早在這些技術(shù)普及之前,Python 就一直擔(dān)負著一個重要的工作:自動化抓取網(wǎng)頁內(nèi)容。
舉個栗子,飛機票的價格每時每刻都在變化,甚至有些 App,你搜索的越多,價格就越貴。那不搜又不行啊,怎么樣才能知道確切的價格呢?
這就是 Python 大顯身手的時候啦~ 我們可以用Python寫一段程序,讓它自動幫你從網(wǎng)絡(luò)上獲取需要的數(shù)據(jù)——這就是所謂的“爬蟲程序”——它能從你指定的一個或多個網(wǎng)站上讀取并記錄數(shù)據(jù)(比如從某個航班數(shù)據(jù)網(wǎng)站上讀取指定日期和航線的機票信息),并根據(jù)數(shù)據(jù)進行一些自動操作,比如記錄下最低價,并通知用戶。
總結(jié)一下:
網(wǎng)頁抓取是一種通過自動化程序從網(wǎng)頁上獲取頁面內(nèi)容的計算機軟件技術(shù)。
我們這里說的“爬蟲”,正式名稱叫做“網(wǎng)頁抓取”。按照維基百科的說法,網(wǎng)頁抓取和大多數(shù)搜索引擎采用的網(wǎng)頁索引的爬蟲技術(shù)不同,網(wǎng)頁抓取更側(cè)重于將網(wǎng)絡(luò)上的非結(jié)構(gòu)化數(shù)據(jù)(常見的是html格式)轉(zhuǎn)換成為能在一個中央數(shù)據(jù)庫中儲存和分析的結(jié)構(gòu)化數(shù)據(jù)。“網(wǎng)頁抓取也涉及到網(wǎng)絡(luò)自動化,它利用計算機軟件模擬了人的瀏覽。網(wǎng)頁抓取的用途包括在線比價,聯(lián)系人數(shù)據(jù)抓取,氣象數(shù)據(jù)監(jiān)測,網(wǎng)頁變化檢測,以及各類科研和Web數(shù)據(jù)集成等。”
對于一般用戶,我們主要關(guān)注的就是網(wǎng)頁抓取。因此,以下提到的“爬蟲”一律指網(wǎng)頁抓取所用的自動化程序。
在今天的文章里,我們將帶你從最基礎(chǔ)的工具和庫入手,詳細了解一下一個爬蟲程序的常用結(jié)構(gòu),爬取網(wǎng)絡(luò)數(shù)據(jù)時應(yīng)該遵循哪些規(guī)則,存在哪些陷阱;最后,我們還將解答一些常見的問題,比如反追蹤,該做什么不該做什么,以及如何采用并行處理技術(shù)加速你的爬蟲等等。
文中介紹的每項內(nèi)容都會附上 Python 的實例代碼,方便你可以直接上手試玩。同時,我們還會介紹幾個非常有用的 Python 庫。
本教程主要分為5個部分:
1. 常用的代碼庫和工具
2. 從最簡單的例子開始
3. 小心陷阱
4. 一些規(guī)則
5. 利用并行加速爬蟲程序
在開始之前,請記住:務(wù)必善待服務(wù)器,我們并不希望把人家網(wǎng)站弄掛了,是吧。
1. 常用的代碼庫和工具
總的來說,網(wǎng)頁抓取并沒有一個一成不變的解決方案,畢竟通常每個網(wǎng)站的數(shù)據(jù)都因為網(wǎng)站自身結(jié)構(gòu)的不同而具有各不相同的特性。事實上,如果你希望從某個網(wǎng)站上抓取數(shù)據(jù),你需要對這個網(wǎng)站的結(jié)構(gòu)有足夠的理解,針對這個網(wǎng)站自己寫出對應(yīng)的腳本,或?qū)⒛硞€腳本設(shè)置到符合網(wǎng)站的結(jié)構(gòu),才可能成功。不過,你也無須重新發(fā)明輪子:已經(jīng)有很多不同的代碼庫,能幫你完成絕大多數(shù)底層的工作,它們多多少少都能幫上你一點忙。
1.1“檢查”選項
大部分時候,在實際爬取之前,你都需要熟悉網(wǎng)站的 HTML 代碼。你可以簡單地在你想查看的網(wǎng)頁元素上點擊右鍵,選擇“檢查”(Chrome)或者“查看元素”(火狐)
之后,系統(tǒng)就會彈出一個調(diào)試工具區(qū),高亮你剛選中的網(wǎng)頁元素。以 Medium 網(wǎng)站的作者信息頁為例:
在頁面上,這個被選中的元素包含了作者的姓名、標簽及個人介紹。這個元素的 class 是 hero hero--profile u-flexTOP。然后在這個元素里還有幾個子元素,其中顯示作者姓名的是 <h1> 標簽,它的 class 是 ui-h2 hero-title,顯示作者個人信息的 <p>,它的 class 是 ui-body hero-description。
你可以在 Mozilla 的開發(fā)者學(xué)院里找到更多關(guān)于 HTML 標記,以及 class 和 id 的區(qū)別等的詳細介紹。
1.2 Scrapy 庫
有個可獨立運行,開箱即用的數(shù)據(jù)抓取框架,名叫 Scrapy。除了抓取并輸出 HTML 外,這個庫還提供了許多額外的功能,比如按特定的格式輸出數(shù)據(jù),記錄日志等。同時,它的可定制性也很高,你可以在多個不同的進程上運行不同的爬蟲,禁用 cookie ¹,設(shè)置下載延時²等。
¹ 有些站點會用 cookie 來識別爬蟲。
² 數(shù)量過多的爬取請求會給網(wǎng)站帶來額外的負擔(dān),甚至可能會導(dǎo)致網(wǎng)站宕機。
但對我個人而言,這個庫有點太大太全面了:我只不過是想讀取站點每個頁面上的鏈接,按順序訪問每個鏈接并導(dǎo)出頁面上的數(shù)據(jù)而已。
1.3 BeautifulSoup 和 Requests 庫
BeautifulSoup 庫能讓你優(yōu)雅地處理 HTML 源碼。同時你還需要一個 Request 庫,用于從指定URL獲取內(nèi)容。不過,你需要自己處理其他的細節(jié)問題,包括錯誤捕獲與處理,導(dǎo)出數(shù)據(jù),并行處理等。
我個人特別喜歡 BeautifulSoup 因為它迫使我自己探索許多 Scrapy 可能已經(jīng)幫我處理好了的技術(shù)細節(jié),讓我從自己動手開始,從錯誤中學(xué)習(xí)。
2. 從最簡單的例子開始
從網(wǎng)站上抓取數(shù)據(jù)其實還是蠻直截了當(dāng)?shù)摹4蟛糠謺r候我們要關(guān)注的就是 HTML 源碼,找到你需要的內(nèi)容所對應(yīng)的 class 和 id。
下面是一個示例的網(wǎng)頁 HTML 代碼,假設(shè)我們要抓取到原價和折后價,那我們需要關(guān)注的就是 main_price 和 discounted_price 兩個元素。請注意,discounted_price 元素并不總是出現(xiàn)。
于是,我們從最基本的代碼開始:先導(dǎo)入需要用的 BeautifulSoup 和 Requests 庫,然后發(fā)起查詢請求( requests.get() ),接著處理 html 源碼,最后找到所有 class 為 main_price 的元素。
文字版源代碼詳見:gist.github.com/jkokatjuhha/02af3a28cf512ee8a3096273850fe029
有的時候,網(wǎng)頁的其他地方可能也有 main_price 的元素。為了避免導(dǎo)出無關(guān)的信息,我們可以先找到我們需要的 id='listings_prices',然后只在這個元素的子元素中查找 main_price 元素。
3. Pitfalls 小心陷阱
3.1 檢查 robots.txt
許多網(wǎng)站會將爬取規(guī)則和限制寫在 robots.txt 里,這個文件通常是在根域名下,你可以直接在域名后面加上 /robots.txt 來獲取這個文件。例如: www.example.com/robots.txt
robots.txt 里一般會規(guī)定哪些網(wǎng)頁不允許被自動抓取,或者限定某個頁面被機器人訪問的頻率。雖然大部分人確實都不理會這些,不過就算你真的不打算遵守這個規(guī)定,起碼也先看一看它的內(nèi)容,給點表面的尊重吧,哈哈。
google官方的幫助文檔中,對此的解釋是:“robots.txt 文件中的命令并不能強制抓取工具對您的網(wǎng)站采取具體的操作;對于訪問您網(wǎng)站的抓取工具來說,這些命令僅作為指令。Googlebot 和其他正規(guī)的網(wǎng)頁抓取工具都會遵循 robots.txt 文件中的命令,但其他抓取工具未必也會如此。”
3.2 小心 HTML 里的坑
HTML 標簽中可能包含 id 或 class,或二者兼有。 HTML id 是一個獨一無二的標記,而 HTML class 可能在多個元素中被重用。class 名或元素內(nèi)容可能會改變,而這種改變可能會讓你的代碼崩潰,或是返回錯誤的結(jié)果。
一般來說,有兩種辦法避免這種情況出現(xiàn):
● 采用 id 來獲取元素內(nèi)容,而不是 class,因為 id 一般來說不那么容易改變。
● 記得檢查返回值,如果返回了 None,那很可能有什么地方出了問題。
不過,因為有一些 class 可能并不總是出現(xiàn)(例如前面例子中的 discounted_price ),相關(guān)的元素并不一定在每個列表中都有。所以你需要統(tǒng)計某個元素在所有列表中出現(xiàn)的比例,比如計算返回 None 的次數(shù)。如果每次都返回 None,那也許你需要檢查代碼或者是 HTML 源碼,看看是不是這個元素在網(wǎng)站的 HTML 中就已經(jīng)改變了。
3.3 對 User agent 進行偽裝
每當(dāng)你訪問一個網(wǎng)站時,網(wǎng)站都會通過瀏覽器的 user agent 獲取到你的瀏覽器信息。有些網(wǎng)站如果沒收到 user agent 信息,就不會返回任何內(nèi)容,還有些網(wǎng)站會根據(jù)不同的 user agent,給不同的瀏覽器提供不同的內(nèi)容。
網(wǎng)站并不會阻止正常用戶的訪問,但如果你用同一個 user agent 發(fā)起每秒 200 次的請求,那看起來也太可疑了一點。怎么解決呢?你可以通過 user_agent 庫,產(chǎn)生(幾乎是)隨機的 user agent,也可以自定義一個特殊的 user agent。
文字版見:gist.github.com/jkokatjuhha/083c1b5e14e64b3b1ff734bb45b859be
3.4 給 request 請求設(shè)置一個超時時間
在默認狀態(tài),request 庫會無止境地等待某個請求返回對應(yīng)的響應(yīng)內(nèi)容。所以,給它設(shè)置一個參數(shù),等待超時就斷開連接,還是很有必要的。
文字版見:gist.github.com/jkokatjuhha/64cecefa0bf31c2b21111373c11fcc66
3.5 我是不是剛被屏蔽了?
如果你拿到的返回值經(jīng)常是 404(找不到頁面)、403(被禁止)、408(訪問超時),就應(yīng)該考慮你是不是被這個站點屏蔽了。
如果你對 HTTP 返回值不熟悉,看看我們之前解釋 HTTP 返回值的漫畫吧~
同樣,你也應(yīng)該在返回的響應(yīng)中對這類錯誤進行處理。
文字版見:gist.github.com/jkokatjuhha/a33467fae4c9f7fac64f067501b484ac
3.6 切換 IP 地址
就算你采用了隨機生成的 user agent,程序發(fā)起的所有連接都還用的是同一個 IP 地址:你的地址。雖然這通常并不會引起太多重視,畢竟很多圖書館、大學(xué)以及企業(yè)分別都只有少數(shù)幾個 IP 地址,由這些機構(gòu)內(nèi)的所有計算機共同使用。然而,如果在短時間內(nèi)從某一個 IP 地址發(fā)出了巨量的請求,還是會被服務(wù)器發(fā)現(xiàn)的。
這時候,你多年珍藏的科學(xué)上網(wǎng)工具就能大顯身手啦。
當(dāng)你采用了代理、VPN或者其他技術(shù)之后,對應(yīng)的網(wǎng)站會將你發(fā)起的請求識別為來自相應(yīng)的服務(wù)器,而不是你的。
3.7 蜜罐攻擊
蜜罐是引誘網(wǎng)頁爬蟲對其進行抓取或索引,從而進行偵測的一種技術(shù)手段。
比如,網(wǎng)頁上可能會存在一些“隱藏”鏈接,正常用戶在訪問的時候看不到這個鏈接,但爬蟲在處理 HTML 源代碼的時候會把它當(dāng)作正常鏈接進行處理。此類鏈接有可能用 css 樣式設(shè)置了 display:none,或者設(shè)置成和背景相同的顏色,甚至采用比如藏在頁面中的不可見位置等手段。一旦你的爬蟲訪問了這類鏈接,你的 IP 地址可能就被記錄日志,甚至服務(wù)器可能直接將你屏蔽。
另外一種蜜罐,是用超鏈接建立一串近乎無限深度的目錄樹,如果有人訪問了足夠深位置的內(nèi)容,那基本上可以確定這人不是個普通用戶。因此,在編寫爬蟲時,需要限制爬蟲取回的頁面數(shù)量,或控制遍歷深度。
4. 一些規(guī)則
-
在抓取之前,先看看目標網(wǎng)站是不是已經(jīng)提供了公開的 API。畢竟通過 API 能更好更快(也合法)地獲取所需的信息。比如社交網(wǎng)站 Twitter 就提供了許多不同的 API。
-
如果你需要抓取非常大量的數(shù)據(jù),你應(yīng)該考慮用一個數(shù)據(jù)庫把這些數(shù)據(jù)整理起來,方便之后進行分析和使用。這里有一篇用 Python 操作本地數(shù)據(jù)庫的教程。
-
務(wù)必保持禮貌。有時候,甚至建議你直接和對方網(wǎng)站的運維人員取得聯(lián)系,說不定他們能更方便快速地幫你解決你的機器人遇到的問題。
同時,再強調(diào)一遍,切記不要貪得無厭地發(fā)起太多請求,這會給目標網(wǎng)站帶來不必要的負載。
5. 利用并行加速爬蟲程序
如果你希望讓你的程序并行運行,一定要小心檢查自己的代碼,否則可能你會突然發(fā)現(xiàn)自己正在榨干目標服務(wù)器的資源。同時,請一定一定認真看完上一節(jié)的幾個規(guī)則。最后,你需要確保自己已經(jīng)理解了并行處理和并發(fā)處理,多線程和多進程之間的區(qū)別。
如果你在抓取過程中還要對巨量的頁面信息進行預(yù)處理,你會發(fā)現(xiàn)平均每秒鐘能發(fā)起的請求數(shù)其實是相當(dāng)少的。
在我個人的另一個抓取出租房價格的項目里,因為抓取時的預(yù)處理信息量實在太大,每秒能發(fā)起的請求數(shù)大約只有1個。處理 4000 個左右的鏈接,需要程序運行上大約一個小時。
為了并行發(fā)送請求,你可能需要采用一個叫做 multiprocessing 的 Python 庫。
假設(shè)我們有100個頁面要發(fā)起請求,我們希望給將任務(wù)量平均分給每個處理器。假設(shè)你有 N 個 CPU,你可以把所有的頁面分成 N 個部分,每個 CPU 處理一個部分。每個進程都將有自己的名字,目標函數(shù)以及需要處理的參數(shù)。每個進程的名字可以在之后被調(diào)用,以便將獲取到的信息寫入具體的文件中。
后來,我將 4000 個頁面分成 4 份,我的 4 個 CPU 各分到 1000 個,于是總的請求數(shù)增加到 4 個/秒,總的抓取時間就減少到了 17 分鐘左右。
文字版見:gist.github.com/jkokatjuhha/7927b27cf7a831c48e223b7c06fbd401
最后,祝大家爬得開心順利!記得多關(guān)注我們哦!!