An image to describe post 想当年,我这样用正则表达式分析《红楼梦》和《儒林外史》

极客时间推出了正则表达式的学习课程,有不少朋友问我“为什么不是你来讲?” 这个问题其实没有必要。知识是全人类的财富,从来就不属于哪个个人,问道固然有先后,但先闻道者并不必然获得垄断特权。

虽然我翻译过和写作过正则表达式相关的书籍,但那都是陈年老黄历了,如今来看并不稀奇,而且现在显然有更多更丰富的学习手段,书籍未必是唯一。《正则表达式入门课》的作者涂伟忠长期从事后端开发,在正则表达式的运用上积累了许多经验,更愿意悉心把它打磨为课程,用尽可能通俗易懂的方式,系统化地梳理和讲解了正则的知识点,实在是造福大家的好事,值得恭喜,值得推荐。

An image to describe post 想当年,我这样用正则表达式分析《红楼梦》和《儒林外史》

👆扫我的二维码,免费试读

结算用口令「weizhong8」,到手仅 ¥50

如果你是新人 只要 ¥9.9

当然我也知道,许多人认为正则表达式很难懂,所以无论书写得再耐心,课程做得再细致,他们仍然对正则表达式敬而远之。每次看到这样的例子,我都未免觉得可惜。要知道,正则这玩意儿,入门确实不那么容易,然而一旦入门了,就能感觉到威力无穷。《正则指引》繁体版的副标题更是用到了“横刃万解”这样的形容,在我看来,这一点不夸张。

An image to describe post 想当年,我这样用正则表达式分析《红楼梦》和《儒林外史》

今天我想举个例子,是许多许多年前,我如何用正则表达式帮朋友分析白话小说。

这位朋友的任务虽然名曰“分析”,其实和技术一点关系没有,纯粹是语言学的研究。简单说,是要把《三国演义》、《红楼梦》、《水浒传》、《红楼梦》、《儒林外史》里的“连字句”找出来。什么是“连字句”呢?具体来说,它有好几种子类别:

“真连我们吃饭的地方也没有了”,这是“连…也…”;“真连个畜生也不如了”,这是“连…也不…”;“连你自己屋里的事还不知道”,这是“连…还不…”;“越发连个体统都没了”,这是“连…都…”。

所以这个任务真正要做的,是辨析这些连字句的结构、类型、数量,尝试找出一些规律。但是为了完成它,首先得把五本小说里的所有的连字句都找出来。这任务,别说文科生无从下手,一般理科生见了也得想破脑袋。

恰好我之前钻研过正则表达式,当时又恰好有空,所以尽管这个例子并不在任何一份正则表达式的相关资料里,甚至都没有可以拿来修改的例子,但我稍微动了动脑筋,很快就用正则表达式解决了问题。如今虽然许多年过去了,我仍然记得大致的思路。

每本小说对应一个TXT文件,那么先把这个TXT文件转成Unicode格式存储。要记得,凡是涉及到多字节字符(或者非ASCII字符)的处理,Unicode格式是必然选择,它可以省去很多不必要的麻烦,而且能用到正则表达式的高级功能,非常方便。

然后要做的重要一步是清洗原始文本。这些小说虽然有全本的TXT下载,但并没有官方版本,所以大多都有格式错误。如果格式有问题,之后的查找和提取一定会有问题,很可能正则表达式要写得复杂无比。见如此,还不如索性先把原始数据洗干净,磨刀不误砍柴工嘛。

于是我们要做的有:

\p{Separator}

用这个正则表达式去掉所有的空白字符——白话小说之前连标点都没有,更不会有空白字符。为什么不能用字符串查找替换,把空格都干掉呢?因为“空白”的情况很复杂,有许多字符看起来都是空白,但它们未必是空格。中文的情况就更复杂,很可能出现中文空格(全角空格,以前就有人用它来严格保证“每段开头缩进2个字”)。

不过情况再复杂也不要紧,支持Unicode特性的正则表达式可以通吃。

(?<=\p{Han})\n(?=\p{Han})

用这个正则表达式删除所有句子内部的换行符。我们需要处理的是句子,句子是以标点符号为结束的,而不是以换行符为结束的,而正则表达式的许多特性都会区分换行符,引起不必要的麻烦。所以删除句子内部的换行符非常重要。

这种换行符也很好识别:如果用前后环视发现都是中文字符而不是标点符号,就必须删掉。

(?<=\p{han})(?<!\p{Punctuation})\n{2,}

用这个正则表达式给所有“不是以标点符号结尾的句子”加上句号。有一些句子是以“换行符+空行”为结束的,这样浏览起来没问题,程序处理起来却很麻烦。用正则表达式找到“之前是汉字但不是标点”的多个连续换行符,替换为中文句号,程序处理起来就简单多了。

到现在为止,我们得到了基本规范的文本:去掉了所有空白字符,没有多余的换行符,每个句子都有标点符号结尾。实际上正则表达式确实应该这样用:与其用一个复杂的正则表达式去处理杂乱的文本,不如先用正则表达式把杂乱文本给洗干净了,然后可以大大降低处理难度。

文本洗干净之后,剩下的事情也就很简单了:用正则表达式找到这样的句子,其中包含“连”字,并且在“连”字之后会出现一段不包含标点符号的文字,再往后出现的是“也”、“也不”、“还不”、“都”之类的字,然后再是一段不包含标点符号的文字,直到句子结束。正则表达式大概长这样:

\P{Punctuation}*连\P{Punctuation}+(也[^不]|也不|还不|都[^不]|都不)\P{Punctuation}+\p{Punctuation}

请注意其中的(也[^不]|也不|还不|都[^不]|都不),用它可以很方便地区分“连…也…”和“连…也不…”的情况,这是正则表达式的独门秘籍,普通的字符串处理是很难做到的。

在我印象里,这整个方案,设计大概花了半小时,写代码和调试花了大概两小时(毕竟意外情况很多,年代久远上面并没有列全)。总之大概就花了一个下午的时间,之后敲下回车键,几百万字的文本分析瞬间结束,所有的连字句分类逐条显示出来。此时,其他人还在进行激烈的心理斗争:这五本小说几百万字,全都看一遍都要花很长时间,把所有连字句都挑出来更是眼睛都要花掉,还不能保证没有漏掉的……

我想说的是,上面这几个正则表达式虽然看起来和常见的不一样,其实丝毫不复杂。如果你肯多花几个小时去学习,肯定能一眼看穿其中的奥妙。而一旦你洞穿了其中的奥妙,那么别说查找连字句,比这复杂得多的任务都不在话下。这样的收益/投入比例,在我看来是绝对值得的。

An image to describe post 想当年,我这样用正则表达式分析《红楼梦》和《儒林外史》

👆扫我的二维码,免费试读

结算用口令「weizhong8」,到手仅 ¥50

如果你是新人 只要 ¥9.9


导读 | 余晟:我是怎么学习和使用正则的?

你好,我是余晟。受伟忠的邀请,今天我来和你聊聊我是怎么学习和使用正则的?

刚工作那会儿,因为密集用到正则表达式,所以我花了不少时间去钻研正则相关的问题,因此获得了机会,翻译了《精通正则表达式》(第三版),后来⼜写了一本书《正则指引》。到如今,许多年过去了,这些东西还历历在目,我也很乐意拿出来和你分享一下,希望在学习正则的道路上,能给你一些启发。

我经常在⽹上看到,许多⼈抱怨正则表达式“难学”,我知道,它确实不好学。但同时我也仔细看过⼤家的抱怨,发现和我之前的做法⼀样:⽤到什么功能,就去⽹上搜⼀个例⼦来改改,能跑通就满意。⾄于这例⼦到底如何构成的,⾃⼰是不是都懂了,其实⼼⾥没底,能⼤概看懂五六分,就已经很满⾜了。

这样浮光掠影的使⽤⽅法或许能解决眼前的问题,但⼀定不算“学会”。它有点像打井,每次挖到⼀点⽔就满⾜了,根本不管有没有持续性,也不关⼼挖没挖到含⽔层。结果就是,每次要喝⽔的时候,你都得重新打⼀眼井。

那么对于正则表达式,我们有没有可能“打出⼀⼝永不⼲涸的深井”呢?当然有,那就是 ⼀次性多投⼊点时间, 由表及里,由术及道。一旦掌握了方法,之后就 简单很多了

按照我的经验,如果每天花一刻钟或者半小时,坚持个把礼拜,通常都能登堂入室,达到“不会忘”的境界。不要以为这时间很多,我知道有些人很喜欢找“正则表达式五分钟入门”,其实每次都没有入门,日积月累,反而浪费了几十甚至上百个五分钟。

那多投⼊时间很好理解,但是什么叫掌握⽅法呢?⽤我的话说,就是摆脱了字符的限制,深⼊到概念思维的层⾯。不要盯着那些⻤画桃符⼀般的字符和表示法皱眉,⽽要摆脱桃符,把真正的“⻤”给认出来——虽然它们不那么容易看⻅。也正因为这样,我们才需要⼀次性多投⼊点时间。

那最终怎样才算“入门”了呢?按照我的经验,就是通过学习掌握方法,后来无论用正则表达式解决什么问题,都能自发遵循下⾯的流程去走,甚至能达到不需要这个流程,也能做到解决问题,那基本上就算入门了。

第⼀步,做分解。 拿到一个问题后,我们要先思考:这个问题可以分为⼏个⼦问题?每个⼦问题是否独⽴?我们拿最常⻅的电⼦邮件地址匹配来说。从文本结构来看,它可以分为“username + @ + domain name”这三个独⽴的部分。怎么画呢?我们可以先画出逻辑结构图。通过这个过程来厘清思路。当然,这是软件⼯程最基本的思路,相信你做起来应该问题不大。

第⼆步,分析各个⼦问题。 某个位置上可能有多个字符?那就⽤字符组。某个位置上可能有多个字符串?那就⽤多选结构。出现的次数不确定?那就⽤量词。对出现的位置有要求?那就⽤锚点锁定位置…… 某种程度上,这就像武术⾥的⻅招拆招,每个问题都有对应的解法,只要熟练掌握了,知道什么时候用字符组,什么时候用多选结构,什么时候用量词,什么时候用锚点,就很容易搭建起完整的概念模型。

第三步,套 你大概注意到了,到现在,我们还没有谈论正则表达式的典型标志,比如方括号、星号、花括号。要知道,这些典型标志无非只是一些符号而已,真正重要的是字符组、多选结构、量词等等这些概念。一旦你的概念模型清楚了,写出正则表达式就非常简单了,无非是查阅语法⼿册,把之前得到的概念模型按照对应语⾔或⼯具的约定写下来而已。许多人觉得正则表达式难懂,总是纠缠于“这里为什么要多一个星号?那里为什么是方括号而不是花括号?”,原因恰恰在于对概念模型不清楚。虽然各种语⾔或⼯具对正则表达式的⽀持⼤同⼩异,但细微差别仍然不可忽视。不过只要你⼼怀正念,洞若观⽕,这些差异其实并不是⼤问题。

第四步,调试。 很多人都说,正则表达式的麻烦之处在于它像个⿊箱⼦,很难调适,迄今为⽌仍然没有特别好⽤的⼯具,所以我们没法⼀步步跟进去看匹配的具体过程,只能笼统地知道“匹配了”或者“没匹配”。那到底怎么调试呢?我的经验是,复杂⼀点的正则表达式不能⼀次写对,这是很正常的。与其纠结“这个正则表达式看起来这么复杂,此处到底要用星号*还是加号+”,不如先搞清楚,星号(*)或加号(+)限定的到底是正则表达式中的哪一部分,对应要匹配文本中的哪一部分。这两个问题搞清楚了,整个问题就迎刃而解了。

另外,还有⼀点统摄全局的经验想和你说一下,那就是 学会了正则表达式之后,务必要保持克制。 写正则表达式很容易上瘾,毕竟它的功能那么强⼤,处理速度那么快,⼜像天书符咒那样充满了“神秘”色彩。于是,“写⼀条其他⼈看不懂的正则表达式,⼀次性解决所有问题”,就成了某些程序员的执念。但是,从软件⼯程的⻆度来看,这种办法绝对是噩梦,不但其他⼈⽆法理解,⾃⼰过⼀段时间也会挠头。

那到底该怎么“克制”呢?我的经验有以下三点。

第⼀,能⽤普通字符串处理的,坚决⽤普通字符串处理 字符串处理的速度不⻅得差,可读性却好上很多。如果要在大段文本中定位所有的today或者tomorrow,用最简单的字符串查找,直接找两遍,明显比to(day|morrow)看起来更清楚。

第⼆,能写注释的正则表达式,⼀定要写注释 正则表达式的语法非常古老,不够直观,为了便于阅读和维护,如今⼤部分语⾔⾥都可以通过x打开注释模式。有了注释,复杂正则表达式的结构也能⼀⽬了然。

第三,能⽤多个简单正则表达式解决的,⼀定不 苛求⽤⼀个复杂的正则表达式 这里最明显的例子就是输⼊条件的验证。比如说,常见的密码要求“必须包含数字、小写字母、大写字母、特殊符号中的至少两种,且长度在8到16之间”。你当然可以绞尽脑汁用一个正则表达式来验证,但如果放下执念,⽤多个正则表达式分别验证“包含数字”“包含小写字母”“包含大写字母”“包含特殊符号”这四个条件,要求验证成功结果数大于等于2,再配合一个正则表达式验证长度,这样做也是可行的。虽然看起来繁琐,但可维护性绝对远远强于单个正则表达式。

小结

好了,到此为⽌,我的经验介绍完了,可以交棒了。

这些年,很多人问过我,我当时到底是怎么学会正则的?说实话,我那会儿根本没想什么,纯粹出于“干一行爱一行”的朴素想法。要用得多,就找书来,哪怕是囫囵吞枣,也要一鼓作气看完。 我一直觉得,真正值得学的东西,没有什么“平滑学习曲线”。在前面的阶段,你总得狠下心来,过了一个又一个坎儿,然后才能有一马平川。

我觉得,正则表达式属于“没有维护成本”的技能。⼀旦学会了,每⼀次遇到这类问题都可以“零成本出击”。所以,⻓期来看,这绝对是一笔“⽆本万利”的⽣意。希望你能通过这个专栏早日达到一马平川!

An image to describe post 想当年,我这样用正则表达式分析《红楼梦》和《儒林外史》

👆扫我的二维码,免费试读👆

最后强调一下,这个可课用口令「weizhong9」仅需 ¥50

如果你是新人 只要 ¥9.9