前言 在之前的 Python 網路爬蟲實例講解過蝦皮購物 ,而今日要來講解同樣在台灣非常多人使用的「PChome 線上購物 」。
實際操作後發現,PChome 在一個頁面中針對不同部分的資料有送出不同的請求,整理成一篇會太長,因此我將分為上下篇來介紹。 上篇也就是你正在看的本篇,主要講解在 PChome 搜尋頁面 的請求,而下篇是說明商品頁面 的請求。
* 「PChome 線上購物 」與「PChome 24h購物 」兩者的商品搜尋都是導到同樣的頁面,因此實際上是一樣的。
備註:此文僅教育學習,切勿用作商業用途,個人實作皆屬個人行為,本作者不負任何法律責任
線上購物 (來源:Pexels) 套件 此次 Python 爬蟲主要使用到的套件:
安裝
搜尋商品 進入 PChome 線上購物 網頁,先開啟瀏覽器的 開發人員工具 (F12 或 Ctrl + Shift + i),回到網頁左上角輸入想要搜尋的關鍵字,點擊"找商品"。
首頁左上角搜尋欄位 畫面跳轉到搜尋結果頁面後,開發人員工具切換到"Network" > "XHR",會發現其中一個請求包含著搜尋結果,而 prods
欄位內分別列出了每一項商品。
"Network" > "XHR" 頁面 切換到"Headers"頁面可以查看請求的網址與方法。
"Headers" 頁面 我們可以開一個新分頁,將此請求網址貼上前往,查看回傳的資料是否正確、長什麼樣子。
請求路徑與參數 請求網址:https://ecshweb.pchome.com.tw/search/v3.3/all/results?q=%E9%8D%B5%E7%9B%A4&page=1&sort=sale/dc
請求方法:GET
商品搜尋頁面有多項篩選條件,從下圖可以看到有關鍵字、商店類別、排序、取貨方式、價格範圍、依館別顯示等等。
搜尋頁面 篩選功能 而這些篩選條件正好對應網址後方帶的參數,我實際一個一個測試後,整理出來如下:
關鍵字 放在 q
參數內。
商店類別 放在網址路徑內,可參考下方程式。
數值 意思 all
全部 24h
24h購物 24b
24h書店 vdr
廠商出貨 tour
PChome旅遊
排序 sort
參數。
數值 意思 sale/dc
有貨優先 rnk/dc
精準度 prc/dc
價錢由高至低 prc/ac
價錢由低至高 new/dc
新上市
取貨方式
數值 意思 cvs=all
超商取貨 ipost=Y
i 郵箱取貨
價格範圍 price
參數。 我測試的結果是兩者都要,也就是要包含最大金額與最小金額的限制。{price_max}-{price_min}
頁數 page
參數。 回傳資料預設是 20 筆商品,如果要繼續往後抓取,需要帶入頁數參數。
回傳資料 它以 JSON 格式回傳,商品會在 prods 欄位內,每一個商品資料如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"Id" : "DGCD1E-A900AIK2Q" ,
"cateId" : "DGCA3Q" ,
"picS" : "/items/DGCD1EA900AIK2Q/000002_1582170821.jpg" ,
"picB" : "/items/DGCD1EA900AIK2Q/000001_1617156434.jpg" ,
"name" : "SanDisk USB Type-C™ 雙用隨身碟128GB (公司貨)" ,
"describe" : "具備usb type-c與type-a旋轉式2合1金屬隨身碟。..." ,
"price" : 629 ,
"originPrice" : 629 ,
"author" : "" ,
"brand" : "" ,
"publishDate" : "" ,
"sellerId" : "" ,
"isPChome" : 1 ,
"isNC17" : 0 ,
"couponActid" : [],
"BU" : "ec"
}
以上有許多欄位,這邊挑幾個已知道意思的來說明:
欄位 代表意思 Id 商品ID cateId 分類ID picS 商品圖片(縮圖) picB 商品圖片(主圖) name 商品名稱 describe 商品描述 price 商品價格 originPrice 商品原始價格 author (書籍)作者 brand (書籍)出版社 publishDate (書籍)出版日期 isNC17 是否限制級
商品圖片分為"picS"與"picB",查看後我猜測"picS"是在搜尋頁面的圖片,而"picB"是位於商品本身頁面的。
前方再加上 https://d.ecimg.tw
即圖片完整網址。 如 https://d.ecimg.tw/items/DGCD1EA900AIK2Q/000002_1582170821.jpg
。
"商品網址"是商品ID前方加上 https://24h.pchome.com.tw/prod/
,以上方為例就是:https://24h.pchome.com.tw/prod/DGCD1E-A900AIK2Q
範例程式 "搜尋商品"部分範例程式如下,完整程式碼在文章最後會附上。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def search_products ( self , keyword , max_page = 1 , shop = '全部' , sort = '有貨優先' , price_min =- 1 , price_max =- 1 , is_store_pickup = False , is_ipost_pickup = False ):
"""搜尋商品
:param keyword: 搜尋關鍵字
:param max_page: 抓取最大頁數
:param shop: 賣場類別 (全部、24h購物、24h書店、廠商出貨、PChome旅遊)
:param sort: 商品排序 (有貨優先、精準度、價錢由高至低、價錢由低至高、新上市)
:param price_min: 篩選"最低價" (需與 price_max 同時用)
:param price_max: 篩選"最高價" (需與 price_min 同時用)
:param is_store_pickup: 篩選"超商取貨"
:param is_ipost_pickup: 篩選"i 郵箱取貨"
:return products: 搜尋結果商品
"""
products = []
all_shop = {
'全部' : 'all' ,
'24h購物' : '24h' ,
'24h書店' : '24b' ,
'廠商出貨' : 'vdr' ,
'PChome旅遊' : 'tour' ,
}
all_sort = {
'有貨優先' : 'sale/dc' ,
'精準度' : 'rnk/dc' ,
'價錢由高至低' : 'prc/dc' ,
'價錢由低至高' : 'prc/ac' ,
'新上市' : 'new/dc' ,
}
url = f 'https://ecshweb.pchome.com.tw/search/v3.3/{all_shop[shop]}/results'
params = {
'q' : keyword ,
'sort' : all_sort [ sort ],
'page' : 0
}
if price_min >= 0 and price_max >= 0 :
params [ 'price' ] = f '{price_min}-{price_max}'
if is_store_pickup :
params [ 'cvs' ] = 'all' # 超商取貨
if is_ipost_pickup :
params [ 'ipost' ] = 'Y' # i 郵箱取貨
while params [ 'page' ] < max_page :
params [ 'page' ] += 1
data = self . request_get ( url , params )
if not data :
print ( f '請求發生錯誤:{url}{params}' )
break
if data [ 'totalRows' ] <= 0 :
print ( '找不到有關的產品' )
break
products . extend ( data [ 'prods' ])
if data [ 'totalPage' ] <= params [ 'page' ]:
break
return products
商品販售狀態 如果你還想要"商品販售狀態”,會發現此欄位不包含在上方"搜尋商品"請求的回傳資料中,我們要再從其他的地方找找。商品狀態按鈕
試試用"立即訂購"之類的關鍵字在開發人員工具中搜尋,在某個搜尋到的 JavaScript 檔案發現到"orderNow"、"soldOut"等字眼。
searchplus JS 再進一步以"soldOut"搜尋,在另外一個 JavaScript 檔案發現到此資訊。button JS
雖然它是 JS 檔,但你只要把網址後方的 &_callback=jsonpcb_button
刪掉,它就會以 JSON 格式回傳了。
請求路徑與參數 請求網址路徑很大一串,經過我刪減、測試,其實最少只需要如下:
請求網址:https://ecapi.pchome.com.tw/ecshop/prodapi/v2/prod/button&id=DRAD1K-A900AWN96
請求方法:GET
如果需要,可以一次帶入多個商品一次請求,只需將商品 ID 以 ,
連接,例如:https://ecapi.pchome.com.tw/ecshop/prodapi/v2/prod/button&id=DRAD1K-A900AWN96,DSAA6I-A900B3488
就不需要分多次請求了。
也可以透過 fields
參數來指定你想要那些欄位,例如我只要商品ID、商品數量、商品狀態:https://ecapi.pchome.com.tw/ecshop/prodapi/v2/prod/button&id=DRAD1K-A900AWN96&fields=Id,Qty,ButtonType
回傳資料 它一樣以 JSON 格式回傳:
1
2
3
4
5
6
7
8
9
10
11
12
[{
"Seq" : 25977002 ,
"Id" : "DCAD6E-A900B1YM4-000" ,
"Store" : "DCAD6E" ,
"Price" : { "M" : 0 , "P" : 550 , "Prime" : "" },
"Qty" : 7 ,
"ButtonType" : "ForSale" ,
"SaleStatus" : 1 ,
"Group" : "DCAD6E-A900B1YM4" ,
"isPrimeOnly" : 0 ,
"SpecialQty" : 0
}]
從 ButtonType
的值可以看出此商品是否還有貨,像是 ForSale 代表此商品有出售、可以購買;SoldOut 代表此商品已售完等等。
範例程式 "商品販售狀態"部分範例程式如下,完整程式碼在文章最後會附上。
這邊我設計成傳入單個商品的字串(String),或多個商品的字串陣列(List)都可以,比較彈性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_products_sale_status ( self , products_id ):
"""取得商品販售狀態
:param products_id: 商品 ID
:return data: 商品販售狀態資料
"""
if type ( products_id ) == list :
products_id = ',' . join ( products_id )
url = f 'https://ecapi.pchome.com.tw/ecshop/prodapi/v2/prod/button&id={products_id}'
data = self . request_get ( url )
if not data :
print ( f '請求發生錯誤:{url}' )
return []
return data
商品規格種類 除了"商品狀態"不在原本的回傳資料中,還有某些商品有不同的規格,這部分也是要另外抓。
商品規格種類 一樣透過搜尋,找到了一個請求的路徑。
查找"商品規格種類"路徑 請求路徑與參數 請求網址:https://ecapi.pchome.com.tw/ecshop/prodapi/v2/prod/spec&id=DYARL9-A900AZTJT&_callback=jsonpcb_spec
請求方法:GET
id
就是給你要找的商品 ID。 跟上一節"商品販售狀態"一樣,如需一次請求多個商品,只需將商品 ID 以 ,
連接,例如:https://ecapi.pchome.com.tw/ecshop/prodapi/v2/prod/spec&id=DYARL9-A900AZTJT,DYARLA-A900AZTJJ&_callback=jsonpcb_spec
回傳資料 這邊有個小小麻煩的點,如果像"商品販售狀態"一樣把後方 _callback=json...
去除,反而它就不回傳資料了,但加了這樣會使得回傳資料前後會有一段 JS 語法,所以我們要先將其字串移除後,才是正確的 JSON 格式。
1
2
3
4
5
6
7
8
9
10
11
12
13
try {
jsonpcb_spec ({
"DYARIO-A900B0P77" : [{
"Seq" : 25866719 ,
...( 省略 )...
"PreOrdDate" : ""
}]
});
} catch ( e ) {
if ( window . console ) {
console . log ( e );
}
}
* 題外話:我原本是想用 str.lstrip()
、str.rstrip()
去移除,但遇到了點問題,所以後來改用字串切割的方式。
去除前後 JS 語法後回傳資料範例:
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
{
"DYARL9-A900AZTJT" : [
{
"Seq" : 25787942 ,
"Id" : "DYARL9-A900AZTJT-001" ,
"Name" : "防摔!四角加厚空壓殼 iPhone 12 / i12 手機殼 保護殼 手機套 軟殼 保護套 防撞" ,
"Spec" : "透藍" ,
"Group" : "DYARL9-A900AZTJT" ,
"Pic" : {
"B" : null ,
"S" : null
},
"Qty" : 0 ,
"isPreOrder24h" : 0 ,
"PreOrdDate" : ""
},
{
"Seq" : 25787944 ,
"Id" : "DYARL9-A900AZTJT-003" ,
"Name" : "防摔!四角加厚空壓殼 iPhone 12 / i12 手機殼 保護殼 手機套 軟殼 保護套 防撞" ,
"Spec" : "透粉" ,
"Group" : "DYARL9-A900AZTJT" ,
"Pic" : {
"B" : null ,
"S" : null
},
"Qty" : 6 ,
"isPreOrder24h" : 0 ,
"PreOrdDate" : ""
}
]
}
範例程式 "商品規格種類"部分範例程式如下,完整程式碼同樣在文章最後會附上。
1
2
3
4
5
6
7
8
9
10
11
12
13
def get_products_specification ( self , products_id ):
"""取得商品規格種類
:param products_id: 商品 ID
:return data: 商品規格種類
"""
if type ( products_id ) == list :
products_id = ',' . join ( products_id )
url = f 'https://ecapi.pchome.com.tw/ecshop/prodapi/v2/prod/spec&id={products_id}&_callback=jsonpcb_spec'
data = self . request_get ( url , to_json = False )
# 去除前後 JS 語法字串
data = json . loads ( data [ 17 : - 48 ])
return data
搜尋商品分類 至於位於搜尋頁面的左側,這一大串的"分類"該從何而來?
搜尋頁面的商品分類 找到了一個請求的路徑。
"搜尋商品分類"路徑 請求路徑與參數 請求網址:https://ecshweb.pchome.com.tw/search/v3.3/all/categories?q=鍵盤
請求方法:GET
q
後方就是接你是用什麼關鍵字搜尋商品的。
回傳資料 因為資料量比較大,我挑一小部分將解其結構:
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
[
{
"Id" : "D" ,
"name" : "24h購物" ,
"qty" : 12552 ,
"nodes" : [
{
"Id" : "DSAU" ,
"name" : "DIY電腦" ,
"qty" : 1418 ,
"nodes" : [
{
"Id" : "DSAUF4" ,
"qty" : 189
},
{
"Id" : "DSAUF5" ,
"qty" : 176
},
{
"Id" : "DSAUFA" ,
"qty" : 154
}
]
}
]
}
]
它是這樣由最大項分類一層一層下來,每一層會有 Id
、name
、qty
等參數,代表此分類的"ID"、"名稱"、"商品數量"。
但你可能會發現,怎麼到了最下面一層(第三層)卻沒了"名稱(name
)"?! 該不會…對😅沒錯,它還要再透過另一個請求去取得,這部分會在下一章節介紹。
範例程式 "搜尋商品分類"部分範例程式如下,很簡單,也沒什麼參數,完整程式碼同樣在文章最後會附上。
1
2
3
4
5
6
7
8
9
def get_search_category ( self , keyword ):
"""取得搜尋商品分類(網頁左側)
:param keyword: 搜尋關鍵字
:return data: 分類資料
"""
url = f 'https://ecshweb.pchome.com.tw/search/v3.3/all/categories?q={keyword}'
data = self . request_get ( url )
return data
至於此分類該如何用來當搜尋商品的篩選條件,這部分就留給你們親自嘗試了,不會很複雜 (真的!)。
搜尋商品子分類名稱 從上一章節,我們發現到商品分類的請求資料中,沒有包含最下面一層(第三層)的"name(名稱)"欄位。
搜尋商品子分類名稱 請求路徑與參數 請求網址:https://ecapi-pchome.cdn.hinet.net/cdn/ecshop/cateapi/v1.5/store&id=DSAUF4
請求方法:GET
id
後方接在上一章節"搜尋商品分類"中回傳的子分類。 一樣,如需一次請求多個商品,只需將子分類 ID 以 ,
連接,例如:https://ecapi-pchome.cdn.hinet.net/cdn/ecshop/cateapi/v1.5/store&id=DSAUF4,DSAUF5,DSAUFA&fields=Id,Name
此請求路徑還有個 fields
參數,可指定回傳欄位,例如我只要"ID"跟"名稱":https://ecapi-pchome.cdn.hinet.net/cdn/ecshop/cateapi/v1.5/store&id=DSAU&fields=Id,Name
回傳資料 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"Id" : "DSAUF4" ,
"Name" : "→10代Core i7八核" ,
"OriginId" : "" ,
"Sort" : 7 ,
"is24h" : 1 ,
"isVip" : 0 ,
"isPick" : 0 ,
"isNC17" : 0 ,
"Extra" : {
"Amount" : "" ,
"Fee" : "" ,
"Intro" : ""
},
"isNFC" : 0
}
]
範例程式 "搜尋商品子分類名稱"部分範例程式如下,完整程式碼同樣在文章最後會附上。
1
2
3
4
5
6
7
8
9
10
11
def get_search_categories_name ( self , categories_id ):
"""取得商品子分類的名稱(網頁左側)
:param categories_id: 分類 ID
:return data: 子分類名稱資料
"""
if type ( categories_id ) == list :
categories_id = ',' . join ( categories_id )
url = f 'https://ecapi-pchome.cdn.hinet.net/cdn/ecshop/cateapi/v1.5/store&id={categories_id}&fields=Id,Name'
data = self . request_get ( url )
return data
完整程式碼 附上完整程式碼:pchome_spider01.py (對超連結右鍵 > 另存連結為)
延伸練習 在「搜尋頁面的商品分類 」章節有介紹到商品所屬的分類,一般操作可以點擊分類來篩選商品,那在程式裡我們該如何送出指定分類的請求呢? (小提示:注意觀察送出請求的路徑) 結語 這篇文章主要是說明在搜尋頁面 的請求,而之後會再寫一篇文章講解商品頁面 ,敬請其待。
如果對文章有任何疑問或心得,歡迎在底下按讚、留言喔~😎
比別人多一點執著,你就會創造奇蹟。
🔻 如果覺得喜歡,歡迎在下方獎勵我 5 個讚~