텍스트를 문장으로 어떻게 분할하나요?
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."]
텍스트가 올바르게 두 문장으로 분할됩니다. "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 속성에는 전체 원본 텍스트가 포함됩니다. 이 메타데이터는 문장 위치를 추적하거나 원본 텍스트를 재구성해야 할 때 유용합니다.
다양한 언어에서 문장 분리하기
언어마다 문장 경계 규칙이 다릅니다. 세그먼터는 지정된 로케일에 따라 동작을 조정합니다.
일본어에서는 문장이 쿠텐(kuten)이라고 불리는 전각 마침표 。로 끝날 수 있습니다.
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);
}
// 구형 환경을 위한 대체 방안
return text.split(/[.!?]\s+/).filter(s => s.length > 0);
}
console.log(splitSentences("Hello world. How are you?"));
// ["Hello world. ", "How are you?"]
이 함수는 Intl.Segmenter가 사용 가능할 때 이를 사용하고, 구형 환경에서는 정규식 분할로 대체합니다. 대체 방안은 약어 처리 및 언어별 경계 감지와 같은 기능을 잃지만 기본적인 기능을 제공합니다.