はじめに
Claude Codeに日本語の記事や紹介文を書いてもらうと、文章そのものはかなり整っています。それでも、使わないと決めた記号や言い回しが、いつの間にか混ざっていることがあります。この記事で書くのは、公開前に grep で機械的に検品している手順です。サンプルのファイルで再現しました。
内容は4つです。frontmatterに「何を確かめたか」を書く、禁止文字を探す、禁止語をリストにする、の3つを1本のスクリプトにまとめ、最後に grep では拾えないものを書きます。スクリプトだけ欲しい方は「4.」から読んでもらえれば足ります。
環境
macOS 26.6.2、/usr/bin/grep(BSD grep 2.6.0-FreeBSD)、/usr/bin/awk(20200816)、bash 3.2.57、perl 5.34.1、Claude Code 2.1.304。2026年10月20日に動作を確認しました。GNU grep では試していません。
この記事をざっくり図解
機械で落とせるものは機械で落とし、人は機械で見られない部分に時間を使う、という分け方です。
1. frontmatterに「何を確かめたか」を書く
私たちは、記事のMarkdownファイルの先頭に compliance_check という項目を置いて、その記事で何を確かめたかを書いています。サンプルはこうです。
---
title: "サンプル記事"
compliance_check:
- "ダッシュ記号(—)なし(grep確認)"
- "禁止語リストの語なし(grep確認)"
---
書いておく理由は、あとから読んだ人が「この記事は何をチェックして出したのか」を追えるようにするためです。チェックしたつもりの項目と、実際に機械で見た項目を区別できるよう、grepで見たものには「(grep確認)」と付けています。
この項目は、Claude Codeに下書きを頼むときに「frontmatterに compliance_check を書いて」と伝えて、一緒に書いてもらっています。ただ、AIが「確認済み」と書いたことと、実際に確認されていることは別の話です。それで次の手順に進みます。
2. 禁止文字を探す:本文だけを対象にする
最初はファイル全体に grep をかけていました。
$ grep -n -e '—' article.md
4: - "ダッシュ記号(—)なし(grep確認)"
12:対応範囲は、要件定義から運用まで — すべてお任せください。
4行目は、frontmatterに書いた「ダッシュ記号なし」という確認メモそのものです。手元の記事ファイル約160本を数え直したところ、108本でダッシュがfrontmatterにだけ出ていました。毎回これが混ざると、本当に直すべき行が埋もれます。
そこで、frontmatterを空行に置き換えてから検査することにしました。行を消さずに空行にしておけば、grep の行番号は元のファイルと同じままです。
NR == 1 && $0 == "---" { fm = 1; print ""; next }
fm { if ($0 == "---") fm = 0; print ""; next }
{ print }
数え直したときに、もう一つ分かったことがあります。frontmatterを除いても、本文側でダッシュが見つかったファイルが7本ありました。中を見ると、どれも記事の後ろに付けていた社内向けのレビューメモで、「ダッシュは使っていない」と書いた行そのものにダッシュが入っているものもありました。Claude Codeと一緒に書いたメモです。今は、こうしたメモは本文の後ろに置かず、frontmatterに入れるようにしています。検品の対象が「貼り付ける部分」と一致していないと、grep の結果は信用できません。
探している文字は、em dash(U+2014)、en dash(U+2013)、horizontal bar(U+2015)の3つです。長音記号の「ー」(U+30FC)は見た目が似ていますが、別の文字なので当たりません。en dash は「2から3週間」のような範囲を数字と記号で書くときに紛れ込みやすいので、範囲は「〜」で書くと決めています。
パターンは [—–―] のような角かっこではなく、-e で1文字ずつ渡しています。マルチバイト文字を角かっこに入れたときの扱いは、ロケールや grep の実装によって変わることがあるためです。手元の BSD grep では LC_ALL=C にしても結果は同じでしたが、ほかの環境で同じとは限らないので、迷わない書き方にしました。
3. 禁止語はリストのファイルに分ける
禁止語は、スクリプトに直接書かず、別のテキストファイルにしています。以下はこの記事用の架空のリストで、私たちが実際に使っているものではありません。
# 誇大表現(サンプル。実際の社内リストではありません)
業界No.1
必ず成功
100%保証
世界最高水準
# 抽象的な言い方
すべてお任せ
照合には grep -F -f を使います。-F は正規表現として解釈せず固定の文字列として探すオプションで、「No.1」の . や「100%」がそのままの意味で扱われます。
このリストには、わざとコメント行と空行を入れました。人が読んで分かるようにするためですが、そのまま -f に渡すのは避けています。# で始まる行も1つのパターンとして扱われますし、空行の扱いは grep の実装によって違うことがあります(手元の BSD grep では、空行があっても全行に一致することはありませんでした)。スクリプトでは、先にこの2種類の行を取り除いてから渡します。
4. 1本のスクリプトにまとめる
ここまでをまとめたのが次のスクリプトです。NGがあれば終了コード1で終わるので、手で実行しても、ほかの仕組みから呼んでも使えます。
#!/bin/bash
# 日本語の記事ファイルを、公開前に機械的に検品する
# 使い方: ./check-ja-content.sh 記事.md [禁止語リスト]
set -u
file="$1"
words="${2:-banned-words.txt}"
status=0
tmp_body="$(mktemp)"
tmp_words="$(mktemp)"
trap 'rm -f "$tmp_body" "$tmp_words"' EXIT
# 1. frontmatterに検品メモがあるか
if ! grep -q '^compliance_check:' "$file"; then
echo "NG: compliance_check がありません"
status=1
fi
# 2. frontmatterを空行に置き換えて本文だけにする(行番号は元のファイルと同じになる)
awk 'NR == 1 && $0 == "---" { fm = 1; print ""; next }
fm { if ($0 == "---") fm = 0; print ""; next }
{ print }' "$file" > "$tmp_body"
# 3. 禁止文字: em dash(U+2014)、en dash(U+2013)、horizontal bar(U+2015)
if grep -n -e '—' -e '–' -e '―' "$tmp_body"; then
echo "NG: ダッシュ記号"
status=1
fi
# 4. 禁止語: コメント行と空行を除いてから -F(固定文字列)で照合する
grep -v -e '^#' -e '^[[:space:]]*$' "$words" > "$tmp_words"
if [ -s "$tmp_words" ] && grep -n -F -f "$tmp_words" "$tmp_body"; then
echo "NG: 禁止語"
status=1
fi
[ "$status" -eq 0 ] && echo "OK: $file"
exit "$status"
サンプルの記事に対して実行すると、こうなります。
$ ./check-ja-content.sh article.md
12:対応範囲は、要件定義から運用まで — すべてお任せください。
16:期間の目安は2–3週間です。
NG: ダッシュ記号
12:対応範囲は、要件定義から運用まで — すべてお任せください。
14:業界No.1の品質で、必ず成功します。
NG: 禁止語
$ echo $?
1
frontmatterの4行目は出てこなくなり、行番号は元のファイルのままです。12行目はダッシュと禁止語の両方に当たっています。3行を直してから流し直すと、OK: article-fixed.md と出て終了コードは0になりました。
一つ、私の端末でつまずいた点を書いておきます。対話用のシェルでは grep が別の実装(ugrep)に差し替わっていて、手で打ったコマンドとスクリプトの中とで、動いている grep が違っていました。結果を比べる前に、type grep でどれが動いているかを確かめておくと、余計な調べものをせずに済みます。
5. grepでは拾えないもの
ここからが、この方法の限界です。
grep が見ているのは文字列だけです。「業界ナンバーワン」や全角の「NO.1」は、リストにない限り素通りします。表記ゆれをすべてリストに足していくと、今度は関係のない文まで当たり始めます。文脈も読めません。同じ「必ず」でも、「必ずバックアップを取ってください」は問題のない文です。
いちばん大きいのは、文体です。AIが書いた日本語は、禁止語を一つも含まないまま、どこか機械っぽく読めることがあります。段落ごとに同じ型で始まる、結論がどれも当たり障りのない一文で終わる、同じ文末が続く、といったものです。
最後のものだけは、目安としてスクリプトで見ています。
#!/usr/bin/perl
# 「。」で文を区切り、末尾2文字が3文続けて同じ箇所を表示する(目安としての確認)
use strict;
use warnings;
use utf8;
binmode STDOUT, ':encoding(UTF-8)';
local $/;
open my $fh, '<:encoding(UTF-8)', $ARGV[0] or die "$ARGV[0]: $!";
my $text = <$fh>;
$text =~ s/\A---\n.*?\n---\n//s; # frontmatterを除く
$text =~ s/^```.*?^```//msg; # コードブロックを除く
my @sentences = grep { /\S/ } split /。/, $text;
my ($p1, $p2) = ('', '');
for my $i (0 .. $#sentences) {
(my $s = $sentences[$i]) =~ s/\s+//g;
my $end = substr($s, -2);
print "同じ文末が3文続いています: 「${end}。」(", $i + 1, "文目)\n"
if $end eq $p1 && $end eq $p2;
($p2, $p1) = ($p1, $end);
}
$ perl endings.pl endings-sample.md
同じ文末が3文続いています: 「ます。」(3文目)
これも、末尾の2文字が同じかどうかを見ているだけです。「用意します。用意します。用意します。」は見つけられても、読んだときの単調さそのものは測れません。書いてある事実が正しいかどうかも、当然ながら分かりません。ちなみに、この記事自体をこのスクリプトにかけると、いま挙げた例の箇所で1回当たります。
そのため、機械の検品を通ったあとに、日本語を母語とするメンバーが声に出して読む工程を残しています。grep は「人が読む前に、機械で落とせるものを落としておく」ための道具で、人の確認を省くためのものとは考えていません。
最後に
AIに「この記号は使わないで」と頼んでも、守られないことはあります。頼んだうえで、出てきたものを機械で確かめる。その順番にしてから、公開前に見落としていた記号や言い回しに気づけるようになりました。リストの中身は、それぞれの現場の決まりに置き換えて試してみてください。
参考文献
- FreeBSD Manual Pages: grep(1)
- GNU Grep Manual(Usage、Character Classes and Bracket Expressions)
- The Unicode Standard, General Punctuation(U+2010〜U+2027)
- Qiita「Qiitaコミュニティガイドライン」(AIが生成した内容の検証について)