# Python 進階爬蟲
[![](https://img.shields.io/badge/dynamic/json?color=orange&label=總觀看人數&query=%24.viewcount&url=https://hackmd.io/Cp1938RtSZ6yu7DHfJvUDQ%2Finfo)]()
> [name=AndyChiang][time=Fri, Feb 5, 2021 9:59 AM][color=#00CDAF]
###### tags: `Python` `爬蟲`
## 靜態網站與動態網站比較
再爬取某些網站時,你可能會發現無法抓到我們想要的資料,這是因為這些網站的資料採取動態載入,當你發出**請求**(Request)後,伺服器會找到你要的資料,並將這些資料打包回傳成**回應**(Response),這樣運作的網站就稱作==動態網站==。
![](https://i.imgur.com/50bEaDB.png)
## 三種方法
想爬動態基本上有三種方法:
1. 模擬使用者法
2. AJAX法
3. cookie法
**模擬使用者法**,顧名思義就是模擬使用者在網站上的行為,達成像是自動輸入、按按鈕等行為,通常是用 Selenium 套件。
**AJAX法**,從 F12 + Network,觀察我們送出請求後,直接抓傳回來的回應。優點是速度快,很直接,缺點是觀察要花時間,通常用 Requests 套件。
**cookie法**,觀察網頁暫存於瀏覽器的 cookie,從中突破一些網站的限制,通常用 Requests 套件。
## 模擬使用者法
### 安裝 Selenium
```
pip install selenium
```
### 安裝 Webdriver
為了要讓 Selenium 套件能夠自動開啟瀏覽器,所以需要安裝對應的 Webdriver (驅動程式),這邊以 Chrome 為範例,有兩種方式:
#### 直接下載驅動程式執行檔下來
1. 到 Python 套件儲存庫 [PyPI](https://pypi.org/),搜尋 Selenium,點第一個。
2. 然後往下找到 Drive,下載對應的瀏覽器。
3. 解壓縮,然後建議將這個執行檔(.exe)放在專案同樣的資料夾(第一層),這樣才找的到路徑。
4. 引用 webdriver
```
from selenium import webdriver
```
5. 使用 webdriver,建立瀏覽器物件,參數為 **驅動程式路徑(必要)** 以及 **瀏覽器設定(chrome_options)(非必要)**。
路徑就是剛剛放執行檔(.exe)的路徑。如果放在跟專案同樣的資料夾,就寫範例的路徑,否則就以絕對路徑吧。
```
browser = webdriver.Chrome('./chromedriver', chrome_options=options)
browser.get(<url>)
```
#### 讓 Webdriver_manager 自動安裝好驅動程式(推薦)
1. 首先安裝 Webdriver_manager,這是一個幫你管理瀏覽器驅動程式的套件。
```
pip install webdriver-manager
```
2. 引用 webdriver 和 Webdriver_manager,這裡以 Chrome 為範例。
```
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
```
3. 使用 Webdriver_manager 自動安裝驅動程式
```
browser = webdriver.Chrome(ChromeDriverManager().install())
browser.get(<url>)
```
### Selenium 常用函數
Selenium 的函數太多了,我只講幾個我用過的,其他要用時可以google或是去[官方文件](https://selenium-python.readthedocs.io/)看一下。
#### 搜尋節點
##### 透過HTML標籤搜尋特定節點
使用 `find_element_by_tag_name()` 可以透過HTML標籤搜尋特定節點,參數為HTML標籤。
```
element = browser.find_element_by_tag_name("input")
```
##### 透過CSS選擇器搜尋特定節點
```
element = browser.find_element_by_css_selector("input")
```
##### 透過ID搜尋特定節點
使用 `find_element_by_id()` 可以透過ID搜尋特定節點,參數為ID名稱。
```
element = browser.find_element_by_id("email")
```
等同於:
```
element = browser.find_element_by_css_selector("#email")
```
##### 透過class搜尋特定節點
使用 `find_element_by_class_name()` 可以透過class搜尋特定節點,參數為class名稱。
```
element = browser.find_element_by_class_name("email")
```
等同於:
```
element = browser.find_element_by_css_selector(".email")
```
以上指令在element後加上s,就會回傳所有符合的結果,並回傳列表。
#### 使用者行為
##### 輸入文字框
使用 `send_keys()` 可以輸入文字框,參數為輸入內容。常用來輸入帳密、關鍵字。
```
element.send_keys('example@gmail.com')
```
##### 送出資料
使用 `submit()` 可以送出資料,通常用於輸入完 input 後。
```
element.submit()
```
##### 點擊元素
使用 `click()` 可以點擊元素,通常用於按鈕。
```
element.click()
```
##### 滾動頁面
使用 `execute_script()` 可以執行 Javascript 指令,再搭配 `window.scrollTo()` 達成滾動頁面的效果,通常用於網站需要滾動來下載頁面的情況。
```
browser.execute_script("window.scrollTo(0,document.body.scrollHeight)")
```
##### 下拉式選單
先引用 Selenium 底下的 Select() 函數,再使用 Select() 轉成下拉式選單物件,最後在` select_by_value()` 改變選取值,參數為選取值(value)。
```
from selenium.webdriver.support.ui import Select
select_year = Select(browser.find_element_by_name("yy"))
select_year.select_by_value(year) # 選擇傳入的年份
```
#### 等待
在用 Selenium 時,等待的時機非常重要,因為如果遇到網路問題,程式想抓取的物件尚未讀取出來,就會出問題。有一些等待時機,下面這篇文章寫得很好。
參考網址:[[Python爬蟲教學]3個建構Python動態網頁爬蟲重要的等待機制](https://www.learncodewithmike.com/2020/06/python-selenium-waits.html)
## AJAX法
有些網站是採取AJAX動態載入,觀念為我們客戶端一開始進入網頁時,伺服端會傳給我們一個沒有內容的HTML框架,之後客戶端發出其他請求,伺服端才會回傳必要資料,然後渲染成我們所看到有內容的網頁。
而我們首先要找的就是這些回傳的資料,通常是 `.json` 或 `.xml`。
### 1. 分析網站Network
按 **F12 >> Network** 檢查我們發出了那些請求(requests),有時可以 F5(重新整理頁面) 重新發送一次請求。
這邊以KKday為例:
這邊要憑經驗一步一步慢慢找,找哪一個回應(response)有我們想要的資料,比方說KKday我們找到紅框圈起來的這個檔案,從 **Header** 可以看到這個檔案的**URL**,以及**狀態碼(Status Code)**。
:::info
篩選開啟 **XHR** 幫助我們更快找到想要的資料(XHR其實就是AJAX的別稱)。
:::
![](https://i.imgur.com/dOP8zPf.jpg)
切換到 **Preview**,可以看到檔案內部的資料,很幸運的,這正是我們要的資料!
![](https://i.imgur.com/IzHUfEg.jpg)
每個網站資料的架構都不同,每爬一個網站就要去分析他的資料架構。
像是KKday,每個索引底下都還有更細微的資料,比如說name就是行程名稱,price就是價錢等等...。
![](https://i.imgur.com/bCnRorO.jpg)
![](https://i.imgur.com/rnrsQvp.jpg)
除此之外,你也可以點兩下檔案開啟他,開啟後會到像是文字檔的地方,從這裡可以看到全部資料。
![](https://i.imgur.com/apzwMwe.jpg)
### 2. 分析AJAX
好啦,既然我們已經找到資料了,那下一步當然就是把他抓下來囉~
還記得剛才 **Header >> Request URL** 嗎? 我們要用 requests 套件抓這個網址下來。
```
import requests
response = requests.get(f"https://www.kkday.com/zh-tw/product/ajax_productlist/?country=&city=&keyword={self.city_name}&availstartdate=&availenddate=&cat={self.all_cat}&time=&glang=&sort=rdesc&page=1&row=10&fprice={self.low_price}&eprice={self.high_price}&precurrency=TWD&csrf_token_name=f6b242cf7046740b055b857dac24e1b9")
```
仔細分析一下URL,你會發現其實URL上面有許多關鍵字,都各自對應到一個條件,比方說,keyword就是查詢的關鍵字,cat就是標籤分類等等。
因此,我們可以直接改AJAX的關鍵字,改變查詢條件。
:::info
URL 前面加 `f`,是為了將字串轉為格式化字串,在格式化字串的 {} 中,可以直接填入區域內的變數。
:::
因網站而異,檔案會有 JSON 或 XML 兩種可能。
#### JSON
JSON 比較簡單,使用 `json()` 函數,將JSON檔轉為陣列型態後,直接取用即可。
```
import requests
r = requests.get("https://opendata.epa.gov.tw/ws/Data/AQI/?$format=json", verify=False)
list_of_dicts = r.json()
print(type(r))
# <class 'requests.models.Response'>
print(type(list_of_dicts))
# <class 'list'>
for i in list_of_dicts:
print(i["County"], i["SiteName"], i["PM2.5"])
```
#### XML
XML 就稍微麻煩了點,需要轉為 Byte 型態,再使用 XML 套件中 xpath() 函數解析。
```
import requests
from lxml import etree
from io import BytesIO
r = requests.get("https://opendata.epa.gov.tw/ws/Data/AQI/?$format=xml", verify=False)
xml_bytes = r.content
print(type(xml_bytes))
# <class 'bytes'>
f = BytesIO(xml_bytes)
tree = etree.parse(f)
counties = [t.text for t in tree.xpath("/AQI/Data/County")]
site_names = [t.text for t in tree.xpath("/AQI/Data/SiteName")]
pm25 = [t.text for t in tree.xpath("/AQI/Data/PM2.5")]
for c, s, p in zip(counties, site_names, pm25):
print(c, s, p)
```
## Cookie法
不了解cookie,可以到 [Python 靜態網頁爬蟲](/ZAdxgQ5aR6mhJ6KWEdll1g)>>Request - 進階 複習
以PPT的八卦版為例:
第一次進去時,會出現詢問是否滿18歲的頁面,點選**是**才能進入文章列表區,但第二次再進去時,就不會再詢問一次是否滿18歲了,這是為甚麼呢?
我們觀察 **F12>>Application>>Cookies**
![](https://i.imgur.com/LiVKv1b.jpg)
然後當我按下**是**時,cookie會多出一個 over18=1,這就是PTT存在瀏覽器中,記錄我是否點過滿18的餅乾。
![](https://i.imgur.com/Dz53JA8.jpg)
再來觀察 Network>>index.html>>Headers>>cookie 的地方,會發現PTT就是把cookie包裝在Headers中發送出去的。
![](https://i.imgur.com/Fx7fEhw.jpg)
也就是說,我們程式只要模擬使用者,發送一個一樣的cookie,就能順利抓到文章列表了!
```
import requests
from bs4 import BeautifulSoup
url = "https://www.ptt.cc/bbs/Gossiping/index.html"
response = requests.get(url, headers={"cookie": "over18=1"})
# 分析原始碼
soup = BeautifulSoup(response.text, "html.parser")
titles = soup.find_all("div", {"class": "title"})
for title in titles:
if title.a != None:
print(title.a.string)
```
## 參考網址
* [開發Python網頁爬蟲前需要知道的五個基本觀念](https://www.learncodewithmike.com/2020/10/python-web-scraping.html)
### Selenium
* [[Python爬蟲教學]整合Python Selenium及BeautifulSoup實現動態網頁爬蟲](https://www.learncodewithmike.com/2020/05/python-selenium-scraper.html)
* [[Python爬蟲教學]學會使用Selenium及BeautifulSoup套件爬取查詢式網頁](https://www.learncodewithmike.com/2020/08/python-integrate-selenium-and-beautifulsoup.html)
* [動態網頁爬蟲第一道鎖 — Selenium教學:如何使用Webdriver、send_keys(附Python 程式碼)](https://medium.com/marketingdatascience/selenium%E6%95%99%E5%AD%B8-%E4%B8%80-%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8webdriver-send-keys-988816ce9bed)
* [Selenium 使用 CSS locator 定位 HTML element](https://jzchangmark.wordpress.com/2015/03/16/selenium-%E4%BD%BF%E7%94%A8-css-locator-%E5%AE%9A%E4%BD%8D%E5%85%83%E4%BB%B6/)
### AJAX
* [[Python爬蟲教學]快速搞懂AJAX動態載入網頁的爬取秘訣](https://www.learncodewithmike.com/2020/10/scraping-ajax-websites-using-python.html)
* [輕鬆學習 Python:透過 API 擷取網站資料](https://medium.com/datainpoint/python-essentials-requesting-web-api-edd417a57ba5)
### Cookie
* [Python 使用 requests 模組產生 HTTP 請求,下載網頁資料教學](https://blog.gtwang.org/programming/python-requests-module-tutorial/)
* [彭彭學院 - cookie爬蟲教學影片](https://www.facebook.com/cwpeng.school/posts/2736890406598558/)
* [Python 自學第十五天:網路爬蟲 Web Crawler - 操作 Cookie、連續抓取頁面](https://jenifers001d.github.io/2019/12/23/Python/learning-Python-day15/)