はじめに
LINEやXの文面を貼ると、その文章が何%「おじさん構文」かを判定する おじさん構文度診断 を作りました。
文章の「らしさ」を判定するなら生成AIに任せるのが手っ取り早いのですが、今回はあえて AIを使わず、ブラウザ内のJavaScript(正規表現)だけで採点 しています。貼られた文面を外部に一切送らないためです。
この記事では、その採点エンジンの実装と、狙いどおりの点数分布に調整するためのテストの組み方をまとめます。
本記事について
今回の診断ツール開発およびこの記事執筆は、Claude Code(Anthropic社のAI)との協業で進めました。
AI時代の新しい開発スタイルの実践例として参考になれば幸いです。
この記事で分かること
- 絵文字を「見た目の1文字」で正しく数える方法(
Intl.Segmenter) - 正規表現のルールで文章を5項目に採点し、マッチ箇所を色付け用に記録する方法
- 「1項目だけで高得点にならない」ための上限設計
- 目標の点数帯をテスト文で固定し、調整を回す方法
- 古いiPhoneのSafariで後読み(
(?<=))が使えない問題の回避
この記事の対象者
- 文章の特徴を数えるツールをJavaScriptで作りたい方
- 個人情報を含みうる入力を、サーバーに送らずに処理したい方
- WordPressにちょっとした診断ツールを載せたい方
動作環境・使用技術
- 開発環境: Windows 11
- 言語: HTML, CSS, JavaScript(ES5相当+
\p{}を使う正規表現) - CMS: WordPress(固定ページにカスタムHTMLとして配置)
- テスト: Node.js
- 開発AI: Claude Code
- API: なし(累計回数だけ
admin-ajax.phpで数える)
完成品・デモ
例えば次の文面を貼ると、
おはよ〜😆☀️ ◯◯ちゃん今日も会えて嬉しいナ💕 朝ごはんちゃんと食べたかな?😋 オジサンは心配だヨ〜(笑)
結果は 66%(立派なおじさん構文)、内訳は A絵文字18/Bカタカナ17/C句読点3/D気遣い15/E自分語り13 になります。結果ページでは、どこで加点されたかを項目ごとの色で塗り分けて表示します。
技術構成
診断ページ(固定ページ・カスタムHTML)
└ ojisanScore(text) … 採点エンジン(ブラウザ内で完結)
↓ sessionStorage に {点数, 文面, 色付け位置} を保存
結果ページ(別の固定ページ・noindex)
└ URLの ?k=0812160416 から点数を復元
同じブラウザなら sessionStorage から文面を読み出して色付け表示
点数はURLパラメータで渡しますが、文面そのものはURLにもサーバーにも出しません。シェアされたURLを他の人が開くと、%とタイプだけが表示されます。
採点は5項目×20点の合計です。
| 項目 | 数えるもの |
|---|---|
| A 絵文字・記号 | 文字数あたりの絵文字の多さ、絵文字の連続、顔文字・(笑)・♪ |
| B カタカナ・語尾 | 「〜だヨ」「〜ネ」、「サムイ」などのカタカナ語、「〇〇ちゃん」 |
| C 句読点・長文 | 文の長さ、細切れの読点、「…」「〜」の多さ |
| D 気遣い・質問攻め | 「?」の数、「ちゃんと食べた」「風邪」などの気遣い語 |
| E 自分語り・距離感 | 「オジサンは」、近況報告、「今度ご馳走する」などのお誘い |
実装手順
1. 入力の前処理
LINEのトーク履歴をまとめて貼った場合に備えて、日付行・時刻・送信者名・スタンプ行・URLを先に取り除きます。
function ojisanClean(raw) {
return String(raw || '')
.replace(/\r\n?/g, '\n')
.split('\n')
.filter(function (l) {
return !/^\s*\d{4}[\/.]\d{1,2}[\/.]\d{1,2}/.test(l) &&
!/^\s*\[(スタンプ|写真|動画|ファイル|アルバム)\]\s*$/.test(l);
})
.map(function (l) { return l.replace(/^\s*\d{1,2}:\d{2}\t[^\t]*\t/, ''); })
.join('\n')
.replace(/https?:\/\/\S+/g, '')
.trim();
}
LINEの「トーク履歴を送信」で書き出したテキストは 12:34<TAB>名前<TAB>本文 の形なので、タブ区切りの先頭2列を落とせば本文だけが残ります。
2. 絵文字を「見た目の1文字」で数える
'👍🏻'.length は4、Array.from('👨👩👧') は5要素になります。肌の色や家族の絵文字を1個として数えるために、Intl.Segmenter で書記素(grapheme)単位に分割します。
function ojisanGraphemes(text) {
if (typeof Intl !== 'undefined' && Intl.Segmenter) {
var seg = new Intl.Segmenter('ja', { granularity: 'grapheme' });
var out = [];
var it = seg.segment(text)[Symbol.iterator]();
for (var r = it.next(); !r.done; r = it.next()) out.push({ s: r.value.segment, i: r.value.index });
return out;
}
// Intl.Segmenter がない古いブラウザ向けのフォールバック(異体字セレクタ・ZWJ・肌色をまとめる)
// …省略…
}
各書記素に \p{Extended_Pictographic} を当てて絵文字かどうかを判定します。© ® ™ もこの範囲に入るので除外しています。位置(index)も持っておくと、そのまま色付け用の範囲として使えます。
絵文字の点数は個数ではなく 100字あたりの密度 で決めます。分母には最低50字を入れて、短文に絵文字が1つあるだけで跳ねないようにしています。
var per100 = emoji / Math.max(len, 50) * 100;
pts.A += per100 === 0 ? 0 : per100 < 3 ? 6 : per100 < 6 ? 10 : per100 < 10 ? 14 : 16;
pts.A += runs * 2; // 絵文字が2つ以上続いた箇所(😆☀️)ごとに加点
3. ルールで加点し、マッチ箇所を記録する
正規表現でマッチした箇所ごとに加点し、同時に [開始, 終了, 項目] を記録する関数を用意しました。
function each(axis, re, add, labelFn, hasPrefix) {
var m, n = 0;
re.lastIndex = 0;
while ((m = re.exec(text)) !== null) {
if (m[0] === '') { re.lastIndex++; continue; }
var off = m.index, str = m[0];
if (hasPrefix) {
// グループ1=前置き(加点対象外)、グループ2=印をつける部分
off += m[1].length;
str = m[2];
}
mark(axis, off, off + str.length, labelFn ? labelFn(str) : str);
pts[axis] += add;
n++;
}
return n;
}
例えばカタカナ語尾は「ひらがな・漢字の直後に来るカタカナ1〜2文字+区切り」で拾います。
each('B', /([ぁ-んー〜一-龯])(ダヨ|ダネ|ダナ|カナ|ヨネ|ナノ|ダゾ|ネ|ヨ|ナ|ゾ)(?=[\s、。!!??〜~ー…♪☆★))]|\p{Extended_Pictographic}|$)/gu, 6,
function (s) { return '〜' + s; }, true);
「だヨ」の「ヨ」だけに色を付けたいので、前の1文字をグループ1で受けて、印はグループ2にだけ付けています(hasPrefix)。
4. 各項目を20点で打ち止めにする
最後に項目ごとに Math.min(20, …) をかけてから合計します。
var k = {}, total = 0, main = 'A';
OJISAN_AXES.forEach(function (a) {
k[a] = Math.min(20, pts[a]);
total += k[a];
if (k[a] > k[main]) main = a;
});
こうすると、絵文字だけ・読点だけの文面は最大でも20%にしかなりません。若者の絵文字多めLINEが「立派なおじさん構文」と判定される事故を、構造的に防げます。いちばん点の高い項目は「主成分」として、結果ページの直し方の表示に使っています。
テスト・デバッグ
目標の点数帯をテスト文で固定する
ルール採点は、加点を1つ変えると別の文面の点数が動きます。そこで「この文面はこの範囲に入るべき」というテストを先に作り、調整のたびに全件を回しました。
var cases = [
// [グループ, 下限, 上限, 文面]
['メーカー例文', 60, 90, 'おはよ〜😆☀️ ◯◯ちゃん今日も会えて嬉しいナ💕 …'],
['普通の友だち', 0, 19, '了解!明日10時に駅でね'],
['仕事の連絡', 0, 39, 'お疲れさまです。先日ご依頼いただいた資料の件ですが、…'],
['若者の絵文字', 0, 49, 'まじで楽しかった😂😂 また行こ〜🥳🥳'],
['句読点おじさん', 40, 90, '〇〇ちゃん、おはよう。今日は、寒いね。…'],
// …全18本
];
最終的な結果は次のとおりです。
| グループ | 本数 | 結果 |
|---|---|---|
| メーカー例文 | 5 | 60〜72% |
| 句読点おじさん | 2 | 42〜45% |
| 若者の絵文字 | 3 | 16〜21% |
| 仕事の連絡 | 3 | 0〜8% |
| 普通の友だち | 5 | 0〜3% |
苦労したのは「読点」の扱い
いちばん手こずったのが C(句読点・長文)です。仕事のメールも読点が多いので、「1文あたりの読点の数」だけで加点すると、丁寧な業務連絡がおじさん扱いされてしまいます。
見比べてみると、おじさん構文は「今日も、一日、頑張ったね」のように 数文字ごとに読点を打つ のに対して、仕事の文面は長い節のあとに打ちます。そこで、読点で区切った断片が4文字以下のものを「細切れの読点」として別に数えるようにしました。
var shortCommas = 0;
text.split(/[。!!??\n]|\p{Extended_Pictographic}/u).forEach(function (s) {
s.split('、').slice(0, -1).forEach(function (seg) { if (seg.trim().length <= 4) shortCommas++; });
});
if (shortCommas >= 3) pts.C += shortCommas >= 4 ? 10 : 5;
これで、仕事の連絡は0〜8%に収まり、句読点おじさんは40%台まで上がるようになりました。
後読み (?<=…) は、古いiPhoneのSafariでは正規表現リテラル自体が構文エラーになり、ページのスクリプトがまるごと動かなくなります。「お疲れサマ」の「サマ」だけに印を付けたい場面でも、後読みではなく前置きをキャプチャグループで受ける形(上の hasPrefix)にしています。
工夫したポイント・苦労した点
-
文面を外に出さない:採点はブラウザ内で完結し、結果ページへの受け渡しも
sessionStorageだけです。URLに入れるのは5項目の点数(10桁)だけにしました。 - 内訳を必ず見せる:ルール採点は外れることもあります。どこで何点ついたかを色付きで見せることで、外れたときも「そこを数えたのか」と笑ってもらえる作りにしています。
- 本家でも100%にならない:おじさん構文メーカーの出力を本番で診断すると87%でした。LINE風の短文は C(句読点・長文)が伸びないためで、20点上限の設計どおりの挙動です。
- Nodeで日本語パスが読めない:このPCのNode.jsでは日本語を含むパスのファイル読み込みが落ちたため、テストは英字フォルダにコピーしてから実行しています。
まとめ
- 文章の特徴判定は、目的によってはAIなしの正規表現でも十分に作れる
- 絵文字は
Intl.Segmenterで書記素単位に数える - 項目ごとの上限で「1項目だけで高得点」を防ぐ
- 目標の点数帯をテスト文で固定してから調整する
外部に送れない入力を扱うツールでは、「AIを使わない」こと自体が機能になります。
サービスはこちら
免責事項
この診断は、文面に含まれる特徴をルールで数えて点数化する娯楽ツールです。書いた人の年齢・性別・人柄を判定するものではありません。特定の人をからかったり傷つけたりする目的での利用はご遠慮ください。