2024年6月22日 星期六

Python 學習筆記 : 網頁爬蟲實戰 (十三) 富時中國 A50 期貨指數

昨晚回到鄉下, 早上去晉德針灸回來後繼續來玩爬蟲 (希望能在六月底結束這場已耗時近三個月的爬蟲戰爭), 今天要爬的對象是從下面陳會安老師的這本書看到的範例 : 


此範例是要擷取新加坡交易所 (SGX) 的富時中國 A50 指數期貨報價資訊, 該指數由英國富時公司編製, 目標是追蹤中國滬深交易所市值最大的 50 家 A 股公司的市場表現. 

本系列之前的筆記參考 : 



一. 檢視目標網頁 :  

新加坡交易所 (SGX) 的富時中國 A50 指數期貨報價資訊網址如下 : 





可見目標資料是以表格方式呈現, 但檢視原始碼會發現裡面一個 table 元素也沒有, 網頁應該是 Javascript 動態產生 :




這可用 Quick Javascript Switcher 來確認, 當關閉 Javascript 功能時整個表格會消失不見, 可見此網頁確實是透過 Javascript 動態產生, 此種網頁無法直接用 requests 套件來擷取 (因為網頁中沒有實體資料, 是網頁載入後才繪製的). 不過, 透過分析 HTTP 請求流程, 有機會找到含有目標資料的未公開 API, 通常是 JSON (主流) 或 XML (較少見了) 格式之資料. 

在 Chrome 按 F12 開啟開發人員工具視窗並切換到 "Network/XHR" 頁籤, 然後重新整理目標網頁, 左下方視窗繪出現瀏覽器所提出的 HTTP 請求, 逐一點選並觀察右下方視窗的 Preview 內容, 可發現其中 CN?order=asc&orderby=... 這個請求的 Preview 內容就是繪製網頁表格的資料來源 (通常是檔案大小較大那個 URL) : 




也可以切到 Response 頁籤觀察回應訊息來確認 : 




切到 Header 頁籤可知此請求使用 HTTP GET 方法 :




在 CN?order=asc&orderby=... 請求上按滑鼠右鍵點選 Copy/Copy URL 即可複製此請求之完整網址, 將其貼到瀏覽器網址列 : 


事實上最後面兩個參數 session 與 t 不給也是可以的 : 


這就是隱藏在背後未公開的 API, 與上一篇測試的景氣對策信號網頁處理方式類似, 只要從 HTTP 請求流程中找出目標資料的 API 網址, 接下來就可以用 requests 套件來擷取它了, 但要注意的是發出 HTTP 請求的方法, 務必依照真人操作瀏覽器所用的方法, 例如此例用的是 GET 方法, 而前一篇景氣對策信號網頁用的是 POST 方法, 方法用錯可能無法擷取到資料, 因為不是每個伺服器都會同時實作資源的 GET 與 POST 方法. 

由於是使用 GET 方法, 可以將上面網址直接貼到瀏覽器網址列來瀏覽此 JSON 資料 :




現在新版瀏覽器都有一個 "美化排版" 的貼心功能, 勾選它就會將 JSON 格式資料以鍵值對方式整齊排列以增進可閱讀性. 由上可知, 目標資料都放在 data 這個鍵裡面. 


二. 使用 requests 擷取目標資料 (JSON) :  

先匯入會用到的套件模組 :

>>> import requests 
>>> import json  
>>> from pprint import pprint  

用 GET 方法擷取目標資料並且用 json.loads() 將回應的 JSON 字串轉成 Python 字典 :

>>> url='https://api.sgx.com/derivatives/v1.0/contract-code/CN?order=asc&orderby=delivery-month&category=futures'    
>>> res=requests.get(url)    # 注意此例必須用 get()
>>> res.encoding    
'utf-8'
>>> data=json.loads(res.text)   

用 pprint.pprint() 來檢視 data 鍵之值 (很長略去) : 

>>> pprint(data['data'])   
[{'aggregate-total-volume': 337725.0,
  'base-date': '20240621',
  'best-ask-price': 1206500.0,
  'best-ask-price-abs': 12065.0,
  'best-ask-price-adj': 12065.0,
  'best-ask-quantity': 5.0,
  'best-bid-price': 1205100.0,
  'best-bid-price-abs': 12051.0,
  'best-bid-price-adj': 12051.0,
  'best-bid-quantity': 9.0,
  'category': 'futures',
  'change': -13100.0,
  'change-abs': 131.0,
  'change-adj': -131.0,
  'change-percentage': -1.0753570842226234,
  'contract-code': 'CN',
  'contract-name': 'SGX FTSE China A50 Index Futures',
  'current-trading-session': '0',
  'daily-settlement-price': 1205100.0,
  'daily-settlement-price-abs': 12051.0,
  'daily-settlement-price-adj': 12051.0,
  'default-dsp': '2',
  'delivery-month': '2024-06',
  'derivative-type': 'equityindex',
  'first-trading-date': '2023-06-29',
  'it': 'cnfc',
  'last-trade-price': 1205100.0,
  'last-traded-price-abs': 12051.0,
  'last-traded-price-adj': 12051.0,
  'last-trading-date': '2024-06-26',
  'last-update-time': '2024-06-22 05:07:18.0',
  'open-interest': 963882.0,
  'outright-traded-volume': 305652.0,
  'p': 'X',
  'preliminary-settlement-price': 1218200.0,
  'preliminary-settlement-price-abs': 12182.0,
  'preliminary-settlement-price-adj': 12182.0,

...(略)...

  'session-close': '-',
  'session-close-abs': None,
  'session-close-adj': None,
  'session-open': None,
  'session-open-abs': None,
  'session-open-adj': None,
  'session-traded-high': None,
  'session-traded-high-abs': None,
  'session-traded-high-adj': None,
  'session-traded-low': None,
  'session-traded-low-abs': None,
  'session-traded-low-adj': None,
  'spread-volume': 0.0,
  'strike-price': None,
  'symbol': 'CNH25',
  'total-volume': 0.0,
  'updated-time': 1719007206472,
  'v': '',
  'volume-session': '1',
  'volume-trade': 0.0}]

由於目標資料較大, 我們可將 data 字典存入 json 檔案 :

>>> with open('sgx-ftse-a50.json', 'w', encoding='utf-8') as f:      
    json.dump(data, f)      

完整程式碼如下 :

import requests 
import json

url='https://api.sgx.com/derivatives/v1.0/contract-code/' +\
    'CN?order=asc&orderby=delivery-month&category=futures'    
res=requests.get(url)
data=json.loads(res.text)     # 亦可用 data=res.json()
with open('sgx-ftse-a50.json', 'w', encoding='utf-8') as f:      
    json.dump(data, f)
print('擷取結果儲存於 ./sgx-ftse-a50.json 檔案')

執行結果如下 :

>>> %Run sgx-ftse-a50-test-1.py  
擷取結果儲存於 ./sgx-ftse-a50.json 檔案


三. 擷取關鍵欄位存成 csv 檔 :  

從上面取得的目標資料可知, data['data'] 本身也是一個字典, 它包含相當多的鍵 (欄位), 其中對交易者較關鍵者如下 :
  • last-update-time: 最近更新時間
  • last-trading-date: 最近到期日
  • symbol: 代號
  • current-trading-session: 盤別
  • change-abs: 漲跌
  • change-percentage: 漲跌幅
  • session-open-abs: 開盤價
  • session-traded-high-abs: 最高價
  • session-traded-low-abs: 最低價
  • last-traded-price-abs: 收盤價
  • daily-settlement-price-abs: 結算價
  • total-volume: 成交量
  • open-interest: 未平倉量
下列程式碼從 data['data'] 中擷取這些關鍵欄位並存成 csv 檔 :

import requests 
import csv

url='https://api.sgx.com/derivatives/v1.0/contract-code/' +\
    'CN?order=asc&orderby=delivery-month&category=futures'    
res=requests.get(url)
data=res.json()
csv_file='sgx-ftse-a50-2.csv'
with open(csv_file, 'w+', newline='', encoding='utf-8') as f:   # newline='' 避免 Excel 空列   
    writer=csv.writer(f)
    header=['最近更新時間',
            '最近到期日',
            '代號',
            '盤別',
            '漲跌',
            '漲跌幅',
            '開盤價',
            '最高價',
            '最低價',
            '收盤價',
            '結算價',
            '成交量',
            '未平倉量']
    writer.writerow(header)    # 寫入標頭列
    for item in data['data']:     # 遍歷所有項目
        row=[]     # 儲存每列資料用
        row.append(item['last-update-time'])
        row.append(item['last-trading-date'])
        row.append(item['symbol'])
        if item['current-trading-session']=='0':   # 改換顯示
            row.append('T')      # 當日交易日
        else:
            row.append('T+')    # 次交易日
        row.append(item['change-abs'])
        row.append(item['change-percentage'])
        row.append(item['session-open-abs'])
        row.append(item['session-traded-high-abs'])
        row.append(item['change-percentage'])
        row.append(item['session-traded-low-abs'])
        row.append(item['last-traded-price-abs'])
        row.append(item['daily-settlement-price-abs'])
        row.append(item['total-volume'])
        row.append(item['open-interest'])
        writer.writerow(row)     # 寫入列
    print(f'擷取結果儲存於 ./{csv_file} 檔案')
    
執行結果如下 :

>>> %Run sgx-ftse-a50-test-2.py  
擷取結果儲存於 ./sgx-ftse-a50-2.csv 檔案  

用純文字編輯器開啟此 csv 檔 :




因 Excel 預設是 ANSI 編碼, 若直接用 Excel 開啟此 UTF-8 編碼的 csv 檔, 則中文會變成亂碼 :



 
解決之道是先用記事本開啟此 csv 檔, 然後另存新檔 (可用不同檔名, 例如後面加 BOM, 以免覆蓋原檔), 並選擇編碼格式為 "使用 BOM 之 UTF-8" 而非單純之 "UTF-8" :




這樣用 Excel 開啟 sgx-ftse-a50-2-BOM.csv 時就不會出現亂碼了 :





沒有留言 :