⛳

【ポートフォリオ 03】MarkdownファイルをHTMLに変換するCLIツールを作ってみた

に公開

はじめに

前回・前々回ポートフォリオに続き、Pythonを使いながらコマンドラインから指定されたファイルに対して、ファイル操作を行なうメソッドを実装しました。

※前回記事の参考: https://qiita.com/mabo23/items/4881206cfa5b658bb8b3

※前々回記事の参考: https://qiita.com/mabo23/items/b0c5fcf4e2bc35348b7f

この記事では、学習のアウトプットとして実装した「Markdown to HTML Program」を紹介します。

課題概要

本課題は、プログラミング学習サイト「RecursionCS」にて出題された演習問題をもとにしています。

【課題】

  • タスクはマークダウンを HTML に変換するプログラムを作成すること

  • シェルを通して python3 file-converter.py markdown inputfile outputfile というコマンドを実行させること

  • markdown: 実行するコマンド

  • inputfile: .md ファイルへのパス、変換元の Markdown ファイルのパス

  • outputfile: プログラムを実行した後に作成される .html ファイルへのパス、変換後の HTML ファイルの保存先を指定するもの

【要件】

プログラムの目的

  • Markdown 形式のファイル(.md)を HTML 形式のファイル(.html)へ変換する

実行方法

  • 以下の形式でシェルからコマンド実行できること
実行コマンド
python3 file-converter.py markdown inputfile outputfile

引数の仕様

  • 第1引数:markdown(固定コマンド名として認識される)

  • 第2引数:変換元となる Markdown ファイルのパス(.md 拡張子)

  • 第3引数:出力先の HTML ファイルのパス(.html 拡張子)

入力ファイルの条件

  • ファイルが存在していること
  • .md 拡張子であること

出力ファイルの条件

  • .html 拡張子であること
  • 存在しない場合は新規作成、存在する場合は上書き

変換処理の仕様

  • Python モジュール markdown を使って Markdown を HTML に変換する
  • 拡張機能として 'extra', 'codehilite', 'toc', 'nl2br' を使用する

エラー処理

  • 入力ファイルが存在しない、もしくは Markdown ファイルでない場合はエラーメッセージを表示して終了
  • 出力ファイルの指定が不正な場合も同様に終了(空や拡張子が .html でない)

完了メッセージ

  • 変換が正常に完了した場合、メッセージを表示する

開発環境

  • 使用PC:MacBook Pro2(2023年モデル)
  • OS:macOS Sequoia 15.5
  • Pythonバージョン:3.13.5(Homebrewでインストール)
  • エディタ:Visual Studio Code(VSCode)
  • 実行:ターミナル上で python3 file-converter.py markdown inputfile outputfile コマンドを使用

目的

  • sysモジュールを使ったコマンドラインプログラムを作成すること
  • 仮想環境の構築方法を学び、実践すること
  • 標準ライブラリ・外部ライブラリの違いを知り、インストール・管理する方法を学ぶこと
  • プロセス生成モデルについて理解すること
  • ファイル操作を行ない、ファイルの読み書きができるようになること

仮説

実装に必要なステップ

1. コマンドライン引数のチェック

  • 数
  • 形式

2. ファイルパスと形式のチェック

  • inputfile の存在確認
  • inputfile の形式が .md かどうか確認
  • outputfile の存在確認
  • outputfile の形式が .html かどうか確認

3. Markdownファイルの読み込み

  • 読み取った .md ファイルの中身を変数に格納

4. Markdown → HTML 変換

  • markdownモジュールをつかって、変数の中身をHTML形式に変換する

5. HTMLファイルとして出力

  • 4.で変換した内容を出力ファイルに書き込み

6. 成功メッセージを出力

検証

仮説の内容を実際に関数としてまとめ、組み合わせることで、概ね期待通りの結果を得ることができた。

ただし、以下の点は仮説設定時点では抜け落ちていました。

1. コマンドライン引数の数のチェック

有効なコマンドライン引数かどうかをチェックするロジックにおいて、len(sys.argv)を使用して適切な数がコマンドラインで使用されているか、チェックすることを失念していました。

2. 空白チェック

コマンドライン引数に不要なスペースが混在している可能性を考慮していませんでした。

3. return f.write(content)は不適切

HTML形式にファイルの中身を変換した後、その内容を新たにファイルに書き込むため、ファイル書き込みのための関数を用意しましたが、戻り値の設定をしていました。

f.write(content) は書き込んだ バイト数(文字数)を返すため、return によって書き込んだバイト数が返されてしまうという結果になってしまいました。

そのため、returnを削除し、ファイルに書き込む操作で処理を終了するロジックに変更しました。

実装で気をつけたこと

関数の責務の分離をできるだけ細かくしたこと

前回のポートフォリオ「File Manipulator Program」を実装した際の課題はバリデーション関数の中で複数の処理をしていたことです。

  1. 検証(整数か?正の数か?)
  2. 変換(str → int)
  3. 値の返却(使える形式の値を返す)

今回は、1関数1目的になるよう、できるだけ処理を細かく分割し、再利用性と保守性を意識しました。

例えば、以下のように、inputfileのパスの存在確認・.md拡張子がついているかのチェック・inputfileのバリデーションをそれぞれ分割して関数化し、read_markdown_file()でロジックを組み合わせて実装しました。

実装例
# 改善前の実装内容
def is_valid_inputfile_path():
    return len(sys.argv) > 2 and os.path.exists(sys.argv[2])

def is_inputfile_markdown():
    return len(sys.argv) > 2 and sys.argv[2].strip().lower().endswith(".md")

def read_markdown_file():
    if not is_valid_inputfile_path():
        print("This file does not exist.")
        sys.exit(1)

    if not is_inputfile_markdown():
        print("This file is not a markdown file.")
        sys.exit(1)
        
    return read_file(sys.argv[2])
    

# 改善後の実装内容
def validate_inputfile():
    if not is_valid_inputfile_path():
        print("This file does not exist.")
        sys.exit(1)

    if not is_inputfile_markdown():
        print("This file is not a markdown file.")
        sys.exit(1)

    return True

def read_markdown_file():
    # バリデーションを実施
    validate_inputfile()
    return read_file(sys.argv[2])

実装時のエラー

Pythonインタープリタの選択ミス

【内容】
仮想環境で開発中、VS Codeのインタープリタをおそらく誤押下してしまい、import markdownの部分に波線が発生
→ モジュールがないという警告が発生

【原因】
Pythonインタープリタが正しく選択されていないこと

【解決策】
新たに仮想環境を構築し、古い仮想環境は削除

学び

1. Python の markdown モジュール

Python の markdown モジュールには、標準的に処理される要素と、拡張機能を使わないと処理されない要素があることを知りました。

  • markdown モジュールは、デフォルトでは基本的な Markdown 構文だけを HTML に変換します。
    見出し (#)、段落、リスト、リンク、強調などは標準で処理されます。

  • それ以外の機能である、表(tables)、コードブロックのハイライト、目次、定義リストなどは、拡張機能を明示的に指定しないと処理されません。

そのため、拡張機能をリスト化して使用するようにしました。

拡張を複数使う場合
例
import markdown

extensions = ["tables", "fenced_code", "toc", "codehilite"]
html = markdown.markdown(markdown_text, extensions=extensions)

※ 拡張機能が増えるたびに markdown.markdown(...) を何度も書かなければならないわけではない

2. returnとsys.exit(1)の違い

比較項目 return sys.exit(1)
対象 関数の終了(呼び出し元に値を返す) プログラム全体の終了(実行を即時停止)
影響の範囲 関数のスコープ内で終了 Python スクリプト自体を終了させる
戻り値 任意の値を返せる(例:return "abc") 終了コード(int)を返すが、呼び出し元には返らない
使いどころ 値を返したいとき、関数内の条件分岐などに使う 明示的に処理を打ち切って終了させたいとき(エラー処理など)

※ ChatGPT解説より表を引用

3. 仮想環境の構築方法

以下、Qiita記事としてまとめました。

https://qiita.com/mabo23/items/c2cb995a98d7003d7bdd

4. 仮想環境の動作の仕組み

以下、Qiita記事としてまとめました。

https://qiita.com/mabo23/items/b3bf6316011236ea6d6b

5. 標準ライブラリと外部ライブラリの違い

以下、Qiita記事としてまとめました。

https://qiita.com/mabo23/items/c9c22f271dd9db56024a

6. プロセス生成モデル fork() → exec()

以下、Qiita記事としてまとめました。

https://qiita.com/mabo23/items/45b4838050a95903d10e

7. コマンド実行の仕組み

https://qiita.com/mabo23/items/8bf73985946c541572c4

次回に向けて

  • わからない箇所が出てきたら、何がわからないのかを紙やノートにメモし、客観視する
  • 何がわかれば、そのわからない箇所が解決できるか、仮説を立てて調べる
  • 実装時間を意識する

まとめ

今回のプログラム課題では、Python仮想環境の構築と仕組み、プロセスの動作フロー、コマンド実行の仕組み、ライブラリの種類などを学ぶことができました。
この理解があって、今何をしようとしていて、何がどういう仕組みで動くからPythonプログラムが処理されるのかを知り、目的をより理解することができたと思います。
関数を細かく分解するには、どんな処理を行なうと目的が実現できそうか、そのためには何が必要か、という逆算力がとても重要だと痛感したため、常にゴールや目的を意識していくよう心がけていきます。

最後までお読みいただき、ありがとうございました。

参考URL

https://python-markdown.github.io/reference/

https://qiita.com/masakuni-ito/items/593b9d753c44da61937b

https://dtnavi.tcdigital.jp/cat_system/language_184/

https://trends.codecamp.jp/blogs/media/column23

https://pythonguides.com/endswith-function-in-python-string/

https://docs.python.org/ja/3.13/tutorial/inputoutput.html#tut-files

Discussion