# 「文字」の数え方 文字数をカウントする7つの方法 (Line Engineering) https://engineering.linecorp.com/ja/blog/the-7-ways-of-counting-characters/ ## 1. 文字の単位 ### バイト (Bytes) 文字をビット列で表したときの、バイト長のことを指す。 ``` A = "01100001" (1byte @ ASCII) あ = "10000010 10100000" (2bytes @ Shift-JIS) あ = "11100011 10000001 10000010" (3bytes @ UTF-8) 🍣 = "11110000 10011111 10001101 10100011" (4bytes @ UTF-8) 🍣 = "11011000 00111100 11011111 01100011" (4bytes @ UTF-16BE) 👨‍💻 = f09f91a8e2808df09f92bb (11bytes @ UTF-8) ``` ### コードポイント (Code Points) 世界中の文字やパーツをコード化したもの。 Unicode と呼ばれ、21\[bit\]のコードに収まるよう定義されている。 一部の文字は複数のコードポイントからなり、結合文字という。 ``` a (U+0061) あ (U+3042) ǔ (U+01D4) = ǔ (= u + ◌̌ ) (U+0075 U+030C) 寧 (U+5BE7, CJK統合漢字) 寧 (U+F95F, CJK互換漢字) 寧 (U+F9AA, CJK互換漢字) 𠮷 (U+20BB7, CJK統合漢字拡張B) 🍣 (U+1F363, その他の記号及び絵記号) 👨‍💻 (U+1F468 U+200D U+1F4BB, その他の記号及び絵記号 一般句読点 その他の記号及び絵記号) 👨‍👩‍👧‍👧 (U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F467) ``` 文字エンコーディングである UTF-8 や UTF-16 は Unicode で定義された文字セットの実装で、それぞれの文字をより少ないビット数で表現する。 また、Unicode自体がほぼ毎年アップデートされている。2021/2/24時点で Unicode v13.0 (2020/3/10リリース) が最新。 ### 書記素クラスタ (Grapheme clusters) 人間が1文字として認識する最小単位。 a, あ, ǔ (U+01D4), ǔ (U+0075 U+030C), 🍣, 👨‍💻 はすべて1\[grapheme\]とカウントする。 1\[grapheme\] を表すのに必要なバイト長の上限がないことに注意。 Unicodeの仕様に、書記素クラスタの区切りを検出するアルゴリズムの記述がある。 https://unicode.org/reports/tr29/ ## 2. プログラミング言語と文字カウント 言語やライブラリによって文字の内部実装が異なる。 ### JavaScript JavaScriptの文字列はUTF-16で表現される。長さはそのバイト長を出力する。 ```javascript > "あ".length 1 > "🍣".length 2 > "👨‍💻".length 5 ``` ### C++ char、wchar_t、char16_t、char32_t (Microsoft Docs) https://docs.microsoft.com/ja-jp/cpp/cpp/char-wchar-t-char16-t-char32-t?view=msvc-160 strlen、wcslen、_mbslen、_mbslen_l、_mbstrlen、_mbstrlen_l (Microsoft Docs) https://docs.microsoft.com/ja-jp/cpp/c-runtime-library/reference/strlen-wcslen-mbslen-mbslen-l-mbstrlen-mbstrlen-l?view=msvc-160 C++では、文字列はマルチバイト文字列という扱い。`char` は通常1byteとして定義される。 ```cpp std::cout << sizeof(char) << std::endl; 1 ``` ```cpp= #include <iostream> #include "string.h" int main() { char text1[] = "あ"; std::cout << strlen(text1) << std::endl; char text2[] = "🍣"; std::cout << strlen(text2) << std::endl; char text3[] = "👨‍💻"; std::cout << strlen(text3) << std::endl; } ``` ```console 3 4 11 ``` C++には `wchar_t` というワイド文字列の表現がある。`w_char` の1文字のバイト数、および文字エンコーディングはコンパイラに依存する。 Visual Studio C++ Compiler では2バイト。 ```cpp std::cout << sizeof(wchar_t) << std::endl; 2 ``` ```cpp= #include <iostream> #include "string.h" int main() { wchar_t text1[] = L"あ"; std::cout << wcslen(text1) << std::endl; wchar_t text2[] = L"🍣"; std::cout << wcslen(text2) << std::endl; wchar_t text3[] = L"👨‍💻"; std::cout << wcslen(text3) << std::endl; } ``` ```console 3 4 11 ``` GCCでは4バイト。 ```cpp std::cout << sizeof(wchar_t) << std::endl; 4 ``` ```cpp= #include <iostream> #include "string.h" int main() { wchar_t text1[] = L"あ"; std::cout << wcslen(text1) << std::endl; wchar_t text2[] = L"🍣"; std::cout << wcslen(text2) << std::endl; wchar_t text3[] = L"👨‍💻"; std::cout << wcslen(text3) << std::endl; } ``` ```console 1 1 3 ``` ### Python3 UnicodeのHOWTO (Python documentation) https://docs.python.org/ja/3/howto/unicode.html Python3の文字列はUnicodeコードポイント列として表現される。 ```python >>> len("あ") 1 >>> len("🍣") 1 >>> len("👨‍💻") 3 ``` ### Java Class Character (Java SE 11 & JDK 11 documentation) https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Character.html Javaの文字列の内部表現はUTF-16である。 ```java > System.out.println("あ".length()); 1 > System.out.println("🍣".length()); 2 > System.out.println("👨‍💻".length()); 5 ``` Javaの文字列には、Unicodeコードポイントに基づいて文字列長を計算するメソッドもある。 ```java > String text = "あ"; > System.out.println(text.codePointCount(0, text.length())); 1 > String text = "🍣"; > System.out.println(text.codePointCount(0, text.length())); 1 > String text = "👨‍💻"; > System.out.println(text.codePointCount(0, text.length())); 3 ``` [java.text.BreakIterator](https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/text/BreakIterator.html) を利用して、書記素クラスタを扱うことができる。JDK11では Unicode v10.0 まで対応している。 ```java= import java.text.BreakIterator; public class Counter { public static int countGrapheme(String text) { if (text.length() == 0) { return 0; } int counts = 0; BreakIterator iter = BreakIterator.getCharacterInstance(); iter.setText(text); int current = iter.next(); while (current != BreakIterator.DONE) { counts += 1; current = iter.next(); } return counts; } } ``` ```java > System.out.println(Counter.countGrapheme("あ")); 1 > System.out.println(Counter.countGrapheme("🍣")); 1 > System.out.println(Counter.countGrapheme("👨‍💻")); 3 ``` ### MySQL 12.5 文字列関数 (MySQL 5.6 リファレンスマニュアル) https://dev.mysql.com/doc/refman/5.6/ja/string-functions.html MySQLの`LENGTH()`はバイト長を、`CHAR_LENGTH()`は文字数を数える。結果は文字コードに依存する。 ```sql show variables like '%char%'; Variable_name Value character_set_client utf8mb4 character_set_connection utf8mb4 character_set_database utf8mb4 character_set_filesystem binary character_set_results utf8mb4 character_set_server utf8mb4 character_set_system utf8 character_sets_dir /usr/share/mysql/charsets/ ``` ```sql select LENGTH('あ'), CHAR_LENGTH('あ'); 3 1 select LENGTH('🍣'), CHAR_LENGTH('🍣'); 4 1 select LENGTH('👨‍💻'), CHAR_LENGTH('👨‍💻'); 11 3 ``` ### PostgreSQL 4.4. 文字列関数と演算子 (PostgreSQL 7.2.3 ユーザガイド) https://www.postgresql.jp/document/7.2/user/functions-string.html PostgreSQLの`OCTET_LENGTH()` はバイト長を、`LENGTH()`と`CHAR_LENGTH()`は文字数を数える。結果は文字コードに依存する。 ```sql select datname, pg_encoding_to_char(encoding) from pg_database; datname pg_encoding_to_char 1 postgres UTF8 2 template1 UTF8 3 template0 UTF8 4 rextester UTF8 SHOW client_encoding; client_encoding 1 UTF8 ``` ```sql select OCTET_LENGTH('あ'), LENGTH('あ'), CHAR_LENGTH('あ'); 3 1 1 select OCTET_LENGTH('🍣'), LENGTH('🍣'), CHAR_LENGTH('🍣'); 4 1 1 select OCTET_LENGTH('👨‍💻'), LENGTH('👨‍💻'), CHAR_LENGTH('👨‍💻'); 11 3 3 ``` ## 3. 文字にまつわる色々 寿司ビール問題① 初心者→中級者へのSTEP20/25 https://qiita.com/kamohicokamo/items/3cc05f63a90148525caf MySQLで文字コードを`utf-8`に設定していると🍣=🍺になるらしい。`utf-8mb4`で回避可能。 文字数をチェックする際にイタズラを目的とした大量の結合文字を見逃さないようにする [https://qiita.com/masakielastic/items/0f2ed9db66202e15ff83](https://qiita.com/masakielastic/items/0f2ed9db66202e15ff83) 書記素クラスタで文字数チェックをするなら、他の手段との組み合わせが必須。 ## 4. 文字カウントの実装 - 言語: Python 3.8 ### UTF-16バイト長に基づく文字カウント #### 仕様 - UTF-16エンコード済み文字列について、2バイトのかたまりを1\[grapheme\]とみなす。 - あ、ǔ (U+01D4) を1文字とみなす。 - 𠮷、ǔ (U+0075 U+030C)、🍣 を2文字とみなす。 - 👨‍💻 を5文字とみなす。 - 👨‍👩‍👧‍👧 を11文字とみなす。 #### 実装 - 文字列をUTF-16で表し、そのバイト長を数え上げる。 - 文字エンコーディングは標準ライブラリに含まれているので、それを使えばよい。 ```python= def count_utf16be_block(text: str) -> int: return (len(text.encode("utf-16be")) + 1) // 2 ``` ```python >>> count_utf16be_block("12345678901234567890") 20 >>> count_utf16be_block("123456789012345678901") 21 >>> count_utf16be_block("あいうえおかきくけこさしすせそたちつてと") 20 >>> count_utf16be_block("あいうえおかきくけこさしすせそたちつてとな") 21 >>> count_utf16be_block("🍣🍺🍣🍺🍣🍺🍣🍺🍣🍺") 20 >>> count_utf16be_block("🍣🍺🍣🍺🍣🍺🍣🍺🍣🍺🍣") 22 >>> count_utf16be_block("👨‍💻🍺🍣👨‍👩‍👧‍👧") 20 >>> count_utf16be_block("👨‍👩‍👧‍👧👨‍👩‍👧‍👧") 22 ``` ### 書記素クラスタに基づく文字カウント #### 仕様 - 書記素クラスタを4コードポイントずつ分解し、かたまりを1\[grapheme\]とみなす。 - あ、𠮷、ǔ (U+01D4)、ǔ (U+0075 U+030C)、🍣、👨‍💻 を1文字とみなす。 - 👨‍👩‍👧‍👧 を2文字とみなす。 #### 実装 - 文字列を書記素クラスタに分解し、文字1つ1つについてUnicodeコードポイントを数える。 - 標準ライブラリでは書記素クラスタを扱うことができない。 - [grapheme](https://github.com/alvinlindstam/grapheme) (License: [MIT](https://github.com/alvinlindstam/grapheme/blob/master/LICENSE))を用いて実装した。 ```console $ pip install grapheme==0.6.0 ``` ```python= import grapheme def count_grapheme(text: str, max_codepoint: int = 4) -> int: if len(text) == 0: return 0 counts = 0 for n in grapheme.grapheme_lengths(text): counts += (n - 1) // max_codepoint + 1 return counts ``` ```python >>> count_grapheme("12345678901234567890") 20 >>> count_grapheme("123456789012345678901") 21 >>> count_grapheme("あいうえおかきくけこさしすせそたちつてと") 20 >>> count_grapheme("あいうえおかきくけこさしすせそたちつてとな") 21 >>> count_grapheme("🍣🍺🍣🍺🍣🍺🍣🍺🍣🍺") 10 >>> count_grapheme("🍣🍺🍣🍺🍣🍺🍣🍺🍣🍺🍣") 11 >>> count_grapheme("👨‍💻🍺🍣👨‍👩‍👧‍👧") 5 >>> count_grapheme("👨‍👩‍👧‍👧👨‍👩‍👧‍👧") 4 ``` ## 5. ICUライブラリの利用 ICU-TC Home Page http://site.icu-project.org/ International Components for Unicode https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_73/nls/rbagsicu.htm ユニコードコンソーシアムが公開しているライブラリ International Components for Unicode (ICU) を用いことで、書記素クラスタ単位での文書解析ができる。 [ダウンロードページ](http://site.icu-project.org/download) では C/C++ と Java のライブラリが公開されている。(License: [UNICODE DATA FILES AND SOFTWARE LICENSE](http://www.unicode.org/copyright.html#License)) また、多くの言語で[ラッパーライブラリ](http://site.icu-project.org/related)が提供されている。 なお、ICU4Jに含まれている [com.ibm.icu.text.BreakIterator](https://unicode-org.github.io/icu-docs/apidoc/released/icu4j/com/ibm/icu/text/BreakIterator.html) を [java.text.BreakIterator](https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/text/BreakIterator.html) の代わりに使うと最新のUnicodeに対応可能となる。 ```java= import com.ibm.icu.text.BreakIterator; public class Counter { public static int countGrapheme(String text) { if (text.length() == 0) { return 0; } int counts = 0; BreakIterator iter = BreakIterator.getCharacterInstance(); iter.setText(text); int current = iter.next(); while (current != BreakIterator.DONE) { counts += 1; current = iter.next(); } return counts; } } ``` ```java > System.out.println(Counter.countGrapheme("あ")); 1 > System.out.println(Counter.countGrapheme("🍣")); 1 > System.out.println(Counter.countGrapheme("👨‍💻")); 1 ```