텍스트를 문장으로 분할하는 방법은 무엇인가요?
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 생성자는 로케일별 규칙에 따라 텍스트를 분할하는 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 객체를 생성하는 iterable을 반환합니다. 각 segment 객체에는 해당 segment의 텍스트를 포함하는 segment 속성이 있습니다. segmenter는 각 문장 끝의 구두점과 공백을 보존합니다.
Array.from()를 사용하여 segment를 배열로 변환할 수 있습니다.
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가 약어를 처리하는 방법
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."]
텍스트가 두 문장으로 올바르게 분할됩니다. "Dr.", "Inc.", "a.m."의 마침표는 segmenter가 이를 약어로 인식하기 때문에 문장 구분을 트리거하지 않습니다. 이러한 엣지 케이스의 자동 처리가 Intl.Segmenter가 정규식 접근 방식보다 우수한 이유입니다.
문장에서 공백 제거
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);
// ["私は猫です。", "名前はまだない。"]
텍스트가 일본어 문장 종결자에서 올바르게 분리됩니다. 영어용으로 구성된 세그먼터는 이러한 경계를 올바르게 인식하지 못합니다.
힌디어에서는 푸르나 비람(purna viram)이라고 하는 세로 막대 ।로 문장이 끝날 수 있습니다.
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를 사용하며, 이전 환경에서는 정규식 분할로 대체됩니다. 대체 방식은 약어 처리 및 언어별 경계 감지와 같은 기능을 잃지만 기본 기능을 제공합니다.