去年 (2024) 8 月時我把使用 mplfinance 套件繪製 K 線圖的方法寫成一個自訂模組 kbar.py 以簡化繪圖程序, 打算將其打包成一個 Python 套件上傳到 PyPI 網站, 這樣就能夠方便地用 pip install 來安裝. 這幾天抽空準備打包發佈事宜, 但是在 Colab 上運行 kbar.py 卻發現無法順利顯示中文, 於是花了兩天利用 AI 進行改版作業, 終於得到一個全平台版的 kbar.py, 過程參考 :
以下的打包發佈便是以此全平台版的 kbar.py 為對象. 由於我過去未曾製作過這樣可供安裝的 Python 套件, 便搜尋谷歌找到下面這篇文章 :
但看完覺得似乎有點小複雜, 所以就轉而詢問 ChatGPT, 它給出的建議感覺比較好懂. 另外, 我從母校借到下面這本碁峰的書, 書末也有介紹將模組上傳 PyPI 的方法 :
# Python Bible 實戰聖經 (碁峰 2021) 第 16 章
我又問了 ChatGPT 如何發佈套見到 PyPI 網站, 它的作法與網路教學以及書裡面的介紹有點出入, 經過來回問 ChatGPT 發現現在 PyPI 有新版打包發佈方式 (PEP621), 書上與網路上的都是較舊的版本 (但也還能用). 以下的做法基本上是參考 ChatGPT 給出的建議, 依照最新的的 PEP621 打包標準來做 (用 pyproject.toml 完全取代 setup.py).
1. 註冊 PyPI 帳號 :
PyPI 是一個 Python 套件儲存庫網站, 用來發佈與管理 Python 第三方開放原始碼套件. 開發者可以將套件發布到 PyPI 供使用者利用 pip 指令下載並安裝到本機的 Python 執行環境中. 此外 PyPI 的套件管理功能可讓使用者查詢套件之版本歷史, 功能描述, 與相依套件等資訊.
按官網首頁右上方的 Signup 鈕 :
填寫 Email 與帳號等資料後按 Create Account 鈕即完成帳戶之創建 :
2. 編輯 kbar.py 模組添加說明字串 (docstring) :
撰寫說明字串只要提供原始碼請 ChatGPT 代勞即可, 不用自己花時間去寫, 提示詞如下 :
請為下面的 kbar.py 寫一個標準的 Python doc 註解範例, 也就是 help(kbar) 的標準輸出 :
# 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
生成結果如下 :
# kbar.py
"""
kbar - K 線圖繪製工具
======================
本模組提供簡單的介面,在 matplotlib 與 mplfinance 上繪製中文化的 K 線圖。
支援自動偵測、安裝中文字型,確保在 Linux、Colab 與 Windows 環境下
皆能正常顯示中文。
"""
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 字型。
Returns
-------
bool or None
成功安裝回傳 True;失敗或例外回傳 None。
"""
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
subprocess.run(['fc-cache', '-fv'], capture_output=True)
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():
"""
在 Google Colab 下載 NotoSansCJK-Regular.ttc 字型。
Returns
-------
str or None
成功下載或已存在回傳字型路徑;失敗回傳 None。
"""
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', '-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):
"""
註冊 Colab 下載的字型,回傳可用的字型名稱。
Parameters
----------
font_path : str
字型檔案的路徑。
Returns
-------
str or None
註冊成功回傳字型名稱;失敗回傳 None。
"""
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}
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():
"""
偵測系統中文字型,並在需要時自動安裝或註冊。
Returns
-------
str or None
偵測或安裝成功回傳字型名稱;未找到回傳 None。
"""
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):
"""
檢查指定字型是否存在,建立候選字型清單。
Parameters
----------
font : str
指定字型名稱。
Returns
-------
list[str]
可用的字型候選清單。
"""
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 字型")
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]}')
return candidates
else:
print('警告: 沒有找到合適的字型')
return ['DejaVu Sans']
class KBar:
"""
K 線圖繪製工具 (基於 mplfinance)
Parameters
----------
df : pandas.DataFrame
股票或資產的 OHLCV 資料。
font : str, optional
指定字型名稱;若 None 則自動偵測中文字型。
Examples
--------
>>> import pandas as pd
>>> from kbar import KBar
>>> df = pd.read_csv('stock.csv', index_col=0, parse_dates=True)
>>> k = KBar(df)
>>> k.addplot(df['Close'].rolling(5).mean(), color='blue', panel=0)
>>> k.plot(volume=True, mav=(5,10,20))
"""
def __init__(self, df, font=None):
self.df = df
self.addplots = []
self.font = font or detect_font()
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):
"""
新增技術指標圖層。
Parameters
----------
data : pandas.Series or numpy.ndarray
要繪製的數據。
**kwargs : dict
傳遞給 mplfinance.make_addplot 的其他參數。
"""
plot = mpf.make_addplot(data, **kwargs)
self.addplots.append(plot)
def plot(self, embedding=False, **kwargs):
"""
繪製 K 線圖。
Parameters
----------
embedding : bool, optional
是否內嵌顯示,預設 False。
**kwargs : dict
傳遞給 mplfinance.plot 的參數,例如 volume, mav, returnfig。
Returns
-------
None 或 (fig, axes)
若 returnfig=True 則回傳圖表物件。
"""
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
重新載入 kbar.py 用 help() 來檢視說明文件 :
>>> import kbar
將整個模組傳入 help() 會輸出模組的全部說明 :
>>> help(kbar)
Help on module kbar:
NAME
kbar
DESCRIPTION
kbar - K 線圖繪製工具
======================
本模組提供簡單的介面,在 matplotlib 與 mplfinance 上繪製中文化的 K 線圖。
支援自動偵測、安裝中文字型,確保在 Linux、Colab 與 Windows 環境下
皆能正常顯示中文。
CLASSES
builtins.object
KBar
class KBar(builtins.object)
| KBar(df, font=None)
|
| K 線圖繪製工具 (基於 mplfinance)
|
| Parameters
| ----------
| df : pandas.DataFrame
| 股票或資產的 OHLCV 資料。
| font : str, optional
| 指定字型名稱;若 None 則自動偵測中文字型。
|
| Examples
| --------
| >>> import pandas as pd
| >>> from kbar import KBar
| >>> df = pd.read_csv('stock.csv', index_col=0, parse_dates=True)
| >>> k = KBar(df)
| >>> k.addplot(df['Close'].rolling(5).mean(), color='blue', panel=0)
| >>> k.plot(volume=True, mav=(5,10,20))
|
| Methods defined here:
|
| __init__(self, df, font=None)
| Initialize self. See help(type(self)) for accurate signature.
|
| addplot(self, data, **kwargs)
| 新增技術指標圖層。
|
| Parameters
| ----------
| data : pandas.Series or numpy.ndarray
| 要繪製的數據。
| **kwargs : dict
| 傳遞給 mplfinance.make_addplot 的其他參數。
|
| plot(self, embedding=False, **kwargs)
| 繪製 K 線圖。
|
| Parameters
| ----------
| embedding : bool, optional
| 是否內嵌顯示,預設 False。
| **kwargs : dict
| 傳遞給 mplfinance.plot 的參數,例如 volume, mav, returnfig。
|
| Returns
| -------
| None 或 (fig, axes)
| 若 returnfig=True 則回傳圖表物件。
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables
|
| __weakref__
| list of weak references to the object
FUNCTIONS
check_font(font)
檢查指定字型是否存在,建立候選字型清單。
Parameters
----------
font : str
指定字型名稱。
Returns
-------
list[str]
可用的字型候選清單。
detect_font()
偵測系統中文字型,並在需要時自動安裝或註冊。
Returns
-------
str or None
偵測或安裝成功回傳字型名稱;未找到回傳 None。
download_noto_cjk_colab()
在 Google Colab 下載 NotoSansCJK-Regular.ttc 字型。
Returns
-------
str or None
成功下載或已存在回傳字型路徑;失敗回傳 None。
install_noto_cjk_linux()
在 Linux (Ubuntu/樹莓派) 安裝 Noto CJK 字型。
Returns
-------
bool or None
成功安裝回傳 True;失敗或例外回傳 None。
register_font_colab(font_path)
註冊 Colab 下載的字型,回傳可用的字型名稱。
Parameters
----------
font_path : str
字型檔案的路徑。
Returns
-------
str or None
註冊成功回傳字型名稱;失敗回傳 None。
顯示函式 addplot() 的說明 :
>>> help(kbar.KBar.addplot)
Help on function addplot in module kbar:
addplot(self, data, **kwargs)
新增技術指標圖層。
Parameters
----------
data : pandas.Series or numpy.ndarray
要繪製的數據。
**kwargs : dict
傳遞給 mplfinance.make_addplot 的其他參數。
顯示函式 plot() 的說明 :
>>> help(kbar.KBar.plot)
Help on function plot in module kbar:
plot(self, embedding=False, **kwargs)
繪製 K 線圖。
Parameters
----------
embedding : bool, optional
是否內嵌顯示,預設 False。
**kwargs : dict
傳遞給 mplfinance.plot 的參數,例如 volume, mav, returnfig。
Returns
-------
None 或 (fig, axes)
若 returnfig=True 則回傳圖表物件。
這樣有說明文件的 kbar.py 就準備好了.
3. 配置檔案目錄結構 :
首先建立一個 PyPI 目錄來存放所有 PyPI 專案, 我的套件名稱是 kbar, 所以就先在 PyPI 下建立一個 kbar 專案目錄, 然後建立下面三個空檔案 (後續會編輯這三個檔案之內容) :
- README.md : 讀我檔
- LICENCE : 授權聲明 (注意無副檔名)
- pyproject.toml : 打包設定檔
接下來在專案目錄 kbar 下建立一個 src 子目錄與一個 kbar 孫目錄, 把上面已經加上說明字串的 kbar.py 複製到此 kbar 孫目錄下, 並且建立一個 __init__.py 檔案以符合 Python 類別的要求 (讓 kbar 孫目錄被視為一個套件), 編輯此 __init__.py 檔, 輸入如下內容後存檔 :
from .kbar import KBar, detect_font
__all__ = ["KBar", "detect_font"]
這主要是為了讓使用者能夠直接寫 from kbar import KBar 匯入類別 (即直接把 kbar.py 裡的 KBar 類別匯入到套件的命名空間內), 而不用管模組的內部結構 (否則就必須寫 from kbar.kbar import KBar 了), 如此不僅可讓套件的 API 更簡潔, 也可以讓 unittest 正常執行. 設定 __all__ 則是限定了套件之公開 API, 避免外部使用者直接 import 內部工具函式以維持乾淨的介面. 其中 detect_font 是為了 unittest 而添加的.
最後在 kbar 專案目錄下建立一個 tests 子目錄, 底下先建立兩個空檔案 __init__.py 與 test_kbar.py (後續要編輯內容), 這樣就完成套件檔案目錄結構之配置了. 用 tree 指令列出目前結構如下 :
D:\pypi>tree kbar /f
列出磁碟區 新增磁碟區 的資料夾 PATH
磁碟區序號為 1258-16B8
D:\PYPI\KBAR
│ LICENSE
│ pyproject.toml
│ README.md
├─src
│ └─kbar
│ │ kbar.py
│ └─ __init__.py
└─tests
│ test_kbar.py
└─__init__.py
目前除了 kbar.py 以外, 其他均為空檔案.
關於 tree 指令用法參考 :


沒有留言 :
張貼留言