---
# System prepended metadata

title: 用爬蟲幫我蒐集埔里餐廳評價！
tags: [web crawler]

---

# 用爬蟲幫我蒐集餐廳評價！
[TOC]

> 這篇文章希望讓沒有網頁基礎的讀者也能看懂，所以會介紹網頁的基礎原理、基礎 HTML、爬蟲工具介紹。以下是文章架構:
> 1. 爬蟲能做什麼及原理
> 2. 網頁怎麼形成
> 3. 怎麼找網頁上資源的網址 
> 4. 看懂 HTML
> 5. 介紹爬蟲工具
## 爬蟲是什麼？
我們平常會透過瀏覽器連上網站來查找資訊，比如購物網站的商品、徵才網站上的職缺、即時股價…，當我們需要定時或是大量蒐集這些資訊時，每次要都手動操作是非常費時費力的，如果能交給程式來做是不是就輕鬆多了？這個能幫我們蒐集網站資料的程式，就叫做爬蟲。
### 爬蟲能做什麼？
簡單來說爬蟲就是可以用程式去模擬真人瀏覽網站的行為，比如取得網站資料或送出資料給網站

| 真人 | 爬蟲 |
| -------- | -------- |
| 瀏覽 Dcard 上的貼文 | 取得 Dcard 上的貼文|
| 登入 Facebook | 送出帳密給 Facebook

> 想要透過爬蟲程式幫我們瀏覽網站，就要先了解我們實際瀏覽網站的流程

[影片連結](https://www.youtube.com/watch?v=BdRjutf8K0c&ab_channel=%E5%AD%B8%E4%BB%81%E5%A4%A7%E5%A4%A7)

## 爬蟲的原理
### 瀏覽網頁的流程
* 人：打開瀏覽器 → 搜尋網站 → 瀏覽器向目標網站發出請求 (request) → 伺服器回應 (response) 網頁 (HTML) 給瀏覽器 → 瀏覽器將網頁顯示在畫面上
![](https://hackmd.io/_uploads/rJuFw85ZT.png)
[資料來源](https://medium.com/%E8%AA%A4%E9%97%96%E6%95%B8%E6%93%9A%E5%8F%A2%E6%9E%97%E7%9A%84%E5%95%86%E7%AE%A1%E4%BA%BAzino/%E5%88%9D%E5%AD%B8%E8%80%85%E5%BF%85%E7%9C%8B-%E4%B8%80%E5%80%8B%E8%A7%80%E5%BF%B5-%E9%96%8B%E5%95%9Fpython-%E7%B6%B2%E8%B7%AF%E7%88%AC%E8%9F%B2%E6%88%90%E9%95%B7%E4%B9%8B%E8%B7%AF-%E5%90%AB%E8%A7%A3%E8%AA%AA%E5%BD%B1%E7%89%87-fac0a17cd261)
* 爬蟲：利用程式向目標網站發出請求 (request) → 伺服器回應 (response) 網頁 (HTML) 給程式，拿到網頁後我們就可以拿來運用
![](https://hackmd.io/_uploads/SkRIIkneT.png)
[資料來源](https://medium.com/%E8%AA%A4%E9%97%96%E6%95%B8%E6%93%9A%E5%8F%A2%E6%9E%97%E7%9A%84%E5%95%86%E7%AE%A1%E4%BA%BAzino/%E5%88%9D%E5%AD%B8%E8%80%85%E5%BF%85%E7%9C%8B-%E4%B8%80%E5%80%8B%E8%A7%80%E5%BF%B5-%E9%96%8B%E5%95%9Fpython-%E7%B6%B2%E8%B7%AF%E7%88%AC%E8%9F%B2%E6%88%90%E9%95%B7%E4%B9%8B%E8%B7%AF-%E5%90%AB%E8%A7%A3%E8%AA%AA%E5%BD%B1%E7%89%87-fac0a17cd261)
* 簡單來說，我們瀏覽網頁時，瀏覽器會去跟網頁伺服器要求網頁的 HTML，拿到後再顯示在網頁上，就會變成我們看到的網頁
* 而爬蟲程式就是模擬這樣的步驟。程式在執行時會取得網頁的 HTML，取得後就可以拿來利用，比如分析網頁上的留言等
> 已經知道爬蟲會拿到 HTML，那 HTML 是什麼？
### HTML 是什麼？
* 全名 : HyperText Markup Language - 超文本標記語言
* 就是我們看到的網頁
    * HTML 就像是一個 Word 檔案，可以在裡面寫入文字、排版、放圖片等，再用瀏覽器打開就會變成我們看到的網頁
    ![](https://hackmd.io/_uploads/SkrHtU5-p.png)
* 也就是說，HTML 包含了網頁上的內容
    * 所以要用爬蟲蒐集網頁上的資料，做法就是拿到網頁的 HTML，再從裡面找出資料
> 已經知道瀏覽網頁的流程了，接下來就要先了解怎麼取得網頁上的資料，進而用程式去取得資料
### 網頁通常是由 HTML 和各種資源組成
* 網頁不一定只是一個 HTML，通常是由 HTML 加上如 CSS (網頁美化)、資料、圖片...等**資源**共同組成
    * 上述的資源都有各自對應的網址。只要對這些網址發送請求，就能取得想要的資源
    ![](https://hackmd.io/_uploads/SyvNXl2g6.png)
[圖片連結](https://developers.google.com/community/gdsc/images/gdsc-social-share.png)
    * 瀏覽器連上網站後會向伺服器請求這些資源，取得回應的資源後再把這些資源放到 HTML 裡，最後顯示在畫面上
    ![](https://hackmd.io/_uploads/ryj3_xngp.png)
* 所以我們瀏覽網頁時所看到的 HTML，是已經將這些資源放進去後產生的完整 HTML
### 怎麼取得資源的網址？
* 前面已經知道資源都有各自對應的網址，只要對這些網址發送請求，就能從回應中取得資源
* Q: 那要怎麼知道資源對應的網址？
* A: 透過瀏覽器的 Network 介面找出我們要的資源
    * 按`F12`或右鍵 -> 檢查
    ![](https://hackmd.io/_uploads/rJ-g2GBWa.png)
    [資料來源](https://www.twse.com.tw/zh/trading/historical/stock-day-avg.html)
    * 開啟後右上方選 Network (之後要先重新整理畫面一次)
    * Network 會監測瀏覽器發出的請求 (request) 與收到的回應 (response)
    * 我們現在看到的畫面也是收到的回應資料之一 (是一個 HTML)
    ![](https://hackmd.io/_uploads/SyQ0iR2Tn.png)
    [資料來源](https://www.twse.com.tw/zh/trading/historical/stock-day-avg.html)
    * 可以根據資源類型篩選，會更容易找到
    * ![](https://hackmd.io/_uploads/rkbgrzpa3.png)
        * All: 全部類型
        * Fetch/XHR: 網頁發送請求取得的資源 (通常資料會在這裡)
        * Js: JavaScript，網頁邏輯程式碼
        * CSS: 網頁美觀程式碼
        * Img: 圖片檔案
        * Doc: HTML檔案
    * 以證交所網站為例，篩選資源後可以一個一個找，從 Preview 或 Response 中查看回應資料的內容，就能找到想要的資料
    ![](https://hackmd.io/_uploads/SJyPLf6T2.png)
    * 找到想要的資料後，從 Headers 中的 Request URL 就能知道資源對應的網址 (URL)
    ![](https://hackmd.io/_uploads/Bk9oPfppn.png)
    * 所以我們只要對這個網址發送請求，就能取得想要的資源，也就是台股收盤價
    ::: info
    Request Method
    > 發送請求的方法，也就是你想對這個資源做什麼樣的操作
    
    * 常見的 Request Method:
        * GET: 取得資料。
        * POST: 送出資料，比如登入時送出帳號資訊
        * PUT: 修改資料
        * DELETE: 刪除資料
    * 如果在 Headers 看到 Request Method 是 GET，那在用爬蟲程式發送 Request (請求) 時，Request Method 就要設定為 GET
        * 以這張圖為例，我們取得這個資源的方法 (method) 是 GET，所以程式的 Request Method 就要設成 GET
        ![](https://hackmd.io/_uploads/Bk9oPfppn.png)
    
    [資料來源](https://ithelp.ithome.com.tw/articles/10299957)
    :::
## 爬蟲時可能會發生的狀況
**Q1: 明明我看網站的 HTML 上有我想要的資料，但用爬蟲爬下來後卻沒看到**
* A: 可能是因為這個網站是**動態**網站，而不是**靜態**網站
> 那什麼是靜態網站，什麼是動態網站？
### 靜態網站 vs 動態網站
* **靜態網站**
    ![](https://hackmd.io/_uploads/ByF4mwUW6.png)
    [資料來源](https://steam.oxxostudio.tw/category/python/spider/about-spider.html)
    * 特點：
        * 伺服器將資料寫在回應給瀏覽器的 HTML 上
        * 只要對這個網站的網址發送請求，就可以得到想要的資料
        * 但因為資料被放在 HTML，所以要自己從 HTML 整理出想要的資料
        * 就像是一本講義，這本講義可以輕鬆買到，但要自己整理重點
    * 例子：https://histock.tw/stock/rank.aspx
        * 打這個網頁時，股票資訊就已經寫在網頁裡了
        * 對這個網頁發送請求，拿到 HTML 後再從 HTML 裡找出想要的資料
* **動態網站**
    ![](https://hackmd.io/_uploads/rJy0BPU-T.png)
    [資料來源](https://steam.oxxostudio.tw/category/python/spider/about-spider.html)
    * 特點：
        * 資料沒有寫在回應給瀏覽器的 HTML 上，而是瀏覽器另外向伺服器請求該資料
        * 直接對這個網站的網址發送請求不一定能取得想要的資料
        * 要對資料的網址發送請求 -> 在 Network 介面中尋找資料的網址並發送請求
        * 因為是直接取得該資料，所以整理起來很方便
        * 就像跟別人要整理好的筆記，需要找一下筆記放在哪裡，但拿到筆記後重點別人都已經整理好了
    * 例子：https://www.twse.com.tw/zh/trading/historical/stock-day-avg.html
        * 在網頁中輸入想要查詢的股票和年、月並按查詢後，才會去向伺服器請求對應的資料，取得回應後再顯示在網頁上
        ![](https://hackmd.io/_uploads/SyMJzmB-6.png)
        * 只要對股價資料的網址發送請求，就能拿到股價的資料

**Q2: 怎麼分辨是靜態網站還是動態網站**
* A: 兩個方法
    1.  在 Network 介面看回應的 HTML 裡有沒有想要的資料，若沒有 -> 可能是動態網站
    2. 用爬蟲爬網站的 HTML 下來看有沒有像要的資料，若沒有 -> 可能是動態網站

**Q3: 爬蟲程式連不上網站**
> 有些網站會擋爬蟲，可能可以透過偽裝成瀏覽器解決
* A: 修改 Request Header 的內容，就能將爬蟲程式偽裝成瀏覽器
    * 網頁伺服器可以從 Request Header 中的 User-Agent 了解使用者使用的瀏覽器
    ![](https://hackmd.io/_uploads/SyPzN0LW6.png)
    * 也就是說，爬蟲程式在發送請求時，我們只要在 Header 的 User-Agent 中放入瀏覽器的資訊，網頁伺服器就會認為這個請求是由瀏覽器發送，而不是爬蟲程式
### 爬蟲注意事項
* 有禮貌
    若是對同一個網頁持續爬蟲，盡量每發一次請求就停頓幾秒，可以減低被網站封鎖的機率。因為對網頁擁有者來說，過多的流量消耗會導致伺服器吃不消而掛掉，所以短時間大量發送請求會被認為是在攻擊網站，而被網站封鎖
* 爬蟲穩定度較低
    * 如果是長期持續的爬蟲，可能會遇到一些例外的情況：
        1. 發送請求時連線失敗
            * 可以利用程式的 try catch 機制確保請求有發送成功
            * 每次發送請求之後都停頓幾秒，讓伺服器有喘息時間
        2. 網頁內容如果有更改，爬蟲程式會出現錯誤
            * 可能要持續記錄資料蒐集的情況，才能及時去更改程式碼
            * 或是有些網站有提供 API 的方式，直接向 API 的網址發送請求就可以拿到資料，而不用解析 HTML 的內容，就不會因網頁內容更改而出現錯誤
        3. 持續向某一網站爬蟲時，伺服器可能因效能問題，會回傳錯誤的資料
            * 可以利用程式的 try catch 機制確保回應的資料是正確的
            * 每次發送請求之後都停頓幾秒，讓伺服器有喘息時間
## HTML 介紹
> 爬到 HTML 之後，要看得懂 HTML 才有辦法整理出我們想要的資料
### 標籤 & 元素
* 透過 **標籤<>** (tag) 來描述文字、超連結、圖片等 **元素** 的種類和顯示方式
    * 簡單來說，標籤就是告訴 HTML 我要寫入哪種資料，而寫入的資料就是元素
    * ex : `<h1>Hello</h1>` 在網頁上會是大標題格式的文字
    > `<h1>` 是標籤，Hello 是元素
* 常見標籤：
    * `<h1>` 大標題文字
        * `<h2>` 比大標題小的文字
        * `<h3>...` 依此類推，到`<h6>`
    * `<p>` 段落文字
    * `<a>` 超連結文字
    * `<img>` 圖片
    * `<button>` 按鈕
    * `<form>` 表單，ex : 登入表單
    * `<input>` 輸入欄位
    * `<table>` 表格。通常會搭配`<th>`表頭、`<tr>`表格的每一列、`<td>`表格的每一欄
    * `<div>` 區塊，可以包含多個標籤，通常是方便調整整個區塊內標籤的排版或美觀時使用
* HTML 範例：
    ```html=1
    <!DOCTYPE html>
    <html>
        <head>
            <title>範例網頁</title>
        </head>
        <body>
            <h1>Hello</h1>
            <p>哈囉！</p>
        </body>
    </html>
    ```
    ![](https://hackmd.io/_uploads/SkKEngBC2.png)
* HTML 通常會搭配 **CSS** 和 **JavaScript** 一起使用
    * **CSS** : 用於調整網頁排版、美觀
    * **JavaScript** : 網頁上的程式碼。可以實現互動功能，比如設定按了某按鈕會跳出警告訊息
* Q: tag 跟爬蟲有什麼關係？
    * A: 爬蟲程式取得 HTML 後，可以透過 tag 找到想要的資料
### CSS & JavaScript 以及 class & id
> 可以用 class 和 id 來快速找到資料的位置
* CSS 和 JavaScript 通常會透過 **tag (標籤)**、**class (類別)**、**id (識別號)** 來控制元素
    * **class** 類別 : 可以將多個元素設為同個 class，並對這個 class 的元素統一進行操作
        * 就像多個學生可以是相同學系，多個元素也可以有相同的 class
    * **id** 識別號 : 可以為一個元素設定該元素獨有的 id，可以透過 id 直接找到這個元素
        * 就像學生有自己唯一的學號，元素也可以有唯一的 id
* 範例：
    ```html=1
    <!DOCTYPE html>
    <html>
        <head>
            <title>範例網頁</title>
            <style>
                .class2 {
                    color: red;
                }
            </style>
        </head>
        <body>
            <h1 id="text1" class="text2">Hello</h1>
            <p class="text2">哈囉！</p>
        </body>
        <script>
            document.getElementById('text1').innerHTML = 'Hello World!';
        </script>
    </html>
    ```
    * 取得 id 為 text1 的元素會找到 `<h1 id="text1" class="text2">Hello</h1>` 這個元素
    * 取得 class 為 text2 的元素會找到 `<h1 id="text1" class="text2">Hello</h1>` 和 `<p class="text2">哈囉！</p>` 這兩個元素
* Q: class 和 id 跟爬蟲有什麼關係？
    * A: 爬蟲程式取得 HTML 後，可以透過 tag 的 class 和 id 從 HTML 中快速找到想要的資料
## 爬蟲工具介紹 (以 Python 為範例)
以下是爬蟲經常會使用到的工具：
* **Requests 請求工具**
    * 用程式發送請求，取得回應的資源
* **BeautifulSoup 解析工具**
    * 如果 Requests 得到的回應資源是 HTML，可以用 BeautifulSoup 來解析(整理) HTML，找出想要的資料
* **Selenium 動態爬蟲**
    * 用程式模擬真人使用瀏覽器(預設會開啟瀏覽器)，在爬取動態網站較難找到資源的網址時適合使用
### Requests
> 程式可以透過 Requests 向伺服器發送請求並回傳回應
* 安裝指令 (在本機執行的話要安裝)
`pip install requests`
* 導入程式
`import requests`
* 發送請求
    * 發送 get 請求
        * 程式在發送請求後會回傳回應結果，可以用變數來存放 (這裡的r)
    `r = requests.get('網址')`
    * 發送 post 請求
        * 第一個位置是網址，第二個位置放要傳送的字典型態資料。ex : 傳送登入的帳號密碼
    `r = requests.post('網址', {'key':'value'})`
            >key 是名稱，value 是數值。ex：'name' : 'user1'
* 印出回應的文字內容
    * 如果回應是 html :
    `print(r.text)`
    * 如果回應是 json :
    `print(r.json())`
* 印出回應的編碼
`print(r.encoding)`
* 印出 HTTP 狀態碼
`print(r.status_code)`
:::info
**HTTP 狀態碼**
* 一個三位數，表示一個 HTTP 請求是否已完成
* 分為五種類：
    * 資訊回應 (Informational responses, 100–199)
        * 表示請求已被接收，伺服器正在等待進一步操作
    * 成功回應 (Successful responses, 200–299)
        * 表示請求已成功接收
    * 重定向 (Redirects, 300–399)
        * 表示客戶端需要採取進一步的操作才能完成請求
    * 客戶端錯誤 (Client errors, 400–499)
        * 表示客戶端出現了錯誤或無法完成請求
    * 伺服器端錯誤 (Server errors, 500–599)
        * 表示伺服器在處理請求時出現錯誤
* 常見狀態碼：
    * 200 OK : 請求成功，很棒
    * 403 Forbidden : 客戶端沒有訪問權限
    * 404 Not Found : 伺服器找不到對應的資源，比如使用者把網址打成不存在的網址
    * 500 Internal Server Error : 伺服器發生錯誤

[資料來源](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Status)

:::
* 如果今天爬的是一個 HTML，即使用 `print(r.text)` 印出網頁內容文字，還是會有 HTML 的 tag，並不是乾淨的資料
* 若要取出乾淨的資料，則要透過解析工具，也就是接下來要介紹的 **BeautifulSoup**
### BeautifulSoup
> 我們透過 Requests 取得網頁 HTML 之後，通常會搭配 BeautifulSoup 來解析 HTML，找出想要的資料
* 安裝指令 (在本機執行的話要安裝)
`pip install BeautifulSoup4`
* 導入程式
`from bs4 import BeautifulSoup`
* 使用解析器解析 HTML
    * 將前面 Requests 回傳的 HTML，利用 `html.parser` 這個解析器解析，並把結果存到變數 `soup` 裡
    ```python=1
    r = requests.get('網址')
    soup = BeautifulSoup(r.text, 'html.parser')
    ```
    ::: info
    BeautifulSoup 所有解析器
    * 除了前面提到的 `html.parser` 這個解析器之外，還有以下這些解析器：
    ![](https://hackmd.io/_uploads/B1S-kyI02.png)
    * 除了 `html.parser` 是內建之外，其他解析器要透過指令安裝
        * `lxml` : `pip install lxml`
        * `html5lib` : `pip install html5lib`

    [資料來源](https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/)
    :::
* 印出排版過後的 HTML
`print(soup.prettify())`
* 解析 HTML 之後，就可以利用 find 和 select 兩種方法來找出想要的資料：
    * **find**
        * 以 **tag** 搜尋
            * 找出第一個符合條件的標籤
                ```python=1
                first_a = soup.find('a')
                # 會回傳第一個 <a> 標籤
                ```
            * 找出所有符合條件的標籤
                ```python=1
                all_a = soup.find_all('a')
                # 會回傳包含所有 <a> 標籤的 list
                ```
            * 同時搜尋多種標籤
                ```python=1
                all_a_p = soup.find_all(['a', 'p'])
                # 將條件的標籤放在 list 裡
                # 只要是 <a> 或 <p> 標籤都會回傳
                # 如果是 find 只會回傳符合條件的第一個
                # find_all 則是回傳所有符合條件標籤的 list
                ```
            * 限制 `find_all` 搜尋數量
                * `find_all` 預設會搜尋所有符合條件的標籤。若 HTML 文件較大或符合條件的標籤數量較多，而想要的資料在比較前面的位置時，可以限制搜尋的數量，當搜尋到設定的數量就不會再繼續搜尋，可以減少執行的時間
                ```python=1
                first_2_a = soup.find_all(['a'], limit=2)
                # 會回傳前兩個 <a> 標籤的 list
                ```
        * 以 **id** 搜尋
            * 找出所有 `id` 為 `test` 的標籤
                ```python=1
                tags = soup.find_all(id='test')
                ```
                > FIXME : id 不能重複為甚麼還有 find_all ?
            * 可以同時使用 tag 和 id 搜尋
                ```python=1
                tag = soup.find_all('h2', id='test')
                ```
        * 以 **class** 搜尋
            * 找出所有 `class` 為 `test` 的標籤
            > 是 class_ 不是 class
            ```python=1
            tags = soup.find_all(class_='test')
            ```
        * 同時搜尋 tag、id、class
            * 找出所有 `tag` 為 `<p>`、`id` 為 `test_id`、`'class` 為 `test_class`
            ```python=1
            soup.find('p', id='test_id', class_='test_class')
            ```
        * 搜尋上層標籤
            * 如果今天無法定位的想要的標籤，但可以該標籤內的子標籤，可以用 `find_parent` 和 `find_parents` 來搜尋子標籤的父標籤，就能定位到我們想要的標籤
            * `find_parent`、`find_parents` 就像 `find`、`find_all`的概念，一個是找第一個，另一個是找全部
            * 如果 HTML 長這樣，我們想要取得這個 `<p>` 標籤但無法定位，就可以搜尋 `<b>` 的父節點來找到 `<p>`：
            ```html=1
            <p>這是一份<b class="boldtext">HTML文件</b>。</p>
            ```
            ```python=1
            tag_b = soup.find('b')
            parent_p = tag_a.find_parent('p')
            print(parent_p)
            ```
        * 用前面的方法得到的會是完整的標籤，如果要取出文字，就是用 `getText()`
            ```python=1
            all_p = soup.find_all('p')
            for p in all_p:
            print(p.getText())
            ```
    * **select**
### Selenium
* 安裝 `selenium` 和相關套件
    * colab
        * 安裝 selenium
        ```=1
        !apt-get update
        !pip install selenium
        ```
        * 安裝 webdriver (要讓 selenium 使用哪種瀏覽器就安裝哪種)
        ```=1
        !apt install chromium-chromedriver // chromium
        !apt install firefox-geckodriver // firefox
        ```
    * 本機
    ```=1
    pip install selenium
    
    ```

* [selenium 4.10.0 之後要用 Service](https://stackoverflow.com/questions/76428561/typeerror-webdriver-init-got-multiple-values-for-argument-options)
* https://googlechromelabs.github.io/chrome-for-testing/#stable
## 爬蟲程式設計流程分享
## 實際爬網站
### 其他
* moodle 登入
* dacard 留言
* 大樂透
## reference
* [檢視網頁](https://ithelp.ithome.com.tw/m/articles/10294726)