【ITニュース解説】Word Counting Is Harder Than It Looks: A Dev's Guide
2026年10月29日に「Dev.to」が公開したITニュース「Word Counting Is Harder Than It Looks: A Dev's Guide」について初心者にもわかりやすく解説しています。
ITニュース概要
単純な単語カウントは、アポストロフィーやハイフン、絵文字、多言語などの扱いで間違いやすい。ブラウザの`Intl.Segmenter`を使えば、言語のルールを考慮し、より正確な単語や文字のカウントを実現できる。ユーザーのプライバシー保護のため、カウント処理はクライアント側で行い、最終的な数字は利用システムで確認すべきだ。
ITニュース解説
多くの開発者が一度は経験するであろうテキスト処理のタスクに、単語数のカウントがある。一見すると簡単そうに見えるこの処理だが、実は私たちが思っているよりもはるかに奥深く、注意が必要な問題が潜んでいる。システムエンジニアを目指す皆さんにとって、この「単語数カウント」という一見シンプルな機能が、いかに多くの技術的な課題とユーザー体験に関わる深い洞察を含んでいるかを理解することは、今後の開発において非常に重要な視点となるだろう。
まず、多くの開発者が最初に思いつく単語数カウントの方法は、テキストを空白で分割し、その数を数えるというものだ。例えば、JavaScriptでtext.trim().split(/\s+/).lengthのようなコードを書く。これは、整形されたきれいな文章であれば、それなりに正しく機能するように見える。しかし、ユーザーが実際に使用する場面では、この素朴な実装が簡単に破綻してしまう。ユーザーが長文を貼り付けた際、Microsoft WordやGoogle ドキュメント、さらには異なるブラウザ間でカウント結果が一致しないというバグ報告が上がってくるのだ。この不一致は、単なる丸め誤差ではなく、「テキストの分割(セグメンテーション)」という根本的な問題に起因している。
なぜ素朴な「空白で分割」する方法が失敗するのか、具体的な例を挙げてみよう。 例えば、「don't」という単語。アポストロフィが含まれるが、これは通常1つの単語として扱われる。同様に、「well-known」のようなハイフンでつながれた複合語も1つの単語だ。しかし、「hello—world」のように「emダッシュ」(長いダッシュ記号)で区切られた場合は、「hello」と「world」の2つの単語として数えるのが一般的である。また、「3.14」のような小数点を含む数字は1つの単語と見なされるべきだ。さらに、絵文字は文字としては存在するが、単語としては数えない、というのが多くのユーザーの感覚だろう。
素朴な空白分割では、「don't」や「well-known」は偶然正しく数えられることもあるが、「hello—world」は1つのトークンとして認識されてしまい間違える。「3.14」は偶然うまくいくかもしれないが、絵文字は単語としてカウントしてしまう。これはユーザーにとって明らかに不自然な結果だ。さらに深刻な問題は、中国語のように単語間に空白を使わない言語が混在するテキストだ。このような場合、空白で分割する正規表現では、どこで中国語の単語が始まり、どこで終わるのかを判断することさえできない。
このような複雑なテキストの分割問題に対し、現代のブラウザは強力な解決策を提供している。それがIntl.SegmenterというAPIだ。これは、言語のルールに基づいてテキストを正確に分割するために作られた機能で、まさにこの単語数カウントのために存在する。Intl.Segmenterを使った単語数カウント関数は、以下のような形になる。まず、古いブラウザのために空白分割のフォールバックを用意しつつ、new Intl.Segmenter(undefined, { granularity: "word" })で単語単位での分割を行うセグメンターオブジェクトを生成する。そして、テキストをセグメンターで分割し、それぞれのセグメントが「単語らしいか(isWordLike)」というプロパティを確認しながらカウントしていくのだ。
このisWordLikeというフィルターが非常に重要で、これを使うことで絵文字のような「単語ではないが文字としては存在する」要素を単語数から除外し、ユーザーが期待する正確なカウントを実現できる。また、Intl.Segmenterを使う上で見過ごせないのが「ロケール(言語・地域設定)」の引数だ。undefinedを渡せばブラウザのデフォルトロケールが使われるが、特定の言語に特化したツールであれば、その言語のロケールを明示的に指定すべきである。なぜなら、単語の分割ルールは言語によって異なるため、誤ったロケールを使うと、自信満々に間違ったカウント結果を出してしまうことになるからだ。
単語数だけでなく、文字数をカウントする際にも同様の注意が必要だ。JavaScriptのtext.lengthは、文字数を数えるのではなく、UTF-16コードユニットの数を数える。これは、ユーザーが見ている「1文字」と必ずしも一致しない。例えば、多くの絵文字は1つの表示文字でも複数のコードユニットで構成されているため、text.lengthを使うと実際の文字数よりも多くカウントされてしまう。この問題もIntl.Segmenterで解決できる。granularity: "grapheme"を指定することで、ユーザーが1文字と認識する単位(書記素)で正確に文字数をカウントできる。これを単語数とともに表示することで、ユーザーはより正確な情報を得られるのだ。
さらに、単語カウンターが提供することの多い「文章数」「段落数」「読書時間」といった情報も、それぞれに独自の課題と限界があることを理解しておくべきだ。
文章数も、句読点だけで分割するのは難しい。略語(Dr., e.g.)や小数点(3.14)、省略記号などがピリオドを文章の区切りではないと誤認させる原因となる。Intl.Segmenterのgranularity: "sentence"を使えば、一般的なケースはかなり正確に処理できるが、すべての略語を正しく解釈することはできないため、完全ではないことを認識する必要がある。
段落数は、比較的信頼性の高い数値だ。一般的には、空白行で分割すればよいが、貼り付けられたテキストの中で単一の改行が段落の区切りなのか、単なるソフトラップ(折り返し)なのかを判断する必要がある場合もある。この判断基準は、ツールのユーザーインターフェースで明確に文書化すべきだ。
読書時間は、あくまで「推定値」であり、厳密な事実ではない。これは、一般的な読書速度という仮定に基づいて計算されるため、読者のスキルやテキストの内容によって大きく変動する。そのため、UI上では「約N分」のように曖昧な表現を使い、正確な数値として提示すべきではない。読書時間を通してユーザーがテキストの質を判断しようとしないよう、これが目安であることを伝えることが重要だ。
異なる単語カウンターツール間で結果が異なるのは、ツールが採用しているセグメンテーションルールが異なるためであり、これは避けられないことだ。ユーザーが「Wordでは512語だったのに、あなたのツールでは508語だ」と報告してきた場合、自分のツールの数値が正しいと主張するのではなく、異なるツールが異なるルールを適用していることを説明し、厳密な文字数制限がある場合は「最終的に提出する先のシステムが公式の数値である」という考え方を伝えるべきだ。ユーザーに対しては、提出する内容を正確に貼り付けること、単語カウンターはあくまで目安として使うこと、そして何よりも、最終的なテキストは提出先のシステム(エディタ、CMS、アプリケーションポータルなど)で必ず検証すること、という実用的なアドバイスを提供することが非常に役立つ。多くのカウンターツールが最後のステップを伝え忘れているが、提出先のルールを知りえない以上、自らのカウントが最終的な権威であると主張するのは不誠実だ。最終的な権威は提出先のシステムにあると伝えることで、ツールの信頼性はむしろ高まる。
このような単語カウンターツールは、ユーザーのプライバシー保護を最優先に考えるべきである。ユーザーが入力するテキストは、未発表のエッセイ、履歴書、クライアントへのドラフト、原稿など、機密性の高い内容であることが非常に多い。これらのテキストを単語数を数えるためだけにサーバーにアップロードさせることは、ユーザーにとって大きなプライバシーリスクとなるが、技術的な正当性はない。なぜなら、単語のセグメンテーション処理は、現代のブラウザ上で完全にローカルで実行できるからだ。ツールが「未完成の作業をここに貼り付けてください」と謳う以上、そのアーキテクチャ自体がプライバシーポリシーとして機能しなければならない。サーバーへのアップロード、アカウントの作成、貼り付けられたテキストの分析など、一切の通信を発生させない「ローカルファースト」の設計は、この種のツールにとって単なる「あれば便利」な機能ではなく、必須の機能であると言える。
まとめると、正確で信頼性の高い単語カウンターを構築するために、以下のチェックリストを念頭に置くと良いだろう。まず、単語数のカウントにはIntl.Segmenterをgranularity: "word"で使い、isWordLikeフィルターを適用すること。古いブラウザのために空白分割のフォールバックも準備しておくこと。次に、文字数はUTF-16コードユニットではなく、Intl.Segmenterのgranularity: "grapheme"を使ってユーザーが認識する書記素単位でカウントすること。そして、単語、文字、文章、段落、読書時間をまとめて表示し、それぞれが異なる質問に答える情報であることを明確にすること。さらに、厳密な文字数制限がある場合は、提出先のシステムが最終的なカウントの権威であることを率直に伝えること。最後に、最も重要な点として、テキストのアップロード、アカウント、分析など一切を行わず、すべての処理をユーザーのブラウザ内で完結させる「ローカルファースト」のアーキテクチャを採用すること。そして、ユーザーが設定した単語の目標や制限は、ユーザー自身のデバイスに永続的に保存されるようにすることだ。
このような細やかな配慮と技術的な理解が、ユーザーにとって真に価値があり、信頼できるツールを生み出す鍵となる。単語数カウントという一見些細な機能の裏には、国際化、プライバシー、ユーザー体験といった多岐にわたる開発の知見が詰まっていることを理解し、今後のシステム開発に役立ててほしい。