Crawler: 網路資料解析與爬蟲

Table of Contents

Hits

1. JSON

1.1. 網路資料分析的兩種類型

  • 可直接下載的靜態結構化資料,如 CSV、JSON、XML
  • 直接分析網站的線上內容(HTML): 爬蟲(Web Crawler)

1.2. JSON

1.2.1. JSON 是什麼?

  • JSON(JavaScript Object Notation,JavaScript 物件表示法)是個以純文字來描述資料的簡單結構,在 JSON 中可以透過特定的格式去儲存任何資料(字串,數字,陣列,物件),也可以透過物件或陣列來傳送較複雜的資料。1
  • JSON 常用於網站上的資料呈現、傳輸 (例如將資料從伺服器送至用戶端,以利顯示網頁)。
  • 一旦建立了 JSON 資料,就可以非常簡單的跟其他程式溝通或交換資料,因為 JSON 就只是純文字格式。

1.2.2. JSON 的優點

  • 相容性高
  • 格式容易瞭解,閱讀及修改方便
  • 支援許多資料格式 (number,string,booleans,nulls,array,associative array)
  • 許多程式都支援函式庫讀取或修改 JSON 資料
  • NoSQL Database

1.2.3. JSON 結構

  • 物件: {}
  • 陣列: []

1.2.4. JSON 支援的資料格式

  • 字串 (string),要以雙引號括起來:
    • {“name”: “Mike”}
  • 數值 (number)
    • {“age”: 25}
  • 布林值 (boolean)
    • {“pass”: true}
  • 空值 (null)
    • {“middlename”: null}
  • 物件 (object)
  • 陣列 (array)

1.2.5. JSON 物件

  • 格式
  • key 只能是字串,也一定要加上雙引號
1: {
2:     "key1": value1,
3:     "key2": value2,
4:     ......
5:     "keyN": valueN
6: }
  • 範例 1
1: {
2:     "id": 1,
3:     "name": "Jamees, Yen",
4:     "age": 19,
5:     "gender": "M",
6:     "hobby": ["music", "programming"]
7: }
  • 範例 2
 1: {
 2:     "id": 382192
 3:     "name": "Jamees, Yen",
 4:     "age": 19,
 5:     "gender": "M",
 6:     "exams": [
 7:         { "title": "期中考",
 8:           "chinese": 85,
 9:           "math": 98,
10:           "english": 92
11:         },
12:         { "title": "期末考",
13:           "chinese": 81,
14:           "math": 92,
15:           "english": 97
16:         }
17:     ],
18:     "hobby": ["music", "programming"]
19: }

1.2.6. JSON 轉 PANDAS dataFrame

1: from pandas.io.json import json_normalize
2: 
3: df = json_normalize( list of dict )
4: print(df.head(3))

1.3. JSON 實作範例

1.3.1. 實作 1: 國際主要國家貨幣每月匯率概況

1.3.1.1. 下載 JSON
 1: # -*- coding: utf-8 -*-
 2: import requests
 3: 
 4: json_url = 'http://quality.data.gov.tw/dq_download_json.php?nid=31897&md5_url=bf1f3b3e4a09f5c75ec9af665afbf7b3'
 5: response = requests.get(json_url)
 6: print(response)
 7: jsonRes = response.json()
 8: print(type(jsonRes))
 9: 
10: import json
11: print(jsonRes[:1])
12: print(json.dumps(jsonRes[:2], indent = 4, ensure_ascii=False))

上面這段程式碼用到了幾個重要的東西,我們先來認識一下:

  1. requests.get() 常用參數

    requests.get() 是用來發送 HTTP GET 請求的函式,完整寫法長這樣:

    1: requests.get(url, params=None, headers=None, timeout=None, verify=True)
    
    參數 說明
    params 查詢字串參數(dict),會自動附加到 URL
    headers 自訂 HTTP 標頭(dict),常用來設定 User-Agent
    timeout 等待回應的秒數,避免程式卡住
    verify SSL 憑證驗證,設為 False 可跳過驗證(不建議,但有時不得已QQ)
  2. response 物件常用屬性

    requests.get() 回傳的是一個 Response 物件,常用的屬性如下:

    屬性 / 方法 說明
    response.status_code HTTP 狀態碼(200 表示成功)
    response.text 回應內容(字串)
    response.json() 將 JSON 回應解析為 dict/list(等同 json.loads(response.text)
    response.encoding 回應的編碼
  3. json.dumps() 常用參數

    json.dumps() 可以把 Python 物件轉成 JSON 格式的字串,完整寫法:

    1: json.dumps(obj, indent=None, ensure_ascii=True, sort_keys=False)
    
    參數 說明
    indent 縮排空格數,讓輸出更易讀
    ensure_ascii 設為 False 才能輸出中文(不然會變 =\uXXXX=)
    sort_keys 設為 True 會按 key 排序

    如果你以上述程式碼執行,結果看到如下的錯誤訊息(請略過不重要的中間部分)留意「CERTIFICATE_VERIFY_FAILED]」,表示你的 Python 環境無法驗證該網站的 SSL 憑證….QQ

    Traceback (most recent call last):
       ...... 中間省略......
    increment
        raise MaxRetryError(_pool, url, reason) from reason  # type: ignore[arg-type]
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='quality.data.gov.tw', port=443): Max retries exceeded with url: /dq_download_json.php?nid=31897&md5_url=bf1f3b3e4a09f5c75ec9af665afbf7b3 (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Missing Subject Key Identifier (_ssl.c:1032)')))
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "<stdin>", line 5, in <module>
       ...... 中間省略......
        raise SSLError(e, request=request)
    requests.exceptions.SSLError: HTTPSConnectionPool(host='quality.data.gov.tw', port=443): Max retries exceeded with url: /dq_download_json.php?nid=31897&md5_url=bf1f3b3e4a09f5c75ec9af665afbf7b3 (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Missing Subject Key Identifier (_ssl.c:1032)')))
    [ Babel evaluation exited with code 1 ]
    

    解決方法:

     1: import requests
     2: import urllib3
     3: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
     4: ## 暫時關閉 SSL 驗證
     5: json_url = "https://quality.data.gov.tw/dq_download_json.php?nid=31897&md5_url=bf1f3b3e4a09f5c75ec9af665afbf7b3"
     6: 
     7: r = requests.get(json_url, verify=False, timeout=30)
     8: print(r.status_code)
     9: 
    10: jsonRes = r.json()
    11: print(type(jsonRes))
    12: print(jsonRes[:1]) #顯示第一筆資料
    13: 
    14: import json
    15: print(json.dumps(jsonRes[:2], indent=4, ensure_ascii=False)) #顯示前兩筆資料
    16: 
    
    200
    <class 'list'>
    [{'月別': '2024-03', '新台幣(匯率)': '31.990', '人民幣(匯率)': '7.2232', '日圓(匯率)': '151.34', '韓元(匯率)': '1347.2', '新加坡元(匯率)': '1.3494', '歐元(匯率)': '1.0769', '英鎊(匯率)': '1.2619', '澳幣(匯率)': '0.6509'}]
    [
        {
            "月別": "2024-03",
            "新台幣(匯率)": "31.990",
            "人民幣(匯率)": "7.2232",
            "日圓(匯率)": "151.34",
            "韓元(匯率)": "1347.2",
            "新加坡元(匯率)": "1.3494",
            "歐元(匯率)": "1.0769",
            "英鎊(匯率)": "1.2619",
            "澳幣(匯率)": "0.6509"
        },
        {
            "月別": "2024-04",
            "新台幣(匯率)": "32.542",
            "人民幣(匯率)": "7.2416",
            "日圓(匯率)": "156.86",
            "韓元(匯率)": "1382.0",
            "新加坡元(匯率)": "1.3614",
            "歐元(匯率)": "1.0705",
            "英鎊(匯率)": "1.2542",
            "澳幣(匯率)": "0.6528"
        }
    ]
    

    由以上輸出可知,這是一個包含多筆資料的 JSON 陣列,而每筆資料都是一個包含多個鍵值對的 JSON 物件。每個物件代表某個月份的匯率資訊,包括新台幣、人民幣、日圓、韓元、新加坡元、歐元、英鎊和澳幣的匯率。

    那,我們來看看如何從這些資料中提取出我們需要的資訊,例如「日期」和「美元/新台幣」的匯率。

1.3.1.2. 輸出日期與匯率

我們可以使用一個簡單的迴圈來遍歷 JSON 陣列,並從每個物件中提取出「月別」和「新台幣(匯率)」這兩個欄位,然後將它們格式化輸出。

1: print('日    期', "\t", '美元/新台幣')
2: print('=======', "\t", '===========')
3: for item in jsonRes[:10]:
4:     print(item['月別'], "\t", item['新台幣(匯率)'])
日    期 	 美元/新台幣
======= 	 ===========
2024-03 	 31.990
2024-04 	 32.542
2024-05 	 32.420
2024-06 	 32.450
2024-07 	 32.836
2024-08 	 31.940
2024-09 	 31.651
2024-10 	 32.031
2024-11 	 32.457
2024-12 	 32.781
1.3.1.3. 製圖

有了日期和匯率的資料後,我們可以使用 Matplotlib 來繪製折線圖,展示美元/新台幣匯率的走勢。

 1: # -*- coding: utf-8 -*-
 2: import matplotlib.pyplot as plt
 3: 
 4: plt.figure(figsize=(10, 6))
 5: 
 6: plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # 步驟一(替換系統中的字型,這裡用的是Mac OSX系統)
 7: plt.rcParams['axes.unicode_minus'] = False  # 步驟二(解決座標軸負數的負號顯示問題)
 8: 
 9: # 提取日期和美元/新台幣的數據
10: dates = [item['月別'] for item in jsonRes]
11: # 上面是一段精簡的寫法,等同於下面的寫法
12: # dates = []
13: # for item in jsonRes:
14: #     dates.append(item['月別'])
15: # 意思就是把每一筆資料的「月別」欄位取出來,放到一個新的列表(dates)中
16: exchange_rates = [float(item['新台幣(匯率)']) for item in jsonRes]
17: # 這裡也一樣,請自行領悟
18: 
19: # 繪製折線圖
20: plt.figure(figsize=(10, 6))
21: plt.plot(dates, exchange_rates, marker='o', linestyle='-')
22: 
23: # 設置圖形標籤和標題
24: plt.xlabel('日期')
25: plt.ylabel('美元/新台幣')
26: plt.title('美元/新台幣匯率走勢')
27: 
28: # 旋轉x軸上的日期標籤,以避免重疊
29: plt.xticks(rotation=45, ha='right')
30: 
31: # 顯示圖形
32: plt.tight_layout()
33: plt.savefig('images/json-plot.png', dpi=300)

json-plot.png

Figure 1: 美元/新台幣匯率走勢

現在,你應該已經具備了從網路上下載 JSON 資料、解析資料、提取所需資訊,並使用 Matplotlib 繪製圖表的基本能力。接下來,你可以嘗試應用這些技巧來分析其他類型的 JSON 資料,並創建更多有趣的視覺化圖表!

1.3.2. 實作 2: 澎湖生活博物館每月參觀人次統計資料

 1: #coding:utf-8
 2: import matplotlib.pyplot as plt
 3: # 解決中文問題
 4: plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # 步驟一(替換系統中的字型,這裡用的是Mac OSX系統)
 5: plt.rcParams['axes.unicode_minus'] = False  #
 6: 
 7: import requests
 8: import pandas as pd
 9: from datetime import datetime
10: 
11: json_url = 'http://opendataap2.penghu.gov.tw/resource/files/2020-01-12/eaa641fc3af66277e60b13201ca11232.json'
12: 
13: response = requests.get(json_url)
14: jsonRes = response.json()
15: 
16: year = [str(int(i['年度'])+1911) for i in jsonRes]
17: month = [i['月份'] for i in jsonRes]
18: visitor = [int(i['人數']) for i in jsonRes]
19: 
20: dates = [x+'/'+y for x, y in zip(year, month)]
21: 
22: from datetime import datetime
23: dates = [datetime.strptime(x, '%Y/%m').date() for x in dates]
24: 
25: import matplotlib.pyplot as plt
26: plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # 步驟一(替換系統中的字型,這裡用的是Mac OSX系統)
27: plt.rcParams['axes.unicode_minus'] = False  # 步驟二(解決座標軸負數的負號顯示問題)
28: 
29: plt.clf()
30: plt.barh(dates, visitor, height=0.5)
31: plt.xticks(rotation=0, fontsize=10)
32: plt.yticks(rotation=0, fontsize=10)
33: plt.xlabel('人數')
34: plt.ylabel('日期')
35: plt.title('澎湖生活博物館每月參觀人次')
36: plt.savefig('images/jsonBar.png', dpi=300, bbox_inches='tight')

jsonBar.png

Figure 2: 參觀人數

1.4. [課堂練習]線上 JSON 讀取   TNFSH

  • [注意]如果 JSON 的資料下載發生錯誤,可以試著將 URL 由’https://’改為’http://’
  • 請上網查詢台南市各區 WIFI 熱點數量,依行政區統計,繪製類似以下圖形,儘可能加上統計圖表所需元素並加以美化

    wifis.png

    Figure 3: 台南市各區熱點分佈

1.6. [課堂練習]社團 JSON 資料處理   TNFSH

以下是某校社團的 JSON 資料,請使用 Python 的 json 模組完成以下任務:

  1. 列出所有社團名稱與人數
  2. 找出人數最多的社團
  3. 按類別(學術/音樂/體育/休閒)統計社團數量
  4. 找出 2019 年之後成立的社團
 1: import json
 2: 
 3: club_data = '''
 4: {
 5:     "school": "台南一中",
 6:     "clubs": [
 7:         {"name": "程式設計社", "members": 25, "category": "學術", "year": 2020},
 8:         {"name": "吉他社", "members": 40, "category": "音樂", "year": 2015},
 9:         {"name": "籃球社", "members": 30, "category": "體育", "year": 2018},
10:         {"name": "動漫社", "members": 35, "category": "休閒", "year": 2019},
11:         {"name": "管樂社", "members": 45, "category": "音樂", "year": 2010},
12:         {"name": "桌遊社", "members": 20, "category": "休閒", "year": 2021},
13:         {"name": "排球社", "members": 28, "category": "體育", "year": 2016},
14:         {"name": "英語辯論社", "members": 15, "category": "學術", "year": 2017}
15:     ]
16: }
17: '''
18: data = json.loads(club_data)

結果範例

=== 台南一中社團資訊 ===
社團總數: 8 個

1. 各社團人數:
   程式設計社: 25 人
   吉他社: 40 人
   籃球社: 30 人
   動漫社: 35 人
   管樂社: 45 人
   桌遊社: 20 人
   排球社: 28 人
   英語辯論社: 15 人

2. 人數最多的社團: 管樂社 (45 人)

3. 各類別社團數:
   學術: 2 個
   音樂: 2 個
   體育: 2 個
   休閒: 2 個

4. 2019年之後成立的社團:
   程式設計社 (2020年成立)
   動漫社 (2019年成立)
   桌遊社 (2021年成立)

2. HTML

2.1. HTML 是什麼

  • 政府資料開放平台
  • 超文本標記語言(HyperText Markup Language)是一種用於建立網頁的標準標記語言。HTML 是一種基礎技術,常與 CSS、JavaScript 一起被眾多網站用於設計網頁、網頁應用程式以及行動應用程式的使用者介面。2
  • HTML 元素是構建網站的基石。HTML 允許嵌入圖像與物件,並且可以用於建立互動式表單,它被用來結構化資訊——例如標題、段落和列表等等,也可用來在一定程度上描述文件的外觀和語意。2
  • HTML 的語言形式為<>包圍的 HTML 元素(如<html>),瀏覽器使用 HTML 標籤和指
    令碼來詮釋網頁內容,但不會將它們顯示在頁面上。 2
  • 網頁就是由各式標籤 (tag) 所組成的階層式文件,例如:
    • <title>我是網頁標題</title>
    • <h1 class=“large”>我是變色且置中的抬頭</h1>
    • <p id=“p1”>我是段落一</p>

2.2. HTML 範例

2.2.1. HTML 程式碼

 1: <html>
 2:   <head>
 3:     <title>我是網頁標題</title>
 4:     <style>
 5:     .large {
 6:       color:blue;
 7:       text-align: center;
 8:     }
 9:     </style>
10:   </head>
11:   <body>
12:     <h1 class="large">我是變色且置中的抬頭</h1>
13:     <p id="p1">我是段落一</p>
14:     <p id="p2" style="">我是段落二</p>
15:     <div><a href='http://blog.castman.net' style="font-size:200%;">我是放大的超連結</a></div>
16:   </body>
17: </html>
2.2.1.1. 執行結果

上述 html code 的執行結果如下

我是網頁標題

我是變色且置中的抬頭

我是段落一

我是段落二

2.2.2. HTML tree

html-tree.png

Figure 4: HTML tree

  • HTML 文件內不同的標籤 (例如 <title>, <h1>, <p>, <a>, <div>)
  • 不同的標籤有著不同的語義,表示建構網頁用的不同元件,且標籤可以有各種屬性 (例如 id, class, style 等通用屬性, 或 href 等專屬屬性),
  • <div>是 division 這個單字取前面三個字母來表示,division 是區分的意思,div 標籤主要的功能就是在形成一個個的區塊,方便網頁排版美化3。例如:
1: <div style="background-color:grey;">
2:     <p>今天是第7天的介紹,div範例程式</p>
3:     <p>今天是第7天的介紹,div範例程式</p>
4: </div>

其結果為:

今天是第7天的介紹,div範例程式

今天是第7天的介紹,div範例程式

  • 我們可以用標籤 + 屬性去定位資料所在的區塊並取得資料。4

3. 網路爬蟲

上述的 html 程式碼只能透過瀏覽器來解析並呈現結果,如果我們想跳過瀏覽器,直接以程式來抓取網頁上的資訊,就要透過爬蟲程式。

3.1. 爬蟲精神:將程式模仿成一般 user

3.1.1. 原則

  1. 要抓網路資料,就要先仔細觀察網頁內容
  2. 要儘量欺騙網站自己是 browser

3.1.2. 實作範例: 取得 HTML 資料

3.1.2.1. 第一版: 以 urllib 模組來抓取 HTML 資料

urllib.request 是一個用來從 URLs (Uniform Resource Locators)取得資料的 Python 模組。它提供了一個非常簡單的介面能接受多種不同的協議(protocol, 如 http, ftp), urlopen 函數。

1: # 抓取PTT電影版header
2: import urllib.request as req
3: url = "https://www.ptt.cc/bbs/Movie/index.html"
4: 
5: with req.urlopen(url) as response:
6:     data = response.read().decode("utf-8")
7: print(data)

結果得到如下錯誤訊息:

1: urllib.error.HTTPError: HTTP Error 403: Forbidden

被拒絕原因:這隻程式的行為不像一般使用者,被網站伺服器拒絕。403 Forbidden 是 HTTP 協議中的一個 HTTP 狀態碼(Status Code)。可以簡單的理解為沒有權限訪問此站,服務器收到請求但拒絕提供服務5

3.1.2.2. 第二版:加入 headers(純文字)
 1: import urllib.request as req
 2: url = "https://www.ptt.cc/bbs/Movie/index.html"
 3: # 幫request加上一個header
 4: request = req.Request(url, headers = {
 5:     "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:72.0) Gecko/20100101 Firefox/72.0"
 6: })
 7: 
 8: with req.urlopen(request) as response:
 9:     data = response.read().decode("utf-8")
10: print('所取得的資料型態:',type(data))
11: print('===得到的部份HTML內容===\n', data[2651:3500])
所取得的資料型態: <class 'str'>
===得到的部份HTML內容===
 		</div>
				<div class="date">12/19</div>
				<div class="mark"></div>
			</div>
		</div>





		<div class="r-ent">
			<div class="nrec"><span class="hl f2">3</span></div>
			<div class="title">

				<a href="/bbs/movie/M.1766116859.A.6F8.html">[問片] 某部非常久遠的二戰片 (有雷)</a>

			</div>
			<div class="meta">
				<div class="author">Maserati</div>
				<div class="article-menu">

					<div class="trigger">&#x22ef;</div>
					<div class="dropdown">
						<div class="item"><a href="/bbs/movie/search?q=thread%3A%5B%E5%95%8F%E7%89%87%5D&#43;%E6%9F%90%E9%83%A8%E9%9D%9E%E5%B8%B8%E4%B9%85%E9%81%A0%E7%9A%84%E4%BA%8C%E6%88%B0%E7%89%87&#43;%28%E6%9C%89%E9%9B%B7%29">搜尋同標題文章</a></div>

						<div class="item"><a href="/bbs/movie/search?q=author%3AMaserati">搜尋看板內 Maserati 的文章</a></div>

					</div>

可以發現抓取下來的結果都是字串型態,不太容易進一步擷取所需資訊。例如,當我們想抓取所有看板中的標題,只使用字串所提供的一些函式就很難完成工作。

3.2. BeautifulSoup

BeautifulSoup 是一個可以從 HTML 或 XML 檔案中擷取資料的 Python 套件。它可以自動將不完整或格式錯誤的標記修正,並以 Python 物件的形式呈現文件結構,讓使用者能夠輕鬆地導航、搜尋和修改文件內容。

建立 BeautifulSoup 物件的基本語法:

1: soup = BeautifulSoup(markup, parser)

其中 parser 是用來解析 HTML 的解析器,常用的有:

解析器 說明
'html.parser' Python 內建,不需額外安裝,夠用了
'lxml' 速度快,需 pip install lxml
'html5lib' 最寬容,能處理嚴重損壞的 HTML,需另外安裝

一般來說用 html.parser 就可以了,除非你遇到什麼奇怪的網頁解析不了再考慮換別的。

3.2.1. 實作:解析 HTML 資料內容

3.2.1.1. 安裝解析 HTML 所需套件(安裝套件名稱後面有 4)

如果尚未安裝 BeautifulSoup4,可以使用 pip 來安裝,在PyCharm裡你可以打開 Terminal 視窗,輸入以下指令安裝

1: pip install beautifulsoup4

如果你是使用Colab,可以在程式碼區塊輸入以下指令安裝

1: !pip install beautifulsoup4
3.2.1.2. 第三版: 以 BS4 解析 HTML
 1: import urllib.request as req
 2: url = "https://www.ptt.cc/bbs/Japan_Travel/index.html"
 3: # 幫request加上一個header
 4: request = req.Request(url, headers = {
 5:     "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:72.0) Gecko/20100101 Firefox/72.0"
 6: })
 7: 
 8: with req.urlopen(request) as response:
 9:     data = response.read()
10: 
11: import bs4
12: doc = bs4.BeautifulSoup(data,"html.parser")
13: print(type(doc))
14: print(doc.prettify()[:4000])
15: 
<class 'bs4.BeautifulSoup'>
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8"/>
  <meta content="width=device-width, initial-scale=1" name="viewport"/>
  <title>
   看板 Japan_Travel 文章列表 - 批踢踢實業坊
  </title>
  <link href="//images.ptt.cc/bbs/v2.27/bbs-common.css" rel="stylesheet" type="text/css"/>
  <link href="//images.ptt.cc/bbs/v2.27/bbs-base.css" media="screen" rel="stylesheet" type="text/css"/>
  <link href="//images.ptt.cc/bbs/v2.27/bbs-custom.css" rel="stylesheet" type="text/css"/>
  <link href="//images.ptt.cc/bbs/v2.27/pushstream.css" media="screen" rel="stylesheet" type="text/css"/>
  <link href="//images.ptt.cc/bbs/v2.27/bbs-print.css" media="print" rel="stylesheet" type="text/css"/>
 </head>
 <body>
  <div id="topbar-container">
   <div class="bbs-content" id="topbar">
    <a href="/bbs/" id="logo">
     批踢踢實業坊
    </a>
    <span>
     ›
    </span>
    <a class="board" href="/bbs/Japan_Travel/index.html">
     <span class="board-label">
      看板
     </span>
     Japan_Travel
    </a>
    <a class="right small" href="/about.html">
     關於我們
    </a>
    <a class="right small" href="/contact.html">
     聯絡資訊
    </a>
   </div>
  </div>
  <div id="main-container">
   <div id="action-bar-container">
    <div class="action-bar">
     <div class="btn-group btn-group-dir">
      <a class="btn selected" href="/bbs/Japan_Travel/index.html">
       看板
      </a>
      <a class="btn" href="/man/Japan_Travel/index.html">
       精華區
      </a>
     </div>
     <div class="btn-group btn-group-paging">
      <a class="btn wide" href="/bbs/Japan_Travel/index1.html">
       最舊
      </a>
      <a class="btn wide" href="/bbs/Japan_Travel/index8564.html">
       ‹ 上頁
      </a>
      <a class="btn wide disabled">
       下頁 ›
      </a>
      <a class="btn wide" href="/bbs/Japan_Travel/index.html">
       最新
      </a>
     </div>
    </div>
   </div>
   <div class="r-list-container action-bar-margin bbs-screen">
    <div class="search-bar">
     <form action="search" id="search-bar" type="get">
      <input class="query" name="q" placeholder="搜尋文章⋯" type="text" value=""/>
     </form>
    </div>
    <div class="r-ent">
     <div class="nrec">
     </div>
     <div class="title">
      <a href="/bbs/Japan_Travel/M.1766056069.A.809.html">
       Fw: [食記] 喜一 福島喜多方 鮮味軟嫩叉燒喜多方拉麵
      </a>
     </div>
     <div class="meta">
      <div class="author">
       Geiwoyujie
      </div>
      <div class="article-menu">
       <div class="trigger">
        ⋯
       </div>
       <div class="dropdown">
        <div class="item">
         <a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E9%A3%9F%E8%A8%98%5D+%E5%96%9C%E4%B8%80+%E7%A6%8F%E5%B3%B6%E5%96%9C%E5%A4%9A%E6%96%B9+%E9%AE%AE%E5%91%B3%E8%BB%9F%E5%AB%A9%E5%8F%89%E7%87%92%E5%96%9C%E5%A4%9A%E6%96%B9%E6%8B%89%E9%BA%B5">
          搜尋同標題文章
         </a>
        </div>
        <div class="item">
         <a href="/bbs/Japan_Travel/search?q=author%3AGeiwoyujie">
          搜尋看板內 Geiwoyujie 的文章
         </a>
        </div>
       </div>
      </div>
      <div class="date">
       12/18
      </div>
      <div class="mark">
      </div>
     </div>
    </div>
    <div class="r-ent">
     <div class="nrec">
      <span class="hl f3">
       63
      </span>
     </div>
     <div class="title">
      <a href="/bbs/Japan_Travel/M.1766058399.A.C20.html">
       [資訊] JCB日本消費滿10萬回饋1萬
      </a>
     </div>
     <div class="meta">
      <div class="author">
       medama
      </div>
      <div class="article-menu">
       <div class="trigger">
        ⋯
       </div>
       <div class="dropdown">
        <div class="item">
         <a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E8%B3%87%E8%A8%8A%5D+JCB%E6%97%A5%E6%9C%AC%E6%B6%88%E8%B2%BB%E6%BB%BF10%E8%90%AC%E5%9B%9E%E9%A5%8B1%E8%90%AC">
          搜尋同標題文章
         </a>
        </div>
        <div class="item">
         <a href="/bbs/Japan_Travel/search?q=author%3Amedama">
          搜尋看板內 medama 的文章
         </a>
        </div>
       </div>
      </div>
      <div class="date">

看到這裡,我們成功地將 HTML 解析成 BeautifulSoup 物件,接下來我們就可以利用 BeautifulSoup 所提供的方法來擷取我們想要的資料了。

3.3. BeautifulSoup 的方法6

當我們把原本亂七八糟的 HTML 程式從「字串」解析成 「BeautifulSoup 物件」後,接下來就可以利用 BeautifulSoup 所提供的方法來擷取我們想要的資料了。以下是常用的方法說明:

方法 說明
select() 以 CSS 選擇器的方式尋找指定的 tag。
find_all() 以所在的 tag 位置,尋找內容裡所有指定的 tag。
find() 以所在的 tag 位置,尋找第一個找到的 tag。
find_parents() 以所在的 tag 位置,尋找父層所有指定的 tag 或第一個找到的 tag。
find_parent()  
find_next_siblings() 以所在的 tag 位置,尋找同一層後方所有指定的 tag 或第一個找到的 tag。
find_next_sibling()  
find_previous_siblings() 以所在的 tag 位置,尋找同一層前方所有指定的 tag 或第一個找到的 tag。
find_previous_sibling()  
find_all_next() 以所在的 tag 位置,尋找後方內容裡所有指定的 tag 或第一個找到的 tag。
find_next()  
find_all_previous() 所在的 tag 位置,尋找前方內容裡所有指定的 tag 或第一個找到的 tag。
find_previous()  

看起來有點複雜,其實一點也不簡單。

啊不是,我是要說,沒關係,在這裡我只打算介紹其中兩個最常用的方法:find()、find_all(),其他方法就留給有興趣的同學自行研究囉!

先看一下這兩個方法的完整參數:

1: soup.find(name, attrs, string, **kwargs)       # 回傳第一個符合的 Tag
2: soup.find_all(name, attrs, string, limit, **kwargs)  # 回傳所有符合的 Tag(list)
參數 說明
name 標籤名稱,如 'div', 'a'
attrs 屬性 dict,如 attrs={'class': 'title'}
string 搜尋標籤內的文字
limit (僅 find_all )限制回傳的最大數量

有個方便的簡寫:可以直接用 class_ 當參數名稱來篩選 class(因為 class 是 Python 保留字,所以要加底線),例如:

1: # 以下兩種寫法效果一樣
2: soup.find('a', class_='link')
3: soup.find('a', attrs={'class': 'link'})

4. 先說我們最後到底想完什麼

目標: 讓Python自己去抓取批踢踢 Japan_Travel 看板(https://www.ptt.cc/bbs/Japan_Travel/index.html)中所有文章的標題,並列出來,

目前的這個網頁長的像這樣:

2025-12-19_14-41-11.png

Figure 5: 2025-12-19的Japan_Travel看板

我們想要的結果是像這樣:

第1則貼文::Fw: [食記] 喜一 福島喜多方 鮮味軟嫩叉燒喜多方拉麵
第2則貼文::[資訊] JCB日本消費滿10萬回饋1萬
第3則貼文::[遊記] 三保松原、富士山五湖、昇仙峽(前篇)
第4則貼文::[資訊] JR四國松山-宇和島指定日期部分路段停駛
第5則貼文::Re: [新聞] 獨/基隆-石垣島渡輪「延至10月開航
第6則貼文::[遊記] 宮崎五天四夜(非自駕獨旅)-2
第7則貼文::Re: [問題] 請教熊本熊港八代交通
第8則貼文::[資訊] 20251219匯率
第9則貼文::[問題] skyliner 成田往日暮里有需要提前買票嗎
第10則貼文::[問題] 御朱印帳的尺寸
第11則貼文::[資訊] 台灣虎航 26夏增班開賣
第12則貼文::[資訊] 長榮航空台北=青森航線增班
第13則貼文::[遊記] SL磐越物語與只見線攝影紀行
第14則貼文::[公告] 日本旅遊板板規 114.02.02
第15則貼文::Fw: [公告] 請避免與登入1次之帳號進行交易
第16則貼文::Fw: [公告] 違反菸害防制法本站將依使用者條款處分
第17則貼文::[公告] 114.12 實況回報 (含天氣/物候/穿著詢問)

現在,我們就一步一步來完成這個目標吧!

#+begin_src python -r -n :async :results output :exports both :session BS

我們可以先觀察 HTML 程式碼,發現每一篇文章的標題都在 <div class=“title”> 這個 tag 裡面,例如:

5. BS4:開始爬

在[第三版: 以 BS4 解析 HTML]中,我們以 bs4.BeautifulSoup()將下載的 html 解析成 BS4 的物件,現在開始我們可以利用 BS4 來探索、抓取 html 的內容。

doc = bs4.BeautifulSoup(data,"html.parser")

5.1. bs4.tag

最簡單的方式是利用如下的格式取得 bs4 物件中的 tag

bs4.tag

5.1.1. <title>

例如,下例為取出 html 程式碼中的標題 tag

1: print(doc.title)
<title>看板 Japan_Travel 文章列表 - 批踢踢實業坊</title>

看的出這是個文字屬性的 tag,所以我們可以進一步以 text 或 string 取得其中的字串

1: print(doc.title.text)
2: print(doc.title.string)
看板 Japan_Travel 文章列表 - 批踢踢實業坊
看板 Japan_Travel 文章列表 - 批踢踢實業坊

5.1.2. <div>

繼續取出 html 中的<div>tag,結果如下

1: print(doc.div)
<div id="topbar-container">
<div class="bbs-content" id="topbar">
<a href="/bbs/" id="logo">批踢踢實業坊</a>
<span>›</span>
<a class="board" href="/bbs/Japan_Travel/index.html"><span class="board-label">看板 </span>Japan_Travel</a>
<a class="right small" href="/about.html">關於我們</a>
<a class="right small" href="/contact.html">聯絡資訊</a>
</div>
</div>

聰明的你應該已經發現,雖然 html 中的非常多組的<div>,但是目前我們就只抓取到其中的 第一組 <div>。

5.1.3. <a>

依此類推,我們還能由取得的 doc.div 繼續抓到裡面的第一組<a> tag

1: print(doc.div.a)
<a href="/bbs/" id="logo">批踢踢實業坊</a>

tag <a>是一個超連結(hyperlink),除了’href’和’id’,還有’target’, ’download’, ’ref’…等屬性7。我們可以透過以下的方式取得 tag 中的相關內容

1: print(doc.div.a.text)
2: print(doc.div.a['id'])
3: print(doc.div.a['href'])
批踢踢實業坊
logo
/bbs/

5.2. find

BS4 的 find()可以較彈性的找出 html 中不同類型的<tag>,find 有兩個常用的用法8

5.2.1. 直接接標籤名稱

5.2.1.1. Soup.find(‘tag’)

可以直接找到第一個 a 標籤的元素,例如

1: print(doc.find('a'))
<a href="/bbs/" id="logo">批踢踢實業坊</a>
1: print(doc.find('div'))
<div id="topbar-container">
<div class="bbs-content" id="topbar">
<a href="/bbs/" id="logo">批踢踢實業坊</a>
<span>›</span>
<a class="board" href="/bbs/Japan_Travel/index.html"><span class="board-label">看板 </span>Japan_Travel</a>
<a class="right small" href="/about.html">關於我們</a>
<a class="right small" href="/contact.html">聯絡資訊</a>
</div>
</div>

5.2.2. 指定屬性名稱用鍵值方式對應

5.2.2.1. Soup.find(class_=’類別名稱’)

可以找到第一個 class 為’abc’的元素,要注意的是因為 python 中 class 是保留字,所以要用 class_才能表達

1: qq = doc.find(class_='r-ent')
2: print(qq)
<div class="r-ent">
<div class="nrec"><span class="hl f2">9</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709715202.A.9ED.html">[問題] 京都住宿請益</a>
</div>
<div class="meta">
<div class="author">poi9430</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E5%95%8F%E9%A1%8C%5D+%E4%BA%AC%E9%83%BD%E4%BD%8F%E5%AE%BF%E8%AB%8B%E7%9B%8A">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Apoi9430">搜尋看板內 poi9430 的文章</a></div>
</div>
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>
5.2.2.2. Soup.find(id=’id 名稱’)

可以找到第一個 id 為’id 名稱’的元素

1: qq = doc.find(id='topbar')
2: print(qq)
<div class="bbs-content" id="topbar">
<a href="/bbs/" id="logo">批踢踢實業坊</a>
<span>›</span>
<a class="board" href="/bbs/Japan_Travel/index.html"><span class="board-label">看板 </span>Japan_Travel</a>
<a class="right small" href="/about.html">關於我們</a>
<a class="right small" href="/contact.html">聯絡資訊</a>
</div>
5.2.2.3. Soup.find(type=’類型名稱’)

可以找到第一個 type 為’類型名稱’的元素

1: qq = doc.find(type='get')
2: print(qq)
<form action="search" id="search-bar" type="get">
<input class="query" name="q" placeholder="搜尋文章⋯" type="text" value=""/>
</form>
5.2.2.4. 複合搜尋

上述的 doc.find(class_=’r-ent’)找出了以下的一整個“r-ent”tag

<div class="r-ent">
<div class="nrec"><span class="hl f2">9</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709715202.A.9ED.html">[問題] 京都住宿請益</a>
</div>
<div class="meta">
<div class="author">poi9430</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E5%95%8F%E9%A1%8C%5D+%E4%BA%AC%E9%83%BD%E4%BD%8F%E5%AE%BF%E8%AB%8B%E7%9B%8A">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Apoi9430">搜尋看板內 poi9430 的文章</a></div>
</div>
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>

這個’r-ent’<div>的結構大致如下:

e-rent.png

透過串接的 find(),我們可以進一步搜尋’r-ent’下的其他 tag,如

  • 例 1

    1:   print(doc.find(class_='r-ent').find(class_='title'))
    
    <div class="title">
    <a href="/bbs/Japan_Travel/M.1709715202.A.9ED.html">[問題] 京都住宿請益</a>
    </div>
    
  • 例 2

    1:   print(doc.find(class_='r-ent').find(class_='title').find('a'))
    
    <a href="/bbs/Japan_Travel/M.1709715202.A.9ED.html">[問題] 京都住宿請益</a>
    
  • 例 3

    1:   print(doc.find(class_='r-ent').find(class_='title').find('a').text)
    
    [問題] 京都住宿請益
    

5.3. find_all

find()只會傳回第一個符合條件的資料,我們可以使用 find_all 找出所有特定的 HTML 標籤節點,再以 Python 的迴圈來依序輸出每個超連結的文字9

1: QQ = doc.find_all(class_='r-ent')
2: print(QQ)
[<div class="r-ent">
<div class="nrec"><span class="hl f2">9</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709715202.A.9ED.html">[問題] 京都住宿請益</a>
</div>
<div class="meta">
<div class="author">poi9430</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E5%95%8F%E9%A1%8C%5D+%E4%BA%AC%E9%83%BD%E4%BD%8F%E5%AE%BF%E8%AB%8B%E7%9B%8A">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Apoi9430">搜尋看板內 poi9430 的文章</a></div>
</div>
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>, <div class="r-ent">
<div class="nrec"><span class="hl f3">17</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709718584.A.E96.html">[問題] 沖繩親子遊首衝安排請益</a>
</div>
<div class="meta">
<div class="author">newsnotti</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E5%95%8F%E9%A1%8C%5D+%E6%B2%96%E7%B9%A9%E8%A6%AA%E5%AD%90%E9%81%8A%E9%A6%96%E8%A1%9D%E5%AE%89%E6%8E%92%E8%AB%8B%E7%9B%8A">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Anewsnotti">搜尋看板內 newsnotti 的文章</a></div>
</div>
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>, <div class="r-ent">
<div class="nrec"><span class="hl f2">7</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709719948.A.882.html">Re: [問題] 請問最近札幌帝王蟹價格?</a>
</div>
<div class="meta">
<div class="author">grlie8027</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E5%95%8F%E9%A1%8C%5D+%E8%AB%8B%E5%95%8F%E6%9C%80%E8%BF%91%E6%9C%AD%E5%B9%8C%E5%B8%9D%E7%8E%8B%E8%9F%B9%E5%83%B9%E6%A0%BC%EF%BC%9F">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Agrlie8027">搜尋看板內 grlie8027 的文章</a></div>
</div>
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>, <div class="r-ent">
<div class="nrec"><span class="hl f3">16</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709724598.A.77E.html">[問題] IT216飛羽田的疑問</a>
</div>
<div class="meta">
<div class="author">Yijhen0525</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E5%95%8F%E9%A1%8C%5D+IT216%E9%A3%9B%E7%BE%BD%E7%94%B0%E7%9A%84%E7%96%91%E5%95%8F">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3AYijhen0525">搜尋看板內 Yijhen0525 的文章</a></div>
</div>
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>, <div class="r-ent">
<div class="nrec"><span class="hl f2">3</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709725167.A.3DC.html">[問題] 小豆島高松-土庄3/10-24航班時間變更</a>
</div>
<div class="meta">
<div class="author">chiao650617</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E5%95%8F%E9%A1%8C%5D+%E5%B0%8F%E8%B1%86%E5%B3%B6%E9%AB%98%E6%9D%BE-%E5%9C%9F%E5%BA%843%2F10-24%E8%88%AA%E7%8F%AD%E6%99%82%E9%96%93%E8%AE%8A%E6%9B%B4">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Achiao650617">搜尋看板內 chiao650617 的文章</a></div>
</div>
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>, <div class="r-ent">
<div class="nrec"></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709725566.A.401.html">[住宿] 佐賀太良町・有明海旁美食海鮮滿滿平浜荘</a>
</div>
<div class="meta">
<div class="author">peikie</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E4%BD%8F%E5%AE%BF%5D+%E4%BD%90%E8%B3%80%E5%A4%AA%E8%89%AF%E7%94%BA%E3%83%BB%E6%9C%89%E6%98%8E%E6%B5%B7%E6%97%81%E7%BE%8E%E9%A3%9F%E6%B5%B7%E9%AE%AE%E6%BB%BF%E6%BB%BF%E5%B9%B3%E6%B5%9C%E8%8D%98">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Apeikie">搜尋看板內 peikie 的文章</a></div>
</div>
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>, <div class="r-ent">
<div class="nrec"><span class="hl f3">16</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709725647.A.9A6.html">[資訊] 台灣虎航 tigerclub會員限定優惠活動</a>
</div>
<div class="meta">
<div class="author">yhls</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E8%B3%87%E8%A8%8A%5D+%E5%8F%B0%E7%81%A3%E8%99%8E%E8%88%AA+tigerclub%E6%9C%83%E5%93%A1%E9%99%90%E5%AE%9A%E5%84%AA%E6%83%A0%E6%B4%BB%E5%8B%95">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Ayhls">搜尋看板內 yhls 的文章</a></div>
</div>
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>, <div class="r-ent">
<div class="nrec"><span class="hl f2">1</span></div>
<div class="title">

				(本文已被刪除) [gragragra]

			</div>
<div class="meta">
<div class="author">-</div>
<div class="article-menu">
</div>
<div class="date"> 3/06</div>
<div class="mark"></div>
</div>
</div>, <div class="r-ent">
<div class="nrec"><span class="hl f2">4</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1694279303.A.1CE.html">[公告] 日本旅遊板板規 112.09.17 </a>
</div>
<div class="meta">
<div class="author">mithralin</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E5%85%AC%E5%91%8A%5D+%E6%97%A5%E6%9C%AC%E6%97%85%E9%81%8A%E6%9D%BF%E6%9D%BF%E8%A6%8F+112.09.17+">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Amithralin">搜尋看板內 mithralin 的文章</a></div>
</div>
</div>
<div class="date"> 9/10</div>
<div class="mark">M</div>
</div>
</div>, <div class="r-ent">
<div class="nrec"><span class="hl f3">31</span></div>
<div class="title">
<a href="/bbs/Japan_Travel/M.1709486877.A.7AF.html">[公告] 113.03 實況回報 (含天氣/物候/穿著詢問)</a>
</div>
<div class="meta">
<div class="author">sinohara</div>
<div class="article-menu">
<div class="trigger">⋯</div>
<div class="dropdown">
<div class="item"><a href="/bbs/Japan_Travel/search?q=thread%3A%5B%E5%85%AC%E5%91%8A%5D+113.03+%E5%AF%A6%E6%B3%81%E5%9B%9E%E5%A0%B1+%28%E5%90%AB%E5%A4%A9%E6%B0%A3%2F%E7%89%A9%E5%80%99%2F%E7%A9%BF%E8%91%97%E8%A9%A2%E5%95%8F%29">搜尋同標題文章</a></div>
<div class="item"><a href="/bbs/Japan_Travel/search?q=author%3Asinohara">搜尋看板內 sinohara 的文章</a></div>
</div>
</div>
<div class="date"> 3/04</div>
<div class="mark"></div>
</div>
</div>]

現在已經抓到所有 class 為’r-ent’的 tag,我們可以再繼續找出裡面的 title

1: for i in doc.find_all(class_='r-ent'):
2:     t = i.find(class_='title')
3:     print(t.prettify())
<div class="title">
 <a href="/bbs/Japan_Travel/M.1709715202.A.9ED.html">
  [問題] 京都住宿請益
 </a>
</div>

<div class="title">
 <a href="/bbs/Japan_Travel/M.1709718584.A.E96.html">
  [問題] 沖繩親子遊首衝安排請益
 </a>
</div>

<div class="title">
 <a href="/bbs/Japan_Travel/M.1709719948.A.882.html">
  Re: [問題] 請問最近札幌帝王蟹價格?
 </a>
</div>

<div class="title">
 <a href="/bbs/Japan_Travel/M.1709724598.A.77E.html">
  [問題] IT216飛羽田的疑問
 </a>
</div>

<div class="title">
 <a href="/bbs/Japan_Travel/M.1709725167.A.3DC.html">
  [問題] 小豆島高松-土庄3/10-24航班時間變更
 </a>
</div>

<div class="title">
 <a href="/bbs/Japan_Travel/M.1709725566.A.401.html">
  [住宿] 佐賀太良町・有明海旁美食海鮮滿滿平浜荘
 </a>
</div>

<div class="title">
 <a href="/bbs/Japan_Travel/M.1709725647.A.9A6.html">
  [資訊] 台灣虎航 tigerclub會員限定優惠活動
 </a>
</div>

<div class="title">
 (本文已被刪除) [gragragra]
</div>

<div class="title">
 <a href="/bbs/Japan_Travel/M.1694279303.A.1CE.html">
  [公告] 日本旅遊板板規 112.09.17
 </a>
</div>

<div class="title">
 <a href="/bbs/Japan_Travel/M.1709486877.A.7AF.html">
  [公告] 113.03 實況回報 (含天氣/物候/穿著詢問)
 </a>
</div>

再找出<a>裡的 text

1: for i in doc.find_all(class_='r-ent'):
2:     t = i.find(class_='title').a.text
3:     print(t)
[問題] 京都住宿請益
[問題] 沖繩親子遊首衝安排請益
Re: [問題] 請問最近札幌帝王蟹價格?
[問題] IT216 飛羽田的疑問
[問題] 小豆島高松-土庄 3/10-24 航班時間變更
[住宿] 佐賀太良町・有明海旁美食海鮮滿滿平浜荘
[資訊] 台灣虎航 tigerclub 會員限定優惠活動
Traceback (most recent call last):
  File "<string>", line 17, in __PYTHON_EL_eval
  File "<string>", line 3, in <module>
AttributeError: 'NoneType' object has no attribute 'text'

因為有些主題被刪除,找不到<a>,此時可以善用 try…except…來避免錯誤

5.4. 第四版:擷取所有主題

 1: import urllib.request as req
 2: url = "https://www.ptt.cc/bbs/Japan_Travel/index.html"
 3: # 幫request加上一個header
 4: request = req.Request(url, headers = {
 5:     "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:72.0) Gecko/20100101 Firefox/72.0"
 6: })
 7: 
 8: with req.urlopen(request) as response:
 9:     data = response.read().decode("utf-8")
10: 
11: import bs4
12: doc = bs4.BeautifulSoup(data,"html.parser")
13: 
14: # 直接找到所有class為title的div
15: titles = doc.find_all("div", class_="title")
16: no = 1
17: for i in titles:
18:     try:
19:         t = i.a
20:         print(f'第{no}則貼文::{t.text}')
21:         no += 1
22:     except:
23:         pass
第1則貼文::Fw: [食記] 喜一 福島喜多方 鮮味軟嫩叉燒喜多方拉麵
第2則貼文::[資訊] JCB日本消費滿10萬回饋1萬
第3則貼文::[遊記] 三保松原、富士山五湖、昇仙峽(前篇)
第4則貼文::[資訊] JR四國松山-宇和島指定日期部分路段停駛
第5則貼文::Re: [新聞] 獨/基隆-石垣島渡輪「延至10月開航
第6則貼文::[遊記] 宮崎五天四夜(非自駕獨旅)-2
第7則貼文::Re: [問題] 請教熊本熊港八代交通
第8則貼文::[資訊] 20251219匯率
第9則貼文::[問題] skyliner 成田往日暮里有需要提前買票嗎
第10則貼文::[問題] 御朱印帳的尺寸
第11則貼文::[資訊] 台灣虎航 26夏增班開賣
第12則貼文::[資訊] 長榮航空台北=青森航線增班
第13則貼文::[遊記] SL磐越物語與只見線攝影紀行
第14則貼文::[公告] 日本旅遊板板規 114.02.02
第15則貼文::Fw: [公告] 請避免與登入1次之帳號進行交易
第16則貼文::Fw: [公告] 違反菸害防制法本站將依使用者條款處分
第17則貼文::[公告] 114.12 實況回報 (含天氣/物候/穿著詢問)

5.5. [課堂練習]爬蟲實作   TNFSH

Google 美食達人「壽司羊」,以爬蟲程式列出其網站最近五篇文章的標題以及文章發表日期。

2024.03.10 / 台北美食【SEASON Artisan Pâtissier 敦南旗艦店】超好吃的法式草莓千層酥/精緻歐式早午餐 黃教練/大安區甜點店/下午茶/SEASON Cuisine Pâtissiartism((附菜單
2024.03.07 / 高雄美食【津筵燒肉酒館】一個人的深夜,也可以好好享受的平價燒肉/好吃又不貴的日式燒肉/美麗島捷運站/高 CP 值((附菜單
2024.03.06 / 高雄美食【光井咖啡 Wellight Coffee】岡山國小後面/簡約風的咖啡外帶小店/肯亞 Tegu 處理廠 AA 水洗((附菜單
2024.03.03 / 台北美食【NOBUO】絕美的羊里肌 羊五花 小羊胸腺/一點都不遜於肉類主餐的鹽漬風乾鰆魚/溫暖又好喝的香菇茶湯((附菜單
2024.03.02 / 台北食記【ä couple coffee】小巷弄裏頭的三角窗咖啡小店/楓糖核桃司康/限量的肉桂捲/美式咖啡/聽說商業午餐不錯吃((附菜單

Footnotes:

Author: Yung-Chin Yen

Created: 2026-02-13 Fri 15:33