我的第一個Python爬蟲——談心得

跬步至以千里發表於2018-03-30

2018年3月27日,繼開學以來,開了軟體工程和資訊系統設計,想來想去也沒什麼好的題目,乾脆就想弄一個實用點的,於是產生了做“學生服務系統”想法。相信各大高校應該都有本校APP或超級課程表之類的軟體,在資訊化的時代能快速收集/查詢自己想要的諮詢也是種很重要的能力,所以記下了這篇部落格,用於總結我所學到的東西,以及用於記錄我的第一個爬蟲的初生。

一、做爬蟲所需要的基礎

要做一隻爬蟲,首先就得知道他會幹些什麼,是怎樣工作的。所以得有一些關於HTML的前置知識,這一點做過網頁的應該最清楚了。
   HTML(超文字標記語言),是一種標記性語言,本身就是一長串字串,利用各種類似 < a >,< /a>這樣的標籤來識別內容,然後通過瀏覽器的實現標準來翻譯成精彩的頁面。當然,一個好看的網頁並不僅僅只有HTML,畢竟字串是靜態的,只能實現靜態效果,要作出漂亮的網頁還需要能美化樣式的CSS和實現動態效果的JavaScipt,只要是瀏覽器都是支援這些玩意兒的。
   嗯,我們做爬蟲不需要了解太多,只需要瞭解HTML是基於文件物件模型(DOM)的,以樹的結構,儲存各種標記,就像這樣:
   DOM的百度百科
   之後會用到這種思想來在一大堆HTML字串中找出我們想要的東西。

瞭解了這個然後還得了解網頁和伺服器之間是怎麼通訊的,這就得稍微瞭解點HTTP協議,基於TCP/IP的應用層協議,規定了瀏覽器和伺服器之間的通訊規則,簡單粗暴的介紹幾點和爬蟲相關的就是:

瀏覽器和伺服器之間有如下幾種通訊方式:
   GET:向伺服器請求資源,請求以明文的方式傳輸,一般就在URL上能看到請求的引數
   POST:從網頁上提交表單,以報文的形式傳輸,請求資源
   還有幾種比較少見就不介紹了。

瞭解了這兩點就可以準備工具了,當然,對爬蟲有興趣還可以瞭解一下爬蟲的發展史。

二、介紹幾款優秀製作爬蟲的輔助工具

由於我是採用python3.6開發的,然後從上文的介紹中,也該知道了一隻爬蟲是需要從HTML中提取內容,以及需要和網頁做互動等。
   如果不採用爬蟲框架的話,我建議採用:
   
    BeautifulSoup 庫 ,一款優秀的HTML/XML解析庫,採用來做爬蟲,
              不用考慮編碼,還有中日韓文的文件,其社群活躍度之高,可見一斑。
              [] 這個在解析的時候需要一個解析器,在文件中可以看到,推薦lxml
              
    Requests 庫,一款比較好用的HTTP庫,當然python自帶有urllib以及urllib2等庫,
            但用起來是絕對沒有這款舒服的,哈哈
           
    Fiddler. 工具,這是一個HTTP抓包軟體,能夠截獲所有的HTTP通訊。
          如果爬蟲執行不了,可以從這裡尋找答案,官方連結可能進不去,可以直接百度下載

爬蟲的輔助開發工具還有很多,比如Postman等,這裡只用到了這三個,相信有了這些能減少不少開發阻礙。

三、最簡單的爬蟲試例

最簡單的爬蟲莫過於單執行緒的靜態頁面了,這甚至都不能叫爬蟲,單單一句正規表示式即可匹配出所有內容,比如各種榜單:豆瓣電影排行榜,這類網站爬取規則變化比較少,用瀏覽器自帶的F12的審查很容易找到需要爬取資訊的特徵:這裡寫圖片描述
    見到花花綠綠的HTML程式碼不要害怕,一個一個點,直到找到需要的資訊就行了,可以看到所有電影名都是在這樣

<div class = "pl2">

之下的,每有一個這樣的標籤就代表一個電影,從他的孩子< span >中即可抓取到電影名。
程式碼如下:

from bs4 import BeautifulSoup
from lxml import html
import xml
import requests

url = "https://movie.douban.com/chart"
f = requests.get(url)                 #Get該網頁從而獲取該html內容
soup = BeautifulSoup(f.content, "lxml")  #用lxml解析器解析該網頁的內容, 好像f.text也是返回的html
#print(f.content.decode())								#嘗試列印出網頁內容,看是否獲取成功
#content = soup.find_all('div',class_="p12" )   #嘗試獲取節點,因為calss和關鍵字衝突,所以改名class_

for k in soup.find_all('div',class_='pl2'):#,找到div並且class為pl2的標籤
   a = k.find_all('span')       #在每個對應div標籤下找span標籤,會發現,一個a裡面有四組span
   print(a[0].string)            #取第一組的span中的字串

抓取結果如下:
這裡寫圖片描述
    乍一看,就這麼個玩意兒,這些電影名還不如直接自己去網頁看,這有什麼用呢?但是,你想想,只要你掌握了這種方法,如果有翻頁你可以按照規則爬完了一頁就解析另外一頁HTML(通常翻頁的時候URL會規律變化,也就是GET請求實現的翻頁),也就是說,只要掌握的爬取方法,無論工作量有多麼大都可以按你的心思去收集想要的資料了。

四、需要模擬登入後再爬取的爬蟲所需要的資訊

4.1.登入分析

剛才的爬蟲未免太簡單,一般也不會涉及到反爬蟲方面,這一次分析需要登入的頁面資訊的爬取,按照往例,首先開啟一個網頁:
    我選擇了我學校資訊服務的網站,登入地方的程式碼如下:
       這裡寫圖片描述
    可以看到驗證碼都沒有,就只有賬號密碼以及提交。光靠猜的當然是不行的,一般輸入密碼的地方都是POST請求。
    POST請求的響應流程就是 客戶在網頁上填上伺服器準備好的表單並且提交,然後伺服器處理表單做出迴應。一般就是使用者填寫帳號、密碼、驗證碼然後把這份表單提交給伺服器,伺服器從資料庫進行驗證,然後作出不同的反應。在這份POST表單中可能還有一些不需要使用者填寫的用指令碼生成的隱藏屬性作為反爬蟲的手段。
    要知道表單格式可以先試著隨便登入一次,然後在F12中的network中檢視登入結果,如圖:
    圖1                                圖1
    這裡寫圖片描述                                 圖2

】如果用真正的賬號密碼登入,要記住勾選上面的Preserve log,這樣即使網頁發生了跳轉之前的資訊也還在。
從上面的兩張圖中很容易發現其中的一個POST請求, login?serv…就是登入請求了
可以看到這個登入請求所攜帶的資訊有:
General: 記錄了請求方式,請求地址,以及伺服器返回的狀態號 200等
Response Headers: 響應頭,HTTP響應後傳輸的頭部訊息
Request Headers: 請求頭,重點!!,向伺服器傳送請求時,發出的頭部訊息,之中很多引數都是爬蟲需要模擬出來傳送給伺服器的。
From Data:表單,重點!!,在這裡表單中有:

username: 12345
password: MTIzNDU=
lt: e1s1
_eventId: submit

我明明都填的12345,為什麼密碼變了呢?可以看出這密碼不是原始值,應該是編碼後的產物,網站常用的幾種編碼/加密方法就幾種,這裡是採用的base64編碼,如果對密碼編碼的方式沒有頭緒可以仔細看看登入前後頁面的前端指令碼。運氣好可以看到encode函式什麼的。

4.2資訊提取

如果瞭解過Resquests庫的文件就知道,傳送一個一般的POST請求所需要的引數構造是這樣的:

 r = requests.post(url,[data],[header],[json],[**kwargs])
 /*
url -- URL for the new Request object.
data -- (optional) Dictionary, bytes, or file-like object to send in the body of the Request.
json -- (optional) json to send in the body of the Request.
**kwargs -- Optional arguments that request takes.
*/

從上面的兩張圖片中即可找到傳送一個正確的請求所需要的引數,即 urldata
   url 即上面的 Request URL:
Request URL: http://uia.hnist.cn/sso/login?service=http%3A%2F%2Fportal.hnist.cn%2Fuser%2FsimpleSSOLogin
   data 即上面的From data:

username: 12345
password: MTIzNDU=
lt: e1s1
_eventId: submit

收集到了必要的資訊還得了解三點:
   一、登入後的網頁和伺服器建立了聯絡,所以能和伺服器進行通訊,但即使你從這個網頁點選裡面的超連結跳轉到另外一個子網頁,在新網頁中還是保持登入狀態的在不斷的跳轉中是怎麼識別使用者的呢?
   在這裡,伺服器端一般是採用的Cookie技術,登陸後給你一個Cookie,以後你發出跳轉網頁的請求就攜帶該Cookie,伺服器就能知道是你在哪以什麼狀態點選的該頁面,也就解決了HTTP傳輸的無狀態問題。
   很明顯,在模擬登入以後保持登入狀態需要用得著這個Cookie,當然Cookie在請求頭中是可見的,為了自己的賬號安全,請不要輕易暴露/洩漏自己的Cookie

二、先了解一下,用python程式訪問網頁的請求頭的User-Agent是什麼樣的呢?沒錯,如下圖所示,很容易分辨這是程式的訪問,也就是伺服器知道這個請求是爬蟲訪問的結果,如果伺服器做了反爬蟲措施程式就會訪問失敗,所以需要程式模擬瀏覽器頭,讓對方伺服器認為你是使用某種瀏覽器去訪問他們的。
   這裡寫圖片描述
  
   三、查詢表單隱藏引數的獲取方式,在上文表單列表中有個lt引數,雖然我也不知道他是幹嘛的,但通過POST傳輸過去的表單肯定是會經過伺服器驗證的,所以需要弄到這份引數,而這份引數一般都會在HTML頁面中由JS指令碼自動生成,可以由Beautifulsoup自動解析抓取。  
  
關於Fiddler的使用和請求資訊相關資訊可以檢視連結:https://zhuanlan.zhihu.com/p/21530833?refer=xmucpp
嗯,最重要的幾樣東西已經收集完畢,對Cookie和請求頭的作用也有了個大概的瞭解,然後開始傳送請求試試吧~

五、開始編碼爬蟲

如果用urllib庫傳送請求,則需要自己編碼Cookie這一塊(雖然也只要幾行程式碼),但用Requests庫就不需要這樣,在目前最新版本中,requests.Session提供了自己管理Cookie的永續性以及一系列配置,可以省事不少。
   先以面對過程的方式實驗地去編碼:

from bs4 import BeautifulSoup
from lxml import html
import requests
####################################################################################
#  在這先準備好請求頭,需要爬的URL,表單引數生成函式,以及建立會話
############################# 1 #################################################
header={
    "Accept": "text/html, application/xhtml+xml, image/jxr, */*",
    "Referer": "http://uia.hnist.cn/sso/login?service=http%3A%2F%2Fportal.hnist.\
			    cn%2Fuser%2FsimpleSSOLogin",    
    "Accept-Language": "zh-Hans-CN,zh-Hans;q=0.8,en-US;q=0.5,en;q=0.3",
    "Content-Type": "application/x-www-form-urlencoded",
    "Accept-Encoding": "gzip, deflate",
    "Connection": "Keep-Alive",
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36",
    "Accept-Encoding": "gzip, deflate",
    "Origin": "http://uia.hnist.cn",
    "Upgrade-Insecure-Requests": "1",
   #Cookie由Session管理,這裡不用傳遞過去,千萬不要亂改頭,我因為改了頭的HOST坑了我兩天
}  

School_login_url = 'http://uia.hnist.cn/sso/login? \
service=http%3A%2F%2Fportal.hnist.cn%2Fuser%2FsimpleSSOLogin'#學校登入的URL

page = requests.Session()     #用Session發出請求能自動處理Cookie等問題
page.headers = header		 #為所有請求設定頭
page.get(School_login_url)    #Get該地址建立連線(通常GET該網址後,伺服器會傳送一些用於\
					驗證的引數用於識別使用者,這些引數在這就全由requests.Session處理了)


def Get_lt():	#獲取引數 lt 的函式
    f = requests.get(School_login_url,headers = header)
    soup = BeautifulSoup(f.content, "lxml")  
    once = soup.find('input', {'name': 'lt'})['value']
    return once

lt = Get_lt()  #獲取lt

From_Data = {   #表單
    'username': 'your username',
    'password': 'Base64 encoded password',   
    #之前說過密碼是通過base64加密過的,這裡得輸入加密後的值,或者像lt一樣寫個函式
    'lt': lt,
    '_eventId': 'submit',
}
############################# 1 end #############################

################################################################
#  在這一段向登入網站傳送POST請求,並判斷是否成功返回正確的內容
############################# 2 #################################

q = page.post(School_login_url,data=From_Data,headers=header) 
#傳送登陸請求

#######檢視POST請求狀態##############
#print(q.url)	#這句可以檢視請求的URL
#print(q.status_code)  #這句可以檢視請求狀態
#for (i,j) in q.headers.items():
#    print(i,':',j)		#這裡可以檢視響應頭
#print('\n\n')
#for (i,j) in q.request.headers.items():
#    print(i,':',j)		#這裡可以檢視請求頭
####上面的內容用於判斷爬取情況,也可以用fiddle抓包檢視 ####

f = page.get('http://uia.hnist.cn')	#GET需要登入後(攜帶cookie)才能檢視的網站
print("body:",f.text)

######## 進入查成績網站,找到地址,請求並接收內容 #############

proxies = {  #代理地址,這裡代理被註釋了,對後面沒影響,這裡也不需要使用代理....
#"http": "http://x.x.x.x:x",
#"https": "http://x.x.x.x:x",
}

########  查成績網站的text格式表單,其中我省略了很多...######
str = """callCount=1
httpSessionId=DA0080E0317A1AD0FDD3E09E095CB4B7.portal254
scriptSessionId=4383521D7E8882CB2F7AB18F62EED380
page=/web/guest/788
"""
#### 這是由於該伺服器關於表單提交部分設計比較垃圾,所以不用去在意表單內容含義 ###

f = page.post('http://portal.hnist.cn/portal_bg_ext/dwr/plainjs/
ShowTableAction.showContent.dwr',\data=str,proxies=proxies)
 #查成績的地址,表單引數為上面的str
  
######  檢視地址,返回狀態,以及原始內容#######"""
print("f:",f.url)
print(f.status_code)
text = f.content.decode('unicode_escape')
print(text.encode().decode()) #因為原始內容中有\uxxx形式的編碼,所以使用這句解碼
###########################################"""
################################### 2 end #########################

###################################################################
#  解析獲得的內容,並清洗資料,格式化輸出...
############################# 3 ####################################




[] 如果使用了Fiddler,他會自動為Web的訪問設定一個代理,這時候如果你關閉了Fiddler可能爬蟲會無法正常工作,這時候你選擇瀏覽器直連,或者設定爬蟲的代理為Fiddler即可。
[注2]爬蟲不要頻率太快,不要影響到別人伺服器的正常執行,如果不小心IP被封了可以使用代理(重要資料不要使用不安全的代理),網上有很多收費/免費的代理,可以去試下。

過程中獲得的經驗:

  • 在上面第一部分,不知道作用的引數不要亂填,只需要填幾個最重要的就夠了,比如UA,有時候填了不該填的請求將會返回錯誤狀態.,儘量把可分離的邏輯寫成函式來呼叫,比如生成的表單引數,加密方法等.
  • 在上面第二部分如果請求失敗可以配合抓包軟體檢視程式和瀏覽器傳送的請求有什麼差別,遺漏了什麼重要的地方,儘量讓程式模仿瀏覽器的必要的行為。
  • 第三部分中,因為拿到的資料是如下圖1這樣的,所以需要最後輸出後decode,然後再使用正規表示式提取出雙引號中的內容連線誒成一個標記語言的形式,再使用Beautifulsoup解析獲得需要的資料,如圖2.
  • 中途可能利用的工具有:
    官方正規表示式學習網站
    HTML格式美化
    正規表示式測試

圖1
                                 圖1     
圖2
                     圖2

六、爬蟲技術的擴充與提高

  經歷了困難重重,終於得到了想要的資料,對於非同步請求,使用JS渲染頁面後才展示資料的網頁,又或是使用JS程式碼加密過的網頁,如果花時間去分析JS程式碼來解密,簡單的公有的加密方法倒是無所謂,但對於特別難的加密就有點費時費力了,在要保持抓取效率的情況下可以使用能使用Splash框架:
  這是一個Javascript渲染服務,它是一個實現了HTTP API的輕量級瀏覽器,Splash是用Python實現的,同時使用Twisted和QT。Twisted(QT)用來讓服務具有非同步處理能力,以發揮webkit的併發能力。
  就比如像上面返回成績地址的表單引數,格式為text,並且無規律,有大幾十行,如果要弄明白每個引數是什麼意思,還不如載入瀏覽器的JS 或 使用瀏覽器自動化測試軟體來獲取HTML了,所以,遇到這種情況,在那麼大一段字串中,只能去猜哪些引數是必要的,哪些引數是不必要的,比如上面的,我就看出兩個是有關於返回頁面結果的,其餘的有可能存在驗證身份的,時間的什麼的。

  對於資訊的獲取源,如果另外的網站也有同樣的資料並且抓取難度更低,那麼換個網站爬可能是個更好的辦法,以及有的網站根據請求頭中的UA會產生不同的佈局和處理,比如用手機的UA可能爬取會更加簡單。

七、後記

  幾天後我發現了另一個格式較好的頁面,於是去爬那個網站,結果他是.jsp的,採用之前的方法跳轉幾個302之後就沒有後續了…後來才猜想了解到,最後一個302可能是由JS指令碼跳轉的,而我沒有執行JS指令碼的環境,也不清楚他執行的哪個指令碼,傳入了什麼引數,於是各種嘗試和對比,最後發現:正常請求時,每次都多2個Cookie,開始我想,Cookie不是由Session管理不用去插手的嗎?然後我想以正常方式獲得該Cookie,請求了N個地址,結果始終得不到想要的Cookie,於是我直接使用Session.cookies.set('COMPANY_ID','10122')新增了兩個Cookie,還真成了…神奇…
  當然,過了一段時間後,又不行了,於是仔細觀察,發現每次就JSESSIONID這一個Cookie對結果有影響,傳遞不同的值到不同的頁面還…雖然我不認同這種猜的,毫無邏輯效率的瞎試。但經歷長時間的測試和猜測,對結果進行總結和整理也是能發現其中規律的。

  關於判斷某動作是不是JS,可以在Internet選項中設定禁止使用JS

  關於失敗了驗證的方法,我強烈建議下載fiddler,利用新建檢視,把登入過程中所有的圖片,CSS等檔案去掉以後放到新檢視中,然後利用程式登入的過程也放一個檢視當中,如果沒有在響應中找到需要的Cookie,還可以在檢視中方便的檢視各個JS檔案,比瀏覽器自帶的F12好用太多了。 如下圖:
這裡寫圖片描述

總之,經過這段時間的嘗試,我對爬蟲也有了個初步的瞭解,在這方面,也有了自己做法:
  
抓包請求 —> 模仿請求頭和表單—>如果請求失敗,則仔細對比正常訪問和程式訪問的資料包 —>成功則根據內容結構進行解析—>清清洗資料並展示

相關文章