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

記得以前在寫 Perl 或 PHP 時都可以直接用內建的 Regular Expression 功能直接比對二進位的字串內容，但到了 .NET 就不知道怎麼做了，因為當你使用 [System.Text.RegularExpressions](http://msdn.microsoft.com/zh-tw/library/system.text.regularexpressions.aspx) 命名空間所接受的輸入參數只有 [String](http://msdn.microsoft.com/zh-tw/library/system.string.aspx) 型別，並不接受 byte\[\] 位元陣列。

你可能會問，怎麼會有這種需求呢？採用 Regular Expression 不就是為了要做**字串樣式比對**嗎，為什麼有必要用來比對二進位的資料呢？

例如在 Perl 中，沒有十分明確的**型別**(Type)觀念，所有變數都是動態轉型的，所以當我們從檔案讀入所有內容時，不管是**文字檔**或**二進位檔**都可以儲存在變數中，所以當我們要用 Regular Expression 比對出檔案內容中所有的 [UTF-8](http://zh.wikipedia.org/wiki/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 也可以這樣寫，你可以從[逸出字元](http://msdn.microsoft.com/zh-tw/library/4edbef7e.aspx)文件得知在 .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](http://msdn.microsoft.com/zh-tw/library/system.io.file.readalltext.aspx) 方法且不指定編碼的情況下，預設會先判斷檔案內容的 [BOM](http://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E9%A0%86%E5%BA%8F%E8%A8%98%E8%99%9F) ([byte order marks](http://en.wikipedia.org/wiki/Byte-order_mark)) 字元用以決定檔案的編碼，如果沒有 [BOM](http://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E9%A0%86%E5%BA%8F%E8%A8%98%E8%99%9F) 字元就會預設以本機的預設編碼 (繁體系統就是 Big5 編碼) 讀入。

對於所有編碼，在 .NET Framework 內部的字串都是 UTF-16 字串，所以只要檔案內容被強迫指定編碼後，所有原本檔案中的 byte order 在讀入 .NET 記憶體中之後就會被打亂，所以你就無法透過 .NET 內建的 Regular Expression 比對原本檔案中二進位字元的 byte 範圍。

所以如果需要將讀入的資料維持原狀，也且可以透過 .NET 的 Regular Expression 進行位元比對，就必須要利用下列程式碼進行讀入，讓檔案中「每一個位元」都轉換成一個 UTF-16 的字元([Char](http://msdn.microsoft.com/en-us/library/x9h8tsay.aspx))。由於每一個位元(Byte)的範圍都固定從 U+0000 to U+ffff，所以我們可以利用這個特色將每一個 byte 都轉換成 Char 字元，先儲存到 List<Char> 中，最後再轉換成字串([String](http://msdn.microsoft.com/zh-tw/library/system.string.aspx))。

```
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](http://msdn.microsoft.com/zh-tw/library/system.text.encoding.aspx))，如果要拿來比對二進位檔案的**病毒特徵碼**我想應該也是有可能的。

我一直很想找到這種比對方式的唯一目的就是因為 .NET 在讀入文字檔或讀入 Stream 資料時 (例如透過WebClient 類別下載網頁)，時常因為在讀入資料前不知道來源資料的編碼([Encoding](http://msdn.microsoft.com/zh-tw/library/system.text.encoding.aspx))，而導致資料下載後全部變成亂碼。

我之前為了找這個方法找了**好幾年**，每次找到一半就放棄，且每次都要用很長的程式碼與一堆 if 判斷式達成這個目的，直到今天才找到這個完美的方法。

**相關連結**

-   [Multilingual form encoding](http://www.w3.org/International/questions/qa-forms-utf-8)
-   [char (C# Reference)](http://msdn.microsoft.com/en-us/library/x9h8tsay.aspx)
-   [Encoding 類別](http://msdn.microsoft.com/zh-tw/library/system.text.encoding.aspx) / [使用 Unicode 編碼方式](http://msdn.microsoft.com/zh-tw/library/zs0350fy.aspx) / [了解編碼](http://msdn.microsoft.com/zh-tw/library/ms404377.aspx)
-   [System.Text.RegularExpressions 命名空間](http://msdn.microsoft.com/zh-tw/library/system.text.regularexpressions.aspx) / [Regex 類別](http://msdn.microsoft.com/zh-tw/library/system.text.regularexpressions.regex.aspx) / [.NET Framework 規則運算式](http://msdn.microsoft.com/zh-tw/library/hs600312.aspx)
-   [9.1. .NET's Regex Flavor](http://tool.87871.com/help/Mastering%20Regular%20Expressions,%203rd%20Edition/0596528124/regex3-CHP-9-SECT-1.html)
-   [位元組順序記號 ( byte-order mark，BOM )](http://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E9%A0%86%E5%BA%8F%E8%A8%98%E8%99%9F)
-   [UTF-16 - 维基百科，自由的百科全书](http://zh.wikipedia.org/wiki/UTF-16) / [UTF-8 - 维基百科，自由的百科全书](http://zh.wikipedia.org/wiki/UTF-8)
-   [Feedback: Extend Regex to process Unicode characters, not UTF-16 code units](https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=357780)
-   [.NET Regular Expressions and Unicode](https://faithlife.codes/blog/2008/07/net_regular_expressions_and_unicode/)
