# 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/)