用Python抓網頁?你想問的都幫答好了,你還有不懂的嗎?

qq1622479435發表於2018-08-05
用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

近年來,隨著大資料、人工智慧、機器學習等技術的興起,Python 語言也越來越為人們所喜愛。但早在這些技術普及之前,Python 就一直擔負著一個重要的工作:自動化抓取網頁內容。

舉個例子,飛機票的價格每時每刻都在變化,甚至有些 app,你搜尋的越多,價格就越貴。那不搜又不行啊,怎麼樣才能知道確切的價格呢?

這就是 Python 大顯身手的時候啦~ 我們可以用Python寫一段程式,讓它自動幫你從網路上獲取需要的資料——這就是所謂的“爬蟲程式”——它能從你指定的一個或多個網站上讀取並記錄資料(比如從某個航班資料網站上讀取指定日期和航線的機票資訊),並根據資料進行一些自動操作,比如記錄下最低價,並通知使用者。

總結一下:

網頁抓取是一種通過自動化程式從網頁上獲取頁面內容的計算機軟體技術。

我們這裡說的“爬蟲”,正式名稱叫做“網頁抓取”。按照維基百科的說法,網頁抓取和大多數搜尋引擎採用的網頁索引的爬蟲技術不同,網頁抓取更側重於將網路上的非結構化資料(常見的是HTML格式)轉換成為能在一箇中央資料庫中儲存和分析的結構化資料。“網頁抓取也涉及到網路自動化,它利用計算機軟體模擬了人的瀏覽。網頁抓取的用途包括線上比價,聯絡人資料抓取,氣象資料監測,網頁變化檢測,以及各類科研和Web資料整合等。”

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

對於一般使用者,我們主要關注的就是網頁抓取。因此,以下提到的“爬蟲”一律指網頁抓取所用的自動化程式。

在今天的文章裡,我們將帶你從最基礎的工具和庫入手,詳細瞭解一下一個爬蟲程式的常用結構,爬取網路資料時應該遵循哪些規則,存在哪些陷阱;最後,我們還將解答一些常見的問題,比如反追蹤,該做什麼不該做什麼,以及如何採用並行處理技術加速你的爬蟲等等。

文中介紹的每項內容都會附上 Python 的例項程式碼,方便你可以直接上手試玩。同時,我們還會介紹幾個非常有用的 Python 庫。

本教程主要分為5個部分:

1. 常用的程式碼庫和工具

2. 從最簡單的例子開始

3. 小心陷阱

4. 一些規則

5. 利用並行加速爬蟲程式

在開始之前,請記住:務必善待伺服器,我們並不希望把人家網站弄掛了,是吧。

1. 常用的程式碼庫和工具

總的來說,網頁抓取並沒有一個一成不變的解決方案,畢竟通常每個網站的資料都因為網站自身結構的不同而具有各不相同的特性。事實上,如果你希望從某個網站上抓取資料,你需要對這個網站的結構有足夠的理解,針對這個網站自己寫出對應的指令碼,或將某個指令碼設定到符合網站的結構,才可能成功。不過,你也無須重新發明輪子:已經有很多不同的程式碼庫,能幫你完成絕大多數底層的工作,它們多多少少都能幫上你一點忙。

1.1“檢查”選項

大部分時候,在實際爬取之前,你都需要熟悉網站的 HTML 程式碼。你可以簡單地在你想檢視的網頁元素上點選右鍵,選擇“檢查”(Chrome)或者“檢視元素”(火狐)

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

之後,系統就會彈出一個除錯工具區,高亮你剛選中的網頁元素。以 Medium 網站的作者資訊頁為例:

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

在頁面上,這個被選中的元素包含了作者的姓名、標籤及個人介紹。這個元素的 class 是 hero hero–profile u-flexTOP。然後在這個元素裡還有幾個子元素,其中顯示作者姓名的是 <h1> 標籤,它的 class 是 ui-h2 hero-title,顯示作者個人資訊的 <p>,它的 class 是 ui-body hero-description。

你可以在 Mozilla 的開發者學院裡找到更多關於 HTML 標記,以及 class 和 id 的區別等的詳細介紹。

1.2 Scrapy 庫

有個可獨立執行,開箱即用的資料抓取框架,名叫 Scrapy。除了抓取並輸出 HTML 外,這個庫還提供了許多額外的功能,比如按特定的格式輸出資料,記錄日誌等。同時,它的可定製性也很高,你可以在多個不同的程式上執行不同的爬蟲,禁用 cookie ¹,設定下載延時²等。

¹ 有些站點會用 cookie 來識別爬蟲。

² 數量過多的爬取請求會給網站帶來額外的負擔,甚至可能會導致網站當機。

但對我個人而言,這個庫有點太大太全面了:我只不過是想讀取站點每個頁面上的連結,按順序訪問每個連結並匯出頁面上的資料而已。

1.3 BeautifulSoup 和 Requests 庫

BeautifulSoup 庫能讓你優雅地處理 HTML 原始碼。同時你還需要一個 Request 庫,用於從指定URL獲取內容。不過,你需要自己處理其他的細節問題,包括錯誤捕獲與處理,匯出資料,並行處理等。

我個人特別喜歡 BeautifulSoup 因為它迫使我自己探索許多 Scrapy 可能已經幫我處理好了的技術細節,讓我從自己動手開始,從錯誤中學習。

2. 從最簡單的例子開始

從網站上抓取資料其實還是蠻直截了當的。大部分時候我們要關注的就是 HTML 原始碼,找到你需要的內容所對應的 class 和 id。

下面是一個示例的網頁 HTML 程式碼,假設我們要抓取到原價和折後價,那我們需要關注的就是 main_price 和 discounted_price 兩個元素。請注意,discounted_price 元素並不總是出現。

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

於是,我們從最基本的程式碼開始:先匯入需要用的 BeautifulSoup 和 Requests 庫,然後發起查詢請求( requests.get() ),接著處理 html 原始碼,最後找到所有 class 為 main_price 的元素。

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

有的時候,網頁的其他地方可能也有 main_price 的元素。為了避免匯出無關的資訊,我們可以先找到我們需要的 id=`listings_prices`,然後只在這個元素的子元素中查詢 main_price 元素。

3. Pitfalls 小心陷阱

3.1 檢查 robots.txt

許多網站會將爬取規則和限制寫在 robots.txt 裡,這個檔案通常是在根域名下,你可以直接在域名後面加上 /robots.txt 來獲取這個檔案。例如: http://www.example.com/robots.txt

robots.txt 裡一般會規定哪些網頁不允許被自動抓取,或者限定某個頁面被機器人訪問的頻率。雖然大部分人確實都不理會這些,不過就算你真的不打算遵守這個規定,起碼也先看一看它的內容,給點表面的尊重吧,哈哈。

Google官方的幫助文件中,對此的解釋是:“robots.txt 檔案中的命令並不能強制抓取工具對您的網站採取具體的操作;對於訪問您網站的抓取工具來說,這些命令僅作為指令。Googlebot 和其他正規的網頁抓取工具都會遵循 robots.txt 檔案中的命令,但其他抓取工具未必也會如此。”

3.2 小心 HTML 裡的坑

HTML 標籤中可能包含 id 或 class,或二者兼有。 HTML id 是一個獨一無二的標記,而 HTML class 可能在多個元素中被重用。class 名或元素內容可能會改變,而這種改變可能會讓你的程式碼崩潰,或是返回錯誤的結果。

一般來說,有兩種辦法避免這種情況出現:

● 採用 id 來獲取元素內容,而不是 class,因為 id 一般來說不那麼容易改變。

● 記得檢查返回值,如果返回了 None,那很可能有什麼地方出了問題。

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

不過,因為有一些 class 可能並不總是出現(例如前面例子中的 discounted_price ),相關的元素並不一定在每個列表中都有。所以你需要統計某個元素在所有列表中出現的比例,比如計算返回 None 的次數。如果每次都返回 None,那也許你需要檢查程式碼或者是 HTML 原始碼,看看是不是這個元素在網站的 HTML 中就已經改變了。

3.3 對 User agent 進行偽裝

每當你訪問一個網站時,網站都會通過瀏覽器的 user agent 獲取到你的瀏覽器資訊。有些網站如果沒收到 user agent 資訊,就不會返回任何內容,還有些網站會根據不同的 user agent,給不同的瀏覽器提供不同的內容。

網站並不會阻止正常使用者的訪問,但如果你用同一個 user agent 發起每秒 200 次的請求,那看起來也太可疑了一點。怎麼解決呢?你可以通過 user_agent 庫,產生(幾乎是)隨機的 user agent,也可以自定義一個特殊的 user agent。

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

3.4 給 request 請求設定一個超時時間

在預設狀態,request 庫會無止境地等待某個請求返回對應的響應內容。所以,給它設定一個引數,等待超時就斷開連線,還是很有必要的。

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

文字版見:https://gist.github.com/jkokatjuhha/64cecefa0bf31c2b21111373c11fcc66

3.5 我是不是剛被遮蔽了?

如果你拿到的返回值經常是 404(找不到頁面)、403(被禁止)、408(訪問超時),就應該考慮你是不是被這個站點遮蔽了。

如果你對 HTTP 返回值不熟悉,看看我們之前解釋 HTTP 返回值的漫畫吧~

同樣,你也應該在返回的響應中對這類錯誤進行處理。

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

文字版見:https://gist.github.com/jkokatjuhha/a33467fae4c9f7fac64f067501b484ac

3.6 切換 IP 地址

就算你採用了隨機生成的 user agent,程式發起的所有連線都還用的是同一個 IP 地址:你的地址。雖然這通常並不會引起太多重視,畢竟很多圖書館、大學以及企業分別都只有少數幾個 IP 地址,由這些機構內的所有計算機共同使用。然而,如果在短時間內從某一個 IP 地址發出了巨量的請求,還是會被伺服器發現的。

這時候,你多年珍藏的科學上網工具就能大顯身手啦。

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

當你採用了代理、VPN或者其他技術之後,對應的網站會將你發起的請求識別為來自相應的伺服器,而不是你的。

3.7 蜜罐攻擊

蜜罐是引誘網頁爬蟲對其進行抓取或索引,從而進行偵測的一種技術手段。

比如,網頁上可能會存在一些“隱藏”連結,正常使用者在訪問的時候看不到這個連結,但爬蟲在處理 HTML 原始碼的時候會把它當作正常連結進行處理。此類連結有可能用 CSS 樣式設定了 display:none,或者設定成和背景相同的顏色,甚至採用比如藏在頁面中的不可見位置等手段。一旦你的爬蟲訪問了這類連結,你的 IP 地址可能就被記錄日誌,甚至伺服器可能直接將你遮蔽。

另外一種蜜罐,是用超連結建立一串近乎無限深度的目錄樹,如果有人訪問了足夠深位置的內容,那基本上可以確定這人不是個普通使用者。因此,在編寫爬蟲時,需要限制爬蟲取回的頁面數量,或控制遍歷深度。

4. 一些規則

  • 在抓取之前,先看看目標網站是不是已經提供了公開的 API。畢竟通過 API 能更好更快(也合法)地獲取所需的資訊。比如社交網站 Twitter 就提供了許多不同的 API。如果你需要抓取非常大量的資料,你應該考慮用一個資料庫把這些資料整理起來,方便之後進行分析和使用。這裡有一篇用 Python 操作本地資料庫的教程。務必保持禮貌。有時候,甚至建議你直接和對方網站的運維人員取得聯絡,說不定他們能更方便快速地幫你解決你的機器人遇到的問題。

同時,再強調一遍,切記不要貪得無厭地發起太多請求,這會給目標網站帶來不必要的負載。

5. 利用並行加速爬蟲程式

如果你希望讓你的程式並行執行,一定要小心檢查自己的程式碼,否則可能你會突然發現自己正在榨乾目標伺服器的資源。同時,請一定一定認真看完上一節的幾個規則。最後,你需要確保自己已經理解了並行處理和併發處理,多執行緒和多程式之間的區別。

如果你在抓取過程中還要對巨量的頁面資訊進行預處理,你會發現平均每秒鐘能發起的請求數其實是相當少的。

在我個人的另一個抓取出租房價格的專案裡,因為抓取時的預處理資訊量實在太大,每秒能發起的請求數大約只有1個。處理 4000 個左右的連結,需要程式執行上大約一個小時。

為了並行傳送請求,你可能需要採用一個叫做 multiprocessing 的 Python 庫。

假設我們有100個頁面要發起請求,我們希望給將任務量平均分給每個處理器。假設你有 N 個 CPU,你可以把所有的頁面分成 N 個部分,每個 CPU 處理一個部分。每個程式都將有自己的名字,目標函式以及需要處理的引數。每個程式的名字可以在之後被呼叫,以便將獲取到的資訊寫入具體的檔案中。

後來,我將 4000 個頁面分成 4 份,我的 4 個 CPU 各分到 1000 個,於是總的請求數增加到 4 個/秒,總的抓取時間就減少到了 17 分鐘左右。

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

最後,祝大家爬得開心順利!記得多關注我哦!!

最後的文末知識點摘要:Python解惑之:整數比較

在 Python 中一切都是物件,毫無例外整數也是物件,物件之間比較是否相等可以用 ==,也可以用 is。 ==和 is操作的區別是:

  • is比較的是兩個物件的id值是否相等,也就是比較倆物件是否為同一個例項物件,是否指向同一個記憶體地址。
  • ==比較的是兩個物件的內容是否相等,預設會呼叫物件的 __eq__()方法。

清楚 is和 ==的區別之後,對此也許你有可能會遇到下面的這些困惑,於是就有了這樣一篇文章,試圖把Python中一些隱晦的東西趴出來,希望對你有一定的幫助。我們先來看兩段程式碼:

片段一:

>>> a = 256

>>> b = 256

>>> a == b

True

>>>

片段二:

>>> a = 256

>>> b = 256

>>> a is b

True

>>>

在互動式命令列執行上面兩段程式碼,程式碼片段一中的 a==b返回 True很好理解,因為兩個物件的值都是256,對於片段二, a is b也返回True,這說明a和b是指向同一個物件的,可以檢查一下他們的id值是否相等:

>>> id(a)

8213296

>>> id(b)

8213296

>>>

結果證明他倆的確是同一個物件,指向的是同一個記憶體地址。那是不是所有的整數物件只要兩個物件的值(內容)相等,它們就是同一個例項物件呢?換句話說,對於整數物件只要 ==返回 True, is操作也會返回 True嗎?帶著這個問題來看下面這兩段程式碼:

片段一:

>>> a = 257

>>> b = 257

>>> a == b

True

>>>

片段二:

>>> a = 257

>>> b = 257

>>> a is b

False

>>>

對於257, a is b返回的竟然是False,結果可能在你的意料之中,也有可能出乎你的意料,但不管怎麼,我們還是要刨根問底,找出問題的真相。

解惑一

出於對效能的考慮,Python內部做了很多的優化工作,對於整數物件,Python把一些頻繁使用的整數物件快取起來,儲存到一個叫 small_ints的連結串列中,在Python的整個生命週期內,任何需要引用這些整數物件的地方,都不再重新建立新的物件,而是直接引用快取中的物件。Python把這些可能頻繁使用的整數物件規定在範圍[-5, 256]之間的小物件放在 small_ints中,但凡是需要用些小整數時,就從這裡面取,不再去臨時建立新的物件。因為257不再小整數範圍內,因此儘管a和b的值是一樣,但是他們在Python內部卻是以兩個獨立的物件存在的,各自為政,互不干涉。


弄明白第一個問題後,我們繼續在Python互動式命令列中寫一個函式,再來看下面這段程式碼:

片段一:

>>> c = 257

>>> def foo():

… a = 257

… b = 257

… print a is b

… print a is c

>>> foo()

True

False

呃,什麼情況,是的,你沒看錯,片段一中的這段程式碼 a、b 值都是257的情況下,出現了 a is b返回 True,而 a is c 返回的 False,a、b、c的值都為257,為什麼會出現不同的結果呢?這對於剛剛好不容易建立起來的認知就被徹底否決了嗎,那這段程式碼中究竟發生了什麼?難道解惑一中的結論是錯誤的嗎?

解惑二

A Python program is constructed from code blocks. A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition. Each command typed interactively is a block. A script file (a file given as standard input to the interpreter or specified as a command line argument to the interpreter) is a code block. A script command (a command specified on the interpreter command line with the ‘-c‘ option) is a code block. structure-of-a-program

為了弄清楚這個問題,我們有必要先理解程式程式碼塊的概念。Python程式由程式碼塊構成,程式碼塊作為程式的一個最小基本單位來執行。一個模組檔案、一個函式體、一個類、互動式命令中的單行程式碼都叫做一個程式碼塊。在上面這段程式碼中,由兩個程式碼塊構成, c = 257作為一個程式碼塊,函式 foo作為另外一個程式碼塊。Python內部為了將效能進一步的提高,凡是在一個程式碼塊中建立的整數物件,如果存在一個值與其相同的物件於該程式碼塊中了,那麼就直接引用,否則建立一個新的物件出來。Python出於對效能的考慮,但凡是不可變物件,在同一個程式碼塊中的物件,只有是值相同的物件,就不會重複建立,而是直接引用已經存在的物件。因此,不僅是整數物件,還有字串物件也遵循同樣的原則。所以 a is b就理所當然的返回 True了,而 c和 a不在同一個程式碼塊中,因此在Python內部建立了兩個值都是257的物件。為了驗證剛剛的結論,我們可以借用 dis模組從位元組碼的角度來看看這段程式碼。

用 Python 抓網頁?你想問的都幫答好了,你還有不懂的嗎?

可以看出兩個257都是從常量池的同一個位置 co_consts[1]獲取的。

總結

一番長篇大論之後,得出兩點結論:1、小整數物件[-5,256]是全域性直譯器範圍內被重複使用,永遠不會被GC回收。2、同一個程式碼塊中的不可變物件,只要值是相等的就不會重複建立新的物件。似乎這些知識點對日常的工作一點忙也幫不上,因為你根本不會用 is來比較兩個整數物件的值是否相等。那為什麼還要拿出來討論呢?嗯,程式設計師學知識,不應該淺嘗輒止,要充分發揮死磕到底的精神。

本篇文章分享就到此結束,部分素材來源網路與自己整理,如有侵權,請聯絡刪除。希望本次的知識點分享對你有所幫助。如果你在學習Python的過程中遇見了很多疑問和難題可以加q u n 二二七  四三五  四五零


相關文章