1.網絡爬蟲何時有用
假設我有一個鞋店,并且想要及時了解競爭對手的價格。我可以每天訪問他們的網站,與我店鋪中鞋子的價格進行對比。但是,如果我店鋪中的鞋類品種繁多,或是希望能夠更加頻繁地查看價格變化的話,就需要花費大量的時間,甚至難以實現。再舉一個例子,我看中了一雙鞋,想等它促銷時再購買。我可能需要每天訪問這家鞋店的網站來查看這雙鞋是否降價,也許需要等待幾個月的時間,我才能如愿盼到這雙鞋促銷。上述這 兩個重復性的手工流程,都可以利用網絡爬蟲技術實現自動化處理。
理想狀態下,網絡爬蟲并不是必須品,每個網站都應該提供API,以結構化的格式共享它們的數據。然而現實情況中,雖然一些網站已經提供了這種API,但是它們通常會限制可以抓取的數據,以及訪問這些數據的頻率。另外,對于網站的開發者而言,維護前端界面比維護后端API接口優先級更高。總之,我們不能僅僅依賴于API去訪問我們所需的在線數據,而是應該學習一些網絡爬蟲技術的相關知識。
2. 網絡爬蟲是否合法
網絡爬蟲目前還處于早期的蠻荒階段,“允許哪些行為”這種基本秩序還處于建設之中。從目前的實踐來看,如果抓取數據的行為用于個人使用,則不存在問題;而如果數據用于轉載,那么抓取的數據類型就非常關鍵了。
世界各地法院的一些案件可以幫助我們確定哪些網絡爬蟲行為是允許的。在Feist Publications, Inc.起訴Rural Telephone Service Co.的案件中,美國聯邦最高法院裁定抓取并轉載真實數據(比如,電話清單)是允許的。而在澳大利亞,Telstra Corporation Limited起訴Phone Directories Company Pty Ltd這一類似案件中,則裁定只有擁有明確作者的數據,才可以獲得版權。此外,在歐盟的ofir.dk起訴home.dk一案中,最終裁定定期抓取和深度鏈接是允許的。
這些案件告訴我們,當抓取的數據是現實生活中的真實數據(比如,營業地址、電話清單)時,是允許轉載的。但是,如果是原創數據(比如,意見和評論),通常就會受到版權限制,而不能轉載。
無論如何,當你抓取某個網站的數據時,請記住自己是該網站的訪客,應當約束自己的抓取行為,否則他們可能會封禁你的IP,甚至采取更進一步的法律行動。這就要求下載請求的速度需要限定在一個合理值之內,并且還需要設定一個專屬的用戶代理來標識自己。在下面的小節中我們將會對這些實踐進行具體介紹。
關于上述幾個法律案件的更多信息可以參考下述地址:
- http://caselaw.lp.findlaw.com/scripts/getcase. pl?court=US&vol=499&invol=340
- http://www.austlii.edu.au/au/cases/cth/FCA/2010/44.html
- http://www.bvhd.dk/uploads/tx_mocarticles/S_og_Handelsrettens_afg_relse_i_Ofir-sagen.pdf
3. 背景調研
在深入討論爬取一個網站之前,我們首先需要對目標站點的規模和結構進行一定程度的了解。網站自身的robots.txt和Sitemap文件都可以為我們提供一定的幫助,此外還有一些能提供更詳細信息的外部工具,比如Google搜索和WHOIS。
3.1 檢查robots.txt
大多數網站都會定義robots.txt文件,這樣可以讓爬蟲了解爬取該網站時存在哪些限制。這些限制雖然僅僅作為建議給出,但是良好的網絡公民都應當遵守這些限制。在爬取之前,檢查robots.txt文件這一寶貴資源可以最小化爬蟲被封禁的可能,而且還能發現和網站結構相關的線索。關于robots.txt協議的更多信息可以參見http://www.robotstxt.org。下面的代碼是我們的示例文件robots.txt中的內容,可以訪問http://example.webscraping.com/robots.txt獲取。
# section 1 User-agent: BadCrawler Disallow: / # section 2 User-agent: * Crawl-delay: 5 Disallow: /trap # section 3 Sitemap: http://example.webscraping.com/sitemap.xml
在section 1中,robots.txt文件禁止用戶代理為BadCrawler的爬蟲爬取該網站,不過這種寫法可能無法起到應有的作用,因為惡意爬蟲根本不會遵從robots.txt的要求。本章后面的一個例子將會展示如何讓爬蟲自動遵守robots.txt的要求。
section 2規定,無論使用哪種用戶代理,都應該在兩次下載請求之間給出5秒的抓取延遲,我們需要遵從該建議以避免服務器過載。這里還有一個/trap鏈接,用于封禁那些爬取了不允許鏈接的惡意爬蟲。如果你訪問了這個鏈接,服務器就會封禁你的IP一分鐘!一個真實的網站可能會對你的IP封禁更長時間,甚至是永久封禁。不過如果這樣設置的話,我們就無法繼續這個例子了。
section 3定義了一個Sitemap文件,我們將在下一節中了解如何檢查該文件。
3.2 檢查網站地圖
網站提供的Sitemap文件(即網站地圖)可以幫助爬蟲定位網站最新的內容,而無須爬取每一個網頁。如果想要了解更多信息,可以從http://www.sitemaps.org/protocol.html獲取網站地圖標準的定義。下面是在robots.txt文件中發現的Sitemap文件的內容。
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>http://example.webscraping.com/view/Afghanistan-1 </loc></url> <url><loc>http://example.webscraping.com/view/Aland-Islands-2 </loc></url> <url><loc>http://example.webscraping.com/view/Albania-3</loc> </url> ... </urlset>
網站地圖提供了所有網頁的鏈接,我們會在后面的小節中使用這些信息,用于創建我們的第一個爬蟲。雖然Sitemap文件提供了一種爬取網站的有效方式,但是我們仍需對其謹慎處理,因為該文件經常存在缺失、過期或不完整的問題。
3.3 估算網站大小
目標網站的大小會影響我們如何進行爬取。如果是像我們的示例站點這樣只有幾百個URL的網站,效率并沒有那么重要;但如果是擁有數百萬個網頁的站點,使用串行下載可能需要持續數月才能完成,這時就需要使用第4章中介紹的分布式下載來解決了。
估算網站大小的一個簡便方法是檢查Google爬蟲的結果,因為Google很可能已經爬取過我們感興趣的網站。我們可以通過Google搜索的site關鍵詞過濾域名結果,從而獲取該信息。我們可以從http://www.google.com/advanced_search了解到該接口及其他高級搜索參數的用法。
圖1所示為使用site關鍵詞對我們的示例網站進行搜索的結果,即在Google中搜索site:example.webscraping.com。
從圖1中可以看出,此時Google估算該網站擁有202個網頁,這和實際情況差不多。不過對于更大型的網站,我們會發現Google的估算并不十分準確。
在域名后面添加URL路徑,可以對結果進行過濾,僅顯示網站的某些部分。圖2所示為搜索site:example.webscraping.com/view的結果。該搜索條件會限制Google只搜索國家頁面。
圖1
圖2
這種附加的過濾條件非常有用,因為在理想情況下,你只希望爬取網站中包含有用數據的部分,而不是爬取網站的每個頁面。
3.4 識別網站所用技術
構建網站所使用的技術類型也會對我們如何爬取產生影響。有一個十分有用的工具可以檢查網站構建的技術類型——builtwith模塊。該模塊的安裝方法如下。
pip install builtwith
該模塊將URL作為參數,下載該URL并對其進行分析,然后返回該網站使用的技術。下面是使用該模塊的一個例子。
>>> import builtwith >>> builtwith.parse('http://example.webscraping.com') {u'javascript-frameworks': [u'jQuery', u'Modernizr', u'jQuery UI'], u'programming-languages': [u'Python'], u'web-frameworks': [u'Web2py', u'Twitter Bootstrap'], u'web-servers': [u'Nginx']}
從上面的返回結果中可以看出,示例網站使用了Python的Web2py框架,另外還使用了一些通用的JavaScript庫,因此該網站的內容很有可能是嵌入在html中的,相對而言比較容易抓取。而如果改用AngularJS構建該網站,此時的網站內容就很可能是動態加載的。另外,如果網站使用了ASP.NET,那么在爬取網頁時,就必須要用到會話管理和表單提交了。
3.5 尋找網站所有者
對于一些網站,我們可能會關心其所有者是誰。比如,我們已知網站的所有者會封禁網絡爬蟲,那么我們最好把下載速度控制得更加保守一些。為了找到網站的所有者,我們可以使用WHOIS協議查詢域名的注冊者是誰。Python中有一個針對該協議的封裝庫,其文檔地址為https://pypi.python.org/pypi/python-whois,我們可以通過pip進行安裝。
pip install python-whois
下面是使用該模塊對appspot.com這個域名進行WHOIS查詢時的返回結果。
>>> import whois >>> print whois.whois('appspot.com') { ... "name_servers": [ "NS1.GOOGLE.COM", "NS2.GOOGLE.COM", "NS3.GOOGLE.COM", "NS4.GOOGLE.COM", "ns4.google.com", "ns2.google.com", "ns1.google.com", "ns3.google.com" ], "org": "Google Inc.", "emails": [ "abusecomplaints@markmonitor.com", "dns-admin@google.com" ] }
從結果中可以看出該域名歸屬于Google,實際上也確實如此。該域名是用于Google App Engine服務的。當我們爬取該域名時就需要十分小心,因為Google經常會阻斷網絡爬蟲,盡管實際上其自身就是一個網絡爬蟲業務。
4. 編寫第一個網絡爬蟲
為了抓取網站,我們首先需要下載包含有感興趣數據的網頁,該過程一般被稱為爬取(crawling)。爬取一個網站有很多種方法,而選用哪種方法更加合適,則取決于目標網站的結構。我們首先會探討如何安全地下載網頁,然后會介紹如下3種爬取網站的常見方法:
- 爬取網站地圖;
- 遍歷每個網頁的數據庫ID;
- 跟蹤網頁鏈接。
4.1 下載網頁
要想爬取網頁,我們首先需要將其下載下來。下面的示例腳本使用Python的urllib2模塊下載URL。
import urllib2 def download(url): return urllib2.urlopen(url).read()
當傳入URL參數時,該函數將會下載網頁并返回其HTML。不過,這個代碼片段存在一個問題,即當下載網頁時,我們可能會遇到一些無法控制的錯誤,比如請求的頁面可能不存在。此時,urllib2會拋出異常,然后退出腳本。安全起見,下面再給出一個更健壯的版本,可以捕獲這些異常。
import urllib2 def download(url): print 'Downloading:', url try: html = urllib2.urlopen(url).read() except urllib2.URLError as e: print 'Download error:', e.reason html = None return html
現在,當出現下載錯誤時,該函數能夠捕獲到異常,然后返回None。
1.重試下載
下載時遇到的錯誤經常是臨時性的,比如服務器過載時返回的503 Service Unavailable錯誤。對于此類錯誤,我們可以嘗試重新下載,因為這個服務器問題現在可能已解決。不過,我們不需要對所有錯誤都嘗試重新下載。如果服務器返回的是404 Not Found這種錯誤,則說明該網頁目前并不存在,再次嘗試同樣的請求一般也不會出現不同的結果。
互聯網工程任務組(Internet Engineering Task Force)定義了HTTP錯誤的完整列表,詳情可參考https://tools.ietf.org/html/rfc7231#section-6。從該文檔中,我們可以了解到4xx錯誤發生在請求存在問題時,而5xx錯誤則發生在服務端存在問題時。所以,我們只需要確保download函數在發生5xx錯誤時重試下載即可。下面是支持重試下載功能的新版本 代碼。
def download(url, num_retries=2): print 'Downloading:', url 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: # recursively retry 5xx HTTP errors return download(url, num_retries-1) return html
現在,當download函數遇到5xx錯誤碼時,將會遞歸調用函數自身進行重試。此外,該函數還增加了一個參數,用于設定重試下載的次數,其默認值為兩次。我們在這里限制網頁下載的嘗試次數,是因為服務器錯誤可能暫時還沒有解決。想要測試該函數,可以嘗試下載http://httpstat.us/500,該網址會始終返回500錯誤碼。
>>> download('http://httpstat.us/500')Downloading: http://httpstat.us/500Download error: Internal Server ErrorDownloading: http://httpstat.us/500Download error: Internal Server ErrorDownloading: http://httpstat.us/500Download error: Internal Server Error
從上面的返回結果可以看出,download函數的行為和預期一致,先嘗試下載網頁,在接收到500錯誤后,又進行了兩次重試才放棄。
2.設置用戶代理
默認情況下,urllib2使用Python-urllib/2.7作為用戶代理下載網頁內容,其中2.7是Python的版本號。如果能使用可辨識的用戶代理則更好,這樣可以避免我們的網絡爬蟲碰到一些問題。此外,也許是因為曾經歷過質量不佳的Python網絡爬蟲造成的服務器過載,一些網站還會封禁這個默認的用戶代理。比如,在使用Python默認用戶代理的情況下,訪問http://www.meetup.com/,目前會返回如圖3所示的訪問拒絕提示。
圖3
因此,為了下載更加可靠,我們需要控制用戶代理的設定。下面的代碼對download函數進行了修改,設定了一個默認的用戶代理“wswp”(即Web Scraping with Python的首字母縮寫)。
def download(url, user_agent='wswp', num_retries=2): print 'Downloading:', 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: # retry 5XX HTTP errors return download(url, user_agent, num_retries-1) return html
現在,我們擁有了一個靈活的下載函數,可以在后續示例中得到復用。該函數能夠捕獲異常、重試下載并設置用戶代理。
4.2 網站地圖爬蟲
在第一個簡單的爬蟲中,我們將使用示例網站robots.txt文件中發現的網站地圖來下載所有網頁。為了解析網站地圖,我們將會使用一個簡單的正則表達式,從<loc>標簽中提取出URL。下面是該示例爬蟲的代碼。
def crawl_sitemap(url): # download the sitemap file sitemap = download(url) # extract the sitemap links links = re.findall('<loc>(.*?)</loc>', sitemap) # download each link for link in links: html = download(link) # scrape html here # ...
現在,運行網站地圖爬蟲,從示例網站中下載所有國家頁面。
>>> crawl_sitemap('http://example.webscraping.com/sitemap.xml')Downloading: http://example.webscraping.com/sitemap.xmlDownloading: http://example.webscraping.com/view/Afghanistan-1Downloading: http://example.webscraping.com/view/Aland-Islands-2Downloading: http://example.webscraping.com/view/Albania-3...
可以看出,上述運行結果和我們的預期一致,不過正如前文所述,我們無法依靠Sitemap文件提供每個網頁的鏈接。下面我們將會介紹另一個簡單的爬蟲,該爬蟲不再依賴于Sitemap文件。
4.3 ID遍歷爬蟲
本節中,我們將利用網站結構的弱點,更加輕松地訪問所有內容。下面是一些示例國家的URL。
- http://example.webscraping.com/view/Afghanistan-1
- http://example.webscraping.com/view/Australia-2
- http://example.webscraping.com/view/Brazil-3
可以看出,這些URL只在結尾處有所區別,包括國家名(作為頁面別名)和ID。在URL中包含頁面別名是非常普遍的做法,可以對搜索引擎優化起到幫助作用。一般情況下,Web服務器會忽略這個字符串,只使用ID來匹配數據庫中的相關記錄。下面我們將其移除,加載http://example.webscraping.com/view/1,測試示例網站中的鏈接是否仍然可用。測試結果如圖4所示。
圖4
從圖4中可以看出,網頁依然可以加載成功,也就是說該方法是有用的。現在,我們就可以忽略頁面別名,只遍歷ID來下載所有國家的頁面。下面是使用了該技巧的代碼片段。
import itertools for page in itertools.count(1): url = 'http://example.webscraping.com/view/-%d' % page html = download(url) if html is None: break else: # success - can scrape the result pass
在這段代碼中,我們對ID進行遍歷,直到出現下載錯誤時停止,我們假設此時已到達最后一個國家的頁面。不過,這種實現方式存在一個缺陷,那就是某些記錄可能已被刪除,數據庫ID之間并不是連續的。此時,只要訪問到某個間隔點,爬蟲就會立即退出。下面是這段代碼的改進版本,在該版本中連續發生多次下載錯誤后才會退出程序。
# maximum number of consecutive download errors allowed max_errors = 5 # current number of consecutive download errors num_errors = 0 for page in itertools.count(1): url = 'http://example.webscraping.com/view/-%d' % page html = download(url) if html is None: # received an error trying to download this webpage num_errors = 1 if num_errors == max_errors: # reached maximum number of # consecutive errors so exit break else: # success - can scrape the result # ... num_errors = 0
上面代碼中實現的爬蟲需要連續5次下載錯誤才會停止遍歷,這樣就很大程度上降低了遇到被刪除記錄時過早停止遍歷的風險。
在爬取網站時,遍歷ID是一個很便捷的方法,但是和網站地圖爬蟲一樣,這種方法也無法保證始終可用。比如,一些網站會檢查頁面別名是否滿足預期,如果不是,則會返回404 Not Found錯誤。而另一些網站則會使用非連續大數作為ID,或是不使用數值作為ID,此時遍歷就難以發揮其作用了。例如,Amazon使用ISBN作為圖書ID,這種編碼包含至少10位數字。使用ID對Amazon的圖書進行遍歷需要測試數十億次,因此這種方法肯定不是抓取該站內容最高效的方法。
4.4 鏈接爬蟲
到目前為止,我們已經利用示例網站的結構特點實現了兩個簡單爬蟲,用于下載所有的國家頁面。只要這兩種技術可用,就應當使用其進行爬取,因為這兩種方法最小化了需要下載的網頁數量。不過,對于另一些網站,我們需要讓爬蟲表現得更像普通用戶,跟蹤鏈接,訪問感興趣的內容。
通過跟蹤所有鏈接的方式,我們可以很容易地下載整個網站的頁面。但是,這種方法會下載大量我們并不需要的網頁。例如,我們想要從一個在線論壇中抓取用戶賬號詳情頁,那么此時我們只需要下載賬號頁,而不需要下載討論貼的頁面。本文中的鏈接爬蟲將使用正則表達式來確定需要下載哪些頁面。下面是這段代碼的初始版本。
import re def link_crawler(seed_url, link_regex): """Crawl from the given seed URL following links matched by link_regex """ crawl_queue = [seed_url] while crawl_queue: url = crawl_queue.pop() html = download(url) # filter for links matching our regular expression for link in get_links(html): if re.match(link_regex, link): crawl_queue.append(link) def get_links(html): """Return a list of links from html """ # a regular expression to extract all links from the webpage webpage_regex = re.compile('<a[^>] href=["'](.*?)["']', re.IGNORECASE) # list of all links from the webpage return webpage_regex.findall(html)
要運行這段代碼,只需要調用link_crawler函數,并傳入兩個參數:要爬取的網站URL和用于跟蹤鏈接的正則表達式。對于示例網站,我們想要爬取的是國家列表索引頁和國家頁面。其中,索引頁鏈接格式如下。
- http://example.webscraping.com/index/1
- http://example.webscraping.com/index/2
國家頁鏈接格式如下。
- http://example.webscraping.com/view/Afghanistan-1
- http://example.webscraping.com/view/Aland-Islands-2
因此,我們可以用/(index|view)/這個簡單的正則表達式來匹配這兩類網頁。當爬蟲使用這些輸入參數運行時會發生什么呢?你會發現我們得到了如下的下載錯誤。
>>> link_crawler('http://example.webscraping.com', '/(index|view)') Downloading: http://example.webscraping.com Downloading: /index/1 Traceback (most recent call last): ... ValueError: unknown url type: /index/1
可以看出,問題出在下載/index/1時,該鏈接只有網頁的路徑部分,而沒有協議和服務器部分,也就是說這是一個相對鏈接。由于瀏覽器知道你正在瀏覽哪個網頁,所以在瀏覽器瀏覽時,相對鏈接是能夠正常工作的。但是,urllib2是無法獲知上下文的。為了讓urllib2能夠定位網頁,我們需要將鏈接轉換為絕對鏈接的形式,以便包含定位網頁的所有細節。如你所愿,Python中確實有用來實現這一功能的模塊,該模塊稱為urlparse。下面是link_crawler的改進版本,使用了urlparse模塊來創建絕對路徑。
import urlparse def link_crawler(seed_url, link_regex): """Crawl from the given seed URL following links matched by link_regex """ crawl_queue = [seed_url] while crawl_queue: url = crawl_queue.pop() html = download(url) for link in get_links(html): if re.match(link_regex, link): link = urlparse.urljoin(seed_url, link) crawl_queue.append(link)
當你運行這段代碼時,會發現雖然網頁下載沒有出現錯誤,但是同樣的地點總是會被不斷下載到。這是因為這些地點相互之間存在鏈接。比如,澳大利亞鏈接到了南極洲,而南極洲也存在到澳大利亞的鏈接,此時爬蟲就會在它們之間不斷循環下去。要想避免重復爬取相同的鏈接,我們需要記錄哪些鏈接已經被爬取過。下面是修改后的link_crawler函數,已具備存儲已發現URL的功能,可以避免重復下載。
def link_crawler(seed_url, link_regex): crawl_queue = [seed_url] # keep track which URL's have seen before seen = set(crawl_queue) while crawl_queue: url = crawl_queue.pop() html = download(url) for link in get_links(html): # check if link matches expected regex if re.match(link_regex, link): # form absolute link link = urlparse.urljoin(seed_url, link) # check if have already seen this link if link not in seen: seen.add(link) crawl_queue.append(link)
當運行該腳本時,它會爬取所有地點,并且能夠如期停止。最終,我們得到了一個可用的爬蟲!
高級功能
現在,讓我們為鏈接爬蟲添加一些功能,使其在爬取其他網站時更加有用。
解析robots.txt
首先,我們需要解析robots.txt文件,以避免下載禁止爬取的URL。使用Python自帶的robotparser模塊,就可以輕松完成這項工作,如下面的代碼所示。
>>> import robotparser>>> rp = robotparser.RobotFileParser()>>> rp.set_url('http://example.webscraping.com/robots.txt')>>> rp.read()>>> url = 'http://example.webscraping.com'>>> user_agent = 'BadCrawler'>>> rp.can_fetch(user_agent, url)False>>> user_agent = 'GoodCrawler'>>> rp.can_fetch(user_agent, url)True
robotparser模塊首先加載robots.txt文件,然后通過can_fetch()函數確定指定的用戶代理是否允許訪問網頁。在本例中,當用戶代理設置為 BadCrawler 時,robotparser模塊會返回結果表明無法獲取網頁,這和示例網站robots.txt的定義一樣。
為了將該功能集成到爬蟲中,我們需要在crawl循環中添加該檢查。
... while crawl_queue: url = crawl_queue.pop() # check url passes robots.txt restrictions if rp.can_fetch(user_agent, url): ... else: print 'Blocked by robots.txt:', url
支持代理
有時我們需要使用代理訪問某個網站。比如,Netflix屏蔽了美國以外的大多數國家。使用urllib2支持代理并沒有想象中那么容易(可以嘗試使用更友好的Python HTTP模塊requests來實現該功能,其文檔地址為http://docs.python-requests.org/)。下面是使用urllib2支持代理的代碼。
proxy = ... opener = urllib2.build_opener() proxy_params = {urlparse.urlparse(url).scheme: proxy} opener.add_handler(urllib2.ProxyHandler(proxy_params)) response = opener.open(request)
下面是集成了該功能的新版本download函數。
def download(url, user_agent='wswp', proxy=None, num_retries=2): print 'Downloading:', 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 = opener.open(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: # retry 5XX HTTP errors html = download(url, user_agent, proxy, num_retries-1) return html
下載限速
如果我們爬取網站的速度過快,就會面臨被封禁或是造成服務器過載的風險。為了降低這些風險,我們可以在兩次下載之間添加延時,從而對爬蟲限速。下面是實現了該功能的類的代碼。
class Throttle: """Add a delay between downloads to the same domain """ def __init__(self, delay): # amount of delay between downloads for each domain self.delay = delay # timestamp of when a domain was last accessed self.domains = {} def wait(self, url): domain = urlparse.urlparse(url).netloc 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: # domain has been accessed recently # so need to sleep time.sleep(sleep_secs) # update the last accessed time self.domains[domain] = datetime.datetime.now()
Throttle類記錄了每個域名上次訪問的時間,如果當前時間距離上次訪問時間小于指定延時,則執行睡眠操作。我們可以在每次下載之前調用Throttle對爬蟲進行限速。
throttle = Throttle(delay) ... throttle.wait(url) result = download(url, headers, proxy=proxy, num_retries=num_retries)
避免爬蟲陷阱
目前,我們的爬蟲會跟蹤所有之前沒有訪問過的鏈接。但是,一些網站會動態生成頁面內容,這樣就會出現無限多的網頁。比如,網站有一個在線日歷功能,提供了可以訪問下個月和下一年的鏈接,那么下個月的頁面中同樣會包含訪問再下個月的鏈接,這樣頁面就會無止境地鏈接下去。這種情況被稱為爬蟲陷阱。
想要避免陷入爬蟲陷阱,一個簡單的方法是記錄到達當前網頁經過了多少個鏈接,也就是深度。當到達最大深度時,爬蟲就不再向隊列中添加該網頁中的鏈接了。要實現這一功能,我們需要修改seen變量。該變量原先只記錄訪問過的網頁鏈接,現在修改為一個字典,增加了頁面深度的記錄。
def link_crawler(..., max_depth=2): max_depth = 2 seen = {} ... depth = seen[url] if depth != max_depth: for link in links: if link not in seen: seen[link] = depth 1 crawl_queue.append(link)
現在有了這一功能,我們就有信心爬蟲最終一定能夠完成。如果想要禁用該功能,只需將max_depth設為一個負數即可,此時當前深度永遠不會與之相等。
最終版本
這個高級鏈接爬蟲的完整源代碼可以在https://bitbucket.org/ wswp/code/src/tip/chapter01/link_crawler3.py下載得到。要測試這段代碼,我們可以將用戶代理設置為BadCrawler,也就是本章前文所述的被robots.txt屏蔽了的那個用戶代理。從下面的運行結果中可以看出,爬蟲果然被屏蔽了,代碼啟動后馬上就會結束。
>>> seed_url = 'http://example.webscraping.com/index'>>> link_regex = '/(index|view)'>>> link_crawler(seed_url, link_regex, user_agent='BadCrawler')Blocked by robots.txt: http://example.webscraping.com/
現在,讓我們使用默認的用戶代理,并將最大深度設置為1,這樣只有主頁上的鏈接才會被下載。
>>> link_crawler(seed_url, link_regex, max_depth=1)Downloading: http://example.webscraping.com//indexDownloading: http://example.webscraping.com/index/1Downloading: http://example.webscraping.com/view/Antigua-and-Barbuda-10Downloading: http://example.webscraping.com/view/Antarctica-9Downloading: http://example.webscraping.com/view/Anguilla-8Downloading: http://example.webscraping.com/view/Angola-7Downloading: http://example.webscraping.com/view/Andorra-6Downloading: http://example.webscraping.com/view/American-Samoa-5Downloading: http://example.webscraping.com/view/Algeria-4Downloading: http://example.webscraping.com/view/Albania-3Downloading: http://example.webscraping.com/view/Aland-Islands-2Downloading: http://example.webscraping.com/view/Afghanistan-1
和預期一樣,爬蟲在下載完國家列表的第一頁之后就停止了。
本文節選自《用Python寫網絡爬蟲》
本書講解了如何使用Python來編寫網絡爬蟲程序,內容包括網絡爬蟲簡介,從頁面中抓取數據的三種方法,提取緩存中的數據,使用多個線程和進程來進行并發抓取,如何抓取動態頁面中的內容,與表單進行交互,處理頁面中的驗證碼問題,以及使用Scarpy和Portia來進行數據抓取,并在最后使用本書介紹的數據抓取技術對幾個真實的網站進行了抓取,旨在幫助讀者活學活用書中介紹的技術。
本書適合有一定Python編程經驗,而且對爬蟲技術感興趣的讀者閱讀。
版權聲明:本文內容由互聯網用戶自發貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如發現本站有涉嫌抄襲侵權/違法違規的內容, 請發送郵件至 舉報,一經查實,本站將立刻刪除。