在使用Python網頁爬蟲爬取網頁時,有些網頁為了提升使用體驗及維持執行效能,會使用AJAX的技術,非同步向伺服器傳送參數,取得資料來進行顯示,使用者不會感受到畫面有任何的閃爍或停頓,依然能夠正常的使用網頁。
不過想要爬取這種類型的網頁,點擊滑鼠右鍵檢視原始碼時,會看到滿滿的Unicode編碼資料,沒有HTML標籤,這時候要來爬取網頁時,會完全抓不到資料。
由於筆者在爬取KKday網站的一日遊票券時,就是遇到這樣的情況,所以想藉此來和大家分享Python網頁爬蟲該如何爬取AJAX類型的網頁,其中的開發流程如下:
- 分析網頁
- 分析AJAX回傳結果
- 開發Python網頁爬蟲
一、分析網頁
首先,前往KKday網站,假設在搜尋的地方輸入「新竹市」進行搜尋後,在網頁左側的「所有商品類別」中,選擇「觀光旅行」下的「一日遊」,就可以查到新竹市一日遊的相關票券,如下圖:
這時候,點擊滑鼠右鍵檢視網頁原始碼時,會完全找不到網頁上所顯示的票券名稱,都是Unicode編碼,這就是典型的AJAX類型網頁,也因為瀏覽器有辦法解析,所以網頁能夠正常的顯示中文字。
為了觀察網頁載入時,網頁向伺服器發送的請求(Request),點擊F12鍵開啟開發者模式,並且在原網頁點擊F5鍵重新整理,就可以在開發者模式的Network頁籤下,看到每一個請求(Request),如下圖:
接著,就要來找哪一個請求(Request)的回應(Response)是網頁上的資料,以上圖第6個請求(Request)為例,點擊後,可以在「Headers」看到它的請求(Request)內容,如下圖:而要看這個請求(Request)的回應內容,可以切換到「Preview」頁籤來檢視,如下圖:從上圖即可看到網頁上所顯示的票券資料,也就代表如果要使用Python網頁爬蟲來發送請求(Request)時,就是要使用「Headers」頁籤下的「Request URL」網址,如下圖:「https://www.kkday.com/zh-tw/product/ajax_productlist/?keyword=新竹市&cat=TAG_4_4&sort=rdesc」
二、分析AJAX回傳結果
在剛剛開發者模式下的「Preview」頁籤下,將回傳結果的「data」欄位點開來後,選擇其中一筆資料,可以看到它所擁有的欄位,如下範例:
{status: "success", msg: "", total: 29, page: 1, total_page: 3, page_title: "新竹市精選商品 - KKday",…}
data: [{id: 100441, prod_oid: 100441, name: "新竹人氣健行景點|高島山一日縱走|台北市區出發",…},…]
0: {id: 100441, prod_oid: 100441, name: "新竹人氣健行景點|高島山一日縱走|台北市區出發",…}
app_cheaper_than_web: false
cat_key: ["TAG_3_1", "TAG_4_3", "TAG_4_4"]
cities: [{id: "A01-001-00001", name: "台北"}, {id: "A01-001-00009", name: "新竹"}]
city_key: ["A01-001-00001", "A01-001-00009"]
confirm_hour: 48
countries: [{id: "A01-001", name: "台灣",…}]
country_key: ["A01-001"]
currency: "TWD"
days: 0
display_price: "1,600"
display_sale_price: "1,600"
duration: 420
earliest_sale_date: "20201024"
free_refund_before_day: null
free_refund_policy: false
hours: 7
id: 100441
img_url: "https://image.kkday.com/v2/image/get/w_600%2Cc_fit/s1.kkday.com/product_100441/20200522065440_dGIRY/jpg"
img_url_list: [,…]
instant_booking: false
instant_use: false
introduction: "現在預訂高島山一日縱走,台北市區出發來回接駁,由專業且經驗豐富的領隊帶領,走訪位於新竹的人氣健行路線,位於那羅部落的高島山,一日走遍高台山、小島田山、中島田山、島田山,漫步在片杉木林的山徑,欣賞大地景色,涼爽舒適!參加一日爬山健行,感受大自然的無限魅力!"
is_display_price: false
is_tourism_product: true
main_cat_key: "M01"
minutes: 0
name: "新竹人氣健行景點|高島山一日縱走|台北市區出發"
order_count: 2
price: 1600
prod_oid: 100441
promo_tag_keys: []
purchase_date: null
purchase_type: null
rating_count: 0
rating_star: 0
readable_url: null
sale_price: 1600
sale_status: "1"
show_city_name: "台北, 多個城市"
show_country_name: "台灣"
show_order_count: ""
show_rating_star: [0, "0", "0", "0", "0"]
show_url_order_num: ""
star_percentage: [0, "0", "0", "0", "0"]
theme_code: []
url: "https://www.kkday.com/zh-tw/product/100441"
...
其中,筆者想要爬取票券的名稱、價格、最早可使用日期、評價及票券內容連結,分別是以上結構的name、price、earliest_sale_date、rating_star及url欄位。
三、開發Python網頁爬蟲
分析完請求連結(Request URL)及回傳結果後,筆者使用物件導向來開發一個Python網頁爬蟲,能夠依據傳入的地區,爬取當地的一日遊票券資訊。
本文以Visual Studio Code為例,建立一個scrapers.py檔案,並且利用以下的指令安裝requests套件:
$ pip install requests
開啟scrapers.py檔案,引用requests模組(Module),如下範例:
import requests
建立「Kkday」類別(Class)及建構式(Constructor),讓使用者能夠在建立物件時,能夠傳入所要爬取的地區,如下範例:
import requests # KKday網站 class Kkday: #建構式 def __init__(self, city_name): self.city_name = city_name # 城市名稱屬性
詳細的Python類別(Class)觀念可以參考[Python物件導向]淺談Python類別(Class)文章。接著,建立scrape()方法(Method),用來發送請求爬取資料,如下範例:
import requests # KKday網站 class Kkday: #建構式 def __init__(self, city_name): self.city_name = city_name # 城市名稱屬性 def scrape(self):
其中,首先定義一個result串列(List),用來打包爬取的結果,如下範例:
import requests # KKday網站 class Kkday: #建構式 def __init__(self, city_name): self.city_name = city_name # 城市名稱屬性 def scrape(self): result = [] # 回傳結果
判斷如果有傳入地區,則利用requests模組(Module)的get()方法(Method)發送請求,如下範例:
import requests # KKday網站 class Kkday: #建構式 def __init__(self, city_name): self.city_name = city_name # 城市名稱屬性 def scrape(self): result = [] # 回傳結果 if self.city_name: # 如果城市名稱非空值 # 取得傳入城市的所有一日遊票券 response = requests.get( f"https://www.kkday.com/zh-tw/product/ajax_productlist/?keyword={self.city_name}&cat=TAG_4_4&sort=rdesc")
取得了回傳結果後,使用Python內建的json()方法(Method)來解析,並且存取「data」欄位,如下範例:
import requests # KKday網站 class Kkday: #建構式 def __init__(self, city_name): self.city_name = city_name # 城市名稱屬性 def scrape(self): result = [] # 回傳結果 if self.city_name: # 如果城市名稱非空值 # 取得傳入城市的所有一日遊票券 response = requests.get( f"https://www.kkday.com/zh-tw/product/ajax_productlist/?keyword={self.city_name}&cat=TAG_4_4&sort=rdesc") # 資料 activities = response.json()["data"]
接下來,就可以透過迴圈,讀取其中的一日遊票券資料,爬取所需的欄位,如下範例:
import requests # KKday網站 class Kkday: #建構式 def __init__(self, city_name): self.city_name = city_name # 城市名稱屬性 def scrape(self): result = [] # 回傳結果 if self.city_name: # 如果城市名稱非空值 # 取得傳入城市的所有一日遊票券 response = requests.get( f"https://www.kkday.com/zh-tw/product/ajax_productlist/?keyword={self.city_name}&cat=TAG_4_4&sort=rdesc") # 資料 activities = response.json()["data"] for activity in activities: # 票券名稱 title = activity["name"] # 票券詳細內容連結 link = activity["url"] # 票券價格 price = activity["price"] # 最早可使用日期 booking_date = activity["earliest_sale_date"] # 評價 star = activity["rating_star"]
最後,將每一筆票券資料利用Python內建的dict()方法(Method)打包成字典(Dictionary),並且加入到result串列(List)中,如下範例:
import requests # KKday網站 class Kkday: #建構式 def __init__(self, city_name): self.city_name = city_name # 城市名稱屬性 def scrape(self): result = [] # 回傳結果 if self.city_name: # 如果城市名稱非空值 # 取得傳入城市的所有一日遊票券 response = requests.get( f"https://www.kkday.com/zh-tw/product/ajax_productlist/?keyword={self.city_name}&cat=TAG_4_4&sort=rdesc") # 資料 activities = response.json()["data"] for activity in activities: # 票券名稱 title = activity["name"] # 票券詳細內容連結 link = activity["url"] # 票券價格 price = activity["price"] # 最早可使用日期 booking_date = activity["earliest_sale_date"] # 評價 star = activity["rating_star"] result.append( dict(title=title, link=link, price=price, booking_date=booking_date, star=star, source="KKday")) return result
假設要爬取花蓮一日遊的票券資料,就可以在建立Kkday物件時,傳入「花蓮」,並且呼叫scrape()方法(Method)來進行爬取,如下範例:
import requests # KKday網站 class Kkday: #建構式 def __init__(self, city_name): self.city_name = city_name # 城市名稱屬性 def scrape(self): result = [] # 回傳結果 if self.city_name: # 如果城市名稱非空值 # 取得傳入城市的所有一日遊票券 response = requests.get( f"https://www.kkday.com/zh-tw/product/ajax_productlist/?keyword={self.city_name}&cat=TAG_4_4&sort=rdesc") # 資料 activities = response.json()["data"] for activity in activities: # 票券名稱 title = activity["name"] # 票券詳細內容連結 link = activity["url"] # 票券價格 price = activity["price"] # 最早可使用日期 booking_date = activity["earliest_sale_date"] # 評價 star = activity["rating_star"] result.append( dict(title=title, link=link, price=price, booking_date=booking_date, star=star, source="KKday")) return result demo = Kkday("花蓮") print(demo.scrape())
執行結果
[ {'title': '【季節限定】花蓮賞鯨半日遊|多羅滿賞鯨團|市區接送', 'link': 'https://www.kkday.com/zh-tw/product/1875', 'price': 750, 'booking_date': '20201006', 'star': 4.64, 'source': 'KKday'}, {'title': '花蓮包車一日遊|太魯閣&清水斷崖&七星潭|花蓮包車推薦', 'link': 'https://www.kkday.com/zh-tw/product/2127', 'price': 1740, 'booking_date': '20201006', 'star': 4.56, 'source': 'KKday'}, {'title': '花蓮一日遊|太魯閣&清水斷崖|花蓮市區出發接送', 'link': 'https://www.kkday.com/zh-tw/product/12181', 'price': 550, 'booking_date': '20201005', 'star': 4.68, 'source': 'KKday'}, {'title': '花蓮六十石山金針花海一日遊|2020 金針花季|花蓮市區接送', 'link': 'https://www.kkday.com/zh-tw/product/9563', 'price': 1300, 'booking_date': '20201006', 'star': 4.44, 'source': 'KKday'}, {'title': '【86折優惠】花蓮包車一日遊 | 太魯閣&七星潭&佳興冰果室 | 花蓮包車推薦', 'link': 'https://www.kkday.com/zh-tw/product/102225', 'price':1680, 'booking_date': '20201006', 'star': 5, 'source': 'KKday'}, {'title': '台灣花蓮|2020 金針花季&富興小火車一日遊|花蓮市區/花蓮車站接送', 'link': 'https://www.kkday.com/zh-tw/product/100793', 'price': 1300, 'booking_date': '20201005', 'star': 4.69, 'source': 'KKday'}, {'title': '【花蓮太平洋左岸一日遊】石門洞、石梯坪、親不知子海上古道', 'link': 'https://www.kkday.com/zh-tw/product/32784', 'price': 520, 'booking_date': '20201006', 'star': 4.77, 'source': 'KKday'}, {'title': '【2020 金針花季】花蓮六十石山/赤柯山專屬包車一日遊', 'link': 'https://www.kkday.com/zh-tw/product/29131', 'price': 3300, 'booking_date': '20201006', 'star': 4.63, 'source': 'KKday'}, {'title': '花蓮一日遊|賞鯨 &太魯閣 &清水斷崖 &七星潭|花蓮市區出發接送', 'link': 'https://www.kkday.com/zh-tw/product/11492', 'price': 1500, 'booking_date': '20201007', 'star': 4.52, 'source': 'KKday'}, {'title': '【親子遊首選】花蓮遠雄海洋公園門票&來回接駁', 'link': 'https://www.kkday.com/zh-tw/product/368', 'price': 200, 'booking_date': '20201009', 'star': 4.66, 'source': 'KKday'} ]
四、小結
當想要使用Python網頁爬蟲爬取AJAX類型網站時,首先利用開發者模式觀察網頁向伺服器取得資料的網址(Request URL),接著,從「Preview」頁籤進行回傳結果的確認,並且決定所要爬取的欄位名稱,最後,即可開發Python網頁爬蟲來取得所需的資料。
讀者可以試著調整請求網址(Request URL)的查詢參數,來更精準的爬取所需的票券資訊,希望本文的分享對您有所幫助。
請問一下,當我再進一層連結到行程介紹的時候,找不到任何有關行程介紹的媒介,請問除了selenium以外,還有別的辦法嗎?
回覆刪除看起來只能使用selenium了 :D
刪除感謝教學,寫得很詳細,但是[將請求網址(Request URL)簡化為:
回覆刪除「https://www.kkday.com/zh-tw/product/ajax_productlist/?keyword=新竹市&cat=TAG_4_4&sort=rdesc」]這部分有點不知道為甚麼是這樣簡化,有什麼參考的教學嗎,謝謝
請問一下,request URL裡的值,若有些是網站動態生成的,每次都不一樣,有什麼方法可以取得正確的URL嗎?
回覆刪除