不少 Web 站都採用先後端分離的技術。之前保存用戶身份信息靠 Cookie,那先後分離這種技術組合靠什麼校驗用戶身份呢?看起來正常的數據,發送過去爲何老是 400 呢?css
scrapy 模擬登陸相信你們都會,並且很是的熟練。可是技術一直在進步(尤爲是前端領域),近幾年先後端分離的趨勢愈來愈明顯,不少 web 站都採用先後端分離的技術。之前保存用戶身份信息靠 Cookie,那先後分離這種技術組合靠什麼校驗用戶身份呢?前端
先後端分離的項目,通常都是 react、vue 等 js 庫編寫的,進而涌現出了一批優秀的前端框架或組件,如阿里巴巴前端團隊的 AntDesign,餓了麼前端團隊的 ElementUI 等。因爲先後端分離的緣由,後端一定有 API,因此最好的爬取策略不是在頁面使用 CSS 定位或者 Xpath 定位,而是觀察網絡請求記錄,找到 api 以及請求時發送的參數並用 Python 進行構造、模擬請求。vue
以這裏的登陸爲例,經過css定位其實也能夠,可是有不穩定的風險。因此仍是看api和參數比較穩妥,前端變化的概率比後端高出太多。在頁面中打開調試工具,而後定位到『網絡』選項卡,接着打開登陸頁並輸入用戶名密碼並登陸。python
在請求記錄中找到並選中方法爲 post 的那條記錄就能夠查看此請求的詳細信息,好比請求地址、請求頭和參數。請求詳情以下圖所示:react
請求參數以下圖所示: web
能夠看到請求參數中有用戶名、密碼以及用戶名類型(好比手機號或郵箱)。獲得完整的請求信息後就能夠根據請求地址、請求頭和參數來構造登陸用的代碼,Scrapy 經常使用登陸代碼以下:json
def start_requests(self):
""" 重載start_requests方法 經過is_login方法判斷是否成功登陸 """
login_url = "http://xxx.yyy.ccc.aa/api/v1/oauth/login"
login_data = {
"username": "abcd@easub.com",
"password": "faabbccddeeffggd5",
"type": "email"
}
return [scrapy.FormRequest(url=login_url, formdata=login_data, callback=self.is_login)]
def is_login(self, response):
""" 根據返回值中的message值來判斷是否登陸成功 若是登陸成功則對數據傳輸頁發起請求,並將結果回傳給parse方法 若是登陸失敗則提示 因爲後面的用戶權限驗證須要用到token信息,因此這裏取到登陸後返回的token並傳遞給下一個方法 """
results = json.loads(response.text)
if results['message'] == "succeed":
urls = 'http://xxx.yyy.ccc.aa'
access_token = results['data']['access_token']
print("登陸成功,開始調用方法")
yield Request(url=urls, callback=self.parse, meta={"access_token": access_token})
else:
print("登陸失敗,請從新檢查")
複製代碼
若是返回信息的 json 裏面 message 值爲 succeed 即認爲登陸成功並調用 parse 方法。後端
登陸完畢後想執行其餘的操做,好比上傳(post)數據的話,我應該怎麼作?api
首先要跟剛纔同樣,須要經過真實操做觀察請求記錄中對應記錄的請求詳情,根據 api 的地址和所需參數請求頭等信息用代碼進行構造,模擬真實的網絡請求發送場景。下圖爲提交表單的請求詳情信息:數組
跟上面相似,根據返回的參數和請求頭構造代碼,結果會如何?
結果返回的狀態碼是 401,因爲 scrapy 默認只處理 2xx 和 3xx 狀態的請求、4開頭和5開頭的都不處理,可是咱們又須要觀察401狀態返回的內容,這怎麼辦呢?
咱們能夠在settings.py中空白處新增代碼:
""" 狀態碼處理 """
HTTPERROR_ALLOWED_CODES = [400, 401]
複製代碼
而後在下一個方法中觀察response回來的數據(這個地方當時做爲萌新的我是懵逼的,因此委屈各位讀者大佬跟我一塊兒懵逼)。
後來查詢了401的意思:未得到受權,也就是用戶權限驗證不經過。通過多方資料查找,發現請求頭中有這麼一條:
它就是用於用戶權限驗證的,authorization 的值分爲兩部分:type 和 credentials,前者是驗證採用的類型,後者是具體的參數值。這裏的類型能夠看到用的是 Bearer 類型。
我又去觀察登陸時候的返回值,發現登陸成功後的返回值除了 succeed 以外,還有其餘的一些返回值,裏面包括了一個叫 access_token 的字段,看樣子它是 JWT 登陸方式用來鑑權的 token 信息,通過比對確認 authorization 用的也正好就是這個 token 做爲值。
那麼代碼就應該在第一次登陸時候,取出access_token的值,並傳遞下去,用於後面請求的鑑權,因此代碼改成:
def is_login(self, response):
""" 根據返回值中的message值來判斷是否登陸成功 若是登陸成功則對數據傳輸頁發起請求,並將結果回傳給parse方法 若是登陸失敗則提示 因爲後面的用戶權限驗證須要用到token信息,因此這裏取到登陸後返回的token並傳遞給下一個方法 """
results = json.loads(response.text)
if results['message'] == "succeed":
urls = 'http://xxx.yyy.ccc.aa'
access_token = results['data']['access_token']
print("登陸成功,開始調用方法")
yield Request(url=urls, callback=self.parse, meta={"access_token": access_token})
else:
print("登陸失敗,請從新檢查")
複製代碼
下面的pase方法中,將 authorization 設定到 header 中以對數據進行請求:
header = {
"authorization": "Bearer " + access_token
}
複製代碼
這樣就解決了用戶權限的問題,再也不出現401
在 parse 方法中根據瀏覽器觀察到的參數進行構造:
datas = {
"url": "https://www.youtube.com/watch?v=eWeACm7v01Y",
"title": "看上去可愛其實很笨的狗#動物萌寵#",
"share_text": "看上去可愛其實很笨的狗#動物萌寵#[doge]",
"categories": {'0': '00e2e120-37fd-47a8-a96b-c6fec7eb563d'}
}
複製代碼
因爲categories裏面是個數組,因此在構造的時候也能夠直接寫數據,而後用 scrapy.Formdata 來進行 post。發現返回的狀態是此次是 400,而且提示:categories 必須是數組。
再次觀察請求頭信息,發現請求頭信息中還有:
我將這個叫作 content-type 的字段和參數加入到 header 中:
header = {
"authorization": "Bearer " + access_token,
"content-type": "application/json",
}
複製代碼
這樣關於 categories 必須是數組的提示就沒有了。
可是返回的狀態碼依然是400,並且提示變成了 "url不能爲空"。
這到底又是怎麼一回事?
多方探查都沒有結果。
真是傷心
後來我又想起了,既然這裏的文本類型 是application/json,那麼提交出去的文本應該是 json 類型數據,而不是 python 的 dict 字典。
因而打開 json 在線解析,對傳遞的參數進行觀察,發現這樣的數據並不知足json格式:
後來嘗試對它進行更改:
在外層增長了一對{},而後又將 categories 的值加上了雙引號,纔是正確的 json 格式(我是真的又菜又蠢)。
將這樣的數據拿到 postman 中進行測試,發現是不行的。又通過我不斷的測試,最終肯定了 postman 的請求格式爲:
我是對 Auth、Headers 和 Raw 進行設置(請跟我一塊兒懵逼),才終於成功發送 post,返回正確的信息!!!
在 postman 測試經過後,說明這樣的作法是可行的,可是代碼上怎麼編寫呢?
用以前的 scrapy.Formdata 是不行的,它的 formdat= 默認使用 dict 格式,若是強行轉成 json 格式也是會報錯的。
通過羣裏諮詢和搜索,發現要用 scrapy.http 的 Requst 方法(平時常常用的這個):
access_token = response.meta['access_token']
urls = "http://aaa.bbb.xxx.yy/api/v1/material/extract"
datas = {
"url": "https://www.youtube.com/watch?v=eWeACm7v01Y",
"title": "看上去可愛其實很笨的狗#動物萌寵#",
"share_text": "看上去可愛其實很笨的狗#動物萌寵#[doge]",
"categories": {'0': '00e2e120-37fd-47a8-a96b-c6fec7eb563d'}
}
header = {
"authorization": "Bearer " + access_token,
"content-type": "application/json",
}
yield Request(url=urls, method='POST', body=json.dumps(datas), headers=header, callback=self.parse_details)
複製代碼
這樣發送請求,終於成功了!!!
首先看一看 json.dumps 函數的用途是什麼: json.dumps() 用於將 dict 類型的數據轉成 str。
雖然沒有摸清楚消息發送失敗的根本緣由(有多是目標網站後端對數據格式進行校驗,也有多是 Scrapy 框架會在發送請求前對參數進行處理因此致使的問題),可是已經能夠猜出個大概。同時也在本次爬蟲任務中學習到了一些知識。
從本文中咱們學會了三個知識:
第 1 是萌新要多問、多測試,沒有解決不了的計算機問題;
第 2 是爬取使用先後端分離技術的 Web 站時應該優先選擇從 API 下手;
第 3 是網絡請求詳情中看到的參數格式並不是是你認爲的參數格式,它有多是通過編碼的字符串;