爬蟲進(jìn)階 - 前后端分離有什么了不起,過程超詳細(xì)
回復(fù)“書籍”即可獲贈(zèng)Python從入門到進(jìn)階共10本電子書

這是一個(gè)詳細(xì)的爬蟲進(jìn)階教程,里面包含了很詳細(xì)的思考和試錯(cuò)過程,如果你對學(xué)爬蟲是認(rèn)真的,建議認(rèn)真看。
我們要抓取下面這個(gè)網(wǎng)站上的所有圖書列表:
https://www.epubit.com/books

1) 探索研究
創(chuàng)建一個(gè)新的python文件,寫入如下代碼:
import requests
url = 'https://www.epubit.com/books'
res = requests.get(url)
print(res.text)
運(yùn)行發(fā)現(xiàn)打印結(jié)果如下:

這里面根本沒有圖書的信息。但使用瀏覽器檢查器可以看到圖書的信息:

我們碰到了一個(gè)基于前后端分離的網(wǎng)站,或者說一個(gè)用JavaScript獲取數(shù)據(jù)的網(wǎng)站。這種網(wǎng)站的數(shù)據(jù)流程是這樣的:
初次請求只返回了網(wǎng)頁的基本框架,并沒有數(shù)據(jù)。就是前面截圖看到那樣。 但網(wǎng)頁的基本框架中包含JavaScript的代碼,這段代碼會(huì)再發(fā)起一次或者多次請求獲取數(shù)據(jù)。我們稱為后續(xù)請求。
為了抓取這樣的網(wǎng)站,有兩個(gè)辦法:
分析出后續(xù)請求的地址和參數(shù),寫代碼發(fā)起同樣的后續(xù)請求。 使用模擬瀏覽器技術(shù),比如selenium。這種技術(shù)可以自動(dòng)發(fā)起后續(xù)請求獲取數(shù)據(jù)。
2) 分析后續(xù)請求
打開谷歌瀏覽器的檢查器,按圖中的指示操作:
點(diǎn)擊Network,這里可以查看瀏覽器發(fā)送的所有網(wǎng)絡(luò)請求。 選XHR,查看瀏覽器用JavaScript發(fā)送的請求。 下面可以看到很多請求。我們要一個(gè)個(gè)看過去找到包含商品列表的請求。
再來理解一下瀏覽器打開一個(gè)網(wǎng)頁的過程,一般并不是一個(gè)請求返回了所有的內(nèi)容,而是包含多個(gè)步驟:
第一個(gè)請求獲得HTML文件,里面可能包含文字,數(shù)據(jù),圖片的地址,樣式表地址等。HTML文件中并沒有直接包含圖片。 瀏覽器根據(jù)HTML中的鏈接,再次發(fā)送請求,讀取圖片,樣式表,基于JavaScript的數(shù)據(jù)等。
所以我們看到有這么不同類型的請求:XHR, JS,CSS,Img,F(xiàn)ont, Doc等。
我們爬取的網(wǎng)站發(fā)送了很多個(gè)XHR請求,分別用來請求圖書列表,網(wǎng)頁的菜單,廣告信息,頁腳信息等。我們要從這些請求中找出圖書的請求。
具體操作步驟如圖:
在左邊選中請求 在右邊選擇Response 下面可以看到這個(gè)請求返回的數(shù)據(jù),從數(shù)據(jù)可以判斷是否包含圖書信息。
Javascript請求返回的格式通常是JSON格式,這是一種JavaScript的數(shù)據(jù)格式,里面包含用冒號隔開的一對對數(shù)據(jù),比較容易看懂。JSON很像Python中的字典。
在眾多的請求中,可以根據(jù)請求的名字大致判斷,提高效率。比如上圖中g(shù)etUBookList看起來就像是獲取圖書列表。點(diǎn)開查看,返回的果然是圖書列表。
請記住這個(gè)鏈接的地址和格式,后面要用到:

https://www.epubit.com/pubcloud/content/front/portal/getUbookList?page=1&row=20&=&startPrice=&endPrice=&tagId= 分析一下,可以看到:
網(wǎng)址是:https://www.epubit.com/pubcloud/content/front/portal/getUbookList page=1表示第1頁,我們可以依次傳入2,3,4等等。 row=20表示每一頁有20本書 startPrice和endPrice表示價(jià)格條件,他們的值都是空,表示不設(shè)定價(jià)格限制。
3) 使用postman測試猜想
為了驗(yàn)證這個(gè)設(shè)想打開谷歌瀏覽器,在地址欄中輸入以下網(wǎng)址:
https://www.epubit.com/pubcloud/content/front/portal/getUbookList?page=1&row=20&=&startPrice=&endPrice=&tagId=
可是得到了如下的返回結(jié)果:
{
"code": "-7",
"data": null,
"msg": "系統(tǒng)臨時(shí)開小差,請稍后再試~",
"success": false
}
這并不是系統(tǒng)出了問題,而是系統(tǒng)檢測到我們是非正常的請求,拒絕給我們返回?cái)?shù)據(jù)。
這說明除了發(fā)送這個(gè)URL,還需要給服務(wù)器傳送額外的信息,這些信息叫做Header,翻譯成中文是請求頭的意思。
在下圖中可以看到正常的請求中包含了多個(gè)請求頭:
選中要查看的請求 在右邊選Headers 往下翻,可以看到Request Headers,下面就是一項(xiàng)項(xiàng)數(shù)據(jù): Accept: application/json, text/plain, / Accept-Encoding:gzip, deflate, br ....
為了讓服務(wù)器正常處理請求,我們要模擬正常的請求,也添加相應(yīng)的header。如果給的Header也都一樣,服務(wù)器根本不可能識別出我們是爬蟲。后面我們會(huì)學(xué)習(xí)如何在發(fā)送請求時(shí)添加header。
但通常服務(wù)器并不會(huì)檢查所有的Header,可能只要添加一兩個(gè)關(guān)鍵Header就可以騙服務(wù)器給我們數(shù)據(jù)了。但我們要一個(gè)個(gè)測試那些Header是必須的。
在瀏覽器中無法添加Header,為了發(fā)送帶Header的HTTP請求,我們要使用另一個(gè)軟件叫做Postman。這是一個(gè)API開發(fā)者和爬蟲工程師最常使用的工具之一。
首先在postman的官網(wǎng)下載:www.postman.com。根據(jù)指示一步步安裝軟件,中間沒有額外的設(shè)置。
打開postman后可以看到如下界面:

在最上面點(diǎn)擊加號,可以添加一個(gè)新的請求 中間填寫請求的URL 點(diǎn)Headers進(jìn)入Headers的設(shè)置界面,添加Header。
這些Header的名字和值可以在檢查器中復(fù)制過來。如果自己拼寫,注意千萬不要寫錯(cuò)。
我們來了解一下幾個(gè)常見的header:
User-Agent: 這個(gè)Header表示請求者是誰,一般是一個(gè)包括詳細(xì)版本信息的瀏覽器的名字,比如:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
如果爬蟲不添加這個(gè)Header,服務(wù)器一下就能識別出這是不正常請求,可以予以拒絕。當(dāng)然,是否拒絕取決于程序員的代碼邏輯。
Cookie: 如果一個(gè)網(wǎng)站需要登錄,登錄的信息就保存在Cookie中。服務(wù)器通過這個(gè)Header判定是否登陸了,登陸的是誰。
假設(shè)我們要自動(dòng)在京東商城下單,我們可以先人工登錄,復(fù)制Cookie的值,用Python發(fā)送請求并包含這個(gè)Cookie,這樣服務(wù)器就認(rèn)為我們已經(jīng)登陸過了,允許我們下單或做其他操作。如果在程序中加上計(jì)時(shí)的功能,指定具體下單的時(shí)間點(diǎn),這就是秒殺程序。這是爬取需要登錄的網(wǎng)站的一種常用方法。
Accept:指瀏覽器接受什么格式的數(shù)據(jù),比如**application/json, text/plain, */***是指接受JSON,文本數(shù)據(jù),或者任何數(shù)據(jù)。
Origin-Domain: 是指請求者來自那個(gè)域名,這個(gè)例子中是:www.epubit.com
關(guān)于更多的HTTP的Header,可以在網(wǎng)上搜索HTTP Headers學(xué)習(xí)。
我一個(gè)個(gè)添加常用的Header,但服務(wù)器一直不返回?cái)?shù)據(jù),直到添加了Origin-Domain這個(gè)Header。這說明這個(gè)Header是必備條件。
網(wǎng)頁的后臺程序有可能不檢查Header,也有可能檢查一個(gè)Header,也有可能檢查多個(gè)Header,這都需要我們嘗試才能知道。
既然Origin-Domain是關(guān)鍵,也許后臺程序只檢查這一個(gè)Header,我們通過左邊的選擇框去掉其他的Header,只保留Origin-Domain,請求仍然成功,這說明后臺只檢查了這一個(gè)Header:

然后修改地址欄中的page參數(shù),獲取其他的頁,比如截圖中修改成了3,再發(fā)送請求,發(fā)現(xiàn)服務(wù)器返回了新的數(shù)據(jù)(其他的20本書)。這樣我們的請求過程就成功了。
4) 寫抓取程序
開發(fā)爬蟲,主要的時(shí)間是分析,一旦分析清楚了,爬取代碼并不復(fù)雜:
import requests
def get_page(page=1):
'''抓取指定頁的數(shù)據(jù),默認(rèn)是第1頁'''
# 使用page動(dòng)態(tài)拼接URL
url = f'https://www.epubit.com/pubcloud/content/front/portal/getUbookList?page={page}&row=20&=&startPrice=&endPrice=&tagId='
headers = {'Origin-Domain': 'www.epubit.com'}
# 請求的時(shí)候同時(shí)傳入headers
res = requests.get(url, headers=headers)
print(res.text)
get_page(5)
這里我們測試了抓取第5頁的數(shù)據(jù),比對打印出的JSON數(shù)據(jù)和網(wǎng)頁上的第5頁數(shù)據(jù),結(jié)果是匹配的。
現(xiàn)在我們?nèi)シ治鯦SON的數(shù)據(jù)結(jié)構(gòu),再來完善這個(gè)程序。
5) 分析JSON數(shù)據(jù)
JSON就像Python中的字典,用大括號存放數(shù)據(jù),用冒號分割鍵和值。下面是省略的JSON數(shù)據(jù):
{
"code": "0",
"data": {
"current": 1, //第一頁
"pages": 144, //一共幾頁
"records": [ //很多本書的信息放在方括號中
{
"authors": "[美] 史蒂芬·普拉達(dá)(Stephen Prata)", //作者
"code": "UB7209840d845c9", //代碼
"collectCount": 416, //喜歡數(shù)
"commentCount": 64, //評論數(shù)
"discountPrice": 0, //折扣價(jià)
"downebookFlag": "N",
"fileType": "",
...
},
{
"authors": "笨叔",
"code": "UB7263761464b35",
"collectCount": 21,
"commentCount": 3,
"discountPrice": 0,
"downebookFlag": "N",
"fileType": "",
...
},
...
],
"size": 20,
"total": 2871
},
"msg": "成功",
"success": true
}
我們來學(xué)習(xí)一下這個(gè)JSON格式:
最外面是一個(gè)大括號,里面包含了code, data, msg, success四塊信息。這個(gè)格式是開發(fā)這個(gè)網(wǎng)頁的程序員自己設(shè)計(jì)的,不同的網(wǎng)頁可能不同。 其中code, msg和sucess表示請求的狀態(tài)碼,請求返回的提示,請求是否成功。而真正的數(shù)據(jù)都在data中。 data的冒號后面是一個(gè)大括號,表示一個(gè)數(shù)據(jù)對象。里面包含了當(dāng)前頁數(shù)(current),總頁數(shù)(pages),書的信息(records)等。 records表示很多本書,所以它用一個(gè)方括號表示,方括號里面又有很多大括號包起來的數(shù)據(jù)對象,每個(gè)大括號表示一本書。
{
"authors": "[美] 史蒂芬·普拉達(dá)(Stephen Prata)", //書名
"code": "UB7209840d845c9", //代碼
"collectCount": 416, //喜歡數(shù)
"commentCount": 64, //評論數(shù)
"discountPrice": 0, //折扣0,表示沒有折扣
...
"forSaleCount": 3, //在售數(shù)量
...
"logo": "https://cdn.ptpress.cn/pubcloud/bookImg/A20190961/20200701F892C57D.jpg",
"name": "C++ Primer Plus 第6版 中文版", //書名
...
"price": 100.30, //價(jià)格
...
}
每本書的信息有很多個(gè)字段,這里省略掉了很多字段,給重要的信息添加了注釋。
6) 完成程序
現(xiàn)在來完善上面的程序,從JSON中解析出我們要的數(shù)據(jù),為了簡化,我們只抓?。簳髡?,編號和價(jià)格。
程序框架:
import requests
import json
import time
class Book:
# --省略--
def get_page(page=1):
# --省略--
books = parse_book(res.text)
return books
def parse_book(json_text):
#--省略--
all_books = []
for i in range(1, 10):
print(f'======抓取第{i}頁======')
books = get_page(i)
for b in books:
print(b)
all_books.extend(books)
print('抓完一頁,休息5秒鐘...')
time.sleep(5)
定義了Book類來表示一本書 添加了parse_book函數(shù)負(fù)責(zé)解析數(shù)據(jù),返回包含當(dāng)前頁的20本書的list 最下面使用for循環(huán)抓取數(shù)據(jù),并放到一個(gè)大的列表中,range中添加要抓取的頁數(shù)。通過前面的分析可以知道一共有幾頁。 抓取完一頁后,一定要sleep幾秒,一是防止給網(wǎng)站帶來太大壓力,二是防止網(wǎng)站會(huì)封鎖你的IP,是為他好,也是為了自己好。 把抓來的信息保存到文件中的代碼,請自行完成。
下面來看看,被省略掉的部分:
Book類:
class Book:
def __init__(self, name, code, author, price):
self.name = name
self.code = code
self.author = author
self.price = price
def __str__(self):
return f'書名:{self.name},作者:{self.author},價(jià)格:{self.price},編號:{self.code}'
下面是__str__函數(shù)是一個(gè)魔法函數(shù),當(dāng)我們使用print打印一個(gè)Book對象的時(shí)候,Python會(huì)自動(dòng)調(diào)用這個(gè)函數(shù)。
parse_book函數(shù):
import json
def parse_book(json_text):
'''根據(jù)返回的JSON字符串,解析書的列表'''
books = []
# 把JSON字符串轉(zhuǎn)成一個(gè)字典dict類
book_json = json.loads(json_text)
records = book_json['data']['records']
for r in records:
author = r['authors']
name = r['name']
code = r['code']
price = r['price']
book = Book(name, code, author, price)
books.append(book)
return books
在最上面import了json模塊,這是Python自帶的,不用安裝 關(guān)鍵的代碼就是使用json把抓來的JSON字符串轉(zhuǎn)成字典,剩下的是對字典的操作,就很容易理解了。
抓取基于JavaScript的網(wǎng)頁,復(fù)雜主要在于分析過程,一旦分析完成了,抓取的代碼比HTML的頁面還要更簡單清爽。
------------------- End -------------------
往期精彩文章推薦:

歡迎大家點(diǎn)贊,留言,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持
想加入Python學(xué)習(xí)群請?jiān)诤笈_回復(fù)【入群】
萬水千山總是情,點(diǎn)個(gè)【在看】行不行
/今日留言主題/
隨便說一兩句吧~~
