前言
  在之前的 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/dcGET
商品搜尋頁面有多項篩選條件,從下圖可以看到有關鍵字、商店類別、排序、取貨方式、價格範圍、依館別顯示等等。
搜尋頁面 篩選功能 而這些篩選條件正好對應網址後方帶的參數,我實際一個一個測試後,整理出來如下:
關鍵字 q 參數內。
商店類別 
數值 意思 all全部 24h24h購物 24b24h書店 vdr廠商出貨 tourPChome旅遊 
排序 sort 參數。
數值 意思 sale/dc有貨優先 rnk/dc精準度 prc/dc價錢由高至低 prc/ac價錢由低至高 new/dc新上市 
取貨方式 
數值 意思 cvs=all超商取貨 ipost=Yi 郵箱取貨 
價格範圍 price 參數。{price_max}-{price_min}
頁數 page 參數。
回傳資料
  它以 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-A900AWN96GET
如果需要,可以一次帶入多個商品一次請求,只需將商品 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_specGET
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)"?!
範例程式
  "搜尋商品分類"部分範例程式如下,很簡單,也沒什麼參數,完整程式碼同樣在文章最後會附上。
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=DSAUF4GET
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 
    } 
] 
範例程式
  "搜尋商品子分類名稱"部分範例程式如下,完整程式碼同樣在文章最後會附上。
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 
延伸練習
  在「
搜尋頁面的商品分類
 」章節有介紹到商品所屬的分類,一般操作可以點擊分類來篩選商品,那在程式裡我們該如何送出指定分類的請求呢? 結語
  這篇文章主要是說明在
搜尋頁面
 的請求,而之後會再寫一篇文章講解
商品頁面
 ,敬請其待。
如果對文章有任何疑問或心得,歡迎在底下按讚、留言喔~😎
比別人多一點執著,你就會創造奇蹟。