语言决定思维。
引子
在 “给产品同学解决一个小问题” 一文中,通过 正则表达式和 sed 命令来抽取 total 值。
正则表达式是什么 ? 就像 1+1=2 可以表示 1 个苹果加 1 个苹果得到 2 个苹果一样,[0-9]{11} 可以表示 11 位数字。正则表达式是一种用于描述文本模式的形式语言,可以从文件里抽取所需的文本和信息。
本文探索用正则表达式来抽取文本的方法。主要使用 Shell 命令演示,但几乎所有的编程语言都支持正则表达式。为了不至于让读者陷入正则语言的琐碎讲解中,将直接进入示例演示,而基础知识放在文末。阅读的时候,可以先扫一遍基础知识,亦可在需要的时候去参考。
目标: 使用正则表达式 + Shell 命令快速抽取所需要的任意文本。
方法与工具
要使用正则表达式进行文本处理,需要了解几个必要方法和工具:
- 按行处理模式。指每次从指定文件中读取一行文本进行处理,处理完成后读取下一行继续处理,直到将所有行处理完成。
- 分解与组合。 匹配一个较长的文本时,会将该文本分割成若干小段,分别匹配每一小段,再组合起来。比如要匹配邮箱 [email protected] ,可以分解为 shuqin 、 @ 、 163、 com 四段,分别匹配再组合。
- sed & grep & awk 。正则表达式处理的三剑客。grep 通常用来搜索符合条件的行,sed 通常用于文本替换,awk 是一种模式语言,通常是匹配所需模式后做一些处理。
- 管道 | 。 可以将上一个小程序的输出定向到下一个小程序的输入,从而将多个命令连接起来构成更强大的能力。
- Python。 当 Shell 命令功能受限时,可以启用 Python 来搞定。
示例
找到符合条件的行
先给一个文本 dream.txt。 使用 cat dream.txt 可以查看文本内容:
I would like painting 100 stars in the sky.
I would like to resist 1001 rules that oppress people.
I would like to eat 30 boxes of tomatoes.
I want to have a long long sleep.
第一个小任务:找到含有数字的行。怎么办 ?制定策略:
- 知道数字的识别。
\d , [0-9], [:digit:]
均可识别数字,在表达式中可以相互替换。通常用 \d 节省键盘敲击量,使用 [0-9] 或 [:digit:] 兼容性更强。因为部分终端可能不支持 \d 。这里统一使用 [0-9]。 - 找到匹配行。使用 grep "RE_COND" file 可以在文件 file 找到符合 RE_COND 描述条件的文本。
结合起来,就得到了命令:
grep "[0-9]" dream.txt
grep "\d" dream.txt
抽取所需文本
找到了含有数字的行, 第二个小任务:抽取出这些数字。制定策略:
- 使用每行处理模式。只要找到处理一行的方法,就可以了。
- 识别多个数字。可以使用 [0-9]+ 表示一个或多个数字。
- 将数字捕获并输出到结果,将非数字替换为空。使用 sed 命令实现替换: sed 's/非数字(数字序列)非数字/(数字序列的引用符)/g' 。非数字用 [^0-9] 。[^range] 是一种范围排除性匹配,表示匹配出除 range 指定的所有字符之外的其它字符。
使用 sed -E 's/^[^0-9]*([0-9]+)[^0-9]*$/\1/g'
可达到目的。^ 标识文本的起始, $ 标识文本的结束, * 表示任意多个 。^[^0-9]*([0-9]+)[^0-9]*$
的意思是,从文本开始,经过任意个非数字字符,然后遇到数字并捕获,再经过任意个非数字字符,走到文本的结束。\1 标识被捕获的第一个分组,也就是数字部分。
结合起来:
grep "[0-9]" dream.txt | sed -E 's/^[^0-9]*([0-9]+)[^0-9]*$/\1/g'
提示: -E 表示使用扩展的正则表达式,表达力更强一些。不同的终端,选项有所不同。有的是 -r ,有的是 -E 。可以 man sed 来查看选项说明。
找到一行的多个匹配
可以使用 cat dream.txt | tr '\n' ' ' > dream_single.txt
将 dream.txt 中的换行符替换成空格,得到单行文本 dream_single.txt :
I would like painting 100 stars in the sky. I would like to resist 1001 rules that oppress people. I would like to eat 30 boxes of tomatoes. I want to have a long long sleep.
第三个小任务:抽取单行文本中的所有数字。想想怎么做 ?
按照第二个小任务的做法,只要稍作扩展即可。有几个数字就添加个 ([0-9]+)[^0-9]*
。可以说笨拙又高效。
sed -E 's/^[^0-9]*([0-9]+)[^0-9]*([0-9]+)[^0-9]*([0-9]+)[^0-9]*$/\1 \2 \3/g' dream_single.txt
不过,现实常常是,我们并不知道有多少个数字。可能有 100 个, 可能没有。上述方式不够灵活。如何能够自动抽取所有数字呢?
使用 sed -E 's/[^0-9]*//g' dream_single.txt
会得到 100100130 ,空格没了,数字都挤到一块了,不符合期望;使用 sed -E 's/[^0-9]*/ /g' dream_single.txt
会得到 1 0 0 1 0 0 1 3 0,空格太多,原来的数字被分割了,也不符合期望。怎么办呢 ?
再想想抽取的含义:
- 将所需要的信息捕获并使用引用在结果中保留;
- 所需要的多条匹配信息必须分割来;可使用空格分开;
- 将不需要的信息替换为空;但是空格不替换。
这样,可以使用 sed -E 's/[^0-9 ]*//g' dream_single.txt
实现第二三点;由于空格太多也不好,可以使用 sed -E 's/[[:space:]]+/ /g'
将多个空白符合并为一个空格符。两个替换动作可以合并写为:
sed -E 's/[^0-9 ]*//g;s/[[:space:]]+/ /g' dream_single.txt
找到某个匹配
上一个小任务,找到了指定条件的所有匹配;如果要找到某个匹配呢 ?
第四个小任务:匹配 boxes 之前的数字。
要完成这个任务,需要将所需要的信息进行更精确的识别。现在要拿到的是 boxes 之前的数字,可以将 boxes 作为一个识别参照物,在模式中加入所需要捕获的数字与该字符串之间的位置关系: ([0-9]+)[^0-9]*boxes
。[^0-9]* 表示不关心 boxes 与之前的数字之间有什么非数字的东西(可能有可能没有)。
sed -E 's/^.*[^0-9]+([0-9]+)[^0-9]*boxes.*$/\1/g' dream_single.txt
为什么要在 ([0-9]+) 之前加 [^0-9]+ 这个呢 ? 读者可以思考下。由于正则表达式的默认贪婪匹配模式,如果不加这个非数字的限制,.* 会把所需要的数字吃掉,只剩一个 0 。 . 是万能通配符,可以匹配任意字符。
应用
假设有如下单行文本,要析取出其中的所有 total 。
{"result":true,"code":0,"message":null,"data":{"success":true,"code":200,"message":"successful","requestId":null,"errorData":null,"data":{"orderNos":["2020030316177500001"],"total":19}}}{"result":true,"code":0,"message":null,"data":{"success":true,"code":200,"message":"successful","requestId":null,"errorData":null,"data":{"orderNos":["2020030113422700003","2020030113283300009"],"total":8}}}{"result":true,"code":0,"message":null,"data":{"success":true,"code":200,"message":"successful","requestId":null,"errorData":null,"data":{"orderNos":["2020030522331200009"],"total":58}}}
目前能描述的是想要的信息 (total.*[0-9]+)
。
第一种策略是,想办法将单行转换成多行相似格式的文本,然后按行处理。比如:
sed 's/}{/};{/g' result.txt| tr ';' "\n" | sed -E 's/^.*total.*[^0-9]+([0-9]+).*$/\1/g'
其中 sed 's/}{/};{/g' result.txt| tr ';' "\n"
在多个相似的文本之间插入换行符,从而将单行文本转换成多行文本,每次只需要解析一个 total 即可。不过,这只是绕开了问题。
第二种策略,如上一节所述,将“不需要的信息替换成空”。不过,与上面的数字范围可以取反不同,对整个正则表达式取反并不简单。
第三种策略,采用正向思维,捕获所有需要的文本。 由于 sed 没法输出所有捕获的分组,因此,可采用 Python 编写一个较通用的正则析取器。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import re
def extract(text, regex):
pat = re.compile(regex)
matches = pat.findall(text)
# print 'matches: ', matches
if matches:
for m in matches:
print m
if __name__ == '__main__':
args = sys.argv
file = args[1]
regex = args[2]
print 'file: ', file, ' regex: ', regex
text = ''
with open(file, 'r') as f:
text = f.read()
extract(text, regex)
使用 python reg.py result.txt '"total":([0-9]+)'
即可。
基础知识
详细可参阅: “正则表达式基础知识”