爬蟲概論 === ###### 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)