原文傳送門:http://blog.csdn.net/column/details/why-bug.html
[Python]網絡爬蟲(一):抓取網頁的含義和URL基本構成
一、網絡爬蟲的定義
網絡爬蟲,即Web Spider,是一個很形象的名字。
把互聯網比喻成一個蜘蛛網,那麼Spider就是在網上爬來爬去的蜘蛛。
網絡蜘蛛是通過網頁的鏈接地址來尋找網頁的。
從網站某一個頁面(通常是首頁)開始,讀取網頁的內容,找到在網頁中的其它鏈接地址,
然後通過這些鏈接地址尋找下一個網頁,這樣一直循環下去,直到把這個網站所有的網頁都抓取完爲止。
如果把整個互聯網當成一個網站,那麼網絡蜘蛛就可以用這個原理把互聯網上所有的網頁都抓取下來。
這樣看來,網絡爬蟲就是一個爬行程序,一個抓取網頁的程序。
網絡爬蟲的基本操作是抓取網頁。
那麼如何才能隨心所欲地獲得自己想要的頁面?
我們先從URL開始。
二、瀏覽網頁的過程
抓取網頁的過程其實和讀者平時使用IE瀏覽器瀏覽網頁的道理是一樣的。
比如說你在瀏覽器的地址欄中輸入 www.baidu.com 這個地址。
打開網頁的過程其實就是瀏覽器作爲一個瀏覽的“客戶端”,向服務器端發送了 一次請求,把服務器端的文件“抓”到本地,再進行解釋、展現。
HTML是一種標記語言,用標籤標記內容並加以解析和區分。
三、URI和URL的概念和舉例
簡單的來講,URL就是在瀏覽器端輸入的 http://www.baidu.com 這個字符串。
在理解URL之前,首先要理解URI的概念。
什麼是URI?
Web上每種可用的資源,如 HTML文檔、圖像、視頻片段、程序等都由一個通用資源標誌符(Universal Resource Identifier, URI)進行定位。
URI通常由三部分組成:
①訪問資源的命名機制;
②存放資源的主機名;
③資源自身 的名稱,由路徑表示。
如下面的URI:
http://www.why.com.cn/myhtml/html1223/
我們可以這樣解釋它:
①這是一個可以通過HTTP協議訪問的資源,
②位於主機 www.webmonkey.com.cn上,
③通過路徑“/html/html40”訪問。
四、URL的理解和舉例
URL是URI的一個子集。它是Uniform Resource Locator的縮寫,譯爲“統一資源定位 符”。
通俗地說,URL是Internet上描述信息資源的字符串,主要用在各種WWW客戶程序和服務器程序上。
採用URL可以用一種統一的格式來描述各種信息資源,包括文件、服務器的地址和目錄等。
URL的一般格式爲(帶方括號[]的爲可選項):
protocol :// hostname[:port] / path / [;parameters][?query]#fragment
URL的格式由三部分組成:
①第一部分是協議(或稱爲服務方式)。
②第二部分是存有該資源的主機IP地址(有時也包括端口號)。
③第三部分是主機資源的具體地址,如目錄和文件名等。
第一部分和第二部分用“://”符號隔開,
第二部分和第三部分用“/”符號隔開。
第一部分和第二部分是不可缺少的,第三部分有時可以省略。
五、URL和URI簡單比較
URI屬於URL更低層次的抽象,一種字符串文本標準。
換句話說,URI屬於父類,而URL屬於URI的子類。URL是URI的一個子集。
URI的定義是:統一資源標識符;
URL的定義是:統一資源定位符。
二者的區別在於,URI表示請求服務器的路徑,定義這麼一個資源。
而URL同時說明要如何訪問這個資源(http://)。
下面來看看兩個URL的小例子。
1.HTTP協議的URL示例:
使用超級文本傳輸協議HTTP,提供超級文本信息服務的資源。
例:http://www.peopledaily.com.cn/channel/welcome.htm
其計算機域名爲www.peopledaily.com.cn。
超級文本文件(文件類型爲.html)是在目錄 /channel下的welcome.htm。
這是中國人民日報的一臺計算機。
例:http://www.rol.cn.net/talk/talk1.htm
其計算機域名爲www.rol.cn.net。
超級文本文件(文件類型爲.html)是在目錄/talk下的talk1.htm。
這是瑞得聊天室的地址,可由此進入瑞得聊天室的第1室。
2.文件的URL
用URL表示文件時,服務器方式用file表示,後面要有主機IP地址、文件的存取路 徑(即目錄)和文件名等信息。
有時可以省略目錄和文件名,但“/”符號不能省略。
例:file://ftp.yoyodyne.com/pub/files/foobar.txt
上面這個URL代表存放在主機ftp.yoyodyne.com上的pub/files/目錄下的一個文件,文件名是foobar.txt。
例:file://ftp.yoyodyne.com/pub
代表主機ftp.yoyodyne.com上的目錄/pub。
例:file://ftp.yoyodyne.com/
代表主機ftp.yoyodyne.com的根目錄。
爬蟲最主要的處理對象就是URL,它根據URL地址取得所需要的文件內容,然後對它 進行進一步的處理。
因此,準確地理解URL對理解網絡爬蟲至關重要。
[Python]網絡爬蟲(二):利用urllib2通過指定的URL抓取網頁內容
版本號:Python2.7.5,Python3改動較大,各位另尋教程。
所謂網頁抓取,就是把URL地址中指定的網絡資源從網絡流中讀取出來,保存到本地。
類似於使用程序模擬IE瀏覽器的功能,把URL作爲HTTP請求的內容發送到服務器端, 然後讀取服務器端的響應資源。
在Python中,我們使用urllib2這個組件來抓取網頁。
urllib2是Python的一個獲取URLs(Uniform Resource Locators)的組件。
它以urlopen函數的形式提供了一個非常簡單的接口。
最簡單的urllib2的應用代碼只需要四行。
我們新建一個文件urllib2_test01.py來感受一下urllib2的作用:
- import urllib2
- response = urllib2.urlopen('http://www.baidu.com/')
- html = response.read()
- print html
按下F5可以看到運行的結果:
我們可以打開百度主頁,右擊,選擇查看源代碼(火狐OR谷歌瀏覽器均可),會發現也是完全一樣的內容。
也就是說,上面這四行代碼將我們訪問百度時瀏覽器收到的代碼們全部打印了出來。
這就是一個最簡單的urllib2的例子。
除了"http:",URL同樣可以使用"ftp:","file:"等等來替代。
HTTP是基於請求和應答機制的:
客戶端提出請求,服務端提供應答。
urllib2用一個Request對象來映射你提出的HTTP請求。
在它最簡單的使用形式中你將用你要請求的地址創建一個Request對象,
通過調用urlopen並傳入Request對象,將返回一個相關請求response對象,
這個應答對象如同一個文件對象,所以你可以在Response中調用.read()。
我們新建一個文件urllib2_test02.py來感受一下:
- import urllib2
- req = urllib2.Request('http://www.baidu.com')
- response = urllib2.urlopen(req)
- the_page = response.read()
- print the_page
可以看到輸出的內容和test01是一樣的。
urllib2使用相同的接口處理所有的URL頭。例如你可以像下面那樣創建一個ftp請求。
- req = urllib2.Request('ftp://example.com/')
1.發送data表單數據
這個內容相信做過Web端的都不會陌生,
有時候你希望發送一些數據到URL(通常URL與CGI[通用網關接口]腳本,或其他WEB應用程序掛接)。
在HTTP中,這個經常使用熟知的POST請求發送。
這個通常在你提交一個HTML表單時由你的瀏覽器來做。
並不是所有的POSTs都來源於表單,你能夠使用POST提交任意的數據到你自己的程序。
一般的HTML表單,data需要編碼成標準形式。然後做爲data參數傳到Request對象。
編碼工作使用urllib的函數而非urllib2。
我們新建一個文件urllib2_test03.py來感受一下:
- import urllib
- import urllib2
- url = 'http://www.someserver.com/register.cgi'
- values = {'name' : 'WHY',
- 'location' : 'SDU',
- 'language' : 'Python' }
- data = urllib.urlencode(values) # 編碼工作
- req = urllib2.Request(url, data) # 發送請求同時傳data表單
- response = urllib2.urlopen(req) #接受反饋的信息
- the_page = response.read() #讀取反饋的內容
如果沒有傳送data參數,urllib2使用GET方式的請求。
GET和POST請求的不同之處是POST請求通常有"副作用",
它們會由於某種途徑改變系統狀態(例如提交成堆垃圾到你的門口)。
Data同樣可以通過在Get請求的URL本身上面編碼來傳送。
- import urllib2
- import urllib
- data = {}
- data['name'] = 'WHY'
- data['location'] = 'SDU'
- data['language'] = 'Python'
- url_values = urllib.urlencode(data)
- print url_values
- name=Somebody+Here&language=Python&location=Northampton
- url = 'http://www.example.com/example.cgi'
- full_url = url + '?' + url_values
- data = urllib2.open(full_url)
這樣就實現了Data數據的Get傳送。
2.設置Headers到http請求
有一些站點不喜歡被程序(非人爲訪問)訪問,或者發送不同版本的內容到不同的瀏覽器。
默認的urllib2把自己作爲“Python-urllib/x.y”(x和y是Python主版本和次版本號,例如Python-urllib/2.7),
這個身份可能會讓站點迷惑,或者乾脆不工作。
瀏覽器確認自己身份是通過User-Agent頭,當你創建了一個請求對象,你可以給他一個包含頭數據的字典。
下面的例子發送跟上面一樣的內容,但把自身模擬成Internet Explorer。
(多謝大家的提醒,現在這個Demo已經不可用了,不過原理還是那樣的)。
- import urllib
- import urllib2
- url = 'http://www.someserver.com/cgi-bin/register.cgi'
- user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
- values = {'name' : 'WHY',
- 'location' : 'SDU',
- 'language' : 'Python' }
- headers = { 'User-Agent' : user_agent }
- data = urllib.urlencode(values)
- req = urllib2.Request(url, data, headers)
- response = urllib2.urlopen(req)
- the_page = response.read()
先來說一說HTTP的異常處理問題。
當urlopen不能夠處理一個response時,產生urlError。
不過通常的Python APIs異常如ValueError,TypeError等也會同時產生。
HTTPError是urlError的子類,通常在特定HTTP URLs中產生。
1.URLError
通常,URLError在沒有網絡連接(沒有路由到特定服務器),或者服務器不存在的情況下產生。
這種情況下,異常同樣會帶有"reason"屬性,它是一個tuple(可以理解爲不可變的數組),
包含了一個錯誤號和一個錯誤信息。
我們建一個urllib2_test06.py來感受一下異常的處理:
- import urllib2
- req = urllib2.Request('http://www.baibai.com')
- try: urllib2.urlopen(req)
- except urllib2.URLError, e:
- print e.reason
按下F5,可以看到打印出來的內容是:
[Errno 11001] getaddrinfo failed
也就是說,錯誤號是11001,內容是getaddrinfo failed
2.HTTPError
服務器上每一個HTTP 應答對象response包含一個數字"狀態碼"。
有時狀態碼指出服務器無法完成請求。默認的處理器會爲你處理一部分這種應答。
例如:假如response是一個"重定向",需要客戶端從別的地址獲取文檔,urllib2將爲你處理。
其他不能處理的,urlopen會產生一個HTTPError。
典型的錯誤包含"404"(頁面無法找到),"403"(請求禁止),和"401"(帶驗證請求)。
HTTP狀態碼錶示HTTP協議所返回的響應的狀態。
比如客戶端向服務器發送請求,如果成功地獲得請求的資源,則返回的狀態碼爲200,表示響應成功。
如果請求的資源不存在, 則通常返回404錯誤。
HTTP狀態碼通常分爲5種類型,分別以1~5五個數字開頭,由3位整數組成:
------------------------------------------------------------------------------------------------
200:請求成功 處理方式:獲得響應的內容,進行處理
201:請求完成,結果是創建了新資源。新創建資源的URI可在響應的實體中得到 處理方式:爬蟲中不會遇到
202:請求被接受,但處理尚未完成 處理方式:阻塞等待
204:服務器端已經實現了請求,但是沒有返回新的信 息。如果客戶是用戶代理,則無須爲此更新自身的文檔視圖。 處理方式:丟棄
300:該狀態碼不被HTTP/1.0的應用程序直接使用, 只是作爲3XX類型迴應的默認解釋。存在多個可用的被請求資源。 處理方式:若程序中能夠處理,則進行進一步處理,如果程序中不能處理,則丟棄
301:請求到的資源都會分配一個永久的URL,這樣就可以在將來通過該URL來訪問此資源 處理方式:重定向到分配的URL
302:請求到的資源在一個不同的URL處臨時保存 處理方式:重定向到臨時的URL
304 請求的資源未更新 處理方式:丟棄
400 非法請求 處理方式:丟棄
401 未授權 處理方式:丟棄
403 禁止 處理方式:丟棄
404 沒有找到 處理方式:丟棄
5XX 迴應代碼以“5”開頭的狀態碼錶示服務器端發現自己出現錯誤,不能繼續執行請求 處理方式:丟棄
------------------------------------------------------------------------------------------------
Error Codes錯誤碼
因爲默認的處理器處理了重定向(300以外號碼),並且100-299範圍的號碼指示成功,所以你只能看到400-599的錯誤號碼。
BaseHTTPServer.BaseHTTPRequestHandler.response是一個很有用的應答號碼字典,顯示了HTTP協議使用的所有的應答號。
當一個錯誤號產生後,服務器返回一個HTTP錯誤號,和一個錯誤頁面。
你可以使用HTTPError實例作爲頁面返回的應答對象response。
這表示和錯誤屬性一樣,它同樣包含了read,geturl,和info方法。
我們建一個urllib2_test07.py來感受一下:
- import urllib2
- req = urllib2.Request('http://bbs.csdn.net/callmewhy')
- try:
- urllib2.urlopen(req)
- except urllib2.URLError, e:
- print e.code
- #print e.read()
按下F5可以看見輸出了404的錯誤碼,也就說沒有找到這個頁面。
3.Wrapping
所以如果你想爲HTTPError或URLError做準備,將有兩個基本的辦法。推薦使用第二種。
我們建一個urllib2_test08.py來示範一下第一種異常處理的方案:
- from urllib2 import Request, urlopen, URLError, HTTPError
- req = Request('http://bbs.csdn.net/callmewhy')
- try:
- response = urlopen(req)
- except HTTPError, e:
- print 'The server couldn\'t fulfill the request.'
- print 'Error code: ', e.code
- except URLError, e:
- print 'We failed to reach a server.'
- print 'Reason: ', e.reason
- else:
- print 'No exception was raised.'
- # everything is fine
和其他語言相似,try之後捕獲異常並且將其內容打印出來。
因爲HTTPError是URLError的子類,如果URLError在前面它會捕捉到所有的URLError(包括HTTPError )。
我們建一個urllib2_test09.py來示範一下第二種異常處理的方案:
- from urllib2 import Request, urlopen, URLError, HTTPError
- req = Request('http://bbs.csdn.net/callmewhy')
- try:
- response = urlopen(req)
- except URLError, e:
- if hasattr(e, 'reason'):
- print 'We failed to reach a server.'
- print 'Reason: ', e.reason
- elif hasattr(e, 'code'):
- print 'The server couldn\'t fulfill the request.'
- print 'Error code: ', e.code
- else:
- print 'No exception was raised.'
- # everything is fine
在開始後面的內容之前,先來解釋一下urllib2中的兩個個方法:info and geturl
urlopen返回的應答對象response(或者HTTPError實例)有兩個很有用的方法info()和geturl()1.geturl():
這個返回獲取的真實的URL,這個很有用,因爲urlopen(或者opener對象使用的)或許會有重定向。獲取的URL或許跟請求URL不同。
以人人中的一個超級鏈接爲例,
我們建一個urllib2_test10.py來比較一下原始URL和重定向的鏈接:
- from urllib2 import Request, urlopen, URLError, HTTPError
- old_url = 'http://rrurl.cn/b1UZuP'
- req = Request(old_url)
- response = urlopen(req)
- print 'Old url :' + old_url
- print 'Real url :' + response.geturl()
2.info():
這個返回對象的字典對象,該字典描述了獲取的頁面情況。通常是服務器發送的特定頭headers。目前是httplib.HTTPMessage 實例。
經典的headers包含"Content-length","Content-type",和其他內容。
我們建一個urllib2_test11.py來測試一下info的應用:
- from urllib2 import Request, urlopen, URLError, HTTPError
- old_url = 'http://www.baidu.com'
- req = Request(old_url)
- response = urlopen(req)
- print 'Info():'
- print response.info()
下面來說一說urllib2中的兩個重要概念:Openers和Handlers。
1.Openers:
當你獲取一個URL你使用一個opener(一個urllib2.OpenerDirector的實例)。
正常情況下,我們使用默認opener:通過urlopen。
但你能夠創建個性的openers。
2.Handles:
Openers使用處理器handlers,所有的“繁重”工作由handlers處理。
每個handlers知道如何通過特定協議打開URLs,或者如何處理URL打開時的各個方面。
例如HTTP重定向或者HTTP cookies。
如果你希望用特定處理器獲取URLs你會想創建一個openers,例如獲取一個能處理cookie的opener,或者獲取一個不重定向的opener。
要創建一個 opener,可以實例化一個OpenerDirector,
然後調用.add_handler(some_handler_instance)。
同樣,可以使用build_opener,這是一個更加方便的函數,用來創建opener對象,他只需要一次函數調用。build_opener默認添加幾個處理器,但提供快捷的方法來添加或更新默認處理器。
其他的處理器handlers你或許會希望處理代理,驗證,和其他常用但有點特殊的情況。
Opener對象有一個open方法。
該方法可以像urlopen函數那樣直接用來獲取urls:通常不必調用install_opener,除了爲了方便。
說完了上面兩個內容,下面我們來看一下基本認證的內容,這裏會用到上面提及的Opener和Handler。
Basic Authentication 基本驗證爲了展示創建和安裝一個handler,我們將使用HTTPBasicAuthHandler。
當需要基礎驗證時,服務器發送一個header(401錯誤碼) 請求驗證。這個指定了scheme 和一個‘realm’,看起來像這樣:Www-authenticate: SCHEME realm="REALM".
例如Www-authenticate: Basic realm="cPanel Users"
客戶端必須使用新的請求,並在請求頭裏包含正確的姓名和密碼。
這是“基礎驗證”,爲了簡化這個過程,我們可以創建一個HTTPBasicAuthHandler的實例,並讓opener使用這個handler就可以啦。
HTTPBasicAuthHandler使用一個密碼管理的對象來處理URLs和realms來映射用戶名和密碼。
如果你知道realm(從服務器發送來的頭裏)是什麼,你就能使用HTTPPasswordMgr。
通常人們不關心realm是什麼。那樣的話,就能用方便的HTTPPasswordMgrWithDefaultRealm。
這個將在你爲URL指定一個默認的用戶名和密碼。
這將在你爲特定realm提供一個其他組合時得到提供。
我們通過給realm參數指定None提供給add_password來指示這種情況。
最高層次的URL是第一個要求驗證的URL。你傳給.add_password()更深層次的URLs將同樣合適。
說了這麼多廢話,下面來用一個例子演示一下上面說到的內容。
我們建一個urllib2_test12.py來測試一下info的應用:
- # -*- coding: utf-8 -*-
- import urllib2
- # 創建一個密碼管理者
- password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
- # 添加用戶名和密碼
- top_level_url = "http://example.com/foo/"
- # 如果知道 realm, 我們可以使用他代替 ``None``.
- # password_mgr.add_password(None, top_level_url, username, password)
- password_mgr.add_password(None, top_level_url,'why', '1223')
- # 創建了一個新的handler
- handler = urllib2.HTTPBasicAuthHandler(password_mgr)
- # 創建 "opener" (OpenerDirector 實例)
- opener = urllib2.build_opener(handler)
- a_url = 'http://www.baidu.com/'
- # 使用 opener 獲取一個URL
- opener.open(a_url)
- # 安裝 opener.
- # 現在所有調用 urllib2.urlopen 將用我們的 opener.
- urllib2.install_opener(opener)
注意:以上的例子我們僅僅提供我們的HHTPBasicAuthHandler給build_opener。
默認的openers有正常狀況的handlers:ProxyHandler,UnknownHandler,HTTPHandler,HTTPDefaultErrorHandler, HTTPRedirectHandler,FTPHandler, FileHandler, HTTPErrorProcessor。
代碼中的top_level_url 實際上可以是完整URL(包含"http:",以及主機名及可選的端口號)。
例如:http://example.com/。
也可以是一個“authority”(即主機名和可選的包含端口號)。
例如:“example.com” or “example.com:8080”。
後者包含了端口號。
[Python]網絡爬蟲(五):urllib2的使用細節與抓站技巧
前面說到了urllib2的簡單入門,下面整理了一部分urllib2的使用細節。
1.Proxy 的設置
urllib2 默認會使用環境變量 http_proxy 來設置 HTTP Proxy。
如果想在程序中明確控制 Proxy 而不受環境變量的影響,可以使用代理。
新建test14來實現一個簡單的代理Demo:
- import urllib2
- enable_proxy = True
- proxy_handler = urllib2.ProxyHandler({"http" : 'http://some-proxy.com:8080'})
- null_proxy_handler = urllib2.ProxyHandler({})
- if enable_proxy:
- opener = urllib2.build_opener(proxy_handler)
- else:
- opener = urllib2.build_opener(null_proxy_handler)
- urllib2.install_opener(opener)
這裏要注意的一個細節,使用 urllib2.install_opener() 會設置 urllib2 的全局 opener 。
這樣後面的使用會很方便,但不能做更細緻的控制,比如想在程序中使用兩個不同的 Proxy 設置等。
比較好的做法是不使用 install_opener 去更改全局的設置,而只是直接調用 opener 的 open 方法代替全局的 urlopen 方法。
2.Timeout 設置
在老版 Python 中(Python2.6前),urllib2 的 API 並沒有暴露 Timeout 的設置,要設置 Timeout 值,只能更改 Socket 的全局 Timeout 值。
- import urllib2
- import socket
- socket.setdefaulttimeout(10) # 10 秒鐘後超時
- urllib2.socket.setdefaulttimeout(10) # 另一種方式
在 Python 2.6 以後,超時可以通過 urllib2.urlopen() 的 timeout 參數直接設置。
- import urllib2
- response = urllib2.urlopen('http://www.google.com', timeout=10)
3.在 HTTP Request 中加入特定的 Header
要加入 header,需要使用 Request 對象:- import urllib2
- request = urllib2.Request('http://www.baidu.com/')
- request.add_header('User-Agent', 'fake-client')
- response = urllib2.urlopen(request)
- print response.read()
對有些 header 要特別留意,服務器會針對這些 header 做檢查
User-Agent : 有些服務器或 Proxy 會通過該值來判斷是否是瀏覽器發出的請求
Content-Type : 在使用 REST 接口時,服務器會檢查該值,用來確定 HTTP Body 中的內容該怎樣解析。常見的取值有:
application/xml : 在 XML RPC,如 RESTful/SOAP 調用時使用
application/json : 在 JSON RPC 調用時使用
application/x-www-form-urlencoded : 瀏覽器提交 Web 表單時使用
在使用服務器提供的 RESTful 或 SOAP 服務時, Content-Type 設置錯誤會導致服務器拒絕服務
urllib2 默認情況下會針對 HTTP 3XX 返回碼自動進行 redirect 動作,無需人工配置。要檢測是否發生了 redirect 動作,只要檢查一下 Response 的 URL 和 Request 的 URL 是否一致就可以了。
- import urllib2
- my_url = 'http://www.google.cn'
- response = urllib2.urlopen(my_url)
- redirected = response.geturl() == my_url
- print redirected
- my_url = 'http://rrurl.cn/b1UZuP'
- response = urllib2.urlopen(my_url)
- redirected = response.geturl() == my_url
- print redirected
如果不想自動 redirect,除了使用更低層次的 httplib 庫之外,還可以自定義HTTPRedirectHandler 類。
- import urllib2
- class RedirectHandler(urllib2.HTTPRedirectHandler):
- def http_error_301(self, req, fp, code, msg, headers):
- print "301"
- pass
- def http_error_302(self, req, fp, code, msg, headers):
- print "303"
- pass
- opener = urllib2.build_opener(RedirectHandler)
- opener.open('http://rrurl.cn/b1UZuP')
5.Cookie
urllib2 對 Cookie 的處理也是自動的。如果需要得到某個 Cookie 項的值,可以這麼做:- import urllib2
- import cookielib
- cookie = cookielib.CookieJar()
- opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
- response = opener.open('http://www.baidu.com')
- for item in cookie:
- print 'Name = '+item.name
- print 'Value = '+item.value
運行之後就會輸出訪問百度的Cookie值:
6.使用 HTTP 的 PUT 和 DELETE 方法
urllib2 只支持 HTTP 的 GET 和 POST 方法,如果要使用 HTTP PUT 和 DELETE ,只能使用比較低層的 httplib 庫。雖然如此,我們還是能通過下面的方式,使 urllib2 能夠發出 PUT 或DELETE 的請求:- import urllib2
- request = urllib2.Request(uri, data=data)
- request.get_method = lambda: 'PUT' # or 'DELETE'
- response = urllib2.urlopen(request)
7.得到 HTTP 的返回碼
對於 200 OK 來說,只要使用 urlopen 返回的 response 對象的 getcode() 方法就可以得到 HTTP 的返回碼。但對其它返回碼來說,urlopen 會拋出異常。這時候,就要檢查異常對象的 code 屬性了:- import urllib2
- try:
- response = urllib2.urlopen('http://bbs.csdn.net/why')
- except urllib2.HTTPError, e:
- print e.code
8.Debug Log
使用 urllib2 時,可以通過下面的方法把 debug Log 打開,這樣收發包的內容就會在屏幕上打印出來,方便調試,有時可以省去抓包的工作- import urllib2
- httpHandler = urllib2.HTTPHandler(debuglevel=1)
- httpsHandler = urllib2.HTTPSHandler(debuglevel=1)
- opener = urllib2.build_opener(httpHandler, httpsHandler)
- urllib2.install_opener(opener)
- response = urllib2.urlopen('http://www.google.com')
這樣就可以看到傳輸的數據包內容了:
9.表單的處理
登錄必要填表,表單怎麼填?
首先利用工具截取所要填表的內容。
比如我一般用firefox+httpfox插件來看看自己到底發送了些什麼包。
以verycd爲例,先找到自己發的POST請求,以及POST表單項。
可以看到verycd的話需要填username,password,continueURI,fk,login_submit這幾項,其中fk是隨機生成的(其實不太隨機,看上去像是把epoch時間經過簡單的編碼生成的),需要從網頁獲取,也就是說得先訪問一次網頁,用正則表達式等工具截取返回數據中的fk項。continueURI顧名思義可以隨便寫,login_submit是固定的,這從源碼可以看出。還有username,password那就很顯然了:
- # -*- coding: utf-8 -*-
- import urllib
- import urllib2
- postdata=urllib.urlencode({
- 'username':'汪小光',
- 'password':'why888',
- 'continueURI':'http://www.verycd.com/',
- 'fk':'',
- 'login_submit':'登錄'
- })
- req = urllib2.Request(
- url = 'http://secure.verycd.com/signin',
- data = postdata
- )
- result = urllib2.urlopen(req)
- print result.read()
10.僞裝成瀏覽器訪問
某些網站反感爬蟲的到訪,於是對爬蟲一律拒絕請求
這時候我們需要僞裝成瀏覽器,這可以通過修改http包中的header來實現
- #…
- headers = {
- 'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
- }
- req = urllib2.Request(
- url = 'http://secure.verycd.com/signin/*/http://www.verycd.com/',
- data = postdata,
- headers = headers
- )
- #...
11.對付"反盜鏈"
某些站點有所謂的反盜鏈設置,其實說穿了很簡單,
就是檢查你發送請求的header裏面,referer站點是不是他自己,
所以我們只需要像把headers的referer改成該網站即可,以cnbeta爲例:
#... headers = { 'Referer':'http://www.cnbeta.com/articles' } #...
headers是一個dict數據結構,你可以放入任何想要的header,來做一些僞裝。
例如,有些網站喜歡讀取header中的X-Forwarded-For來看看人家的真實IP,可以直接把X-Forwarde-For改了。
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:百度貼吧爬蟲
- # 版本:0.1
- # 作者:why
- # 日期:2013-05-14
- # 語言:Python 2.7
- # 操作:輸入帶分頁的地址,去掉最後面的數字,設置一下起始頁數和終點頁數。
- # 功能:下載對應頁碼內的所有頁面並存儲爲html文件。
- #---------------------------------------
- import string, urllib2
- #定義百度函數
- def baidu_tieba(url,begin_page,end_page):
- for i in range(begin_page, end_page+1):
- sName = string.zfill(i,5) + '.html'#自動填充成六位的文件名
- print '正在下載第' + str(i) + '個網頁,並將其存儲爲' + sName + '......'
- f = open(sName,'w+')
- m = urllib2.urlopen(url + str(i)).read()
- f.write(m)
- f.close()
- #-------- 在這裏輸入參數 ------------------
- # 這個是山東大學的百度貼吧中某一個帖子的地址
- #bdurl = 'http://tieba.baidu.com/p/2296017831?pn='
- #iPostBegin = 1
- #iPostEnd = 10
- bdurl = str(raw_input(u'請輸入貼吧的地址,去掉pn=後面的數字:\n'))
- begin_page = int(raw_input(u'請輸入開始的頁數:\n'))
- end_page = int(raw_input(u'請輸入終點的頁數:\n'))
- #-------- 在這裏輸入參數 ------------------
- #調用
- baidu_tieba(bdurl,begin_page,end_page)
接下來準備用糗百做一個爬蟲的小例子。
但是在這之前,先詳細的整理一下Python中的正則表達式的相關內容。
正則表達式在Python爬蟲中的作用就像是老師點名時用的花名冊一樣,是必不可少的神兵利器。
以下內容轉自CNBLOG:http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html
整理時沒有注意,實在抱歉。
一、 正則表達式基礎
1.1.概念介紹正則表達式是用於處理字符串的強大工具,它並不是Python的一部分。
其他編程語言中也有正則表達式的概念,區別只在於不同的編程語言實現支持的語法數量不同。
它擁有自己獨特的語法以及一個獨立的處理引擎,在提供了正則表達式的語言裏,正則表達式的語法都是一樣的。
下圖展示了使用正則表達式進行匹配的流程:
正則表達式的大致匹配過程是:
1.依次拿出表達式和文本中的字符比較,
2.如果每一個字符都能匹配,則匹配成功;一旦有匹配不成功的字符則匹配失敗。
3.如果表達式中有量詞或邊界,這個過程會稍微有一些不同。
下圖列出了Python支持的正則表達式元字符和語法:
1.2. 數量詞的貪婪模式與非貪婪模式
正則表達式通常用於在文本中查找匹配的字符串。
貪婪模式,總是嘗試匹配儘可能多的字符;
非貪婪模式則相反,總是嘗試匹配儘可能少的字符。
Python裏數量詞默認是貪婪的。
例如:正則表達式"ab*"如果用於查找"abbbc",將找到"abbb"。
而如果使用非貪婪的數量詞"ab*?",將找到"a"。
1.3. 反斜槓的問題
與大多數編程語言相同,正則表達式裏使用"\"作爲轉義字符,這就可能造成反斜槓困擾。
假如你需要匹配文本中的字符"\",那麼使用編程語言表示的正則表達式裏將需要4個反斜槓"\\\\":
第一個和第三個用於在編程語言裏將第二個和第四個轉義成反斜槓,
轉換成兩個反斜槓\\後再在正則表達式裏轉義成一個反斜槓用來匹配反斜槓\。
這樣顯然是非常麻煩的。
Python裏的原生字符串很好地解決了這個問題,這個例子中的正則表達式可以使用r"\\"表示。
同樣,匹配一個數字的"\\d"可以寫成r"\d"。
有了原生字符串,媽媽再也不用擔心我的反斜槓問題~
二、 介紹re模塊
2.1. Compile
Python通過re模塊提供對正則表達式的支持。
使用re的一般步驟是:
Step1:先將正則表達式的字符串形式編譯爲Pattern實例。
Step2:然後使用Pattern實例處理文本並獲得匹配結果(一個Match實例)。
Step3:最後使用Match實例獲得信息,進行其他的操作。
我們新建一個re01.py來試驗一下re的應用:
- # -*- coding: utf-8 -*-
- #一個簡單的re實例,匹配字符串中的hello字符串
- #導入re模塊
- import re
- # 將正則表達式編譯成Pattern對象,注意hello前面的r的意思是“原生字符串”
- pattern = re.compile(r'hello')
- # 使用Pattern匹配文本,獲得匹配結果,無法匹配時將返回None
- match1 = pattern.match('hello world!')
- match2 = pattern.match('helloo world!')
- match3 = pattern.match('helllo world!')
- #如果match1匹配成功
- if match1:
- # 使用Match獲得分組信息
- print match1.group()
- else:
- print 'match1匹配失敗!'
- #如果match2匹配成功
- if match2:
- # 使用Match獲得分組信息
- print match2.group()
- else:
- print 'match2匹配失敗!'
- #如果match3匹配成功
- if match3:
- # 使用Match獲得分組信息
- print match3.group()
- else:
- print 'match3匹配失敗!'
可以看到控制檯輸出了匹配的三個結果:
下面來具體看看代碼中的關鍵方法。
★ re.compile(strPattern[, flag]):
這個方法是Pattern類的工廠方法,用於將字符串形式的正則表達式編譯爲Pattern對象。
第二個參數flag是匹配模式,取值可以使用按位或運算符'|'表示同時生效,比如re.I | re.M。
另外,你也可以在regex字符串中指定模式,
比如re.compile('pattern', re.I | re.M)與re.compile('(?im)pattern')是等價的。
可選值有:
- re.I(全拼:IGNORECASE): 忽略大小寫(括號內是完整寫法,下同)
- re.M(全拼:MULTILINE): 多行模式,改變'^'和'$'的行爲(參見上圖)
- re.S(全拼:DOTALL): 點任意匹配模式,改變'.'的行爲
- re.L(全拼:LOCALE): 使預定字符類 \w \W \b \B \s \S 取決於當前區域設定
- re.U(全拼:UNICODE): 使預定字符類 \w \W \b \B \s \S \d \D 取決於unicode定義的字符屬性
- re.X(全拼:VERBOSE): 詳細模式。這個模式下正則表達式可以是多行,忽略空白字符,並可以加入註釋。
以下兩個正則表達式是等價的:
- # -*- coding: utf-8 -*-
- #兩個等價的re匹配,匹配一個小數
- import re
- a = re.compile(r"""\d + # the integral part
- \. # the decimal point
- \d * # some fractional digits""", re.X)
- b = re.compile(r"\d+\.\d*")
- match11 = a.match('3.1415')
- match12 = a.match('33')
- match21 = b.match('3.1415')
- match22 = b.match('33')
- if match11:
- # 使用Match獲得分組信息
- print match11.group()
- else:
- print u'match11不是小數'
- if match12:
- # 使用Match獲得分組信息
- print match12.group()
- else:
- print u'match12不是小數'
- if match21:
- # 使用Match獲得分組信息
- print match21.group()
- else:
- print u'match21不是小數'
- if match22:
- # 使用Match獲得分組信息
- print match22.group()
- else:
- print u'match22不是小數'
re提供了衆多模塊方法用於完成正則表達式的功能。
這些方法可以使用Pattern實例的相應方法替代,唯一的好處是少寫一行re.compile()代碼,
但同時也無法複用編譯後的Pattern對象。
這些方法將在Pattern類的實例方法部分一起介紹。
如一開始的hello實例可以簡寫爲:
- # -*- coding: utf-8 -*-
- #一個簡單的re實例,匹配字符串中的hello字符串
- import re
- m = re.match(r'hello', 'hello world!')
- print m.group()
re模塊還提供了一個方法escape(string),用於將string中的正則表達式元字符如*/+/?等之前加上轉義符再返回
2.2. Match
Match對象是一次匹配的結果,包含了很多關於此次匹配的信息,可以使用Match提供的可讀屬性或方法來獲取這些信息。
屬性:
- string: 匹配時使用的文本。
- re: 匹配時使用的Pattern對象。
- pos: 文本中正則表達式開始搜索的索引。值與Pattern.match()和Pattern.seach()方法的同名參數相同。
- endpos: 文本中正則表達式結束搜索的索引。值與Pattern.match()和Pattern.seach()方法的同名參數相同。
- lastindex: 最後一個被捕獲的分組在文本中的索引。如果沒有被捕獲的分組,將爲None。
- lastgroup: 最後一個被捕獲的分組的別名。如果這個分組沒有別名或者沒有被捕獲的分組,將爲None。
方法:
- group([group1, …]):
獲得一個或多個分組截獲的字符串;指定多個參數時將以元組形式返回。group1可以使用編號也可以使用別名;編號0代表整個匹配的子串;不填寫參數時,返回group(0);沒有截獲字符串的組返回None;截獲了多次的組返回最後一次截獲的子串。 - groups([default]):
以元組形式返回全部分組截獲的字符串。相當於調用group(1,2,…last)。default表示沒有截獲字符串的組以這個值替代,默認爲None。 - groupdict([default]):
返回以有別名的組的別名爲鍵、以該組截獲的子串爲值的字典,沒有別名的組不包含在內。default含義同上。 - start([group]):
返回指定的組截獲的子串在string中的起始索引(子串第一個字符的索引)。group默認值爲0。 - end([group]):
返回指定的組截獲的子串在string中的結束索引(子串最後一個字符的索引+1)。group默認值爲0。 - span([group]):
返回(start(group), end(group))。 - expand(template):
將匹配到的分組代入template中然後返回。template中可以使用\id或\g<id>、\g<name>引用分組,但不能使用編號0。\id與\g<id>是等價的;但\10將被認爲是第10個分組,如果你想表達\1之後是字符'0',只能使用\g<1>0。
- # -*- coding: utf-8 -*-
- #一個簡單的match實例
- import re
- # 匹配如下內容:單詞+空格+單詞+任意字符
- m = re.match(r'(\w+) (\w+)(?P<sign>.*)', 'hello world!')
- print "m.string:", m.string
- print "m.re:", m.re
- print "m.pos:", m.pos
- print "m.endpos:", m.endpos
- print "m.lastindex:", m.lastindex
- print "m.lastgroup:", m.lastgroup
- print "m.group():", m.group()
- print "m.group(1,2):", m.group(1, 2)
- print "m.groups():", m.groups()
- print "m.groupdict():", m.groupdict()
- print "m.start(2):", m.start(2)
- print "m.end(2):", m.end(2)
- print "m.span(2):", m.span(2)
- print r"m.expand(r'\g<2> \g<1>\g<3>'):", m.expand(r'\2 \1\3')
- ### output ###
- # m.string: hello world!
- # m.re: <_sre.SRE_Pattern object at 0x016E1A38>
- # m.pos: 0
- # m.endpos: 12
- # m.lastindex: 3
- # m.lastgroup: sign
- # m.group(1,2): ('hello', 'world')
- # m.groups(): ('hello', 'world', '!')
- # m.groupdict(): {'sign': '!'}
- # m.start(2): 6
- # m.end(2): 11
- # m.span(2): (6, 11)
- # m.expand(r'\2 \1\3'): world hello!
2.3. Pattern
Pattern對象是一個編譯好的正則表達式,通過Pattern提供的一系列方法可以對文本進行匹配查找。
Pattern不能直接實例化,必須使用re.compile()進行構造,也就是re.compile()返回的對象。
Pattern提供了幾個可讀屬性用於獲取表達式的相關信息:
- pattern: 編譯時用的表達式字符串。
- flags: 編譯時用的匹配模式。數字形式。
- groups: 表達式中分組的數量。
- groupindex: 以表達式中有別名的組的別名爲鍵、以該組對應的編號爲值的字典,沒有別名的組不包含在內。
- # -*- coding: utf-8 -*-
- #一個簡單的pattern實例
- import re
- p = re.compile(r'(\w+) (\w+)(?P<sign>.*)', re.DOTALL)
- print "p.pattern:", p.pattern
- print "p.flags:", p.flags
- print "p.groups:", p.groups
- print "p.groupindex:", p.groupindex
- ### output ###
- # p.pattern: (\w+) (\w+)(?P<sign>.*)
- # p.flags: 16
- # p.groups: 3
- # p.groupindex: {'sign': 3}
下面重點介紹一下pattern的實例方法及其使用。
1.match
match(string[, pos[, endpos]]) | re.match(pattern, string[, flags]):
這個方法將從string的pos下標處起嘗試匹配pattern;
如果pattern結束時仍可匹配,則返回一個Match對象;
如果匹配過程中pattern無法匹配,或者匹配未結束就已到達endpos,則返回None。
pos和endpos的默認值分別爲0和len(string);
re.match()無法指定這兩個參數,參數flags用於編譯pattern時指定匹配模式。
注意:這個方法並不是完全匹配。
當pattern結束時若string還有剩餘字符,仍然視爲成功。
想要完全匹配,可以在表達式末尾加上邊界匹配符'$'。
下面來看一個Match的簡單案例:
- # encoding: UTF-8
- import re
- # 將正則表達式編譯成Pattern對象
- pattern = re.compile(r'hello')
- # 使用Pattern匹配文本,獲得匹配結果,無法匹配時將返回None
- match = pattern.match('hello world!')
- if match:
- # 使用Match獲得分組信息
- print match.group()
- ### 輸出 ###
- # hello
2.search
search(string[, pos[, endpos]]) | re.search(pattern, string[, flags]):
這個方法用於查找字符串中可以匹配成功的子串。
從string的pos下標處起嘗試匹配pattern,
如果pattern結束時仍可匹配,則返回一個Match對象;
若無法匹配,則將pos加1後重新嘗試匹配;
直到pos=endpos時仍無法匹配則返回None。
pos和endpos的默認值分別爲0和len(string));
re.search()無法指定這兩個參數,參數flags用於編譯pattern時指定匹配模式。
那麼它和match有什麼區別呢?
match()函數只檢測re是不是在string的開始位置匹配,
search()會掃描整個string查找匹配,
match()只有在0位置匹配成功的話纔有返回,如果不是開始位置匹配成功的話,match()就返回none
例如:
print(re.match(‘super’, ‘superstition’).span())
會返回(0, 5)
print(re.match(‘super’, ‘insuperable’))
則返回None
search()會掃描整個字符串並返回第一個成功的匹配
例如:
print(re.search(‘super’, ‘superstition’).span())
返回(0, 5)
print(re.search(‘super’, ‘insuperable’).span())
返回(2, 7)
看一個search的實例:
- # -*- coding: utf-8 -*-
- #一個簡單的search實例
- import re
- # 將正則表達式編譯成Pattern對象
- pattern = re.compile(r'world')
- # 使用search()查找匹配的子串,不存在能匹配的子串時將返回None
- # 這個例子中使用match()無法成功匹配
- match = pattern.search('hello world!')
- if match:
- # 使用Match獲得分組信息
- print match.group()
- ### 輸出 ###
- # world
3.split
split(string[, maxsplit]) | re.split(pattern, string[, maxsplit]):
按照能夠匹配的子串將string分割後返回列表。
maxsplit用於指定最大分割次數,不指定將全部分割。
- import re
- p = re.compile(r'\d+')
- print p.split('one1two2three3four4')
- ### output ###
- # ['one', 'two', 'three', 'four', '']
4.findall
findall(string[, pos[, endpos]]) | re.findall(pattern, string[, flags]):
搜索string,以列表形式返回全部能匹配的子串。
- import re
- p = re.compile(r'\d+')
- print p.findall('one1two2three3four4')
- ### output ###
- # ['1', '2', '3', '4']
5.finditer
finditer(string[, pos[, endpos]]) | re.finditer(pattern, string[, flags]):
搜索string,返回一個順序訪問每一個匹配結果(Match對象)的迭代器。
- import re
- p = re.compile(r'\d+')
- for m in p.finditer('one1two2three3four4'):
- print m.group(),
- ### output ###
- # 1 2 3 4
6.sub
sub(repl, string[, count]) | re.sub(pattern, repl, string[, count]):
使用repl替換string中每一個匹配的子串後返回替換後的字符串。
當repl是一個字符串時,可以使用\id或\g<id>、\g<name>引用分組,但不能使用編號0。
當repl是一個方法時,這個方法應當只接受一個參數(Match對象),並返回一個字符串用於替換(返回的字符串中不能再引用分組)。
count用於指定最多替換次數,不指定時全部替換。
- import re
- p = re.compile(r'(\w+) (\w+)')
- s = 'i say, hello world!'
- print p.sub(r'\2 \1', s)
- def func(m):
- return m.group(1).title() + ' ' + m.group(2).title()
- print p.sub(func, s)
- ### output ###
- # say i, world hello!
- # I Say, Hello World!
7.subn
subn(repl, string[, count]) |re.sub(pattern, repl, string[, count]):
返回 (sub(repl, string[, count]), 替換次數)。
- import re
- p = re.compile(r'(\w+) (\w+)')
- s = 'i say, hello world!'
- print p.subn(r'\2 \1', s)
- def func(m):
- return m.group(1).title() + ' ' + m.group(2).title()
- print p.subn(func, s)
- ### output ###
- # ('say i, world hello!', 2)
- # ('I Say, Hello World!', 2)
至此,Python的正則表達式基本介紹就算是完成了^_^
[Python]網絡爬蟲(八):糗事百科的網絡爬蟲(v0.3)源碼及解析(簡化更新)
Q&A:
1.爲什麼有段時間顯示糗事百科不可用?
答:前段時間因爲糗事百科添加了Header的檢驗,導致無法爬取,需要在代碼中模擬Header。現在代碼已經作了修改,可以正常使用。
2.爲什麼需要單獨新建個線程?
答:基本流程是這樣的:爬蟲在後臺新起一個線程,一直爬取兩頁的糗事百科,如果剩餘不足兩頁,則再爬一頁。用戶按下回車只是從庫存中獲取最新的內容,而不是上網獲取,所以瀏覽更順暢。也可以把加載放在主線程,不過這樣會導致爬取過程中等待時間過長的問題。
項目內容:
用Python寫的糗事百科的網絡爬蟲。
使用方法:
新建一個Bug.py文件,然後將代碼複製到裏面後,雙擊運行。
程序功能:
在命令提示行中瀏覽糗事百科。
原理解釋:
首先,先瀏覽一下糗事百科的主頁:http://www.qiushibaike.com/hot/page/1
可以看出來,鏈接中page/後面的數字就是對應的頁碼,記住這一點爲以後的編寫做準備。
然後,右擊查看頁面源碼:
觀察發現,每一個段子都用div標記,其中class必爲content,title是發帖時間,我們只需要用正則表達式將其“扣”出來就可以了。
明白了原理之後,剩下的就是正則表達式的內容了,可以參照這篇博文:
http://blog.csdn.net/wxg694175346/article/details/8929576
運行效果:
- <pre code_snippet_id="189704" snippet_file_name="blog_20140215_1_8153875" name="code" class="python"># -*- coding: utf-8 -*-
- import urllib2
- import urllib
- import re
- import thread
- import time
- #----------- 加載處理糗事百科 -----------
- class Spider_Model:
- def __init__(self):
- self.page = 1
- self.pages = []
- self.enable = False
- # 將所有的段子都扣出來,添加到列表中並且返回列表
- def GetPage(self,page):
- myUrl = "http://m.qiushibaike.com/hot/page/" + page
- user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
- headers = { 'User-Agent' : user_agent }
- req = urllib2.Request(myUrl, headers = headers)
- myResponse = urllib2.urlopen(req)
- myPage = myResponse.read()
- #encode的作用是將unicode編碼轉換成其他編碼的字符串
- #decode的作用是將其他編碼的字符串轉換成unicode編碼
- unicodePage = myPage.decode("utf-8")
- # 找出所有class="content"的div標記
- #re.S是任意匹配模式,也就是.可以匹配換行符
- myItems = re.findall('<div.*?class="content".*?title="(.*?)">(.*?)</div>',unicodePage,re.S)
- items = []
- for item in myItems:
- # item 中第一個是div的標題,也就是時間
- # item 中第二個是div的內容,也就是內容
- items.append([item[0].replace("\n",""),item[1].replace("\n","")])
- return items
- # 用於加載新的段子
- def LoadPage(self):
- # 如果用戶未輸入quit則一直運行
- while self.enable:
- # 如果pages數組中的內容小於2個
- if len(self.pages) < 2:
- try:
- # 獲取新的頁面中的段子們
- myPage = self.GetPage(str(self.page))
- self.page += 1
- self.pages.append(myPage)
- except:
- print '無法鏈接糗事百科!'
- else:
- time.sleep(1)
- def ShowPage(self,nowPage,page):
- for items in nowPage:
- print u'第%d頁' % page , items[0] , items[1]
- myInput = raw_input()
- if myInput == "quit":
- self.enable = False
- break
- def Start(self):
- self.enable = True
- page = self.page
- print u'正在加載中請稍候......'
- # 新建一個線程在後臺加載段子並存儲
- thread.start_new_thread(self.LoadPage,())
- #----------- 加載處理糗事百科 -----------
- while self.enable:
- # 如果self的page數組中存有元素
- if self.pages:
- nowPage = self.pages[0]
- del self.pages[0]
- self.ShowPage(nowPage,page)
- page += 1
- #----------- 程序的入口處 -----------
- print u"""
- ---------------------------------------
- 程序:糗百爬蟲
- 版本:0.3
- 作者:why
- 日期:2014-06-03
- 語言:Python 2.7
- 操作:輸入quit退出閱讀糗事百科
- 功能:按下回車依次瀏覽今日的糗百熱點
- ---------------------------------------
- """
- print u'請按下回車瀏覽今日的糗百內容:'
- raw_input(' ')
- myModel = Spider_Model()
- myModel.Start()
- </pre><br><br>
[Python]網絡爬蟲(九):百度貼吧的網絡爬蟲(v0.4)源碼及解析
百度貼吧的爬蟲製作和糗百的爬蟲製作原理基本相同,都是通過查看源碼扣出關鍵數據,然後將其存儲到本地txt文件。
源碼下載:
http://download.csdn.net/detail/wxg694175346/6925583
用Python寫的百度貼吧的網絡爬蟲。
使用方法:
新建一個BugBaidu.py文件,然後將代碼複製到裏面後,雙擊運行。
程序功能:
將貼吧中樓主發佈的內容打包txt存儲到本地。
原理解釋:
首先,先瀏覽一下某一條貼吧,點擊只看樓主並點擊第二頁之後url發生了一點變化,變成了:
http://tieba.baidu.com/p/2296712428?see_lz=1&pn=1
可以看出來,see_lz=1是隻看樓主,pn=1是對應的頁碼,記住這一點爲以後的編寫做準備。
這就是我們需要利用的url。接下來就是查看頁面源碼。
首先把題目摳出來存儲文件的時候會用到。
可以看到百度使用gbk編碼,標題使用h1標記:
- <h1 class="core_title_txt" title="【原創】時尚首席(關於時尚,名利,事業,愛情,勵志)">【原創】時尚首席(關於時尚,名利,事業,愛情,勵志)</h1>
同樣,正文部分用div和class綜合標記,接下來要做的只是用正則表達式來匹配即可。
運行截圖:
生成的txt文件:
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:百度貼吧爬蟲
- # 版本:0.5
- # 作者:why
- # 日期:2013-05-16
- # 語言:Python 2.7
- # 操作:輸入網址後自動只看樓主並保存到本地文件
- # 功能:將樓主發佈的內容打包txt存儲到本地。
- #---------------------------------------
- import string
- import urllib2
- import re
- #----------- 處理頁面上的各種標籤 -----------
- class HTML_Tool:
- # 用非 貪婪模式 匹配 \t 或者 \n 或者 空格 或者 超鏈接 或者 圖片
- BgnCharToNoneRex = re.compile("(\t|\n| |<a.*?>|<img.*?>)")
- # 用非 貪婪模式 匹配 任意<>標籤
- EndCharToNoneRex = re.compile("<.*?>")
- # 用非 貪婪模式 匹配 任意<p>標籤
- BgnPartRex = re.compile("<p.*?>")
- CharToNewLineRex = re.compile("(<br/>|</p>|<tr>|<div>|</div>)")
- CharToNextTabRex = re.compile("<td>")
- # 將一些html的符號實體轉變爲原始符號
- replaceTab = [("<","<"),(">",">"),("&","&"),("&","\""),(" "," ")]
- def Replace_Char(self,x):
- x = self.BgnCharToNoneRex.sub("",x)
- x = self.BgnPartRex.sub("\n ",x)
- x = self.CharToNewLineRex.sub("\n",x)
- x = self.CharToNextTabRex.sub("\t",x)
- x = self.EndCharToNoneRex.sub("",x)
- for t in self.replaceTab:
- x = x.replace(t[0],t[1])
- return x
- class Baidu_Spider:
- # 申明相關的屬性
- def __init__(self,url):
- self.myUrl = url + '?see_lz=1'
- self.datas = []
- self.myTool = HTML_Tool()
- print u'已經啓動百度貼吧爬蟲,咔嚓咔嚓'
- # 初始化加載頁面並將其轉碼儲存
- def baidu_tieba(self):
- # 讀取頁面的原始信息並將其從gbk轉碼
- myPage = urllib2.urlopen(self.myUrl).read().decode("gbk")
- # 計算樓主發佈內容一共有多少頁
- endPage = self.page_counter(myPage)
- # 獲取該帖的標題
- title = self.find_title(myPage)
- print u'文章名稱:' + title
- # 獲取最終的數據
- self.save_data(self.myUrl,title,endPage)
- #用來計算一共有多少頁
- def page_counter(self,myPage):
- # 匹配 "共有<span class="red">12</span>頁" 來獲取一共有多少頁
- myMatch = re.search(r'class="red">(\d+?)</span>', myPage, re.S)
- if myMatch:
- endPage = int(myMatch.group(1))
- print u'爬蟲報告:發現樓主共有%d頁的原創內容' % endPage
- else:
- endPage = 0
- print u'爬蟲報告:無法計算樓主發佈內容有多少頁!'
- return endPage
- # 用來尋找該帖的標題
- def find_title(self,myPage):
- # 匹配 <h1 class="core_title_txt" title="">xxxxxxxxxx</h1> 找出標題
- myMatch = re.search(r'<h1.*?>(.*?)</h1>', myPage, re.S)
- title = u'暫無標題'
- if myMatch:
- title = myMatch.group(1)
- else:
- print u'爬蟲報告:無法加載文章標題!'
- # 文件名不能包含以下字符: \ / : * ? " < > |
- title = title.replace('\\','').replace('/','').replace(':','').replace('*','').replace('?','').replace('"','').replace('>','').replace('<','').replace('|','')
- return title
- # 用來存儲樓主發佈的內容
- def save_data(self,url,title,endPage):
- # 加載頁面數據到數組中
- self.get_data(url,endPage)
- # 打開本地文件
- f = open(title+'.txt','w+')
- f.writelines(self.datas)
- f.close()
- print u'爬蟲報告:文件已下載到本地並打包成txt文件'
- print u'請按任意鍵退出...'
- raw_input();
- # 獲取頁面源碼並將其存儲到數組中
- def get_data(self,url,endPage):
- url = url + '&pn='
- for i in range(1,endPage+1):
- print u'爬蟲報告:爬蟲%d號正在加載中...' % i
- myPage = urllib2.urlopen(url + str(i)).read()
- # 將myPage中的html代碼處理並存儲到datas裏面
- self.deal_data(myPage.decode('gbk'))
- # 將內容從頁面代碼中摳出來
- def deal_data(self,myPage):
- myItems = re.findall('id="post_content.*?>(.*?)</div>',myPage,re.S)
- for item in myItems:
- data = self.myTool.Replace_Char(item.replace("\n","").encode('gbk'))
- self.datas.append(data+'\n')
- #-------- 程序入口處 ------------------
- print u"""#---------------------------------------
- # 程序:百度貼吧爬蟲
- # 版本:0.5
- # 作者:why
- # 日期:2013-05-16
- # 語言:Python 2.7
- # 操作:輸入網址後自動只看樓主並保存到本地文件
- # 功能:將樓主發佈的內容打包txt存儲到本地。
- #---------------------------------------
- """
- # 以某小說貼吧爲例子
- # bdurl = 'http://tieba.baidu.com/p/2296712428?see_lz=1&pn=1'
- print u'請輸入貼吧的地址最後的數字串:'
- bdurl = 'http://tieba.baidu.com/p/' + str(raw_input(u'http://tieba.baidu.com/p/'))
- #調用
- mySpider = Baidu_Spider(bdurl)
- mySpider.baidu_tieba()
[Python]網絡爬蟲(十):一個爬蟲的誕生全過程(以山東大學績點運算爲例)
先來說一下我們學校的網站:
http://jwxt.sdu.edu.cn:7777/zhxt_bks/zhxt_bks.html
查詢成績需要登錄,然後顯示各學科成績,但是隻顯示成績而沒有績點,也就是加權平均分。
顯然這樣手動計算績點是一件非常麻煩的事情。所以我們可以用python做一個爬蟲來解決這個問題。
1.決戰前夜
先來準備一下工具:HttpFox插件。
這是一款http協議分析插件,分析頁面請求和響應的時間、內容、以及瀏覽器用到的COOKIE等。
以我爲例,安裝在火狐上即可,效果如圖:
可以非常直觀的查看相應的信息。
點擊start是開始檢測,點擊stop暫停檢測,點擊clear清除內容。
一般在使用之前,點擊stop暫停,然後點擊clear清屏,確保看到的是訪問當前頁面獲得的數據。
2.深入敵後
下面就去山東大學的成績查詢網站,看一看在登錄的時候,到底發送了那些信息。
先來到登錄頁面,把httpfox打開,clear之後,點擊start開啓檢測:
輸入完了個人信息,確保httpfox處於開啓狀態,然後點擊確定提交信息,實現登錄。
這個時候可以看到,httpfox檢測到了三條信息:
這時點擊stop鍵,確保捕獲到的是訪問該頁面之後反饋的數據,以便我們做爬蟲的時候模擬登陸使用。
3.庖丁解牛
乍一看我們拿到了三個數據,兩個是GET的一個是POST的,但是它們到底是什麼,應該怎麼用,我們還一無所知。
所以,我們需要挨個查看一下捕獲到的內容。
先看POST的信息:
既然是POST的信息,我們就直接看PostData即可。
可以看到一共POST兩個數據,stuid和pwd。
並且從Type的Redirect to可以看出,POST完畢之後跳轉到了bks_login2.loginmessage頁面。
由此看出,這個數據是點擊確定之後提交的表單數據。
點擊cookie標籤,看看cookie信息:
沒錯,收到了一個ACCOUNT的cookie,並且在session結束之後自動銷燬。
那麼提交之後收到了哪些信息呢?
我們來看看後面的兩個GET數據。
先看第一個,我們點擊content標籤可以查看收到的內容,是不是有一種生吞活剝的快感-。-HTML源碼暴露無疑了:
看來這個只是顯示頁面的html源碼而已,點擊cookie,查看cookie的相關信息:
啊哈,原來html頁面的內容是發送了cookie信息之後才接受到的。
再來看看最後一個接收到的信息:
大致看了一下應該只是一個叫做style.css的css文件,對我們沒有太大的作用。
4.冷靜應戰
既然已經知道了我們向服務器發送了什麼數據,也知道了我們接收到了什麼數據,基本的流程如下:
- 首先,我們POST學號和密碼--->然後返回cookie的值
- 然後發送cookie給服務器--->返回頁面信息。
- 獲取到成績頁面的數據,用正則表達式將成績和學分單獨取出並計算加權平均數。
OK,看上去好像很簡單的樣紙。那下面我們就來試試看吧。
但是在實驗之前,還有一個問題沒有解決,就是POST的數據到底發送到了哪裏?
再來看一下當初的頁面:
很明顯是用一個html框架來實現的,也就是說,我們在地址欄看到的地址並不是右邊提交表單的地址。
那麼怎樣才能獲得真正的地址-。-右擊查看頁面源代碼:
嗯沒錯,那個name="w_right"的就是我們要的登錄頁面。
網站的原來的地址是:
http://jwxt.sdu.edu.cn:7777/zhxt_bks/zhxt_bks.html
所以,真正的表單提交的地址應該是:
http://jwxt.sdu.edu.cn:7777/zhxt_bks/xk_login.html
輸入一看,果不其然:
靠居然是清華大學的選課系統。。。目測是我校懶得做頁面了就直接借了。。結果連標題都不改一下。。。
但是這個頁面依舊不是我們需要的頁面,因爲我們的POST數據提交到的頁面,應該是表單form的ACTION中提交到的頁面。
也就是說,我們需要查看源碼,來知道POST數據到底發送到了哪裏:
嗯,目測這個纔是提交POST數據的地址。
整理到地址欄中,完整的地址應該如下:
http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login
(獲取的方式很簡單,在火狐瀏覽器中直接點擊那個鏈接就能看到這個鏈接的地址了)
5.小試牛刀
接下來的任務就是:用python模擬發送一個POST的數據並取到返回的cookie值。
關於cookie的操作可以看看這篇博文:
http://blog.csdn.net/wxg694175346/article/details/8925978
我們先準備一個POST的數據,再準備一個cookie的接收,然後寫出源碼如下:
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:山東大學爬蟲
- # 版本:0.1
- # 作者:why
- # 日期:2013-07-12
- # 語言:Python 2.7
- # 操作:輸入學號和密碼
- # 功能:輸出成績的加權平均值也就是績點
- #---------------------------------------
- import urllib
- import urllib2
- import cookielib
- cookie = cookielib.CookieJar()
- opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
- #需要POST的數據#
- postdata=urllib.urlencode({
- 'stuid':'201100300428',
- 'pwd':'921030'
- })
- #自定義一個請求#
- req = urllib2.Request(
- url = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login',
- data = postdata
- )
- #訪問該鏈接#
- result = opener.open(req)
- #打印返回的內容#
- print result.read()
如此這般之後,再看看運行的效果:
ok,如此這般,我們就算模擬登陸成功了。
6.偷天換日
接下來的任務就是用爬蟲獲取到學生的成績。
再來看看源網站。
開啓HTTPFOX之後,點擊查看成績,發現捕獲到了如下的數據:
點擊第一個GET的數據,查看內容可以發現Content就是獲取到的成績的內容。
而獲取到的頁面鏈接,從頁面源代碼中右擊查看元素,可以看到點擊鏈接之後跳轉的頁面(火狐瀏覽器只需要右擊,“查看此框架”,即可):
從而可以得到查看成績的鏈接如下:
http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bkscjcx.curscopre
7.萬事俱備
現在萬事俱備啦,所以只需要把鏈接應用到爬蟲裏面,看看能否查看到成績的頁面。
從httpfox可以看到,我們發送了一個cookie才能返回成績的信息,所以我們就用python模擬一個cookie的發送,以此來請求成績的信息:
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:山東大學爬蟲
- # 版本:0.1
- # 作者:why
- # 日期:2013-07-12
- # 語言:Python 2.7
- # 操作:輸入學號和密碼
- # 功能:輸出成績的加權平均值也就是績點
- #---------------------------------------
- import urllib
- import urllib2
- import cookielib
- #初始化一個CookieJar來處理Cookie的信息#
- cookie = cookielib.CookieJar()
- #創建一個新的opener來使用我們的CookieJar#
- opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
- #需要POST的數據#
- postdata=urllib.urlencode({
- 'stuid':'201100300428',
- 'pwd':'921030'
- })
- #自定義一個請求#
- req = urllib2.Request(
- url = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login',
- data = postdata
- )
- #訪問該鏈接#
- result = opener.open(req)
- #打印返回的內容#
- print result.read()
- #打印cookie的值
- for item in cookie:
- print 'Cookie:Name = '+item.name
- print 'Cookie:Value = '+item.value
- #訪問該鏈接#
- result = opener.open('http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bkscjcx.curscopre')
- #打印返回的內容#
- print result.read()
按下F5運行即可,看看捕獲到的數據吧:
既然這樣就沒有什麼問題了吧,用正則表達式將數據稍稍處理一下,取出學分和相應的分數就可以了。
8.手到擒來
這麼一大堆html源碼顯然是不利於我們處理的,下面要用正則表達式來摳出必須的數據。
關於正則表達式的教程可以看看這個博文:
http://blog.csdn.net/wxg694175346/article/details/8929576
我們來看看成績的源碼:
既然如此,用正則表達式就易如反掌了。
我們將代碼稍稍整理一下,然後用正則來取出數據:
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:山東大學爬蟲
- # 版本:0.1
- # 作者:why
- # 日期:2013-07-12
- # 語言:Python 2.7
- # 操作:輸入學號和密碼
- # 功能:輸出成績的加權平均值也就是績點
- #---------------------------------------
- import urllib
- import urllib2
- import cookielib
- import re
- class SDU_Spider:
- # 申明相關的屬性
- def __init__(self):
- self.loginUrl = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login' # 登錄的url
- self.resultUrl = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bkscjcx.curscopre' # 顯示成績的url
- self.cookieJar = cookielib.CookieJar() # 初始化一個CookieJar來處理Cookie的信息
- self.postdata=urllib.urlencode({'stuid':'201100300428','pwd':'921030'}) # POST的數據
- self.weights = [] #存儲權重,也就是學分
- self.points = [] #存儲分數,也就是成績
- self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookieJar))
- def sdu_init(self):
- # 初始化鏈接並且獲取cookie
- myRequest = urllib2.Request(url = self.loginUrl,data = self.postdata) # 自定義一個請求
- result = self.opener.open(myRequest) # 訪問登錄頁面,獲取到必須的cookie的值
- result = self.opener.open(self.resultUrl) # 訪問成績頁面,獲得成績的數據
- # 打印返回的內容
- # print result.read()
- self.deal_data(result.read().decode('gbk'))
- self.print_data(self.weights);
- self.print_data(self.points);
- # 將內容從頁面代碼中摳出來
- def deal_data(self,myPage):
- myItems = re.findall('<TR>.*?<p.*?<p.*?<p.*?<p.*?<p.*?>(.*?)</p>.*?<p.*?<p.*?>(.*?)</p>.*?</TR>',myPage,re.S) #獲取到學分
- for item in myItems:
- self.weights.append(item[0].encode('gbk'))
- self.points.append(item[1].encode('gbk'))
- # 將內容從頁面代碼中摳出來
- def print_data(self,items):
- for item in items:
- print item
- #調用
- mySpider = SDU_Spider()
- mySpider.sdu_init()
水平有限,,正則是有點醜,。運行的效果如圖:
ok,接下來的只是數據的處理問題了。。
9.凱旋而歸
完整的代碼如下,至此一個完整的爬蟲項目便完工了。
- # -*- coding: utf-8 -*-
- #---------------------------------------
- # 程序:山東大學爬蟲
- # 版本:0.1
- # 作者:why
- # 日期:2013-07-12
- # 語言:Python 2.7
- # 操作:輸入學號和密碼
- # 功能:輸出成績的加權平均值也就是績點
- #---------------------------------------
- import urllib
- import urllib2
- import cookielib
- import re
- import string
- class SDU_Spider:
- # 申明相關的屬性
- def __init__(self):
- self.loginUrl = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bks_login2.login' # 登錄的url
- self.resultUrl = 'http://jwxt.sdu.edu.cn:7777/pls/wwwbks/bkscjcx.curscopre' # 顯示成績的url
- self.cookieJar = cookielib.CookieJar() # 初始化一個CookieJar來處理Cookie的信息
- self.postdata=urllib.urlencode({'stuid':'201100300428','pwd':'921030'}) # POST的數據
- self.weights = [] #存儲權重,也就是學分
- self.points = [] #存儲分數,也就是成績
- self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookieJar))
- def sdu_init(self):
- # 初始化鏈接並且獲取cookie
- myRequest = urllib2.Request(url = self.loginUrl,data = self.postdata) # 自定義一個請求
- result = self.opener.open(myRequest) # 訪問登錄頁面,獲取到必須的cookie的值
- result = self.opener.open(self.resultUrl) # 訪問成績頁面,獲得成績的數據
- # 打印返回的內容
- # print result.read()
- self.deal_data(result.read().decode('gbk'))
- self.calculate_date();
- # 將內容從頁面代碼中摳出來
- def deal_data(self,myPage):
- myItems = re.findall('<TR>.*?<p.*?<p.*?<p.*?<p.*?<p.*?>(.*?)</p>.*?<p.*?<p.*?>(.*?)</p>.*?</TR>',myPage,re.S) #獲取到學分
- for item in myItems:
- self.weights.append(item[0].encode('gbk'))
- self.points.append(item[1].encode('gbk'))
- #計算績點,如果成績還沒出來,或者成績是優秀良好,就不運算該成績
- def calculate_date(self):
- point = 0.0
- weight = 0.0
- for i in range(len(self.points)):
- if(self.points[i].isdigit()):
- point += string.atof(self.points[i])*string.atof(self.weights[i])
- weight += string.atof(self.weights[i])
- print point/weight
- #調用
- mySpider = SDU_Spider()
- mySpider.sdu_init()
[Python]網絡爬蟲(11):亮劍!爬蟲框架小抓抓Scrapy閃亮登場!
前面十章爬蟲筆記陸陸續續記錄了一些簡單的Python爬蟲知識,
用來解決簡單的貼吧下載,績點運算自然不在話下。
不過要想批量下載大量的內容,比如知乎的所有的問答,那便顯得遊刃不有餘了點。
於是乎,爬蟲框架Scrapy就這樣出場了!
Scrapy = Scrach+Python,Scrach這個單詞是抓取的意思,
暫且可以叫它:小抓抓吧。
小抓抓的官網地址:點我點我。
那麼下面來簡單的演示一下小抓抓Scrapy的安裝流程。
具體流程參照:官網教程
友情提醒:一定要按照Python的版本下載,要不然安裝的時候會提醒找不到Python。建議大家安裝32位是因爲有些版本的必備軟件64位不好找。
1.安裝Python(建議32位)
建議安裝Python2.7.x,3.x貌似還不支持。
安裝完了記得配置環境,將python目錄和python目錄下的Scripts目錄添加到系統環境變量的Path裏。
在cmd中輸入python如果出現版本信息說明配置完畢。
2.安裝lxml
lxml是一種使用 Python 編寫的庫,可以迅速、靈活地處理 XML。點擊這裏選擇對應的Python版本安裝。
3.安裝setuptools
用來安裝egg文件,點擊這裏下載python2.7的對應版本的setuptools。
4.安裝zope.interface
可以使用第三步下載的setuptools來安裝egg文件,現在也有exe版本,點擊這裏下載。
5.安裝Twisted
Twisted是用Python實現的基於事件驅動的網絡引擎框架,點擊這裏下載。
6.安裝pyOpenSSL
pyOpenSSL是Python的OpenSSL接口,點擊這裏下載。
7.安裝win32py
提供win32api,點擊這裏下載
8.安裝Scrapy
終於到了激動人心的時候了!安裝了那麼多小部件之後終於輪到主角登場。
直接在cmd中輸入easy_install scrapy回車即可。
9.檢查安裝
打開一個cmd窗口,在任意位置執行scrapy命令,得到下列頁面,表示環境配置成功。
[Python]網絡爬蟲(12):爬蟲框架Scrapy的第一個爬蟲示例入門教程
(建議大家多看看官網教程:教程地址)
我們使用dmoz.org這個網站來作爲小抓抓一展身手的對象。
首先先要回答一個問題。
問:把網站裝進爬蟲裏,總共分幾步?
答案很簡單,四步:
- 新建項目 (Project):新建一個新的爬蟲項目
- 明確目標(Items):明確你想要抓取的目標
- 製作爬蟲(Spider):製作爬蟲開始爬取網頁
- 存儲內容(Pipeline):設計管道存儲爬取內容
好的,基本流程既然確定了,那接下來就一步一步的完成就可以了。
1.新建項目(Project)
在空目錄下按住Shift鍵右擊,選擇“在此處打開命令窗口”,輸入一下命令:
- scrapy startproject tutorial
其中,tutorial爲項目名稱。
可以看到將會創建一個tutorial文件夾,目錄結構如下:
- tutorial/
- scrapy.cfg
- tutorial/
- __init__.py
- items.py
- pipelines.py
- settings.py
- spiders/
- __init__.py
- ...
下面來簡單介紹一下各個文件的作用:
- scrapy.cfg:項目的配置文件
- tutorial/:項目的Python模塊,將會從這裏引用代碼
- tutorial/items.py:項目的items文件
- tutorial/pipelines.py:項目的pipelines文件
- tutorial/settings.py:項目的設置文件
- tutorial/spiders/:存儲爬蟲的目錄
2.明確目標(Item)
在Scrapy中,items是用來加載抓取內容的容器,有點像Python中的Dic,也就是字典,但是提供了一些額外的保護減少錯誤。
一般來說,item可以用scrapy.item.Item類來創建,並且用scrapy.item.Field對象來定義屬性(可以理解成類似於ORM的映射關係)。
接下來,我們開始來構建item模型(model)。
首先,我們想要的內容有:
- 名稱(name)
- 鏈接(url)
- 描述(description)
修改tutorial目錄下的items.py文件,在原本的class後面添加我們自己的class。
因爲要抓dmoz.org網站的內容,所以我們可以將其命名爲DmozItem:
- # Define here the models for your scraped items
- #
- # See documentation in:
- # http://doc.scrapy.org/en/latest/topics/items.html
- from scrapy.item import Item, Field
- class TutorialItem(Item):
- # define the fields for your item here like:
- # name = Field()
- pass
- class DmozItem(Item):
- title = Field()
- link = Field()
- desc = Field()
剛開始看起來可能會有些看不懂,但是定義這些item能讓你用其他組件的時候知道你的 items到底是什麼。
可以把Item簡單的理解成封裝好的類對象。
3.製作爬蟲(Spider)
製作爬蟲,總體分兩步:先爬再取。
也就是說,首先你要獲取整個網頁的所有內容,然後再取出其中對你有用的部分。
3.1爬
Spider是用戶自己編寫的類,用來從一個域(或域組)中抓取信息。
他們定義了用於下載的URL列表、跟蹤鏈接的方案、解析網頁內容的方式,以此來提取items。
要建立一個Spider,你必須用scrapy.spider.BaseSpider創建一個子類,並確定三個強制的屬性:
- name:爬蟲的識別名稱,必須是唯一的,在不同的爬蟲中你必須定義不同的名字。
- start_urls:爬取的URL列表。爬蟲從這裏開始抓取數據,所以,第一次下載的數據將會從這些urls開始。其他子URL將會從這些起始URL中繼承性生成。
- parse():解析的方法,調用的時候傳入從每一個URL傳回的Response對象作爲唯一參數,負責解析並匹配抓取的數據(解析爲item),跟蹤更多的URL。
這裏可以參考寬度爬蟲教程中提及的思想來幫助理解,教程傳送:[Java]
知乎下巴第5集:使用HttpClient工具包和寬度爬蟲。
也就是把Url存儲下來並依此爲起點逐步擴散開去,抓取所有符合條件的網頁Url存儲起來繼續爬取。
下面我們來寫第一隻爬蟲,命名爲dmoz_spider.py,保存在tutorial\spiders目錄下。
dmoz_spider.py代碼如下:
- from scrapy.spider import Spider
- class DmozSpider(Spider):
- name = "dmoz"
- allowed_domains = ["dmoz.org"]
- start_urls = [
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"
- ]
- def parse(self, response):
- filename = response.url.split("/")[-2]
- open(filename, 'wb').write(response.body)
從parse函數可以看出,將鏈接的最後兩個地址取出作爲文件名進行存儲。
然後運行一下看看,在tutorial目錄下按住shift右擊,在此處打開命令窗口,輸入:
- scrapy crawl dmoz
運行結果如圖:
報錯了:
UnicodeDecodeError: 'ascii' codec can't decode byte 0xb0 in position 1: ordinal not in range(128)
運行第一個Scrapy項目就報錯,真是命運多舛。
應該是出了編碼問題,谷歌了一下找到了解決方案:
在python的Lib\site-packages文件夾下新建一個sitecustomize.py:
- import sys
- sys.setdefaultencoding('gb2312')
再次運行,OK,問題解決了,看一下結果:
最後一句INFO: Closing spider (finished)表明爬蟲已經成功運行並且自行關閉了。
包含 [dmoz]的行 ,那對應着我們的爬蟲運行的結果。
可以看到start_urls中定義的每個URL都有日誌行。
還記得我們的start_urls嗎?
http://www.dmoz.org/Computers/Programming/Languages/Python/Books
http://www.dmoz.org/Computers/Programming/Languages/Python/Resources
因爲這些URL是起始頁面,所以他們沒有引用(referrers),所以在它們的每行末尾你會看到 (referer: <None>)。
在parse 方法的作用下,兩個文件被創建:分別是 Books 和 Resources,這兩個文件中有URL的頁面內容。
那麼在剛剛的電閃雷鳴之中到底發生了什麼呢?
首先,Scrapy爲爬蟲的 start_urls屬性中的每個URL創建了一個 scrapy.http.Request 對象 ,並將爬蟲的parse 方法指定爲回調函數。
然後,這些 Request被調度並執行,之後通過parse()方法返回scrapy.http.Response對象,並反饋給爬蟲。
3.2取
爬取整個網頁完畢,接下來的就是的取過程了。
光存儲一整個網頁還是不夠用的。
在基礎的爬蟲裏,這一步可以用正則表達式來抓。
在Scrapy裏,使用一種叫做 XPath selectors的機制,它基於 XPath表達式。
如果你想了解更多selectors和其他機制你可以查閱資料:點我點我
這是一些XPath表達式的例子和他們的含義
- /html/head/title: 選擇HTML文檔<head>元素下面的<title> 標籤。
- /html/head/title/text(): 選擇前面提到的<title> 元素下面的文本內容
- //td: 選擇所有 <td> 元素
- //div[@class="mine"]: 選擇所有包含 class="mine" 屬性的div 標籤元素
以上只是幾個使用XPath的簡單例子,但是實際上XPath非常強大。
可以參照W3C教程:點我點我。
必須通過一個 Response 對象對他們進行實例化操作。
你會發現Selector對象展示了文檔的節點結構。因此,第一個實例化的selector必與根節點或者是整個目錄有關 。
在Scrapy裏面,Selectors 有四種基礎的方法(點擊查看API文檔):
- xpath():返回一系列的selectors,每一個select表示一個xpath參數表達式選擇的節點
- css():返回一系列的selectors,每一個select表示一個css參數表達式選擇的節點
- extract():返回一個unicode字符串,爲選中的數據
- re():返回一串一個unicode字符串,爲使用正則表達式抓取出來的內容
3.3xpath實驗
下面我們在Shell裏面嘗試一下Selector的用法。
實驗的網址:http://www.dmoz.org/Computers/Programming/Languages/Python/Books/
熟悉完了實驗的小白鼠,接下來就是用Shell爬取網頁了。
進入到項目的頂層目錄,也就是第一層tutorial文件夾下,在cmd中輸入:
- scrapy shell http://www.dmoz.org/Computers/Programming/Languages/Python/Books/
回車後可以看到如下的內容:
在Shell載入後,你將獲得response迴應,存儲在本地變量 response中。
所以如果你輸入response.body,你將會看到response的body部分,也就是抓取到的頁面內容:
或者輸入response.headers 來查看它的 header部分:
現在就像是一大堆沙子握在手裏,裏面藏着我們想要的金子,所以下一步,就是用篩子搖兩下,把雜質出去,選出關鍵的內容。
selector就是這樣一個篩子。
在舊的版本中,Shell實例化兩種selectors,一個是解析HTML的 hxs 變量,一個是解析XML 的 xxs 變量。
而現在的Shell爲我們準備好的selector對象,sel,可以根據返回的數據類型自動選擇最佳的解析方案(XML or HTML)。
然後我們來搗弄一下!~
要徹底搞清楚這個問題,首先先要知道,抓到的頁面到底是個什麼樣子。
比如,我們要抓取網頁的標題,也就是<title>這個標籤:
可以輸入:
- sel.xpath('//title')
結果就是:
這樣就能把這個標籤取出來了,用extract()和text()還可以進一步做處理。
備註:簡單的羅列一下有用的xpath路徑表達式:
表達式 | 描述 |
---|---|
nodename | 選取此節點的所有子節點。 |
/ | 從根節點選取。 |
// | 從匹配選擇的當前節點選擇文檔中的節點,而不考慮它們的位置。 |
. | 選取當前節點。 |
.. | 選取當前節點的父節點。 |
@ | 選取屬性。 |
全部的實驗結果如下,In[i]表示第i次實驗的輸入,Out[i]表示第i次結果的輸出(建議大家參照:W3C教程):
- In [1]: sel.xpath('//title')
- Out[1]: [<Selector xpath='//title' data=u'<title>Open Directory - Computers: Progr'>]
- In [2]: sel.xpath('//title').extract()
- Out[2]: [u'<title>Open Directory - Computers: Programming: Languages: Python: Books</title>']
- In [3]: sel.xpath('//title/text()')
- Out[3]: [<Selector xpath='//title/text()' data=u'Open Directory - Computers: Programming:'>]
- In [4]: sel.xpath('//title/text()').extract()
- Out[4]: [u'Open Directory - Computers: Programming: Languages: Python: Books']
- In [5]: sel.xpath('//title/text()').re('(\w+):')
- Out[5]: [u'Computers', u'Programming', u'Languages', u'Python']
當然title這個標籤對我們來說沒有太多的價值,下面我們就來真正抓取一些有意義的東西。
使用火狐的審查元素我們可以清楚地看到,我們需要的東西如下:
我們可以用如下代碼來抓取這個<li>標籤:
- sel.xpath('//ul/li')
從<li>標籤中,可以這樣獲取網站的描述:
- sel.xpath('//ul/li/text()').extract()
可以這樣獲取網站的標題:
- sel.xpath('//ul/li/a/text()').extract()
可以這樣獲取網站的超鏈接:
- sel.xpath('//ul/li/a/@href').extract()
當然,前面的這些例子是直接獲取屬性的方法。
我們注意到xpath返回了一個對象列表,
那麼我們也可以直接調用這個列表中對象的屬性挖掘更深的節點
(參考:Nesting selectors andWorking with relative XPaths in the Selectors):
sites = sel.xpath('//ul/li')
for site in sites:
title = site.xpath('a/text()').extract()
link = site.xpath('a/@href').extract()
desc = site.xpath('text()').extract()
print title, link, desc
3.4xpath實戰
我們用shell做了這麼久的實戰,最後我們可以把前面學習到的內容應用到dmoz_spider這個爬蟲中。
在原爬蟲的parse函數中做如下修改:
- from scrapy.spider import Spider
- from scrapy.selector import Selector
- class DmozSpider(Spider):
- name = "dmoz"
- allowed_domains = ["dmoz.org"]
- start_urls = [
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"
- ]
- def parse(self, response):
- sel = Selector(response)
- sites = sel.xpath('//ul/li')
- for site in sites:
- title = site.xpath('a/text()').extract()
- link = site.xpath('a/@href').extract()
- desc = site.xpath('text()').extract()
- print title
注意,我們從scrapy.selector中導入了Selector類,並且實例化了一個新的Selector對象。這樣我們就可以像Shell中一樣操作xpath了。
我們來試着輸入一下命令運行爬蟲(在tutorial根目錄裏面):
scrapy crawl dmoz
運行結果如下:
果然,成功的抓到了所有的標題。但是好像不太對啊,怎麼Top,Python這種導航欄也抓取出來了呢?
我們只需要紅圈中的內容:
看來是我們的xpath語句有點問題,沒有僅僅把我們需要的項目名稱抓取出來,也抓了一些無辜的但是xpath語法相同的元素。
審查元素我們發現我們需要的<ul>具有class='directory-url'的屬性,
那麼只要把xpath語句改成sel.xpath('//ul[@class="directory-url"]/li')即可
將xpath語句做如下調整:
- from scrapy.spider import Spider
- from scrapy.selector import Selector
- class DmozSpider(Spider):
- name = "dmoz"
- allowed_domains = ["dmoz.org"]
- start_urls = [
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"
- ]
- def parse(self, response):
- sel = Selector(response)
- sites = sel.xpath('//ul[@class="directory-url"]/li')
- for site in sites:
- title = site.xpath('a/text()').extract()
- link = site.xpath('a/@href').extract()
- desc = site.xpath('text()').extract()
- print title
成功抓出了所有的標題,絕對沒有濫殺無辜:
3.5使用Item
接下來我們來看一看如何使用Item。
前面我們說過,Item 對象是自定義的python字典,可以使用標準字典語法獲取某個屬性的值:
- >>> item = DmozItem()
- >>> item['title'] = 'Example title'
- >>> item['title']
- 'Example title'
作爲一隻爬蟲,Spiders希望能將其抓取的數據存放到Item對象中。爲了返回我們抓取數據,spider的最終代碼應當是這樣:
- from scrapy.spider import Spider
- from scrapy.selector import Selector
- from tutorial.items import DmozItem
- class DmozSpider(Spider):
- name = "dmoz"
- allowed_domains = ["dmoz.org"]
- start_urls = [
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
- "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"
- ]
- def parse(self, response):
- sel = Selector(response)
- sites = sel.xpath('//ul[@class="directory-url"]/li')
- items = []
- for site in sites:
- item = DmozItem()
- item['title'] = site.xpath('a/text()').extract()
- item['link'] = site.xpath('a/@href').extract()
- item['desc'] = site.xpath('text()').extract()
- items.append(item)
- return items
4.存儲內容(Pipeline)
保存信息的最簡單的方法是通過Feed exports,主要有四種:JSON,JSON lines,CSV,XML。
我們將結果用最常用的JSON導出,命令如下:
- scrapy crawl dmoz -o items.json -t json
-o 後面是導出文件名,-t 後面是導出類型。
然後來看一下導出的結果,用文本編輯器打開json文件即可(爲了方便顯示,在item中刪去了除了title之外的屬性):
因爲這個只是一個小型的例子,所以這樣簡單的處理就可以了。
如果你想用抓取的items做更復雜的事情,你可以寫一個 Item Pipeline(條目管道)。
這個我們以後再慢慢玩^_^