2008年09月05日

经常会有人问起正则表达式的问题,常见的形式是:要解决这个问题,该怎么写正则表达式?

OK,常见的某些问题,正则表达式应用起来的确得心应手。譬如,验证一个字符串是否数字字符串,\d+\,确实比逐个检查字符,用“或”连接10个判断(是否0-9之间的字符)要方便。

然而,正则表达式终究是一种工具,功能再强大,也不是万能的。许多时候,仅仅希望依靠正则表达式,更准确地说,是“一条”正则表达式来完成任务,反而会走远路。

譬如要找出这样的字符串:aba, aabaa, aaabaaa……
也就是这样的字符串:在字母b的前后,字母a连续出现的次数一样多
这种问题,正则表达式确实能解决。但如果要仅仅用“一条正则表达式”来解决,目前只有Perl的正则表达式强大到这种程度,其它语言中的正则表达式,恐怕做不到这一点。
怎么办呢?其实,我们只需要另外加上一个判断作为辅助,就好了:
正则表达式写作
(?<!a)(a+)b(a+)(?!a)
然后比较group 1和group 2的长度,就可以满足要求了。
这里解释一下开头和结尾的两个结构:

  • (?<!a)表示这个正则表达式匹配文本的左端(而不是在匹配文本之内)不能是字符a,可以是其它任何字符(甚至没有字符)。
  • (?=!a)表示这个正则表达式匹配文本的右端(而不是在匹配文本之内)不能是字符a,可以是其它任何字符(甚至没有字符)。

在应用正则表达式时,记住添加一些用于判断的“锚点”(anchor),准确地表达我们的意图,是一个好习惯。

另一类常见的问题是,没有找到合适的思路,于是写不出正确的表达式。

譬如要处理类似这样的字符串(这个字符串太神奇,但确实有人问起):
printf("%d; %d; %d;", %a, %b, %c);
去掉引号外的%。
“我要怎么用正则表达式来表示“引号外部”的概念呢?”,这个朋友被困住了。
但是,我们仔细观察就会发现,其实我们真正关注的不是“引号外部”,而是引号字符串(也就是表示格式的字符串)之后的内容,也就是说,最后一个引号右边的内容,仔细观察就会发现,其中文字的特点是,它们的右边都没有引号,于是问题就变成了,“去掉这样的百分号,它右边的字符串中不包含引号”。
于是我们很顺利地写出正则表达式
%(?=[^"]*$)
这里,一定不要忘记添加$,这样,括号内的结构才能匹配右边的所有字符,而不是其中的一串字符。

2008年05月02日

2008年04月07日

数据库?不就是MySQL之类嘛!
关系?不就是表嘛!
数据库设计?对着界面就能想出来嘛!
考虑不周全?改表嘛!
速度不够快?你建索引了吗?加索引嘛!
……
在我的开发经验中,类似的问答并不罕见。可是,这正常吗?
如今,DBMS似乎已经成了大多数应用中数据持久化当仁不让的选择;而大量的开发场合里,并没有专职的DBA存在,于是,数据库的安装、设计、维护、操作,往往由程序员兼任。不幸的是,在不少程序员那里,数据库的知识却是非常匮乏的——否则,就不会有上面这些问答了。
这也难怪,学校开设的数据库课程,往往太过古老——不少课本还在讲Foxbase(把课程当考古呢);太过枯燥——1NF、2NF、3NF、BCNF,想想就头疼(况且有过开发经验的人都知道,很多时侯不能完全规范化呢);太过理论化——索引的检索比率(retrieve ratio)在10%左右是正常的(拜托,说那话的时候500,000行数据就算大表了,如今,这样的表司空见惯吧)……况且,如今的DBMS又这么智能,这么易用,索性彻底打破那些条条框框,按照直觉,怎样趁手就怎样来,似乎也是个不错的选择。
然而,问题似乎并不是这么简单,尤其是在应用日益复杂,对速度的要求又愈来愈严格的今天——数据库没设计好,复杂应用或是无法实现,或是效率极低;SQL语句写的不够简洁高效,速度就会大打折扣。日常开发中,这样的例子屡见不鲜:两条结果相同的SQL语句,一条需要0.05秒,一条需要0.2秒,如果一天执行50,000次,时间差别就是将近2小时!我见过某个普遍使用的论坛程序中,会把整个数据库锁死172秒的SQL语句;也遇见过设计不当的数据库,在里面60秒钟才能完成本来只需要5秒的查询。
每次遇到这种情况,我都在想,如果有一本书,从实际出发,教给我们“真正实用”的数据库的知识,该多好?

O’Reilly的the Art of SQL,就是这样一本书!
光看名字,这本书讲的是SQL语言的“技艺”,然而随便翻阅两页,你就会发现,这其实是一本数据库实战全书。它的内容巨细靡遗,上至数据库设计的整体思想,下至SQL语句的调优细节;而且针对各种实际开发中遇到的各类典型问题,不但给出了解决方案,更给出了理由:
一家公司,会计部门、办公部门和生产部门的地址可能各不相同,我们该怎么办?留出三个字段吗?可是许多大多数时候,一个地址就够了,那么,另两个地址设为NULL,还是直接复制数据?如果设为NULL,判断并读取地址的操作,是应该放在数据库中呢,还是程序语言中呢?如果复制数据,如何保持更新的一致性呢?
如果要判断是否存在合适的数据,再做修改,应该先用count(*)吗?可是,有经验的程序员似乎有另外的办法,他们是怎么做的呢?
教科书上说,规范化是必须的,但是,有时候去规范化也是必须的,什么情况下应该去规范化,我们如何判断呢?
更多的索引能够带来更高的效率吗?系统地为外键建立索引,真的是良好的习惯吗?如果已经建立了多字段索引,是否还有必要为其中某个字段单独建索引?
数据量大了,表分区(或者“表空间”)是个不错的解决办法。然而,该如何分区呢?什么情况下选择循环分区?什么情况下选择数据驱动分区?
复杂的数据处理逻辑,是应该放到数据库内部(PL/SQL),还是交给外边的程序语言(C语言)?
类似的问题还有很多。可以说,这样的问题,许多时候并没有固定的、现成的答案,我们必须依据具体情况,进行考量,作出抉择。此时,书中介绍的经验,给出的抉择指导、判断依据,就显得尤其珍贵了。
除此之外,这本书还有两个地方深得我喜爱:一点是介绍了好的开发习惯,譬如在每条SQL语句开头加上注释,以便调试(相信我,在复杂系统中,这个习惯非常、非常有用);另一点是,作者写书借用了《孙子兵法》的智慧,所举的例子也多与军事有关——譬如介绍索引时,所用的数据就是拿破仑麾下战将的名字——我相信,“好程序员大都不专注”,所以对军事有兴趣的程序员,应该能体会到更多的妙趣。

关系数据库与软件开发中的其它许多领域不同,它有完备的理论基础,又必须融入纷繁复杂的现实。the Art of SQL,或许更适合翻译为《SQL的技艺》,因为它是理论与现实之间,一座经验的桥梁。

2007年12月14日

在用浏览器访问Gmail时,经常会遇到这样的情况,登录成功,但马上便提示无法连接服务器,这其实是因为Gmail的Web页面默认启用的聊天功能被GFW所致,解决的办法就是关掉Web中的聊天功能,具体设置如。

下面的截图是登录Gmail之后,在浏览器最下边显示的信息。在默认情况下,用户的选项是最左边的“standard with chat”,我们登录Gmail之后,抢在连接被阻断之前,选择“standard without chat”,就可以了。

2007年10月18日

(欢迎转载,转载请注明出处)

在日常应用正则表达式时,经常遇到的一类问题是,弄不清楚到底该如何转义——最明显的表现就是,搞不懂究竟要使用多少个反斜线,大部分时候,只好盲目尝试,直到测试成功为止。但是,许多时候,这样的方法并不能完整解决问题。
为了彻底解决这类问题,我们需要弄清楚正则表达式与字符串的关系:它其实很简单,根据我的经验,我们只需要牢记下面两条原则:

1.正则表达式不等于字符串,但必须以字符串的形式给出

大多数语言中都存在正则表达式(regex)对象,譬如Java语言中的Pattern对象,即使没有提供专门的对象,也需要用某些特殊的字符来标注正则表达式,譬如PHP中常用的反斜线‘/’。另一方面,正则表达式对某些字符或字符序列有自己的规定,不同于字符串的规定,譬如‘\b’在正则表达式中表示单词分界符,而在普通字符串中表示退格字符。因此我们可以说,正则表达式并不等于普通的字符串。
但是,正则表达式又终究是一种处理文本的语言,因此,我们写出的所有正则表达式,都是以字符串形式提供的。
所以,在应用正则表达式的过程中,需要进行从字符串到正则表达式本身的转换。
也就是
“源代码中的字符序列”->“字符串”->“正则表达式”

譬如,在Java语言中,正则表达式中的‘\b’,在以字符串表示时,必须写做
String regex = "\\b";
其中第1个反斜线用来转义第2个反斜线,这样,正则表达式接受到的才是真正的‘\b’,对应正则表达式中的单词分界符,如果我们写
String regex = "\b";
编译也没有问题,但此时正则表达式接受的是一个字符,即退格符。

需要注意的是,在Java和C#之类的语言中,如果字符串中出现的字符序列无法识别,编译会出错,譬如这样:
String regex = "\w";
编译无法通过,因为根据字符串的规定,‘\w’不是一个合法的转义序列,在字符串这一关就卡住了。

但是在PHP和Python之类的语言中,这样写却没有问题。原因在于,如果PHP和Python发现字符串中有无法识别的转义序列,会原封不动地保存下来,这样,正则表达式接受到的,仍然是‘\w’。
当然,我们也可以在这些语言中使用‘\\w’,结果是一样的,因为此时,在进行字符串处理时,第1个反斜线转义了第2个反斜线,正则表达式接受到的,仍然是‘\w’。

这是在实际开发中非常迷惑人的一点,只要我们弄清了正则表达式和字符串的关系,就不会再感到迷惑。

2.正则表达式中单独出现的反斜线也需要转义

在正则表达式中,反斜线通常与其它字符一起构成特殊的结构,譬如‘\d’用来匹配数字字符,‘\s’用来匹配空白字符,‘\1’用来反向引用第一个分组捕获的文本。
可是,如果我们在正则表达式中,仅仅需要表示“反斜线”字符本身,该如何做呢?
其实,正则表达式对这个问题的处理,与字符串的处理是一样的,也就是说,在正则表达式中,必须用转义序列‘\\’来表示单个反斜线。
这个规定会带来一个有趣的问题:正则表达式中单独出现的反斜线字符,在正则表达式的层面,必须以转义序列‘\\’来表示,然而,每个反斜线,在表示正则表达式的字符串中,又必须以转义序列‘\\’来表示。所以,在字符串中,必须写出四个反斜线‘\\\\’,才能对应到正则表达式中单独出现的一个反斜线字符:在字符串处理层面,它们会被识别为两个反斜线‘\\’,在正则表达式的层面,它们会被识别为单个反斜线字符‘\’。

很麻烦,很困惑,但是,其实也不难明白,对吗?

2007年09月28日

Analyze Table

MySQL的Optimizer(优化元件)在优化SQL语句时,首先需要收集一些相关信息,其中就包括表的cardinality(可以翻译为“散列程度”),它表示某个索引对应的列包含多少个不同的值——如果cardinality大大少于数据的实际散列程度,那么索引就基本失效了。
我们可以使用SHOW INDEX语句来查看索引的散列程度:

SHOW INDEX FROM PLAYERS;

TABLE   KEY_NAME COLUMN_NAME CARDINALITY
——- ——– ———– ———–
PLAYERS PRIMARY  PLAYERNO             14

因为此时PLAYER表中不同的PLAYERNO数量远远多于14,索引基本失效。
下面我们通过Analyze Table语句来修复索引:

ANALYZE TABLE PLAYERS;
SHOW INDEX FROM PLAYERS;
结果是:
TABLE   KEY_NAME COLUMN_NAME CARDINALITY
——- ——– ———– ———–
PLAYERS PRIMARY  PLAYERNO           1000

此时索引已经修复,查询效率大大提高。

需要注意的是,如果开启了binlog,那么Analyze Table的结果也会写入binlog,我们可以在analyze和table之间添加关键字local取消写入。

Checksum Table

数据在传输时,可能会发生变化,也有可能因为其它原因损坏,为了保证数据的一致,我们可以计算checksum(校验值)。
使用MyISAM引擎的表会把checksum存储起来,称为live checksum,当数据发生变化时,checksum会相应变化。
在执行Checksum Table时,可以在最后指定选项qiuck或是extended;qiuck表示返回存储的checksum值,而extended会重新计算checksum,如果没有指定选项,则默认使用extended。

Optimize Table

经常更新数据的磁盘需要整理碎片,数据库也是这样,Optimize Table语句对MyISAM和InnoDB类型的表都有效。
如果表经常更新,就应当定期运行Optimize Table语句,保证效率。
与Analyze Table一样,Optimize Table也可以使用local来取消写入binlog。

Check Table

数据库经常可能遇到错误,譬如数据写入磁盘时发生错误,或是索引没有同步更新,或是数据库未关闭MySQL就停止了。
遇到这些情况,数据就可能发生错误:
Incorrect key file for table: ‘ ‘. Try to repair it.
此时,我们可以使用Check Table语句来检查表及其对应的索引。
譬如我们运行
CHECK TABLE PLAYERS;

结果是
TABLE          OP    MSG_TYPE MSG_TEXT
————– —– ——– ——–
TENNIS.PLAYERS check status   OK

MySQL会保存表最近一次检查的时间,每次运行check table都会存储这些信息:

执行
SELECT    TABLE_NAME, CHECK_TIME
FROM      INFORMATION_SCHEMA.TABLES
WHERE     TABLE_NAME = ‘PLAYERS’
AND       TABLE_SCHEMA = ‘TENNIS’;

结果是

TABLE_NAME   CHECK_TIME
———-   ——————-
PLAYERS      2006-08-21 16:44:25

Check Table还可以指定其它选项:
UPGRADE:用来测试在更早版本的MySQL中建立的表是否与当前版本兼容。
QUICK:速度最快的选项,在检查各列的数据时,不会检查链接(link)的正确与否,如果没有遇到什么问题,可以使用这个选项。
FAST:只检查表是否正常关闭,如果在系统掉电之后没有遇到严重问题,可以使用这个选项。
CHANGED:只检查上次检查时间之后更新的数据。
MEDIUM:默认的选项,会检查索引文件和数据文件之间的链接正确性。
EXTENDED:最慢的选项,会进行全面的检查。

Repair Table

用于修复表,只对MyISAM和ARCHIVE类型的表有效。
这条语句同样可以指定选项:
QUICK:最快的选项,只修复索引树。
EXTENDED:最慢的选项,需要逐行重建索引。
USE_FRM:只有当MYI文件丢失时才使用这个选项,全面重建整个索引。

与Analyze Table一样,Repair Table也可以使用local来取消写入binlog。

2007年09月26日

当年,斯大林同志对高尔基同志的《姑娘与死神》的批示,曾引起两名教授在《真理报》上撰文论证:斯大林同志之所以少写一个字母,是因为新生阶级的爱情截然不同于腐朽没落的资产阶级爱情。最后领袖再次批示:笨蛋,此系笔误。

最近,贾平凹老师又为铁凝老师的题词张罗了一次诠释。我们的表现,确实还远远比不上当年的老大哥。

奥威尔的《1984》,最后一句话是:他战胜了他自己,他热爱老大哥!看来,我们的某些同志,确实还无法战胜自己,离老大哥还差的远呢。

2007年09月14日

因为工作不够细致,最终版本的《精通正则表达式》还存在如下问题,只能以勘误形式发布作为补救了,请各位读者见谅。
如果大家在阅读中发现其他问题,欢迎来信指出。
yusheng.regex@gmail.com


推荐序

vi页, 第1行, “那它就被成为阳春应用”,应该修改为“那它就被称为阳春应用”;

前言

Ⅱ页,倒数第3段,“读这本书以前,我以为自己了解正则表达式,但现在我才真正了解”应修改为“读这本书以前,我以为自己了解正则表达式,但现在我才真正弄明白”

Ⅱ页,倒数第2段,“在其它任何地方都难以找到这样丰富的细节”应修改为“在其它任何地方都难以找到这样完整而详尽的资料”

Ⅴ页,第1段,“来开发引擎的能力,并避免其中的缺陷”应修改为“来发掘引擎的能力,绕开引擎的缺陷”

Ⅵ页,第1段,“[…]表示一对方括号,之间的内容无关紧要;而[…]表示一对方括号,其中包含三个句点”应修改为“[…]表示其间内容无关紧要的一对方括号,而[…]表示包含三个句点的方括号”

第1章

2页,倒数第2段,“但是它不一定能代表正则表达式在平时解决的那些“不值一提”(uninteresting)的问题。这里的“不值一提”是指这类问题并不能成为谈 资,可是不解决它们,你就没法继续干活”应修改为“但是正则表达式在平时还用来解决那些“讨人厌(uninteresting)”的问题。说“讨人厌”, 是因为它们不适合跟外人吹嘘,可是不解决它们,你就没法继续干活”

3页,第12行,“^(From|Sbuject):”应修改为“^(From|Subject):”

6页,标题“正则表达式的思维框架”应修改为“理解正则表达式的结构”

9页,倒数第3段,“在搜索HTML代码的头文件时这非常有用”应修改为“在搜索HTML Header时这非常有用”

12页,注4中“作为一个小孩子,那时候我感觉非常受伤”应修改为“当时我还是个孩子,很伤心”

14页,第4段,“匹配一行的起始位置,然后匹配「^From」、「Subject」或「Date」中的任意一个”应修改为“匹配一行的起始位置,然后匹配「From」、「Subject」或「Date」中的任意一个”

15页,第3段,“我使用-i参数的频率很高”应修改为“我经常使用-i参数”

15页,倒数第4段,“>”字符不应该是黑体

17页,倒数第3段,“无论u是否出现,匹配都是成功的”应修改为“无论u是否出现,匹配都会成功”

18页,倒数第3段,“因为它们限定了所作用元素的匹配次数”应修改为“因为它们限定了所作用元素的重现次数”

19页,第2段,“一个字符组是一个“元素”(unit),所以它可以直接加加号、星号等,而不需要用括号”应修改为“一个字符组就是一个“元素”(unit),可以对它直接使用加号、星号等,而不需要括号”

19页,倒数第1段,“每个量词都规定了匹配成功至少需要的次数下限,以及尝试匹配的次数上限”应修改为“每个量词都规定了匹配成功至少需要的重现次数下限,以及尝试匹配的重现次数上限”

20页,表1-2中:“可以不出现,也可以只出现一次”修改为“可以出现,也可以只重现一次”;“可以出现无数次,也可以不出现”修改为“可以重现无穷多次,也可以不出现”; “可以出现无数次,但至少要出现一次”修改为“可以重现无穷多次,但至少要出现一次”

21页,倒数第2段,“这并不是正则表达式的错误”应修改为“正则表达式对此无能为力”

27 页,第1段,“而且说“如果你写一个正则”,“巧妙的正则”(budding regexers),甚至是“正则化”(regexification)”修改为“而且说“如果你写一个正则”,“巧妙的正则”(budding regexers),甚至是“正则化”(regexification)听起来更顺一些。我说的“正则引擎(regex engine)”指的是程序中实际执行匹配尝试的那个部分”

第2章

37页,第1段,“它们都非常不同于“传统”的语言,例如C和Pascal”应修改为“它们截然不同于C和Pascal之类“传统”的语言”

37页,第13行,“print “celsuius C is … #返回摄氏和华氏温度” 应修改为 “print “celsuius C is … #输出摄氏和华氏温度”

38页,倒数第1段,“运算符==用来测试两个数字是否相等”修改为“运算符==用来测试两个数值是否相等”

40 页,第1段,“我不想在本章中讨论Perl的细节,但是我告诉你用printf(“格式化输出(print formatted)”)可以解决这个问题”修改为“我不想在本章中讨论Perl的细节,不过我还是想说,printf(“格式化输出(print formatted)”)可以解决这个问题”

40页,第3段,“Perl通常情况下不区分整数和浮点数”修改为“Perl一般不区分整数和浮点数”

41页,倒数第1段,“我们发现,这个图让我们很容易地决定匹配之后应该干什么”修改为“看了这张图,我们很容易就能决定匹配之后应该干什么”

46页,补充内容的最后1段,“尽管因为第4章将会解释其原因,字符组的效率通常还是会高一点”修改为“不过根据第4章解释的原因,字符组的效率通常要高一些”

56页,倒数第2行,“^From: (\s+) \”修改为“^From: (\S+) \”(S的大小写不同)

57页,第3行,“^From: (\s+) \”修改为“^From: (\S+) \”(S的大小写不同)

67页,第3段下面,正则表达式“$text =~ s/(\d)(?=(\d\d\d)+(?!\d)/$1,/g”应修改为“$text =~ s/(\d)(?=(\d\d\d)+(?!\d))/$1,/g”,少了一个括号

70页,第2段,所以整个正则表达式的意义就不再是“寻找空行及只包含空白字符的行”,而是“寻找连续、空行和只包括空白字符的行的结合”
应修改为
所以整个正则表达式的意义就不再是“寻找空行或只包含空白字符的行”,而是“寻找空行和只包含空白字符的行的结合”

76页,倒数第2段,“此外,我们的匹配主机名的正则表达式只存在一个“主源(main source)””应修改为“此外,我们的匹配主机名的正则表达式只存在一个“源头(main source)”,”

第78页
示例2-3中第4标注
{\e[7m$1\e[m$2\e[7m$3\e[m ] igx;
应修改为
{\e[7m$1\e[m$2\e[7m$3\e[m } igx;

第3章

89页,第1段,“不支持字符组中的\w(完全不支持\d和\s)”应修改为“在字符组中无法使用\w(\d和\s在任何地方都无法使用)”

92页,倒数第2段,“在极端的情况下,反向引用的“行为”有意义吗?”应修改为“在极端的情况下,反向引用还能正常工作吗?”

93页,第12行,”┌(July|Jul)┘和┌\(July\|Jul\)┘能够取得同样的匹配结果…“应修改为”┌(Jul|July)┘和┌\(July\|Jul\)┘能够取得同样的匹配结果…“

95页,标题“函数式处理的例子”应修改为“程序式处理的例子”

95页,倒数第1段,“不过,Java也提供了一些函数式处理的……”应修改为“不过,Java也提供了一些程序式处理的……”

95页,Pattern r = Pattern.compile("^Sujbcet:(.*)",Pattern.CASE_INSENSITIVE);
其中的Sujbcet应写为Subject

96页,第2段,“Sun的package同时提供了程序式和面向对象式的处理方式是常见的做法”应修改为“Sun的package同时提供了程序式和面向对象式的处理方式,这是种常见的做法”

96页,倒数第六行,”出现NULL(而且可以用点号来匹配)。“应修改为”出现NUL(而且可以用点号来匹配)。“

111页,第1段,“常见的例子是大写的ß是两个字符的组合“SS”。这种情况只有Perl能够正确处理”应修改为“常见的例子是,大写的ß由两个字符“SS”组合而成。这种情况只有Perl能够正确处理”

114页,“字符组及相关结构”中,“字符组缩略表示法”应修改为“字符组简记法”

119页,第2行,”范围内的所有字符…(例如,[0-9]与[908176354]是一样的。)…“应修改为”范围内的所有字符…(例如,[0-9]与[9081763542]是一样的。)…“

130页,3-11表名“脚本语言中的行锚点”应修改为“若干语言中的行锚点”

131页,第1段,“如果结合上面那一点”应修改为“结合第一点”

132页,第3段,“但是这段程序的执行单位不是一次表达式而是一次匹配”应修改为“但是,循环的单位不是单个的表达式,而以一组表达式的匹配”

134页,“中间一级不过是“语法(syntactic sugar)”,表达方式更美观而已,”应修改为“中间一级不过是用起来更方便而已(syntactic sugar)”

第4章

152页,倒数第1段,括号内,“抑制自己的天性”应修改为“克制自己的本能”

156 页,第3段,“也就是说“不到最后关头不能分胜负(It’s not over until the fat lady sings)”,但这段话又不符合本段的语境”
应修改为
“也就是说“不到最后关头不能分胜负(It’s not over until the fat lady sings)””

156页 第3段 第2行
原文: 即使某个字表达式能够匹配
应改为:即使某个子表达式能够匹配

160页 文字部分的 第5行
原文: 字符串的b之前(也就是当前的位置)匹配
应该为:字符串的c之前(也就是当前的位置)匹配

172页,第2段,“就可以命令正则引擎不必检查它们:「^(?>\w+)」”应修改为“就可以命令正则引擎不必检查它们:「^(?>\w+):」

172页,最后一段,最后一个正则表达式(\.\d\d[1-9]?+)^\d+,应该修改为(\.\d\d[1-9]?+)\d+

173页,第一段,第2个正则表达式(?>M)+应修改为(?>M+)。

第5章

188页 文字部分的 第4段 第2行 结尾
原文: 也不比担心
应该为:也不必担心

189页,倒数第9行,"我们也把能把匹配三位数的多选分支放在最前面,这样…"应修改为"我们就把能把匹配三位数的多选分支放在最前面,这样…"

191页,代码段中的注释大小写错误,“#利用正则表达式检测wholePath”应修改为“#利用正则表达式检测WholePath”

193页,第6行,"算式中的括号。我们…想到┌\bfoo\([^])*\)┘,但这行不通。"应修改为"算式中的括号。我们…想到┌\bfoo\([^)]*\)┘,但这行不通。"

194页,第5行,”┌\[^()]*(\([^()]*\)[^()]*)*\)┘“应修改为"┌\([^()]*(\([^()]*\)[^()]*)*\)┘"

195页,倒数第1段,“虽然这个表达式比最开始的好得多”应修改为“虽然这个表达式比开头那个好一些”

197页,第3段,“所以,我们得用别的办法来解决”应修改为“所以,得想点别的办法”

197页,第4段,“仔细想想我们想要匹配的位于开始分隔符和结束分隔符之间的文本”应修改为“仔细想想要匹配的位于开始分隔符和结束分隔符之间的文本”

197页,倒数第3段,“如果回溯会导致不期望,与多选结构有关的匹配结果”应修改为“如果回溯会导致不期望的、与多选结构有关的匹配结果”

199页,第5段,“这个正则表达式曾被用作降解忽略优先量词的绝佳例子”应修改为“这个正则表达式曾被用作讲解忽略优先量词的绝佳例子”

199页,倒数第2段,“但它并不正确(其实这三个表达式都不正确)”应修改为“但它并没有错(其实这三个表达式都没有错)”

203页,标题“检查HTTP URL”应修改为“校验HTTP URL”,目录中也应同样修改

205页,代码段注释中,“零个或多个据点分隔的部分”应修改为“零个或多个点号分隔的部分”

210页 最后一行
原文:导致逆序环视(?!44)失败
应改为:导致否定顺序环视(?!44)失败

第6章

225页,第2段,“对于没有转义字符的字符串来说,这样会一次读入整个字符串”应修改为“不包含转义字符的字符串,会被一次读入”

228页,倒数第3行,"从最后保存的状态开始回溯,在“…anise”处开始尝试…"应修改为"从最后保存的状态开始回溯,在“…anese”处开始尝试…"

233页,倒数第1段,“所以,对这个例子来说,多选结构要比字符组快22倍左右”应修改为“所以,对这个例子来说,字符组比多选结构快22倍左右”

234页,第4段,“新增的开销大约花费了5s的时间”应修改为“新增的开销大约是5秒”

234页 正文部分第2行
原文:现在,测试字符串只是上面的长度的1/1000,而测试需要进行1000次。
应该为:现在,测试字符串只是上面的长度的1/1000,而测试需要进行1000000次。

246页,第1段,“也能够减少传动装置真正应用正则表达式的位置”应修改为“也能减少传动装置真正应用正则表达式的次数”

262页,倒数第12行,”举例来说,如果目标…,那么使用的自表达式就是…“应修改为"举例来说,如果目标…,那么使用的正则表达式就是…"

265页,第12行,”┌(\{[^}]*\)| +)*┘(它永远不会终止)。或许…“应修改为"┌(\{[^}]*\}| +)*┘(它永远不会终止)。或许…"

265页,第13行,表格中的"Special"应修改为”special"

265页,第14行,“ ┌\{[^]}*\}}”应修改为" ┌\{[^}]*\}┘"

265页,第15行,“ 我们得到┌\{[^]}*\)┘*”应修改为" 我们得到┌\{[^}]*\)*"

266页,第3行,“┌ *(\{[^]}*\} *)*┘”应修改为“┌ *(\{[^}]*\} *)*┘”

266页,倒数第2行,“每次[^\\"]+的匹配终止…”应修改为“每次┌[^\\"]+┘的匹配终止…”

266页,第3段,“你就能把这几条推广开来”应修改为“就能把这几条推广开来”

266页,倒数第4段,“事实上,如果点号不能匹配换行符”应修改为“事实上,如果点号能够匹配换行符”

269页,第1段,“不会在匹配时陷入徒劳的尝试”应修改为“无法匹配时会陷入徒劳的尝试”

271页,倒数第3行,"现在处理$field…"应修改为“#现在处理$field…”

274页 上数第4行
原文:紧跟在x之后的斜线
改正:[^x]/ 是紧跟在不是x的字母后面的斜线

第7章

293页,倒数第1段,“动态作用域会临时“保护”全局变量”应修改为“动态作用域会把全局变量临时“遮蔽”起来”

296页,倒数第2段,“为方便起见,我们也可以给本地变量赋一个值 local($SomeVar),这等于把undef赋值给$SomeVar”应修改为“为方便起见,我们也可以给 local($SomeVar)赋值,这等于给$SomeVar明确赋值,而不是使用undef”

299页,第3段,“分散在散落的 local、子程序和本地变量引用之间的复杂交互”应修改为“分布在散落的 local、子程序和本地变量引用之间的复杂交互”

300页,正文第1段,“你可以期望$ 的起点是开始尝试位置的文本,但它每次都是从整个字符串的开始位置开始的”应修改为“你可能认为$ 的起点是开始尝试位置的文本,其实它每次都是从整个字符串的开始位置开始的”

301页,倒数第1段,“明确设定的$1 等变量中,闭括号在最后”应修改为“明确设定的$1 等变量中,闭括号在最后的那对括号”

308页,标题“指定目标运算元”应修改为“指定匹配目标运算元”

326页,第2段,“split 没有返回的部分或全部的文本”应修改为“通常情况下split不会返回的部分或全部的文本”

327页,标题“嵌套代码结构”应修改为“内嵌代码结构”

356页,第1段,“不包含捕获型括号的正则表达式可以不必保存拷贝”应修改为“不包含捕获型括号的正则表达式可以不必保存副本”

361页,第4段,“这很适合检查单个的正则表达式的编译方法”应修改为“这很适合检查单个正则表达式的编译过程”

第8章

365页,第5行,“sjava.util.regex一经发布就给人留下了深刻印象”应修改为“java.util.regex一经发布就给人留下了深刻印象”

367页,倒数第7行,"\0octal要求开头为0,后接1到3位十进制数字。"应修改为"\0octal要求开头为0,后接1到3位八进制数字。"

368页,表头,"表8-3:java.util.regex 中Match和Regex的方法"应修改为"表8-3:java.util.regex 中Match和Regex的模式"

368页,表8-3,“Patern.UNIX_LINES”应修改为“Pattern.UNIX_LINES”,"Pattern.CANNON_EQ"应修改为"Pattern.CANON_EQ"

368页,表8-3倒数第3行,“对Unicode字符进行不区分大小写的匹配”应修改为“对非ASCII字符进行不区分大小写的匹配”

369页,倒数第4段,“Unicode block的支持要求使用‘In’前缀”应修改为“Unicode区块的支持必须使用‘In’前缀”

371页,第2段,“就我个人来说,更喜欢简称前两个为“pattern”和“matcher””应修改为“我自己更喜欢把前两者简称为“pattern”和“matcher””

371页,第16行,"IllegalAgumentException"应修改为“IllegalArgumentException”

371页,倒数第4行,"结果是‘matched [1st] from 12 to 15.’。在本章的所有例子…"应修改为“结果是‘matched [1st] from 11 to 14.’。在本章的所有例子…”

373页,第1行,“调用Patern.compile可能抛出两种类型的异常:如果…”应修改为“调用Pattern.compile可能抛出两种类型的异常:如果…”

374页,第13行,"…和hasTran-sparentBounds方法来修改和查询…"应修改为"…和hasTransparentBounds方法来修改和查询…"

374页,倒数第6段,“当前pattern的捕获型括号的数目”应修改为“当前pattern中捕获型括号的数目”

377页 倒数第7行
原文:int end()
次方法返回整个匹配的终点的绝对偏移值
改为:此方法返回整个匹配的终点的绝对偏移值

377页,函数“String gropu(int num)”应修改为“String group(int num)”

377页,倒数第5行,“MatcheResult toMatchResult()”应修改为“MatchResult toMatchResult()”

377页,倒数第2行,“如果前一次匹配不成功,或者…调用 toMatcheResult”应修改为“如果前一次匹配不成功,或者…调用 toMatchResult”

378页,第5行
String regex = "(?x) ^(https?):// ([^/:]+) (?:(\\ d+))?";
应修改为
String regex = "(?x) ^(https?):// ([^/:]+) (?::(\\ d+))?";

378页倒数第3行,“string.replaceAll(regex,replacement)”应修改为“String.replaceAll(regex,replacement)”

388页,最后一段,“即检索范围不等于整个目标字符串”应修改为“即使检索范围不等于整个目标字符串也是如此”

389页,标题“构建扫描程序”应修改为“构建扫描程序的方法”

401页,倒数第6行,"不断创建-回收 Mather"应修改为“不断创建-回收 Matcher”

第9章

410页,第5段,“即用即编译(On-The-Fly Compilation)”应修改为“直接编译(On-The-Fly Compilation)”

410页,倒数第2段,“来进行“即用即编译”的编译”应修改为“来进行“直接编译”的编译”

432页,标题“支持函数”应修改为“辅助函数”

432页,倒数第4段,“还有一些静态的支持函数”应修改为“还有一些静态辅助函数”

438页,倒数第3段,“尽管涌出并不大”应修改为“尽管用处并不大”

第10章

442页,第3段,“十进制转义只能接受两到三位八位数值”应修改为“八进制转义只能接受两到三位八进制数字”

445页,倒数第5段,“第448也给出了一个“不好”的原因”应修改为“第448页给出了一个“不好”的理由”

455页,第3段,“这就是默认的配列方式”应修改为“这就是默认的排列方式”

459页,第3段,“这个简单的例子把HTML中的bold tag全部转换为大写”应修改为“这个简单的例子在HTML中的所有大写单词两端加上bold tag”

459页,倒数第4段,“下面这个扩展的例子把bold tag里的单词变为小写”应修改为“下面这个扩展的例子在添加bold tag的同时把单词转换为小写”

460页的表格,第三行开头应该为“数组 数组”,第四行开头应该为“字符串 数组”

2007年08月31日

《精通正则表达式(第3版)》(即Mastering Regular Expression,3rd Edition)是一本好书。
我还记得,自己刚开始工作时,就遇到了关于正则表达式的问题(从此被逼上梁山):若从文本中抽取E-mail地址,还可以用字符串来查找(先定位到@,然 后向两端查找),若要抽取URL,简单的文本查找就无能为力了。正当我一筹莫展之时,项目经理说:“可以用正则表达式,去网上找找资料吧。”抱着这根救命 稻草,我搜索了之前只是听说过名字的正则表达式的资料,并打印了java.util.regex(开发用的Java)的文档来看。摸索了半天,我的感觉就 是,这玩意儿,真神奇,真复杂,真好用。
此后,用到正则表达式的地方越来越多,我也越来越感觉到它的重要,然而使用起来却总感觉捉襟见肘。当时是夏天,北京非常热,我决定下班之后不再着急赶车回家,而是在公司安心看看技术文档,于是邂逅了这本Mastering Regular Expression。该书原文是相当通畅易懂的,看完全书大概花了我一周的业余时间,之后便如拨云见日,感觉别有洞天——原来正则表达式可以这样用,真是奇妙,真是令人拍案叫绝。
此后我运用正则表达式便不用再看什么资料了,充其量就是查查语言的具体文档,表达式的基本模型和思路,完全是在阅读本书时确立的。也正是因为细心阅读过本书,所以有时我能以正则表达式解决某些复杂的问题。我的朋友郝培强(Tinyfool, 昵称Tiny)曾问过我这样一个正则表达式的问题:在Apache服务器的Rewrite规则中,要求以一个正则表达式匹配“除两个特定子域名之外的所有 其他子域名”,其他人的办法都无法满足要求:要么只能匹配这两个特定的子域名,要么必须依赖程序分支才能进行判断。其实这个问题,是可以用一个正则表达式 匹配的。事后,Tiny说,看来,会用正则的人很多,但真正懂得正则的人很少。现实情况也确实如此,就我所见,不少同仁对正则表达式的运用,不外乎从网上 找一些现成的表达式,套用在自己的程序中,但对到底该用几个反斜线转义,转义是在字符串级别还是表达式级别进行的,捕获型括号是否必须,表达式的效率如 何,等等问题,往往都是一知半解,甚至毫无概念,在Tiny的问题面前,更是束手无策,一筹莫展。
就我个人来说,我所掌握的正则表达式的知识,绝大多数来自本书。正是依靠这些知识,我几乎能以正则表达式进行自己期望的任何文本处理,所以我相信,能够耐 心读完这本书的读者,一定能深入正则表达式的世界,若再加以练习和思考,就能熟练地依靠它解决各种复杂的问题(其中就包括类似Tiny的问题)了。
去年,通过霍炬(Virushuo)的 介绍,我参加了博文视点的试译活动,很幸运地获得了翻译本书的机会。有机会与大家分享这样一本好书,我深感荣幸。500多页的书,拖拖拉拉,也花了半年多 的时间。虽然之前读过原著,积累了一些运用正则表达式的经验,也翻译过数十万字的资料,但要尽可能准确、贴切地传达原文的阅读感觉,我仍感颇费心力。部分 译文在确认理解原文的基础上,要以符合中文习惯的方式加以表述仍然颇费周折(例如,直译的“正则表达式确实容许出现这种错误”,原文的意思是“这样的错误 超出了正则表达式的能力”,最后修改为“出现这样的错误,不能怪正则表达式”或“这样的问题,错不在正则表达式”)。另有部分词语,虽可译为中文,但为保 证阅读的流畅,没有翻译(例如,“它包含特殊和一般两个部分,特殊部分之所以是特殊的,原因在于……”,此处special和normal是专指,故翻译 为“它包含special和normal两个部分,special部分之所以得名,原因在于……”),这样的处理,相信不会影响读者的理解。
在本书翻译结束之际,我首先要感谢霍炬,他的引荐让我获得了翻译这本书的机会;还要感谢博文视点的周筠老师,她谨慎严格的工作态度,时刻提醒我不能马虎对待这本经典之作;还有本书的责编晓菲,她为本书的编辑和校对做了大量细致而深入的工作。
另外我还要感谢东北师范大学文学院的王确老师,在我求学期间,王老师给予我诸多指点,离校时间愈长,愈是怀念和庆幸那段经历,可以说,没有与他的相识,便没有我的今天。
本书是讲授正则表达式的经典之作,翻译过程中,我虽力求把握原文,语言通畅,但翻译中的错误或许是在所难免的,对此本人愿负全部责任。希望广大读者发现错 误能及时与我和出版社联系以便再版时修正,或是以勘误的形式公布出来以惠及其他读者。如果读者有任何想法或建议,欢迎给我写信,我的邮件地址是: yusheng.regex@gmail.com。


如今正则表达式已经成为几乎所有主流编程语言中的必备元素:Java、Perl、Python、PHP、Ruby……莫不如此,甚至功能稍强大一些的文本 编辑工具,都支持正则表达式。尤其是在Web兴起之后,开发任务中的一大部分甚至全部,都是对字符串的处理。相比简单的字符串比较、查找、替换,正则表达 式提供了强大得多的处理能力(最重要的是,它能够处理“符合某种抽象模式”的字符串,而不是固化的、具体的字符串)。熟练运用它们,能够节省大量的开发时 间,甚至解决一些之前看来是mission impossible的问题。
本书是讲解正则表达式的经典之作。其他介绍正则表达式的资料,往往局限于具体的语法和函数的讲解,于语法细节处着墨太多,忽略了正则表达式本身。这样,读 者虽然对关于正则表达式的具体规定有所了解,但终究是只见树木不见森林,遇上复杂的情况,往往束手无策,举步维艰。而本书自第1版开始便着力于教会读者 “以正则表达式来思考(think regular expression)”,向读者讲授正则表达式的精髓(正则表达式的各种流派、匹配原理、优化原则,等等),而不拘泥于具体的规定和形式。了解这些精 髓,再辅以具体操作的文档,读者便可做到“胸中有丘壑,下笔如有神”;即便问题无法以正则表达式来解决,读者也能很快作出判断,而不必盲目尝试,徒费工 夫。
不了解正则表达式的读者,可循序渐进,依次阅读各章,即便之前完全未接触过正则表达式,读过前两章,也能在心中描绘出概略的图谱。第3、4、5、6章是本 书的重点,也是核心价值所在,它们分别介绍了正则表达式的特性和流派、匹配原理、实用诀窍以及调校措施。这样的知识与具体语言无关,适用于几乎所有的语言 和工具(当然,如果使用DFA引擎,第6章的价值要打些折扣),所谓“大象无形”,便是如此。读者如能仔细研读,悉心揣摩,之后解决各种问题时,必定获益 匪浅。第7、8、9、10章分别讲解了Perl、Java、.NET、PHP中正则表达式的用法,看来类似参考手册,其实是对前面4章知识的包装,将抽象 的知识辅以具体的语言规定,以具体的形式表现出来。所以,心急的读者,在阅读这些章节之前,最好先通读第3、4、5、6章,以便更好地理解其中的逻辑和思 路。
相信仔细阅读完本书的读者,定会有登堂入室的感觉。不但能见识到正则表达式各种令人眼花缭乱的特性,更能够深入了解表达式、匹配、引擎背后的原理,从而写出复杂、神奇而又高效的正则表达式,快速地解决工作中的各种问题。

余晟
2007年6月于北京

2007年08月21日

朋友问这样的问题:
写了一个函数,从网页源代码中抽取一个结点的文本,函数本身没有问题,但同事在code review时指出,如果提取不成功,应该返回None,而不是空字符串""。

提问的朋友跟我说:你之前说过,从数据库里取出一批数据,如果不存在,要返回一个空的Collection,而不是None(这样才能正确表示“需要的数据不存在”),那么这里,是否应该听取同事的意见呢?
当时我的回答是,因为从这个函数接受数据的代码期望获得的是一个字符串,所以,如果提取不成功,应该返回""。
但是,这样,就无法区分下面两种情况了:存在这个结点,但是一个空结点;不存在这个结点。

这几天晚上一直在思考这个问题,并与Patrick讨论,最后的结论是:
之所以会出现这个问题,原因在于函数的定义是不明确的——虽然函数名看来很清楚,但它的意义是不确定的。
按照契约式设计(Design by Contract)的原则,每个模块都应该有前驱条件(pre-condition)和后继条件(post-condition)。正是因为两位开发人员对这一点的认识存在分歧,问题才会出现:

  • 如果这个函数的前驱条件是,必须提供一段包含此结点的源代码(也就是说,这个结点必须存在),就不能返回None,返回"",就表示这个结点内容为空,如果提供的源代码不包含这个结点,应抛出异常;
  • 如果前驱条件是,提供的源代码不必包含这个结点,那么,这个函数其实就履行了两项职责——检查是否存在结点,以及提取结点的文本,必须与调用方有一个约定,来表示这两重含义(此时可以用None表示“不存在结点”的情况,用""表示结点内容为空的情况,当然我不推荐这么做)。

同样,按照契约式设计的原则,数据验证的职责在调用方,也就是说,约定了这个函数的前驱和后继条件之后,调用方在调用之前,必须保证前驱条件:如果函数要求源代码必须包含这个结点,那么验证是否存在的职责就应该由调用方来承担。