Crawler: 網路資料解析與爬蟲

Table of Contents

Hits

1. JSON

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

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

1.2. JSON

1.2.1. What is 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:           "englihs": 92
11:         },
12:         { "title": "期末考",
13:           "chinese": 81,
14:           "math": 92,
15:           "englihs": 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 = 'https://quality.data.gov.tw/dq_download_json.php?nid=11339&md5_url=f2fdbc21603c55b11aead08c84184b8f'
 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))
/Users/letranger/Library/Python/3.9/lib/python/site-packages/urllib3/__init__.py:34: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
  warnings.warn(
<Response [200]>
<class 'list'>
[{'日期': '20201005', '美元/新台幣': '29.02', '人民幣/新台幣': '4.29882', '歐元/美元': '1.17405', '美元/日幣': '105.645', '英鎊/美元': '1.2947', '澳幣/美元': '0.71775', '美元/港幣': '7.75005', '美元/人民幣': '6.7507', '美元/南非幣': '16.4019', '紐幣/美元': '0.66435'}]
[
    {
        "日期": "20201005",
        "美元/新台幣": "29.02",
        "人民幣/新台幣": "4.29882",
        "歐元/美元": "1.17405",
        "美元/日幣": "105.645",
        "英鎊/美元": "1.2947",
        "澳幣/美元": "0.71775",
        "美元/港幣": "7.75005",
        "美元/人民幣": "6.7507",
        "美元/南非幣": "16.4019",
        "紐幣/美元": "0.66435"
    },
    {
        "日期": "20201006",
        "美元/新台幣": "28.96",
        "人民幣/新台幣": "4.300705",
        "歐元/美元": "1.17735",
        "美元/日幣": "105.555",
        "英鎊/美元": "1.297",
        "澳幣/美元": "0.7156",
        "美元/港幣": "7.75005",
        "美元/人民幣": "6.7338",
        "美元/南非幣": "16.63735",
        "紐幣/美元": "0.66365"
    }
]
1.3.1.2. 輸出日期與匯率
1: print('日期', "\t\t", '美元/新台幣')
2: print('====', "\t\t", '===========')
3: for item in jsonRes[:10]:
4:     print(item['日期'], "\t", item['美元/新台幣'])
日期 		 美元/新台幣
==== 		 ===========
20201005 	 29.02
20201006 	 28.96
20201007 	 28.965
20201008 	 28.966
20201012 	 28.909
20201013 	 28.92
20201014 	 28.952
20201015 	 28.96
20201016 	 28.979
20201019 	 28.95
1.3.1.3. 製圖
 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: exchange_rates = [float(item['美元/新台幣']) for item in jsonRes]
12: 
13: # 繪製折線圖
14: plt.figure(figsize=(10, 6))
15: plt.plot(dates, exchange_rates, marker='o', linestyle='-')
16: 
17: # 設置圖形標籤和標題
18: plt.xlabel('日期')
19: plt.ylabel('美元/新台幣')
20: plt.title('美元/新台幣匯率走勢')
21: 
22: # 旋轉x軸上的日期標籤,以避免重疊
23: plt.xticks(rotation=45, ha='right')
24: 
25: # 顯示圖形
26: plt.tight_layout()
27: plt.savefig('images/json-plot.png', dpi=300)

json-plot.png

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

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: 台南市各區熱點分佈

2. HTML

2.1. What is 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 code

 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 class="mark"></div>
			</div>
		</div>





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

				<a href="/bbs/movie/M.1709626126.A.AAB.html">[  好雷] 時空永恆的愛戀</a>

			</div>
			<div class="meta">
				<div class="author">tilwemeet</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&#43;&#43;%E5%A5%BD%E9%9B%B7%5D&#43;%E6%99%82%E7%A9%BA%E6%B0%B8%E6%81%86%E7%9A%84%E6%84%9B%E6%88%80">搜尋同標題文章</a></div>

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

					</div>

				</div>
				<div class="date"> 3/05</div>
				<div class="mark"></div>
			</div>
		</div

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

3.2. BeautifulSoup

3.2.1. BeautifulSoup4 簡介

  • BeautifulSoup4 和 lxml 一樣,Beautiful Soup 也是一個 HTML/XML 的解析器,
  • 主要的功能是解析和提取 HTML/XML 資料。

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

3.2.2.1. 安裝解析 HTML 所需套件(安裝套件名稱後面有 4)
1: pip install beautifulsoup4
3.2.2.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/index7965.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">
      <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>
     </di

3.3. Beautifulsoup 解析器

現在我們可以進一步來研究 bs4 的玩法

  • 在上面的語句中,我們使用了一個 html.parser。 這是一個解析器,在構造 BeautifulSoup 物件的時候,需要用到解析器。BeautifulSoup 支援 python 內建的解析器和少數第三方解析器。6:

    parsers.jpg

    Figure 5: Parsers 比較

  • 一般來說,對於速度或效能要求不太高的話,也可以使用 html5lib 來進行解析;如果較在乎效能則建議使用 lxml 來進行解析。

如果要換用 html5lib,則要先安裝:

1: pip install html5lib

接下來更換 parser:
批踢踢實業坊›看板 PC_Shopping

 1: import urllib.request as req
 2: url = "https://www.ptt.cc/bbs/PC_Shopping/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: root = bs4.BeautifulSoup(data,"html5lib")
13: 
14: # 依序執行丁列程式
15: print(type(root)) #取得的資料型態變成是bs4的物件,而非字串
16: print(type(root.prettify())) #可以先美化輸出,方便查看HTML結構
17: print(root.prettify()[3000:])

請自行執行、比對解析後的結果

3.4. BeautifulSoup 的方法7

方法 說明
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()  

4. BS4:開始爬

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

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

4.1. bs4.tag

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

bs4.tag

4.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 文章列表 - 批踢踢實業坊

4.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>。

4.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’…等屬性8。我們可以透過以下的方式取得 tag 中的相關內容

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

4.2. find

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

4.2.1. 直接接標籤名稱

4.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>

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

4.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>
4.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>
4.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>
4.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)
    
    [問題] 京都住宿請益
    

4.3. find_all

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

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…來避免錯誤

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

 1: import urllib.request as req
 2: url = "https://www.ptt.cc/bbs/Food/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: for i in titles:
17:     try:
18:         t = i.a
19:         print(t.text)
20:     except:
21:         pass
[食記] 台北車站 倪記鍋貼 歷史悠久的平價美食
[食記] 四川蜀渝小吃 台北大安 麻辣下飯麻婆豆腐
[轉讓] 饗 A Joy 3/30 六位
[食記] 桃園中壢區。豐園祖傳客家大湯圓
[食記] 台中 網美餐廳近中國醫分店,KOI PLUS
[食記] 台中 打盹 Nap & Knife - 義大利麵專賣
[食記] 台中大墩燒肉中山 最難預訂精緻和牛燒肉
[食記] 新北市新店區-新馬辣經典麻辣鍋
[食記] 基隆 義美石頭火鍋 二訪
[食記] 釜山 No Brand Burger
[食記] 台北大安 韓庭州韓國料理 公館老字號
[食記] 新北新店 山東水餃王 多種小菜與麵食
[食記] 台北福華大飯店-彩虹座-台北老字號buffet
[廣宣] 桃園 高級日本A5和牛有專人代烤-純水燒肉
[食記]台南美食|米亞漢堡|蔬菜量豐富早餐店
[食記] 台北天母 家傳美味小館- 好吃的麻醬麵
[食記] 台中中區—久流麵攤|全臺最狂什錦麵 大胃王版海陸齊聚銅板
[公告] Food板 板規 V3.91
[公告] 發文請在標題加上地區及提供地址電話。^^
[公告] 文章被刪除者請洽精華區的資源回收桶
[公告] 新增板規22:發文禁附延伸閱讀連結

4.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: 2024-12-06 Fri 08:08