跳到內容

網路爬蟲

網路爬蟲就是用程式自動去抓網頁上的資料。這個專題要做的是:給一個 LINE 貼圖商店的網址,把那一組貼圖的每一張圖片都下載到電腦上。會用到兩個很常見的套件:requests 負責把網頁抓下來,BeautifulSoup 負責從一堆 HTML 裡挑出你要的內容;再加上內建的 json 和 os,以及檔案讀寫那章的 open。

Terminal window
uv add requests beautifulsoup4

沒用 uv 的話:

Terminal window
pip install requests beautifulsoup4

裝的套件叫 beautifulsoup4,但程式裡 import 的名字是 bs4,兩個不一樣,別被搞混。

requests.get() 會去抓指定網址,回傳的物件裡有這個網頁的內容:

import requests
res = requests.get("https://store.line.me/stickershop/product/30973429/zh-Hant")
res.raise_for_status()
print(res.status_code)
print(res.text)
  • res.status_code:狀態碼,200 代表成功,404 代表找不到頁面。
  • res.raise_for_status():如果抓失敗了(狀態碼不是 2 開頭),就直接丟出錯誤讓程式停下來,不必拿著一頁錯誤訊息往下解析。
  • res.text:整個網頁的 HTML 原始碼,是一個很長的字串。

第二步:找出圖片在 HTML 的哪裡

Section titled “第二步:找出圖片在 HTML 的哪裡”

res.text 是一大坨 HTML,直接看很難用。要挑出貼圖,得先知道它們藏在哪個標籤裡。在瀏覽器打開貼圖頁面,對著一張貼圖按右鍵選「檢查」,會看到類似這樣的結構:

<li
class="mdCMN09Li FnStickerPreviewItem"
data-preview='{"type":"static","id":"...","staticUrl":"https://...png","animationUrl":"", ...}'
></li>

每一張貼圖是一個 <li>,class 裡有 mdCMN09Li,而圖片的網址寫在 data-preview 這個屬性裡,內容是一段 JSON 文字:type 說明這張是靜態(static)還是動態(animation)的貼圖,staticUrl 和 animationUrl 分別是兩種圖片的網址。

知道位置之後,把 HTML 交給 BeautifulSoup,用 CSS 選擇器把這些 <li> 全部挑出來:

import bs4
import requests
res = requests.get("https://store.line.me/stickershop/product/30973429/zh-Hant")
res.raise_for_status()
soup = bs4.BeautifulSoup(res.text, "html.parser")
image_tags = soup.select("li.mdCMN09Li")
print(len(image_tags))
print(image_tags[0]["data-preview"])
  • bs4.BeautifulSoup(res.text, "html.parser"):把 HTML 交給 BeautifulSoup 解析。
  • soup.select("li.mdCMN09Li"):用 CSS 選擇器找出所有 class 含 mdCMN09Li 的 <li> 標籤,回傳一個串列。
  • image_tags[0]["data-preview"]:標籤的屬性可以像字典一樣用 [] 取,拿到的是那段 JSON 字串。

把兩步接起來,再加上「解析 JSON、挑出網址、下載、存檔」,就是完整的程式。存成 line_sticker.py 執行,會在同一個資料夾下多出一個 S1 資料夾,裡面是編號好的貼圖:

import json
import os
import bs4
import requests
# 靜態的
sticker_name = "S1"
sticker_url = "https://store.line.me/stickershop/product/30973429/zh-Hant"
# 動態的
# sticker_name = "S2"
# sticker_url = "https://store.line.me/stickershop/product/37070/zh-Hant"
os.makedirs(sticker_name, exist_ok=True)
res = requests.get(sticker_url)
res.raise_for_status()
soup = bs4.BeautifulSoup(res.text, "html.parser")
image_tags = soup.select("li.mdCMN09Li")
for idx, image_tag in enumerate(image_tags, 1):
data = json.loads(image_tag["data-preview"])
if data["type"] == "animation":
image_url = data["animationUrl"]
else:
image_url = data["staticUrl"]
image_res = requests.get(image_url)
image_res.raise_for_status()
with open(f"{sticker_name}/{idx}.png", "wb") as file:
file.write(image_res.content)
  • 兩組網址:程式開頭準備了一組靜態貼圖和一組動態貼圖的網址,sticker_name 是要存進去的資料夾名稱。預設跑靜態那組;把上面兩行註解掉、下面兩行的 # 拿掉,就換成動態那組。
  • os.makedirs(sticker_name, exist_ok=True):建立存圖片的資料夾。exist_ok=True 的意思是資料夾已經存在也沒關係,不然第二次執行會因為資料夾已經有了而出錯。
  • for idx, image_tag in enumerate(image_tags, 1):走過每一個 <li>,同時拿一個從 1 開始的編號當檔名。
  • json.loads(image_tag["data-preview"]):data-preview 裡是 JSON 字串,json.loads 把它變成 Python 的字典,之後才能用 data["type"] 這樣取值。
  • 靜態還是動態:type 是 animation 就拿 animationUrl,否則拿 staticUrl。動態貼圖也是 PNG 格式(APNG),所以副檔名一樣用 .png。
  • 再抓一次:requests.get(image_url) 這次抓的是圖片本身,同樣先 raise_for_status() 確認成功。
  • 存成檔案:圖片是二進位資料,不是文字,所以 open 要用 "wb" 模式(write binary),寫入的也不是 res.text 而是 image_res.content。檔名用 f-string 組成 S1/1.png、S1/2.png……

網站的 HTML 結構是對方隨時可以改的。如果某天程式突然抓不到東西(image_tags 是空串列),先回瀏覽器用「檢查」看看 class 名稱或 data-preview 是不是換了,改一下 select 裡的選擇器就好。

爬別人的網站,有幾件事要放在心上:

  • 看看對方允不允許:很多網站的 /robots.txt(例如 https://store.line.me/robots.txt)會寫明哪些頁面歡迎爬、哪些不希望被爬。
  • 不要抓太快、太頻繁:短時間內狂抓會對別人的伺服器造成負擔,也容易被封鎖。需要抓很多頁時,記得每次之間停一下(用 time.sleep())。
  • 尊重資料的使用規範:抓下來的資料怎麼用,要留意對方網站的服務條款。貼圖是有著作權的作品,自己練習、自己看沒問題,別拿去轉載或做商業用途。

v0.9.1 (c924b5b)