Try   HackMD

Python 進階爬蟲

AndyChiangFri, Feb 5, 2021 9:59 AM

tags: Python 爬蟲

靜態網站與動態網站比較

再爬取某些網站時,你可能會發現無法抓到我們想要的資料,這是因為這些網站的資料採取動態載入,當你發出請求(Request)後,伺服器會找到你要的資料,並將這些資料打包回傳成回應(Response),這樣運作的網站就稱作動態網站

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

三種方法

想爬動態基本上有三種方法:

  1. 模擬使用者法
  2. AJAX法
  3. cookie法

模擬使用者法,顧名思義就是模擬使用者在網站上的行為,達成像是自動輸入、按按鈕等行為,通常是用 Selenium 套件。

AJAX法,從 F12 + Network,觀察我們送出請求後,直接抓傳回來的回應。優點是速度快,很直接,缺點是觀察要花時間,通常用 Requests 套件。

cookie法,觀察網頁暫存於瀏覽器的 cookie,從中突破一些網站的限制,通常用 Requests 套件。

模擬使用者法

安裝 Selenium

pip install selenium

安裝 Webdriver

為了要讓 Selenium 套件能夠自動開啟瀏覽器,所以需要安裝對應的 Webdriver (驅動程式),這邊以 Chrome 為範例,有兩種方式:

直接下載驅動程式執行檔下來

  1. 到 Python 套件儲存庫 PyPI,搜尋 Selenium,點第一個。
  2. 然後往下找到 Drive,下載對應的瀏覽器。
  3. 解壓縮,然後建議將這個執行檔(.exe)放在專案同樣的資料夾(第一層),這樣才找的到路徑。
  4. 引用 webdriver
from selenium import webdriver
  1. 使用 webdriver,建立瀏覽器物件,參數為 驅動程式路徑(必要) 以及 瀏覽器設定(chrome_options)(非必要)
    路徑就是剛剛放執行檔(.exe)的路徑。如果放在跟專案同樣的資料夾,就寫範例的路徑,否則就以絕對路徑吧。
browser = webdriver.Chrome('./chromedriver', chrome_options=options)
browser.get(<url>)

讓 Webdriver_manager 自動安裝好驅動程式(推薦)

  1. 首先安裝 Webdriver_manager,這是一個幫你管理瀏覽器驅動程式的套件。
pip install webdriver-manager
  1. 引用 webdriver 和 Webdriver_manager,這裡以 Chrome 為範例。
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
  1. 使用 Webdriver_manager 自動安裝驅動程式
browser = webdriver.Chrome(ChromeDriverManager().install())
browser.get(<url>)

Selenium 常用函數

Selenium 的函數太多了,我只講幾個我用過的,其他要用時可以google或是去官方文件看一下。

搜尋節點

透過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動態網頁爬蟲重要的等待機制

AJAX法

有些網站是採取AJAX動態載入,觀念為我們客戶端一開始進入網頁時,伺服端會傳給我們一個沒有內容的HTML框架,之後客戶端發出其他請求,伺服端才會回傳必要資料,然後渲染成我們所看到有內容的網頁。
而我們首先要找的就是這些回傳的資料,通常是 .json.xml

1. 分析網站Network

F12 >> Network 檢查我們發出了那些請求(requests),有時可以 F5(重新整理頁面) 重新發送一次請求。
這邊以KKday為例:
這邊要憑經驗一步一步慢慢找,找哪一個回應(response)有我們想要的資料,比方說KKday我們找到紅框圈起來的這個檔案,從 Header 可以看到這個檔案的URL,以及狀態碼(Status Code)

篩選開啟 XHR 幫助我們更快找到想要的資料(XHR其實就是AJAX的別稱)。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

切換到 Preview,可以看到檔案內部的資料,很幸運的,這正是我們要的資料!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

每個網站資料的架構都不同,每爬一個網站就要去分析他的資料架構。
像是KKday,每個索引底下都還有更細微的資料,比如說name就是行程名稱,price就是價錢等等

除此之外,你也可以點兩下檔案開啟他,開啟後會到像是文字檔的地方,從這裡可以看到全部資料。

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的關鍵字,改變查詢條件。

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 靜態網頁爬蟲>>Request - 進階 複習

以PPT的八卦版為例:
第一次進去時,會出現詢問是否滿18歲的頁面,點選才能進入文章列表區,但第二次再進去時,就不會再詢問一次是否滿18歲了,這是為甚麼呢?

我們觀察 F12>>Application>>Cookies

然後當我按下時,cookie會多出一個 over18=1,這就是PTT存在瀏覽器中,記錄我是否點過滿18的餅乾。

再來觀察 Network>>index.html>>Headers>>cookie 的地方,會發現PTT就是把cookie包裝在Headers中發送出去的。

也就是說,我們程式只要模擬使用者,發送一個一樣的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)

參考網址

Selenium

AJAX