📝

ちょこちょこ作ってたmarkdown生成ライブラリを公開しました🐍

2023/10/09に公開

こんにちは.
最近論文を提出して研究にひと段落ついたので,実験しながらちょこちょこ作ってたPythonのMarkdown生成ライブラリを公開しました.

元々は,実験結果を研究室で共有するために,pythonから直接markdown形式の表を直感的に出力できるようにしようと思って作っていました,

作っているうちに,思ったよりいい感じになってきて,他に人でも役に立てるようなものになったんじゃないかと思ったので,綺麗にラッピングしてPyPIに上げてみました.

もしよければ使ってみてくれると嬉しいです.

mdfy

GitHub Repository

Read the Docs

PyPI

Installation

pip install mdfy

どんなやつ?

pythonで簡単かつ直感的にmarkdownを生成できます。
また,ファイルに書き込むだけでなく,単にmarkdown形式の文字列に変換することもできるModularityの高い設計になっています.

from mdfy import Mdfier, MdText, MdHeader, MdTable

contents = [
  MdHeader("Hello, MDFY!"),
  MdText("[Life:bold] is [like:italic] a bicycle."),
  MdTable(["head1": "content", "head2": "content"])
]
Mdfier("markdown.md").write(contents)

# もしくは,単純にそれぞれを文字列に変換できます.

print(MdText("[Life:bold] is [like:italic] a bicycle."))
# => '**Life** is *like* a bicycle.'

いくつか,特徴的な機能を紹介します.

MdText

markdownでは,さまざまテキスト装飾が可能です

これを,mdfyでは,string formatterに似せた構文で,スタイルの指定が可能です.

上の文章を,mdfyでは次のように表現できます.

MdText("[markdown:bold]では,[さまざま:quote]な[[テキスト装飾:st]が可能:bold]です[よ:not]ね")

実際には,これをstr()もしくは.to_str()すると,次のような文字列が得られるわけです.

**markdown**では,`さまざま`な*****テキスト装飾***が可能**です~~よ~~ね

markdownの文字列の装飾指定は入れ子で指定できるため,入れ子に対応しています.

Markdown Table

mdfyでは,dictのmarkdown化も容易です.
次の例は,同じkeyを持つ辞書のリストをtableにする例です.

data = [
    {"precision": 0.845, "Recall": 0.662},
    {"precision": 0.637, "Recall": 0.802},
    {"precision": 0.710, "Recall": 0.680},
]

print(MdTable(data))

# 次のような文字列になります.
# | precision | Recall |
# | --- | --- |
# | 0.845 | 0.662 |
# | 0.637 | 0.802 |
# | 0.71 | 0.68 |

また,transpose=Trueを指定することで,テーブルを転置し,キーを行に含めることができます.

print(MdTable(data, transpose=True))

# | Key | Value 0 | Value 1 | Value 2 |
# | --- | --- | --- | --- |
# | precision | 0.845 | 0.637 | 0.71 |
# | Recall | 0.662 | 0.802 | 0.68 |

# transposeする時は,ヘッダーを指定できます.
labels = ["Metrics", "Model 1", "Model 2", "Model 3"]
print(MdTable(data, transpose=True, labels=labels))

# | Metrics | Model 1 | Model 2 | Model 3 |
# | --- | --- | --- | --- |
# | precision | 0.845 | 0.637 | 0.71 |
# | Recall | 0.662 | 0.802 | 0.68 |

既存ライブラリと比べてどう良いの?

「でも,python から便利に markdown 生成できるライブラリなんて,ほかにもあるんじゃない?」

その通り.すでに有名な python パッケージとして「mdutils」というものがあります.

ではここで,mdutils の example を見てみましょう.

次のmarkdownをそれぞれのライブラリで出力する処理で比較してみます.

# はじめに
本書は Markdown の自動生成テストしたものです。mdutils を使用して作成しました。
# 各要素の例
## 箇条書き
- 項目1
    - 項目1.1
- 項目2
## イメージ
![写真](mdutils-test/pypi.svg)

# mdutils
from mdutils.mdutils import MdUtils

md_file = MdUtils(file_name='markdown')

md_file.new_header(level=1, title='はじめに')
md_file.new_line("本書は Markdown の自動生成テストしたものです。"
        "mdutils を使用して作成しました。")

md_file.new_header(level=1, title='各要素の例')

# 箇条書き
md_file.new_header(level=2, title='箇条書き')
md_file.new_list(["項目1",["項目1.1"],"項目2"])

image_path = "mdutils-test/pypi.svg"
md_file.new_header(level=2, title='イメージ')
md_file.new_line(md_file.new_inline_image(text='写真', path=image_path))

md_file.create_md_file()

mdfy を使うと,これがこうなります.

# mdfy
from mdfy import Mdfier, MdHeader, MdText, MdList, MdImage

contents = [
  MdHeader("はじめに"),
  MdText(
    "本書は Markdown の自動生成テストしたものです。"
    "mdutils を使用して作成しました。"
  ),
  MdHeader("各要素の例"),
  MdHeader("箇条書き", 2),
  MdList(["項目1",["項目1.1"],"項目2"]),
  MdHeader("イメージ", 2),
  MdImage("mdutils-test/pypi.svg", alt="写真")
]
Mdfier("markdown.mdfy.md").write(contents)

どうでしょうか?

mdutils では,MdUtilsのメソッドを呼び出していくことで,markdown の中身を記述していきます.

これは.md ファイルへの書き出しを強く意識した設計っぽいですね.

mdfy の方では,markdown を構成する要素がそれぞれMdElementという単位になっています.

このMdElementを積み重ねることで markdown の中身を記述できます.

そのため,実際の markdown を記述する感覚により近いと思いますし,コードを見てもどのような markdown が生成されるか,イメージしやすいのではないでしょうか?(願望)

list に積み重ねたMdElementを,最後にMdfier(filename).write()に放り込むことで,ファイルに書き出せます.

もちろん,mdutilsにできてmdfyにできないこともあります (table of contentsの生成など).
これらは,今の所僕自身がそれほど困ってないため実装されていないんですが,気が向くかmdfyの利用者が増えるか要望を頂いたら実装しようと思っています.

Plugin

markdownでの投稿に対応しているサービスはたくさんありますね.

zennもそうですし,GitHubやNotionも部分的に対応しています.

これらはしばしば独自の方言を持っていたり,画像を載せるにはアップロードが必要なことがあります.

mdfyは拡張しやすいので,これらの要望に対応した新たなクラスを作成し,簡単に接続することができます.

例えば,esa.ioというサービスのプラグインを紹介します.

https://github.com/argonism/mdfy-esa

GitHub Repository

pypi

このプラグインではEsaMdfierというクラスを実装しています.
EsaMdfierMdfierと同じように使えるんですが,esa.io記事を作成/更新できます.
このとき,MdImageやMdLinkでは,urlやsrcがlocalパスであるときに,esa.ioに自動的にアップロードしてくれます.
これで,ファイルのアップロードを意識することなく,直感的にmdfyを使用することができます.

mdfyとmdfy-esaを使って,必要なコードで記事を作成できます.

from mdfy import MdImage, MdLink, MdText
from mdfy_esa import EsaMdfier

esa_team = "your esa team name"
post_fullname = "post name as you like"
contents = [
    MdText("This is a test article."),
    MdImage(src="examples/test_image.png"),
    MdLink(url="examples/dummy.pdf"),
]

mdfier = EsaMdfier(post_fullname=post_fullname, esa_team=esa_team)
created_post_info = mdfier.write(contents=contents)

感想

個人的には今の所,便利でいい感じのものが作れたと思っています.
もともと作り始めが実験結果の共有だったので,dictを表に変換する部分の機能が充実気味です.

MdTextでは,スタイルの指定のパースのために,Larkという構文解析ライブラリを使って,構文解析しています.
最初はpythonの組み込みstring formatterを拡張して作ろうと思ったんですが,このformatterのパーサーが思ったよりシンプルなもので,ネストに対応していませんでした.
ネストは正規表現では表現できないので,構文解析器を使ってパースしてから,markdown化するようにしています.

今回のプロジェクトではCI周りにも気を使ってみました.

両方ともgithub actionsでpypiへのアップロードやテストをしており,mdfyではdispatch_workflowからバージョンの指定・releaseの文言を指定してpypiへアップロードします.

mdfy-esaでは,cookiecutterを使用して,waynerv/cookiecutter-pypackageを使用しました. このテンプレートでは,CHANGELOG.mdを用意して,tagがpushされたらそのtagのバージョンのCHANGELOGを読み,pipyにアップロードして,CHANGELOGの内容からリリースを書くという流れになっています.

色々やったことないこともできて,楽しかったです.
ちなみに,ロゴは目玉焼きの土星なんですが,stable diffusionでlogo生成向けのLoRAを使って生成しました.

mdfy,便利だと思うんだけど,流行らないかなぁ

Discussion