假設我有一個鞋店,并且想要及時了解競爭對手的價格。我可以每天訪問他們的網站,與我店鋪中鞋子的價格進行對比。但是,如果我店鋪中的鞋類品種繁多,或是希望能夠更加頻繁地查看價格變化的話,就需要花費大量的時間,甚至難以實現。再舉一個例子,我看中了一雙鞋,想等它促銷時再購買。我可能需要每天訪問這家鞋店的網站來查看這雙鞋是否降價,也許需要等待幾個月的時間,我才能如愿盼到這雙鞋促銷。上述這 兩個重復性的手工流程,都可以利用網絡爬蟲技術實現自動化處理。
理想狀態下,網絡爬蟲并不是必須品,每個網站都應該提供API,以結構化的格式共享它們的數據。然而現實情況中,雖然一些網站已經提供了這種API,但是它們通常會限制可以抓取的數據,以及訪問這些數據的頻率。另外,對于網站的開發者而言,維護前端界面比維護后端API接口優先級更高??傊覀儾荒軆H僅依賴于API去訪問我們所需的在線數據,而是應該學習一些網絡爬蟲技術的相關知識。
網絡爬蟲目前還處于早期的蠻荒階段,“允許哪些行為”這種基本秩序還處于建設之中。從目前的實踐來看,如果抓取數據的行為用于個人使用,則不存在問題;而如果數據用于轉載,那么抓取的數據類型就非常關鍵了。
世界各地法院的一些案件可以幫助我們確定哪些網絡爬蟲行為是允許的。在Feist Publications, Inc.起訴Rural Telephone Service Co.的案件中,美國聯邦最高法院裁定抓取并轉載真實數據(比如,電話清單)是允許的。而在澳大利亞,Telstra Corporation Limited起訴Phone Directories Company Pty Ltd這一類似案件中,則裁定只有擁有明確作者的數據,才可以獲得版權。此外,在歐盟的ofir.dk起訴home.dk一案中,最終裁定定期抓取和深度鏈接是允許的。
這些案件告訴我們,當抓取的數據是現實生活中的真實數據(比如,營業地址、電話清單)時,是允許轉載的。但是,如果是原創數據(比如,意見和評論),通常就會受到版權限制,而不能轉載。
無論如何,當你抓取某個網站的數據時,請記住自己是該網站的訪客,應當約束自己的抓取行為,否則他們可能會封禁你的IP,甚至采取更進一步的法律行動。這就要求下載請求的速度需要限定在一個合理值之內,并且還需要設定一個專屬的用戶代理來標識自己。在下面的小節中我們將會對這些實踐進行具體介紹。
關于上述幾個法律案件的更多信息可以參考下述地址:
在深入討論爬取一個網站之前,我們首先需要對目標站點的規模和結構進行一定程度的了解。網站自身的robots.txt和Sitemap文件都可以為我們提供一定的幫助,此外還有一些能提供更詳細信息的外部工具,比如Google搜索和WHOIS。
大多數網站都會定義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文件,我們將在下一節中了解如何檢查該文件。
網站提供的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文件提供了一種爬取網站的有效方式,但是我們仍需對其謹慎處理,因為該文件經常存在缺失、過期或不完整的問題。
目標網站的大小會影響我們如何進行爬取。如果是像我們的示例站點這樣只有幾百個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
這種附加的過濾條件非常有用,因為在理想情況下,你只希望爬取網站中包含有用數據的部分,而不是爬取網站的每個頁面。
構建網站所使用的技術類型也會對我們如何爬取產生影響。有一個十分有用的工具可以檢查網站構建的技術類型——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,那么在爬取網頁時,就必須要用到會話管理和表單提交了。
對于一些網站,我們可能會關心其所有者是誰。比如,我們已知網站的所有者會封禁網絡爬蟲,那么我們最好把下載速度控制得更加保守一些。為了找到網站的所有者,我們可以使用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經常會阻斷網絡爬蟲,盡管實際上其自身就是一個網絡爬蟲業務。
為了抓取網站,我們首先需要下載包含有感興趣數據的網頁,該過程一般被稱為爬取(crawling)。爬取一個網站有很多種方法,而選用哪種方法更加合適,則取決于目標網站的結構。我們首先會探討如何安全地下載網頁,然后會介紹如下3種爬取網站的常見方法:
要想爬取網頁,我們首先需要將其下載下來。下面的示例腳本使用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。
下載時遇到的錯誤經常是臨時性的,比如服務器過載時返回的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/500
Download error: Internal Server Error
Downloading: http://httpstat.us/500
Download error: Internal Server Error
Downloading: http://httpstat.us/500
Download error: Internal Server Error
從上面的返回結果可以看出,download函數的行為和預期一致,先嘗試下載網頁,在接收到500錯誤后,又進行了兩次重試才放棄。
默認情況下,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
現在,我們擁有了一個靈活的下載函數,可以在后續示例中得到復用。該函數能夠捕獲異常、重試下載并設置用戶代理。
在第一個簡單的爬蟲中,我們將使用示例網站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.xml
Downloading: http://example.webscraping.com/view/Afghanistan-1
Downloading: http://example.webscraping.com/view/Aland-Islands-2
Downloading: http://example.webscraping.com/view/Albania-3
...
可以看出,上述運行結果和我們的預期一致,不過正如前文所述,我們無法依靠Sitemap文件提供每個網頁的鏈接。下面我們將會介紹另一個簡單的爬蟲,該爬蟲不再依賴于Sitemap文件。
本節中,我們將利用網站結構的弱點,更加輕松地訪問所有內容。下面是一些示例國家的URL。
可以看出,這些URL只在結尾處有所區別,包括國家名(作為頁面別名)和ID。在URL中包含頁面別名是非常普遍的做法,可以對搜索引擎優化起到幫助作用。一般情況下,Web服務器會忽略這個字符串,只使用ID來匹配數據庫中的相關記錄。下面我們將其移除,加載http://example.webscraping.com/view/1,測試示例網站中的鏈接是否仍然可用。測試結果如圖4所示。
圖4
從圖4中可以看出,網頁依然可以加載成功,也就是說該方法是有用的?,F在,我們就可以忽略頁面別名,只遍歷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的圖書進行遍歷需要測試數十億次,因此這種方法肯定不是抓取該站內容最高效的方法。
到目前為止,我們已經利用示例網站的結構特點實現了兩個簡單爬蟲,用于下載所有的國家頁面。只要這兩種技術可用,就應當使用其進行爬取,因為這兩種方法最小化了需要下載的網頁數量。不過,對于另一些網站,我們需要讓爬蟲表現得更像普通用戶,跟蹤鏈接,訪問感興趣的內容。
通過跟蹤所有鏈接的方式,我們可以很容易地下載整個網站的頁面。但是,這種方法會下載大量我們并不需要的網頁。例如,我們想要從一個在線論壇中抓取用戶賬號詳情頁,那么此時我們只需要下載賬號頁,而不需要下載討論貼的頁面。本文中的鏈接爬蟲將使用正則表達式來確定需要下載哪些頁面。下面是這段代碼的初始版本。
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和用于跟蹤鏈接的正則表達式。對于示例網站,我們想要爬取的是國家列表索引頁和國家頁面。其中,索引頁鏈接格式如下。
國家頁鏈接格式如下。
因此,我們可以用/(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文件,以避免下載禁止爬取的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//index
Downloading: http://example.webscraping.com/index/1
Downloading: http://example.webscraping.com/view/Antigua-and-Barbuda-10
Downloading: http://example.webscraping.com/view/Antarctica-9
Downloading: http://example.webscraping.com/view/Anguilla-8
Downloading: http://example.webscraping.com/view/Angola-7
Downloading: http://example.webscraping.com/view/Andorra-6
Downloading: http://example.webscraping.com/view/American-Samoa-5
Downloading: http://example.webscraping.com/view/Algeria-4
Downloading: http://example.webscraping.com/view/Albania-3
Downloading: http://example.webscraping.com/view/Aland-Islands-2
Downloading: http://example.webscraping.com/view/Afghanistan-1
和預期一樣,爬蟲在下載完國家列表的第一頁之后就停止了。
本文節選自《用Python寫網絡爬蟲》
本書講解了如何使用Python來編寫網絡爬蟲程序,內容包括網絡爬蟲簡介,從頁面中抓取數據的三種方法,提取緩存中的數據,使用多個線程和進程來進行并發抓取,如何抓取動態頁面中的內容,與表單進行交互,處理頁面中的驗證碼問題,以及使用Scarpy和Portia來進行數據抓取,并在最后使用本書介紹的數據抓取技術對幾個真實的網站進行了抓取,旨在幫助讀者活學活用書中介紹的技術。
本書適合有一定Python編程經驗,而且對爬蟲技術感興趣的讀者閱讀。
讀:本文主要分為兩個部分:一部分是網絡爬蟲的概述,幫助大家詳細了解網絡爬蟲;另一部分是HTTP請求的Python實現,幫助大家了解Python中實現HTTP請求的各種方式,以便具備編寫HTTP網絡程序的能力。
作者:范傳輝
如需轉載請聯系華章科技
接下來從網絡爬蟲的概念、用處與價值和結構等三個方面,讓大家對網絡爬蟲有一個基本的了解。
1. 網絡爬蟲及其應用
隨著網絡的迅速發展,萬維網成為大量信息的載體,如何有效地提取并利用這些信息成為一個巨大的挑戰,網絡爬蟲應運而生。網絡爬蟲(又被稱為網頁蜘蛛、網絡機器人),是一種按照一定的規則,自動地抓取萬維網信息的程序或者腳本。下面通過圖3-1展示一下網絡爬蟲在互聯網中起到的作用:
▲圖3-1 網絡爬蟲
網絡爬蟲按照系統結構和實現技術,大致可以分為以下幾種類型:通用網絡爬蟲、聚焦網絡爬蟲、增量式網絡爬蟲、深層網絡爬蟲。實際的網絡爬蟲系統通常是幾種爬蟲技術相結合實現的。
搜索引擎(Search Engine),例如傳統的通用搜索引擎baidu、Yahoo和Google等,是一種大型復雜的網絡爬蟲,屬于通用性網絡爬蟲的范疇。但是通用性搜索引擎存在著一定的局限性:
為了解決上述問題,定向抓取相關網頁資源的聚焦爬蟲應運而生。
聚焦爬蟲是一個自動下載網頁的程序,它根據既定的抓取目標,有選擇地訪問萬維網上的網頁與相關的鏈接,獲取所需要的信息。與通用爬蟲不同,聚焦爬蟲并不追求大的覆蓋,而將目標定為抓取與某一特定主題內容相關的網頁,為面向主題的用戶查詢準備數據資源。
說完了聚焦爬蟲,接下來再說一下增量式網絡爬蟲。增量式網絡爬蟲是指對已下載網頁采取增量式更新和只爬行新產生的或者已經發生變化網頁的爬蟲,它能夠在一定程度上保證所爬行的頁面是盡可能新的頁面。
和周期性爬行和刷新頁面的網絡爬蟲相比,增量式爬蟲只會在需要的時候爬行新產生或發生更新的頁面,并不重新下載沒有發生變化的頁面,可有效減少數據下載量,及時更新已爬行的網頁,減小時間和空間上的耗費,但是增加了爬行算法的復雜度和實現難度。
例如:想獲取趕集網的招聘信息,以前爬取過的數據沒有必要重復爬取,只需要獲取更新的招聘數據,這時候就要用到增量式爬蟲。
最后說一下深層網絡爬蟲。Web頁面按存在方式可以分為表層網頁和深層網頁。表層網頁是指傳統搜索引擎可以索引的頁面,以超鏈接可以到達的靜態網頁為主構成的Web頁面。深層網絡是那些大部分內容不能通過靜態鏈接獲取的、隱藏在搜索表單后的,只有用戶提交一些關鍵詞才能獲得的Web頁面。
例如用戶登錄或者注冊才能訪問的頁面??梢韵胂筮@樣一個場景:爬取貼吧或者論壇中的數據,必須在用戶登錄后,有權限的情況下才能獲取完整的數據。
2. 網絡爬蟲結構
下面用一個通用的網絡爬蟲結構來說明網絡爬蟲的基本工作流程,如圖3-4所示。
▲圖3-4 網絡爬蟲結構
網絡爬蟲的基本工作流程如下:
通過上面的網絡爬蟲結構,我們可以看到讀取URL、下載網頁是每一個爬蟲必備而且關鍵的功能,這就需要和HTTP請求打交道。接下來講解Python中實現HTTP請求的三種方式:urllib2/urllib、httplib/urllib以及Requests。
1. urllib2/urllib實現
urllib2和urllib是Python中的兩個內置模塊,要實現HTTP功能,實現方式是以urllib2為主,urllib為輔。
1.1 首先實現一個完整的請求與響應模型
urllib2提供一個基礎函數urlopen,通過向指定的URL發出請求來獲取數據。最簡單的形式是:
import urllib2 response=urllib2.urlopen('http://www.zhihu.com') html=response.read() print html
其實可以將上面對http://www.zhihu.com的請求響應分為兩步,一步是請求,一步是響應,形式如下:
import urllib2 # 請求 request=urllib2.Request('http://www.zhihu.com') # 響應 response = urllib2.urlopen(request) html=response.read() print html
上面這兩種形式都是GET請求,接下來演示一下POST請求,其實大同小異,只是增加了請求數據,這時候用到了urllib。示例如下:
import urllib import urllib2 url = 'http://www.xxxxxx.com/login' postdata = {'username' : 'qiye', 'password' : 'qiye_pass'} # info 需要被編碼為urllib2能理解的格式,這里用到的是urllib data = urllib.urlencode(postdata) req = urllib2.Request(url, data) response = urllib2.urlopen(req) html = response.read()
但是有時會出現這種情況:即使POST請求的數據是對的,但是服務器拒絕你的訪問。這是為什么呢?問題出在請求中的頭信息,服務器會檢驗請求頭,來判斷是否是來自瀏覽器的訪問,這也是反爬蟲的常用手段。
1.2 請求頭headers處理
將上面的例子改寫一下,加上請求頭信息,設置一下請求頭中的User-Agent域和Referer域信息。
import urllib import urllib2 url = 'http://www.xxxxxx.com/login' user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' referer='http://www.xxxxxx.com/' postdata = {'username' : 'qiye', 'password' : 'qiye_pass'} # 將user_agent,referer寫入頭信息 headers={'User-Agent':user_agent,'Referer':referer} data = urllib.urlencode(postdata) req = urllib2.Request(url, data,headers) response = urllib2.urlopen(req) html = response.read()
也可以這樣寫,使用add_header來添加請求頭信息,修改如下:
import urllib import urllib2 url = 'http://www.xxxxxx.com/login' user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' referer='http://www.xxxxxx.com/' postdata = {'username' : 'qiye', 'password' : 'qiye_pass'} data = urllib.urlencode(postdata) req = urllib2.Request(url) # 將user_agent,referer寫入頭信息 req.add_header('User-Agent',user_agent) req.add_header('Referer',referer) req.add_data(data) response = urllib2.urlopen(req) html = response.read()
對有些header要特別留意,服務器會針對這些header做檢查,例如:
1.3 Cookie處理
urllib2對Cookie的處理也是自動的,使用CookieJar函數進行Cookie的管理。如果需要得到某個Cookie項的值,可以這么做:
import urllib2 import cookielib cookie = cookielib.CookieJar() opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie)) response = opener.open('http://www.zhihu.com') for item in cookie: print item.name+':'+item.value
但是有時候會遇到這種情況,我們不想讓urllib2自動處理,我們想自己添加Cookie的內容,可以通過設置請求頭中的Cookie域來做:
import urllib2 opener = urllib2.build_opener() opener.addheaders.append( ( 'Cookie', 'email=' + "xxxxxxx@163.com" ) ) req = urllib2.Request( "http://www.zhihu.com/" ) response = opener.open(req) print response.headers retdata = response.read()
1.4 Timeout設置超時
在Python2.6之前的版本,urllib2的API并沒有暴露Timeout的設置,要設置Timeout值,只能更改Socket的全局Timeout值。示例如下:
import urllib2 import socket socket.setdefaulttimeout(10) # 10 秒鐘后超時 urllib2.socket.setdefaulttimeout(10) # 另一種方式
在Python2.6及新的版本中,urlopen函數提供了對Timeout的設置,示例如下:
import urllib2 request=urllib2.Request('http://www.zhihu.com') response = urllib2.urlopen(request,timeout=2) html=response.read() print html
1.5 獲取HTTP響應碼
對于200 OK來說,只要使用urlopen返回的response對象的getcode()方法就可以得到HTTP的返回碼。但對其他返回碼來說,urlopen會拋出異常。這時候,就要檢查異常對象的code屬性了,示例如下:
import urllib2 try: response = urllib2.urlopen('http://www.google.com') print response except urllib2.HTTPError as e: if hasattr(e, 'code'): print 'Error code:',e.code
1.6 重定向
urllib2默認情況下會針對HTTP 3XX返回碼自動進行重定向動作。要檢測是否發生了重定向動作,只要檢查一下Response的URL和Request的URL是否一致就可以了,示例如下:
import urllib2 response = urllib2.urlopen('http://www.zhihu.cn') isRedirected = response.geturl() == 'http://www.zhihu.cn'
如果不想自動重定向,可以自定義HTTPRedirectHandler類,示例如下:
import urllib2 class RedirectHandler(urllib2.HTTPRedirectHandler): def http_error_301(self, req, fp, code, msg, headers): pass def http_error_302(self, req, fp, code, msg, headers): result = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, msg, headers) result.status = code result.newurl = result.geturl() return result opener = urllib2.build_opener(RedirectHandler) opener.open('http://www.zhihu.cn')
1.7 Proxy的設置
在做爬蟲開發中,必不可少地會用到代理。urllib2默認會使用環境變量http_proxy來設置HTTP Proxy。但是我們一般不采用這種方式,而是使用ProxyHandler在程序中動態設置代理,示例代碼如下:
import urllib2 proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'}) opener = urllib2.build_opener([proxy,]) urllib2.install_opener(opener) response = urllib2.urlopen('http://www.zhihu.com/') print response.read()
這里要注意的一個細節,使用urllib2.install_opener()會設置urllib2的全局opener,之后所有的HTTP訪問都會使用這個代理。這樣使用會很方便,但不能做更細粒度的控制,比如想在程序中使用兩個不同的Proxy設置,這種場景在爬蟲中很常見。比較好的做法是不使用install_opener去更改全局的設置,而只是直接調用opener的open方法代替全局的urlopen方法,修改如下:
import urllib2 proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'}) opener = urllib2.build_opener(proxy,) response = opener.open("http://www.zhihu.com/") print response.read()
2. httplib/urllib實現
httplib模塊是一個底層基礎模塊,可以看到建立HTTP請求的每一步,但是實現的功能比較少,正常情況下比較少用到。在Python爬蟲開發中基本上用不到,所以在此只是進行一下知識普及。下面介紹一下常用的對象和函數:
接下來演示一下GET請求和POST請求的發送,首先是GET請求的示例,如下所示:
import httplib conn =None try: conn = httplib.HTTPConnection("www.zhihu.com") conn.request("GET", "/") response = conn.getresponse() print response.status, response.reason print '-' * 40 headers = response.getheaders() for h in headers: print h print '-' * 40 print response.msg except Exception,e: print e finally: if conn: conn.close()
POST請求的示例如下:
import httplib, urllib conn = None try: params = urllib.urlencode({'name': 'qiye', 'age': 22}) headers = {"Content-type": "application/x-www-form-urlencoded" , "Accept": "text/plain"} conn = httplib.HTTPConnection("www.zhihu.com", 80, timeout=3) conn.request("POST", "/login", params, headers) response = conn.getresponse() print response.getheaders() # 獲取頭信息 print response.status print response.read() except Exception, e: print e finally: if conn: conn.close()
3. 更人性化的Requests
Python中Requests實現HTTP請求的方式,是本人極力推薦的,也是在Python爬蟲開發中最為常用的方式。Requests實現HTTP請求非常簡單,操作更加人性化。
Requests庫是第三方模塊,需要額外進行安裝。Requests是一個開源庫,源碼位于:
GitHub: https://github.com/kennethreitz/requests
希望大家多多支持作者。
使用Requests庫需要先進行安裝,一般有兩種安裝方式:
如何驗證Requests模塊安裝是否成功呢?在Python的shell中輸入import requests,如果不報錯,則是安裝成功。如圖3-5所示。
▲圖3-5 驗證Requests安裝
3.1 首先還是實現一個完整的請求與響應模型
以GET請求為例,最簡單的形式如下:
import requests r = requests.get('http://www.baidu.com') print r.content
大家可以看到比urllib2實現方式的代碼量少。接下來演示一下POST請求,同樣是非常簡短,更加具有Python風格。示例如下:
import requests postdata={'key':'value'} r = requests.post('http://www.xxxxxx.com/login',data=postdata) print r.content
HTTP中的其他請求方式也可以用Requests來實現,示例如下:
r = requests.put('http://www.xxxxxx.com/put', data = {'key':'value'}) r = requests.delete('http://www.xxxxxx.com/delete') r = requests.head('http://www.xxxxxx.com/get') r = requests.options('http://www.xxxxxx.com/get')
接著講解一下稍微復雜的方式,大家肯定見過類似這樣的URL:
http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex=1
就是在網址后面緊跟著“?”,“?”后面還有參數。那么這樣的GET請求該如何發送呢?肯定有人會說,直接將完整的URL帶入即可,不過Requests還提供了其他方式,示例如下:
import requests payload = {'Keywords': 'blog:qiyeboy','pageindex':1} r = requests.get('http://zzk.cnblogs.com/s/blogpost', params=payload) print r.url
通過打印結果,我們看到最終的URL變成了:
http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex=1
3.2 響應與編碼
還是從代碼入手,示例如下:
import requests r = requests.get('http://www.baidu.com') print 'content-->'+r.content print 'text-->'+r.text print 'encoding-->'+r.encoding r.encoding='utf-8' print 'new text-->'+r.text
其中r.content返回的是字節形式,r.text返回的是文本形式,r.encoding返回的是根據HTTP頭猜測的網頁編碼格式。
輸出結果中:“text-->”之后的內容在控制臺看到的是亂碼,“encoding-->”之后的內容是ISO-8859-1(實際上的編碼格式是UTF-8),由于Requests猜測編碼錯誤,導致解析文本出現了亂碼。Requests提供了解決方案,可以自行設置編碼格式,r.encoding='utf-8'設置成UTF-8之后,“new text-->”的內容就不會出現亂碼。
但是這種手動的方式略顯笨拙,下面提供一種更加簡便的方式:chardet,這是一個非常優秀的字符串/文件編碼檢測模塊。安裝方式如下:
pip install chardet
安裝完成后,使用chardet.detect()返回字典,其中confidence是檢測精確度,encoding是編碼形式。示例如下:
import requests r = requests.get('http://www.baidu.com') print chardet.detect(r.content) r.encoding = chardet.detect(r.content)['encoding'] print r.text
直接將chardet探測到的編碼,賦給r.encoding實現解碼,r.text輸出就不會有亂碼了。
除了上面那種直接獲取全部響應的方式,還有一種流模式,示例如下:
import requests r = requests.get('http://www.baidu.com',stream=True) print r.raw.read(10)
設置stream=True標志位,使響應以字節流方式進行讀取,r.raw.read函數指定讀取的字節數。
3.3 請求頭headers處理
Requests對headers的處理和urllib2非常相似,在Requests的get函數中添加headers參數即可。示例如下:
import requests user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers={'User-Agent':user_agent} r = requests.get('http://www.baidu.com',headers=headers) print r.content
3.4 響應碼code和響應頭headers處理
獲取響應碼是使用Requests中的status_code字段,獲取響應頭使用Requests中的headers字段。示例如下:
import requests r = requests.get('http://www.baidu.com') if r.status_code == requests.codes.ok: print r.status_code# 響應碼 print r.headers# 響應頭 print r.headers.get('content-type')# 推薦使用這種獲取方式,獲取其中的某個字段 print r.headers['content-type']# 不推薦使用這種獲取方式 else: r.raise_for_status()
上述程序中,r.headers包含所有的響應頭信息,可以通過get函數獲取其中的某一個字段,也可以通過字典引用的方式獲取字典值,但是不推薦,因為如果字段中沒有這個字段,第二種方式會拋出異常,第一種方式會返回None。
r.raise_for_status()是用來主動地產生一個異常,當響應碼是4XX或5XX時,raise_for_status()函數會拋出異常,而響應碼為200時,raise_for_status()函數返回None。
3.5 Cookie處理
如果響應中包含Cookie的值,可以如下方式獲取Cookie字段的值,示例如下:
import requests user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers={'User-Agent':user_agent} r = requests.get('http://www.baidu.com',headers=headers) # 遍歷出所有的cookie字段的值 for cookie in r.cookies.keys(): print cookie+':'+r.cookies.get(cookie)
如果想自定義Cookie值發送出去,可以使用以下方式,示例如下:
import requests user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers={'User-Agent':user_agent} cookies = dict(name='qiye',age='10') r = requests.get('http://www.baidu.com',headers=headers,cookies=cookies) print r.text
還有一種更加高級,且能自動處理Cookie的方式,有時候我們不需要關心Cookie值是多少,只是希望每次訪問的時候,程序自動把Cookie的值帶上,像瀏覽器一樣。Requests提供了一個session的概念,在連續訪問網頁,處理登錄跳轉時特別方便,不需要關注具體細節。使用方法示例如下:
import Requests oginUrl = 'http://www.xxxxxxx.com/login' s = requests.Session() #首先訪問登錄界面,作為游客,服務器會先分配一個cookie r = s.get(loginUrl,allow_redirects=True) datas={'name':'qiye','passwd':'qiye'} #向登錄鏈接發送post請求,驗證成功,游客權限轉為會員權限 r = s.post(loginUrl, data=datas,allow_redirects= True) print r.text
上面的這段程序,其實是正式做Python開發中遇到的問題,如果沒有第一步訪問登錄的頁面,而是直接向登錄鏈接發送Post請求,系統會把你當做非法用戶,因為訪問登錄界面時會分配一個Cookie,需要將這個Cookie在發送Post請求時帶上,這種使用Session函數處理Cookie的方式之后會很常用。
3.6 重定向與歷史信息
處理重定向只是需要設置一下allow_redirects字段即可,例如:
r=requests.get('http://www.baidu.com',allow_redirects=True)
將allow_redirects設置為True,則是允許重定向;設置為False,則是禁止重定向。如果是允許重定向,可以通過r.history字段查看歷史信息,即訪問成功之前的所有請求跳轉信息。示例如下:
import requests r = requests.get('http://github.com') print r.url print r.status_code print r.history
打印結果如下:
https://github.com/ 200 (<Response [301]>,)
上面的示例代碼顯示的效果是訪問GitHub網址時,會將所有的HTTP請求全部重定向為HTTPS。
3.7 超時設置
超時選項是通過參數timeout來進行設置的,示例如下:
requests.get('http://github.com', timeout=2)
3.8 代理設置
使用代理Proxy,你可以為任意請求方法通過設置proxies參數來配置單個請求:
import requests proxies = { "http": "http://0.10.1.10:3128", "https": "http://10.10.1.10:1080", } requests.get("http://example.org", proxies=proxies)
也可以通過環境變量HTTP_PROXY和HTTPS_PROXY?來配置代理,但是在爬蟲開發中不常用。你的代理需要使用HTTP Basic Auth,可以使用http://user:password@host/語法:
proxies = { "http": "http://user:pass@10.10.1.10:3128/", }
本文主要講解了網絡爬蟲的結構和應用,以及Python實現HTTP請求的幾種方法。希望大家對本文中的網絡爬蟲工作流程和Requests實現HTTP請求的方式重點吸收消化。
關于作者:范傳輝,資深網蟲,Python開發者,參與開發了多項網絡應用,在實際開發中積累了豐富的實戰經驗,并善于總結,貢獻了多篇技術文章廣受好評。研究興趣是網絡安全、爬蟲技術、數據分析、驅動開發等技術。
本文摘編自《Python爬蟲開發與項目實戰》,經出版方授權發布。
延伸閱讀《Python爬蟲開發與項目實戰》
推薦語:零基礎學習爬蟲技術,從Python和Web前端基礎開始講起,由淺入深,包含大量案例,實用性強。
多程序員新人想了解爬蟲的實現,然而,網絡上的許多信息是教大家如何爬蟲工具。工具的使用對于快速完成頁面爬取工作是有幫助的,但不利于大家掌握爬蟲的原理。
本文將帶大家從最基本和最本質的途徑去編寫一個爬蟲,讓大家真正了解爬蟲的工作原理。并且,能夠在此基礎上根據自身需求改造出需要的爬蟲。
在開始爬蟲工作之前,我們先了解下,什么是網頁。
以某網頁為例,其展示效果如下:、
我們在網頁上點擊右鍵,“查看源代碼”可以看到網頁的代碼信息,如下:
其中有很多文本,也有很多鏈接。這些鏈接有的指向另一個頁面,有的指向css文件、js文件、圖片文件等。
分類一下,網頁一共包含以下幾個部分,如圖所示。
各個部分的含義如下:
爬蟲就是主要爬取HTML部分。因為HTML中包含了主要的信息。
而爬蟲的一個重要特點就是順藤摸瓜——根據鏈接,從一個網頁跳轉到另一個網頁,不斷進行。從而獲取眾多網頁的信息。
那實現爬蟲,要完成的基本功能是:
基于此,我們可以寫出一個爬蟲的核心偽代碼。
爬蟲(網頁地址)
爬取某個頁面
分析頁面中的鏈接
使用分析得到的鏈接再次調用方法 爬蟲(網頁地址)
對的,你沒看錯,就是這么簡單。
其中有兩個功能需要實現,即爬取某個頁面、分析頁面中的鏈接。
接下來我們分別介紹這兩個功能的實現。
這個功能使用Python實現起來比較簡單,只要打開一個文本,然后將網絡某地址的信息寫入文本就算是爬取完成了。
實現代碼如下。
htmlFile=open('./output/'+(str(pageId)+'.txt'),'w')
htmlFile.write(urllib.urlopen(url).read())
htmlFile.close()
該工作需要正則表達式的幫助,'href="[^(javascript)]\S*[^(#)(css)(js)(ico)]\"'可以幫助我們匹配出網頁中的鏈接。
pattern=re.compile('href="[^(javascript)]\S*[^(#)(css)(js)(ico)]\"')
htmlFile=open('./output/'+(str(pageId)+'.txt'),'r')
for line in htmlFile:
ans=re.findall(pattern,line)
有了偽代碼和兩個核心功能的實現代碼后,我們可以直接寫出爬蟲的主要代碼:
htmlFile=open('./output/'+(str(pageId)+'.txt'),'w')
htmlFile.write(urllib.urlopen(url).read())
htmlFile.close()
htmlFile=open('./output/'+(str(pageId)+'.txt'),'r')
for line in htmlFile:
ans=re.findall(pattern,line)
for one in ans :
urlTail=one.split('"')[1]
url=urlparse.urljoin(url,urlTail)
if urlMap.has_key(url):
print 'skip---'+url
else:
print 'download---'+url
pageId += 1
urlMap[url]=pageId
idMap[pageId]=url
catchFile=open('./output/'+(str(urlMap[url])+'.txt'),'w')
try:
catchFile.write(urllib.urlopen(url).read())
except:
pass
finally:
catchFile.close()
htmlFile.close()
即爬取某個頁面、分析頁面中的鏈接、繼續下載。
當然,在這個過程中,有幾點要注意:
1、遇到死鏈要跳過,不要一直卡在那里
2、凡是下載過的頁面不要重復下載,否則可能形成環路,永無止境
只要注意了以上兩點,就可以寫出爬蟲。
我直接給出爬蟲的代碼,放在下面的開源地址上。
https://github.com/yeecode/EasyCrawler
該爬蟲十分基礎、簡單、容易理解,就是上面偽代碼的直接實現。
該爬蟲的基本功能如下:
整個示例極少依賴外部項目,十分簡單、易懂、純粹。因此該項目不僅便于學習,也便于在此基礎上擴展新功能。
基于以上功能,我們可以修改實現眾多其他功能,包括但不限于:
好了,希望能夠幫到題主。也希望大家能在此基礎上實現更酷炫的功能。
往期精彩文章:
歡迎關注我們,不錯過軟件架構和編程方面的干貨知識。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。