JavaScriptでテキストを単語に分割する方法
Intl.Segmenterを使用して、単語間にスペースがない言語を含む、あらゆる言語のテキストから単語を抽出します。
はじめに
テキストから単語を抽出する必要がある場合、一般的なアプローチはsplit(" ")を使用してスペースで分割することです。これは英語では機能しますが、単語間にスペースを使用しない言語では完全に失敗します。中国語、日本語、タイ語、その他の言語は、単語の区切り文字なしでテキストを連続して記述しますが、ユーザーはそのテキスト内で明確な単語を認識します。
Intl.Segmenter APIはこの問題を解決します。これは、Unicode標準と各言語の言語規則に従って単語の境界を識別します。言語がスペースを使用するかどうかに関係なく、テキストから単語を抽出でき、セグメンターは単語の開始位置と終了位置を決定する複雑さを処理します。
この記事では、基本的な文字列分割が国際的なテキストで失敗する理由、異なる表記体系における単語境界の仕組み、およびIntl.Segmenterを使用してすべての言語でテキストを正しく単語に分割する方法について説明します。
スペースでの分割が失敗する理由
split()メソッドは、区切り文字が出現するたびに文字列を分割します。英語のテキストの場合、スペースで分割すると単語が抽出されます。
const text = "Hello world";
const words = text.split(" ");
console.log(words);
// ["Hello", "world"]
このアプローチは、単語がスペースで区切られていることを前提としています。多くの言語はこのパターンに従っていません。
中国語のテキストには単語間にスペースが含まれていません。
const text = "你好世界";
const words = text.split(" ");
console.log(words);
// ["你好世界"]
ユーザーは2つの明確な単語を認識しますが、split()は分割するスペースがないため、文字列全体を単一の要素として返します。
日本語のテキストは複数の文字体系を混在させ、単語間にスペースを使用しません。
const text = "今日は良い天気です";
const words = text.split(" ");
console.log(words);
// ["今日は良い天気です"]
この文には複数の単語が含まれていますが、スペースで分割すると1つの要素になります。
タイ語のテキストも、スペースなしで単語を連続して記述します。
const text = "สวัสดีครับ";
const words = text.split(" ");
console.log(words);
// ["สวัสดีครับ"]
このテキストには2つの単語が含まれていますが、split()は1つの要素を返します。
これらの言語では、単語の境界を識別するために異なるアプローチが必要です。
正規表現が単語境界で失敗する理由
正規表現の単語境界は、\bパターンを使用して、単語文字と非単語文字の間の位置を照合します。これは英語では機能します。
const text = "Hello world!";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["Hello", "world"]
このパターンは、正規表現エンジンが中国語、日本語、タイ語などのスクリプトの単語境界を認識しないため、スペースのない言語では失敗します。
const text = "你好世界";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["你好世界"]
正規表現は中国語の単語境界を理解しないため、文字列全体を1つの単語として扱います。
英語であっても、正規表現パターンは句読点、短縮形、特殊文字で誤った結果を生成する可能性があります。正規表現は、すべての表記体系にわたる言語的な単語分割を処理するように設計されていません。
言語間での単語境界とは
単語境界とは、テキスト内で1つの単語が終わり、別の単語が始まる位置です。異なる表記体系は、単語境界に対して異なる規則を使用します。
英語、スペイン語、フランス語、ドイツ語などのスペース区切り言語は、スペースを使用して単語境界を示します。「hello」という単語は、スペースによって「world」から分離されています。
中国語、日本語、タイ語などの連続表記言語は、単語間にスペースを使用しません。単語境界は意味論的および形態論的規則に基づいて存在しますが、これらの境界はテキスト内で視覚的に示されません。中国語の読者は、視覚的な区切り記号ではなく、言語への精通を通じて、1つの単語がどこで終わり、別の単語がどこで始まるかを認識します。
一部の言語では混合規則が使用されます。日本語は漢字、ひらがな、カタカナを組み合わせており、単語の境界は文字種の切り替わりや文法構造に基づいて発生します。
Unicode標準はUAX 29で単語境界規則を定義しています。これらの規則は、すべての文字体系における単語境界の識別方法を規定しています。規則は文字プロパティ、文字体系の種類、言語パターンを考慮して、単語の開始位置と終了位置を決定します。
Intl.Segmenterを使用してテキストを単語に分割する
Intl.Segmenterコンストラクタは、Unicode規則に従ってテキストを分割するセグメンタオブジェクトを作成します。ロケールと粒度を指定します。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
const segments = segmenter.segment(text);
最初の引数はロケール識別子です。2番目の引数はオプションオブジェクトで、granularity: "word"はセグメンタに単語境界で分割するよう指示します。
segment()メソッドは、セグメントを含む反復可能オブジェクトを返します。for...ofを使用してセグメントを反復処理できます。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
for (const segment of segmenter.segment(text)) {
console.log(segment);
}
// { segment: "Hello", index: 0, input: "Hello world!", isWordLike: true }
// { segment: " ", index: 5, input: "Hello world!", isWordLike: false }
// { segment: "world", index: 6, input: "Hello world!", isWordLike: true }
// { segment: "!", index: 11, input: "Hello world!", isWordLike: false }
各セグメントオブジェクトには次のプロパティが含まれます:
segment: このセグメントのテキストindex: 元の文字列内でこのセグメントが開始する位置input: セグメント化されている元の文字列isWordLike: このセグメントが単語か非単語コンテンツかを示す
isWordLikeプロパティの理解
テキストを単語でセグメント化すると、セグメンタは単語セグメントと非単語セグメントの両方を返します。単語には文字、数字、表意文字が含まれます。非単語セグメントには空白、句読点、その他の区切り文字が含まれます。
isWordLikeプロパティは、セグメントが単語であるかどうかを示します。このプロパティは、単語文字を含むセグメントの場合はtrue、空白、句読点、その他の非単語文字のみを含むセグメントの場合はfalseになります。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello, world!";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "Hello" true
// "," false
// " " false
// "world" true
// "!" false
isWordLikeプロパティを使用して、句読点や空白から単語セグメントをフィルタリングします。これにより、区切り文字なしで単語のみを取得できます。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello, world!";
const segments = segmenter.segment(text);
const words = Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment);
console.log(words);
// ["Hello", "world"]
このパターンは、スペースを使用しない言語を含む、あらゆる言語で機能します。
スペースなしのテキストから単語を抽出する
セグメンターは、スペースを使用しない言語の単語境界を正しく識別します。中国語テキストの場合、セグメンターはUnicodeルールと言語パターンに基づいて単語境界で分割します。
const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "你好" true
// "世界" true
セグメンターは、このテキスト内の2つの単語を識別します。スペースはありませんが、セグメンターは中国語の単語境界を理解し、テキストを適切に分割します。
日本語テキストの場合、セグメンターは混在するスクリプトの複雑さを処理し、単語境界を識別します。
const segmenter = new Intl.Segmenter("ja", { granularity: "word" });
const text = "今日は良い天気です";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "今日" true
// "は" true
// "良い" true
// "天気" true
// "です" true
セグメンターは、この文を5つの単語セグメントに分割します。「は」のような助詞が別の単語であることを認識し、「天気」のような複合語が単一の単位を形成することを認識します。
タイ語テキストの場合、セグメンターはスペースなしで単語境界を識別します。
const segmenter = new Intl.Segmenter("th", { granularity: "word" });
const text = "สวัสดีครับ";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "สวัสดี" true
// "ครับ" true
セグメンターは、この挨拶の中の2つの単語を正しく識別します。
単語抽出関数の構築
あらゆる言語のテキストから単語を抽出する関数を作成します。
function getWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment);
}
この関数は、スペース区切りおよび非スペース区切りの言語で機能します。
getWords("Hello, world!", "en");
// ["Hello", "world"]
getWords("你好世界", "zh");
// ["你好", "世界"]
getWords("今日は良い天気です", "ja");
// ["今日", "は", "良い", "天気", "です"]
getWords("Bonjour le monde!", "fr");
// ["Bonjour", "le", "monde"]
getWords("สวัสดีครับ", "th");
// ["สวัสดี", "ครับ"]
この関数は、言語や表記体系に関係なく、単語の配列を返します。
言語を超えて正確に単語をカウントする
単語のようなセグメントをカウントすることで、すべての言語で機能する単語カウンターを構築します。
function countWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments).filter(s => s.isWordLike).length;
}
この関数は、あらゆる言語のテキストに対して正確な単語数を生成します。
countWords("Hello world", "en");
// 2
countWords("你好世界", "zh");
// 2
countWords("今日は良い天気です", "ja");
// 5
countWords("Bonjour le monde", "fr");
// 3
countWords("สวัสดีครับ", "th");
// 2
カウント数は、各言語における単語境界のユーザー認識と一致します。
位置を含む単語の検索
containing()メソッドは、文字列内の特定のインデックスを含むセグメントを検索します。これは、カーソルがどの単語にあるか、またはどの単語がクリックされたかを判断するのに役立ちます。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world";
const segments = segmenter.segment(text);
const segment = segments.containing(7);
console.log(segment);
// { segment: "world", index: 6, input: "Hello world", isWordLike: true }
インデックス7は、インデックス6から始まる単語「world」内に含まれます。このメソッドは、その単語のセグメントオブジェクトを返します。
インデックスが空白または句読点内に含まれる場合、メソッドはisWordLike: falseを持つそのセグメントを返します。
const segment = segments.containing(5);
console.log(segment);
// { segment: " ", index: 5, input: "Hello world", isWordLike: false }
これは、ダブルクリックによる単語選択、カーソル位置に基づくコンテキストメニュー、現在の単語のハイライト表示などのテキストエディタ機能に使用します。
句読点と短縮形の処理
セグメンターは句読点を個別のセグメントとして扱います。英語の短縮形は通常、複数のセグメントに分割されます。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "I can't do it.";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "I" true
// " " false
// "can" true
// "'" false
// "t" true
// " " false
// "do" true
// " " false
// "it" true
// "." false
短縮形「can't」は「can」、「'」、「t」に分割されます。短縮形を単一の単語として保持する必要がある場合は、アポストロフィに基づいてセグメントをマージする追加のロジックが必要です。
ほとんどのユースケースでは、短縮形が分割されていても、単語のようなセグメントをカウントすることで意味のある単語数が得られます。
ロケールが単語セグメンテーションに与える影響
セグメンターに渡すロケールは、単語境界の決定方法に影響します。異なるロケールでは、同じテキストに対して異なるルールが適用される場合があります。
明確に定義された単語境界ルールを持つ言語の場合、ロケールにより正しいルールが適用されます。
const segmenterEn = new Intl.Segmenter("en", { granularity: "word" });
const segmenterZh = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
const wordsEn = Array.from(segmenterEn.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
const wordsZh = Array.from(segmenterZh.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
console.log(wordsEn);
// ["你好世界"]
console.log(wordsZh);
// ["你好", "世界"]
英語ロケールは中国語の単語境界を認識せず、文字列全体を1つの単語として扱います。中国語ロケールは中国語の単語境界ルールを適用し、2つの単語を正しく識別します。
セグメント化するテキストの言語に適したロケールを常に使用してください。
パフォーマンスのための再利用可能なセグメンターの作成
セグメンターの作成はコストがかかりませんが、複数の文字列でセグメンターを再利用することで、パフォーマンスを向上させることができます。
const enSegmenter = new Intl.Segmenter("en", { granularity: "word" });
const zhSegmenter = new Intl.Segmenter("zh", { granularity: "word" });
const jaSegmenter = new Intl.Segmenter("ja", { granularity: "word" });
function getWords(text, locale) {
const segmenter = locale === "zh" ? zhSegmenter
: locale === "ja" ? jaSegmenter
: enSegmenter;
return Array.from(segmenter.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
}
このアプローチでは、セグメンターを一度作成し、getWords()のすべての呼び出しで再利用します。セグメンターはロケールデータをキャッシュするため、インスタンスを再利用することで繰り返しの初期化を回避できます。
実践例:単語頻度分析ツールの構築
単語のセグメント化とカウントを組み合わせて、テキスト内の単語頻度を分析します。
function getWordFrequency(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
const words = Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment.toLowerCase());
const frequency = {};
for (const word of words) {
frequency[word] = (frequency[word] || 0) + 1;
}
return frequency;
}
const text = "Hello world! Hello everyone in this world.";
const frequency = getWordFrequency(text, "en");
console.log(frequency);
// { hello: 2, world: 2, everyone: 1, in: 1, this: 1 }
この関数は、テキストを単語に分割し、小文字に正規化して、出現回数をカウントします。あらゆる言語で機能します。
const textZh = "你好世界!你好大家!";
const frequencyZh = getWordFrequency(textZh, "zh");
console.log(frequencyZh);
// { "你好": 2, "世界": 1, "大家": 1 }
同じロジックで、変更なしに中国語のテキストを処理できます。
ブラウザサポートの確認
Intl.Segmenter APIは2024年4月にベースラインステータスに達しました。Chrome、Firefox、Safari、Edgeの現行バージョンで動作します。古いブラウザではサポートされていません。
APIを使用する前にサポートを確認してください。
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
// Use segmenter
} else {
// Fallback for older browsers
}
古いブラウザを対象とする本番アプリケーションの場合は、フォールバック実装を提供してください。シンプルなフォールバックでは、英語テキストにsplit()を使用し、その他の言語では文字列全体を返します。
function getWords(text, locale) {
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
return Array.from(segmenter.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
}
// Fallback: only works for space-separated languages
return text.split(/\s+/).filter(word => word.length > 0);
}
これにより、スペースで区切られていない言語の機能は制限されますが、古いブラウザでもコードが実行されます。
避けるべき一般的な間違い
多言語テキストに対してスペースや正規表現パターンで分割しないでください。これらのアプローチは一部の言語でのみ機能し、中国語、日本語、タイ語、その他のスペースのない言語では失敗します。
単語を抽出する際にisWordLikeでフィルタリングすることを忘れないでください。このフィルターがないと、結果にスペース、句読点、その他の単語以外のセグメントが含まれてしまいます。
テキストをセグメント化する際に誤ったロケールを使用しないでください。ロケールによって、どの単語境界ルールが適用されるかが決まります。中国語のテキストに英語のロケールを使用すると、誤った結果が生成されます。
すべての言語が同じ方法で単語を定義していると仮定しないでください。単語の境界は、表記体系や言語的慣習によって異なります。これらの違いに対応するには、ロケールを考慮したセグメント化を使用してください。
国際的なテキストに対してsplit(" ").lengthを使用して単語数をカウントしないでください。これはスペースで区切られた言語でのみ機能し、他の言語では誤ったカウントが生成されます。
単語セグメント化を使用するタイミング
次の場合に単語セグメント化を使用します。
- 複数の言語にわたるユーザー生成コンテンツの単語数をカウントする
- あらゆる表記体系で機能する検索およびハイライト機能を実装する
- 国際的なテキストを処理するテキスト分析ツールを構築する
- テキストエディタで単語ベースのナビゲーションまたは編集機能を作成する
- 多言語ドキュメントからキーワードまたは用語を抽出する
- あらゆる言語を受け入れるフォームで単語数制限を検証する
文字数のみが必要な場合は、単語セグメント化を使用しないでください。文字レベルの操作には書記素セグメント化を使用してください。
文の分割には単語セグメント化を使用しないでください。その目的には文の粒度を使用してください。
単語セグメント化が国際化にどのように適合するか
Intl.Segmenter APIは、ECMAScript国際化APIの一部です。このファミリーの他のAPIは、国際化のさまざまな側面を処理します。
Intl.DateTimeFormat: ロケールに応じて日付と時刻をフォーマットするIntl.NumberFormat: ロケールに応じて数値、通貨、単位をフォーマットするIntl.Collator: ロケールに応じて文字列をソートおよび比較するIntl.PluralRules: 異なる言語における数値の複数形を決定する
これらのAPIを組み合わせることで、世界中のユーザーに対して正しく動作するアプリケーションを構築するために必要なツールが提供されます。単語の境界を識別する必要がある場合は、単語粒度で Intl.Segmenter を使用し、書式設定と比較には他の Intl APIを使用してください。