正则表达式学习(二) - 正则的处理

在第一篇正则表达式的笔记中提到了很多元字符及其使用举例。细心一点的话,会发现不同语言或工具下正则表达式的写法和用法都有很大的不同。

在某种特定的编程语言或工具软件中使用正则表达式时,需要注意以下3个问题:

  1. 支持的元字符,以及这些元字符的意义。
  2. 正则表达式与语言或者工具的“交互”方式。比如如何进行正则表达式的操作,容许进行哪些操作,以及这些操作的目标文本类型。
  3. 正则表达式引擎如何将表达式应用到文本。语言或工具的设计者实现正则表达式的方法对正则表达式能够取得的结果有重要的影响。

上一篇介绍了正则表达式最常用的部分元字符,本篇将正则表达式与语言或工具的几种常见“交互方式”。


处理方式

首先,从整体的角度来看看不用编程语言或工具对正则表达式的处理大致有哪些方式,再通过举例了解一下Java和Perl中具体的实现。

编程语言对于正则表达式的处理方式主要有三种:集成式(integrated)、程序式(procedural)和面向对象式(object-oriented)。

集成式处理

在集成式处理方式中,正则表达式是直接内建在语言之中的,通过使用语言内建的操作符就可使用,例如Perl。集成式的处理减轻了程序员的负担,它隐藏了诸如正则表达式的预处理、准备匹配、应用正则表达式、返回结果等更细节的工作。

举个栗子:

1
2
3
if ($line =~ m/^Subject: (.*)/i) {
$subject = $1;
}

以上段代码为例,在Perl中,m/···/就表示尝试进行 正则表达式匹配=~用来连接正则表达式m/···/和与搜索的字符串,在本例中保存在变量$line中,$1中保存了捕获的第一组字符。


程序式处理和面向对象式处理

程序式处理和面向对象式处理差别不大,这两种处理方式不是由内建的操作符提供,而是由普通函数(编程式)或构造函数及方法(面向对象式)来提供。也就是说,并没有专属于正则表达式的操作符,只有一般的字符串,普通函数、构造函数及方法把这些字符串作为正则表达式来处理。

举个咖啡味的栗子(本栗取自Java Tutorials - Methods of the Matcher Class):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class MatcherDemo {
private static final String REGEX =
"\\bdog\\b";
private static final String INPUT =
"dog dog dog Doggie dogg";
public static void main(String[] args) {
Pattern p = Pattern.compile(REGEX, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(INPUT);
int count = 0;
while(m.find()) {
count++;
System.out.println("Match number "+ count);
System.out.println("start(): " + m.start());
System.out.println("end(): " + m.end());
}
}
}

上面的代码展示了Java中以面向对象式处理正则表达式的方式。可以看到在Java中正则表达式的类型就是个普通字符串String类型。上例中的 PatternMatcher 是由java.util.regex提供的专门用于处理正则表达式的两个类。

Pattern和Matcher提供了一系列操作用于处理正则表达式。在上例中就包括:

  • Pattern.compile —— 检查正则表达式字符串并将其编译为不区分大小写的内部形式,获得一个Pattern对象
  • p.matcher —— 获得一个与待匹配文本相关联的Matcher对象
  • m.find —— 检查是否存在匹配
  • m.start/m.end —— 获取匹配的起始结束为止

使用任何正则表达式的语言都要进行这些操作,在Java中它选择暴露具体的细节,需要程序员显式地调用,而在Perl中则隐藏了大多数细节。

下面我们再来看看编程式处理方式,就以Java为例。Java除了提供了创建Pattern对象来处理的方式,还提供了一些便捷函数来节省工作量,在这些静态函数中提供了临时对象,执行完成后,这些对象就会被自动抛弃。

举个栗子:

1
2
3
if (!Pattern.matches("\\s*", line)) {
// do something
}

参考Java API官方文档由如下说明:

An invocation of this convenience method of the form Pattern.matches(regex, input); behaves in exactly the same way as the expression Pattern.compile(regex).matcher(input).matches();

综上,程序式处理和面向对象式处理的差别主要在于以下三点:

  • 便捷程度 - 程序式完成简单任务时更容易,但处理复杂任务很麻烦
  • 功能 - 程序式比面向对象式要少
  • 效率 - 要根据具体使用场景分析

匹配模式

匹配模式规定了正则表达式应该如何解释和应用。不同的语言和工具对模式的设置方式也不同,一般采取修饰符或者配置选项来决定。

例如Perl中的/i修饰符代表不区分大小写,/x容许自有空格和注释;而Java中则采用标志位如Patter.CASE_INSENSITIVE来设置。

下面列举比较常见的几种模式:

  • 不区分大小写的模式 - 在匹配过程中忽略字母大小写。

  • 宽松排列和注释模式 - 忽略字符组外部的所有空白字符,字符组内部的空白字符仍然有效(java.util.regex比较特殊), #和换行符之间的内容视为注释。

  • 点号通配模式(dot-match-all,又称单行文本模式) - 通常情况下点号是不能匹配换行符的,但在此模式下,点号不受限制,可以匹配任何字符。

  • 增强的行锚点模式(Enhanced line-anchor,又称多行文本模式) - 此模式会影响^$的匹配。通常情况下,^不能匹配字符串内部的换行符,只能匹配目标字符串的起始位置。但在此模式下,它能够匹配字符串中内嵌的文本行的开头位置。$类似。在支持此模式的程序中,一般还会提供\A\Z,它们的作用于普通^$一样,并且不会发生改变。如果\Z也被影响的话,一般还会有个\z用于唯一匹配整个字符串的结尾位置。

  • 文字文本模式 - 不能识别任何正则表达式元字符,等同于简单字符串搜索。


字符编码

字符编码规定不同数值的字节应该如何解释。不同的字符编码,对于同一数值的字节的解释,不一定相同。

那么,在使用正则表达式时,需要格外关注一些问题,例如

  • 是否支持多字节字符?
  • \w,\b,\s等元字符是否能识别编码中的所有字符?
  • 忽略大小写模式是否对所有字符有效?(是否需要考虑一个大写对应多个小写,单独的标题格式等等特殊情况)

Unicode

代码点

Unicode中的一个数值通常被称为一个code point,通常使用十六进制表示。一个字符在Unicode中,可能由一个代码点组成,也可能由两个代码点构成(一个基本字符加上用于修饰的组合字符)。

那么,问题来了,对于一个由两个代码点组成的字符,在正则表达式中使用.匹配时,点号应该匹配单个代码点还是整个字符(两个代码点)呢?

目前的实践中,大部分程序将“字符”与“代码点”等价。也就是说,点号匹配单个代码点,无论是基本字符还是组合字符。

举个栗子,à,在Unicode中由 U+0061(a)U+0300(`) 组成,可以由^..$匹配,而不是^.$


行终止符

Unicode定义了多个表示行终止符的字符,如果行终止符获得完全的支持,它会影响文本行从文件读入的方式。在正则表达式中,它们会影响.^$\Z的匹配。


这里也只是对正则表达式中比较概念性的内容作了记录,具体的细节仍然是在使用时要去查看文档进行确认的。

参考资料

  • 精通正则表达式 Mastering Regular Expressions (3rd Edition).Jeffrey E.F. Friedl