2008年5月13日星期二

正则表达式介绍及其在EmEditor的应用

转自:http://bbs.et8.net/bbs/showthread.php?t=652159

作者:nb590@ccf

应lyh728之约, 也算是对命令行和正则表达式专题的支持, 随便写点东西介绍下正则表达式的基础概念. 由于本版偏重应用, 故只取EmEditor的正则子集来作介绍. Perl 和 CLR 的 Regex 的内容远比这类编辑器所支持的功能多.
正则表达式实在包含的内容太多, 仅仅用一篇文章来涵盖是没可能的了, 所以我只是简要的做些介绍和基本的模式应用举例. 即使这样也需要多次分章节的来连载了~~~ 闲话少说, 以下正文:
正则表达式, 英文 Regular expression, 简写Regexes或Regex.
应用概述: 提供与预期的搜索结果匹配的确切文本来进行字符串的搜索和替换操作, 这种技术不仅仅用于开发领域, 更被集成到一些常见的文本扩展编辑器, 如UltraEdit, Emeditor等. 历史上第一个实用应用程序是Unix 中的qed 编辑器。
举一个简单的类比: 我们对DOS中的通配符"*"和"?"应该很熟悉, 如命令"dir *.exe" 将列出所有后缀名为exe的文件名. 正则表达式提供的方法与其类似, 而且远比通配符强大的多.
从某种意义上说, 正则表达式是一种语言, 通过及其简短的一行代码即可以高效, 精确的描述要匹配的复杂文本, 当然, 它最大的优点也是他最大的缺点: 语法复杂, 创建困难. (熟悉之后就可以忽略后半句了 )
主要应用:

  • 数据验证; 这是正则表达式在开发中最常见的应用, 通过测试字符串内的模式。来验证输入的字符串是否为邮政编码, 电话号码, 电子邮件地址, 信用卡号码等等。
  • 搜索和替换文本; 用正则表达式来搜索文档中的特定文本块, 根据需要用其他指定的文本块进行替换。这也是文本编辑中的一个常见应用, 如将网页中的HTML代码转化为UBB代码.

既然发在『软件使用』板, 正则表达式的开发中的应用就不介绍了, 以下仅以EmEditor中的正则表达式来作介绍:
1. 启用正则表达式
菜单: Search - Find (Replace) - 选中 Use Regular Expressions
2. Emeditor 正则语法
正则表达式是普通字符和元字符组合的一种模式. 它的结构与算术表达式的结构类似, 各种元字符和运算符可以将小的表达式组合起来,创建大的表达式。通过在一对分隔符之间放置表达式模式的各种组件,就可以构建正则表达式。
2.1 普通字符
普通字符是指除了 ".", "*", "?", "+", "(", ")", "{", "}", "[", "]", "^", "$" 和 "\" 这些特殊字符之外的所有其他字符. 而这些特殊字符也可以通过前面加上"\"前缀而变为普通字符. 比如, 搜索"CCF"即为在文本中匹配所有的"CCF"字符串, 搜索"\[CCF\]"则是在文本中匹配所有的"[CCF]"字符串.
简而言之, 普通字符即为只匹配自身的字符.
2.2 元字符
元字符不匹配其自身,它用特殊方式来解析从而实现更多的逻辑功能。正则表达式通过元字符在模式中包含选择和循环
2.2.1 特殊字符
  • . 匹配除换行符 \n 之外的任何单个字符。
  • ( ) 分组捕获(子表达式)的开始和结束。可以捕获子表达式以供以后使用。
  • [ ] 中括号表达式的开始。
    中括号表达式是在方括号内包含一个或多个字符构成的列表的表达式。普通字符在中括号内表示本身,大多数特殊字符在中括号表达式内出现时失去它们的意义。除了转义字符'\', (要包含'\', 需要使用'\\') 如: 正则表达式 No [1234] 匹配 No 1, No 2, No 3 和 No 4.
    如果想在中括号中使用一个范围作为列表来匹配字符,可以用连字符 '-' 将范围中的开始字符和结束字符分开。单个字符的字符值确定范围内的相对顺序。如: 正则表达式 No [1-4] = No [1234]
    注意 1. 开始值的Unicode值必须在结束值Unicode值的前面。
    注意 2. [\-]匹配连字符'-', 放在中括号列表的开始或结尾也可起到同样的效果, 如 [-c-f] 匹配 c 至 f 的字符和连字符
    如果需要匹配不属于列表或范围内的任何字符,可以在列表开头加上'^'前缀。如: 正则表达式 No [^1-4] 匹配 No 5 和更大的编号.
    中括号表达式还可进行组合, 如 [A-Za-z0-9] 匹配A-Z, a-z, 0-9 的字符
  • \ 将下一字符标记为特殊字符、文本、反向引用或八进制转义符。例如,
    字符 n 匹配字符 n
    \n 匹配换行符
    序列 \\ 匹配 \
    序列 \( 匹配 (
  • | 替换字符, 对|左右的两个项分别匹配进行选择。或者说, 就是逻辑的OR的概念
  • { } 标记限定符表达式的开始。
    (数量)限定字符
    限定字符能够指定正则表达式的某个部分必须出现的次数
    • * 零次或多次匹配前面的字符或子表达式。如,c*f 可以匹配 f 和 ccf。* = {0,}
    • + 一次或多次匹配前面的字符或子表达式。如,c+f 可以匹配 cf 和 ccf,但不匹配 f。+ = {1,}
    • ? 零次或一次匹配前面的字符或子表达式。如,cc?f 可以匹配 cf 或 ccf。? = {0,1}
    • {n} n 是非负整数。正好匹配 n 次。如,c{2}f 可以匹配 ccf。
    • {n,} n 是非负整数。至少匹配 n 次。如,c{2,}f 不匹配 cf,而可以匹配 ccccccf。c{1,} = c+。c{0,} = c*
    • {n,m} m 和 n 是非负整数,其中 n <= m。至少匹配 n 次,至多匹配 m 次。如,c{1,3} 可以匹配 ccf 中的cc。c{0,1} 等效于 c?。

2.2.2 控制字符
  • \a Bell 字符。= 0x07
  • \f 换页符匹配。= 0x0C
  • \n 换行符匹配。= 0x0A
  • \r 匹配一个回车符。= 0x0D
  • \t 制表符匹配。= 0x09
  • \v 垂直制表符匹配。= 0x0B
  • \e ASCII 换码字符。= 0x1B
  • \0dd 八进制换码字符, dd代表八进制数字。
  • \xXXXX或\x{XXXX} 4位十六进制Unicode字符, XXXX代表十六进制数字。
  • \cZ Z-'@' 控制字符Control-Z, Z为大于等与"@"的ASCII字符
2.2.3 换码字符
  • \w 任一单词字符, 如A-Z, a-z, 0-9, _等, 如 \w\w\w可以匹配 U_4 但不匹配 %^e
  • \W 任一非单词字符, 如 \W\W 可以匹配 *& 但不匹配 7#
  • \s 任一空白字符,包括空格、制表符、换页符、回车符和垂直制表符。= [ \f\n\r\t\v]
  • \S 任一非空白字符. = [^ \f\n\r\t\v]
  • \d 0-9的任一数字字符, 如 \d\d可以匹配 54 但不匹配 a4
  • \D 任一非数字字符. 如 \D\D可以匹配 a4 但不匹配 54
  • \l a-z 之间的任一小写字符, 如 \l\l\l可以匹配 ccf 但不匹配 ccF
  • \L 任一非小写字符, 如 \L\L\L可以匹配 CCF 但不匹配 cCF
  • \u a-z 之间的任一大写字符, 如 \u\u\u可以匹配 CCF 但不匹配 CCf
  • \U 任一非大写字符, 如 \U\U\U可以匹配 ccf 但不匹配 ccF
  • \C 任一字符, = '.'
  • \Q 前置引号符, 其后的任意字符均被认为普通字符直至出现后置引号符\E. 同时匹配单引号和双引号
  • \E 后置引号符
2.2.4 转义字符串
表示为[:classname:], 如"[[:space:]]"表示所有的空格字符
  • alnum 任一单词字符和数字字符. = [\w\d]
  • alpha 任何一个单词字符, 如A-Z, a-z, 0-9
  • blank 任一空白字符,包括空格、制表符、换页符、回车符和垂直制表符。= [ \f\n\r\t\v] = \s
  • cntrl 任一控制字符.
  • digit 0-9的任一数字字符, = \d
  • graph 任一图形字符.
  • lower a-z 之间的任一小写字符 =\l
  • print 任一可打印字符 = '.' = \C
  • punct 任一标点符号
  • space 任一空格字符
  • upper a-z 之间的任一大写字符 = \u
  • xdigit 4位十六进制Unicode字符, = \xXXXX
  • word 任何一个单词字符, 如A-Z, a-z, 0-9, _等, = \w
  • unicode 任何一个ASCII值大于255的字符
2.2.5 定位字符
定位字符可以把正则表达式固定到行首或行尾。在Perl正则全集中还能使正则表达式出现在一个单词内、在一个单词的开头或者一个单词的结尾, emeditor只是一个子集, 不包含这个功能。
  • ^ 匹配输入字符串开始的位置。如果设置customize中的"regular expressions can match new line characters",那么 ^ 还匹配 \n 或 \r 后面的位置。 但在中括号表达式中使用的情况除外,在那种情况下它对字符集求反。
  • $ 匹配输入字符串结尾的位置。如果设置customize中的"regular expressions can match new line characters",那么 $ 还匹配 \n 或 \r 前面的位置。
3. 分组捕获和替换
分组通常用来捕获特定模式的一组文本, 并用与之后的替换操作, 这也就是将分组和替换结合起来讲解的原因.
最基本的分组构造方式就是(),在左右括号中括起来的部分,就是一个分组;在正则全集中还有如(? )的命名分组方式,这种方式组合了模式在就是对分组的部分进行了命名,这样就可以通过该组的命名来获取信息, 但这种方式在emeditor中不被支持. 以下分别来介绍各种不同的分组:
  • () 组捕获. 这种分组对模式在括号内所捕获的字符进行组合, 并且每个分组捕获的匹配结果都将保存为一个实体以备其后的操作所引用. 甚至在正则全集中还可对前面的分组进行反向引用(这是题外话, emeditor不支持). 举例说明:
    源文本:

    代码:

    site status- online members: 65, online guests: 12



    使用正则表达式:



    代码:



    (members|guests): 其后是冒号和一个空格, 最后匹配至少一个数字. 匹配模式结果如下:


    代码:

    members: 65
    guests: 12

    其中members和guests在两次匹配中被捕捉, 可以在随后的操作中引用.

  • (?:) 非组捕获. 这种分组仅仅对模式在括号内所匹配的字符进行组合, 模式所匹配的字符将不会作为一个组来捕获. 虽然他也同样成为最终的匹配结果的一部分, 但无法为其后的操作所引用. 同样以上例继续:
    使用正则表达式:

    代码:

    (?:members|guests): \d+

    匹配模式结果同样为:

    代码:

    members: 65
    guests: 12

    但是members和guests仅仅在两次匹配中被分组, 并不被捕获, 也不可以在随后的操作中引用.
    使用非捕获组有其原因和场合. 其一, 从效率上说, 捕获一个分组需要消耗额外的资源和处理时间, 所以不应该捕获不需要使用的数据. 其二, 对模式中有多个捕获组的情况, 对不需要处理的分组进行捕获只会对分组信息造成混乱. 其三, 避免不需要贪婪匹配的场合发生贪婪匹配, 贪婪匹配是正则引擎的一个重要特性, 要说清楚其机理可能还需要另外开一个专题了. 对这一点, 还以上例说明一下:
    使用不带分组的正则表达式:

    代码:

    members|guests: \d+

    匹配模式为:

    代码:

    members
    guests: 12

    这个正则表达式的问题在于, 他匹配的是"members" 或 "guests: \d+", 这是模式中贪婪"消费"字符引起的. 而通过增加括号进行分组, 使正则引擎将两个匹配选项作为一个组处理, 从而正确匹配其中的一个匹配项.

  • (?=) 正声明组, 非捕获. 此分组中的模式必须出现在声明的右侧, 并且, 这个模式不构成匹配结果的一部分. 举例:
    源文本:

    代码:

    site status- online members: 65, online guests: 12

    使用正则表达式:

    代码:

    \S+(?=\s\d+)

    此模式中规定了\s\d+必须出现在\S+声明的右侧. 也就是说, 在至少一个非空格字符(声明)的右侧必须出现一个空格字符和至少一个数字, 而且只有这个声明构成匹配结果. 匹配模式结果如下:

    代码:

    members:
    guests:

    这两次匹配中不被捕捉.

  • (?!) 负声明组, 非捕获. 此分组中的模式不得出现在声明的右侧, 并且, 这个模式不构成匹配结果的一部分. 还是用上面的例子:
    使用正则表达式:

    代码:

    \d{2}(?!,)

    此模式中规定了","不得出现在\d{2}声明的右侧. 也就是说, 在连续两个数字(声明)的右侧不得出现逗号才能被匹配. 匹配模式结果如下:

    代码:

    12

    这两次匹配中不被捕捉.





严格的说, 后面两个分组不能称之为分组, 他们只是模式声明, 他们不能成为匹配结果, 也不能被捕获. 在正则全集中, 还有反向声明分组(?<=)(?), 在emeditor中不被支持.


说到括号的功能, 本来正则中的一个重要指令-条件指令和分组内联设定是不得不说的, 可惜的是... emeditor也同样不支持~~~~


在前面的例子中一直提到匹配之后的操作, 而这个进一步的操作最常见的就是替换. 先继续上面的例子:


源文本:

代码:



site status- online members: 65, online guests: 12



使用搜索正则表达式:



代码:



(members|guests)



和替换正则表达式:



代码:



ccf-\1



匹配模式结果如下:



代码:



members
guests



替换后的文本为:



代码:



site status- online ccf-members: 65, online ccf-guests: 12



其中members和guests在两次匹配中被捕捉, 在随后被引用, 并添加ccf-前缀后替换源文本中的匹配字符.

在匹配模式中的分组匹配结果将按前后顺序被正则引擎分别赋予内部组号, 在替换操作中就可以用\加上这个组号来引用相应的匹配结果. 继续上例:


使用搜索正则表达式:



代码:



(members|guests): (\d{2})



和替换正则表达式:



代码:



ccf-\1 = \2



匹配模式结果如下:



代码:



members: 65
guests: 12



替换后的文本为:



代码:



site status- online ccf-members = 65, online ccf-guests = 12



在emeditor的正则子集中增加了一个特殊的引用: \0 , \0 将引用上次的匹配结果, 继续把:

使用搜索正则表达式:



代码:



\d{2}



和替换正则表达式:



代码:



*\0*



匹配模式结果如下:



代码:



65
12



替换后的文本为:



代码:



site status- online ccf-members: *65*, online ccf-guests: *12*



作为一个编辑软件, emeditor的正则子集中增加了一些替换修饰符:





  • \U 大写修饰. 将其后的所有的字符替换为大写


  • \L 小写修饰. 将其后的所有的字符替换为小写


  • \H 半角修饰. 将其后的所有的字符替换为半角字符. 写到这里, 不得不称许一下emeditor对中文的良好支持, 这个\H至少我是很常用的, 不喜欢看到文本里面都是些123abc之类的全角字符...


  • \F 全角修饰. 将其后的所有的字符替换为全角字符


  • \E 关闭之前的\U, \L, \H, \F修饰.



4. 常见模式举例 .... 待续

没有评论:

发表评论