正則基礎之神奇轉換

 

1        概述

這或許會是一個讓人迷惑,甚至感到混亂的話題,但也正因爲如此,纔有了討論的必要。

在正則中,一些具有特殊意義的字符,或是字符序列,被稱作元字符,如“?”表示被修飾的子表達式匹配0次或1次,“(?i)”表示忽略大小寫的匹配模式等等。而當這些元字符被要求匹配其本身時,就要進行轉義處理了。

不同的語言或應用場景下,正則定義方式、元字符出現的位置不同,轉義的方式也是林林總總,不一而同。

2        .NET正則中的字符轉義

2.1     .NET正則中的轉義符

絕大多數語言中,“\”都被作爲轉義符,用來轉義一些具有特殊意義的字符或字符序列,比如“\n”表示換行,“\t”表示水平製表符等。而這樣的轉義,應用到正則中,又會有一些意想不到的變化。

話題由C#中一個正則問題引出

string[] test = new string[]{"\\", "\\\\"};

Regex reg = new Regex("^\\\\$");

foreach (string s in test)

{

     richTextBox2.Text += "源字符串: " + s.PadRight(5, ' ') + "匹配結果: " + reg.IsMatch(s) + "\n";

}

/*--------輸出--------

源字符串: \    匹配結果: True

源字符串: \\   匹配結果: False

*/

對於這個結果,或許有人會感到迷惑,字符串中的“\\”不是代表一個經過轉義的“\”字符嗎?而“\\\\”不就應該代表兩個經過轉義的“\”字符嗎?那麼上面正則匹配的結果應該是第一個爲False,第二個爲True纔對啊?

對於這一問題,直接解釋或許不太容易理解,還是換種方式來解釋吧。

比如要匹配的字符是這樣的

string test = "(";

那麼正則如何寫呢?因爲“(”在正則中是有特殊意義的,所以寫正則時必須對它進行轉義,也就是“\(”,而在字符串中,要使用“\\” 來表示“\”本身,也就是

Regex reg = new Regex("^\\($");

這個如果理解了,那再把“(”換回“\”,同樣道理,在字符串中,要使用“\\” 來表示“\”本身,也就是

Regex reg = new Regex("^\\\\$");

通過這樣的分析,可以看出,其實在以字符串形式聲明的正則中,“\\\\”匹配的實際上就是單獨的一個“\”字符。總結一下它們之間的關係:

輸出到控制檯或界面的字符串:\

程序中聲明的字符串:string test = "\\";

程序中聲明的正則:Regex reg = new Regex("^\\\\$");

這樣解釋是不是已經可以理解了,那麼是不是感覺這樣很笨拙?是的,在程序中以字符串形式聲明的正則,涉及到轉義符時就是這樣笨拙的。

所以在C#中,還提供了另一種字符串聲明方式,在字符串前加個“@”,就可以忽略轉義。

string[] test = new string[] { @"\", @"\\" };

Regex reg = new Regex(@"^\\$");

foreach (string s in test)

{

    richTextBox2.Text += "源字符串: " + s.PadRight(5, ' ') + "匹配結果: " + reg.IsMatch(s) + "\n";

}

/*--------輸出--------

源字符串: \    匹配結果: True

源字符串: \\   匹配結果: False

*/

這樣就簡潔多了,也符合通常的理解。

但同時也帶來另一個問題,就是雙引號的轉義處理。在普通的字符串聲明中,可以用“\””對雙引號進行轉義。

string test = "<a href=\"www.test.com\">only a test</a>";

但是在字符串前加了“@”後,“\”會被識別爲“\”字符本身,這樣就不能用“\””對雙引號進行轉義了,需要用“”””對雙引號進行轉義。

string test = @"<a href=""www.test.com"">only a test</a>";

而在VB.NET中,正則的定義只有一種形式,與C#中加了“@”後的定義方式是一致的。

Dim test As String() = New String() {"\", "\\"}

Dim reg As Regex = New Regex("^\\$")

For Each s As String In test

    RichTextBox2.Text += "源字符串:" & s.PadRight(5, " "c) & "匹配結果:" & reg.IsMatch(s) & vbCrLf

Next

'--------輸出--------

'源字符串:\    匹配結果:True

'源字符串:\\   匹配結果:False

'--------------------

2.2     .NET正則中需要轉義的元字符

在MSDN中,以下字符作爲正則中的元字符,在匹配其本身時,需要對其進行轉義

. $ ^ { [ ( | ) * + ? \

但實際應用中,還要根據實際情況來判斷,以上字符可能不需要轉義,也可能不止以上字符需要轉義。

在正常的正則書寫過程中,以上字符的轉義通常都能被編寫人員正常處理,但是在動態生成正則時,就需要格外的注意,否則變量中包含元字符時,動態生成的正則在編譯時可能會拋異常。好在.NET中提供了Regex.Escape方法來處理這一問題。比如根據動態獲取的id來提取相應的div標籤內容。

string id = Regex.Escape(textBox1.Text);

Regex reg = new Regex(@"(?is)<div(?:(?!id=).)*id=(['""]?)" + id  + @"\1[^>]*>(?><div[^>]*>(?<o>)|</div>(?<-o>)|(?:(?!</?div\b).)*)* (?(o)(?!))</div>");

如果不做轉義處理,那麼動態獲取的id如果爲“abc(def”這種形式,程序運行過程中就會拋出異常了。

2.3     .NET正則中字符組的轉義

在字符組[]中,元字符通常是不需要轉義的,甚至於“[”也是不需要轉義的。

string test = @"the test string:  . $ ^ { [ ( | ) * + ? \";

Regex reg = new Regex(@"[.$^{[(|)*+?\\]");

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "\n";

}

/*--------輸出--------

.

$

^

{

[

(

|

)

*

+

?

\

*/

但是在正則書寫時,字符組中的“[”還是建議使用“\[”對其轉義的,正則本身就已經是非常抽象,可讀性很低的了,如果在字符組中再摻雜進這樣不經轉義的“[”,會使得可讀性更差。而且在出現不正確的嵌套時,可能會導致正則編譯異常,以下正則在編譯時就會拋異常的。

Regex reg = new Regex(@"[.$^{[(]|)*+?\\]");

然而,.NET的字符組中,是支持集合減法的,在這種正常語法形式下,是允許字符組嵌套的。

string test = @"abcdefghijklmnopqrstuvwxyz";

Regex reg = new Regex(@"[a-z-[aeiou]]+");

MatchCollection mc = reg.Matches(test);

foreach (Match m in mc)

{

     richTextBox2.Text += m.Value + "\n";

}

/*--------輸出--------

bcd

fgh

jklmn

pqrst

vwxyz

*/

這種用法可讀性很差,應用也很少見,即使有這種需求也可以通過其它方式實現,瞭解一下即可,不必深究。

話題再回到轉義上,字符組中必須轉義的只有“\”,而“[”和“]”出現在字符組中時,也是建議一定做轉義處理的。另外有兩個字符“^”和“-”,出現在字符組中特定位置時,如果要匹配其本身,也是需要轉義的。

“^”出現在字符組開始位置,表示排除型字符組,“[^Char]”也就是匹配除字符組中包含的字符之外的任意一個字符,比如“[^0-9]”表示除數字外的任意一個字符。所以在字符組中,要匹配“^”字符本身,要麼不放在字符組開始位置,要麼用“\^”進行轉義。

Regex reg1 = new Regex(@"[0-9^]");

Regex reg2 = new Regex(@"[\^0-9]");

這兩種方式都表達匹配任意一個數字或普通字符“^”。

至於“-”在字符組中特殊性,舉一個例子。

string test = @"$";

Regex reg = new Regex(@"[#-*%&]");

richTextBox2.Text = "匹配結果:" + reg.IsMatch(test);

/*--------輸出--------

匹配結果:True

*/

正則表達式中明明沒有“$”,爲什麼匹配結果會是“True”呢?

[]支持用連字符“-”連接兩個字符,來表示一個字符範圍。需要注意的是,“-”前後的兩個字符是有順序的,在使用相同的編碼時,後面的字符碼位應大於或等於前面字符的碼位。

for (int i = '#'; i <= '*'; i++)

{

     richTextBox2.Text += (char)i + "\n";

}

/*--------輸出--------

#

$

%

&

'

(

)

*

*/

由於“#”和“*”符合要求,“[#-*]”可以表示一個字符範圍,其中就包含了字符“$”,所以上面的正則是可以匹配“$”的,如果只是把“-”當作一個普通字符處理,那麼要麼換個位置,要麼把“-”轉義。

Regex reg1 = new Regex(@"[#*%&-]");

Regex reg2 = new Regex(@"[#\-*%&]");

這兩種方式都表示匹配字符組中列舉的字符中的任意一個。

在字符組中,還有一個比較特殊的轉義字符,“\b”出現在正則表達式中一般位置時,表示單詞邊界,也就是一側爲組成單詞的字符,另一側不是;而當“\b”出現在字符組中時,表示的是退格符,與普通字符串中出現的“\b”意義是一樣的。

同樣的,還有一個容易被忽視,而且經常被忽視的轉義符“|”,當“|”出現在正則表達式中一般位置時,表示左右兩側“或”的關係;而當“|”出現在字符組中時,它僅僅表示“|”字符本身,沒有任何特殊意義,所以如果不是要匹配“|”本身,而試圖在字符組中使用“|”時,是錯誤的。比如正則表達式“[a|b]”表示的是“a”、“b”、“|”中的任意一個,而不是“a”或“b”。

2.4     .NET正則應用中不可見字符轉義處理

對於一些不可見字符,要在字符串中表示時,需要用轉義字符,比較常見的有“\r”、“\n”、“\t”等等,而這些字符在正則中應用,就變得有些神奇了,先看一段代碼。

string test = "one line. \n another line.";

List<Regex> list = new List<Regex>();

list.Add(new Regex("\n"));

list.Add(new Regex("\\n"));

list.Add(new Regex(@"\n"));

list.Add(new Regex(@"\\n"));

foreach (Regex reg in list)

{

    richTextBox2.Text += "正則表達式:" + reg.ToString();

    MatchCollection mc = reg.Matches(test);

    foreach (Match m in mc)

    {

        richTextBox2.Text += "   匹配內容:" + m.Value + "   匹配起始位置:" + m.Index + "   匹配長度:" + m.Length;

    }

    richTextBox2.Text += "   匹配總數:" + reg.Matches(test).Count + "\n----------------\n";

}

/*--------輸出--------

正則表達式:

   匹配內容:

   匹配起始位置:10   匹配長度:1   匹配總數:1

----------------

正則表達式:\n   匹配內容:

   匹配起始位置:10   匹配長度:1   匹配總數:1

----------------

正則表達式:\n   匹配內容:

   匹配起始位置:10   匹配長度:1   匹配總數:1

----------------

正則表達式:\\n   匹配總數:0

----------------

*/

可以看到,前三種寫法,輸出的正則雖不同,但執行結果卻是完全相同的,只有最後一種是沒有匹配的。

正則表達式一Regex("\n"),其實就是以普通字符串形式來聲明正則的,與用Regex("a")來匹配字符“a”是同樣的道理,是不經過正則引擎轉義的。

正則表達式二Regex("\\n"),是以正則表達式形式來聲明正則的,正如正則中的“\\\\”就等同於字符串中的“\\”一樣,正則中的“\\n”就等同於字符串中的“\n”,是經過正則引擎轉義的。

正則表達式三Regex(@"\n"),與正則表達式二等價,是字符串前加“@”的寫法。

正則表達式四Regex(@"\\n"),其實這個表示的是字符“\”後面跟一個字符“n”,是兩個字符,這個在源字符串中自然是找不到匹配項的。

這裏需要特別注意的還是“\b”,不同的聲明方式,“\b”的意義是不同的。

string test = "one line. \n another line.";

List<Regex> list = new List<Regex>();

list.Add(new Regex("line\b"));

list.Add(new Regex("line\\b"));

list.Add(new Regex(@"line\b"));

list.Add(new Regex(@"line\\b"));

foreach (Regex reg in list)

{

     richTextBox2.Text += "正則表達式:" + reg.ToString() + "\n";

     MatchCollection mc = reg.Matches(test);

     foreach (Match m in mc)

     {

          richTextBox2.Text += "匹配內容:" + m.Value + "   匹配起始位置:" + m.Index + "   匹配長度:" + m.Length + "\n";

     }

     richTextBox2.Text += "匹配總數:" + reg.Matches(test).Count + "\n----------------\n";

}

/*--------輸出--------

正則表達式:line_

匹配總數:0

----------------

正則表達式:line\b

匹配內容:line   匹配起始位置:4   匹配長度:4

匹配內容:line   匹配起始位置:20   匹配長度:4

匹配總數:2

----------------

正則表達式:line\b

匹配內容:line   匹配起始位置:4   匹配長度:4

匹配內容:line   匹配起始位置:20   匹配長度:4

匹配總數:2

----------------

正則表達式:line\\b

匹配總數:0

----------------

*/

正則表達式一Regex("line\b"),這裏的“\b”是退格符,是不經過正則引擎轉義的。源字符串中是沒有的,所以匹配結果爲0。

正則表達式二Regex("line\\b"),是以正則表達式形式來聲明正則的,這裏的“\\b”是單詞邊界,是經過正則引擎轉義的。

正則表達式三Regex(@"line\b"),與正則表達式二等價,指單詞邊界。

正則表達式四Regex(@"line\\b"),其實這個表示的是字符“\”後面跟一個字符“b”,是兩個字符,這個在源字符串中自然是找不到匹配項的。

2.5     .NET正則應用中其它轉義處理

.NET正則應用中還有一些其它轉義方式,雖然用得不多,但也順便提一下吧。

需求:把字符串中“<”和“>”之間的數字前加上“$”

string test = "one test <123>, another test <321>";

Regex reg = new Regex(@"<(\d+)>");

string result = reg.Replace(test, "<$$1>");

richTextBox2.Text = result;

/*--------輸出--------

one test <$1>, another test <$1>

*/

也許你會驚奇的發現,替換結果不是在數字前加了“$”,而是將所有數字都替換爲“$1”了。

爲什麼會這樣呢,這是因爲在替換結構中,“$”是有特殊意義的,在它後面接數字,表示對對應編號捕獲組匹配結果的引用,而有些情況下,需要在替換結果中出現“$”字符本身,但它後面又跟了數字,這時候就需要用“$$”對它進行轉義了。而上面這個例子卻恰恰是由於這種轉義效果導致出現了異常結果,要規避這一問題,可以使替換結果中不出現對捕獲組的引用。

string test = "one test <123>, another test <321>";

Regex reg = new Regex(@"(?<=<)(?=\d+>)");

string result = reg.Replace(test, "$");

richTextBox2.Text = result;

/*--------輸出--------

one test <$123>, another test <$321>

*/

3       JavaScript及Java中的轉義符

JavaScript及Java中正則的轉義符處理,以字符串形式聲明時,基本上都是與.NET中一致的,簡單的介紹一下。

在JavaScript中,以字符串形式聲明正則,與C#中的表現是一樣的,同樣會顯得很笨拙。

<script type="text/javascript">

    var data = ["\\", "\\\\"];

    var reg = new RegExp("^\\\\$", "");

    for(var i=0;i<data.length;i++)

    {

        document.write("源字符串:" + data[i]  + "   匹配結果:" + reg.test(data[i]) + "<br />");

    }

</script>

/*--------輸出--------

源字符串:\ 匹配結果:true

源字符串:\\ 匹配結果:false

*/

JavaScript中雖然沒有提供C#中這種“@”方式的字符串聲明方式,但提供了另一種正則表達式的專有聲明方式。

<script type="text/javascript">

    var data = ["\\", "\\\\"];

    var reg = /^\\$/;

    for(var i=0;i<data.length;i++)

    {

        document.write("源字符串:" + data[i]  + "   匹配結果:" + reg.test(data[i]) + "<br />");

    }

</script>

/*--------輸出--------

源字符串:\ 匹配結果:true

源字符串:\\ 匹配結果:false

*/

JavaScript中

var reg = /Expression/igm;

這種聲明方式,一樣可以簡化含有轉義符的正則。

當然,以這種形式聲明正則時,“/”自然也就成爲了元字符,正則中出現這一字符時,必須進行轉義處理。比如匹配鏈接中域名的正則

var reg = /http:\/\/:([^\/]+)/ig;

很不幸的是,在Java中,目前只提供了一種正則聲明方式,也就是字符串形式的聲明方式

String test[] = new String[]{"\\", "\\\\" };

String reg = "^\\\\$";

for(int i=0;i<test.length ;i++)

{

  System.out.println("源字符串:" + test[i] + "   匹配結果:" + Pattern.compile(reg).matcher(test[i]).find());

}

/*--------輸出--------

源字符串:\   匹配結果:true

源字符串:\\   匹配結果:false

*/

只能期待Java的後續版本能提供這方面的優化了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章