# 用爬蟲幫我蒐集餐廳評價!
[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://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://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 檔案,可以在裡面寫入文字、排版、放圖片等,再用瀏覽器打開就會變成我們看到的網頁

* 也就是說,HTML 包含了網頁上的內容
* 所以要用爬蟲蒐集網頁上的資料,做法就是拿到網頁的 HTML,再從裡面找出資料
> 已經知道瀏覽網頁的流程了,接下來就要先了解怎麼取得網頁上的資料,進而用程式去取得資料
### 網頁通常是由 HTML 和各種資源組成
* 網頁不一定只是一個 HTML,通常是由 HTML 加上如 CSS (網頁美化)、資料、圖片...等**資源**共同組成
* 上述的資源都有各自對應的網址。只要對這些網址發送請求,就能取得想要的資源

[圖片連結](https://developers.google.com/community/gdsc/images/gdsc-social-share.png)
* 瀏覽器連上網站後會向伺服器請求這些資源,取得回應的資源後再把這些資源放到 HTML 裡,最後顯示在畫面上

* 所以我們瀏覽網頁時所看到的 HTML,是已經將這些資源放進去後產生的完整 HTML
### 怎麼取得資源的網址?
* 前面已經知道資源都有各自對應的網址,只要對這些網址發送請求,就能從回應中取得資源
* Q: 那要怎麼知道資源對應的網址?
* A: 透過瀏覽器的 Network 介面找出我們要的資源
* 按`F12`或右鍵 -> 檢查

[資料來源](https://www.twse.com.tw/zh/trading/historical/stock-day-avg.html)
* 開啟後右上方選 Network (之後要先重新整理畫面一次)
* Network 會監測瀏覽器發出的請求 (request) 與收到的回應 (response)
* 我們現在看到的畫面也是收到的回應資料之一 (是一個 HTML)

[資料來源](https://www.twse.com.tw/zh/trading/historical/stock-day-avg.html)
* 可以根據資源類型篩選,會更容易找到
* 
* All: 全部類型
* Fetch/XHR: 網頁發送請求取得的資源 (通常資料會在這裡)
* Js: JavaScript,網頁邏輯程式碼
* CSS: 網頁美觀程式碼
* Img: 圖片檔案
* Doc: HTML檔案
* 以證交所網站為例,篩選資源後可以一個一個找,從 Preview 或 Response 中查看回應資料的內容,就能找到想要的資料

* 找到想要的資料後,從 Headers 中的 Request URL 就能知道資源對應的網址 (URL)

* 所以我們只要對這個網址發送請求,就能取得想要的資源,也就是台股收盤價
::: info
Request Method
> 發送請求的方法,也就是你想對這個資源做什麼樣的操作
* 常見的 Request Method:
* GET: 取得資料。
* POST: 送出資料,比如登入時送出帳號資訊
* PUT: 修改資料
* DELETE: 刪除資料
* 如果在 Headers 看到 Request Method 是 GET,那在用爬蟲程式發送 Request (請求) 時,Request Method 就要設定為 GET
* 以這張圖為例,我們取得這個資源的方法 (method) 是 GET,所以程式的 Request Method 就要設成 GET

[資料來源](https://ithelp.ithome.com.tw/articles/10299957)
:::
## 爬蟲時可能會發生的狀況
**Q1: 明明我看網站的 HTML 上有我想要的資料,但用爬蟲爬下來後卻沒看到**
* A: 可能是因為這個網站是**動態**網站,而不是**靜態**網站
> 那什麼是靜態網站,什麼是動態網站?
### 靜態網站 vs 動態網站
* **靜態網站**

[資料來源](https://steam.oxxostudio.tw/category/python/spider/about-spider.html)
* 特點:
* 伺服器將資料寫在回應給瀏覽器的 HTML 上
* 只要對這個網站的網址發送請求,就可以得到想要的資料
* 但因為資料被放在 HTML,所以要自己從 HTML 整理出想要的資料
* 就像是一本講義,這本講義可以輕鬆買到,但要自己整理重點
* 例子:https://histock.tw/stock/rank.aspx
* 打這個網頁時,股票資訊就已經寫在網頁裡了
* 對這個網頁發送請求,拿到 HTML 後再從 HTML 裡找出想要的資料
* **動態網站**

[資料來源](https://steam.oxxostudio.tw/category/python/spider/about-spider.html)
* 特點:
* 資料沒有寫在回應給瀏覽器的 HTML 上,而是瀏覽器另外向伺服器請求該資料
* 直接對這個網站的網址發送請求不一定能取得想要的資料
* 要對資料的網址發送請求 -> 在 Network 介面中尋找資料的網址並發送請求
* 因為是直接取得該資料,所以整理起來很方便
* 就像跟別人要整理好的筆記,需要找一下筆記放在哪裡,但拿到筆記後重點別人都已經整理好了
* 例子:https://www.twse.com.tw/zh/trading/historical/stock-day-avg.html
* 在網頁中輸入想要查詢的股票和年、月並按查詢後,才會去向伺服器請求對應的資料,取得回應後再顯示在網頁上

* 只要對股價資料的網址發送請求,就能拿到股價的資料
**Q2: 怎麼分辨是靜態網站還是動態網站**
* A: 兩個方法
1. 在 Network 介面看回應的 HTML 裡有沒有想要的資料,若沒有 -> 可能是動態網站
2. 用爬蟲爬網站的 HTML 下來看有沒有像要的資料,若沒有 -> 可能是動態網站
**Q3: 爬蟲程式連不上網站**
> 有些網站會擋爬蟲,可能可以透過偽裝成瀏覽器解決
* A: 修改 Request Header 的內容,就能將爬蟲程式偽裝成瀏覽器
* 網頁伺服器可以從 Request Header 中的 User-Agent 了解使用者使用的瀏覽器

* 也就是說,爬蟲程式在發送請求時,我們只要在 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>
```

* 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` 這個解析器之外,還有以下這些解析器:

* 除了 `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)