正则基础之 神奇的转义

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的后续版本能提供这方面的优化了。

(0)

相关推荐

  • 正则基础之 神奇的转义

    1 概述这或许会是一个让人迷惑,甚至感到混乱的话题,但也正因为如此,才有了讨论的必要.在正则中,一些具有特殊意义的字符,或是字符序列,被称作元字符,如"?"表示被修饰的子表达式匹配0次或1次,"(?i)"表示忽略大小写的匹配模式等等.而当这些元字符被要求匹配其本身时,就要进行转义处理了.不同的语言或应用场景下,正则定义方式.元字符出现的位置不同,转义的方式也是林林总总,不一而同.2 .NET正则中的字符转义2.1     .NET正则中的转义符绝大多数语言中,&qu

  • 正则基础之 \b 单词边界

    1概述 "\b"匹配单词边界,不匹配任何字符. "\b"匹配的只是一个位置,这个位置的一侧是构成单词的字符,另一侧为非单词字符.字符串的开始或结束位置."\b"是零宽度的. 基本上所有的资料里都会说"\b"是单词边界,但是关于"单词"的范围却是少有提及.通常情况下,正则表达式中所谓的"单词",就是由"\w"所定义的字符所组成的子串. "\b"表示所

  • 正则基础之 环视 Lookaround

    1       环视基础 环视只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果,是零宽度的.环视匹配的最终结果就是一个位置. 环视的作用相当于对所在位置加了一个附加条件,只有满足这个条件,环视子表达式才能匹配成功. 环视按照方向划分有顺序和逆序两种,按照是否匹配有肯定和否定两种,组合起来就有四种环视.顺序环视相当于在当前位置右侧附加一个条件,而逆序环视相当于在当前位置左侧附加一个条件. 表达式 说明 (?<=Expression) 逆序肯定环视,表示所在位置左侧能够匹配Exp

  • 正则基础之 NFA引擎匹配原理

    1       为什么要了解引擎匹配原理 一个个音符杂乱无章的组合在一起,弹奏出的或许就是噪音,同样的音符经过作曲家的手,就可以谱出非常动听的乐曲,一个演奏者同样可以照着乐谱奏出动听的乐曲,但他/她或许不知道该如何去改变音符的组合,使得乐曲更动听. 作为正则的使用者也一样,不懂正则引擎原理的情况下,同样可以写出满足需求的正则,但是不知道原理,却很难写出高效且没有隐患的正则.所以对于经常使用正则,或是有兴趣深入学习正则的人,还是有必要了解一下正则引擎的匹配原理的. 2       正则表达式引擎

  • 正则基础之 小数点

    一些细节 对于使用传统NFA引擎的大多数语言和工具,如Java..NET来说,"."的匹配范围是匹配除了换行符"\n"以外的任意一个字符. 但是对于javascript来说有些特殊,由于各浏览器的解析引擎不同,"."的匹配范围也有所不同,对于Trident内核的浏览器,如IE来说,"."同样是匹配除了换行符"\n"以外的任意一个字符,但是对于其它内核的浏览器,如Firefox.Opera.Chrome来说,

  • jQuery中的正则表达式分析 正则基础

    quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/ (?:-)表示是一个非捕获型 [^<]表示是以"<"起始,包含0个或多个'<'括号 (<[\w\W]+>)表示是一个捕获型,以'<>'起始,中间包含一个或多个字符 $表示字符的结尾 (#([\w\-]+))表示是一个捕获型,以'#'号和字符串.数字._以及-组成 rnotwhite = /\S/ \S表示是空白字符

  • PHP preg_replace() 正则替换所有符合条件的字符串

    需要我们用程序处理的数据并不总是预先以数据库思维设计的,或者说是无法用数据库的结构去存储的. 比如模版引擎解析模版.垃圾敏感信息过滤等等. 一般这种情况,我们用正则按我们的规则去匹配preg_match.替换preg_replace. 但一般的应用中,无非是些数据库CRUD,正则摆弄的机会很少. 根据前面说的,两种场景:统计分析,用匹配:处理用替换. PHP preg_replace() 正则替换,与Javascript 正则替换不同,PHP preg_replace() 默认就是替换所有符号匹

  • 正则匹配原理之 逆序环视深入 .

    说明:部分内容有待进一步研究和修正,因为最近工作太忙,暂时抽不出时间来,未研究过的可以跳过这一篇,想研究的不要被我的思路所左右了,有研究清楚的还请指正1 问题引出 前几天在CSDN论坛遇到这样一个问题: var str="8912341253789"; 需要将这个字符串中的重复的数字给去掉,也就是结果89123457. 首先需要说明的是,这种需求并不适合用正则来实现,至少,正则不是最好的实现方式. 这个问题本身不是本文讨论的重点,本文所要讨论的,主要是由这一问题的解决方案而引出的另一个

  • 正则应用之 逆序环视探索 .

    1 问题引出 前几天在CSDN论坛遇到这样一个问题. 我要通过正则分别取出下面 <font color="#008000"> 与 </font> 之间的字符串 1.在 <font color="#008000"> 与 </font> 之间的字符串是没法固定的,是随机自动生成的 2.其中 <font color="#008000"> 与 </font>的数量也是没法固定的,也是

  • webregexp 正则测试实现代码

    WebRegExp 1.0 - 客服果果 [ 无忧版 ] body{background:#2B3C53;} *{font-size:12px;} #win{ width:900px;font-size:12px; position:absolute;left:0;top:0; background:#F1F0EA; border:2px outset;color:#000; -moz-border-top-colors:#d4d0c8 white; -moz-border-left-color

随机推荐