記得以前在寫 Perl 或 PHP 時都可以直接用內建的 Regular Expression 功能直接比對二進位的字串內容,但到了 .NET 就不知道怎麼做了,因為當你使用 System.Text.RegularExpressions 命名空間所接受的輸入參數只有 String 型別,並不接受 byte[] 位元陣列。
你可能會問,怎麼會有這種需求呢?採用 Regular Expression 不就是為了要做字串樣式比對嗎,為什麼有必要用來比對二進位的資料呢?
例如在 Perl 中,沒有十分明確的型別(Type)觀念,所有變數都是動態轉型的,所以當我們從檔案讀入所有內容時,不管是文字檔或二進位檔都可以儲存在變數中,所以當我們要用 Regular Expression 比對出檔案內容中所有的 UTF-8 字串,就可以用以下 Regular Expression 取法獲得:
$field =~
m/\A(
[\x09\x0A\x0D\x20-\x7E] # ASCII
| [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
| \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
| \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
| \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)*\z/x;
這是一種多麼直覺的用法阿!你可以比對出任意檔案的內容,直接用二進位比對文字編碼,用以判斷該檔案採用哪一種編碼(Encoding),不過到了 .NET 就沒那麼直覺了。
事實上,.NET 也可以這樣寫,你可以從逸出字元文件得知在 .NET 中也有支援符合使用十六進位表示的 ASCII 字元比對 ( 例如: 0x20 ),所以上述 Perl 語法的 Regular Expression 也可以完整搬到 .NET 上,若用 C# 語法表示範例如下:
Regex rx = new Regex(@"[\x09\x0A\x0D\x20-\x7E] # ASCII
| [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
| \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
| \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
| \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
", RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
不過,當你要將一個 未知編碼(Unknown Encoding) 的檔案讀入比對時,卻會立即發生問題,例如:
string data = File.ReadAllText(@"C:\test.txt");
當在嘗試使用 File.ReadAllText 方法且不指定編碼的情況下,預設會先判斷檔案內容的 BOM (byte order marks) 字元用以決定檔案的編碼,如果沒有 BOM 字元就會預設以本機的預設編碼 (繁體系統就是 Big5 編碼) 讀入。
對於所有編碼,在 .NET Framework 內部的字串都是 UTF-16 字串,所以只要檔案內容被強迫指定編碼後,所有原本檔案中的 byte order 在讀入 .NET 記憶體中之後就會被打亂,所以你就無法透過 .NET 內建的 Regular Expression 比對原本檔案中二進位字元的 byte 範圍。
所以如果需要將讀入的資料維持原狀,也且可以透過 .NET 的 Regular Expression 進行位元比對,就必須要利用下列程式碼進行讀入,讓檔案中「每一個位元」都轉換成一個 UTF-16 的字元(Char)。由於每一個位元(Byte)的範圍都固定從 U+0000 to U+ffff,所以我們可以利用這個特色將每一個 byte 都轉換成 Char 字元,先儲存到 List<Char> 中,最後再轉換成字串(String)。
byte[] _bytes = File.ReadAllBytes(@"C:\test.txt");
List<char> _cList = new List<char>();
foreach (byte b in _bytes)
{
_cList.Add((char)b);
}
string data = new string(_cList.ToArray());
成功將檔案內容轉成一連串的 Char 字元並轉成 String 後,就可以利用上述的 rx 物件進行比對了,如下範例:
foreach (Match mx in rx.Matches(data))
{
byte[] bb = new byte[mx.Value.Length];
Console.Write("Length: {0} ", mx.Value.Length);
Console.Write("Bytes: ");
for (int i = 0; i < mx.Value.Length; i++)
{
bb[i] = (byte)mx.Value[i];
Console.Write("{1}(0x{2:X}) ", i, bb[i], bb[i]);
}
string a = Encoding.UTF8.GetString(bb);
Console.WriteLine("\tChar: [{1}]", mx.Value.Length, a);
}
透過這個方式就可以利用 .NET 直接對任意檔案進行二進位位元的 Regular Expression 比對,這看起來似乎是很罕見的使用方式,但卻很適合用來判斷來源檔案的文字編碼(Encoding),如果要拿來比對二進位檔案的病毒特徵碼我想應該也是有可能的。
我一直很想找到這種比對方式的唯一目的就是因為 .NET 在讀入文字檔或讀入 Stream 資料時 (例如透過WebClient 類別下載網頁),時常因為在讀入資料前不知道來源資料的編碼(Encoding),而導致資料下載後全部變成亂碼。
我之前為了找這個方法找了好幾年,每次找到一半就放棄,且每次都要用很長的程式碼與一堆 if 判斷式達成這個目的,直到今天才找到這個完美的方法。
相關連結