MS_CharacterValidation - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki

文字のチェック方匏

抂芁

文字チェックは、文字化け等を事前に防ぐために行われる。

以䞋の文字チェック・ルヌチンが䞀般的である。

補足3 ぀の方匏の䜍眮付け: 本ペヌゞは
範囲チェック / 個別チェック / 可逆チェックの 3 ぀を扱う。
それぞれ埗意な堎面が違うので、先に敎理しおおく。

方匏 仕組み 向く堎面 匱点
範囲チェック コヌド倀の範囲で刀定 連続した領域JIS 第1第2氎準、倖字領域 飛び地には䜿えない
個別チェック 文字の䞀芧ず突き合わせ 飛び地の文字JIS2004 远加文字 䞀芧の保守が芁る
可逆チェック 埀埩倉換しお䞀臎を芋る 「この文字コヌドで送れるか」 実装䟝存。遅い

最も実務的なのは可逆チェックである。
「盞手システムに送れるか」を盎接確かめるため、
文字衚の知識が芁らない。本ペヌゞの最埌で扱われおいる。

範囲チェック

゚ンコヌド埌に、範囲チェックをする。

JIS第1第2氎準挢字チェック

仕様:

JIS 第1第2氎準挢字をチェックする

実装:

シフト JIS のコヌド範囲でチェックを行う必芁があるので、
䞀文字づ぀シフト JIS に゚ンコヌド、数倀型に倉換し範囲チェックを行う。

サンプルコヌド:

//**********************************************************************************
//* 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0xFC

1 バむト目ず 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の倖字範囲チェック

仕様

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 で远加された文字をチェックする。
  • 実装
    Unicode のみに存圚し、コヌド範囲もバラバラなので、
    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 のように倉わるため、
氏名や䜏所をそのたた NFKC するず、原文が倱われる。
怜玢甚のキヌを別に持぀のが正しい蚭蚈である。

【macOS 由来のファむル名が「濁点が分かれおいる」問題】
   macOSHFS+は【NFD 盞圓】でファむル名を保存する
     → 「ガ」が「カ + ゙」の 2 文字ずしお届く
     → Windows 偎で怜玢が䞀臎しない
   察策 受信時に NFC 正芏化する

Lengthチェック

サロゲヌト ペア文字・結合文字は、

  • 通垞の 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);
  // 長さを衚瀺
  MessageBox.Show("長さ文字列長" + strSurrogatesPair.Length);
  // 長さを衚瀺
  MessageBox.Show("長さ文字列長" + si.LengthInTextElements);
  // 長さを衚瀺
  MessageBox.Show("長さプログラム䞭UTF-16でのバむト長" +
  Encoding.Unicode.GetBytes(strSurrogatesPair).Length);
}

補足「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 からの双方向の゚ンコヌディングか可胜か

をチェックできる。

䟋MS932の範囲内であるかのチェック

  • 以䞋のように゚ンコヌディングを行う。

    1. ①Unicode 文字列
    2. → MS932 ゚ンコヌディング
    3. → ②MS932 文字列バむト配列
    4. → MS932 デコヌディング
    5. → ③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】になる
   → しかし、芋た目は同じで、実務䞊は「送れおいる」

どちらを正ずするかは業務刀断である
゚ンコヌディング の事䟋。
厳密に刀定したいなら、事前に正芏化しおから埀埩させる。

萜ずし穎 ③重耇笊号化された文字

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。

参考

Microsoft Learn


Tags: 移行, .NET開発, 囜際化察応, 文字コヌド

⚠ **GitHub.com Fallback** ⚠