「絶対」をgrepしたら24件。商品説明の断定語チェックは、語の一覧だけでは使えなかった

grepで「絶対」を数えると24件ありました。ほとんどは「絶対パス」と「絶対値」です。
販売ページの説明文では、「必ず」「誰でも簡単に」のような断定語を出す前に止めたくなります。消費者庁は、内容を実際より著しく優良に見せる表示を優良誤認表示として、景品表示法で禁じています。例として挙がっているのは、カシミヤ混用率が約80%のセーターに「カシミヤ100%」と書くケースです。
ここまで、デジタル商品のファイルを売る前に機械で止める検査を、形式ごとに作ってきました。説明文はまだ人の目だけです。そこで断定語を拾う小さなスクリプトを作りました。最初に動かした相手は説明文の見本ではなく、自分のブログ113本です。普通の日本語の中で、語の一覧がどれだけ空振りするかを先に知りたかったからです。
数えるだけで、もう読みにくかった
リポジトリのルートで、候補にした5語をそのまま数えます。コードブロックの中も含めた単純なgrepです。数えたのは、このPRを作る前の main にある113本です。
for w in 絶対 必ず 簡単 完全 確実; do printf '%s ' $w; grep -o "$w" src/content/posts/*.md | wc -l; done
絶対 24
必ず 34
簡単 11
完全 18
確実 9
合計96件。全部読む気になるかというと、なりません。
コードブロックを除いた最初のスクリプトでは、「絶対」が23件、「最大」が26件、「即」が13件でした。「最大」は「最大 20 snapshot」のような仕様の上限で、「即」は「即時反映」です。どちらも買い手への約束ではないので、候補の語から外しました。
タイトルとdescriptionだけに絞ると、5語のどれかを含む行は3行でした。見出しだけを検査していたら、問題なしと思って終わっていたはずです。断定語は本文の中ほどにあります。
例外を足して52件にした
出力を読んで、語の一覧に例外を足していきました。「絶対パス」「絶対値」は別の意味です。「不完全」には「完全」が入っています。予想していなかったのは「不確実性」で、「確実」がそのまま引っかかっていました。「必ずしも」も、断定どころか逆の意味です。
もう1つ足したのが、同じ文が「ません」「ではなく」「とは限」で打ち消されていたら negated に分ける処理です。「個人 vault 全体を必ず奪われるわけではありません」は、約束ではなく打ち消しです。
保存するスクリプトは次のとおりです。$TMPDIR/copyscan/copyscan.py に置きます。
copyscan.py(40行)
import re, sys, pathlib, collections
CLAIM = ["絶対", "必ず", "誰でも", "簡単", "完全", "100%", "最強", "最短", "確実", "完璧"]
# 複合語として自然に使われる並び。ここに入るものは判定から外す
SAFE = re.compile("|".join([
r"絶対(パス|値|URL|座標|指定|時刻|時間|path|アドレス|参照)", r"不完全", r"不確実", r"不確か",
r"完全(一致|な単色|版|修飾|に同じ)", r"必ずしも",
]))
# 同じ文が否定・限定で終わっていれば、約束ではなく打ち消しとみなす
NEG = re.compile(r"(ません|ではなく|とは限|はない|てはいけ)")
def hits(text):
text = re.sub(r"```.*?```", "", text, flags=re.S) # コードブロック
text = re.sub(r"`[^`\n]*`", "", text) # インラインコード
out = []
for sent in re.split(r"[。\n]", text):
masked = SAFE.sub(lambda m: "□" * len(m.group(0)), sent)
for w in CLAIM:
if w in masked:
out.append((w, sent.strip(), "negated" if NEG.search(sent) else "review"))
return out
if __name__ == "__main__":
if "--extra" in sys.argv: # 追加語はカンマ区切り
CLAIM += sys.argv[sys.argv.index("--extra") + 1].split(",")
if sys.argv[1] == "--lines": # 1行1文の見本ファイルを調べる
for i, line in enumerate(open(sys.argv[2], encoding="utf-8"), 1):
print(f"S{i}", [(w, label) for w, _, label in hits(line)])
sys.exit()
root = pathlib.Path(sys.argv[1])
rows = []
for p in sorted(root.glob("*.md")):
body = p.read_text(encoding="utf-8").split("\n---\n", 1)[1]
rows += [(p.stem,) + h for h in hits(body)]
by = collections.Counter(r[3] for r in rows)
print("files", len(list(root.glob("*.md"))), "hits", len(rows), dict(by))
if "--list" in sys.argv:
for r in rows:
if r[3] == "review":
print(r[0][:30], "|", r[1], "|", r[2][:48])
ブログ全体に流します。
python3 "$TMPDIR/copyscan/copyscan.py" src/content/posts
files 113 hits 62 {'negated': 10, 'review': 52}
grepの96件は、コードを外して例外を除くと文単位で62件になり、そこから打ち消し10件を除いて52件が残りました。打ち消しの10件は全部読みました。9件は文字どおりの打ち消しです。残る1件の「複数の profile や利用者で必ず守りたい値には向きません」は、打ち消しというより条件の説明でした。どちらにしても約束ではないので、分類は動かしていません。
52件を読んで分けた
残った52件を、1件ずつ読んで4つに分けました。
| 分類 | 件数 | 例 |
|---|---|---|
| 手順・規則・事実の記述 | 26 | 「パターンは必ず引用符で囲みます」 |
| 技術用語・機能名の一部 | 13 | 「完全復元」「簡単な問い」 |
| 実際に測った結果 | 2 | 「root filesystem の削除を確実に拒否しました」 |
| 読者への「簡単」「確実」の言い切り | 11 | 「署名済みの ipa ファイルがある場合は、iMazing や 3uTools を使う方が簡単です」 |
販売文の検査として拾いたいのは、最後の11件だけです。52件の2割ほどで、10本の記事に散らばっていました。技術ブログの「必ず」は、たいてい手順の命令です。商品説明文でこの比率がどう変わるかは、まだ測っていません。
11件に実測の裏づけがあるかどうかは、記事ごとに違います。公開済みなので、今回は書き換えていません。
6行の架空の説明文で、漏れを確かめる
ここまでは誤検出の話でした。では逆に、拾うべきものを見逃していないか。販売文の見本を6行用意しました。断定語を試すために意図して作った文で、実在の商品の説明ではありません。
cat > "$TMPDIR/copyscan/samples.txt" <<'EOF'
誰でも簡単に使えるNotionテンプレート。導入したその日から確実に家計が整います。
ほったらかしで毎月の収支が自動で貯まる、失敗しない家計簿テンプレートです。
完璧なレビュー管理を、最短10分で。
100%満足の保証つき。購入者の9割が継続しています。
初期設定は5ステップです。所要時間は私の環境で約12分でした。
全部入りの決定版。これ1つで家計管理の悩みが消えます。
EOF
python3 "$TMPDIR/copyscan/copyscan.py" --lines "$TMPDIR/copyscan/samples.txt"
S1 [('誰でも', 'review'), ('簡単', 'review'), ('確実', 'review')]
S2 []
S3 [('最短', 'review'), ('完璧', 'review')]
S4 [('100%', 'review')]
S5 []
S6 []
S1、S3、S4は拾えました。S5は断定のない普通の文なので、空で正解です。
問題はS2とS6です。「ほったらかし」「失敗しない」「決定版」「悩みが消えます」は、10語のどれにも入っていません。S4の「購入者の9割が継続しています」も通りました。この数字に根拠があるかどうかは、消費者庁も見るところです。商品の効果や性能の表示では、裏づけとなる合理的な根拠を示す資料の提出を求められる場合があります。語の一覧が持つのは存在確認までで、根拠の有無は見えません。
見逃した4語を足して、もう一度流してみます。
python3 "$TMPDIR/copyscan/copyscan.py" --lines "$TMPDIR/copyscan/samples.txt" --extra 'ほったらかし,失敗しない,決定版,悩みが消え'
S1 [('誰でも', 'review'), ('簡単', 'review'), ('確実', 'review')]
S2 [('ほったらかし', 'review'), ('失敗しない', 'review')]
S3 [('最短', 'review'), ('完璧', 'review')]
S4 [('100%', 'review')]
S5 []
S6 [('決定版', 'review'), ('悩みが消え', 'review')]
S2とS6も拾えました。ただし、その6行を見てから語を足したのですから、当たって当然です。この結果は、チェックが良くなった証拠にはなりません。
足した4語でブログ113本を流し直すと、結果は前と同じ hits 62 でした。誤検出が増えなかったことは確認できました。実際に書かれる断定語は、この4語の外にいくらでもあります。
「9割」のような数字の言い方を、正規表現 [0-9]割 で拾う案も試しました。ブログ本文では6件出ました。5件はトークン費用の記事の比率(実測の比較や料金ページの説明)で、1件は端数処理の記事の割合です。どれも買い手への約束ではないので、入れませんでした。
まだ納得していないこと
誤検出を減らす方向と、見逃しを減らす方向は引っ張り合います。例外を足せば静かになり、語を足せば騒がしくなります。手元の113本と6行に合わせただけの調整です。どちらへ寄せるのが正しいのか、今の私には決められません。
もう1つ、AIに説明文を書かせると断定語が増えるのかは、今回は測っていません。見本6行は断定語を入れるつもりで作った文なので、AIがふだん書く説明文の癖とは別物です。次は、実際に売る予定の商品の説明文をAIに3本下書きさせ、この一覧に通したときの件数を数えます。
今回の結論は小さいです。語の一覧は、そのまま止める門にすると毎回52件を読むことになります。使い道は、人が読む場所に印を付けることまでです。
出典
- 消費者庁「表示規制の概要」: https://www.caa.go.jp/policies/policy/representation/fair_labeling/representation_regulation/
- 消費者庁「景品表示法」: https://www.caa.go.jp/policies/policy/representation/fair_labeling/
- Python 3 ドキュメント「re」: https://docs.python.org/ja/3/library/re.html


