去年此時寫了一個方便繪製 K 線圖的模組 kbar.py 後一直想把它打包後發佈到 PyPI 網站分享, 但總是抽不出時間. 最近忙完 Mapleboard 的函式執行平台開發後, 終於騰出時間來研究打包與發佈套件的方法. 不過在測試時卻發現 kbar.py 模組存在一個臭蟲, 在 Colab 上使用它來畫 K 線圖時出現找不到微軟正黑體的警告訊息 :
WARNING:matplotlib.font_manager:findfont: Font family 'Microsoft JhengHei' not found.
詢問 ChatGPT 才知道, 原來 Colab 虛擬機根本就沒有中文字型可用, 遑論微軟正黑體. 之前寫好的 kbar.py 當時是在 Windows 下測試開發的, 所謂的臭蟲是指我在程式碼中把字型寫死為微軟正黑體, 這在 Windows 上跑沒問題 (正黑體是系統內建的) :
font={'font.family': 'Microsoft JhengHei'}
把 kbar.py 用下列程式碼上傳到 Colab :
from google.colab import files
uploaded=files.upload()
先用下列指令安裝要用到的 yfinance 與 mplfinance 套件 (Colab 沒有預載) :
!pip install mplfinance yfinance
這樣就可以用下列程式碼來畫 K 線圖了 :
import yfinance as yf
from kbar import KBar
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)
kb.plot(volume=True)
K 線圖是畫出來了, 但是中文部分變成怪碼 :
而且會出現找不到正黑體字型 'Microsoft JhengHei' 的警告 :
抄錄這個 Windows 專用的 kbar.py 程式碼如下 :
# kbar.py (only for Windows)
import mplfinance as mpf
class KBar():
def __init__(self, df):
self.df=df
self.addplots=[]
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={'font.family': 'Microsoft JhengHei'}
style=mpf.make_mpf_style(base_mpf_style='default',
marketcolors=color,
rc=font)
kwargs['type']='candle'
kwargs['style']=style
kwargs['addplot']=self.addplots
if 'returnfig' in kwargs and kwargs['returnfig'] is True:
fig, axes=mpf.plot(self.df, **kwargs)
return fig, axes
else:
mpf.plot(self.df, **kwargs)
此警告出現的原因是因為在 plot() 中硬性指定使用微軟正黑體字型, 這在 Windows 機器上因為是內建字型完全沒有問題, 但在 Colab 虛擬機器上就會因為找不到正黑體字型而出現警告 (但 K 線圖還是正常繪製, 只是中文標題與軸標籤會變成怪碼).
以下分兩步驟進行改版, 首先是移除寫死的正黑體, 改成在建立 KBar 物件時可傳入 font 參數指定要使用的字型, 當找不到指定字型 (例如名稱拼錯) 時就使用退回清單中的字型 :
一. kbar.py 改版 A (可指定字型) :
修正後的程式碼如下 :
# kbar.py (new version 1)
import mplfinance as mpf
import matplotlib as mpl
from matplotlib import font_manager
def detect_font(): # 偵測系統中是否有常見的中文字型
fonts={f.name for f in font_manager.fontManager.ttflist}
if 'Microsoft JhengHei' in fonts: # Windows 內建中文字型 (正黑)
return 'Microsoft JhengHei'
elif 'PingFang TC' in fonts: # macOS 內建中文字型
return 'PingFang TC'
elif 'Noto Sans CJK TC' in fonts: # Linux/Colab 常用中文字型
return 'Noto Sans CJK TC'
else: # 沒有找到常見中文字型傳回 None
return None # 交給 fallback 處理
def check_font(font): # 檢查指定字型是否存在
fonts={f.name for f in font_manager.fontManager.ttflist}
candidates=[] # 儲存可用字型清單
if font and font in fonts: # 指定的字型存在
candidates.append(font)
elif font: # 指定字型不存在
print(f"[警告] 找不到字型 '{font}',將使用 fallback 字型")
# 通用 fallback 清單 (先放中文字型再放英文字型)
fallbacks=[
'Microsoft JhengHei', # Windows 中文字型
'PingFang TC', # macOS 中文字型
'Noto Sans CJK TC', # Linux 中文字型
'Arial', # 英文字型
'Liberation Sans', # 英文字型
'DejaVu Sans', # 英文字型
'sans-serif' # 英文字型
]
for fallback in fallbacks:
if fallback in fonts and fallback not in candidates:
candidates.append(fallback)
return candidates or None
class KBar():
def __init__(self, df, font=None):
self.df=df # 儲存 OHLCV DataFrame (開高低收成交量)
self.addplots=[] # 儲存額外副圖的串列
self.font=font or detect_font() # 若未指定字型則自動偵測
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
)
style=mpf.make_mpf_style( # 自訂 K 線圖風格
base_mpf_style='default', # 指定預設風格
marketcolors=color, # 指定配色
rc={'font.family': check_font(self.font)}
)
kwargs['type']='candle' # 預設使用蠟燭圖
kwargs['style']=style # 套用自訂風格
kwargs['addplot']=self.addplots # 加入副圖設定
result=mpf.plot(self.df, **kwargs) # 繪製 K 線圖
if kwargs.get('returnfig', False): # 是否回傳 Figure 與 Axes
return result # 回傳繪圖物件 fig, axes
主要的更改之處摘要說明如下 :
- KBar 類別建構式增加了一個 font 參數用來指定字型, 在呼叫 KBar() 建立 KBar 物件時可以傳入 font 指定字型, 所以在初始化函式 __init__() 裡添加了 font 參數來接收建構式傳來的指定字型, 若未指定則呼叫 detect_font() 函式取得常用中文字型, 結果儲存在 KBar 物件的 font 屬性裡.
- 新增 detect_font() 函式用來偵測作業系統中是否有常見的中文字型, 傳回各系統常見的內建中文字型, 如果都沒有就傳回 None 進入退回模式.
- 新增 check_font() 函式, 在呼叫 mplfinance 的 make_mpf_style() 設定繪圖風格, 用 rc 參數設定字型時會呼叫此函式並傳入 KBar 物件的 font 屬性, 它儲存了物件初始化時偵測出來的中文字型或呼叫者指定的字型名稱, 如果初始化時已偵測出中文字型, 那它會被放在可用字型清單的第一個元素, 後面可能跟著測試出來的系統常見之退回字型 (英文), rc 參數的 font-family 鍵接受一個字型清單, matplotlib 會自動依序調用裡面下一個的字型.
- 修改呼叫 mpf.make_mpf_style() 傳回之自訂 K 線圖風格變數 style 的 rc 參數, 不再硬性指定正黑體 (這正是 warning 的原因), 而是由 KBar 物件的 font 屬性取得.
先在 Windows 上測試 :
>>> from kbar import KBar
>>> 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) # 未傳 font 參數預設使用正黑體
>>> kb.plot(title='台灣五十(0050.TW)', volume=True)
結果會使用內建正黑體顯示標題與軸標籤等文字 :
然後刪除 KBar 物件, 重新呼叫 KBar() 並傳入新細明體字型名稱 MingLiU :
>>> del kb
>>> kb=KBar(df, font='MingLiU')
>>> kb.plot(title='台灣五十(0050.TW)', volume=True)
結果文字果然都變成了新細明體 :
接下來指定一個不存在的字型 "PMingLiU", 這樣會因為找不到此字型而進入 fallback (退回) 模式, 在呼叫 check_font() 時從一組各系統常見中英文字型組成的退回清單中找尋系統有支援的字型組成串列來退回, 在 Windows 上會選擇第一個元素正黑體字型 :
>>> kb=KBar(df, font='PMingLiU')
>>> kb.plot(title='台灣五十(0050.TW)', volume=True)
[警告] 找不到字型 'PMingLiU',將使用 fallback 字型
結果如下 :
可見傳入不存在的字型會以退回字型顯示, 在 Windows 會退回改用正黑體.
但是這次的 kbar.py 改版只是把字型從寫死的正黑體中解放出來而已, 並沒有解決在 Colab 上無法顯示中文的問題.
我向 ChatGPT 尋求 Colab 上的解決方案, 但花了半天做了許多嘗試仍然失敗, 於是轉而向 Claude 求解, 它給出的改版作法是偵測是否在 Colab 執行環境, 是的話就下載中文字型來跑, Claude 果然是 coding 之王者, 解決了 Colab 上的中文顯示問題.
二. kbar.py 改版 B (Colab 上可顯示中文) :
經 Cloude 優化改版後的程式碼如下 :
# kbar.py
import mplfinance as mpf
import matplotlib as mpl
from matplotlib import font_manager
import os
import sys
import subprocess
def download_chinese_font(): # 下載中文字體到 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(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}
# 檢查常見的中文字體
if 'Microsoft JhengHei' in fonts:
return 'Microsoft JhengHei'
elif 'PingFang TC' in fonts:
return 'PingFang TC'
elif 'Noto Sans CJK TC' in fonts:
return 'Noto Sans CJK TC'
elif 'Noto Sans CJK JP' in fonts:
return 'Noto Sans CJK JP'
elif 'Noto Sans CJK SC' in fonts:
return 'Noto Sans CJK SC'
else: # 檢測是否在 Colab 環境
in_colab='google.colab' in sys.modules
if in_colab:
print('Colab 環境偵測到,正在設定中文字體...')
font_path=download_chinese_font()
if font_path:
return register_font(font_path)
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
摘要說明如下 :
- 新增了專為 Colab 而設的 download_chinese_font() 與 register_font() 函式, 並且修改 detect_font() 函式, 增加偵測執行環境是否為 Colab 的程式碼, KBar 物件初始化時會呼叫 detect_font(), 若偵測到 Colab 環境就呼叫 download_chinese_font() 與 register_font() 函式去下載與註冊 NotoSansCJK 中文字型.
- check_font() 中的退回字型清單中添加了幾個 NotoSansCJK 相關字型, 當指定的字型不存在時 Matplotlib 就會在退回字型清單中找尋可用之中文字型.
此新版 kbar 在 Windows 下測試均與上面改版 1 結果相同不另貼圖, 從下列執行過程可知程式會顯示設定之字型與退回時所使用之字型 :
>>> from kbar import KBar
>>> 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) # 未傳 font 參數預設使用正黑體
設定字體為: Microsoft JhengHei
>>> kb.plot(title='台灣五十(0050.TW)', volume=True)
使用指定字體: Microsoft JhengHei
字體候選清單: ['Microsoft JhengHei', 'DejaVu Sans', 'Arial']
刪除 KBar 物件, 重建物件時傳入新細明體 font='MingLiU' 測試結果為顯示新細明體 :
>>> del kb
>>> kb=KBar(df, font='MingLiU')
設定字體為: MingLiU
>>> kb.plot(title='台灣五十(0050.TW)', volume=True)
使用指定字體: MingLiU
字體候選清單: ['MingLiU', 'Microsoft JhengHei', 'DejaVu Sans']
接下來指定使用不存在的字型 'PMingLiU', 會退回使用候選清單中的正黑體 :
>>> del kb
>>> kb=KBar(df, font='PMingLiU')
設定字體為: PMingLiU
>>> kb.plot(title='台灣五十(0050.TW)', volume=True)
[警告] 找不到字型 'PMingLiU',將使用 fallback 字型
字體候選清單: ['Microsoft JhengHei', 'DejaVu Sans', 'Arial']
可見在 Windows 上執行結果與上面改版 1 相同.
重頭戲是在 Colab, 將此新版 kbar.py 上傳到 Colab, 重新啟動工作階段 :
再次繪製 K 線圖成功顯示中文字型 :
Claude 再次幫了我大忙節省了不少時間, 讚!








沒有留言 :
張貼留言