ソフト関連Trial−No.104 |
はじめに |
アクセスログ集計・解析ソフト「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]メソッドの引数 vwords は、アクセスログ中の検索語句そのものです。
// [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;
}
}
// [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;
}
ご質問・ご意見・ご感想 |
ご質問、ご意見、ご感想、バグ等のご連絡は、
こちらへ