2
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?

生成AIが書いた日本語の「読みにくい」を数えて直す:計測スキルと校正の手順

2
Last updated at Posted at 2026-09-24

はじめに

生成AIが作った文章が、どうも読みにくい。漢字の多さや言い回しに原因があるのではないかと思って始めた計測と、その結果を説明します。

この記事の手順を使うと、次のことができるようになります。

  • 直す場所が、行番号付きの一覧で出る。読みながら長い段落を探さなくてよい
  • 原因を段落 → 文 → 読点 → 漢字率の順に切り分けられる。開けない漢字を開くような、効果の薄い修正に手を付けずに済む
  • 直したあとで、本文が意図せず変わっていないかを機械的に確かめられる
  • 直ったことを、感覚ではなく数字で示せる

自分のブログ88記事を数えてみたところ、目立っていたのは漢字ではなく段落の長さでした。200字以上の段落が全体の9.5%あり、これを分割すると0.7%になりました。漢字率は30.1%のまま変わっていません。

そのとき作った計測スクリプトと、校正の手順を以下にまとめました。上から順に進めれば、自分の文章で同じことができます。

  1. 設置する(Python 3 の標準ライブラリだけで動きます)
  2. 測って、直す箇所を行番号付きで出す
  3. どこから直すか決める
  4. 校正する(段落・文・読点・漢字の直し方と、Claude Code への頼み方)
  5. 本文が意図せず変わっていないか確かめる
  6. 再計測して、直ったことを数字で確かめる

計測は Claude Code のスキルとしても、スクリプト単体としても使えます。ファイルの全文は末尾の付録に載せています。

1. 設置する

スクリプト単体で使う場合

付録の measure.py を保存するだけです。形態素解析器も外部APIも使いません。

python3 measure.py --locate article.md

Claude Code のスキルとして使う場合

スキルにしておけば、「この記事の読みやすさを測って」と頼むだけで計測から修正候補の提示まで進みます。ファイル構成は次のとおりです。

~/.claude/skills/japanese-readability-metrics/
├── SKILL.md              … 使いどころ・数値の読み方・直し方の原則
├── scripts/
│   └── measure.py        … 計測スクリプト
└── references/
    └── baselines.md      … 基準値の出典と限界
  1. ディレクトリを作ります。

    mkdir -p ~/.claude/skills/japanese-readability-metrics/{scripts,references}
    
  2. 付録の3ファイルを、それぞれ SKILL.md、scripts/measure.py、references/baselines.md として保存します。

  3. Claude Code を起動し直すと認識され、/japanese-readability-metrics で呼べます。「この記事の読みやすさを測って」のように頼む形でも呼び出せます(スキルの description をもとに Claude Code が選びます)。

Claude Code 以外(ChatGPT や Codex など)で使う場合は、SKILL.md と baselines.md を指示として読み込ませます。measure.py は手元で実行して、結果を渡してください。

2. 測る

修正候補を出す

python3 measure.py --locate article.md

ナフサ価格について書いた私の記事(修正前)では、こう出ました。

    漢字    ひら    文長    読点    段落  200+  ファイル
------------------------------------  ----------------------------------------
 26.7  58.6    38+ 1.60   221   9/14  naphtha.md

凡例: ! = 基準から読みにくい方向へ乖離 / + = 読みやすい方向
基準値: 国立国語研究所1979年・雑誌55誌(references/baselines.md)

============================================================
修正候補
============================================================

## naphtha.md
  L32: 段落344字  ここで、私はもう一度つまずきました。需要が減っているのなら、接着剤や樹脂だって余るはずではないか、と。け...
  L24: 段落342字  並べてみて、なるほど、と声が出ました。赤い線、日々のスポット相場は4〜5月の高値圏から6月へ下げている。...
  L26: 段落321字  なぜ、こんなふうに食い違うのか。腰を据えて考えてみて、いちばん腑に落ちたのは、市場と現場とでは見ている時...
  (中略)
  L18: 文111字(読点3)  ホルムズ海峡が完全には閉まりきらないだろうという見通し、喜望峰回りといった代替ルートの確立、それに石油化...

上段が記事全体の数値、下段の「修正候補」が作業リストです。L32 はファイル先頭からの行番号で、長いものから順に並びます。

数値の読み方

列 意味 数え方
漢字・ひら 漢字率・ひらがな率(%) 各文字種 ÷ 総文字数
文長 平均文長(字) 本文の文字数 ÷ 「。」の数
読点 1文あたりの読点 「、」の数 ÷ 「。」の数
段落 平均段落長(字) 空行で区切ったブロックの文字数
200+ 200字以上の段落 / 全段落

! は基準値から読みにくい方向へ10%以上ずれていること、+ は読みやすい方向を示します。

修正候補に出るしきい値は、段落200字以上と文80字以上です。どちらもコーパス由来ではない実務上の目安です。200字はスマートフォンで1段落がほぼ画面を埋める量、80字は主語と述語が離れて修飾関係を追いにくくなる量を想定しています。

上の例は、漢字率・ひらがな率・文長・読点のどれにも ! が付いていません。それでも平均段落長が221字あり、14段落のうち9段落が200字以上でした。字面が詰まって見える原因はここにあります。

基準値について

文字種・文長・読点の基準値は、国立国語研究所の報告「各種文章の字種比率」(佐竹秀雄)から取りました。1979年7〜8月発行の雑誌55誌、約35万字を数えた調査です。

漢字26.52%、ひらがな57.29%です。句点2.27%・読点3.96%から、平均文長は約44字、読点は1文あたり約1.74個と逆算できます。

46年前の紙の雑誌なので、技術文書とは前提が違います。特に英数字は基準側では0.51%しかなく、コマンド名や製品名が入る記事ではひらがな率が構造的に下がります。合否の判定ではなく、ずれの方向を見る参照点として使ってください。段落長には基準値がありません。

複数の記事から直す対象を選ぶ

python3 measure.py 'content/posts/*.md'              # 200字以上の段落の割合が多い順
python3 measure.py --sort para 'content/posts/*.md'  # 平均段落長の長い順
python3 measure.py --locate 'content/posts/*.md'     # 全記事の修正候補

記事が多いときは、一覧の上位から手を付けます。

3. どこから直すか決める

原因は次の順で疑います。

  1. 段落が長い(200字以上)。字面が詰まって見える
  2. 文が長い(80字以上)。一文が頭に入ってこない
  3. 読点が少ない(1文あたり1.2個未満)。同じ長さでも切れ目がない
  4. 漢字率。上の3つを直しても解決しないときに初めて検討する

漢字率は目に見えやすいので、最初に疑いがちです。しかし頻出する漢語が固有名詞や専門用語なら、減らしようがありません。この順番にした根拠は、後半の「なぜ漢字ではなく段落から疑うのか」に書きました。

4. 校正する

作業の前に、元のファイルを残しておきます。5章の確認で使います。

cp article.md article.before.md

長い段落は、話題の切れ目で割る

段落を割るときは、既存の文をそのまま2つの段落に分けるだけにします。つなぎの接続詞や文は書き足しません。

ナフサ記事の342字の段落(L24)は、次の3つの内容が続いていました。

  • グラフの読み取り(「赤い線、日々のスポット相場は〜」から「〜この時間差そのものでした。」まで)
  • そこからの気づき(「値段は一つではなくて〜」)
  • グラフの作り方の注記(「(このグラフは〜)」)

この3つの境目に空行を入れただけです。本文は一字も変えていません。

割ると前後のつながりが弱く見え、接続詞を足したくなる場面があります。しかし、それは元の文章になかった論理を足すことになります。つながりが本当に弱いなら、書き手に確かめる問題として残します。

長い文は、構造を解く

80字以上の文は、読点を足して数字を合わせるのではなく、主語と述語のあいだに何が挟まっているかを見ます。

別の記事にあった141字の文です(読点2個)。

Jevを開発したTypeSafe AIは、OpenAIでChatGPTやRLHF(人間のフィードバックによる強化学習)の開発に関わったDiego Almeida氏が、2年間の非公開開発とDCVCが主導した4,000万ドルの資金調達を経て立ち上げた会社です

主語「TypeSafe AIは」と述語「会社です」のあいだに、創業者の経歴・開発期間・資金調達という3つの事実が入れ子で挟まっています。人名を先に出して3文に分けました。

Jevを開発したTypeSafe AIを立ち上げたのは、Diego Almeida氏。OpenAIでChatGPTやRLHF(人間のフィードバックによる強化学習)の開発に関わった人物です。同社は2年間の非公開開発と、DCVCが主導した4,000万ドルの資金調達を経て設立されました

元の文には出典リンクが付いていました。分割するときは、そのリンクがどの事実を支えているのか(この場合は資金調達)を確かめ、その文に付けたまま残します。

読点は、単独では直さない

読点が少ないという数値だけを見て読点を足すと、かえって読みにくくなります。読点の数は、文の構造を直した結果として変わるものです。上の例でも、修正前は1文に読点2個、修正後は3文に読点2個でした。1文あたりの読点は減っています。

漢字率が高いときは、内訳を見る

漢字率に ! が付いたら、どんな漢字の塊が多いのかを数えます。付録の kanji_runs.py を使います。

python3 kanji_runs.py <measure.pyのあるディレクトリ> 'content/posts/*.md'

頻出するのが「経済産業省」「航空宇宙産業」のような固有名詞や、記事のテーマそのものの語なら、開く対象はありません。「〜することができます」のような冗長な言い回しが多いなら、次の「表現の推敲」の担当です。

表現を推敲する

計測で分かるのは文章の形までです。「〜と言えるでしょう」のようなAI的な言い回し、論証の飛び、用語の揺れは、別の観点で見る必要があります。私はここで、公開スキルの humanizer-ja(MITライセンス)を使っています。AIが書いた日本語に出やすい20パターンをチェックして書き換えるスキルです。「浮き彫りにしており」のような定型評価語、太字+コロンの箇条書き、全角ダッシュなどが対象です。

計測と語彙チェックは役割が違います。「この段落は247字ある」はどんな単語リストにも書けず、逆に「浮き彫りにしており」は数えても見つかりません。両方を使います。

Claude Code に頼むときの言い方

場所と制約を一緒に伝えます。

measure.py --locate article.md を実行して、修正候補を一覧にして
L24 の342字の段落を、話題の切れ目で分割して。文言は一字も変えず、空行の挿入だけにして
L48 の141字の文について、主語と述語のあいだに何が挟まっているかを説明してから、分割案を出して。
出典リンクは、それが支えている事実の文に残して
分割した結果、前後のつながりが弱くなる箇所があれば、接続詞を足さずに指摘だけして

「読みやすくして」とだけ頼むと、範囲の指定がないため、言い換えや文の追加まで入る余地があります。直してほしい場所と、変えてはいけないものを具体的に書きます。

5. 本文が変わっていないか確かめる

段落の分割だけなら、空白を除いた本文は修正前と完全に一致するはずです。

python3 -c "import re,sys; a,b=(re.sub(r'\s','',open(p,encoding='utf-8').read()) for p in sys.argv[1:]); print('本文は同一です' if a==b else '本文が変わっています')" article.before.md article.md

長い文を書き換えた箇所があれば、当然一致しなくなります。その場合は diff article.before.md article.md(Git管理なら git diff)で、意図した箇所だけが変わっているかを目で確かめます。

多数の記事をまとめて直すとき

私は88記事のうち、最初の2本は1段落ずつ内容を見て分割位置を決め、残りは分割用のスクリプトで処理しました。

そのスクリプトは、文字数の累積が段落の半分を超えた文境界(「。」の直後)で割り、200字未満になるまで繰り返すという機械的なものです。話題の切れ目を判断しているわけではありません。そのかわり、次の検証を組み込みました。

# 空白を除いた本文が、修正前と完全に一致しなければ書き込まない
assert re.sub(r'\s', '', new) == re.sub(r'\s', '', raw), f'CONTENT CHANGED: {path}'

この検証が保証するのは「文字が一字も変わっていない」ことだけです。割った位置が話題の切れ目として適切かは保証しません。後者は人が読んで確かめるしかありません。一括処理した切れ目を私がすべて読み直したわけではないので、ここは残った課題です。

まとめて処理する場合も、一度に全部ではなく数本で結果を読んでから広げることを勧めます。

6. 再計測する

同じコマンドをもう一度実行します。

python3 measure.py --locate article.md

ナフサ記事は次のように変わりました。

指標 修正前 修正後
平均段落長 221字 124字
最長段落 344字 197字
200字以上の段落 9/14 0/25
漢字率 26.7% 26.7%

88記事全体では、200字以上の段落が172/1,816(9.5%)から13/1,994(0.7%)になりました。この間には段落分割のほかに、全角ダッシュの除去(63件)と一部記事の表現修正も入っています。段落分割だけを行った区間に限ると、空白を除いた本文は全88記事で修正前と一致しており、段落数は1,826から1,994へ168増えました。

残った13段落のうち、1段落は文が1つしかなく、12段落は最後の1文だけで段落の半分以上を占めていました。これらは段落の分割では解決しないので、長い文として直す対象です。

なお、確かめたのは数値の変化までです。修正後の記事を読者に読み比べてもらったわけではありません。

7. 基準値を自分用に調整する(任意)

自分の過去記事と比べる(--profile)

1979年の雑誌より、自分の過去記事のほうが比較相手として近いこともあります。--profile は、文長・読点・段落長・200字以上の段落の割合を、自分の平常値と比べるオプションです。

python3 measure.py --json 'content/posts/*.md'     # 自分の記事を測る
python3 measure.py --profile 'content/posts/*.md'  # 平常値と比べる

コード中の PROFILES は私のブログの値です。classify() も、ファイル名に -weekly- / -monthly- を含むものをニュースレターとして分ける、私のブログ専用の規則です。使う前に自分の記事を測って書き換えてください。

平常値は目標ではありません。私の修正前の値(200字以上9.5%)を平常値にしていたら、172段落に手を入れる判断にはなりませんでした。漢字率・カタカナ率・英数字率は、テーマで決まる値なので比較対象から外しています。

外部コーパスで基準値を差し替える(--reference / --baseline)

既定の基準値を1979年の論文の数値にしているのは、ライセンス上の制約がないからです。国立国語研究所が公開している語彙データは、調べた範囲では利用目的が限られていました。

  • BCCWJ(現代日本語書き言葉均衡コーパス)の語彙表:「研究、教育目的であれば無償」
  • SHC(昭和・平成書き言葉コーパス)の統合語彙表:CC BY-NC-SA 4.0(非営利)

私の用途は商用にあたるので、これらは使っていません。研究や教育の目的で条件を満たす人向けに、手元のテキストから基準値を作るオプションを付けました。

python3 measure.py --reference 'corpus/*.txt' article.md                            # 手元のテキストを測って基準値にする
python3 measure.py --reference 'corpus/*.txt' --save-baseline base.json article.md  # その基準値を保存
python3 measure.py --baseline base.json article.md                                  # 保存した基準値で測る

--reference は、記事と同じ計測処理でコーパスを測るので、数え方の違いによるずれが出ません。保存した値は次回から --baseline で読み込めます。

渡すのはコーパスの本文テキストです。語彙表(頻度リスト)は語単位の集計なので、文字種比率も文長も計算できません。利用条件の確認は、使う人の責任で行ってください。

語種まで見たい場合

漢語・和語・外来語といった語種の比率は、形態素解析が必要なのでこのスクリプトでは測れません。李在鎬氏らの jReadability にテキストを貼れば、日本語教育の級に対応した難易度が出ます。

公開されている公式の「漢語率」は語種の比率で、このスクリプトの「漢字率」(文字種)とは別物です。measure.py の出力を公式に代入しないでください。

  • 論文:李在鎬「日本語教育のための文章難易度に関する研究」早稲田日本語教育学 第21号(PDF)

なぜ漢字ではなく段落から疑うのか

3章の順番は、私のブログ88記事(front matter・コードブロック・URLを除いて約27万字)を測った結果から決めました。

この88記事は、AIを使って書いた記事が中心です。人が書いた文章と分けて比べてはいないので、この順番が書き手を問わず当てはまるかは確かめていません。

最初の仮説は「漢字が多すぎる」でした。文字種を数えると、たしかにそう見えます。

当ブログ88記事 国語研1979
漢字 30.1% 26.52%
ひらがな 38.4% 57.29%
カタカナ 10.5% 6.95%
英数字 10.9% 0.51%

ただ、ひらがなの不足分の大半は、カタカナと英数字(合計21.4%、基準側は7.5%)に押し出された結果です。技術ブログである以上、ここは避けられません。

漢字の中身も数えました。2字以上続く漢字の塊を長さ別に見ると、次のとおりです(割合は漢字連続の総数25,227件に対するもの)。

漢字の連続 件数 割合
2字 16,891 67.0%
3字 3,778 15.0%
4字 3,254 12.9%
5字以上 1,304 5.2%

5字以上の塊で多いのは「中小製造業」(20回)、「航空宇宙産業」(10回)、「経済産業省」(8回)などの固有名詞でした。2字の頻出語は「自分」「確認」「現場」「必要」「問題」で、ひらがなに開いても読みやすくはなりません。開いて意味のある漢語は、ほとんど残っていませんでした。

一方、文と段落の指標はこうでした。

指標 当ブログ88記事 国語研1979(逆算)
平均文長 46.3字 約44字
読点/文 1.28個 約1.74個
平均段落長 116字 (データなし)
最長段落 447字 (データなし)
200字以上の段落 172/1,816(9.5%) (データなし)

文長は基準とほぼ同じです。読点は基準より約26%少なく、ここは基準からずれていました。段落長には基準値がありませんが、200字の目安を1割近くの段落が超えており、最長は447字でした。

読点より段落を先に直したのは、読点の数は文の構造を直した結果として変わる値だからです。段落は改行を入れるだけで、本文を変えずに直せます。

この記事自体を測る

この記事にも measure.py --locate をかけました。対象は付録を含むファイル全体です。付録のコードブロックと、~~~ で囲んだ SKILL.md・baselines.md の中身は計測から除かれます。

    漢字    ひら    文長    読点    段落  200+  ファイル
------------------------------------  ----------------------------------------
 30.5! 39.3!   33+ 0.84!   73   0/72  japanese-readability-paragraph-length.md

凡例: ! = 基準から読みにくい方向へ乖離 / + = 読みやすい方向
基準値: 国立国語研究所1979年・雑誌55誌(references/baselines.md)

============================================================
修正候補
============================================================

修正候補は0件でした。3章の順番で見ると、次のとおりです。

  1. 段落。200字以上は0/72で、最長は198字です
  2. 文。80字以上の文はありません。平均文長33字は基準より短く、+ が付いています
  3. 読点。1文あたり0.84個で ! が付き、3章の目安(1.2個未満)にも当たります
  4. 漢字率。30.5%で ! が付いています

読点が少ないのは、文が基準より短いためです。1文が短ければ、切れ目も少なくて済みます。4章のとおり、数値に合わせて読点を足すことはしていません。

漢字率は kanji_runs.py で内訳を見ました。5字以上の漢字の塊は漢字連続全体の3.8%で、中身は「平均段落長」「国立国語研究所」「非公開開発」といった用語と固有名詞でした。2字の頻出語は「段落」「記事」「読点」で、記事のテーマそのものです。開く対象はありません。

ひらがな率が39.3%と低いのは、コマンド名やファイル名などの英数字が11.8%を占めているためです。

測って見つかった measure.py の不具合

最初にこの記事を測ったときは、修正候補が3件出ました。どれも本文の問題ではなく、measure.py の誤検出でした。

  • 番号付きリスト(「はじめに」の1〜6)の項目がつながり、142字の1文と判定された
  • 付録で ~~~ に囲んだ SKILL.md の中身を、本文として数えていた
  • コードブロックより後ろで、--locate の行番号が実際より小さくずれていた

あわせて、front matter のある記事で行番号が1大きく出る不具合も見つかりました。付録の measure.py は、これらを直した版です。自分の書いた道具を自分の記事にかけると、こうした取りこぼしが見つかります。

まとめ

  • 「読みにくい」の原因は、段落 → 文 → 読点 → 漢字率の順に疑う
  • measure.py --locate で、直す箇所を行番号付きの作業リストにする
  • 段落は既存の文の境目で割り、接続詞を創作しない。長い文は読点を足さず、入れ子の構造を解く
  • 直す前にファイルを残し、空白を除いた本文の比較で意図しない変更がないか確かめる
  • 再計測して、直ったことを数字で確かめる。ただし数字が保証するのは形までで、切れ目の適切さは読んで確かめる

「なんか読みにくい」と感じたら、まず段落の長さを数えてみてください。漢字率より先に、直せる場所が見つかるかもしれません。

付録:ファイル全文

Claude Code のスキルにする場合は、次の場所に保存します(1章と同じ構成です)。スクリプト単体で使うなら measure.py だけで動きます。

~/.claude/skills/japanese-readability-metrics/
├── SKILL.md              … 下の「SKILL.md」
├── scripts/
│   └── measure.py        … 下の「scripts/measure.py」
└── references/
    └── baselines.md      … 下の「references/baselines.md」

kanji_runs.py はスキルには含めない補助スクリプトです。置き場所は自由で、実行時に measure.py のあるディレクトリを引数で渡します。

scripts/measure.py

measure.py(クリックで展開)
measure.py
#!/usr/bin/env python3
"""日本語文書の可読性指標を計測し、修正候補箇所を file:line で提示する。

使い方:
    python3 measure.py <file|glob> [...]           # 計測のみ
    python3 measure.py --locate <file|glob> [...]  # 修正候補の箇所も出す
    python3 measure.py --json <file|glob> [...]    # JSON出力

基準値の差し替え(外部コーパスを使う場合):
    python3 measure.py --reference 'corpus/*.txt' <file|glob>   # 手元のテキストを測って基準値にする
    python3 measure.py --reference 'corpus/*.txt' --save-baseline base.json <file|glob>
    python3 measure.py --baseline base.json <file|glob>        # 保存した基準値を使う

計測するもの(すべて文字種・文字数ベース。形態素解析は不要):
    漢字率 / ひらがな率 / カタカナ率 / 英数字率
    平均文長 / 読点密度 / 平均段落長 / 200字超の段落

基準値は references/baselines.md を参照(国立国語研究所1979年・雑誌55誌)。
"""

import argparse
import glob as globmod
import json
import re
import statistics
import sys
from pathlib import Path

# 国立国語研究所「各種文章の字種比率」(佐竹秀雄, 1979年雑誌55誌・約35万字)
# 分母は総文字数(記号・句読点・スペースを含み、各行の合計が100%になる)
BASELINE = {
    "kanji": 26.52,
    "hiragana": 57.29,
    "katakana": 6.95,
    "sentence_len": 44.0,   # 句点2.27% から逆算(100/2.27)
    "comma_per_sent": 1.74,  # 読点3.96% ÷ 句点2.27%
}
BASELINE_LABEL = "国立国語研究所1979年・雑誌55誌(references/baselines.md)"
BASELINE_KEYS = ("kanji", "hiragana", "katakana", "sentence_len", "comma_per_sent")

# 自分の文章の実測値(--profile で使う参照プロファイル)。
# 外部コーパスと違い「あるべき値」ではなく「現在こうである」という記述にすぎない。
# 段落長と200字超率は *推敲後* の値なので目標として使えるが、
# 漢字率・カタカナ率・英数字率はテーマの必然で決まる値で、
# 寄せにいく対象ではない(PROFILE_ADVISORY を参照)。
PROFILES = {
    "newsletter": {
        "label": "ニュースレター(31本・78,963字)",
        "kanji": 32.3, "hiragana": 28.1, "katakana": 13.1, "alnum": 15.9,
        "sentence_len": 53.8, "comma_per_sent": 1.17, "para_mean": 119,
    },
    "standalone": {
        "label": "単発記事(61本・206,789字)",
        "kanji": 29.4, "hiragana": 43.3, "katakana": 9.2, "alnum": 8.4,
        "sentence_len": 43.1, "comma_per_sent": 1.31, "para_mean": 104,
    },
}

# プロファイルのうち、寄せにいってよい指標だけを比較対象にする。
# 漢字率などを「平常値に合わせる」と、テーマ固有の語彙を削る誤った修正を招く。
PROFILE_ADVISORY = ("sentence_len", "comma_per_sent", "para_mean")

# 実務上のしきい値
PARA_LIMIT = 200   # これを超える段落は視覚的に重い
SENT_LIMIT = 80    # これを超える文は主述が離れやすい

KANJI = re.compile(r"[一-鿿々〇〻]")
HIRA = re.compile(r"[ぁ-ゖゝゞ]")
KATA = re.compile(r"[ァ-ヺーヽヾ]")
ALNUM = re.compile(r"[0-9A-Za-z0-9A-Za-z]")

SKIP_PREFIX = ("#", "-", "*", "|", ">", "!", "[")
NUMBERED = re.compile(r"\d+[.)]\s")  # 番号付きリスト(1. / 1) )


def strip_front_matter(raw):
    """TOML(+++)/YAML(---) front matter を除き、(本文, 開始行番号) を返す。"""
    for sep in ("+++", "---"):
        if raw.startswith(sep):
            parts = raw.split(sep, 2)
            if len(parts) == 3:
                offset = raw[: len(sep) + len(parts[1]) + len(sep)].count("\n") + 1
                return parts[2], offset
    return raw, 0


def blank_out(m):
    """除いた範囲を同じ数の改行に置き換え、以降の行番号がずれないようにする。"""
    return "\n" * m.group(0).count("\n")


# 行頭のフェンスから、同じ記号・同じ長さの閉じフェンスまで(``` と ~~~、````の入れ子にも対応)
FENCE = re.compile(r"^[ \t]*(`{3,}|~{3,})[^\n]*\n.*?^[ \t]*\1[ \t]*$", re.M | re.S)


def strip_noise(text):
    """コードブロック(``` と ~~~)・インラインコード・HTMLタグを除く。"""
    text = FENCE.sub(blank_out, text)
    text = re.sub(r"`[^`]*`", blank_out, text)
    return re.sub(r"<[^>]+>", blank_out, text)


def strip_links(text):
    """リンク先URLを除く(リンクテキストは残す)。"""
    text = re.sub(r"\]\([^)]*\)", "]", text)
    return re.sub(r"https?://\S+", "", text)


def visible_len(text):
    return len(re.sub(r"\s", "", text))


def iter_paragraphs(body, offset):
    """本文段落を (開始行, テキスト, 可視文字数) で返す。見出し・リスト(番号付きを含む)・表は除く。"""
    # front matter がある場合、本文は閉じ行(+++ / ---)の行末の改行から始まるので、
    # 本文の1行目はファイルの offset 行目にあたる
    line_no = offset if offset else 1
    for block in body.split("\n\n"):
        stripped = block.strip()
        if stripped and not stripped.startswith(SKIP_PREFIX) and not NUMBERED.match(stripped):
            n = visible_len(strip_links(stripped))
            if n > 20:
                yield line_no + (len(block) - len(block.lstrip("\n"))), stripped, n
        line_no += block.count("\n") + 2


def analyze(path):
    raw = Path(path).read_text(encoding="utf-8")
    body, offset = strip_front_matter(raw)
    body = strip_noise(body)
    clean = strip_links(body)

    total = visible_len(clean)
    if total == 0:
        return None

    paras = list(iter_paragraphs(body, offset))
    prose = re.sub(r"^\s*(?:[-*|#>]|\d+[.)]\s).*$", "", clean, flags=re.M)
    n_sent = prose.count("。")

    counts = {
        "kanji": len(KANJI.findall(clean)),
        "hiragana": len(HIRA.findall(clean)),
        "katakana": len(KATA.findall(clean)),
        "alnum": len(ALNUM.findall(clean)),
    }
    para_lens = [n for _, _, n in paras]

    return {
        "path": str(path),
        "total": total,
        "ratio": {k: v / total * 100 for k, v in counts.items()},
        "sentences": n_sent,
        "sentence_len": visible_len(prose) / n_sent if n_sent else 0,
        "comma_per_sent": prose.count("、") / n_sent if n_sent else 0,
        "paragraphs": len(paras),
        "para_mean": statistics.mean(para_lens) if para_lens else 0,
        "para_max": max(para_lens) if para_lens else 0,
        "para_over": sum(1 for n in para_lens if n >= PARA_LIMIT),
        "_paras": paras,
    }


def find_long_sentences(paras):
    """SENT_LIMIT を超える文を (行, 文字数, 読点数, 本文) で返す。"""
    out = []
    for line, text, _ in paras:
        for sent in re.split(r"(?<=。)", strip_links(text)):
            s = sent.strip().replace("\n", "")
            n = visible_len(s)
            if n >= SENT_LIMIT:
                out.append((line, n, s.count("、"), s))
    return out


def classify(path):
    """ファイル名からニュースレターか単発記事かを判定する。"""
    name = Path(path).name
    return "newsletter" if ("-weekly-" in name or "-monthly-" in name) else "standalone"


def profile_report(results):
    """自分の平常値(PROFILES)と突き合わせて出す。"""
    for key in ("newsletter", "standalone"):
        sub = [r for r in results if classify(r["path"]) == key]
        if not sub:
            continue
        prof = PROFILES[key]
        n_sent = sum(r["sentences"] for r in sub) or 1
        n_para = sum(r["paragraphs"] for r in sub) or 1
        actual = {
            "sentence_len": sum(r["sentence_len"] * r["sentences"] for r in sub) / n_sent,
            "comma_per_sent": sum(r["comma_per_sent"] * r["sentences"] for r in sub) / n_sent,
            "para_mean": sum(r["para_mean"] * r["paragraphs"] for r in sub) / n_para,
        }
        labels = {"sentence_len": "平均文長", "comma_per_sent": "読点/文", "para_mean": "平均段落長"}
        print(f"\n[{prof['label']}] 対象 {len(sub)}件")
        for k in PROFILE_ADVISORY:
            diff = actual[k] - prof[k]
            sign = "+" if diff >= 0 else ""
            print(f"  {labels[k]:9} {actual[k]:6.2f}  (平常値 {prof[k]}  差 {sign}{diff:.2f})")
        over = sum(r["para_over"] for r in sub)
        print(f"  200字超   {over}/{n_para} ({over/n_para:.1%})  (平常値 0.6%)")
    print("\n※ 平常値は自分の文章の実測値であり、目標ではない。")
    print("  漢字率・カタカナ率・英数字率はテーマで決まるため比較対象に含めていない。")


def mark(value, base, higher_is_worse=True, tol=0.1):
    """基準値からの乖離を記号で示す。"""
    if base == 0:
        return " "
    diff = (value - base) / base
    if abs(diff) < tol:
        return " "
    worse = diff > 0 if higher_is_worse else diff < 0
    return "!" if worse else "+"


def baseline_from(results):
    """計測結果を集計し、基準値と同じ形の辞書にする(--reference 用)。

    文字種比率は総文字数で、文長と読点密度は文数で重み付けした平均を取る。
    """
    total = sum(r["total"] for r in results)
    n_sent = sum(r["sentences"] for r in results)
    base = {k: sum(r["ratio"][k] * r["total"] for r in results) / total
            for k in ("kanji", "hiragana", "katakana")}
    base["sentence_len"] = (sum(r["sentence_len"] * r["sentences"] for r in results) / n_sent
                            if n_sent else 0)
    base["comma_per_sent"] = (sum(r["comma_per_sent"] * r["sentences"] for r in results) / n_sent
                              if n_sent else 0)
    return {k: round(v, 2) for k, v in base.items()}


def load_baseline(path):
    """--baseline のJSONを読む。書かれていない指標は既定値(国語研1979)のまま使う。"""
    data = json.loads(Path(path).read_text(encoding="utf-8"))
    base = dict(BASELINE)
    for k in BASELINE_KEYS:
        if k in data:
            base[k] = float(data[k])
    return base, data.get("label", str(path))


def save_baseline(path, base, label):
    data = {"label": label, **{k: base[k] for k in BASELINE_KEYS}}
    Path(path).write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def report(results, baseline=BASELINE, label=BASELINE_LABEL, locate=False):
    print(f"{'漢字':>6}{'ひら':>6}{'文長':>6}{'読点':>6}{'段落':>6}{'200+':>6}  ファイル")
    print(f"{'-'*6}{'-'*6}{'-'*6}{'-'*6}{'-'*6}{'-'*6}  {'-'*40}")

    for r in results:
        k = r["ratio"]["kanji"]
        h = r["ratio"]["hiragana"]
        over = f"{r['para_over']}/{r['paragraphs']}"
        print(
            f"{k:5.1f}{mark(k, baseline['kanji'])}"
            f"{h:5.1f}{mark(h, baseline['hiragana'], higher_is_worse=False)}"
            f"{r['sentence_len']:5.0f}{mark(r['sentence_len'], baseline['sentence_len'])}"
            f"{r['comma_per_sent']:5.2f}{mark(r['comma_per_sent'], baseline['comma_per_sent'], higher_is_worse=False)}"
            f"{r['para_mean']:5.0f} "
            f"{over:>6}  {Path(r['path']).name}"
        )

    if len(results) > 1:
        tw = sum(r["total"] for r in results)
        print(f"\n{len(results)}件 / 総{tw:,}字")
        for key, name in (("kanji", "漢字"), ("hiragana", "ひらがな")):
            w = sum(r["ratio"][key] * r["total"] for r in results) / tw
            print(f"  {name}: {w:.1f}%  (基準 {baseline[key]}%)")
        tp = sum(r["paragraphs"] for r in results)
        to = sum(r["para_over"] for r in results)
        print(f"  200字超の段落: {to}/{tp} ({to/tp:.1%})" if tp else "")

    print(f"\n凡例: ! = 基準から読みにくい方向へ乖離 / + = 読みやすい方向")
    print(f"基準値: {label}")

    if not locate:
        return

    print(f"\n{'='*60}\n修正候補\n{'='*60}")
    for r in results:
        longs = find_long_sentences(r["_paras"])
        overs = [(ln, n, t) for ln, t, n in r["_paras"] if n >= PARA_LIMIT]
        if not longs and not overs:
            continue
        print(f"\n## {r['path']}")
        for ln, n, text in sorted(overs, key=lambda x: -x[1]):
            print(f"  L{ln}: 段落{n}字  {strip_links(text)[:52]}...")
        for ln, n, c, s in sorted(longs, key=lambda x: -x[1]):
            print(f"  L{ln}: 文{n}字(読点{c})  {s[:52]}...")


def expand(patterns):
    out = []
    for pat in patterns:
        hits = globmod.glob(pat, recursive=True) if any(c in pat for c in "*?[") else [pat]
        out.extend(h for h in hits if h.endswith((".md", ".markdown", ".txt")))
    return sorted(set(out))


def main():
    ap = argparse.ArgumentParser(description="日本語文書の可読性を計測する")
    ap.add_argument("paths", nargs="+", help="ファイルまたはglobパターン")
    ap.add_argument("--locate", action="store_true", help="修正候補の箇所を file:line で出す")
    ap.add_argument("--profile", action="store_true",
                    help="外部コーパスではなく自分の平常値(PROFILES)と比較する")
    ap.add_argument("--json", action="store_true", help="JSON形式で出力")
    ap.add_argument("--sort", choices=["over", "para", "kanji", "name"], default="over",
                    help="並び順(既定: 200字超の段落の多い順)")
    src = ap.add_mutually_exclusive_group()
    src.add_argument("--reference", action="append", metavar="GLOB",
                     help="手元のテキスト(コーパス)を同じ方法で測り、その値を基準値にする"
                          "(複数あれば繰り返し指定する)")
    src.add_argument("--baseline", metavar="JSON",
                     help="基準値をJSONファイルから読む(--save-baseline で作ったもの等)")
    ap.add_argument("--save-baseline", metavar="JSON",
                    help="使った基準値をJSONに保存する(--reference の結果を再利用するため)")
    args = ap.parse_args()

    baseline, label = BASELINE, BASELINE_LABEL
    if args.baseline:
        baseline, label = load_baseline(args.baseline)
    elif args.reference:
        refs = [r for r in (analyze(f) for f in expand(args.reference)) if r]
        if not refs:
            print("--reference に解析できるファイルがありません", file=sys.stderr)
            return 1
        baseline = baseline_from(refs)
        label = f"--reference {len(refs)}件・{sum(r['total'] for r in refs):,}字"
    if args.save_baseline:
        save_baseline(args.save_baseline, baseline, label)

    files = expand(args.paths)
    if not files:
        print("対象ファイルが見つかりません", file=sys.stderr)
        return 1

    results = [r for r in (analyze(f) for f in files) if r]
    if not results:
        print("解析できる本文がありません", file=sys.stderr)
        return 1

    keys = {
        "over": lambda r: -(r["para_over"] / r["paragraphs"] if r["paragraphs"] else 0),
        "para": lambda r: -r["para_mean"],
        "kanji": lambda r: -r["ratio"]["kanji"],
        "name": lambda r: r["path"],
    }
    results.sort(key=keys[args.sort])

    if args.json:
        for r in results:
            r.pop("_paras", None)
        print(json.dumps(results, ensure_ascii=False, indent=2))
    else:
        report(results, baseline, label, locate=args.locate)
        if args.profile:
            profile_report(results)
    return 0


if __name__ == "__main__":
    sys.exit(main())

SKILL.md

ここに載せた SKILL.md と baselines.md は、私が手元で使っている版から、私の環境にしかない別スキルの名前などを除いて一般向けに調整したものです。

SKILL.md(クリックで展開)
SKILL.md
---
name: japanese-readability-metrics
description: "日本語文書の可読性を数値で計測し、直すべき箇所を file:line で特定する。段落長・文長・読点密度・漢字率を国立国語研究所の基準値と比較する。語彙・言い回しのチェックとは別に、文章の「形」を診断する。"
version: "1.0.0"
---

# 日本語文書の可読性計測

文章が読みにくいとき、原因が語彙なのか構造なのかを**測って切り分ける**スキル。

## 語彙チェックとの違い

「〜と言えるでしょう」のようなAI的な言い回しを文字列で検出するチェック(語彙・書式の規範)とは役割が違う。併用するもので、片方が他方を置き換えることはない。

| | 語彙・書式のチェック | このスキル |
|---|---|---|
| 対象 | 語彙・書式パターン | 文章の形状 |
| 手段 | 文字列マッチ | 計測 |
| 例 | 「〜と言えるでしょう」「**ラベル:**」 | 段落247字、文141字、読点0.9/文 |
| 性質 | 規範的(こう直せ) | 診断的(ここが外れている) |

語彙のルールで段落長・文長・読点密度は扱えない。逆にこのスキルは「浮き彫りにしており」を検出できない。

語彙チェックの公開スキルには、たとえば humanizer-ja(https://github.com/gonta223/humanizer-ja 、MITライセンス)がある。「浮き彫りにしており」のような定型評価語、太字+コロンの箇条書き、全角ダッシュなど、AIが書いた日本語に出やすい20パターンをチェックして書き換える。導入している場合は、下の推奨順序の 1 に入れる。

**推奨する順序:**

```
[0] このスキルで計測 → 何を直すべきか決める
 1  既存の推敲(構成・文体・語彙のチェックなど。例:humanizer-ja)
[2] このスキルで再計測 → 直った証拠を出す
```

## いつ使うか

- 「なんか読みにくい」と言われたが原因が特定できないとき
- 推敲の前に、どこに手を入れるべきか決めたいとき
- 推敲の後に、実際に改善したか確認したいとき
- 複数記事の中から、手を入れるべきものを選びたいとき

## 使い方

```bash
S=~/.claude/skills/japanese-readability-metrics/scripts/measure.py

python3 $S 'content/posts/*.md'              # 一覧を計測
python3 $S --locate article.md               # 修正候補を file:line で出す
python3 $S --profile article.md              # 自分の平常値と比較する
python3 $S --sort para 'posts/*.md'          # 平均段落長の長い順
python3 $S --json article.md                 # JSON出力

# 基準値を差し替える(外部コーパスを使う場合)
python3 $S --reference 'corpus/*.txt' article.md                            # 手元のテキストを測って基準値にする
python3 $S --reference 'corpus/*.txt' --save-baseline base.json article.md  # その基準値を保存
python3 $S --baseline base.json article.md                                  # 保存した基準値で測る
```

出力の `!` は基準から読みにくい方向への乖離、`+` は読みやすい方向。

### --profile(自分の平常値との比較)

外部コーパス(1979年の雑誌)は年代もジャンルも違うため、ズレの方向しか分からない。
`--profile` は代わりに**自分の文章の実測値**と比べる。ファイル名に `-weekly-` / `-monthly-`
を含むものをニュースレター、それ以外を単発記事として自動で振り分け、対応する値と突き合わせる。

両者は性質が違うので、基準を分けてある。

**以下の値と `PROFILES` の初期値は筆者のブログの実測値(例)である。** 使う前に自分の記事を
`--json` で測り、`scripts/measure.py` の `PROFILES` と、ファイル名で振り分ける `classify()` を
自分の記事に合わせて書き換えること。

| | 漢字 | ひらがな | 文長 | 読点/文 | 段落 |
|---|---|---|---|---|---|
| ニュースレター(31本) | 32.3% | 28.1% | 53.8字 | 1.17 | 119字 |
| 単発記事(61本) | 29.4% | 43.3% | 43.1字 | 1.31 | 104字 |

**比較するのは平均文長・読点/文・平均段落長・200字超率の4つだけ。**
漢字率・カタカナ率・英数字率は比較対象から外してある。これらはテーマで決まる値で、
平常値に寄せにいくと「航空宇宙産業」のようなテーマ固有の語を削る誤った修正を招くため。

平常値は**目標ではなく記述**である。推敲後の実測値なので段落長は目安として使えるが、
「平常値どおりだから直さなくてよい」という判断には使わない。
記事が増えて実態が変わったら、`scripts/measure.py` の `PROFILES` を測り直して更新する。

### --reference / --baseline(外部コーパスで基準値を差し替える)

既定の基準値は1979年の雑誌(国語研)で、ライセンス上の制約なく使える論文の数値に限っている。
手元に比べたいテキストの集まり(コーパス)があるなら、`--reference` で同じ方法で測り、
その値を基準値にできる。対象の記事と同じ計測処理を通すので、数え方の違いによるずれが出ない。

- `--reference GLOB` は繰り返し指定できる(`--reference 'a/*.txt' --reference 'b/*.md'`)
- `--save-baseline base.json` で算出した基準値を保存し、次回から `--baseline base.json` で読める。
  大きなコーパスを毎回測り直さずに済む
- `--baseline` のJSONは次の形。書かなかった指標は既定値(国語研1979)のまま使う

```json
{
  "label": "自分で用意したコーパス",
  "kanji": 28.0,
  "hiragana": 52.0,
  "katakana": 8.0,
  "sentence_len": 45.0,
  "comma_per_sent": 1.6
}
```

**使う前に確認すること:**

- **コーパスの利用条件は利用者が確認する。** 公開されている語彙表には利用目的の制限がある。
  BCCWJ語彙表は「研究、教育目的であれば無償」、SHC(昭和・平成書き言葉コーパス)の統合語彙表は
  CC BY-NC-SA 4.0(非営利)。業務や商用の文書に使えるかは、それぞれの条件で判断する
- **必要なのは本文のテキストで、語彙表(頻度リスト)ではない。** 語彙表は語単位の集計で、
  文字種比率も文長も計算できない。`--reference` に渡しても意味のある値にならない
- 読めるのは UTF-8 の `.md` / `.markdown` / `.txt` だけ。Shift_JIS などは変換してから渡す。
  ルビ(`《よみ》`)や入力者注(`[#…]`)のような記法が残っていると、文字種比率がずれるので除いておく
- 基準値が変わるのは漢字・ひらがな・カタカナ・文長・読点の5つ。段落長は従来どおり基準値なし
  (200字・80字のしきい値で判定)

## 数値の読み方(重要)

**漢字率が高いこと自体は、ほとんどの場合そのままでは問題にならない。**

漢字率が基準より高いとき、まず内訳を見る。

```bash
python3 -c "
import re,sys,collections
t=open(sys.argv[1],encoding='utf-8').read()
c=collections.Counter(re.findall(r'[一-鿿々]{2,}',t))
print('5字以上:',sum(v for k,v in c.items() if len(k)>=5),'件')
for w,n in c.most_common(15): print(f'  {n:3d} {w}')
" article.md
```

頻出語が固有名詞・専門用語(「航空宇宙産業」「経済産業省」「土偶」など)なら、**開く対象はない**。テーマの必然であり、ここを削ろうとすると内容が損なわれる。

一方「〜することができます」のような冗長表現が並んでいるなら、それは語彙チェック(humanizer-ja など)の担当。

### 経験則

実際に診断すると、原因は次の順で見つかることが多い。

1. **段落が長い**(200字超が2割以上)← 「字面が詰まって見える」の主因
2. **文が長い**(80字超)← 「一文が頭に入ってこない」の主因
3. **読点が少ない**(1文あたり1.2未満)← 同じ長さの文でも切れ目がなく読みにくい
4. 漢字率 ← 上の3つを直しても解決しないときに初めて検討する

## 直し方

このスキルは箇所を示すところまで。実際の修正は内容を見て判断する。

**段落分割は、既存の話題の切れ目で割る。** 接続詞やつなぎの文を新しく創作しない。元の文をそのまま2つの段落に分けるだけで、たいていは足りる。

**長文は構造を見る。** 読点を足して数値を合わせにいくのではなく、主語と述語が離れている原因(入れ子の修飾、複数の事実の詰め込み)を解く。読点の数は結果として増えることが多い。

**出典・引用は元の主張に付けたまま動かす。** 文を分割するとき、`[出典](URL)` がどの事実を支えていたか確認する。

## 基準値について

`references/baselines.md` に出典と数値、そして**その限界**を記載してある。使う前に一度読むこと。
`--reference` / `--baseline` で差し替えた場合は、出力の末尾の「基準値:」にその旨が表示される。

要点だけ:

- 基準値は1979年の雑誌55誌・約35万字(国立国語研究所)
- 分母は総文字数(記号・句読点を含む)
- **46年前の紙媒体なので、技術文書にそのまま当てはめない**。特に英数字は基準側が0.51%しかない
- 合否判定ではなく、ズレの方向を見るための参照点

### jReadability を使う場合の注意

`references/baselines.md` にリーダビリティ公式を載せてあるが、**その「漢語率」は語種の比率で、このスキルが測る漢字率とは別物**。measure.py の出力を公式に代入してはいけない。

語種レベルの判定が要るなら https://jreadability.net/sys/ にテキストを貼る(形態素解析込み、インストール不要)。

## 制約

- 形態素解析を使わないので、語種(漢語/和語)の判別はできない
- しきい値(段落200字・文80字)はコーパス由来ではなく実務上の経験値
- front matter は計測対象外(タイトルの文字種は含まない)
- コードブロック(``` と ~~~)・インラインコード・URL・HTMLタグは除外して計測する
- 見出し・箇条書き・番号付きリスト・表・引用・画像の行は段落として数えない

references/baselines.md

baselines.md(クリックで展開)
baselines.md
# 基準値と出典

## 国立国語研究所「各種文章の字種比率」

- 著者: 佐竹秀雄
- 出典: https://repository.ninjal.ac.jp/record/1332/files/kkrep_071_07.pdf
- 調査対象: 1979年7〜8月発行の雑誌55誌から抽出した約35〜36万字
- 分類: 小説/評論・論文/実用・解説/ルポ・報告/インタビュー・座談会/随筆・エッセー/読者投稿

### 全体値(表2)

| 文字種 | 比率 |
|---|---|
| 漢字 | 26.52% |
| ひらがな | 57.29% |
| カタカナ | 6.95% |
| 記号 | 1.49% |
| 句点 | 2.27% |
| 読点 | 3.96% |
| スペース | 1.00% |
| 英文字・洋数字 | 0.51% |

**分母は総文字数。** 各行の合計が100%になることを確認済み(記号・句読点・スペースを含む)。

句点2.27%から平均文長は約44字、読点3.96%÷句点2.27%から読点は1文あたり約1.74個と逆算できる。

### 記事種類別(表3、ラベルが明確に読み取れた行のみ)

| 記事種類 | 漢字 | ひらがな | カタカナ |
|---|---|---|---|
| 実用・解説 | 23.39% | 53.91% | 13.75% |
| ルポ・報告 | 31.41% | 51.27% | 7.34% |
| 随筆・エッセー | 26.05% | 59.59% | 5.63% |
| 読者投稿 | 26.24% | 60.27% | 5.03% |

※ PDFはスキャン画像のOCRで、一部の行ラベルが崩れている。上記以外の行は引用しない。

### 個別作品のばらつき

漢字含有率は最小10.47%〜最大46.58%。論文自身が「文章の種類や内容によってさまざまな値をとる」ことを主題にしており、**単一の「正しい比率」は存在しない**という立場をとっている。

## 重要な注意:この基準値の限界

**1979年の雑誌データである。** 46年前の紙媒体が対象で、技術ブログとは前提が違う。

特に英数字は0.51%しかない。固有名詞・製品名・コマンド名が入る技術文書では10%を超えることがあり、その分だけひらがな比率が構造的に押し下げられる。ひらがなが57.29%に届かないことを、ただちに問題と見なしてはいけない。

**合否判定ではなく、ズレの方向を見るための参照点として使う。**

## jReadability(李在鎬らのリーダビリティ公式)

- 論文: 李在鎬「日本語教育のための文章難易度に関する研究」早稲田日本語教育学 第21号
- https://researchmap.jp/jhlee/published_papers/1284306/attachment_file.pdf
- システム: https://jreadability.net/sys/

```
X = 11.724 − 0.056×平均文長 − 0.126×漢語率 − 0.042×和語率
          − 0.145×動詞率 − 0.044×助詞率        (R² = 0.896)
```

### 使用上の最重要注意

**ここでいう「漢語率」は語種(word origin)の比率であり、文字種の漢字率とは別物である。**

- 漢語率 = 漢語の語数 ÷ 総語数(形態素解析が必要)
- 漢字率 = 漢字の文字数 ÷ 総文字数(measure.py が測るもの)

measure.py の出力をこの式に代入してはいけない。まったく違う値が出る。

語種レベルの分析が必要なら jreadability.net にテキストを貼る(インストール不要)。

### 判定基準(表5)

| リーダビリティ値 | レベル |
|---|---|
| 6.4–5.5 | 初級前半 |
| 5.4–4.5 | 初級後半 |
| 4.4–3.5 | 中級前半 |
| 3.4–2.5 | 中級後半 |
| 2.4–1.5 | 上級前半 |
| 1.4–0.5 | 上級後半 |

論文が挙げる参考値: 白書コーパス=上級前半〜超級/新聞記事=上級前半/書籍コーパス=中級前半〜上級前半/Yahoo!知恵袋=初級後半〜中級後半。

白書・新聞が「上級前半」である点は重要で、**白書や統計を情報源にする文章が硬くなるのは、ある程度まで必然**である。

## 外部コーパスで基準値を差し替える場合

measure.py の `--reference` は、手元のテキストを記事と同じ処理で測って基準値にする。
`--baseline` は数値をJSONで直接与える(使い方は SKILL.md)。

既定値をこの論文の数値に限っているのは、ライセンス上の制約がないためである。
調べた範囲では、公開されている国語研系の語彙データは利用目的が限られ、しかも語単位だった。

| データ | 利用条件 | 文字種比率 |
|---|---|---|
| BCCWJ 語彙表 | 「研究、教育目的であれば無償」 | なし(語単位) |
| SHC 統合語彙表 | CC BY-NC-SA 4.0(非営利) | なし(語単位) |
| GitHub `a1da4/shc-data` | CC BY-SA 4.0 | なし(SHCを加工した n-gram・単語ベクトルで、語彙表そのものではない) |

研究・教育目的など条件を満たす人は、これらのコーパスの**本文テキスト**を入手できれば
`--reference` に渡せる。語彙表そのものは文字種比率を計算できないので使えない。
どのコーパスを使う場合も、利用条件の確認は利用者の責任で行う。

- SHC 統合語彙表: https://repository.ninjal.ac.jp/records/2000043
- a1da4/shc-data: https://github.com/a1da4/shc-data

## BCCWJ 語彙表

- https://clrd.ninjal.ac.jp/bccwj/freq-list.html
- 短単位語彙表/長単位語彙表/品詞構成表/語種構成表(zip)

ページに「研究、教育目的であれば無償で自由にお使いになれます」とある。**商用利用にあたるかは要確認。** このスキルはBCCWJに依存しない設計にしてある。

## しきい値の根拠

measure.py が使う 200字(段落)・80字(文)は上記コーパスに由来しない、実務上の経験値である。

- 200字: スマートフォン画面で1段落がほぼ画面を埋める量
- 80字: 主語と述語の距離が開き、修飾関係を保持しにくくなる目安

絶対的な基準ではないので、超えていても内容上必要なら残してよい。

## 自分の平常値(--profile で使う)

外部コーパスと違い、これは「あるべき値」ではなく **筆者のブログの実測値(例)**である。
自分で使うときは、自分の記事を測った値に置き換える。
2026-09-23 時点、推敲後(段落分割・全角ダッシュ除去を適用したあと)の値。

| | 本数 | 漢字 | ひらがな | カタカナ | 英数 | 文長 | 読点/文 | 段落 | 200字超 |
|---|---|---|---|---|---|---|---|---|---|
| ニュースレター | 31 | 32.3% | 28.1% | 13.1% | 15.9% | 53.8字 | 1.17 | 119字 | 0.6% |
| 単発記事 | 61 | 29.4% | 43.3% | 9.2% | 8.4% | 43.1字 | 1.31 | 104字 | 0.6% |

ニュースレターは白書・統計・企業発表を情報源にするため、漢字・カタカナ・英数字が多く、
文も長くなる。単発記事(開発記録・歴史考察・エッセイ)とは性質が違うので基準を分けている。

### 比較に使ってよい指標

`--profile` が突き合わせるのは **平均文長・読点/文・平均段落長・200字超率** の4つだけ。

漢字率・カタカナ率・英数字率は意図的に除外している。これらはテーマが決める値であり、
平常値に寄せようとすると、固有名詞や専門用語を削るという誤った修正に向かうため。

### 更新のしかた

記事が増えて実態が変わったら測り直し、`scripts/measure.py` の `PROFILES` を書き換える。

```bash
python3 scripts/measure.py --json 'content/posts/*.md'
```

出力を集計して、加重平均(文数・段落数で重み付け)を取る。

### 注意

平常値は目標ではなく記述にすぎない。「平常値どおりだから直さなくてよい」という使い方をすると、
改善前の状態を正常として固定してしまう。実際、筆者のブログの推敲前は 200字以上の段落が 9.5% あり、
それを平常値にしていたら 172段落の修正は発生しなかった。

kanji_runs.py(漢字の内訳を見る補助スクリプト)

measure.py と同じ除外処理を使うため、measure.py を import しています。

kanji_runs.py
"""漢字の連続(2字以上)を長さ別に数え、頻出語を出す。"""
import collections
import glob
import re
import sys

sys.path.insert(0, sys.argv[1])
from measure import strip_front_matter, strip_links, strip_noise

runs = collections.Counter()
for path in glob.glob(sys.argv[2]):
    body, _ = strip_front_matter(open(path, encoding="utf-8").read())
    runs.update(re.findall(r"[一-鿿々〇〻]{2,}", strip_links(strip_noise(body))))

total = sum(runs.values())
for label, cond in (("2字", lambda n: n == 2), ("3字", lambda n: n == 3),
                    ("4字", lambda n: n == 4), ("5字以上", lambda n: n >= 5)):
    k = sum(v for w, v in runs.items() if cond(len(w)))
    print(f"{label:6} {k:6d}  {k / total:.1%}")
print("5字以上の頻出:", [(w, n) for w, n in runs.most_common() if len(w) >= 5][:8])
print("2字の頻出:", [(w, n) for w, n in runs.most_common() if len(w) == 2][:8])
2
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
2
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?