請啟用JavaScript來查看內容

[Python爬蟲實例] 教你爬取"foodpanda"餐飲外送平台

前言

現在疫情肆虐,全台提升至第三級警戒,餐廳也被限制不能內用,只能被迫發展外帶(外送)或休店,因而許多人開始轉往像 foodpanda、Ubereats 此類餐飲外送平台來訂餐,也避免外出與人接觸的風險。

今天的Python網路爬蟲實例系列將來介紹 foodpanda(網頁版) 該如何搜尋餐廳、查看餐廳資訊與菜單,看看它是如何發出請求、解析回傳資料。

餐點 (來源:Unsplash)
餐點 (來源:Unsplash)

備註:此文僅教育學習,切勿用作商業用途,個人實作皆屬個人行為,本作者不負任何法律責任。

套件

此次 Python 爬蟲主要使用到的套件:

安裝

1
pip install requests

搜尋餐廳

請求路徑與參數

假如我目前位在"高雄車站",想要喝杯"星巴克"的美式咖啡,因此我到 foodpanda 搜尋"星巴克",點擊"搜尋"。

此時,你的網址會長的大概像這樣:
https://www.foodpanda.com.tw/restaurants/new?lat=22.639473&lng=120.3025185&vertical=restaurants&expedition=delivery&query=%E6%98%9F%E5%B7%B4%E5%85%8B

此時在瀏覽器的 開發人員工具 (F12 或 Ctrl + Shift + i) > Network(網路) > XHR 分頁,會看見一個 feed 的請求(如果沒出現再重新搜尋),裡面放的即是搜尋出來的餐廳結果。

開發人員工具 > Network > XHR > feed
開發人員工具 > Network > XHR > feed

切到 Headers 查看請求的方式與參數。

網址是 https://disco.deliveryhero.io/search/api/v1/feed、使用 POST 方法、需要帶的 payload

"搜尋餐廳"請求的方式與參數
"搜尋餐廳"請求的方式與參數

我將其整理出來,相關參數如下:

Request URL:https://disco.deliveryhero.io/search/api/v1/feed
Request Methon:POST

Request Payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
    "q": "星巴克",
    "location": {
        "point": {
            "latitude": 22.639473,
            "longitude": 120.3025185
        }
    },
    "config": "Variant21",
    "customer_id": "",
    "vertical_types": [
        "restaurants"
    ],
    "include_component_types": [
        "vendors"
    ],
    "include_fields": [
        "feed"
    ],
    "language_id": "6",
    "opening_type": "delivery",
    "platform": "web",
    "session_id": "",
    "language_code": "zh",
    "customer_type": "regular",
    "limit": 48,
    "offset": 0,
    "dps_session_id": "eyJzZXNzaX2lk......MjM1zOTl9",
    "dynamic_pricing": 0,
    "brand": "foodpanda",
    "country_code": "tw",
    "use_free_delivery_label": false
}


foodpanda 會依照你附近的餐廳去搜尋,因此請求內需要帶入經緯度,你可以用自己目前所在的位置,或指定特定位置。
例如我使用 Google 地圖點擊地點後,在網址列的部分可以看到此地點的經緯度:

高雄車站經緯度
高雄車站經緯度

q 參數就是放入搜尋的關鍵字。

limitoffset 即是限制此次請求的最大資料筆數與偏移值,如果請求你使用 limit:48 offset:0 (第一頁),那改成 limit:48 offset:48 就是第二頁。

其餘參數照原樣帶入即可,以及有些可以刪除。

有一點要注意,搜尋結果只會顯示目前還在營業時間內的餐廳。所以我自己在測試時,一開始還覺得奇怪,怎麼有時候搜的到這家店,但有時候又搜不到 XDDD

回傳資料

回傳資料內,data['feed']['count'] 會顯示此搜尋的餐廳總數,在 data['feed']['items'][0]['items'] 存放著搜尋結果(餐廳列表)。

餐廳列表包含著每一間餐廳的基本資訊,像是"餐廳名稱"、"地址"、"評分"、"評論數量"、"餐廳圖片"等等,詳細可參考底下 JSON 內資料。

回覆資料範例:fp_search.json (單一一間餐廳)

範例程式

使用 Python 搭配 requests 套件的寫法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import json
import requests


url = 'https://disco.deliveryhero.io/search/api/v1/feed'
payload = {
    'q': '星巴克',
    'location': {
        'point': {
            'longitude': 120.3025185,  # 經度
            'latitude': 22.639473  # 緯度
        }
    },
    'config': 'Variant17',
    'vertical_types': ['restaurants'],
    'include_component_types': ['vendors'],
    'include_fields': ['feed'],
    'language_id': '6',
    'opening_type': 'delivery',
    'platform': 'web',
    'language_code': 'zh',
    'customer_type': 'regular',
    'limit': 48,  # 一次最多顯示幾筆(預設 48 筆)
    'offset': 0,  # 偏移值,想要獲取更多資料時使用
    'dynamic_pricing': 0,
    'brand': 'foodpanda',
    'country_code': 'tw',
    'use_free_delivery_label': False
}
headers = {
    'content-type': "application/json",
}
r = requests.post(url=url, data=json.dumps(payload), headers=headers)
if r.status_code == requests.codes.ok:
    data = r.json()
    restaurants = data['feed']['items'][0]['items']
    for restaurant in restaurants[:5]:
        print(restaurant['name'])
else:
    print('請求失敗')

* 傳入 Payload 前需要用 json.dumps() 將其轉成 JSON 格式。
* Headers 內要加入 content-type(HTTP 內容類型) 來表明用 JSON 格式來傳遞參數資料。


除了透過"搜尋關鍵字"來取得餐廳列表,接下來介紹常見的"附近所有餐廳"、"分類推薦的餐廳"兩種方式。

取得附近所有餐廳

請求路徑與參數

網址是 https://disco.deliveryhero.io/listing/api/v1/pandora/vendors、使用 GET 方法以及需要的查詢參數Query String Parameters

"附近所有餐廳"請求的方式與參數
"附近所有餐廳"請求的方式與參數

經過測試,請求的標頭還需要帶上'x-disco-client-id': 'web'


整理出來,相關參數如下:

Request URL:https://disco.deliveryhero.io/listing/api/v1/pandora/vendors
Request Methon:GET
Request Headers:{'x-disco-client-id': 'web'}

Query String Parameters:

latitude: 22.6394088
longitude: 120.3025474
language_id: 6
include: characteristics
dynamic_pricing: 0
configuration: Original
country: tw
customer_id: 
customer_hash: 
budgets: 
cuisine: 
sort: 
food_characteristic: 
use_free_delivery_label: false
vertical: restaurants
limit: 48
offset: 0
customer_type: regular


還可以透過帶入不同的參數值,來達到對結果做不同的篩選。

取餐方式

  • 外送
    vertical: restaurants

  • 外帶自取
    vertical: restaurants
    opening_type: pickup

  • 生鮮雜貨
    vertical: shop

排序方式

排序sort:
排序(預設)
評分最高rating_desc
最快送達delivery_time_asc
距離distance_asc

例如想將結果的餐廳依照"評分最高到低"排序,則只需要將 sort 帶入 rating_desc (sort:rating_desc)。

有折扣
has_discount: 1

料理種類

料理種類cuisine:
中港166
健康餐225
小吃214
披薩165
日韓164
東南亞252
歐式175
漢堡177
甜點176
異國183
素食186
美式179
飲料181
麵食料理201

可多選,例如:“日韓"和"麵食料理"就是 cuisine:164,201

特色種類

特色food_characteristic:
<pro專屬優惠>257
<店內價>55
<每月推薦>206
<熱搜人氣餐廳>207
<米其林推薦>205
<聚餐首選>241
<買一送一>239
Vogue野餐日258
三明治162
便當58
咖哩155
咖啡輕食191
早餐159
滷味158
火鍋160
炸雞154
粥 & 湯161
鐵板燒156
餃子157

與"料理種類"一樣可多選。

預算高低

預算budgets:
1
2
3

同樣可多選。

回傳資料

回傳資料內,
data['status_code']data['message'] 顯示本次請求狀態;
data['data']['available_count'] 會顯示可用的餐廳總數;
data['data']['aggregations']['cuisines'] 顯示各種"料理種類"的餐廳數量;
data['data']['aggregations']['foodCharacteristics'] 顯示各個"特色"的餐廳數量;
data['data']['items'] 列出此次回傳的餐廳列表。

至於餐廳列表中,各餐廳的資訊與文章前一節"搜尋餐廳"的雷同,在此就不再贅述。

回覆資料範例:fp_nearby_restaurants.json

範例程式

使用 Python 搭配 requests 套件的寫法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import requests


url = 'https://disco.deliveryhero.io/listing/api/v1/pandora/vendors'
query = {
    'longitude': 120.3025185,  # 經度
    'latitude': 22.639473,  # 緯度
    'language_id': 6,
    'include': 'characteristics',
    'dynamic_pricing': 0,
    'configuration': 'Variant1',
    'country': 'tw',
    'budgets': '',
    'cuisine': '',
    'sort': '',
    'food_characteristic': '',
    'use_free_delivery_label': False,
    'vertical': 'restaurants',
    'limit': 48,
    'offset': 0,
    'customer_type': 'regular'
}
headers = {
    'x-disco-client-id': 'web',
}
r = requests.get(url=url, params=query, headers=headers)
if r.status_code == requests.codes.ok:
    data = r.json()
    restaurants = data['data']['items']
    for restaurant in restaurants[:5]:
        print(restaurant['name'])
else:
    print('請求失敗')

取得分類推薦的餐廳

這邊我不太知道要把它叫做什麼名稱,指的是剛進來首頁底下推薦的部分,如下圖:

各分類餐廳
各分類餐廳

請求路徑與參數

網址是 https://disco.deliveryhero.io/core/api/v1/swimlanes、使用 GET 方法以及需要的查詢參數Query String Parameters

"分類推薦的餐廳"請求的方式與參數
"分類推薦的餐廳"請求的方式與參數

整理出來,相關參數如下:

Request URL:https://disco.deliveryhero.io/core/api/v1/swimlanes
Request Methon:GET

Query String Parameters:

brand: foodpanda
config: Original
latitude: 22.6394088
longitude: 120.3025474
language_code: zh
language_id: 6
country_code: tw
dynamic_pricing: 0
vertical_type: restaurants
opening_type: delivery
use_free_delivery_label: false
customer_type: regular


取餐方式

  • 外送
    config: Original
    vertical_type: restaurants
    opening_type: delivery

  • 外帶自取
    config: pickup-control
    vertical_type: restaurants
    opening_type: pickup

  • 生鮮雜貨
    config: shops-variant
    vertical_type: shop,darkstores
    opening_type: delivery

回傳資料

從回傳資料的 data['data']['items'] 裡列出不同的推薦分類,例如"優惠主打星⭐"、"在家吃遍全世界"、"本月推薦"等等,各推薦分類底下的 vendors 中也列出各家餐廳。

而各家餐廳的資訊與文章"搜尋餐廳"的說明雷同,在此就不再贅述。

回覆資料範例:fp_category_restaurant.json

範例程式

使用 Python 搭配 requests 套件的寫法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests


url = 'https://disco.deliveryhero.io/core/api/v1/swimlanes'
query = {
    'longitude': 120.3025185,  # 經度
    'latitude': 22.639473,  # 緯度
    'brand': 'foodpanda',
    'language_code': 'zh',
    'language_id': 6,
    'country_code': 'tw',
    'dynamic_pricing': 0,
    'use_free_delivery_label': False,
    'customer_type': 'regular',
    'config': 'Original',
    'vertical_type': 'restaurants',
    'opening_type': 'delivery'
}

r = requests.get(url=url, params=query)
if r.status_code == requests.codes.ok:
    data = r.json()
    recommendations = data['data']['items']
    for recommendation in recommendations[:5]:
        print(recommendation['headline'])
else:
    print('請求失敗')

取得餐廳基本資料與菜單

餐廳基本資料與菜單是在同一個請求中。

請求路徑與參數

網址是 https://tw.fd-api.com/api/v5/vendors/{餐廳代碼}、使用 GET 方法以及需要的查詢參數Query String Parameters

"餐廳代碼"從上方各種取得餐貼列表的方式,其中餐廳資訊裡的 code 欄位即顯示著此餐廳的代碼。

"餐廳基本資料與菜單"請求的方式與參數
"餐廳基本資料與菜單"請求的方式與參數

整理出來,相關參數如下:

Request URL:https://tw.fd-api.com/api/v5/vendors/{餐廳代碼}
Request Methon:GET

Query String Parameters:

include: menus
language_id: 6
dynamic_pricing: 0
opening_type: delivery
latitude: 22.639473
longitude: 120.3025185


有一點要注意,傳入的當前座標如果離店家太遠,會導致無法取得資料。
此時可以改成距離較近的座標,或者乾脆不要帶入座標(但會導致回傳資料的距離不正確)。

回傳資料

從回傳資料的 data['data'] 包含此餐廳的資訊,例如前面餐廳列表就有的店名、地址、最低訂購金額外,像是營業時間、折扣活動、餐廳分類、付款類型也有。還有重要的菜單資訊、配料資訊等等,都在這個請求的回應資料裡了。

回覆資料範例:fp_info_menu.json

範例程式

使用 Python 搭配 requests 套件的寫法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import requests


url = 'https://tw.fd-api.com/api/v5/vendors/g1mk'
query = {
    'include': 'menus',
    'language_id': '6',
    'dynamic_pricing': '0',
    'opening_type': 'delivery',
    'longitude': 120.3025185,  # 經度
    'latitude': 22.639473,  # 緯度
}

r = requests.get(url=url, params=query)
if r.status_code == requests.codes.ok:
    data = r.json()
    print('店名:', data['data']['name'])
    print('地址:', data['data']['address'])
    print('距離(公里):', data['data']['distance'])
else:
    print('請求失敗')

完整程式碼

我將以上四種請求寫在同一個 class 中,並可以傳入不同的值來篩選,供需要的人參考。

附上完整程式碼:foodpanda_spider.py | GitHub

延伸練習

  1. 在"搜尋餐廳"與"取得附近所有餐廳"章節中,說到能透過 limitoffset 來抓到更多的餐廳,試著自己實際寫寫看吧。

  2. 試著輸入餐廳代碼後,程式自動顯示餐廳的"基本資訊"與隨機五樣"餐點資訊"。

結語

之後會繼續陸續寫一些網站的Python網路爬蟲實例,如果你正好是剛開始想學爬蟲的新手、想知道某網站如何爬取資料、遇到其他問題,也歡迎在底下留言,或追蹤 FB 粉專『IT空間』~ 🔔




還能回頭說明旅途還沒有開始,當你無法回頭的時候,才是真正的旅途。

—— 《比宇宙更遠的地方》


🔻 如果覺得喜歡,歡迎在下方獎勵我 5 個讚~
分享

Jia
作者
Jia
軟體工程師