개발 이야기3분 읽기

글자 수를 센다는 것

공백으로 자르면 일본어가 한 단어가 됩니다. 문자·단어·문장을 세는 일이 언어마다 어떻게 달라지는지 정리했습니다.

입력란 아래 "0/200자"를 붙이는 일은 value.length면 끝날 것 같습니다. 몇 가지 입력에서 사용자가 보는 것과 다른 숫자가 나옵니다.

무엇을 한 글자로 세는가

"가"      length=1   코드포인트=1   자소=1
"é"       length=1   코드포인트=1   자소=1
"é"       length=2   코드포인트=2   자소=1
"🇰🇷"      length=4   코드포인트=2   자소=1
"👨‍👩‍👧‍👦"      length=11  코드포인트=7   자소=1

세 번째 줄의 ée와 결합 악센트 두 조각입니다. 화면에는 두 번째 줄과 똑같이 보입니다.

'é' === 'é'                          // false
'é'.normalize('NFC') === 'é'         // true

한글도 같습니다. 완성형 은 한 글자, 조합형은 ++ 세 조각입니다. macOS 파일 이름이 조합형으로 저장되는 것이 알려진 사례입니다 — 같은 이름인데 문자열 비교가 실패합니다.

length는 UTF-16 코드 유닛 수입니다. 이모지 가족은 11이 나옵니다. 사용자가 보기에는 한 글자입니다.

셋 중 무엇을 셀지는 용도가 정합니다. 저장 한도라면 바이트 수, 트윗처럼 표시 한도라면 자소 수, API 명세가 코드포인트를 말한다면 코드포인트입니다. 세 값이 다 다를 수 있다는 것을 알고 고르면 됩니다.

공백으로 자르면 되는 언어와 아닌 언어

ko  공백분할 10개   분절 10개
en  공백분할 13개   분절 13개
ja  공백분할  1개   분절 16개

한국어와 영어는 공백 분할과 단어 분절이 같은 수를 냅니다. 한국어는 조사가 붙어도 어절이 공백으로 갈리기 때문입니다.

일본어는 아닙니다. 띄어쓰기를 하지 않아 문장 하나가 한 단어가 됩니다.

공백으로   ["日本語は分かち書きをしないので、空白で切ると一文が一語になります。"]
분절기로   ["日本語","は","分かち書き","を","しない","ので","空白","で"] …

중국어와 태국어도 같습니다. 브라우저와 Node에 있는 Intl.Segmenter가 이 일을 합니다.

const seg = new Intl.Segmenter('ja', { granularity: 'word' });
[...seg.segment(text)].filter(s => s.isWordLike).length

isWordLike로 거르지 않으면 구두점과 공백도 조각으로 셉니다.

문장 세기는 마침표로 안 된다

문단 2개
Segmenter 로 7개
마침표로 자르면 6개

마침표로 자르면 Dr. Kim에서 잘못 끊깁니다. 그런데 Intl.Segmenter도 같은 자리에서 끊었습니다.

1. "첫 문장입니다."
2. "두 번째 문장이죠!"
3. "세 번째?"
4. ""
5. "새 문단입니다."
6. "Dr."
7. "Kim 은 약어라 문장이 끊기지 않아야 합니다."

유니코드 문장 분절 규칙은 약어 목록을 갖고 있지 않습니다. 마침표 다음에 공백과 대문자가 오면 문장 경계로 봅니다. 약어를 제대로 처리하려면 언어별 사전이 필요하고, 그건 분절기의 범위 밖입니다.

빈 조각(4번)도 나옵니다. 문단 사이 줄바꿈입니다. 세기 전에 걸러야 합니다.

키워드 밀도가 재는 것

전체 27단어, 서로 다른 단어 25개
  문서의    2회  7.4%
  문서는    2회  7.4%
  검색엔진은 1회  3.7%

문서의문서는이 따로 세집니다. 한국어에서 조사가 붙은 형태를 그대로 세면 같은 낱말이 흩어집니다. 어간을 뽑으려면 형태소 분석이 필요하고, 단순 빈도 계산과는 다른 작업입니다.

영어에서도 runrunning이 갈립니다. 그래서 밀도 수치는 절대값보다 상대 비교로 읽는 편이 맞습니다 — 같은 방식으로 잰 두 글을 비교하는 용도입니다.

2-gram을 함께 보면 단어 하나로는 안 보이는 것이 나옵니다. 짧은 글에서는 모든 조합이 1회씩이라 의미가 없고, 분량이 있어야 반복되는 표현이 드러납니다.

확인 순서

  1. 글자 수 제한이 바이트인지 자소인지 정합니다. 이모지와 결합 문자에서 갈립니다
  2. 사용자에게 보이는 카운터는 자소 단위로 셉니다. Intl.Segmentergrapheme입니다
  3. 문자열 비교 전에 정규화합니다. macOS에서 온 한글 파일명이 대표적입니다
  4. 단어 수를 센다면 대상 언어를 확인합니다. 일본어·중국어·태국어는 공백 분할이 무의미합니다
  5. 문장 수는 참고값으로 씁니다. 약어와 소수점에서 분절기도 틀립니다

tools.onuel.dev단어·문자 수 세기는 문자·단어·문장·문단을 함께 세고 CJK 문자를 한 문장 통째로 세지 않습니다. 키워드 밀도 검사기는 어떤 단어와 구가 전체에서 몇 퍼센트를 차지하는지 냅니다. 목록을 다듬을 때는 줄 정렬·중복 제거로 정렬과 중복 제거를 한 번에 하고, 긴 글에서 주소나 메일만 뽑아낼 때는 이메일·URL 추출기를 씁니다. 두 글을 대조하려면 텍스트 비교가 있습니다.

  • #텍스트
  • #유니코드
  • #분절