0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

提案資料のリンク切れを公開前に検査するPython

0
Posted at

提案資料のリンク、最後に全部開いていますか?

生成AIでたたき台を作ると、文章は早く仕上がります。困るのは、提案の中身が通った後です。古い料金表や移転済みの事例ページを1本混ぜるだけで、送付後の信用を落とします。リンク確認は担当者の気合いに任せず、送付前のチェックに入れた方がいいです。

僕はMarkdownで下書きを受け取る時、まず 形式のURLだけを抜き出して確認するようにしています。Python標準ライブラリで動くので、追加の契約もいりません。

リンク先には、HEAD リクエストを拒否して 405 Method Not Allowed を返すサイトがあります。そこで最初はHEADで軽く確認し、405だけGETで再確認します。404を200扱いにしないことと、転送後のURLを出すことがポイントです。

この3段階に分けると、資料を直す人とリンクを直す人の会話が短くなります。

提案資料のMarkdownからリンクを抽出し、HEAD確認、405時のGET再確認、失敗URLの一覧化へ進む流れ

Markdownのリンクを検査するPython

check_links.py として保存します。対象は通常のMarkdownリンクです。裸のURLや画像リンクまで拾いたい場合は、別ルールを足してください。

import re
import sys
import urllib.error
import urllib.request

LINK_RE = re.compile(r"\[[^]]*\]\((https?://[^\s)]+)(?:\s+[^)]*)?\)")

def links_in_markdown(path):
    with open(path, encoding="utf-8") as source:
        return sorted(set(LINK_RE.findall(source.read())))

def request(url, method, timeout):
    req = urllib.request.Request(
        url, method=method, headers={"User-Agent": "link-check/1.0"}
    )
    with urllib.request.urlopen(req, timeout=timeout) as response:
        return response.status, response.geturl()

def check(url, timeout):
    try:
        return request(url, "HEAD", timeout)
    except urllib.error.HTTPError as exc:
        if exc.code != 405:
            raise
    return request(url, "GET", timeout)

def main(path):
    urls = links_in_markdown(path)
    if not urls:
        raise ValueError("Markdownリンクが見つかりません")

    failed = 0
    for url in urls:
        try:
            status, final_url = check(url, timeout=10)
            print(f"OK  {status}  {url} -> {final_url}")
        except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError) as exc:
            failed += 1
            status = getattr(exc, "code", "接続失敗")
            print(f"NG  {status}  {url}")
    if failed:
        raise SystemExit(1)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        raise SystemExit("使い方: python check_links.py proposal.md")
    try:
        main(sys.argv[1])
    except (OSError, ValueError) as exc:
        raise SystemExit(f"エラー: {exc}")

実行は次の通りです。1件でも NG があれば終了コードが1になるので、後で定例の送付チェックにも組み込めます。

python check_links.py proposal.md

テストでは、200を返すURL、HEADだけ405になるURL、転送されるURLの3ケースを通しました。

OK  200  http://127.0.0.1:8765/head-405 -> http://127.0.0.1:8765/head-405
OK  200  http://127.0.0.1:8765/ok -> http://127.0.0.1:8765/ok
OK  200  http://127.0.0.1:8765/redirect -> http://127.0.0.1:8765/ok

NG 404 はURLの差し替え候補です。NG 接続失敗 は、リンク切れと決めつけず、社内ネットワーク・認証・一時的な障害を確認します。この分け方がないと、営業側はリンクを直したつもりで、相手側ではまだ開けないままになります。

社内限定のURLまで検査対象にすると、VPNの有無で接続失敗になります。資料を社外に送る前の確認なら、まず公開URLだけを対象にする方が運用しやすいです。ここ地味に効きます。

送る直前に見るものを減らす

リンク検査は、資料の良し悪しを判定する道具ではありません。壊れている場所を、送る前に一覧で出すだけです。それでも人が1本ずつブラウザで開く工程を外せます。

生成AIを使った資料ほど、本文のレビューに時間を使いたいところです。提出ルールにこのコマンドを1行足しておけば、URLの確認は毎回同じ品質で終えられます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?