---
# System prepended metadata

title: 爬蟲概論
tags: [python, 爬蟲]

---

爬蟲概論
===
###### tags: `爬蟲` `python`

Tips for crawlers:
1. 有沒有現成的 code
2. 該網站是否已經提供 API 接口
* 不要造成別人伺服器的負擔!

News APIs: https://newsapi.org/s/bbc-news-api
## 基本分類
* 靜態爬蟲: 透過 get, post 獲取頁面資訊，依照不同網站架構，可能需要多次的 request
    * Text
    * Image
    * file
* 網站爬蟲: 將整個網站架構下的所有頁面進行巢狀爬取 (可以多利用框架如 Scrapy 來執行) [blog](http://woodenrobot.me/2017/04/09/Scrapy%E7%88%AC%E8%99%AB%E6%A1%86%E6%9E%B6%E6%95%99%E7%A8%8B%EF%BC%88%E5%9B%9B%EF%BC%89-%E6%8A%93%E5%8F%96AJAX%E5%BC%82%E6%AD%A5%E5%8A%A0%E8%BD%BD%E7%BD%91%E9%A1%B5/)
* 動態爬蟲: 是透過 javascript 即時生成資料，無法直接從網頁原始碼中獲得，需要透過模擬瀏覽器或自己找出網址、程式碼生成邏輯來處理。[Quick Javascript switcher](https://chrome.google.com/webstore/detail/quick-javascript-switcher/geddoclleiomckbhadiaipdggiiccfje)
    * 可解析 => 獲取資訊重複爬取
    * 不可解析 => 模擬瀏覽器 (但很多視覺化呈現之動態圖表，其資料也多由API傳輸，所以可以直接尋找該API)

## 了解網頁架構
![](https://i.imgur.com/1wwd9ZC.png)
* HTML (HyperText Markup Language)
![](https://i.imgur.com/zQWDqm7.png)
還是沒什麼感覺?
實際來看看[網站](https://www.ncdr.nat.gov.tw/)吧!

### HTTP 標籤
![](https://i.imgur.com/CEo2YYT.png)

例子:
`<p class="Hi!我是attributes">   Hi!我是contents  </p>`

|標籤名稱|用途|
| -------- | -------- | 
|`<h1> -<h6>`|標題|
|`<p>`|段落|
|`<a>`|超連結|
|`<table>`|表格|
|`<tr>`|表格內的row|
|`<td>`|表格內的cell|
|`<br/>`|換行(無結束標籤)|

|屬性名稱|意義|
| -------- | -------- | 
|**class**|標籤的類別(可重複)|
|**id**|標籤的id(不可重複)|
|title|標籤的顯示資訊|
|style|標籤的樣式|
|**href**|超連結|
|data-*|自行定義新的屬性|

### HTTP 協定 
**請求（request）& 回應（response）**

HTTP是一個用戶端終端（用戶）和伺服器端（網站）請求和應答的標準（TCP）。通過使用網頁瀏覽器、網路爬蟲或者其它的工具，用戶端發起一個HTTP請求到伺服器上指定埠（預設埠為80）。其中我們稱這個用戶端為用戶代理程式（user agent）。HTTP/2 為目前版本，於2015年5月作為網際網路標準正式發布。  --- from wiki
補充: [HTTP & HTTPs 間的差異](https://www.webdesigns.com.tw/HTTPorHTTPS.asp)
![](https://i.imgur.com/asi3Pq9.png)


* 請求方法 (request)

| 方法(動作)| 解釋 (modified from wiki) |
| -------- | -------- | 
| **GET**     | 向指定的資源發出「顯示」請求，只用在讀取資料 | 
|HEAD|與GET方法一樣，但伺服器將不傳回資源的本文部份。|
|**POST**|向指定資源提交資料，請求伺服器進行處理。|
|PUT|向指定資源位置上傳其最新內容。|
|DELETE|請求伺服器刪除Request-URI所標識的資源。|
|TRACE|回顯伺服器收到的請求，主要用於測試或診斷。|
|OPTIONS|使伺服器傳回該資源所支援的所有HTTP請求方法。|
|CONNECT|HTTP/1.1協定中預留給能夠將連線改為管道方式的代理伺服器。|

* 狀態碼
    -   [1xx訊息](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81#1xx%E6%B6%88%E6%81%AF "HTTP狀態碼")——請求已被伺服器接收，繼續處理
    -   [2xx成功](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81#2xx%E6%88%90%E5%8A%9F "HTTP狀態碼")——請求已成功被伺服器接收、理解、並接受  EX: 200 OK
    -   [3xx重新導向](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81#3xx%E9%87%8D%E5%AE%9A%E5%90%91 "HTTP狀態碼")——需要後續操作才能完成這一請求
    -   [4xx請求錯誤](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81#4xx%E8%AF%B7%E6%B1%82%E9%94%99%E8%AF%AF "HTTP狀態碼")——請求含有詞法錯誤或者無法被執行  EX: 404 Not Found
    -   [5xx伺服器錯誤](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81#5xx%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%94%99%E8%AF%AF "HTTP狀態碼")——伺服器在處理某個正確請求時發生錯誤

* Header
    -   通用頭欄位(英語：General Header Fields)
    -   請求頭欄位(英語：Request Header Fields)
    -   回應頭欄位(英語：Response Header Fields)
    -   實體頭欄位(英語：Entity Header Fields)

定義了一個HTTP協定事務中的操作參數，可分為四類。 [wiki](https://zh.wikipedia.org/wiki/HTTP%E5%A4%B4%E5%AD%97%E6%AE%B5)



讓我們把網頁[原始碼變漂亮](https://www.cleancss.com/html-beautify/)吧!

### 所以到底什麼是 "請求(request)" & "回應(response)" 呢?
下載 [**Postman**](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=zh-TW) Google Chrome plugin 來實際體驗看看吧!

in Python EX: [高鐵](https://www.thsrc.com.tw/tw/TimeTable/SearchResult)

**Form Data** 是用 post 時，需要傳給 server 的資料!
```
form_data = {
"StartStation":"2f940836-cedc-41ef-8e28-c2336ac8fe68",
"EndStation":"e6e26e66-7dc1-458f-b2f3-71ce65fdc95f",
"SearchDate":"2017/08/13",
"SearchTime":"20:30",
"SearchWay":"DepartureInMandarin"}

response_post = requests.post("https://www.thsrc.com.tw/tw/TimeTable/SearchResult", data = form_data)
```

## Python 爬蟲 GO!

教學重點:
1. request
2. 清理、篩選: BeautifulSoup, regular expression, XPath etc.
3. 重組、輸出: Pandas

### BeautifulSoup
*參考文獻：[https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/](https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/)*
How?
```
from bs4 import BeautifulSoup
soup = BeautifulSoup(page,'lxml')
```
![](https://i.imgur.com/cqoMN6s.png)

BeautifulSoup 中分為四大類:
-   **Tag**
    -   name
    -   attrs
```
print('印出第一個a Tag\n',soup.a)
print('印出該 Tag 下的 attrs\n',soup.a.attrs)
soup.a.get('id')
```
印出第一個a Tag
 `<a href="/" id="logo">批踢踢實業坊</a>`
印出該 Tag 下的 attrs
 `{'id': 'logo', 'href': '/'}`
 印出 Tag 下特定屬性
 `logo`


-   **NavigableString**
    -   .string
    -   .text
```
print('取出標籤中的文字:\n',soup.a.string)
print('取出標籤中的文字:\n',soup.a.text)
```
取出標籤中的文字:
 `批踢踢實業坊`

-   **BeautifulSoup**
BeautifulSoup對象表示的是一個文檔的全部內容，大部分時候可以把它當作Tag對象。  
  

-   **Comment**
Comment對象是一個特殊類型的NavigableString對象。
  
### 常用 BeautifulSoup methods
#### **find_all**( tag_name , attrs , recursive , text , **kwargs )  
  
* tag_name
    * 標籤名稱: soup.find_all('b')
    * 正規表示式:  soup.find_all(re.compile("^b"))  
    * 列表回傳: soup.find_all(["a", "p"])  
    * 規則回傳: soup.find_all(has_class_but_no_id)  
```
def has_class_but_no_id(tag): 
    return (tag.has_attr('class') and not tag.has_attr('id') ) 
```  
* tag_name + attrs
`soup.find_all("a", class_="ptt_title")`
`soup.find_all(attrs={"data-foo": "value"}) ` 
* 直接搜索標籤中文字
`soup.find_all(text=["Tillie", "Elsie", "Lacie"])`

* 限制回傳數量
`soup.find_all("div", limit=2)  `


### Python regular expression: **re.findall()**
[Official doc](https://docs.python.org/3/library/re.html)
[中文筆記](https://hackmd.io/CwE2GNgMwThhaSAOApvYAjATAVnhjGAdnxA3AGYsQQKYsig=?view)
```
import re
re.findall(pattern, string)
```


# 實戰演練
## 1. 水庫水位爬蟲: 靜態與可解析動態爬蟲
見 **爬蟲系列-2-3 水庫即時資訊**

## 2. [高鐵時刻表](https://www.thsrc.com.tw/tw/TimeTable/SearchResult):
cookie 與 header
```
import requests
import pandas as pd
from bs4 import BeautifulSoup
res = requests.get('https://www.thsrc.com.tw/tw/TimeTable/SearchResult')
soup = BeautifulSoup(res.text, "lxml")
name = [tag.text for tag in soup.find_all('option')[1:13]]
code = [value.attrs['value'] for value in soup.find_all('option')[1:13]]
station = pd.DataFrame({'站名':name,'代號':code},columns = ['站名','代號'])
station
```
![](https://i.imgur.com/wrsFadj.png)
```
import requests
import pandas as pd
# Input
url = "https://www.thsrc.com.tw/tw/TimeTable/Search"

form_data = {
'SearchType': 'S',
'StartStation': 'e6e26e66-7dc1-458f-b2f3-71ce65fdc95f',
'EndStation': 'e8fc2123-2aaf-46ff-ad79-51d4002a1ef3',
'DepartueSearchDate': '2019/02/28',
'DepartueSearchTime': '12:00',
}

headers = {'accept': 'application/json, text/javascript, */*;q=0.01',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh,zh-TW;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6',
'content-length': '340',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'cookie':,
'origin': 'https://www.thsrc.com.tw',
'referer': 'https://www.thsrc.com.tw/tw/TimeTable/SearchResult',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36',
'x-requested-with': 'XMLHttpRequest'}

# Requests
response = requests.request("POST", url, data=form_data, headers=headers)
r = response.json()
print(response.text)

r1 = r["data"]["DepartureTable"]["TrainItem"]
df = pd.DataFrame(r1)
```
![](https://i.imgur.com/hl37Itu.png)








