Python 爬取 "王者榮耀.英雄桌布" 過程中的矛和盾

一枚大果殼發表於2022-03-05

1. 前言

學習爬蟲,最好的方式就是自己編寫爬蟲程式。

爬取目標網站上的資料,理論上講是簡單的,無非就是分析頁面中的資源連結、然後下載、最後儲存

但是在實施過程卻會遇到一些阻礙。

很多網站為了阻止爬蟲程式爬取資料,會對資源路徑進行加密、或隱藏等保護操作。

編寫爬蟲程式的第一關鍵邏輯就解析資源路徑。

2. 靜態資源路徑

什麼是靜態資源路徑?

在下載下來的原始碼中可以直接分析並找出資源路徑。

向伺服器請求 入口(主)頁面 時,伺服器就已經把主頁面中需要展示的資源路徑一併返回給請求者。

爬蟲任務:爬取王者榮耀網站上的英雄資料。

3.1 下載入口網頁

找到王者榮耀英雄資料的入口連結:https://pvp.qq.com/web201605/herolist.shtml,開啟谷歌瀏覽器,下載並顯示出所有的英雄的圖片。

3.2 編寫正規表示式

為了下載入口頁中的所有英雄圖片資源,則需要使用一個統一的規則找到所的資源路徑(url),正規表示式是一個不錯的選擇。

編寫正規表示式之前,先分析圖片路徑的描述規則。

在瀏覽器中選擇任意一張圖片,然後右擊,再在彈出來的快捷選單中選擇“檢查”,便可以看到此圖片的路徑。

複製出圖片路徑:

<img src="//game.gtimg.cn/images/yxzj/img201606/heroimg/525/525.jpg" width="91" height="91" alt="魯班大師">

再選擇任意張圖片,同理使用瀏覽器的“檢查”功能,獲取所選擇圖片的路徑:

<img src="//game.gtimg.cn/images/yxzj/img201606/heroimg/522/522.jpg" width="91" height="91" alt="曜">
<img src="//game.gtimg.cn/images/yxzj/img201606/heroimg/504/504.jpg" width="91" height="91" alt="米萊狄">
<img src="//game.gtimg.cn/images/yxzj/img201606/heroimg/180/180.jpg" width="91" height="91" alt="哪吒">
……

縱觀現獲取到的圖片路徑,可以解析出其中規律:

  • 伺服器地址: game.gtimg.cn/images,可以發現所有圖片路徑的這部分都是相同的。
  • 伺服器上圖片儲存的目錄結構: yxzj/img201606/heroimg/數字。對於所有圖片,目錄結構中的 “yxzj/img201606/heroimg” 是相同的,但每一張圖片都有自己的子目錄,應該是圖片的編號,雖然不相同,但都是數字。
  • 圖片檔名: 數字.jpg,檔名的格式應該是圖片編號+副檔名

有了如上基礎資訊後,就可以編寫一個用來描述圖片資源的正規表示式。

img_re = r"//game.gtimg.cn/images/yxzj/img201606/heroimg/\d+/\d+.jpg"

3.3 編寫爬蟲程式

import requests
import re
      
# 伺服器地址
url = "https://pvp.qq.com/web201605/herolist.shtml"
# 與圖片路徑匹配的正規表示式
img_re = r"(//game.gtimg.cn/images/yxzj/img201606/heroimg/\d+/(\d+.jpg))"
# 偽裝成瀏覽器
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'}
# 傳送請求
resp = requests.get(url, headers=headers)
content = resp.text
# 查詢所有圖片路徑
img_urls = re.findall(img_re, content)
print(img_urls)
#下載所有圖片,儲存到本地
for img_url in img_urls:
    resp = requests.get("https:" + img_url[0])
    with open("d:/heros/" + img_url[1], "wb") as f:
         print("正在儲存", img_url[1])
         f.write(resp.content)

執行程式後,再在本地磁碟中的 d:\heros 目錄檢視,可以看到英雄圖片已經全部下載成功。

靜態資源的路徑解析相對而言較簡單,考核的是正則表達語言的編寫能力。

3. 動態資源路徑

所謂動態資源路徑指無法從下載下來的入口頁面原始碼中直接找出來的資源路徑。

當使用者請求伺服器入口頁面時,響應包中並沒有直接返回資源路徑。而是在使用者的後續操作過程通過 ajax 在客戶端動態載入。

原始碼中沒有,在動態執行過程中由邏輯動態產生。

為了更好的理解動態資源路徑,現給爬蟲一個任務:

下載下載王者榮耀官方網站所提供的大量高清英雄桌布。

3.1 常 規解析資源路徑

找到入口 https://pvp.qq.com/web201605/wallpaper.shtml 連結,使用谷歌瀏覽器開啟,可顯示出英雄桌布。

因為已經有了前面下載英雄資料的經驗,現在我們如法炮製。

分析圖片的路徑規則:

選擇任一張高清桌布,然後右擊,再在快捷選單中選擇“檢查”

可以查閱到此圖片的完整路徑:

https://shp.qpic.cn/ishow/2735022317/1645610302_1265602313_48245_sProdImgNo_1.jpg/0

再找任意的幾張圖片,用同樣的方法查詢出它們的路徑。

https://shp.qpic.cn/ishow/2735021517/1644917442_1265602313_38411_sProdImgNo_1.jpg/0
https://shp.qpic.cn/ishow/2735012712/1643257997_1265602313_18054_sProdImgNo_1.jpg/0
https://shp.qpic.cn/ishow/2735010717/1641547765_1265602313_26451_sProdImgNo_1.jpg/0

分析後,可知圖片(資源)的路徑由幾個部分組成:

  • 伺服器地址: https://shp.qpic.cn ,所有圖片都在同一個伺服器。

  • 圖片在伺服器上的儲存目錄: ishow/2735010717/ 目錄結構中的 ishow 即父目錄是相同的,雖然子目錄不相同,但其有一個規律,都是數字。

  • 圖片名稱: 1641547765_1265602313_26451_sProdImgNo_1.jpg 圖片名稱由如下幾部分組成:

    • 3 串數字: 1641547765_1265602313_26451, 3 串數字中的第 1 串和第 3 串不相同,第2 串數字是相同的。

    • sProdImgNo_1.jpg: 1 串字串,這部分所有圖片都相同。

  • 在整個路徑的最後還有一個 /0

有了上面的分析基礎,編寫正規表示式就簡單了。

img_url_re = r"(https://shp.qpic.cn/ishow/\d+/(\d+_){3}sProdImgNo_1.jpg/0)"

有了正規表示式,感覺這些桌布馬上就能唾手可得。

然而,使用上面正規表示式在入口頁面( https://pvp.qq.com/web201605/wallpaper.shtml )中試圖查詢出所有圖片資源路徑時卻讓我們失望了。

import re
import requests

# 王者榮耀官方桌布地址
wzry_url = "https://pvp.qq.com/web201605/wallpaper.shtml"
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'}
# 解析圖片的正規表示式
img_url_re = r"(https://shp.qpic.cn/ishow/\d+/(\d+_){3}sProdImgNo_1.jpg/0)"
# 訪問王者榮耀高清桌布頁面
response = requests.get(wzry_url)
# 獲得網站資料
content = response.text
print(content)
# 查詢所有圖片路徑
lst = re.findall(img_url_re, content)
print(lst)

輸出結果是 [ ]。沒有查詢到任何圖片資源路徑。

為什麼會這樣?

因為在我們請求 https://pvp.qq.com/web201605/wallpaper.shtml 入口頁面後,在返回的入口資料中沒有包含桌布的路徑。

它使用的是動態載入桌布的方案,也就是 https://pvp.qq.com/web201605/wallpaper.shtml 不是真正的資源入口連結。

相當於給了你一個禮盒,開啟沒有看到真正的禮物,只有一些線索,需要你通過這個索引再找到禮物。

3.2 查詢真正的資源入口

真正的資源入口連結可能加密,也可能隱藏在一群連結的中間。

開始尋找的旅程。

在谷歌瀏覽器的開發者工具中選擇 ”Network“ 並在皮膚中選擇 ”Fetch/XHR“,會看到了一個 herolist.json 路徑,從字面意義上解讀,感覺應該是它。

於是,以此路徑作為入口連結開始編碼。

import requests

wzry_url = "https://pvp.qq.com/web201605/js/herolist.json"
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'}
response = requests.get(wzry_url)
# 獲得網站資料
content = response.text
print(content)

執行程式後,看到如下輸出結果。

[{
	"ename": 105,
	"cname": "廉頗",
	"title": "正義爆轟",
	"new_type": 0,
	"hero_type": 3,
	"skin_name": "正義爆轟|地獄巖魂"
}, {……},……
]

資料以 JSON 格式返回,但是沒有看到圖片路徑資訊。

看來此路徑不是資源的真正入口連結。

現在擴大路徑查詢範圍,找到一個workList_inc.cgi 路徑。

複製此路徑後分析發現,此路徑的嫌疑非常大。

https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=4&totalpage=0&page=0&iOrder=0&iSortNumClose=1&jsoncallback=jQuery17108072345473566771_1646484482973&iAMSActivityId=51991&_everyRead=true&iTypeId=1&iFlowId=267733&iActId=2735&iModuleId=2735&_=1646484483043

在 "Response"中可以看到請求返回值是一個 JSON 格式。

jQuery17108072345473566771_1646484482973({"iBltFlag":"0","iCache":"1","iRet":"0","iTotalLines":"299","iTotalPages":"75","sMsg":"Successful","List":[{"dtInputDT":"2022%2D02%2D23%2017%3A57%3A56","iBallotNum":"0","iClickNum":"0","iCommentNum":"0","iDownloadNum":"0","iNonsupportNum":"0","iProdId":"1931","iStatus":"1","sProdImgNo_1":"https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610274%5F1265602313%5F44569%5FsProdImgNo%5F1%2Ejpg%2F200","sProdImgNo_2":"https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610274%5F1265602313%5F44569%5FsProdImgNo%5F2%2Ejpg%2F200","sProdImgNo_3":"https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610274%5F1265602313%5F44569%5FsProdImgNo%5F3%2Ejpg%2F200","sProdImgNo_4":"https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610275%5F1265602313%5F44569%5FsProdImgNo%5F4%2Ejpg%2F200","sProdImgNo_5":"https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610275%5F1265602313%5F44569%5FsProdImgNo%5F5%2Ejpg%2F200","sProdImgNo_6":"https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610275%5F1265602313%5F44569%5FsProdImgNo%5F6%2Ejpg%2F200","sProdImgNo_7":"https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610275%5F1265602313%5F44569%5FsProdImgNo%5F7%2Ejpg%2F200","sProdImgNo_8":"https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610275%5F1265602313%5F44569%5FsProdImgNo%5F8%2Ejpg%2F200","sProdName":"%E5%A5%B3%E5%A8%B2%2D%E8%A1%A5%E5%A4%A9%E5%A3%81%E7%BA%B8","sThumbURL":"https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610274%5F1265602313%5F44569%5FsProdImgNo%5F1%2Ejpg%2F200"},{……}]})

資料裡面有一個 List 屬性,返回的是一個陣列,每一個陣列中包括一個 JSON 物件,關鍵是 JSON 物件中有語義很明確的地址資訊。

https%3A%2F%2Fshp%2Eqpic%2Ecn%2Fishow%2F2735022317%2F1645610274%5F1265602313%5F44569%5FsProdImgNo%5F1%2Ejpg%2F200

把此路徑和前面分析出來的圖片路徑比較一下

https://shp.qpic.cn/ishow/2735021517/1644917442_1265602313_38411_sProdImgNo_1.jpg/0

會發現,JSON 中的路徑是對真正圖片路徑中的特殊符號轉碼後生成的。

看來包括真正資源的入口連結就是它了。

https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=4&totalpage=0&page=0&iOrder=0&iSortNumClose=1&iAMSActivityId=51991&_everyRead=true&iTypeId=1&iFlowId=267733&iActId=2735&iModuleId=2735&_=1646484483043

開始編碼之前,先根據語義修改一下請求引數:

  • iListNum 引數應該表示每一頁有多少張 ,現修改成 10。
  • 刪除 jsoncallback=jQuery17108072345473566771_1646484482973 請求引數。
import requests
import urllib.parse
import json

# 真正資源的入口連結
url = "https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=10&totalpage=0&page=0&iOrder=0&iSortNumClose=1&iAMSActivityId=51991&_everyRead=true&iTypeId=1&iFlowId=267733&iActId=2735&iModuleId=2735&_=1646484483043"
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'}
# 請求第一頁資料(page=0)
resp = requests.get(url, headers=headers)
res_text = resp.text
# 反序列化 JSON
data_dict = json.loads(res_text)
# 所有圖片資料
all_imgs_data = data_dict.get("List")
# 迭代圖片
for img_data in all_imgs_data:
    # 獲取圖片的路徑(網站提供不同解析度的圖片,數字編號從0 到 8)
    img_url = img_data["sProdImgNo_4"]
    # 解碼
    img_url = urllib.parse.unquote(img_url)
    # 把 URL 後面的 200 替換成 0
    img_url = img_url.replace("200", "0")
    # 抓取圖片資料
    resp = requests.get(img_url, headers=headers)
    # 儲存圖片到本地
    with open("d:/heros/" + urllib.parse.unquote(img_data["sProdName"])+".jpg", "wb") as f:
        f.write(resp.content)

以上程式碼僅獲取了10 張圖片,如果需要更多,可以修改:

把 iListNum 設定成一個更大的值。
或者通過迭代方式修改 page=0 後面的值 ,這個參數列示頁碼

開啟本地目錄,可看到下載下來的高清桌布。

4. 總結

爬蟲程式的編寫關鍵,準確分析到資源路徑。

相關文章