ZIPで渡す商品のREADME、相対リンクを確かめる

ZIPで渡す商品のREADME、相対リンクを確かめる

ZIPを開いた購入者は、最初にREADMEを読みます。素材一覧へのリンクがあっても、リンク先が入っていなければそこで止まります。AIに説明文の下書きを手伝わせるほど、文章の更新は速くなります。その速さを、配布物の確認漏れにしないための小さな見本を作りました。

前回は、同じ内容のZIPでも作成日時や格納順でハッシュが変わることを見ました(配布ZIPの中身は同じなのにハッシュが変わる)。今回はその次です。アーカイブ自体が読めても、案内文が中のファイルを指せるかは別に確かめます。

ここで扱うのは実際の販売物ではありません。週末の献立テンプレートを想定し、README.md、docs/START.md、LICENSE.txt、CSVの4ファイルを作った合成見本です。AIがこの見本を生成したわけでも、販売結果を測ったわけでもありません。

リンクはREADMEの場所から数える

Markdownのリンク先がassets/catalog.csvなら、起点はREADME自身の置き場所です。docs/START.mdから../assets/catalog.csvへ進む場合は、いったんdocsを1段戻ります。この..を文字列のままにすると、ZIPには存在するファイルを「壊れている」と誤判定します。

Webのアドレスの決まり(RFC 3986)にも、リンクを書いたページの場所を起点にして、.や..(今の場所・1つ上の場所を表す部分)を整理する手順があります。ZIP内のMarkdownはWebページそのものではありませんが、「どのファイルから数えるか」と「..をどう整理するか」を分けて考えると混乱しません。

[README.md] ── assets/catalog.csv ──> [assets/catalog.csv]
     │
     ├── LICENSE.txt ───────────────> [LICENSE.txt]
     │
     └── docs/START.md ── ../assets/catalog.csv ──> [assets/catalog.csv]

4ファイルだけの見本を作る

Python 3.13.5で、下のスクリプトをcheck_zip_markdown_links.pyとして保存し、見本のディレクトリをZIPへ収め、Markdownのインラインリンクを数えました。外部URLと同じページ内のアンカーは、この確認の対象から外しています。配布物の中にある相対リンクだけを見ます。

実行したスクリプトを開く
from __future__ import annotations

import posixpath
import re
import shutil
import zipfile
from pathlib import Path, PurePosixPath

root = Path("zip-link-sample")
if root.exists():
    shutil.rmtree(root)
(root / "assets").mkdir(parents=True)
(root / "README.md").write_text(
    "# 週末の献立テンプレート\n\n"
    "[使い方](docs/START.md) と [利用条件](LICENSE.txt) を先に読みます。\n"
    "素材の一覧は [CSV](assets/catalog.csv) です。\n",
    encoding="utf-8",
)
(root / "docs").mkdir()
(root / "docs" / "START.md").write_text(
    "# 使い方\n\n[素材一覧](../assets/catalog.csv) を確認します。\n",
    encoding="utf-8",
)
(root / "LICENSE.txt").write_text("個人利用の見本です。\n", encoding="utf-8")
(root / "assets" / "catalog.csv").write_text("name,format\nmeal-plan,pdf\n", encoding="utf-8")

archive = Path("meal-plan-sample.zip")
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf:
    for item in sorted(root.rglob("*")):
        if item.is_file():
            zf.write(item, item.as_posix())

link = re.compile(r"\[[^]]+\]\(([^)]+)\)")
issues = []
checked = 0
with zipfile.ZipFile(archive) as zf:
    members = set(zf.namelist())
    for name in sorted(members):
        if not name.endswith(".md"):
            continue
        for target in link.findall(zf.read(name).decode("utf-8")):
            if "://" in target or target.startswith("#"):
                continue
            candidate = (PurePosixPath(name).parent / target).as_posix()
            normalized = posixpath.normpath(candidate)
            checked += 1
            if normalized not in members:
                issues.append(f"broken {name} -> {target} ({normalized})")

print(f"archive={archive.name}")
print(f"members={len(members)}")
print(f"markdown_links_checked={checked}")
print(f"broken_links={len(issues)}")
for issue in issues:
    print(issue)
mkdir -p "$TMPDIR/zip-link-check"
cd "$TMPDIR/zip-link-check"
表示はありません。

このディレクトリに、上のスクリプトをcheck_zip_markdown_links.pyとして保存します。

python3 check_zip_markdown_links.py
python3 -m zipfile -t meal-plan-sample.zip
archive=meal-plan-sample.zip
members=4
markdown_links_checked=4
broken_links=0
Done testing

zipfileはZIPの作成、読み取り、一覧取得を扱えます。ここではアーカイブを一度作り、namelist()で得た名前と、READMEから計算した相対パスを比べました。python3 -m zipfile -tのDone testingは、ZIPとしての読取り検査が終わった表示です。リンク先の意味や商品の内容まで保証するものではありません。

最初の実装は正しいリンクを壊れたことにした

最初はPurePosixPathでdocs/START.mdと../assets/catalog.csvをつなげるだけにしました。normpathを入れる前の1行は次のとおりです。

-            normalized = candidate
+            normalized = posixpath.normpath(candidate)

結果はzip-link-sample/docs/../assets/catalog.csvです。アーカイブ内の名前はzip-link-sample/assets/catalog.csvなので、一致せず、壊れたリンク1件と出ました。

archive=meal-plan-sample.zip
members=4
markdown_links_checked=4
broken_links=1
broken zip-link-sample/docs/START.md -> ../assets/catalog.csv (zip-link-sample/docs/../assets/catalog.csv)

リンク先のCSVは入っていました。間違っていたのは見本ではなく、点検側です。posixpath.normpath()で..を整理してから照合すると、4件すべてが見つかり、壊れたリンクは0件になりました。私は、判定器が出した赤い表示をそのまま信じず、実際のアーカイブ名と比較する工程を残したいと思います。

用語

用語この見本での意味
相対リンク現在のMarkdownファイルを起点に書くリンク先です
基準ファイルリンクを書いたMarkdownファイルです。ここではREADME.mdやdocs/START.mdです
ドット区間.や..のように、現在または親の場所を表すパスの部分です
正規化docs/../assetsのような表記を、同じ場所を表す短い表記へ整理することです
ZIPメンバーZIP内に格納された個々のファイル名です
誤検知実際には問題がないものを、問題ありと判定することです

販売前に残す境界

この確認はMarkdownの単純なインラインリンクだけを数えます。画像、HTML、参照形式リンク、URLエンコード、リンク先に空白や括弧を含むケースは扱いません。../がZIPの外を指すリンクは、販売前のルールとして拒否します。

また、リンクが開けても説明が十分とは限りません。返金条件、対応環境、利用範囲、更新方法などをREADMEに書くかは、商品と販売先の規約で決めます。今回の4ファイルは、それらを満たす商品見本ではありません。

AIで説明文を増やす前に、案内先をファイルとして確認する。小さな順番ですが、購入後の「どこを見ればよいですか」を減らす土台になります。

出典

この記事をシェア