-
Notifications
You must be signed in to change notification settings - Fork 0
MS_CharacterValidation
- 戻る(文字コード)
文字チェックは、文字化け等を事前に防ぐために行われる。
以下の文字チェック・ルーチンが一般的である。
補足(3 つの方式の位置付け): 本ページは
範囲チェック / 個別チェック / 可逆チェックの 3 つを扱う。
それぞれ得意な場面が違うので、先に整理しておく。
方式 仕組み 向く場面 弱点 範囲チェック コード値の範囲で判定 連続した領域(JIS 第1第2水準、外字領域) 飛び地には使えない 個別チェック 文字の一覧と突き合わせ 飛び地の文字(JIS2004 追加文字) 一覧の保守が要る 可逆チェック 往復変換して一致を見る 「この文字コードで送れるか」 実装依存。遅い 最も実務的なのは可逆チェックである。
「相手システムに送れるか」を直接確かめるため、
文字表の知識が要らない。本ページの最後で扱われている。
エンコード後に、範囲チェックをする。
JIS 第1第2水準漢字をチェックする
シフト JIS のコード範囲でチェックを行う必要があるので、
一文字づつシフト JIS にエンコード、数値型に変換し範囲チェックを行う。
- JISX0208_1983Checker
https://github.com/OpenTouryoProject/OpenTouryo/blob/develop/root/programs/CS/Frameworks/Infrastructure/Business/Str/JISX0208_1983Checker.cs
//**********************************************************************************
//* All Rights Reserved, Copyright (C) 2007,2012 Hitachi Solutions,Ltd.
//**********************************************************************************
//**********************************************************************************
//* クラス名 :JISX0208_1983Checker
//* クラス日本語名 :JIS X 0208-1983文字コード範囲チェック・クラス
//* ・01~08区:記号、英数字、かな
//* ・16~47区:JIS第1水準漢字
//* ・48~84区:JIS第2水準漢字
//* ※JIS X 0208-1990で追加された「凜[7425]」「熙[7426]」は含まれない
//* ※NEC機種依存文字、NECのIBM拡張文字、IBM拡張文字は含まれない補足(区点と Shift_JIS の対応): コメントの「区」は
区点コードであり、そのままでは Shift_JIS のバイト値ではない。
原文が「シフト JIS にエンコード、数値型に変換し」と書いているのは、
区点ではなく Shift_JIS の 2 バイト値で判定するという意味である。【Shift_JIS の 2 バイト文字の構造】 1 バイト目: 0x81~0x9F、0xE0~0xEF(JIS X 0208 の範囲) 0xF0~0xFC(外字領域) 0xFA~0xFC(IBM 拡張文字) 2 バイト目: 0x40~0x7E、0x80~0xFC1 バイト目と 2 バイト目の範囲が重なっているため、
バイト単位で走査すると誤判定する(いわゆる「5C 問題」)。「表」= 0x95 0x5C ↑ 2 バイト目が 0x5C(= ASCII の "\") → バイト列を素朴に扱うと、パス区切りやエスケープと誤認される → 「ソ」(0x83 0x5C)、「十」(0x8F 0x5C) 等も同様原文が「一文字づつ」と強調しているのは、この問題を避けるためであり、
重要な指摘である。
現在の .NET ではstringが UTF-16 なので、
foreach (char c in s)で自然に文字単位になるが、
byte[]に落としてから走査すると同じ罠にはまる。なお、この判定は現在では「文字集合の絞り込み」として使われる。
目的は文字化け防止ではなく、・レガシー システム(CP932 前提)へ連携する際の入力制限 ・帳票の外字フォントに存在する文字だけに限定する ・氏名欄で「登録可能な文字」を業務ルールとして定めるといった業務要件の実装である。
純粋な文字化け対策としては、UTF-8 で通す方が根本的である
(アプリケーションのUnicode化)。
Unicode の外字範囲をチェックする。
- BMP 領域 U+E000‐U+F8FF (6,400字)
- 15面 U+F0000‐U+FFFFD (65,534字)
- 16面 U+100000‐U+10FFFD (65,534字)
- 余談:シフト JIS の外字範囲:U+E000~U+E757(0xF040~0xF9FC)。
- 一文字づつ Unicode 数値に変換し範囲チェックを行う。
- Java、.NET の文字列は Unicode のため Unicode の範囲チェックは、
以下の様なコードで簡単にチェックできる。
(Unicode? JIS X 0221? 各国の工業規格で使用されるか?)
string from = "文字列を初期化する。"
StringBuilder to = new StringBuilder();
foreach (char c in from)
{
int charCode = (int)c;
if (57344 <= charCode && charCode <= 63743)
{
// Unicodeの外字範囲
// BMP領域 U+E000‐U+F8FF (6,400字)
}
else if (983040 <= charCode && charCode <= 1048573)
{
// Unicodeの外字範囲
// 15面 U+F0000‐U+FFFFD (65,534字)
}
else if (1048576 <= charCode && charCode <= 1114109)
{
// Unicodeの外字範囲
// 16面 U+100000‐U+10FFFD (65,534字)
}
else
{
// Unicodeの外字でない。
}
}移行メモ(このサンプルは 15 面・16 面を検出できない): 仕様は
正しいが、実装がその仕様を満たしていない。【問題】 C# の char は【16 ビット(UTF-16 の 1 単位)】である → (int)c の最大値は 65535 → 983040(U+F0000)以上の条件は【絶対に成立しない】 15 面・16 面の文字は【サロゲート ペア】で表現されるため、 char 1 つずつ見ていては検出できない → U+F0000 は char 2 つ(0xDB80, 0xDC00)に分かれている本ページの後半でサロゲート ペアの話が別項目として出てくるが、
この 2 つは同じ問題であり、
サロゲート ペアを考慮した走査が必要である。修正版(
charではなくコードポイントで走査する):// .NET Core 3.0 以降:Rune がコードポイントを表す foreach (Rune r in from.EnumerateRunes()) { int cp = r.Value; // ← 0x10FFFF まで正しく入る if (0xE000 <= cp && cp <= 0xF8FF) // BMP の私用領域 { /* 外字 */ } else if (0xF0000 <= cp && cp <= 0xFFFFD) // 15 面(補助私用領域 A) { /* 外字 */ } else if (0x100000 <= cp && cp <= 0x10FFFD) // 16 面(補助私用領域 B) { /* 外字 */ } else { /* 外字でない */ } }
Runeが使えない環境(.NET Framework)では、
char.ConvertToUtf32を使う。for (int i = 0; i < from.Length; i++) { int cp; if (char.IsHighSurrogate(from[i]) && i + 1 < from.Length && char.IsLowSurrogate(from[i + 1])) { cp = char.ConvertToUtf32(from[i], from[i + 1]); i++; // ← ペアなので 2 つ進める } else { cp = from[i]; } // cp で判定する }
Runeを使えば、この定型処理を書かずに済む。
.NET Core 3.0 以降ならRuneが第一選択である。
補足(私用領域の性質): 仕様に挙げられた 3 つの領域は、
Unicode では **PUA(Private Use Area、私用領域)**と呼ばれる。
領域 名称 字数 U+E000~U+F8FF 私用領域(BMP 内) 6,400 U+F0000~U+FFFFD 補助私用領域 A(15 面) 65,534 U+100000~U+10FFFD 補助私用領域 B(16 面) 65,534 **「私用」の意味は「Unicode は何も定義しない」**である。
・どの文字を割り当てるかは【使う側の勝手】 ・したがって【組織をまたぐと意味が変わる】 → 外字が化ける根本原因([Windowsの外字] 参照) ・フォントがなければ □ や豆腐で表示される原文が「シフト JIS の外字範囲:U+E000~U+E757」と書いているのは、
CP932 の外字領域(0xF040~0xF9FC、1,880 字)が
Unicode の私用領域の先頭に順に対応付けられているためである。
これは Microsoft がそう決めただけの対応であり、
他社の変換表では別の値になり得る。現在の指針:
・新規システムで【私用領域を使わない】 ・既存の外字は【IVS(異体字セレクタ)や Unicode の正規の文字へ移行】 → [Windowsの外字] を参照 ・どうしても残すなら、【変換表を明示的に管理する】
コード表で連続しない文字をチェックする。
JIS2004チェック
-
仕様:
JIS2004 で追加されたサロゲートペア文字をチェックする。 -
実装:
-
Regex.IsMatch() メソッド、char.IsSurrogate() メソッドを使用する。
-
Regex.IsMatch() メソッド(正規表現)を使用する。
-
以下サンプル・コード。
//ここに判定する文字列を入れる。 string strSurrogatesPair = textBox1.Text; Regex rg = new Regex("^[^\uD800-\uDBFF\uDC00-\uDFFF]+$"); //サロゲート ペア文字が文字列中に含まれているか //Regex.IsMatch() メソッド判定 if ( rg.IsMatch( strSurrogatesPair ) ) { // サロゲートペア文字が含まれていない。 } else { // サロゲートペア文字が含まれている。 } // 結合文字はチェックできない。
-
-
char.IsSurrogate() メソッドを使用する。
-
以下サンプル・コード。
//ここに判定する文字列を入れる。 string strSurrogatesPair = textBox1.Text; //サロゲート ペア文字が文字列中に含まれているか //char.IsSurrogate()メソッドで判定 int i = 1; foreach (char ch in strSurrogatesPair.ToCharArray()) { if(char.IsSurrogate(ch)) { MessageBox.Show(i.ToString() + "文字目にサロゲート ペア文字が含まれています"); return; } else { i++; } } MessageBox.Show("サロゲート ペア文字が含まれていません");
-
サロゲート ペア文字を削除するサンプル コード
//ここに判定する文字列を入れる。 string strSurrogatesPair = textBox1.Text; StringBuilder sb = new StringBuilder(); //サロゲート ペアが文字列中に含まれているか //char.IsSurrogate()メソッドで判定 foreach (char ch in strSurrogatesPair.ToCharArray()) { if (char.IsSurrogate(ch)) { // 破棄 } else { sb.Append(ch); } } textBox1.Text = sb.ToString();
-
-
※ 前者は、存在チェックのみ、
後者は、文字の位置まで特定可能(∴削除も可能。)。
※ また、上記の方法では、結合文字はチェックできない。
補足(サンプルの注意点): どちらも動くが、
細部に注意すべき点がある。① 正規表現版は「空文字列」で結果が反転する
// "^[^...]+$" は 1 文字以上を要求する rg.IsMatch(""); // false → 「含まれている」と誤判定される
+を*にするか、空文字列を先に弾く。②
char.IsSurrogate版は「文字目」の数え方が実際とずれる"あ𠮷い" を走査すると index 0: 'あ' → i = 2 index 1: 0xD842(上位)→ ここで「2 文字目」と報告 実際の見た目は【2 文字目】で合っているが、 サロゲートより前にサロゲートがあると、以降が 2 ずつずれる見た目の位置を出したいなら
StringInfoを使う(後述)。③ 削除サンプルは「壊れた文字列」を作り得る
char.IsSurrogateは上位・下位の両方に true を返すため、
ペアの両方が消える。この点は正しい。
ただし孤立サロゲート(ペアの片割れだけ)が入力にあった場合も
黙って消えるため、不正な入力を検出できない。現在の書き方:
// サロゲート ペア(BMP 外の文字)を含むか bool hasNonBmp = str.EnumerateRunes().Any(r => !r.IsBmp); // BMP 外の文字を除去する string removed = string.Concat( str.EnumerateRunes().Where(r => r.IsBmp).Select(r => r.ToString()));
Runeは不正なサロゲートをU+FFFD(置換文字)として返すため、
壊れた入力が検出できる利点がある。
- 現状、ハッキリした方法が無い。
-
サロゲートペアや結合文字が含まれているか調べる .NET Tips C#, VB.NET
https://dobon.net/vb/dotnet/string/issurrogatepair.html#section4\ 結合文字が含まれているか調べるただし、Marks カテゴリにすべての結合文字が含まれているか、
そして、結合文字以外の文字が一切含まれていないかについては、はっきりしていません。
-
補足(現在は
StringInfo/TextElementEnumeratorで扱える): 原文の
「ハッキリした方法が無い」という状況は、現在は改善している。.NET 5 以降、
StringInfoは Unicode の
書記素クラスタ(Grapheme Cluster)の規則(UAX #29)に準拠した。
つまり、「人間が 1 文字と見なす単位」を正しく数えられる。// 「見た目の 1 文字」で列挙する var e = StringInfo.GetTextElementEnumerator("が𠮷👨👩👧"); while (e.MoveNext()) Console.WriteLine(e.GetTextElement()); // が ← 結合文字(か + 濁点)が 1 つにまとまる // 𠮷 ← サロゲート ペアが 1 つ // 👨👩👧 ← ZWJ で繋がった絵文字も 1 つ// 結合文字(Mark カテゴリ)を含むか bool hasCombining = s.Any(c => { var cat = CharUnicodeInfo.GetUnicodeCategory(c); return cat == UnicodeCategory.NonSpacingMark || cat == UnicodeCategory.SpacingCombiningMark || cat == UnicodeCategory.EnclosingMark; });**より実務的なのは「正規化してから比較する」**方法である。
// 合成済み文字に寄せる(NFC) string nfc = s.Normalize(NormalizationForm.FormC); // 「が」は 2 通りの表現がある "が" (U+304C) ← 合成済み(NFC) "か" + "゙" (U+304B U+3099) ← 分解(NFD) // 正規化すれば、どちらも同じになる Assert.Equal("が", "が".Normalize(NormalizationForm.FormC));
形式 内容 用途 NFC 合成済みに寄せる DB 保存・比較の既定。Windows の慣習 NFD 分解する macOS のファイル名がこれ NFKC 合成+互換分解(全角→半角、① → 1) 検索キーの正規化 NFKD 分解+互換分解 同上 NFKC は影響が大きい点に注意する。
㈱→(株)、①→1、A→Aのように変わるため、
氏名や住所をそのまま NFKC すると、原文が失われる。
検索用のキーを別に持つのが正しい設計である。【macOS 由来のファイル名が「濁点が分かれている」問題】 macOS(HFS+)は【NFD 相当】でファイル名を保存する → 「ガ」が「カ + ゙」の 2 文字として届く → Windows 側で検索が一致しない 対策: 受信時に NFC 正規化する
サロゲート ペア文字・結合文字は、
- 通常の Length チェックでは、2 文字分として表示される。ただし、見た目の文字は 1 文字。
- また、プログラム中でのバイト表現(UTF-16)でのバイト長は、通常の文字が 2 バイトであるのに対し、4 バイト。
サロゲート ペア文字・結合文字を含む文字列の見た目の文字列長を調べる場合は、
- System.Globalization 名前空間の StringInfo クラスを使用する。
- string.Length、string.Substring などは原則禁止
(string.Length は、文字列のバイト長を調査する場合などは使用可能) - ただし、.NET Framework 1.1 以前のランタイムでは、
このクラスの仕様が異なり下記の様に利用できない。
/// <summary>文字列情報の表示</summary>
/// <param name="strSurrogatesPair">文字列</param>
private void GetStringInfo(string strSurrogatesPair)
{
// System.Globalization.StringInfoを使用する。
StringInfo si = new StringInfo(strSurrogatesPair);
// 文字列を表示
MessageBox.Show(strSurrogatesPair);
// 長さを表示1
MessageBox.Show("長さ(文字列長1):" + strSurrogatesPair.Length);
// 長さを表示2
MessageBox.Show("長さ(文字列長2):" + si.LengthInTextElements);
// 長さを表示3
MessageBox.Show("長さ(プログラム中(UTF-16)でのバイト長):" +
Encoding.Unicode.GetBytes(strSurrogatesPair).Length);
}- [参考]:Microsoft Learn > .NET Framework クラス ライブラリ > StringInfo メンバ
https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.stringinfo
補足(「Length は原則禁止」の意味): 原文のこの指針は
強い表現だが、方向は正しい。ただし
「何を数えたいか」で答えが変わるため、整理しておく。string s = "あ𠮷が"; // あ + 𠮷(サロゲート)+ か + 濁点 s.Length // 5(UTF-16 の単位数) s.EnumerateRunes().Count() // 4(コードポイント数) new StringInfo(s).LengthInTextElements // 3(見た目の文字数)★ Encoding.UTF8.GetByteCount(s) // 13(UTF-8 のバイト数) Encoding.Unicode.GetByteCount(s) // 10(UTF-16 のバイト数)
目的 使うもの 入力欄の「○文字まで」の判定 LengthInTextElements(利用者の感覚に一致)DB の nvarchar(n)に収まるかs.Length(SQL Server の n は UTF-16 単位)ファイル・通信のサイズ見積り Encoding.UTF8.GetByteCount固定長ファイル(CP932)の桁数 Encoding.GetEncoding(932).GetByteCount単に空かどうか string.IsNullOrEmpty(Length == 0より明確)
Substringが危険な理由も同じである。// ✗ サロゲート ペアの途中で切ると、壊れた文字ができる "𠮷野家".Substring(0, 1); // 孤立サロゲート(表示は "?") // ○ 見た目の文字で切る var si = new StringInfo("𠮷野家"); si.SubstringByTextElements(0, 1); // "𠮷"「…」で省略表示する処理は、この罠の典型である。
絵文字や結合文字を含む入力(現在はごく普通に来る)で、
文字が壊れる/豆腐になるという不具合になる。移行メモ: 原文の「.NET Framework 1.1 以前のランタイムでは
このクラスの仕様が異なり」という注記は、
現在は考慮不要である(.NET Framework 1.1 は 2013 年にサポート終了)。
むしろ、.NET 5 でStringInfoが UAX #29 準拠に修正されたことの方が
現在の関心事である(それ以前は絵文字の扱いが不正確だった)。
エンコーディングを使用して可逆チェックをする。
特定のエンコーディングの
- コードページ範囲内の文字であるか?
- Unicode からの双方向のエンコーディングか可能か?
をチェックできる。
-
以下のようにエンコーディングを行う。
- ①Unicode 文字列
- → MS932 エンコーディング
- → ②MS932 文字列(バイト配列)
- → MS932 デコーディング
- → ③Unicode 文字列
-
上記の結果から、
①Unicode 文字列と、③Unicode 文字列を比較し=か?をチェックする。
補足(実装と落とし穴): この方式が最も実務的である。
文字表を持たずに「相手に送れるか」を直接確かめられる。// .NET Core 以降は最初に 1 度だけ登録が必要 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); static bool IsRoundTrippable(string s, int codePage) { var enc = Encoding.GetEncoding(codePage); return enc.GetString(enc.GetBytes(s)) == s; } IsRoundTrippable("髙島屋", 932); // true (CP932 にある) IsRoundTrippable("𠮷野家", 932); // false(JIS2004 の文字) IsRoundTrippable("café", 932); // false(é が CP932 にない)落とし穴 ①:既定では失敗が「?」に化けて分からない
上の実装で false は返るが、
「どの文字が駄目だったか」は分からない。
例外を出す設定にすると、位置が特定できる。var enc = Encoding.GetEncoding(932, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback); try { enc.GetBytes(s); } catch (EncoderFallbackException ex) { // ex.Index … 何文字目か // ex.CharUnknown … 変換できなかった文字 }落とし穴 ②:波ダッシュ問題で「往復するのに値が変わる」
"〜"(U+301C) → CP932 → 0x8160 → Unicode → "~"(U+FF5E) → 往復チェックは【false】になる → しかし、見た目は同じで、実務上は「送れている」どちらを正とするかは業務判断である
(エンコーディング の事例4)。
厳密に判定したいなら、事前に正規化してから往復させる。落とし穴 ③:重複符号化された文字
NEC 選定 IBM 拡張文字は、往復で別のバイト値になるが、
Unicode に戻せば同じ文字なので、
この方式では検出されない(=送れると判定される)。
バイト値の一致まで求めるなら、バイト列同士を比較する。落とし穴 ④:性能
1 文字ごとに
GetEncodingを呼ぶ実装は遅い。
Encodingインスタンスは使い回す(スレッド セーフ)。// ✗ ループの中で毎回取得 foreach (var s in list) { Encoding.GetEncoding(932)... } // ○ 1 度だけ取得して使い回す static readonly Encoding Cp932 = Encoding.GetEncoding(932);
補足(入力チェックの現在の設計): 3 方式を踏まえた上での、
現在の実務的な指針をまとめる。① まず「なぜ制限するのか」を決める
理由 適切な手段 連携先が CP932 しか受けられない 可逆チェック(相手の文字コードで) 帳票のフォントに字がない フォント側の対応文字で個別チェック 業務ルール(氏名は漢字・かなのみ等) 正規表現による文字種チェック 文字化けが怖い(漠然と) 制限しない。UTF-8 で通す方が根本的 最後の行が重要で、
「念のため」で文字を制限すると、利用者の氏名が入力できない
という問題を生む。
制限には必ず理由が要る。② 制限するなら、入口で・明確なメッセージで
✗ 「入力に誤りがあります」 ○ 「3 文字目の『𠮷』は登録できません。『吉』でご入力ください」どの文字が駄目かを返せる実装にしておく
(前述のEncoderFallbackExceptionのIndex/CharUnknown)。③ 正規化のタイミングを決める
【入力時】 NFC 正規化 → 保存 (macOS 由来の NFD を吸収する) 【検索時】 NFKC 正規化した別カラムで突き合わせる (全角・半角、丸数字の揺れを吸収する)④ DB 側も合わせる
推奨 列の型 nvarchar(varcharはコードページ依存)SQL Server 2019 以降 UTF-8 照合順序の varcharも選べる照合順序 _SC付き(Supplementary Characters。サロゲート ペア対応)
_SCが付いていない照合順序では、
LEN()やSUBSTRING()がサロゲート ペアを正しく扱えない。
.NET 側のStringInfoと同じ問題が DB 側にもある
(SQL Server)。
- .NET での文字エンコード
https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/character-encoding-introduction - System.Text.Rune 構造体
https://learn.microsoft.com/ja-jp/dotnet/api/system.text.rune - System.Globalization.StringInfo クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.stringinfo - 文字列での文字の操作(書記素クラスタ)
https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/character-encoding-introduction#grapheme-clusters - String.Normalize メソッド
https://learn.microsoft.com/ja-jp/dotnet/api/system.string.normalize
Tags: 移行, .NET開発, 国際化対応, 文字コード
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。