ソフト関連Trial−No.104
        URLエンコード方式の自動判別方法紹介(C#)

トップページへ

 はじめに

アクセスログ集計・解析ソフト「CKAccessLog」 の制作において、アクセスログ中に記載されている検索語句をURLデコード(以下、単にデコードと記載)する という課題に出会いました。
アクセスログ中のほとんどの検索語句はutf−8の方式でURLエンコード(以下、単にエンコードと記載)されていますが、 一部には、非常に僅かですが、shift−jis、euc−jpでエンコードされているログデータもありました。
従って、エンコード方式をutf−8としてデコードを行うと、一部のログデータの解析では文字化けが発生します。
そこで、「CKAccessLog」では、エンコードされている検索語句の文字の並びからエンコード方式を判別して デコードをすることにしました。
「CKAccessLog」での今までの実績では、この自動判別方法は有効に機能していますので、 今回このページで内容を紹介することにしました。

 自動判別の基本的な進め方

今回紹介する判別方法は、アクセスログ中に記載されている検索語句を対象にするものです。
「CKAccessLog」の制作過程で856件の検索語句を検討したところ、そのエンコード方式の分布は次のようになりました。

utf−8 842件(98.3%)
shift−jis 9件( 1.1%)
euc−jp 5件( 0.6%)

そこで、以下のような考え方で判別を行うことにしました。
1) 検索語句の並びにutf−8の方式と矛盾する点がなければ、utf−8と判定する。
2) 1)でutf−8と判定できない場合には、検索語句の並びにeuc−jpの方式と矛盾する点がないかを調べて、 矛盾点がなければ、euc−jpと判定する。
3) 1)、2)でそれぞれの方式と判定できない場合には、検索語句の並びにshift−jisの方式と矛盾する点がないかを調べて、 矛盾点がなければ、shift−jisと判定する。
4) 1)〜3)でいずれの方式とも判定できない場合には、utf−8と判定する。
ここで、4)の判定は明らかにおかしいものですが、なんらかの結果を返さないと、 パソコンのプログラムを組むことができないので、このようにしました。
4)の事態が発生した場合には、文字化けした結果となるはずです。
そして、この事態が発生するのは、
 @上記三方式以外のエンコード方式が使われている、
 A対象となる検索語句自体が間違っている、 
 Bプログラムのどこかに瑕疵がある、
のいずれかですので、内容を調査して、
 @であればそのエンコード方式用の判別プログラムを追加する、
 Aであれば検索語句自体を修正する、
 Bであればバグを修正する、
ということになります。

 各方式のエンコード方法

utf−8、shift−jis、euc−jpに共通したエンコード方法
半角英数字、および半角の「*」、「-」、「.」、「_」(以下、半角英数字等)はエンコードされずに、そのままの形で表記されます。
また、16進数アスキーコードが0x20〜0x7Eの範囲にあって上記半角英数字等に該当しない文字は、 その16進数バイト値の前に「%」を付けた形で1バイト文字として表記されます。
 例:半角の「@」⇒「%40」に変換
   半角の「+」⇒「%2B」に変換
半角のスペースは半角の「+」に変換されます。

utf−8方式
utf−8では、前記半角英数字等あるいは1バイト文字に変換されるものを除いて、全て2バイト文字または3バイト文字に変換されます。
2バイト文字となる場合の変換後の文字は、以下の範囲となります。
 1バイト目:「%C2」〜「%DF」
 2バイト目:「%80」〜「%BF」
なお、2バイト文字に変換される変換前の文字は、A〜Z、a〜z以外の特殊文字等(ドイツ語のウムラウト、ギリシャ文字、 ロシア文字等)の文字であって、通常の日本語の文字で2バイト文字に変換されるものはありません。
3バイト文字となる場合の変換後の文字は、以下の範囲となります。
 1バイト目:「%E0」〜「%EF」
 2バイト目、3バイト目:「%80」〜「%BF」
具体例として、『@IT3+座標コード』をutf−8方式でエンコードして、それを文字別に対比すると、以下のようになります。
  @(%40)IT3(IT3←無変換)+(%2B)座(%E5%BA%A7)標(%E6%A8%99)コ(%EF%BD%BA)ー(%EF%BD%B0)ト(%EF%BE%84)゙(%EF%BE%9E)

shift−jis方式
shift−jisでは、前記半角英数字等あるいは1バイト文字に変換されるもの以外で、半角文字(半角のカナ文字、句読点、 「」記号)は1バイト文字へ、全角文字は全て2バイト文字に変換されます。
1バイト文字となる場合の変換後の文字は、以下の範囲となります。
 「%20」〜「%7E」(注:既出)、または「%A1」〜「%DF」
2バイト文字となる場合の変換後の文字は、以下の範囲となります。
  1バイト目:「%81」〜「%9F」、または「%E0」〜「%FB」
  2バイト目:「%40」〜「%7E」、または「%80」〜「%FC」
 または、
  1バイト目:「%FC」
  2バイト目:「%40」〜「%4B」
さらにshift−jisでは、2バイト目が「%41」〜「%5A」または「%61」〜「%7A」といった半角英文字に該当する場合には、 バイト表記ではなく、該当する英文字に変換されます。
例えば、全角の「エ」は、「%83%47」ではなく、「%83G」へ、
     全角の「フ」は、「%83%74」ではなく、「%83t」へ変換されます。
具体例として、『@IT3+座標コード』をshift−jis方式でエンコードして、それを文字別に対比すると、以下のようになります。
  @(%40)IT3(IT3←無変換)+(%2B)座(%8D%C0)標(%95W)コ(%BA)ー(%B0)ト(%C4)゙(%DE)

euc−jp方式
euc−jpでは、前記半角英数字等あるいは1バイト文字に変換されるものを除いて、全て2バイト文字に変換されます。
2バイト文字となる場合の変換後の文字は、以下の範囲となります。
  1バイト目:「%8E」で、
  2バイト目:「%A1」〜「%DF」
  これは、前記半角英数字等あるいは1バイト文字に変換される文字以外の半角文字に対応します。
 または、
  1バイト目:「%A1」〜「%FC」で、
  2バイト目:「%A1」〜「%FE」
具体例として、『@IT3+座標コード』をeuc−jp方式でエンコードして、それを文字別に対比すると、以下のようになります。
  @(%40)IT3(IT3←無変換)+(%2B)座(%BA%C2)標(%C9%B8)コ(%8E%BA)ー(%8E%B0)ト(%8E%C4)゙(%8E%DE)

 判別プログラム

上記のように、各エンコード方式の変換結果には特徴的なスタイルがあります。
そこで、アクセスログ中の検索語句を「%」を区切り文字としてSplitメソッドによって分割し、分割された文字の並びが各エンコード方式に特徴的な変換結果のスタイルに合致するかを見るような 判別プログラムを作成しました。
使用したプログラム言語はC#2008(.NET Framework 2.0)です。

判別プログラムは、メインとなる[GetEnc]メソッドと、そこから呼び出されて実際の判別を行う[IsUTF8]メソッド、 [IsEUC]メソッド、[IsShiftJis]メソッドから構成されます。
また、使用するバイト数値、区切り文字の「%」を、予めクラスレベルの定数として定義しておきます。


    const Byte B20 = (Byte)0x20;
    const Byte B40 = (Byte)0x40;
    const Byte B4B = (Byte)0x4B;
    const Byte B7E = (Byte)0x7E;
    const Byte B80 = (Byte)0x80;
    const Byte B8E = (Byte)0x8E;
    const Byte B9F = (Byte)0x9F;
    const Byte BA1 = (Byte)0xA1;
    const Byte BBF = (Byte)0xBF;
    const Byte BC2 = (Byte)0xC2;
    const Byte BDF = (Byte)0xDF;
    const Byte BE0 = (Byte)0xE0;
    const Byte BEF = (Byte)0xEF;
    const Byte BFC = (Byte)0xFC;
    const Byte BFE = (Byte)0xFE;
    const char DLMTR = '%';


続いて、[GetEnc]メソッドを、次のように構成します。

    // [GetEnc]メソッド
    public static Encoding GetEnc(string vwords)
    {
      string[] aword = vwords.Split(DLMTR);

      Encoding defEnc = Encoding.GetEncoding("UTF-8");

      if (aword.Length == 1) return defEnc; // delimiterがない場合

      if (IsUTF8(aword))
      {
        return defEnc;
      }
      else if (IsEUC(aword))
      {
        return Encoding.GetEncoding("euc-jp");
      }
      else if (IsShiftJis(aword))
      {
        return Encoding.GetEncoding("Shift_Jis");
      }
      else
      {
        return defEnc;
      }
    }

[GetEnc]メソッドの引数 vwords は、アクセスログ中の検索語句そのものです。
vwords を区切り文字「%」で分割して、文字列配列 aword[ ] とし、あとは実際の判別を行うメソッドにこの配列を引数として渡します。

以下、判別用のメソッド[IsEUC]、[IsShiftJis]、[IsUTF8]のコードを紹介します。
各メソッドに渡される配列引数のスタイルが「各方式のエンコード方法」の項に記載した各方式の エンコードスタイルと合致するかを判定します。

    // [IsEUC]メソッド
    private static bool IsEUC(string[] vword)
    {
      int i = 1;
      Byte b1;
      string s1 = String.Empty;
      
      while (i < vword.Length)
      {
        s1 = vword[i];
        if (s1.Length == 2)
        {
          b1 = (Byte)(Convert.ToInt32("0x" + s1, 16));
          if (b1 >= B20 && b1 <= B7E)  // 1バイト文字の場合
          {
            i += 1;
          }
          elseif (b1 == B8E) // 半角カナ文字の場合
          {
            i += 1;
            if (i >= vword.Length) return false;
            s1 = vword[i];
            if (s1.Length >= 2)
            {
              b1 = (Byte)(Convert .ToInt32("0x" + s1.Substring(0, 2), 16));
              if (b1 >= BA1 && b1 <= BDF)    // 2バイト文字目
                i += 1;
              else
                return false;
            }
            else
            {
              return false;
            }
          }
          else if (b1 >= BA1 && b1 <= BFC)   // 2バイト文字の場合
          {
            i += 1;
            if (i >= vword.Length) return false;
            s1 = vword[i];
            if (s1.Length >= 2)
            {
              b1 = (Byte)(Convert.ToInt32( "0x" + s1.Substring(0, 2), 16));
              if (b1 >= BA1 && b1 <= BFE)    // 2バイト文字目
                i += 1;
              else
                return false;
            }
            else
            {
              return false;
            }
          }
          else
          {
            return false;
          }
        }
        else if (s1.Length >= 3)
        {
          b1 = (Byte)(Convert.ToInt32("0x" + s1.Substring(0, 2), 16));
          if (b1 >= B20 && b1 <= B7E)    // 1バイト文字の場合
          {
            i += 1;
          }
          else
          {
            return false;
          }
        }
        else
        {
          return false;
        }
      }
      return true;
    }
    
    
    // [IsShiftJis]メソッド
    private static bool IsShiftJis(string[] vword)
    {
      int i = 1;
      Byte b1;
      string s1 = String.Empty;
      string s2 = String.Empty;
      
      while (i < vword.Length)
      {
        s1 = vword[i];
        if (s1.Length == 2)
        {
          b1 = (Byte)(Convert.ToInt32("0x" + s1, 16));
          if ((b1 >= B20 && b1 <= B7E) || (b1 >= BA1 && b1 <= BDF)) // 1バイト文字
          {
            i += 1;
          }
          else if ((b1 > B80 && b1 <= B9F) || (b1 >= BE0 && b1 < BFC))
          {
            i += 1;
            if (i >= vword.Length) return false;
            s1 = vword[i];
            if (s1.Length >= 2)
            {
              b1 = (Byte)(Convert.ToInt32("0x" + s1.Substring(0, 2), 16));
              if ((b1 >= B40 && b1 <= B7E) || (b1 >= B80 && b1 <= BFC))
                i += 1;
              else
                return false;
            }
            else
            {
              return false;
            }
          }
          else if (b1 == BFC)
          {
            i += 1;
            if (i >= vword.Length) return false;
            s1 = vword[i];
            if (s1.Length >= 2)
            {
              b1 = (Byte)(Convert.ToInt32("0x" + s1.Substring(0, 2), 16));
              if (b1 >= B40 && b1 <= B4B)
                i += 1;
              else
                return false;
            }
          }
          else
          {
            return false;
          }
        }
        else if (s1.Length >= 3)
        {
          s2 = s1.Substring(2, 1);
          s1 = s1.Substring(0, 2);
          b1 = (Byte)(Convert.ToInt32("0x" + s1, 16));
          if ((b1 >= B20 && b1 <= B7E) || (b1 >= BA1 && b1 <= BDF)) // 1バイト文字
          {
            i += 1;
          }
          else if ((b1 > B80 && b1 <= B9F) || (b1 >= BE0 && b1 < BFC))
          {
            if (System.Text.RegularExpressions.Regex.IsMatch(s2, "^[A-Za-z@]$"))
              i += 1;
            else
              return false;
          }
          else if (b1 == BFC)
          {
            if (System.Text.RegularExpressions.Regex.IsMatch(s2, "^[A-K@]$"))
              i += 1;
            else
              return false;
          }
          else
          {
            return false;
          }
        }
        else
        {
          return false;
        }
      }
      return true;
    }
    
    // [IsUTF8]メソッド
    private static bool IsUTF8(string[] vword)
    {
      int i = 1;
      Byte b1;
      string s1 = String.Empty;
      
      while (i < vword.Length)
      {
        s1 = vword[i];
        if (s1.Length == 2)
        {
          b1 = (Byte)(Convert.ToInt32("0x" + s1, 16));
          if (b1 >= B20 && b1 <= B7E)    // 1バイト文字の場合
          {
            i += 1;
          }
          else if (b1 >= BC2 && b1 <= BDF)   // 2バイト文字の場合
          {
            i += 1;
            if (i >= vword.Length) return false;
            s1 = vword[i];
            if (s1.Length >= 2)
            {
              b1 = (Byte)(Convert.ToInt32("0x" + s1.Substring(0, 2), 16));
              if (b1 >= B80 && b1 <= BBF) // 2バイト文字目
                i += 1;
              else
                return false;
            }
            else
            {
              return false;
            }
          }
          else if (b1 >= BE0 && b1 <= BEF)  // 3バイト文字の場合
          {
            i += 1;
            if (i >= vword.Length) return false;
            s1 = vword[i];
            if (s1.Length == 2)
            {
              b1 = (Byte)(Convert.ToInt32("0x" + s1, 16));
              if (b1 >= B80 && b1 <= BBF)  // 2バイト文字目
              {
                i += 1;
                if (i >= vword.Length) return false;
                s1 = vword[i];
                if (s1.Length >= 2)
                {
                  b1 = (Byte)(Convert.ToInt32("0x" + s1.Substring(0, 2), 16));
                  if (b1 >= B80 && b1 <= BBF)  // 3バイト文字目
                    i += 1;
                  else
                    return false;
                }
                else
                {
                  return false;
                }
              }
              else
              {
                return false;
              }
            }
            else
            {
              return false;
            }
          }
          else
          {
            return false;
          }
        }
        else if (s1.Length >= 3)
        {
          b1 = (Byte)(Convert.ToInt32("0x" + s1.Substring(0, 2), 16));
          if (b1 >= B20 && b1 <= B7E)  // 1バイト文字の場合
          {
            i += 1;
          }
          else
          {
            return false;
          }
        }
        else
        {
          return false;
        }
      }
      return true;
    }

 ご質問・ご意見・ご感想

ご質問、ご意見、ご感想、バグ等のご連絡は、 こちらへ

トップページへ