1 つのテンプレートを全員に使う

PythonBeginner
オンラインで実践に進む

はじめに

地域の夕食会では、来場者全員に招待状を用意する必要があります。CSV の名簿、JSON のイベント情報、用意された HTML デザインから、個別の招待状ファイルを新しいバッチに生成します。同姓同名の人にも別々のファイルが必要で、Zoë, "Z" & <Friends> のような名前は文字として表示しなければなりません。

Python の文字列、辞書、ループ、関数と、基本的なターミナル操作を前提とします。この実習では、ファイルの読み取り、CSV レコード、テンプレート、HTML テキストのエスケープ、新しい出力ディレクトリを説明します。CLI とページデザインは用意されており、実装するのはフィールドの対応付け、全件検証、来場者ごとの生成です。CSS の作成、メール送信、PDF 作成は扱いません。著者による所要時間の目安は、読解と通常のミスへの対処を含めて 55~75 分です。

最初の招待状を開く

このステップでは、通常の招待状を 1 枚生成し、ページの内容と入力フィールドを対応付けます。WebIDE で Terminal → New Terminal を開き、labex のシェルプロンプトが出るまで待ちます。ウィンドウが狭い場合、Terminal は More(...)内にあることがあります。すべてのコマンドをこのターミナルで実行してください。用意された作業領域は、以前の実習とは独立しています。

cd は現在のディレクトリを変更します。絶対パスは / から始まり、相対パスは現在のディレクトリを基準に解釈されます。移動後にファイルが見つからなくなった場合は、同じコマンドでここへ戻ります。

cd /home/labex/project/invitations

cat はテキストファイルを表示します。イベント情報を読みます。

cat event.json

JSON はオブジェクトを波括弧で囲み、文字列のキーと値を二重引用符で囲みます。この 4 つの値が招待状のタイトル、日付、時刻、会場になります。実行可能なサンプル one.py では、すでにこれらと 1 人分の情報をデザインに対応付けています。python3 は Python ファイルを実行します。新しい batches/first ディレクトリへサンプルを生成します。

python3 one.py

想定される出力:

Created batch: batches/first

プラットフォームの WebIDE タブから Web 8080 へ切り替えます。環境の準備時に招待状ビューアーは起動済みなので、Go Live は不要です。プレビューを更新し、first/g001.html を探します。Open invitation をクリックすると、実際の HTML ファイルが新しいブラウザータブで開きます。Maya Patel、The Long Table、時刻、会場を確認してください。そのブラウザータブを閉じると Web 8080 に戻ります。Download HTML は同じファイルを保存し、実習の外でも利用できるようにします。

Maya Patel 宛ての完成した招待状

あいさつ文に来場者名があり、下側の 2 つの領域にイベントの時刻と会場があります。

プレビューは batches に実在するファイルだけを読み、存在しない招待状を生成しません。以前の batches/archive は過去の成果として残されています。完成させるプログラムは、これらのファイルとすべての入力を維持しながら新しいバッチを作成します。次のステップへ進むため WebIDE に戻ってください。

CSV レコードを HTML テキストに対応付ける

このステップでは、再利用できるレンダラーが来場者の各値を正しいテキスト領域に入れるようにします。同じ WebIDE のターミナルと作業領域に戻ってください。以下の相対パスの基準は /home/labex/project/invitations です。

カンマで分割せずレコードとして読む

CSV は表形式で、最初の行にフィールド名があります。カンマはフィールドを区切りますが、引用符で囲まれたフィールド内のカンマは例外です。その中の二重引用符は、二重引用符を 2 つ並べて表します。そのため、split(",") ではすべての行を正しく読めません。用意された名簿を表示します。

cat guests.csv

Alex Chen のレコードが 2 件あり、guest_id は異なります。最後の人の名前にはカンマ、引用符、&、山括弧が含まれます。いずれも名前として有効な文字です。人を識別するのは安定した ID であり、表示名ではありません。

用意された generate.py がこれらのレコードを読み取ります。レンダラーを変更する前に読んでください。

cat generate.py

with ... as source はファイルを開き、インデントされたブロックを抜けたときに閉じます。encoding="utf-8" は Unicode 文字を保持します。newline="" はレコード境界の処理を CSV リーダーに任せます。csv.DictReader(source) はヘッダーを使って各レコードを辞書にし、list(...) はレコードをリストへ読み込みます。例えば guest["name"] は完全な名前であり、名前に含まれるカンマも残ります。json.loads(...) はイベントの JSON テキストを辞書に変換します。これらの読み取り処理と CLI は実装済みです。

CSV の引用符と HTML エスケープを分けて考える

HTML は <...> をタグ、&...; を文字参照として使います。CSV の引用符は表の構造を保護しますが、<Friends> のような名前が HTML として解釈されることは防ぎません。html.escape(value, quote=True) は特殊文字を文字参照に変え、ブラウザーが元の文字として表示できるようにします。ここではすべての値を HTML のテキスト領域へ入れます。任意の JavaScript、CSS、URL に値を挿入するための一般的な方法ではありません。

Template は invitation.html の $name、$title、その他の $field プレースホルダーを使います。substitute(mapping) は各プレースホルダーを対応する辞書の値で置き換え、対応が欠けていれば拒否します。本文の 6 つのマーカー、data-field="name"、message、title、date、time、venue は、意味に対応するテキスト領域を示します。それぞれを 1 つずつ残し、対応する文字を入れてください。提供された入力テンプレートは変更しません。生成された出力では、同等のタグ、配置、文字参照の表記を利用できます。ページの <title> だけに名前があっても、本文の名前領域を満たしたことにはなりません。

以下のレンダラーを作成します。cat > rendering.py はファイルを書き込み、それまでの内容を置き換えます。引用符付きのヒアドキュメント(here-document)区切り <<'PYCODE' は、終端の PYCODE までをそのままのテキストとして扱います。終端マーカーは単独の行に置きます。置き換えるのはレンダラーだけで、入力ファイルは変更しません。

辞書で来場者とイベントのフィールドを明示的に対応付けます。values.items() はキーと値のペアを取り出します。辞書内包表記 {key: ... for key, value in values.items()} はキーを保持し、各値をエスケープします。from html import escape は標準ライブラリの関数をインポートします。

cat > rendering.py <<'PYCODE'
from html import escape


def render_guest(guest, event, template):
    values = {"name": guest["name"], "message": guest["message"],
              "title": event["title"], "date": event["date"],
              "time": event["time"], "venue": event["venue"]}
    return template.substitute({key: escape(value, quote=True)
                                for key, value in values.items()})
PYCODE

この関数は HTML テキストを返し、ファイルは作成しません。ローカルの補助モジュールは、generate.py と同じ階層の .py ファイルにできます。パッケージのサブディレクトリは対象外です。置換前に 1 度だけエスケープするため、ブラウザーには &amp; という文字列ではなく & が表示されます。これでデザインと個別の文章を分離できました。

バッチ作成前に全員を検証する

このステップでは、以前の成果を保護しつつ、ID ごとにファイルを生成します。同じ WebIDE ターミナルで続けます。

元の batch.py は名前をファイル名にし、既存ディレクトリも許可します。2 人の Alex Chen が互いのファイルを上書きするおそれがあります。この短い関数を読みます。

cat batch.py

必要なのは、各行の <guest_id>.html を含む新しいバッチディレクトリです。ID に使える文字は ASCII の小文字、数字、ハイフンだけです。名前とメッセージはこの制限を受けない UTF-8 テキストです。来場者の必須フィールドは guest_id、name、message、イベントの必須フィールドは title、date、time、venue です。必須値が空または空白のみの場合、ID が重複する場合、出力ディレクトリが既存の場合は、役立つエラーを表示して拒否します。CSV、2 つのイベントファイル、テンプレート、batches/archive は変更しません。

準備と書き込みを分離する

先にすべてのレコードを検証し、すべてのページをレンダリングします。それが完了してからディレクトリを作成し、ファイルを書き込みます。そうしないと、最後のレコードに問題があるだけで、不完全なバッチが残りかねません。ここで扱う入力エラーには対応しますが、ディスク故障や突然の電源断に対するトランザクション保証ではありません。

isinstance(value, str) は値が文字列かどうかを調べます。event.get(field) はキーがなければ None を返し、分かりやすいエラーを出せます。strip() は空白以外の内容があるかを調べますが、後で表示する元のテキストは変更しません。raise ValueError(...) は無効な入力を報告します。用意された CLI が例外を捕捉し、Cannot generate invitations: ... を表示して失敗終了します。

set() は出現済みの ID を保存します。in は含まれているかを調べ、seen.add(...) は ID を記録します。all(...) はすべての文字が公開された文字ルールを満たすか確認します。pages.append((guest_id, page)) は ID とページのペアを保存します。最後の for guest_id, page in pages はそのペアを分解して受け取ります。

Path(output) は出力先ディレクトリを表します。mkdir() はそれを作成します。exist_ok=True を渡していないため、既存パスは拒否されます。/ 演算子はパスを結合します。f"{guest_id}.html" のような f-string は、ファイル名に ID を埋め込みます。write_text(..., encoding="utf-8") は完全な HTML テキストを UTF-8 で書き込みます。

batch.py だけを以下の中心処理に置き換えます。

cat > batch.py <<'PYCODE'
from pathlib import Path
from rendering import render_guest


def write_batch(guests, event, template, output):
    for field in ("title", "date", "time", "venue"):
        if not isinstance(event.get(field), str) or not event[field].strip():
            raise ValueError(f"Missing event field: {field}")
    if not guests:
        raise ValueError("The guest list is empty")
    seen = set()
    pages = []
    for guest in guests:
        for field in ("guest_id", "name", "message"):
            if not isinstance(guest.get(field), str) or not guest[field].strip():
                raise ValueError(f"Missing guest field: {field}")
        guest_id = guest["guest_id"]
        if not all(character in "abcdefghijklmnopqrstuvwxyz0123456789-" for character in guest_id):
            raise ValueError("Guest IDs must use lowercase letters, digits, or hyphens")
        if guest_id in seen:
            raise ValueError(f"Duplicate guest ID: {guest_id}")
        seen.add(guest_id)
        pages.append((guest_id, render_guest(guest, event, template)))
    output = Path(output)
    output.mkdir()  ## An existing batch is always refused.
    for guest_id, page in pages:
        (output / f"{guest_id}.html").write_text(page, encoding="utf-8")
PYCODE

from rendering import render_guest はローカルのレンダラーを使います。最初のループがイベントを確認し、次のループが来場者を検証してページを準備し、最後のループが完成したバッチを書き込みます。検証とレンダリングのループが終了するまでファイルは作られません。

CLI は 4 つの位置引数として、来場者 CSV、イベント JSON、HTML テンプレート、新しい出力ディレクトリを、この順で受け取ります。名簿全体で実行します。

python3 generate.py guests.csv event.json invitation.html batches/evening

想定される出力:

Created batch: batches/evening

ls はディレクトリ内の項目を表示します。

ls batches/evening

g101.html、g102.html、g103.html があるはずです。2 枚の Alex の招待状は別々の ID を持ちます。各ファイルには本人のメッセージが残り、3 枚目にはカンマ、引用符、Unicode、文字としての山括弧も残ります。

生成器を再利用して以前の成果を保つ

このステップでは、同じプログラムを別のイベントに使い、通常のエラーからの復旧を練習します。同じ WebIDE ターミナルで始めます。以下のコマンドはそれぞれ独立しているため、想定された失敗が起きても続けられます。

まず、完成済みの出力パスを意図的に再指定します。これは失敗するはずです。mkdir() が既存バッチを拒否します。メッセージは Cannot generate invitations: で始まり、既存ファイルに関するエラーが含まれます。以前のバッチは変更してはいけません。

python3 generate.py guests.csv event.json invitation.html batches/evening

用意された ID 重複の例を読みます。2 人目に 1 人目と同じ ID があります。

cat invalid-duplicate.csv

このファイルから新しいバッチを作成してみます。これも想定された失敗であり、ディレクトリを作成する前に Duplicate guest ID: g101 が表示されます。

python3 generate.py invalid-duplicate.csv event.json invitation.html batches/rejected-duplicate

メッセージが欠けた例も、意図的に Missing guest field: message で失敗します。先行するレコードが正しくても、一部だけのバッチが残ってはいけません。

python3 generate.py invalid-missing.csv event.json invitation.html batches/rejected-missing

入力パスのタイプミスは別の問題です。OS がファイルを開けません。次のコマンドは存在しない guest.csv をわざと使っています。エラーを読み、環境をリセットしないでください。

python3 generate.py guest.csv daytime.json invitation.html batches/daytime

実在する guests.csv に直して復旧します。2 つ目のイベント情報を読み、同じプログラムと名簿で新しいバッチを作ります。

cat daytime.json
python3 generate.py guests.csv daytime.json invitation.html batches/daytime

想定される出力:

Created batch: batches/daytime

Web 8080 に切り替えて更新します。evening/g101.html と evening/g102.html を順に開きます。同じ名前でも、それぞれのメッセージは異なります。evening/g103.html を開き、カンマ、引用符、ë、アンパサンド、<Friends> がすべて文字として表示されることを確認します。daytime/g103.html を開き、新しいタイトル、時刻、会場を読みます。各招待状のブラウザータブを閉じると一覧に戻ります。昼のイベントの招待状を Download HTML で保存して開いてください。個別の文章がプレビューと一致するはずです。

特殊文字をそのまま表示する招待状

名前のカンマ、引用符、山括弧は、あいさつ文の中に見える文字として残っています。

2 つのイベントバッチは独立しています。CSV、イベントファイル、テンプレートは変わらず、古いアーカイブも残っています。無効な入力、既存バッチ、存在しない入力パスは、それぞれ理由が異なります。以前の招待状を壊さずに修正できます。

まとめ

CSV レコードと JSON のイベント情報から、実際に開いてダウンロードできる HTML 招待状を生成しました。安定した ID と表示名を区別し、フィールドをテンプレートのテキスト領域へ対応付け、UTF-8 と HTML エスケープで Unicode や特殊文字を保持し、書き込み前にバッチ全体を検証しました。これで、入力と以前の成果を残したまま、新しいイベントのバッチを作成できます。