網路爬蟲
網路爬蟲就是用程式自動去抓網頁上的資料。這個專題要做的是:給一個 LINE 貼圖商店的網址,把那一組貼圖的每一張圖片都下載到電腦上。會用到兩個很常見的套件:requests 負責把網頁抓下來,BeautifulSoup 負責從一堆 HTML 裡挑出你要的內容;再加上內建的 json 和 os,以及檔案讀寫那章的 open。
uv add requests beautifulsoup4沒用 uv 的話:
pip install requests beautifulsoup4裝的套件叫 beautifulsoup4,但程式裡 import 的名字是 bs4,兩個不一樣,別被搞混。
第一步:把網頁抓下來
Section titled “第一步:把網頁抓下來”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 bs4import 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 jsonimport os
import bs4import 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 裡的選擇器就好。
爬蟲的禮貌與規矩
Section titled “爬蟲的禮貌與規矩”爬別人的網站,有幾件事要放在心上:
- 看看對方允不允許:很多網站的
/robots.txt(例如https://store.line.me/robots.txt)會寫明哪些頁面歡迎爬、哪些不希望被爬。 - 不要抓太快、太頻繁:短時間內狂抓會對別人的伺服器造成負擔,也容易被封鎖。需要抓很多頁時,記得每次之間停一下(用
time.sleep())。 - 尊重資料的使用規範:抓下來的資料怎麼用,要留意對方網站的服務條款。貼圖是有著作權的作品,自己練習、自己看沒問題,別拿去轉載或做商業用途。
v0.9.1 (c924b5b)