Learn More →
AndyChiangFri, Feb 5, 2021 9:59 AM
Python
爬蟲
再爬取某些網站時,你可能會發現無法抓到我們想要的資料,這是因為這些網站的資料採取動態載入,當你發出請求(Request)後,伺服器會找到你要的資料,並將這些資料打包回傳成回應(Response),這樣運作的網站就稱作動態網站。
想爬動態基本上有三種方法:
模擬使用者法,顧名思義就是模擬使用者在網站上的行為,達成像是自動輸入、按按鈕等行為,通常是用 Selenium 套件。
AJAX法,從 F12 + Network,觀察我們送出請求後,直接抓傳回來的回應。優點是速度快,很直接,缺點是觀察要花時間,通常用 Requests 套件。
cookie法,觀察網頁暫存於瀏覽器的 cookie,從中突破一些網站的限制,通常用 Requests 套件。
pip install selenium
為了要讓 Selenium 套件能夠自動開啟瀏覽器,所以需要安裝對應的 Webdriver (驅動程式),這邊以 Chrome 為範例,有兩種方式:
from selenium import webdriver
browser = webdriver.Chrome('./chromedriver', chrome_options=options)
browser.get(<url>)
pip install webdriver-manager
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
browser = webdriver.Chrome(ChromeDriverManager().install())
browser.get(<url>)
Selenium 的函數太多了,我只講幾個我用過的,其他要用時可以google或是去官方文件看一下。
使用 find_element_by_tag_name()
可以透過HTML標籤搜尋特定節點,參數為HTML標籤。
element = browser.find_element_by_tag_name("input")
element = browser.find_element_by_css_selector("input")
使用 find_element_by_id()
可以透過ID搜尋特定節點,參數為ID名稱。
element = browser.find_element_by_id("email")
等同於:
element = browser.find_element_by_css_selector("#email")
使用 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動態載入,觀念為我們客戶端一開始進入網頁時,伺服端會傳給我們一個沒有內容的HTML框架,之後客戶端發出其他請求,伺服端才會回傳必要資料,然後渲染成我們所看到有內容的網頁。
而我們首先要找的就是這些回傳的資料,通常是 .json
或 .xml
。
按 F12 >> Network 檢查我們發出了那些請求(requests),有時可以 F5(重新整理頁面) 重新發送一次請求。
這邊以KKday為例:
這邊要憑經驗一步一步慢慢找,找哪一個回應(response)有我們想要的資料,比方說KKday我們找到紅框圈起來的這個檔案,從 Header 可以看到這個檔案的URL,以及狀態碼(Status Code)。
篩選開啟 XHR 幫助我們更快找到想要的資料(XHR其實就是AJAX的別稱)。
切換到 Preview,可以看到檔案內部的資料,很幸運的,這正是我們要的資料!
每個網站資料的架構都不同,每爬一個網站就要去分析他的資料架構。
像是KKday,每個索引底下都還有更細微的資料,比如說name就是行程名稱,price就是價錢等等…。
除此之外,你也可以點兩下檔案開啟他,開啟後會到像是文字檔的地方,從這裡可以看到全部資料。
好啦,既然我們已經找到資料了,那下一步當然就是把他抓下來囉~
還記得剛才 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檔轉為陣列型態後,直接取用即可。
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 就稍微麻煩了點,需要轉為 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,可以到 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)