1 min read · 327 words
活用テクニック / ブログ運営 / Python・自動化
約2,300文字
ブログを200記事以上運営していると、人が直接記事を検品しても必ず見落としが発生します。マークダウンの残り(太字がそのまま露出)、絵文字のホワイトリスト違反、出典の漏れ、空の表、ボックススタイルの残りなどです。そのため、私たちは記事がブログAPIに渡される直前に、自動でチェックして修正するステップを別途用意しました。
この記事では、その自動QCシステムをどのような意図で作り、どのように動作し、実際にどのような効果があったのか、そしてどのように検証したのかまでを解説します。同じ悩みを持つ運営者が、コード1ページ分で真似して作れるように要点だけをまとめました。
開発した理由
最初の1年間、私たちは2種類のトラブルを頻繁に目にしました。
1つ目は、モデル出力の残り。LLMで本文を生成すると、太字、## 小見出し、---のようなマークダウンのトークンがHTMLに変換されないまま残ってしまうことがよくありました。本番環境でアスタリスクがそのまま表示されてしまっていたのです。
2つ目は、作成直後は問題ないものの、公開直前のどこかのフック(hook)がコンテンツを台無しにしてしまうケース。特定の関数が本文内で 検問所は2つのステップで構成されています。 ステップ1:sanitize(サニタイズ) — 無条件で修正する作業 HTMLを受け取り、以下を一律で適用します。 このステップは、人の判断を必要としない機械的な作業です。どのような記事であっても、同じ結果が得られるようにしました。 ステップ2:quality gate(クオリティゲート) — パス失敗時に公開をブロック 人が見れば気づくような抜け漏れを自動でチェックします。パスできない場合、公開自体が拒否されます。 導入後6ヶ月間の結果: ブロックされた38本は、記事が失われたわけではありません。作成者が事前に気づいてブラッシュアップした上で再試行したため、すべて正常に公開されました。ブロックされた理由の分布は、出典漏れ 41% / 文字数不足 26% / 画像0枚 21% / その他 12% でした。 検問所を作成した後に、私たちが行った検証です。 ゴールデンセット回帰テスト — 過去にトラブルが発生した記事41本の原文を集めてゴールデンセットを作成しました。sanitize + quality gate を通過させた際、41本すべてでトラブルのパターンが消えるかどうかを自動比較しました。結果は39/41がパス。失敗した2本を確認して正規表現を補強し、41/41パスまで引き上げました。 本番環境のスポットチェック(spot-check) — 新しい sanitize を適用した最初の週に公開された18本のうち、8本をランダムに選んで本番ページを直接フェッチ(fetch)しました。デスクトップ(desktop)1280px、モバイル(mobile)360pxの2つの幅で横スクロールが発生していないか、文字がコンテナの外にはみ出していないか、画像が崩れていないかをチェックしました。結果は8/8で正常でした。 ダブルパスの冪等性(idempotency) — sanitize を一度通過した出力結果に対して、もう一度 sanitize を通過させた際に出力が同一になるかを確認しました。これは、公開フックチェーン(publish hook chain)が2回実行されるケースを防ぐための検証です。結果は100/100で同一でした。 コード全体をそのままコピーするよりも、コアとなる1つか2つの要素をご自身の環境に合わせて取り入れることをおすすめします。 この2つの関数を、公開直前のただ1箇所で呼び出すだけで十分です。quality_gate が fail を返した場合は公開をブロックし、その理由を作成者にフィードバックしてください。sanitize は、出力されたHTMLを受け取ってそのまま公開APIに渡します。 要約すると、「公開前の検問所1箇所で、すべてのトラブルを自動的に防ぐ」という1行に尽きます。人が毎回検品に費やしていた時間が丸ごと不要になります。 Category Coverage Notice This article follows our label-specific editorial criteria. Details:
動作原理
width:800px、margin-left:-30px、position:absoluteなど)タグの固定width/height属性の除去 → レスポンシブ対応の維持X → X、任意の--- → )border、box-shadow、padding>20pxが指定されたmax-width:100%、overflow-wrap:anywhere)
が3個未満 → fail(ガイド・比較記事)
実際の効果
検証方法
実装方法
import re
def sanitize_pre_publish(html: str) -> tuple[str, list[str]]:
fixes = []
# 危険なインラインwidthの除去
html, n = re.subn(r'width\s*:\s*(?:[4-9]\d{2}|[1-9]\d{3,})px\s*;?', '', html)
if n: fixes.append('strip_wide_width')
# マークダウンの残り → HTML
html, n = re.subn(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
if n: fixes.append('md_bold')
# 絵文字の除去(必要に応じて)
html, n = re.subn(r'[\U0001F300-\U0001FAFF]', '', html)
if n: fixes.append('strip_emoji')
return html, fixes
def quality_gate(html: str, post_type: str) -> tuple[bool, list[str]]:
fails = []
text = re.sub(r'<[^>]+>', '', html)
if len(text.replace(' ', '')) < 600: fails.append('too_short')
if html.count('<h2') < 3 and post_type in ('howto', 'compare'): fails.append('few_h2')
if '<img' not in html: fails.append('no_image')
if 'TODO' in html or 'REDACTED' in html: fails.append('placeholder')
return (len(fails) == 0), fails