iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 7
2

解析 JSON

以取得個股盤後資訊為例。

分析資料結構

https://www.twse.com.tw/exchangeReport/MI_INDEX?response=json&type=ALLBUT0999&date=20190904
個股權證盤後資訊

  1. 所需資料表格位於第 9 表格。
  2. 股票的代碼為 4 位數字,且為 1-9 開頭。
  3. 其他的代碼是 0 開頭,但長度不一定且並非全為數字。

定義儲存類別

class AfterHoursInfo:
    def __init__(
        self,
        code,
        name,
        totalShare,
        totalTurnover,
        openPrice,
        highestPrice,
        lowestPrice,
        closePrice):
        # 代碼
        self.Code = code
        # 名稱
        self.Name = name
        # 成交股數
        self.TotalShare = totalShare
        # 成交金額
        self.TotalTurnover = totalTurnover
        # 開盤價
        self.OpenPrice = openPrice
        # 最高價
        self.HighestPrice = highestPrice
        # 最低價
        self.LowestPrice = lowestPrice
        # 收盤價
        self.ClosePrice = closePrice

下載資料並解析出個股盤後資訊

import datetime
import json
import os
import re

import loguru
import requests

def main():
    # 下載當日個股盤後資訊
    resp = requests.get(
        f'https://www.twse.com.tw/exchangeReport/MI_INDEX?' +
        f'response=json&' +
        f'type=ALLBUT0999' +
        f'&date={datetime.date.today():%Y%m%d}'
    )
    if resp.status_code != 200:
        loguru.logger.error('RESP: status code is not 200')
    loguru.logger.success('RESP: success')

    # 盤後資訊清單
    afterHoursInfos = []

    # 取出 JSON 內容
    body = resp.json()
    # 取出 stat 欄位
    stat = body['stat']
    # 如果 stat 不是 OK,代表查詢日期尚無資料
    if stat != 'OK':
        loguru.logger.error(f'RESP: body.stat error is {stat}.')
        return
    # 取出第 9 表格內容
    records = body['data9']
    # 依序取出每筆盤後資訊
    for record in records:
        # 取出代碼欄位值
        code = record[0].strip()
        # 符合股票代碼規則才處理
        if re.match(r'^[1-9][0-9][0-9][0-9]$', code) is not None:
            # 取出名稱欄位值
            name = record[1].strip()
            # 取出成交股數欄位值
            totalShare = record[2].replace(',', '').strip()
            # 取出成交金額欄位值
            totalTurnover = record[4].replace(',', '').strip()
            # 取出開盤價欄位值
            openPrice = record[5].replace(',', '').strip()
            # 取出最高價欄位值
            highestPrice = record[6].replace(',', '').strip()
            # 取出最低價欄位值
            lowestPrice = record[7].replace(',', '').strip()
            # 取出收盤價欄位值
            closePrice = record[8].replace(',', '').strip()
            afterHoursInfo = AfterHoursInfo(
                code=code,
                name=name,
                totalShare=totalShare,
                totalTurnover=totalTurnover,
                openPrice=openPrice,
                highestPrice=highestPrice,
                lowestPrice=lowestPrice,
                closePrice=closePrice
            )
            afterHoursInfos.append(afterHoursInfo)

    # 將每筆物件表達式輸出的字串以系統換行符號相接,讓每筆物件表達式各自獨立一行
    message = os.linesep.join([
        str(afterHoursInfo)
        for afterHoursInfo in afterHoursInfos
    ])
    loguru.logger.info('AFTERHOURSINFOS' + os.linesep + message)

if __name__ == '__main__':
    loguru.logger.add(
        f'{datetime.date.today():%Y%m%d}.log',
        rotation='1 day',
        retention='7 days',
        level='DEBUG'
    )
    main()

重新定義資料儲存結構

因為存入資料都是字串,但實際內容有整數(成交股數、成交金額),還有浮點數(開盤價、最高價、最低價、收盤價),所以需要在儲存類別的建構子中進行轉換處理。

import fractions

class AfterHoursInfo:
    def __init__(
        self,
        code,
        name,
        totalShare,
        totalTurnover,
        openPrice,
        highestPrice,
        lowestPrice,
        closePrice):
        # 代碼
        self.Code = code
        # 名稱
        self.Name = name
        # 成交股數
        self.TotalShare = int(totalShare)
        # 成交金額
        self.TotalTurnover = int(totalTurnover)
        # 開盤價
        self.OpenPrice = fractions.Fraction(openPrice)
        # 最高價
        self.HighestPrice = fractions.Fraction(highestPrice)
        # 最低價
        self.LowestPrice = fractions.Fraction(lowestPrice)
        # 收盤價
        self.ClosePrice = fractions.Fraction(closePrice)
    # 物件表達式
    def __repr__(self):
        totalShare = self.TotalShare
        if totalShare is not None:
            totalShare = f'{totalShare:.2f}'
        totalTurnover = self.TotalTurnover
        if totalTurnover is not None:
            totalTurnover = f'{totalTurnover:.2f}'
        openPrice = self.OpenPrice
        if openPrice is not None:
            openPrice = f'{openPrice:.2f}'
        highestPrice = self.HighestPrice
        if highestPrice is not None:
            highestPrice = f'{highestPrice:.2f}'
        lowestPrice = self.LowestPrice
        if lowestPrice is not None:
            lowestPrice = f'{lowestPrice:.2f}'
        closePrice = self.ClosePrice
        if closePrice is not None:
            closePrice = f'{closePrice:.2f}'
        return (
            f'class AfterHoursInfo {{ '
            f'Code={self.Code}, '
            f'Name={self.Name}, '
            f'TotalShare={totalShare}, '
            f'TotalTurnover={totalTurnover}, '
            f'OpenPrice={openPrice}, '
            f'HighestPrice={highestPrice}, '
            f'LowestPrice={float(lowestPrice):.2f}, '
            f'ClosePrice={closePrice} '
            f'}}'
        )

但執行後發現與前一日相同數值者會標示為 --,造成轉型錯誤,所以重新修正。

import datetime
import fractions
import json
import os
import re

import loguru
import requests

class AfterHoursInfo:
    def __init__(
        self,
        code,
        name,
        totalShare,
        totalTurnover,
        openPrice,
        highestPrice,
        lowestPrice,
        closePrice):
        # 代碼
        self.Code = code
        # 名稱
        self.Name = name
        # 成交股數
        self.TotalShare = self.checkNumber(totalShare)
        if self.TotalShare is not None:
            self.TotalShare = int(totalShare)
        # 成交金額
        self.TotalTurnover = self.checkNumber(totalTurnover)
        if self.TotalTurnover is not None:
            self.TotalTurnover = int(totalTurnover)
        # 開盤價
        self.OpenPrice = self.checkNumber(openPrice)
        if self.OpenPrice is not None:
            self.OpenPrice = fractions.Fraction(openPrice)
        # 最高價
        self.HighestPrice = self.checkNumber(highestPrice)
        if self.HighestPrice is not None:
            self.HighestPrice = fractions.Fraction(highestPrice)
        # 最低價
        self.LowestPrice = self.checkNumber(lowestPrice)
        if self.LowestPrice is not None:
            self.LowestPrice = fractions.Fraction(lowestPrice)
        # 收盤價
        self.ClosePrice = self.checkNumber(closePrice)
        if self.ClosePrice is not None:
            self.ClosePrice = fractions.Fraction(closePrice)
    # 物件表達式
    def __repr__(self):
        totalShare = self.TotalShare
        if totalShare is not None:
            totalShare = f'{totalShare}'
        totalTurnover = self.TotalTurnover
        if totalTurnover is not None:
            totalTurnover = f'{totalTurnover}'
        openPrice = self.OpenPrice
        if openPrice is not None:
            openPrice = f'{float(openPrice):.2f}'
        highestPrice = self.HighestPrice
        if highestPrice is not None:
            highestPrice = f'{float(highestPrice):.2f}'
        lowestPrice = self.LowestPrice
        if lowestPrice is not None:
            lowestPrice = f'{float(lowestPrice):.2f}'
        closePrice = self.ClosePrice
        if closePrice is not None:
            closePrice = f'{float(closePrice):.2f}'
        return (
            f'class AfterHoursInfo {{ '
            f'Code={self.Code}, '
            f'Name={self.Name}, '
            f'TotalShare={totalShare}, '
            f'TotalTurnover={totalTurnover}, '
            f'OpenPrice={openPrice}, '
            f'HighestPrice={highestPrice}, '
            f'LowestPrice={lowestPrice}, '
            f'ClosePrice={closePrice} '
            f'}}'
        )
    # 檢查數值是否有效
    def checkNumber(self, value):
        if value == '--':
            return None
        else:
            return value

def main():
    resp = requests.get(
        f'https://www.twse.com.tw/exchangeReport/MI_INDEX?' +
        f'response=json&' +
        f'type=ALLBUT0999' +
        f'&date={datetime.date.today():%Y%m%d}'
    )
    if resp.status_code != 200:
        loguru.logger.error('RESP: status code is not 200')
    loguru.logger.success('RESP: success')

    afterHoursInfos = []

    body = resp.json()
    stat = body['stat']
    if stat != 'OK':
        loguru.logger.error(f'RESP: body.stat error is {stat}.')
        return
    records = body['data9']
    for record in records:
        code = record[0].strip()
        if re.match(r'^[1-9][0-9][0-9][0-9]$', code) is not None:
            name = record[1].strip()
            totalShare = record[2].replace(',', '').strip()
            totalTurnover = record[4].replace(',', '').strip()
            openPrice = record[5].replace(',', '').strip()
            highestPrice = record[6].replace(',', '').strip()
            lowestPrice = record[7].replace(',', '').strip()
            closePrice = record[8].replace(',', '').strip()
            afterHoursInfo = AfterHoursInfo(
                code=code,
                name=name,
                totalShare=totalShare,
                totalTurnover=totalTurnover,
                openPrice=openPrice,
                highestPrice=highestPrice,
                lowestPrice=lowestPrice,
                closePrice=closePrice
            )
            afterHoursInfos.append(afterHoursInfo)

    message = os.linesep.join([
        str(afterHoursInfo)
        for afterHoursInfo in afterHoursInfos
    ])
    loguru.logger.info('AFTERHOURSINFOS' + os.linesep + message)

if __name__ == '__main__':
    loguru.logger.add(
        f'{datetime.date.today():%Y%m%d}.log',
        rotation='1 day',
        retention='7 days',
        level='DEBUG'
    )
    main()

重新執行得到輸出結果。
執行結果

Would You Like To Know More?

https://docs.python.org/3/library/json.html


團隊系列文:

CSScoke - 金魚都能懂的這個網頁畫面怎麼切 - 金魚都能懂了你還怕學不會嗎
Clarence - LINE bot 好好玩 30 天玩轉 LINE API
Hina Hina - 陣列大亂鬥
King Tzeng - IoT沒那麼難!新手用JavaScript入門做自己的玩具
Vita Ora - 好 Js 不學嗎 !? JavaScript 入門中的入門。
TaTaMo - 用Python開發的網頁不能放到Github上?Lektor說可以!!


上一篇
Day-06 資料蒐集:如何避免頻繁存取被封鎖
下一篇
Day-08 資料蒐集:取得個股每月各交易日盤後資訊 + 解析 CSV
系列文
Python 程式交易 30 天新手入門30

1 則留言

0
kyc1109
iT邦新手 4 級 ‧ 2019-10-02 17:05:04

跟著用最後一個範例,我遇到兩個錯誤,一個是要import os,另一個是試在__init__()中,少了self.在checkNumber的部分。

init(): #裡面有用到checkNumber的部分
self.TotalShare = checkNumber(totalShare)
#要改成
self.TotalShare = self.checkNumber(totalShare)

謝謝樓主的教學與分享。

阿斬 iT邦新手 5 級 ‧ 2019-10-06 16:27:34 檢舉

感謝找到程式碼錯誤之處,我馬上修正,謝謝您

我要留言

立即登入留言