2025年8月17日 星期日

Python 學習筆記 : K 線圖模組 kbar.py 改版 (全平台)

早上完成 kbar.py 程式改版, 經測試確認可在 Windows 與 Colab 平台上順利繪製有中文的 K 線圖後才匆匆出門去市集買菜. 中午飯後繼續趕工, 看看能否在其他 Linux 主機 (我手上只有 Mapleboard 的 Ubuntu Mate 與樹莓派 Rasbian) 上繪製有中文的 K 線圖. 雖然 Colab 的虛擬機也是 Linux, 但它與一般 Linux 在中文字型安裝上不同, 必須另外處理. 

我繼續與 Claude 協作, 但這次它有點秀逗, 先後測試了 Claude 因應 Linux 環境而改寫的兩款程式碼都失敗, 連在 Colab 上跑都倒退無法顯示中文, 由於程式碼比先前 Windows + Colab 可運作版還複雜, 實在不想一行一行檢查哪裡出錯, 乾脆回 ChatGPT, 把前一篇的 Windows + Colab 可運作版程式碼丟給它, 請它在此基礎上添加可在一般 Linux 平台順利執行的 kbar.py 程式碼 : 

# kbar.py
import mplfinance as mpf
import matplotlib as mpl
from matplotlib import font_manager
import os
import sys
import subprocess

def install_noto_cjk_linux():  # 在 Linux (Ubuntu/樹莓派) 安裝 Noto CJK 字體
    try:
        print('偵測到 Linux 環境,嘗試安裝 Noto CJK 字體...')
        result=subprocess.run(
            ['sudo', 'apt-get', 'install', '-y', 'fonts-noto-cjk'],
            capture_output=True,
            text=True
            )
        if result.returncode != 0:
            print(f'字體安裝失敗: {result.stderr}')
            return None
        # 更新 font cache
        subprocess.run(['fc-cache', '-fv'], capture_output=True)
        # 強制刷新 matplotlib 字體快取
        font_manager._load_fontmanager(try_read_cache=False)
        print('Noto CJK 字體安裝完成')
        return True
    except Exception as e:
        print(f'安裝過程出錯: {e}')
        return None

def download_noto_cjk_colab():  # 下載中文字體到 Colab 環境
    font_path='/content/NotoSansCJK-Regular.ttc'
    if not os.path.exists(font_path):
        print("正在下載中文字體...")
        url='https://github.com/googlefonts/noto-cjk/raw/main/' +\
            'Sans/OTC/NotoSansCJK-Regular.ttc'
        try:            
            result=subprocess.run(  # 使用 wget 下載字體
                ['wget', '-O', font_path, url],
                capture_output=True,
                text=True
                )            
            if result.returncode != 0:
                print(f'下載失敗: {result.stderr}')
                return None            
            print('中文字體下載完成')
            return font_path
        except Exception as e:
            print(f'字體下載失敗: {e}')
            return None
    else:
        print('字體檔案已存在')
        return font_path

def register_font_colab(font_path):  # 註冊字體並回傳可用的字體名稱
    if not font_path or not os.path.exists(font_path):
        return None    
    try:  # 註冊字體       
        font_manager.fontManager.addfont(font_path)        
        # 重建字體管理器快取
        font_manager._load_fontmanager(try_read_cache=False)        
        # 檢查註冊後可用的字體名稱
        fonts={f.name for f in font_manager.fontManager.ttflist}        
        # Noto Sans CJK 可能的名稱變化
        possible_names=[
            'Noto Sans CJK TC',
            'Noto Sans CJK JP', 
            'Noto Sans CJK SC',
            'Noto Sans CJK KR',
            'Noto Sans CJK'
            ]
        for name in possible_names:
            if name in fonts:
                print(f'成功註冊字體: {name}')
                return name
        print(f"字體註冊後可用名稱: {[f for f in fonts if 'Noto' in f or 'CJK' in f]}")
        return None
    except Exception as e:
        print(f"字體註冊失敗: {e}")
        return None

def detect_font():  # 偵測系統中是否有常見的中文字型 
    fonts={f.name for f in font_manager.fontManager.ttflist}
    # 檢查常見中文字體
    linux_chinese_fonts=['Noto Sans CJK TC','Noto Sans CJK JP','Noto Sans CJK SC']
    if 'Microsoft JhengHei' in fonts:
        return 'Microsoft JhengHei'
    elif 'PingFang TC' in fonts:
        return 'PingFang TC'
    elif any(name in fonts for name in linux_chinese_fonts):
        return next(name for name in linux_chinese_fonts if name in fonts)
    else:  # 檢測環境        
        in_colab='google.colab' in sys.modules
        in_linux=sys.platform.startswith('linux')
        if in_colab:
            print('Colab 環境偵測到,正在下載並設定中文字體...')
            font_path=download_noto_cjk_colab()
            if font_path:
                return register_font_colab(font_path)
        elif in_linux:
            print('Linux 環境偵測到,嘗試安裝 Noto CJK 字體...')
            if install_noto_cjk_linux():
                # 再檢查一次
                fonts={f.name for f in font_manager.fontManager.ttflist}
                for name in linux_chinese_fonts:
                    if name in fonts:
                        return name
        return None

def check_font(font):  # 檢查指定字型是否存在並回傳候選清單
    fonts={f.name for f in font_manager.fontManager.ttflist}
    candidates=[]    
    if font and font in fonts:
        candidates.append(font)
        print(f'使用指定字體: {font}')
    elif font:
        print(f"[警告] 找不到字型 '{font}',將使用 fallback 字型")
    # 通用 fallback 清單
    fallbacks=[  
        'Microsoft JhengHei',
        'PingFang TC',
        'Noto Sans CJK TC',
        'Noto Sans CJK JP',
        'Noto Sans CJK SC',
        'DejaVu Sans',
        'Liberation Sans',
        'Arial',
        'sans-serif'
        ]    
    for fallback in fallbacks:
        if fallback in fonts and fallback not in candidates:
            candidates.append(fallback)
    if candidates:
        print(f'字體候選清單: {candidates[:3]}')  # 只顯示前3個
        return candidates
    else:
        print('警告: 沒有找到合適的字體')
        return ['DejaVu Sans']

class KBar():
    def __init__(self, df, font=None):
        self.df=df
        self.addplots=[]
        self.font=font or detect_font()
        # 設定 matplotlib 全域參數
        if self.font:
            # 清除舊的字體設定
            if 'font.sans-serif' in mpl.rcParams:
                current_fonts=mpl.rcParams['font.sans-serif'].copy()
                # 移除可能造成問題的字體
                current_fonts=[f for f in current_fonts if f not in ['SimHei']]
                mpl.rcParams['font.sans-serif']=[self.font] + current_fonts
            else:
                mpl.rcParams['font.sans-serif']=[self.font, 'DejaVu Sans']
            mpl.rcParams['axes.unicode_minus']=False
            print(f'設定字體為: {self.font}')
        else:
            print('警告: 未找到中文字體,中文可能無法正常顯示')
    
    def addplot(self, data, **kwargs):
        plot=mpf.make_addplot(data, **kwargs)
        self.addplots.append(plot)
    
    def plot(self, embedding=False, **kwargs):
        color=mpf.make_marketcolors(
            up='red',
            down='green',
            inherit=True
            )   
        # 取得字體候選清單
        font_candidates=check_font(self.font)
        style=mpf.make_mpf_style(
            base_mpf_style='default',
            marketcolors=color,
            rc={
                'font.family': font_candidates,
                'font.sans-serif': font_candidates,
                'axes.unicode_minus': False
                }
            )  
        kwargs['type']='candle'
        kwargs['style']=style
        kwargs['addplot']=self.addplots
        result=mpf.plot(self.df, **kwargs)
        if kwargs.get('returnfig', False):
            return result

可見它並沒有大幅改寫程式架構, 僅新增了一個 Linux 平台下載與安裝中文字型的函式 install_noto_cjk_linux(), 以及修改了一部分的 detect_font() 函式內容以納入 Linux 平台的偵測程式碼而已. 為了較好地區別函式功能, 我把前一版本 (Windows + Colab) 中的 register_font() 改名為 register_font_colab(); 把 download_chinese_font() 函式改名為 download_noto_cjk_colab(). 其他函式都沒變. 

先在 Windows 與 Colab 平台上測試此全平台版的 kbar.py, 結果與前一版本相同, 都能正常在 K 線圖上顯示中文, 在此略過. 接下來便是重頭戲, 要到 Linux 主機上測試, 我的 Mapleboard 系統為 Ubuntu Mate, 之前尚未安裝過 yfinance 與 mplfinance, 所以先安裝此二套件 :

安裝 yfinance : 

tony1966@LX2438:~/python$ pip3 install yfinance    
Defaulting to user installation because normal site-packages is not writeable
Collecting yfinance
  Downloading yfinance-0.2.65-py2.py3-none-any.whl (119 kB)
... (略) ...
Successfully built multitasking peewee
Installing collected packages: pytz, peewee, multitasking, pycparser, frozendict, beautifulsoup4, cffi, curl_cffi, yfinance
Successfully installed beautifulsoup4-4.13.4 cffi-1.17.1 curl_cffi-0.13.0 frozendict-2.4.6 multitasking-0.0.12 peewee-3.18.2 pycparser-2.22 pytz-2025.2 yfinance-0.2.65

安裝 mplfinance : 

tony1966@LX2438:~/python$ pip3 install mplfinance   
Defaulting to user installation because normal site-packages is not writeable
Collecting mplfinance
  Downloading mplfinance-0.12.10b0-py3-none-any.whl (75 kB)
... (略) ...
Installing collected packages: kiwisolver, fonttools, cycler, contourpy, matplotlib, mplfinance
Successfully installed contourpy-1.3.2 cycler-0.12.1 fonttools-4.59.1 kiwisolver-1.4.9 matplotlib-3.10.5 mplfinance-0.12.10b0

開啟 Thonny 於交談視窗輸入下列程式碼繪製 K 線圖 : 

from kbar import KBar   
import yfinance as yf   
df=yf.download('0050.TW', start='2024-07-01', end='2024-08-21', auto_adjust=False)   
df.columns=df.columns.map(lambda x: x[0])    # 改成舊版單層索引
kb=KBar(df)    # 未傳 font 參數使用預設字型
kb.plot(title='台灣五十(0050.TW)', volume=True)

>>> from kbar import KBar    
Matplotlib is building the font cache; this may take a moment.
>>> import yfinance as yf   
>>> df=yf.download('0050.TW', start='2024-07-01', end='2024-08-21', auto_adjust=False)    
[*********************100%***********************]  1 of 1 completed
>>> df.columns=df.columns.map(lambda x: x[0])     
>>> kb=KBar(df)    
設定字體為: Noto Sans CJK JP
>>> kb.plot(title='台灣五十(0050.TW)', volume=True)    
使用指定字體: Noto Sans CJK JP
字體候選清單: ['Noto Sans CJK JP', 'DejaVu Sans', 'Liberation Sans']

可見 Ubuntu Mate 下載安裝了 Noto Sans CJK 字型來顯示中文, 結果如下 : 




OK, kbar.py 模組的改版工作至此就完工啦! 可以進行套件打包發佈了. 

沒有留言 :