The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何利用 .NET 對 byte[] 進行 Reguar Expression 比對

記得以前在寫 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 判斷式達成這個目的,直到今天才找到這個完美的方法。

相關連結