1. 程式人生 > >第一章 網路爬蟲簡介

第一章 網路爬蟲簡介

本章將介紹如下主題:

  1. 網路爬蟲領域介紹
  2. 爬蟲的合法與非法性
  3. 對目標網站進行背景調研
  4. 逐步完善一個高階網路爬蟲

1.1 網路爬蟲的使用場景

        網路爬蟲(又被稱為網頁蜘蛛,網路機器人,在FOAF社群中間,更經常的稱為網頁追逐者),是一種按照一定的規則,自動地抓取全球資訊網資訊的程式或者指令碼。另外一些不常使用的名字還有螞蟻、自動索引、模擬程式或者蠕蟲

        爬蟲從一個或若干初始網頁的URL開始,獲得初始網頁上的URL,在抓取網頁的過程中,不斷從當前頁面上抽取新的URL放入佇列,直到滿足系統的一定停止條件。        理想狀態下,網路爬蟲並不是必需品,每個網站都應該提供API,以結構化的的格式共享它們的資料。,但是它們通常會限制可以抓取的資料,以及訪問這些資料的頻率。另外對於網站的開發者而言,維護前端介面比維護後端API介面優先順序更高。總之,我們不能僅僅依賴於API去訪間我們所需的線上資料,而是應該學習些網路爬蟲技術的相關知識。

1.2 網路爬蟲是否合法

        爬蟲作為一種計算機技術就決定了它的中立性,因此爬蟲本身在法律上並不被禁止,但是利用爬蟲技術獲取資料這一行為是具有違法甚至是犯罪的風險的。所謂具體問題具體分析,正如水果刀本身在法律上並不被禁止使用,但是用來捅人,就不被法律所容忍了。

        或者我們可以這麼理解:爬蟲是用來批量獲得網頁上的公開資訊的,也就是前端顯示的資料資訊。因此,既然本身就是公開資訊,其實就像瀏覽器一樣,瀏覽器解析並顯示了頁面內容,爬蟲也是一樣,只不過爬蟲會批量下載而已,所以是合法的。不合法的情況就是配合爬蟲,利用黑客技術攻擊網站後臺,竊取後臺資料(比如使用者資料等)。

        舉個例子:像谷歌這樣的搜尋引擎爬蟲,每隔幾天對全網的網頁掃一遍,供大家查閱,各個被掃的網站大都很開心。這種就被定義為“善意爬蟲”。但是像搶票軟體這樣的爬蟲,對著 12306 每秒鐘恨不得擼幾萬次,鐵總並不覺得很開心,這種就被定義為“惡意爬蟲”。

爬蟲所帶來風險主要體現在以下3個方面:

  1. 違反網站意願,例如網站採取反爬措施後,強行突破其反爬措施;
  2. 爬蟲干擾了被訪問網站的正常運營;
  3. 爬蟲抓取了受到法律保護的特定型別的資料或資訊。

那麼作為爬蟲開發者,如何在使用爬蟲時避免進局子的厄運呢?

  1. 嚴格遵守網站設定的robots協議;
  2. 在規避反爬蟲措施的同時,需要優化自己的程式碼,避免干擾被訪問網站的正常執行;
  3. 在設定抓取策略時,應注意編碼抓取視訊、音樂等可能構成作品的資料,或者針對某些特定網站批量抓取其中的使用者生成內容;
  4. 在使用、傳播抓取到的資訊時,應審查所抓取的內容,如發現屬於使用者的個人資訊、隱私或者他人的商業祕密的,應及時停止並刪除。

        可以說在我們身邊的網路上已經密密麻麻爬滿了各種網路爬蟲,它們善惡不同,各懷心思。而越是每個人切身利益所在的地方,就越是爬滿了爬蟲。所以爬蟲是趨利的,它們永遠會向有利益的地方爬行。技術本身是無罪的,問題往往出在人無限的慾望上。因此爬蟲開發者的道德自持和企業經營者的良知才是避免觸碰法律底線的根本所在。

關於上述幾個法律案件的更多資訊可以參考下述地址:

http://caselaw.lp.findlaw.com/scripts/getcase. pl?court=US&vo1-499&invol=340
http://www.austlii.edu.au/au/cases/cth/ECA/2010/44.html

1.3 目標網站背景調查

1.3.1 檢查robots.txt

大多數網站都會定義robots.txt檔案,這樣可以讓爬蟲交接爬取該網站時存在哪些限制。這些限制雖然僅僅作為限制給出,但是良好的公民都應該遵守。在爬取之前,檢查robots.txt檔案這一寶貴資源可以最小化爬蟲被封禁的可能性,而且還能發現和網站結構相關的線索。關於robots.txt協議可以參考http://www.robotstxt.org。下面的程式碼是事例檔案robots.txt中的內容,可以訪問http://example.webscraping.com/robots.txt獲取(百度的 https://www.baidu.com/robots.txt)。

# section 1
User-agent: BadCrawler  # 禁止代理為BadCrawler的爬蟲爬取該網站。但是惡意爬蟲不會遵守這個規定。
Disallow: /  # 後面會講如何讓爬蟲自動遵守robots.txt的要求。

# section 2
User-agent: *  # 任何使用者代理都被允許。
Crawl-delay: 5  # 但是需要在兩次下載請求之間給出5秒的抓取延遲。我們應該遵從建議避免伺服器過載。
Disallow: /trap  # 這裡的/trap連結,用於封禁那些爬去了不允許連結的惡意爬蟲。封禁IP的時間可能是1分鐘,可能會更久,也可能是永久封禁。

# section 3
Sitemap: http://example.webscraping.com/sitemap.xml  # 定義了一個Sitemap檔案,後面1.3.2會講解如何檢查該檔案。

1.3.2 檢查網站地圖

        網站提供的 Sitemap檔案(即網站地圖)可以幫助爬蟲定位網站最新的內容,而無須爬取每一個網頁。如果想要了解更多資訊,可以從http://www.sitemaps.org/protocol.html獲取網站地圖標準的定義。下面是在 robots.txt檔案中發現的 Sitemap檔案的內容:

<?xml version="1.0" encoding="ISO-8859-1"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
        <loc>http://example.webscraping.com/places/default/view/Afghanistan-1</loc>
    </url>
    <url>
        <loc>http://example.webscraping.com/places/default/view/Aland-Islands-2</loc>
    </url>
    <url>
        <loc>http://example.webscraping.com/places/default/view/Albania-3</loc>
    </url>
    <url>
        <loc>http://example.webscraping.com/places/default/view/Algeria-4</loc>
    </url>
    <url>
        <loc>http://example.webscraping.com/places/default/view/American-Samoa-5</loc>
    </url>
    ...
</urlset>

這個檔案地圖提供了該網站所有網頁的連結。後面我們會用到這些資訊來建立爬蟲,但是需要謹慎處理,因為這個檔案經常存在缺失、過期或不完整的問題。

1.3.3 估算網站的大小

        目標網站的大小會影響我們如何進行爬取。如果是像我們的示例站點這樣只有幾百個URL的網站,效率並沒有那麼重要;但如果是擁有數百萬個網頁的站點,使用序列下載可能需要持續數月才能完成,這時就需要使用第4章中介紹的分散式下載來解決了。

        估算網站大小的一個簡便方法是檢查google爬蟲的結果,因為谷歌很可能已經爬取過我們感興趣的網站。我們可以通過Google搜尋的site關鍵詞過濾域名結果,從而獲取該資訊。我們可以從http://www.google.com/advanced_search瞭解到該介面及其他高階搜尋引數的用法。

        下圖所示為使用site關鍵詞對我們的示例網站進行搜尋的結果,即在百度中搜索site:cnblogs.com(也可以在Google搜尋中輸入site:cnblogs.com)。

        從圖中可以看出,此時百度估算該網站擁有九百多萬個網頁。在域名後面新增URL路徑,可以對結果進行過濾,僅顯示網站的某些部分。

1.3.4 識別網站所用的技術

        構建網站所使用的技術型別也會對我們如何爬取產生影響。有一個十分有用的工具可以檢查網站構建的技術型別——builtwith模組。

該模組的安裝方法如下:

pip install builtwith

該模組將URL作為引數,下載該URL並對其進行分析,然後返回該網站使用的技術。

例子如下:

import builtwith

ret = builtwith.parse('http://example.webscraping.com')
print ret

結果如下:

{
u'javascript-frameworks': [u'jQuery', u'Modernizr', u'jQuery UI'], # 該網站使用了js庫,其內容很可能是嵌入到HTML中的,相對比較容易抓取。 u'web-frameworks': [u'Web2py', u'Twitter Bootstrap'], # 該網站使用了python的Web2py框架 u'programming-languages': [u'Python'], # 該網站使用的是python語言 u'web-servers': [u'Nginx']
}

1.3.5 尋找網站所有者

        為了找到網站的所有者,我們可以使用 WHOIS協議查詢域名的註冊者是誰。 Python中有一個針對該協議的封裝庫,其文件地址為https://pypi.python.org/pypi/python- whois,我們可以通過pip進行安裝。

pip install python-whois

使用該模組對baidu.com這個域名進行WHIOS查詢,如下:

import whois

ret = whois.whois("baidu.com")
print ret

結果如下:

{
  "updated_date": [
    "2017-07-28 02:36:28", 
    "2017-07-27 19:36:28"
  ], 
  "status": [
    "clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited", 
    "clientTransferProhibited https://icann.org/epp#clientTransferProhibited", 
    "clientUpdateProhibited https://icann.org/epp#clientUpdateProhibited", 
    "serverDeleteProhibited https://icann.org/epp#serverDeleteProhibited", 
    "serverTransferProhibited https://icann.org/epp#serverTransferProhibited", 
    "serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited", 
    "clientUpdateProhibited (https://www.icann.org/epp#clientUpdateProhibited)", 
    "clientTransferProhibited (https://www.icann.org/epp#clientTransferProhibited)", 
    "clientDeleteProhibited (https://www.icann.org/epp#clientDeleteProhibited)", 
    "serverUpdateProhibited (https://www.icann.org/epp#serverUpdateProhibited)", 
    "serverTransferProhibited (https://www.icann.org/epp#serverTransferProhibited)", 
    "serverDeleteProhibited (https://www.icann.org/epp#serverDeleteProhibited)"
  ], 
  "name": null, 
  "dnssec": "unsigned", 
  "city": null, 
  "expiration_date": [
    "2026-10-11 11:05:17", 
    "2026-10-11 00:00:00"
  ], 
  "zipcode": null, 
  "domain_name": [
    "BAIDU.COM", 
    "baidu.com"
  ], 
  "country": "CN", 
  "whois_server": "whois.markmonitor.com", 
  "state": "Beijing", 
  "registrar": "MarkMonitor, Inc.", 
  "referral_url": null, 
  "address": null, 
  "name_servers": [
    "DNS.BAIDU.COM", 
    "NS2.BAIDU.COM", 
    "NS3.BAIDU.COM", 
    "NS4.BAIDU.COM", 
    "NS7.BAIDU.COM", 
    "ns4.baidu.com", 
    "ns7.baidu.com", 
    "ns3.baidu.com", 
    "ns2.baidu.com", 
    "dns.baidu.com"
  ], 
  "org": "Beijing Baidu Netcom Science Technology Co., Ltd.", 
  "creation_date": [
    "1999-10-11 11:05:17", 
    "1999-10-11 04:05:17"
  ], 
  "emails": [
    "[email protected]", 
    "[email protected]"
  ]
}

1.4 編寫第一個網路爬蟲

    3種爬取網站的常用方法:

  1. 爬取網站地圖
  2. 遍歷每個網頁的資料庫ID
  3. 跟蹤網頁連結

1.4.1 下載網頁

        使用python的urllib2模組下載URL:

import urllib2

def download(url):
    return urllib2.urlopen(url).read()

ret = download("https://www.cnblogs.com/aaronthon/p/10176432.html")
print ret

上面的程式碼會獲取完整的HTML。但是當網頁不存時,urllib2會丟擲異常,然後結束程式。

如下改進:

import urllib2

def download(url):
    try:
        html = urllib2.urlopen(url).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
    return html

ret = download("https://www.cnblogs.com/aaronthon/p/10176432.html")
print ret

如果下載出錯,該函式可以捕捉異常,並返回None。

1.4.1.1 重試下載

        下載時遇到的錯誤往往是臨時性的,比如伺服器過載時返回的503 Service Unavailable錯誤。對於此類錯誤,我們可以嘗試重新下載,因為這個伺服器問題現在可能已經修復了。不過,我們不需要對所有錯誤都嘗試重新下載。如果伺服器返回的是404 Not Fount這種錯誤,則說明該網頁不存在,再次嘗試下載毫無意義。

        HTTP錯誤的完整列表,詳情可參考https://tools.ietf.org/html/rfc7231#section-6。我們只需要確保download函式在發生5xx錯誤時重試下載即可。

程式碼如下:

import urllib2

def download(url, num_retries=2):
    try:
        html = urllib2.urlopen(url).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1)
    return html

ret = download("https://www.cnblogs.com/aaronthon/p/1076432.html")
print ret

1.4.1.2 設定使用者代理

        預設情況下,urllib2使用Python-urllib/2.7 作為使用者代理下載網頁內容,其中2.7是python的版本號。也許曾經使用質量不佳的網路爬蟲造成伺服器過載,一些網站還會這個預設的使用者代理。

        因此,為了下載更加可靠,我們需要控制使用者代理的設定。下面程式碼對download函式進行了修改,設定了一個預設的使用者代理“wswp”。

import urllib2

def download(url, num_retries=2, user_agent="wswp"):
    headers = {"User-agent" : user_agent}
    request = urllib2.Request(url, headers=headers)
    try:
        html = urllib2.urlopen(request).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1, user_agent)

    return html

ret = download("https://www.cnblogs.com/aaronthon/p/10176432.html")
print ret

這個爬蟲可以捕捉異常、重試下載和設定使用者代理。

1.4.2 網路地圖爬蟲

        這一節,我們使用簡單的正則表示式,解析網站地圖裡面的被<loc>標籤包裹的URL。

程式碼如下:

import re
import urllib2

def download(url, num_retries=2, user_agent="wswp"):
    print 'downloding:', url
    headers = {"User-agent" : user_agent}
    request = urllib2.Request(url, headers=headers)
    try:
        html = urllib2.urlopen(request).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1, user_agent)

    return html

def crawl_sitemap(url):
    sitemap = download(url)
    links = re.findall('<loc>(.*?)</loc>', sitemap)
    for link in links:
        html = download(link)

crawl_sitemap("http://example.webscraping.com/sitemap.xml")

執行結果:

downloding: http://example.webscraping.com/sitemap.xml
downloding: http://example.webscraping.com/places/default/view/Afghanistan-1
downloding: http://example.webscraping.com/places/default/view/Aland-Islands-2
downloding: http://example.webscraping.com/places/default/view/Albania-3
downloding: http://example.webscraping.com/places/default/view/Algeria-4
downloding: http://example.webscraping.com/places/default/view/American-Samoa-5
downloding: http://example.webscraping.com/places/default/view/Andorra-6
downloding: http://example.webscraping.com/places/default/view/Angola-7
downloding: http://example.webscraping.com/places/default/view/Anguilla-8
downloding: http://example.webscraping.com/places/default/view/Antarctica-9
downloding: http://example.webscraping.com/places/default/view/Antigua-and-Barbuda-10
downloding: http://example.webscraping.com/places/default/view/Argentina-11
downloding: http://example.webscraping.com/places/default/view/Armenia-12
downloding: http://example.webscraping.com/places/default/view/Aruba-13
downloding: http://example.webscraping.com/places/default/view/Australia-14
...

侷限性太強。

1.4.3 ID遍歷爬蟲

        我們例子中的網站地圖是這個樣子的:

<?xml version="1.0" encoding="ISO-8859-1"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://example.webscraping.com/places/default/view/Afghanistan-1</loc>
</url>
<url>
<loc>http://example.webscraping.com/places/default/view/Aland-Islands-2</loc>
</url>
<url>
<loc>http://example.webscraping.com/places/default/view/Albania-3</loc>
</url>
<url>
<loc>http://example.webscraping.com/places/default/view/Algeria-4</loc>
</url>
<url>
<loc>http://example.webscraping.com/places/default/view/American-Samoa-5</loc>
</url>
...
</urlset>

        可以看出,這些URL的尾處是連續的數字,並且我們這樣訪問http://example.webscraping.com/places/default/view/1,測試這個連結是可用的。

測試結果如下:

我們利用這個特性,寫出如下網路爬蟲:

import re
import urllib2
import itertools


def download(url, num_retries=2, user_agent="wswp"):
    print 'downloding:', url
    headers = {"User-agent" : user_agent}
    request = urllib2.Request(url, headers=headers)
    try:
        html = urllib2.urlopen(request).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1, user_agent)

    return html

for page in itertools.count(1):
    url = 'http://example.webscraping.com/places/default/view/-%d'%page
    html = download(url)
    if html is None:
        break
    else:
        pass

但是,上面這段程式碼卻顯示明顯的。當ID是不連續的,這是程式會立即退出。

如下改進:

import re
import urllib2
import itertools


def download(url, num_retries=2, user_agent="wswp"):
    print 'downloding:', url
    headers = {"User-agent" : user_agent}
    request = urllib2.Request(url, headers=headers)
    try:
        html = urllib2.urlopen(request).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1, user_agent)

    return html
max_errors = 5
num_errors = 0
for page in itertools.count(1):
    url = 'http://example.webscraping.com/places/default/view/-%d'%page
    html = download(url)
    if html is None:
        num_errors += 1
        if num_errors == max_errors:
            break
    else:
        num_errors = 0
        pass

這個爬蟲會在出現5此連續的下載錯誤才會退出程式。

ID遍歷爬蟲侷限性太大了,很多網站使用非連續打書做ID,或者不適應數值做ID。此時ID遍歷爬蟲就失去作用了。

1.4.4 連結爬蟲

        連結爬蟲通過追蹤所有連結的方式,很容易下載整個網站的頁面。但是我們只想下載我們需要的頁面,本節連結爬蟲將使用正則表示式來確定下載的頁面。

原始連結爬蟲版本如下:

import re
import urllib2
import urlparse

def download(url, num_retries=2, user_agent="wswp"):
    print 'downloding:', url
    headers = {"User-agent" : user_agent}
    request = urllib2.Request(url, headers=headers)
    try:
        html = urllib2.urlopen(request).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1, user_agent)
    return html

def get_links(html):
    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
    print webpage_regex.findall(html)
    return webpage_regex.findall(html)


def link_crawler(send_url, link_regex):
    crawl_queue = [send_url]
    while crawl_queue:
        url = crawl_queue.pop()
        html = download(url)
        for link in get_links(html):
            if re.search(link_regex, link):
                crawl_queue.append(link)

link_crawler('http://example.webscraping.com', '/(index|view)')

執行之後會有如下保錯:

downloding: http://example.webscraping.com
['/places/default/index', '#', '/places/default/user/register?_next=/places/default/index', '/places/default/user/login?_next=/places/default/index', '/places/default/index', '/places/default/search', '/places/default/view/Afghanistan-1', '/places/default/view/Aland-Islands-2', '/places/default/view/Albania-3', '/places/default/view/Algeria-4', '/places/default/view/American-Samoa-5', '/places/default/view/Andorra-6', '/places/default/view/Angola-7', '/places/default/view/Anguilla-8', '/places/default/view/Antarctica-9', '/places/default/view/Antigua-and-Barbuda-10', '/places/default/index/1']
Traceback (most recent call last):
  File "C:/Users/WIStron/PycharmProjects/flask_demo/logs/p.py", line 35, in <module>
    link_crawler('http://example.webscraping.com', '/(index|view)')
  File "C:/Users/WIStron/PycharmProjects/flask_demo/logs/p.py", line 29, in link_crawler
    html = download(url)
  File "C:/Users/WIStron/PycharmProjects/flask_demo/logs/p.py", line 10, in download
    html = urllib2.urlopen(request).read()
  File "C:\Python27\lib\urllib2.py", line 154, in urlopen
    return opener.open(url, data, timeout)
  File "C:\Python27\lib\urllib2.py", line 423, in open
    protocol = req.get_type()
  File "C:\Python27\lib\urllib2.py", line 285, in get_type
downloding: /places/default/index/1
    raise ValueError, "unknown url type: %s" % self.__original
ValueError: unknown url type: /places/default/index/1

        可以看出,問題出在載入/places/default/index/1時,該連結只有網頁的部分路徑,而沒有協議和伺服器部分,就是一個相對路徑。由於瀏覽器知道你正在瀏覽哪個網頁,所以在瀏覽器瀏覽時,相對路徑能夠正常工作。但是,urllib2是無法獲知上下文的。問了讓urllib2能夠定位網頁,我們需將路徑相對轉換成絕對路徑,以便定位網頁所有細節。Python中的urlparse模組就是來實現這一功能的。

如下趕緊程式碼,使用urlparse模組來建立絕對路徑:

 

import re
import time
import urllib2
import urlparse

def download(url, num_retries=2, user_agent="wswp"):
    print 'downloding:', url
    headers = {"User-agent" : user_agent}
    request = urllib2.Request(url, headers=headers)
    try:
        html = urllib2.urlopen(request).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1, user_agent)
    return html

def get_links(html):
    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
    return webpage_regex.findall(html)


def link_crawler(send_url, link_regex):
    crawl_queue = [send_url]
    while crawl_queue:
        url = crawl_queue.pop()
        html = download(url)
        for link in get_links(html):
            time.sleep(0.1)  # 防止伺服器過載
            if re.search(link_regex, link):
                link = urlparse.urljoin(send_url, link)
                crawl_queue.append(link)

link_crawler('http://example.webscraping.com', '/(index|view)')

 

這段程式碼不報錯了,但是同一個網頁會被重複下載,因為這些網頁相互之間存在連結。

如下優化,避免重複下載:

import re
import time
import urllib2
import urlparse

def download(url, num_retries=2, user_agent="wswp"):
    print 'downloding:', url
    headers = {"User-agent" : user_agent}
    request = urllib2.Request(url, headers=headers)
    try:
        html = urllib2.urlopen(request).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1, user_agent)
    return html

def get_links(html):
    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
    return webpage_regex.findall(html)


def link_crawler(send_url, link_regex):
    crawl_queue = [send_url]
    crawl_queue_a = [send_url]
    # seen = set(crawl_queue)
    while crawl_queue:
        url = crawl_queue.pop()
        html = download(url)
        for link in get_links(html):
            time.sleep(0.1)  # 防止網站過載
            if re.search(link_regex, link):
                link = urlparse.urljoin(send_url, link)
                if link in crawl_queue_a:
                    pass
                else:
                    crawl_queue.append(link)
                    crawl_queue_a.append(link)

link_crawler('http://example.webscraping.com', '/(index|view)')

這樣就不會下載重複的網頁了。

1.4.4.1 高階功能

為這個爬蟲新增新功能,使其爬取其他網站時更有用。

首先我們先解析robots.txt檔案,以避免下載禁止爬取的URL。使用Python自帶的rpbotparser模組,就可以輕鬆完成這項工作。

如下所示:

rp = robotparser.RobotFileParser()
rp.set_url('http://example.webscraping.com/robots.txt')
rp.read()
print rp  # rp是拿到的整個檔案內容
url = 'http://example.webscraping.com'
user_agent = 'BadCrawler'
r2 = rp.can_fetch(user_agent, url)
print r2
user_agent = 'GoodCrawler'
r3 = rp.can_fetch(user_agent, url)
print r3

結果為:

User-agent: BadCrawler
Disallow: /
False
True

        robotparser模組首先載入robots.txt檔案,然後通過can_fetch()函式確定指定的使用者代理是否允許訪問網頁。

        通過在crawl迴圈中新增該檢查,將此功能新增到我們的爬蟲中:

import re
import time
import datetime
import urllib2
import urlparse
import robotparser

rp = robotparser.RobotFileParser()
rp.set_url('http://example.webscraping.com/robots.txt')
rp.read()


def download(url, num_retries=2, user_agent="wswp"):
    print 'downloding:', url
    headers = {"User-agent" : user_agent}
    request = urllib2.Request(url, headers=headers)
    try:
        html = urllib2.urlopen(request).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1, user_agent)
    return html

def get_links(html):
    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
    return webpage_regex.findall(html)


def link_crawler(send_url, link_regex, user_agent=None):
    crawl_queue = [send_url]
    crawl_queue_a = [send_url]

    while crawl_queue:
        url = crawl_queue.pop()
        html = download(url)
        if rp.can_fetch(useragent=user_agent, url=url):
            for link in get_links(html):
                time.sleep(0.1)
                if re.search(link_regex, link):
                    link = urlparse.urljoin(send_url, link)
                    if link in crawl_queue_a:
                        pass
                    else:
                        crawl_queue.append(link)
                        crawl_queue_a.append(link)
        else:
            print 'Blocked by robots.txt:', url

link_crawler('http://example.webscraping.com', '/(index|view)', user_agent="BadCrawler")

1.4.4.2 支援代理

        有時我們需要使用代理訪問某個網站,但是有些網站遮蔽了太多的國家。使用urllib支援代理並沒有想象的那麼容易(可以嘗試使用更友好的Python HTTP的模組requests來實現該功能,其文件 http://docs.python-requests.org/)。

下面是使用urllib2支援代理的程式碼:

proxy = None
opener = urllib2.build_opener()
proxy_params = { urlparse.urlparse(url).scheme: proxy }
opener.add_handler(urllib2.ProxyHandler(proxy_params))
response = opener.open(request)

下面是集成了該功能的新版本download函式:

 

import re
import time
import datetime
import urllib2
import urlparse
import robotparser

rp = robotparser.RobotFileParser()
rp.set_url('http://example.webscraping.com/robots.txt')
rp.read()
def download(url, num_retries=2, user_agent="wswp", proxy=None):
    print 'downloding:', url
    headers = {"User-agent" : user_agent}
    request = urllib2.Request(url, headers=headers)
    opener = urllib2.build_opener()
    if proxy:
        proxy_params = { urlparse.urlparse(url).scheme: proxy }
        opener.add_handler(urllib2.ProxyHandler(proxy_params))
    try:
        html = urllib2.urlopen(request).read()
    except urllib2.URLError as e:
        print "Download error:", e.reason
        html = None
        if num_retries > 0:
            if hasattr(e, 'code') and 500 <= e.code <= 600:
                return download(url, num_retries-1, user_agent, proxy)
    return html

def get_links(html):
    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
    return webpage_regex.findall(html)


def link_crawler(send_url, link_regex, user_agent=None):
    crawl_queue = [send_url]
    crawl_queue_a = [send_url]

    while crawl_queue:
        url = crawl_queue.pop()
        html = download(url)
        if rp.can_fetch(useragent=user_agent, url=url):
            for link in get_links(html):
                time.sleep(0.1)
                if re.search(link_regex, link):
                    link = urlparse.urljoin(send_url, link)
                    if link in crawl_queue_a:
                        pass
                    else:
                        crawl_queue.append(link)
                        crawl_queue_a.append(link)
        else:
            print 'Blocked by robots.txt:', url

link_crawler('http://example.webscraping.com', '/(index|view)', user_agent="GoodCrawler")

 1.4.4.3 下載限速

# coding: utf-8
import re
import time
import datetime
import urllib2
import urlparse
import robotparser

rp = robotparser.RobotFileParser()  # robotparser模組用來獲取目標網站的robots.txt內容
rp.set_url('http://example.webscraping.com/robots.txt')
rp.read()  # 獲取整個檔案
class Throttle:

    def __init__(self, delay):
        self.delay = delay
        self.domains = {}  # 記錄最近一訪問時間

    def wait(self, url):
        domain = urlparse.urlparse(url).netloc  # 拿到傳入的url
        last_accessed = self.domains.get(domain)  # 獲取最近一次訪問時間

        if self.delay > 0 and last_accessed is not None:  # 最近有訪問
            sleep_secs = self.delay - (datetime.datetime.now() - last_accessed).seconds
            if sleep_secs > 0:
                time.sleep(sleep_secs)
        self.domains[domain] = datetime.datetime.now()  # 更新最近訪問的時間


def download(url, num_retries=2, user_agent="wswp", proxy=None):
    print 'downloding:', url
    headers = {"User-agent" : user_agent}  # 請求頭
    request = urllib2.Request(url, headers=headers)  # urllib2
    opener = urllib2.build_opener()
    if proxy:
        proxy_params = { urlparse.urlparse(url).scheme: proxy }
        opener.add_handler(urllib2.ProxyHandler(proxy_params))
    try:
        html = urllib2.urlopen(request).read()  # 爬去網頁
    except urllib2.URLError as e:  # 出錯,列印錯誤
        print "Download error:", e.reason
        html = None
        if num_retries > 0:  # 多試幾次
            if hasattr(e, 'code') and 500 <= e.code <= 600:  # 如果是伺服器錯誤,就多試幾次
                return download(url, num_retries-1, user_agent, proxy)
    return html  # 返回拿到的html

def get_links(html):
    webpage_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
    return webpage_regex.findall(html)


def link_crawler(send_url, link_regex, user_agent=None):
    crawl_queue = [send_url]  # 將url放到列表中
    crawl_queue_a = [send_url]  # 備份一份url列表

    while crawl_queue:  # 迴圈這個列表,爬取每一個urll
        url = crawl_queue.pop()  # 拿到要爬的url

        html = download(url)  # 下載html
        throttle.wait(url)

        print url, "url"
        if rp.can_fetch(useragent=user_agent, url=url):  # 檢測該url是否允許爬蟲訪問
            for link in get_links(html):  # 追蹤所有的url
                # time.sleep(0.1)  # 防止伺服器過載
                if re.search(link_regex, link):  # 正則匹配
                    link = urlparse.urljoin(send_url, link)  # 拼接路徑
                    if link in crawl_queue_a:  # 避免重複爬取
                        pass
                    else:
                        crawl_queue.append(link)  # 加到需要迴圈的列表
                        crawl_queue_a.append(link)  # 備份,避免重複爬取

        else:
            print 'Blocked by robots.txt:', url  # 拒絕爬蟲


delay = 10  # 兩次下載間隔10秒,低於10秒會睡一下

url = 'http://example.webscraping.com'
headers = {"User-agent" : "GoodCrawler"}
throttle = Throttle(delay)
result = link_crawler(url, link_regex='/(index|view)', user_agent="GoodCrawler")

 1.4.4.4 避免爬蟲陷阱

        上面我們寫好的爬蟲會跟蹤所有沒有訪問過的連結。但是一些網站會動態生成頁面內容,比如一個線上日曆功能,提供了一個可以訪問下一年和下一月的連結,這樣的頁面就會無限連結下去。這種情況被稱為爬蟲陷阱。

        想要避免爬蟲陷阱,最簡單的方法就是記錄當前頁面經過了多少個連結,也就是深度。當達到最大深度時,爬蟲就不再向佇列新增該網頁中的連結了。要實現該功能,我們需要修改seen變數。該變數先只記錄訪問過的網頁連結,現在修改為一個字典,增加了深度的記錄。

 

 宣告:

        本文章為本人學習《用Python寫網路爬蟲》時的學習隨筆,不作為商業用途。

        原書作者:【澳】 Richard Lawson

        譯者:李斌

        出版社:中國工信出版集團  人民郵電出版社

        請支援原著!