今天有教到遞迴a_tags = soup.find_all(['a','b'],limit=4,recursive=True)
理解背後的原理和思考過程,比單純拿到一段「能用的程式碼」更重要。這樣你才能靈活應用、修改,甚至面對未來網站結構變動時,也能自己調整
1. 了解網頁結構與 HTML 元素
學會用瀏覽器的「檢查元素」工具,觀察你想抓的資料在網頁上的 HTML 標籤、class、id 等屬性。
理解標籤之間的父子關係,這樣可以用更精確的 CSS selector 或 BeautifulSoup 的方法定位目標。
2. 學會用 BeautifulSoup 的多種查找方法
find(), find_all():根據標籤、屬性查找元素。
select(): 用 CSS selector 查找元素,彈性更大。
利用文字內容、屬性值做篩選。
3. 掌握 HTTP 請求的細節
了解 headers(特別是 User-Agent)的重要性,知道為什麼要模擬瀏覽器。
學會錯誤處理,讓程式更穩定。
4. 練習並嘗試改寫
嘗試自己改變查找條件,觀察結果變化。
嘗試抓不同網站、不同區塊的資料,累積經驗。
這段 Python 程式的功能是「從網站抓取旅遊產品資訊,並寫入 travel.csv 檔案中」。下面我分段詳細說明:
🔹 1. 匯入套件
import csv import requests from bs4 import BeautifulSoup
-
csv:用來寫入.csv檔案。 -
requests:發送 HTTP 請求,抓取網頁內容。 -
BeautifulSoup:解析 HTML 網頁內容。
🔹 2. 設定標頭與準備資料儲存容器
information = [] headers = { "User-Agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) ..." }
-
information是用來儲存所有旅遊資訊的列表。 -
headers模擬瀏覽器送出請求,避免被網站拒絕(網站可能會擋掉機器人)。
🔹 3. 抓取網頁內容
r = requests.get('https://www.etholiday.com/Products/Group/Search?regm_cd=0005&signup_st=&page=1', headers=headers) r.raise_for_status() soup = BeautifulSoup(r.text, 'html.parser')
-
向網站送出 GET 請求並接收回傳的 HTML。
-
raise_for_status()會在請求失敗時產生錯誤。 -
用
BeautifulSoup將 HTML 解析為 Python 可以操作的格式。
🔹 4. 擷取旅遊商品資訊
stories = soup.find_all('div', class_='row no-gutters') for s in stories:
-
find_all()找出所有商品區塊。 -
每個
s就是一筆商品資料,接著從裡面擷取資訊:
👉 擷取標題
title_tag = s.find('a', class_='w-100') if not (title_tag and title_tag.h4 and title_tag.h4.string): continue title = title_tag.h4.get_text(strip=True)
-
找不到標題就
continue跳過。 -
.get_text(strip=True)會去除前後空白。
👉 擷取出發日期
date_tag = s.find('div', class_='go-date') date = date_tag.get_text(strip=True) if date_tag else ''
-
如果有日期就取文字,否則設為空字串。
👉 擷取航空公司名稱(可能為空)
plane_tag = s.find('span', class_='plane-abbr') plane = plane_tag.get_text(strip=True) if plane_tag else ''
👉 擷取價格
price_box = s.find('div', class_='price_box') if price_box and price_box.h4 and price_box.h4.span: price = price_box.h4.span.get_text(strip=True) else: price = ''
-
如果能成功找到價格標籤,就取出文字,否則設為空。
👉 加入結果列表
information.append([date, title, plane, price])
🔹 5. 寫入 CSV 檔案
with open('travel.csv', 'w', newline='', encoding='utf-8-sig') as csvfile: writer = csv.writer(csvfile) writer.writerow(['date', 'title', 'plane', 'price']) writer.writerows(information)
-
使用
utf-8-sig避免 Excel 打開亂碼。 -
第一行是標題,接著寫入所有資料。
(也可以改用 pandas 寫入,程式下方有備用程式碼)
🔹 6. 顯示結果
print(f"共寫入 {len(information)} 筆資料到 travel.csv")
-
顯示寫入的筆數。
下載YAHOO頭條新聞
導致失敗的觀念與操作程式碼一 分析:
import requests
from bs4 import BeautifulSoup
r = requests.get('https://tw.yahoo.com/')
if r.status_code == requests.codes.ok:
soup = BeautifulSoup(r.text, 'html.parser')
stories = soup.find_all('a', class_='Fz(16px) LineClamp(1,20px) Fw(700) Td(n) Td(u):h C(#324fe1) V(h) active_V(v)')
for s in stories:
print("標題:", s.text)
print("網址:", s.get('href'))
1. 加入 headers 假裝瀏覽器
錯誤範例:直接用 requests.get('https://tw.yahoo.com/') 取得網頁。
成功範例:在 requests.get 裡加入 headers,模擬瀏覽器(如 iPad)發送請求。
headers = {"User-Agent": "Mozilla/5.0 (iPad; ..."}
r = requests.get('https://tw.yahoo.com/', headers=headers)
原因:有些網站(如 Yahoo)會檢查 User-Agent,沒有瀏覽器資訊就會拒絕或回傳不完整內容。
效果:加了 headers 後,網站會回傳正常網頁內容,BeautifulSoup 才能正確解析。
2. 增加錯誤處理(try-except)
錯誤範例:沒有任何錯誤處理,遇到網路問題或網址錯誤就直接當掉。
成功範例:用 try-except 包住主要程式,捕捉 HTTPError 和 URLError。
try:
# 主要程式
except HTTPError as e:
print("httperror")
except URLError as e2:
print("urlerror")
原因:網路請求常會失敗,必須捕捉例外,避免程式崩潰。
效果:即使遇到錯誤,程式會顯示錯誤訊息,不會整個中斷。
3. 結尾加上識別字串
錯誤範例:沒有特別標記程式結束。
成功範例:最後加上 print("lccnet")。
效果:方便辨識程式有正常執行到最後。
4. 其它小細節
import:成功範例多 import 了 HTTPError, URLError,但實際上這是 urllib 的錯誤型態,若用 requests 可以改用 requests.exceptions,但這裡主要是示範 try-except 結構。功能本身(爬取標題與網址)兩段都一樣,重點差在「能否順利取得網頁」和「遇到錯誤時的處理」。
成功案例,並形成DataFrame
import requests
from bs4 import BeautifulSoup
title = []
link_url = []
# 1. 加入 headers 模擬瀏覽器
headers = {"User-Agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"}
try:
# 2. 加入 try-except 錯誤處理
r = requests.get('https://tw.yahoo.com/', headers=headers)
r.raise_for_status() # 若發生 HTTP 錯誤會直接丟出例外
if r.status_code == requests.codes.ok:
soup = BeautifulSoup(r.text, 'html.parser')
stories = soup.find_all('a', class_='Td(n) C(varTextPrimary) C(varTextPurpleHighlight):h Ov(h) Fw(700) Fz(16px) Lh(24px)')
for s in stories:
print("標題:" + s.text)
title.append(s.text)
print("網址:" + s.get('href'))
link_url.append(s.get('href'))
except requests.exceptions.HTTPError as e:
print("HTTP error:", e)
except requests.exceptions.RequestException as e:
print("Request error:", e)
except Exception as e:
print("Other error:", e)
# 3. 結尾提示
print("共抓到", len(title), "則新聞")
print(link_url)
print("lccnet")
成功抓取新聞頁面,請試著抓其他頁面
import requests
from bs4 import BeautifulSoup
r = requests.get('https://tw.yahoo.com/')
if r.status_code == requests.codes.ok:
soup = BeautifulSoup(r.text, 'html.parser')
stories = soup.find_all('a', class_='Fz(16px) LineClamp(1,20px) Fw(700) Td(n) Td(u):h C(#324fe1) V(h) active_V(v)')
for s in stories:
print("標題:", s.text)
print("網址:", s.get('href'))
| 步驟 | 操作 | 背後觀念 |
|---|---|---|
| 1 | 選對網址 https://tw.news.yahoo.com/ |
不抓 JS 動態頁面,而抓靜態的新聞列表 |
| 2 | 找出穩定 class 的 <a> 標籤 |
避免找不到或 class 變動 |
| 3 | 使用 BeautifulSoup 解析並找出資料 | 確保能從 HTML 結構中正確抓資料 |
| 4 | 擷取 .text 和 .get('href') |
分別拿標題與連結 |
| 5 | 用 print() 除錯與確認結果 |
驗證是否成功抓到你想要的內容 |
從 PTT 笑話版(joke 看板)抓取文章標題與連結,並且往前翻頁,連續抓取 5 頁的文章標題。
import requests
from bs4 import BeautifulSoup
url="https://www.ptt.cc/bbs/joke/index.html"
for i in range(5):
r=requests.get(url)
if r.status_code==requests.codes.ok:
soup=BeautifulSoup(r.text,'html.parser')
sel=soup.select("div.title a") #標題
u=soup.select("div.btn-group.btn-group-paging a")
url="https://www.ptt.cc"+u[1]["href"] #新網頁(網址)的新連結
for s in sel:
print(s["href"],s.text)
1.匯入套件
requests:用來發送 HTTP 請求,取得網頁內容。
BeautifulSoup:用來解析 HTML,方便抓取特定標籤內容。
2.設定起始網址
url = "https://www.ptt.cc/bbs/joke/index.html"
這是笑話版的最新文章列表頁面。
3.迴圈抓取 5 頁資料
for i in range(5):
迴圈執行 5 次,每次抓取一頁的文章標題。
4.發送請求並確認成功
r = requests.get(url)
if r.status_code == requests.codes.ok: 確認 HTTP 狀態碼是 200,代表成功取得網頁。
5.解析網頁內容
soup = BeautifulSoup(r.text, 'html.parser')
將 HTML 文字用 BeautifulSoup 解析。
6.抓取文章標題與連結
sel = soup.select("div.title a")
PTT 文章列表中,文章標題在 <div class="title"> 裡的 <a> 標籤中。
sel 是一個列表,裡面每個元素是 <a> 標籤。
7.抓取分頁按鈕
u = soup.select("div.btn-group.btn-group-paging a")
這裡抓取分頁按鈕,通常有「最舊」、「‹ 上頁」、「下頁 ›」、「最新」四個連結。
u[1] 指的是「‹ 上頁」的連結。
8.更新下一次要抓取的頁面網址
url = "https://www.ptt.cc" + u[1]["href"]
取得上一頁的網址,讓下一次迴圈抓取前一頁的文章。
9.印出每篇文章的連結與標題
for s in sel:
print(s["href"], s.text)
印出文章相對連結(例如 /bbs/joke/M.1234567890.A.html)與標題文字。
一、處理 PTT 的「滿 18 歲確認頁」
import requests
from bs4 import BeautifulSoup
r=requests.Session()
payload={
"from":"bbs/Gossiping/index.html",
"yes":"yes"}
r1=r.post("https://www.ptt.cc/ask/over18",payload)
r2=r.get("https://www.ptt.cc/bbs/Gossiping/index.html")
print(r2.text)
requests.Session():建立一個「會話」物件,可在後續請求中保留 cookie 等登入狀態。
payload:是我們要傳給 PTT 的資料,表示我們同意「已滿 18 歲」。
- "from" 指想前往的頁面。
- "yes": "yes" 表示我們按下「我已滿 18 歲」的按鈕。
r.post(...):模擬表單送出,通過「滿 18 歲」驗證。
🔸 為什麼要這樣做?
PTT 的 Gossiping 板需要滿 18 歲才能瀏覽,
用 requests 直接 get 的話,會只拿到驗證頁。
這裡用 Session + post() 模擬通過驗證,
後續請求才能正常取得文章。
二、抓取 Gossiping 板的 HTML 內容
r2 = r.get("https://www.ptt.cc/bbs/Gossiping/index.html")
print(r2.text)
🔸 說明:
用剛剛已經通過驗證的 r session 去抓取八卦板首頁。r2.text 是拿到的 HTML 文字內容。
此時因為 Session 已通過「滿 18 歲」驗證,所以這個 .get() 會成功拿到文章列表頁,不會再被導去問年齡的頁面。
三、後續爬多頁的程式碼
for i in range(5):
r=requests.get(url)
if r.status_code==requests.codes.ok:
soup=BeautifulSoup(r.text,'html.parser')
sel=soup.select("div.title a") #標題
u=soup.select("div.btn-group.btn-group-paging a")
url="https://www.ptt.cc"+u[1]["href"] #新網頁(網址)的新連結
for s in sel:
print(s["href"],s.text)
| 程式碼 | 操作 | 說明 |
|---|---|---|
for i in range(5): |
重複跑 5 頁 | 每次都抓一頁的文章清單 |
soup.select("div.title a") |
找出每篇文章的標題與連結 | <div class="title"><a href=...> 是文章的區塊 |
soup.select("div.btn-group.btn-group-paging a") |
找出「上一頁」的按鈕連結 | u[1] 就是「上一頁」按鈕 |
url = "https://www.ptt.cc" + u[1]["href"] |
把相對網址轉成完整網址 | 例如 /bbs/Gossiping/index39123.html |
print(s["href"], s.text) |
印出每篇文章的網址與標題 | s["href"] 是文章的連結,s.text 是標題 |
find_all(class_=re.compile("^bold")) ^ 符號出現在 bold 前面的原因
尋找所有 HTML 標籤,這些標籤的 class 屬性值是以 "bold" 開頭的字串。
舉例來說,以下這些 class 屬性值都會被這個正規表達式匹配到:
bold
bold-text
boldItalic
bold_important
但是,以下這些 class 屬性值則不會被匹配到:
text-bold (因為 "bold" 不是開頭)
lightbold (因為 "bold" 不是開頭)
CSS 選擇器的優點:
在處理具有多個 CSS class 的情況時,使用 CSS 選擇器通常比使用 find_all() 搭配正規表達式或其他方式
更簡潔、更直觀,並且更符合 CSS 的語法習慣。這使得程式碼更容易閱讀和維護。
總之,css_soup.select("p.strikeout.body")
這行程式碼精確地選取了 HTML 文件中那些同時擁有 strikeout 和 body 這兩個 CSS class 的 <p> 標籤
p_tag = css_soup.select("p.strikeout.body")
print(p_tag)
re 模組(正規表示式)進行文字比對與擷取,分兩段詳細解說
第一段程式碼解說
import re
string = '台中市南屯區埔興段35-1253號'
regex = re.compile(r'段(\d+-*\d*)(\d)(\d)')
match = regex.search(string)
print(match.group(1))
說明:
string 是目標字串:「台中市南屯區埔興段35-1253號」
正則表達式 r'段(\d+-*\d*)(\d)(\d)' 是要抓出「段」後面的數字部分。
正則式解析:
部分 意義
段 精確匹配「段」這個字
(\d+-*\d*) 第一群組:一串數字,中間可以有「-」,例如 35-1253
(\d) 第二群組:單一數字
(\d) 第三群組:單一數字
第二段程式碼解說
test_string = 'aabb abb aab dcd hab aee ab aaaaab'
pattern = 'a+b'
ans = re.findall(pattern, test_string)
print(ans)
說明:
re.findall() 是搜尋所有符合條件的子字串,並以列表回傳。
模式 'a+b' 代表:
一個或多個 a(a+)
緊接著一個 b
| 字串片段 | 符合嗎 | 原因 |
| aabb | O | aa + b(前面 a+ 吃掉兩個 a) |
| abb | O | a + b(符合 a+b) |
| aab | O | aa + b |
| dcd | False | 沒有 a 或 b |
| hab | O | ab 符合,但前面 h 被忽略 |
| aee | False | 沒有符合 a+b |
| ab | O | a + b |
| aaaaab | O | aaaaa + b |
['aab', 'ab', 'aab', 'ab', 'ab', 'aaaaab']
雖然看到「hab」,但 re.findall() 抓的是符合「a+b」的部分,不會包含 h。
比對單一結果 re.search() 回傳第一個匹配物件(可用 .group() 抓取群組)
搜尋所有結果 re.findall() 回傳所有符合條件的字串(回傳 list)
編譯正則表達式 re.compile() 讓樣式重複使用更方便
