跳到主要內容

快速搞懂Python網頁爬蟲爬取AJAX動態網頁的秘訣

scraping_ajax_websites_using_python
Photo by LinkedIn Sales Navigator on Unsplash

在使用Python網頁爬蟲爬取網頁時,有些網頁為了提升使用體驗及維持執行效能,會使用AJAX的技術,非同步向伺服器傳送參數,取得資料來進行顯示,使用者不會感受到畫面有任何的閃爍或停頓,依然能夠正常的使用網頁。

不過想要爬取這種類型的網頁,點擊滑鼠右鍵檢視原始碼時,會看到滿滿的Unicode編碼資料,沒有HTML標籤,這時候要來爬取網頁時,會完全抓不到資料。

由於筆者在爬取KKday網站的一日遊票券時,就是遇到這樣的情況,所以想藉此來和大家分享Python網頁爬蟲該如何爬取AJAX類型的網頁,其中的開發流程如下:

  • 分析網頁
  • 分析AJAX回傳結果
  • 開發Python網頁爬蟲

一、分析網頁

首先,前往KKday網站,假設在搜尋的地方輸入「新竹市」進行搜尋後,在網頁左側的「所有商品類別」中,選擇「觀光旅行」下的「一日遊」,就可以查到新竹市一日遊的相關票券,如下圖:

scraping_ajax_websites_using_python

這時候,點擊滑鼠右鍵檢視網頁原始碼時,會完全找不到網頁上所顯示的票券名稱,都是Unicode編碼,這就是典型的AJAX類型網頁,也因為瀏覽器有辦法解析,所以網頁能夠正常的顯示中文字。

為了觀察網頁載入時,網頁向伺服器發送的請求(Request),點擊F12鍵開啟開發者模式,並且在原網頁點擊F5鍵重新整理,就可以在開發者模式的Network頁籤下,看到每一個請求(Request),如下圖:

scraping_ajax_websites_using_python
接著,就要來找哪一個請求(Request)的回應(Response)是網頁上的資料,以上圖第6個請求(Request)為例,點擊後,可以在「Headers」看到它的請求(Request)內容,如下圖:

scraping_ajax_websites_using_python
而要看這個請求(Request)的回應內容,可以切換到「Preview」頁籤來檢視,如下圖:

scraping_ajax_websites_using_python
從上圖即可看到網頁上所顯示的票券資料,也就代表如果要使用Python網頁爬蟲來發送請求(Request)時,就是要使用「Headers」頁籤下的「Request URL」網址,如下圖:

scraping_ajax_websites_using_python
從請求網址(Request URL)可以看到其中包含了幾個查詢的條件,目前只有輸入關鍵字(keyword)及選擇商品類別(cat),其它查詢條件都沒有輸入或保留預設值,所以,可以將請求網址(Request URL)簡化為:

「https://www.kkday.com/zh-tw/product/ajax_productlist/?keyword=新竹市&cat=TAG_4_4&sort=rdesc」

如果要查詢其它地區的一日遊票券,只需要替換關鍵字(keyword)參數即可。

二、分析AJAX回傳結果

在使用Python網頁爬蟲爬取一般網頁時,需要分析HTML標籤的結構,而AJAX類型的網頁,由於網頁原始碼中並沒有HTML標籤,所以,就需要分析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)的查詢參數,來更精準的爬取所需的票券資訊,希望本文的分享對您有所幫助。

如果您喜歡我的文章,請幫我按五下Like(使用GoogleFacebook帳號免費註冊),支持我創作教學文章,回饋由LikeCoin基金會出資,完全不會花到錢,感謝大家。
有想要看的教學內容嗎?歡迎利用以下的Google表單讓我知道,將有機會成為教學文章,分享給大家😊

Python學習資源
Python網頁爬蟲推薦課程
    Python網頁爬蟲-BeautifulSoup教學
    Python網頁爬蟲-Selenium教學
    Python非同步網頁爬蟲
    Python網頁爬蟲應用
    Python網頁爬蟲部署
    Python網頁爬蟲資料儲存
    Python網頁爬蟲技巧









    留言

    1. 請問一下,當我再進一層連結到行程介紹的時候,找不到任何有關行程介紹的媒介,請問除了selenium以外,還有別的辦法嗎?

      回覆刪除
    2. 感謝教學,寫得很詳細,但是[將請求網址(Request URL)簡化為:
      「https://www.kkday.com/zh-tw/product/ajax_productlist/?keyword=新竹市&cat=TAG_4_4&sort=rdesc」]這部分有點不知道為甚麼是這樣簡化,有什麼參考的教學嗎,謝謝

      回覆刪除
    3. 請問一下,request URL裡的值,若有些是網站動態生成的,每次都不一樣,有什麼方法可以取得正確的URL嗎?

      回覆刪除

    張貼留言