テキストを文に分割する方法

Intl.Segmenterを使用して、句読点、略語、言語固有のルールを処理するロケール対応の境界検出でテキストを文に分割します。

はじめに

翻訳、分析、表示のためにテキストを処理する際、個々の文に分割する必要がよくあります。正規表現を使用した単純なアプローチは失敗します。なぜなら、文の境界はピリオドの後にスペースが続くよりも複雑だからです。文は疑問符、感嘆符、三点リーダーで終わることがあります。ピリオドは「Dr.」や「Inc.」などの略語に現れますが、文を終わらせません。異なる言語では、文の終端記号として異なる句読点が使用されます。

Intl.Segmenter APIは、ロケール対応の文境界検出を提供することで、この問題を解決します。異なる言語における文境界を識別するルールを理解し、略語、数字、複雑な句読点などのエッジケースを自動的に処理します。

ピリオドで分割する問題

ピリオドの後にスペースが続く箇所で分割することで、テキストを文に分割しようとすることができます。

const text = "Hello world. How are you? I am fine.";
const sentences = text.split(". ");
console.log(sentences);
// ["Hello world", "How are you? I am fine."]

このアプローチには複数の問題があります。第一に、疑問符や感嘆符を処理しません。第二に、ピリオドを含む略語で分割してしまいます。第三に、最後の文を除く各文からピリオドが削除されます。第四に、ピリオドの後に複数のスペースがある場合に機能しません。

const text = "Dr. Smith works at Acme Inc. He starts at 9 a.m.";
const sentences = text.split(". ");
console.log(sentences);
// ["Dr", "Smith works at Acme Inc", "He starts at 9 a.m."]

これらの略語にはピリオドが含まれているため、テキストは「Dr.」と「Inc.」で誤って分割されます。文境界のルールを理解する、よりスマートなアプローチが必要です。

より複雑な正規表現の使用

より多くのケースを処理するために正規表現を改善できます。

const text = "Hello world. How are you? I am fine!";
const sentences = text.split(/[.?!]\s+/);
console.log(sentences);
// ["Hello world", "How are you", "I am fine", ""]

これは、空白が続くピリオド、疑問符、感嘆符で分割します。より多くのケースを処理しますが、略語では依然として失敗し、空文字列を作成します。また、各文から句読点が削除されます。

const text = "Dr. Smith works at Acme Inc. He starts at 9 a.m.";
const sentences = text.split(/[.?!]\s+/);
console.log(sentences);
// ["Dr", "Smith works at Acme Inc", "He starts at 9 a", "m", ""]

正規表現アプローチでは、文を終了するピリオドと略語に現れるピリオドを確実に区別することができません。すべてのエッジケースを処理する包括的な正規表現を構築することは非現実的です。言語ルールを理解するソリューションが必要です。

文分割にIntl.Segmenterを使用する

Intl.Segmenterコンストラクタは、ロケール固有のルールに基づいてテキストを分割するセグメンタを作成します。ロケールを指定し、granularityオプションを"sentence"に設定します。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you? I am fine!";
const segments = segmenter.segment(text);

for (const segment of segments) {
  console.log(segment.segment);
}
// "Hello world. "
// "How are you? "
// "I am fine!"

segment()メソッドは、セグメントオブジェクトを生成するイテラブルを返します。各セグメントオブジェクトには、そのセグメントのテキストを含むsegmentプロパティがあります。セグメンタは各文の末尾の句読点と空白を保持します。

Array.from()を使用してセグメントを配列に変換できます。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you? I am fine!";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["Hello world. ", "How are you? ", "I am fine!"]

これにより、各要素が元の句読点とスペースを含む文である配列が作成されます。

Intl.Segmenterが略語を処理する方法

セグメンタは一般的な略語パターンを理解し、略語内に現れるピリオドで分割しません。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Dr. Smith works at Acme Inc. He starts at 9 a.m.";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["Dr. Smith works at Acme Inc. ", "He starts at 9 a.m."]

テキストは正しく2つの文に分割されます。「Dr.」、「Inc.」、「a.m.」のピリオドは、セグメンタがこれらを略語として認識するため、文の区切りをトリガーしません。このエッジケースの自動処理が、Intl.Segmenterが正規表現アプローチより優れている理由です。

文から空白をトリミングする

セグメンタは各文に末尾の空白を含めます。必要に応じてこの空白をトリミングできます。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you? I am fine!";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment.trim());
console.log(sentences);
// ["Hello world.", "How are you?", "I am fine!"]

trim()メソッドは、各文から先頭と末尾の空白を削除します。これは、余分なスペースなしでクリーンな文の境界が必要な場合に便利です。

セグメントメタデータの取得

各セグメントオブジェクトには、元のテキスト内のセグメント位置に関するメタデータが含まれています。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you?";
const segments = segmenter.segment(text);

for (const segment of segments) {
  console.log({
    text: segment.segment,
    index: segment.index,
    input: segment.input
  });
}
// { text: "Hello world. ", index: 0, input: "Hello world. How are you?" }
// { text: "How are you?", index: 13, input: "Hello world. How are you?" }

indexプロパティは、元のテキスト内でセグメントが開始する位置を示します。inputプロパティには、完全な元のテキストが含まれています。このメタデータは、文の位置を追跡したり、元のテキストを再構築したりする必要がある場合に便利です。

異なる言語での文の分割

言語によって文の境界ルールは異なります。セグメンターは、指定されたロケールに基づいて動作を適応させます。

日本語では、文は句点と呼ばれる全角ピリオドで終わることができます。

const segmenter = new Intl.Segmenter("ja", { granularity: "sentence" });
const text = "私は猫です。名前はまだない。";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["私は猫です。", "名前はまだない。"]

テキストは日本語の文末記号で正しく分割されます。英語用に設定されたセグメンターでは、これらの境界を正しく認識できません。

ヒンディー語では、文はプールナ・ヴィラームと呼ばれる縦棒で終わることができます。

const segmenter = new Intl.Segmenter("hi", { granularity: "sentence" });
const text = "यह एक वाक्य है। यह दूसरा वाक्य है।";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["यह एक वाक्य है। ", "यह दूसरा वाक्य है।"]

セグメンターは、デーヴァナーガリー文字の句点を文の境界として認識します。このロケール対応の動作は、国際化されたテキスト処理において重要です。

多言語テキストに対する正しいロケールの使用

複数の言語を含むテキストを処理する場合は、テキストの主要言語に一致するロケールを選択してください。セグメンターは、指定されたロケールを使用して、適用する境界ルールを決定します。

const englishText = "Hello world. How are you?";
const japaneseText = "私は猫です。名前はまだない。";

const englishSegmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const japaneseSegmenter = new Intl.Segmenter("ja", { granularity: "sentence" });

const englishSentences = Array.from(
  englishSegmenter.segment(englishText),
  s => s.segment
);

const japaneseSentences = Array.from(
  japaneseSegmenter.segment(japaneseText),
  s => s.segment
);

console.log(englishSentences);
// ["Hello world. ", "How are you?"]

console.log(japaneseSentences);
// ["私は猫です。", "名前はまだない。"]

各言語に対して個別のセグメンターを作成することで、正確な境界検出が保証されます。言語が不明なテキストを処理する場合は、"en"のような汎用ロケールをフォールバックとして使用できますが、これにより英語以外のテキストの精度は低下します。

文の境界がないテキストの処理

テキストに文末記号が含まれていない場合、セグメンターはテキスト全体を単一のセグメントとして返します。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["Hello world"]

この動作は正しいです。テキストに文の境界が含まれていないためです。セグメンターは、単一の文を構成するテキストを人為的に分割しません。

空文字列の処理

セグメンターは空文字列を処理する際、空のイテレーターを返します。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// []

これにより空の配列が生成されます。これは空の入力に対する期待される結果です。

パフォーマンス向上のためのセグメンターの再利用

セグメンターの作成にはオーバーヘッドがあります。同じロケールとオプションで複数のテキストをセグメント化する必要がある場合は、セグメンターを一度作成して再利用してください。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });

const texts = [
  "First text. With two sentences.",
  "Second text. With three sentences. And more.",
  "Third text."
];

texts.forEach(text => {
  const sentences = Array.from(segmenter.segment(text), s => s.segment);
  console.log(sentences);
});
// ["First text. ", "With two sentences."]
// ["Second text. ", "With three sentences. ", "And more."]
// ["Third text."]

セグメンターを再利用する方が、テキストごとに新しいセグメンターを作成するよりも効率的です。

文カウント関数の構築

セグメンターを使用してテキスト内の文を数えることができます。

function countSentences(text, locale = "en") {
  const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
  const segments = segmenter.segment(text);
  return Array.from(segments).length;
}

console.log(countSentences("Hello world. How are you?"));
// 2

console.log(countSentences("Dr. Smith works at Acme Inc. He starts at 9 a.m."));
// 2

console.log(countSentences("Single sentence"));
// 1

console.log(countSentences("私は猫です。名前はまだない。", "ja"));
// 2

この関数はセグメンターを作成し、テキストを分割して、セグメント数を返します。略語や言語固有の境界を正しく処理します。

文抽出関数の構築

インデックスによってテキストから特定の文を抽出する関数を作成できます。

function getSentence(text, index, locale = "en") {
  const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
  const segments = Array.from(segmenter.segment(text), s => s.segment);
  return segments[index] || null;
}

const text = "First sentence. Second sentence. Third sentence.";

console.log(getSentence(text, 0));
// "First sentence. "

console.log(getSentence(text, 1));
// "Second sentence. "

console.log(getSentence(text, 2));
// "Third sentence."

console.log(getSentence(text, 3));
// null

この関数は指定されたインデックスの文を返します。インデックスが範囲外の場合はnullを返します。

ブラウザとランタイムのサポート確認

Intl.Segmenter APIは、モダンブラウザとNode.jsで利用可能です。2024年4月にウェブプラットフォームベースラインの一部となり、すべての主要ブラウザエンジンでサポートされています。

使用する前にAPIが利用可能かどうかを確認できます。

if (typeof Intl.Segmenter !== "undefined") {
  const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
  const text = "Hello world. How are you?";
  const sentences = Array.from(segmenter.segment(text), s => s.segment);
  console.log(sentences);
} else {
  console.log("Intl.Segmenter is not supported");
}

サポートされていない環境では、フォールバックを提供する必要があります。シンプルなフォールバックは基本的な正規表現分割を使用しますが、ロケール対応セグメンテーションの精度は失われます。

function splitSentences(text, locale = "en") {
  if (typeof Intl.Segmenter !== "undefined") {
    const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
    return Array.from(segmenter.segment(text), s => s.segment);
  }

  // Fallback for older environments
  return text.split(/[.!?]\s+/).filter(s => s.length > 0);
}

console.log(splitSentences("Hello world. How are you?"));
// ["Hello world. ", "How are you?"]

この関数は、利用可能な場合はIntl.Segmenterを使用し、古い環境では正規表現による分割にフォールバックします。フォールバックでは、略語の処理や言語固有の境界検出などの機能は失われますが、基本的な機能は提供されます。