使用正则表达式抽取所需文本

语言决定思维。

引子

“给产品同学解决一个小问题” 一文中,通过 正则表达式和 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]+)' 即可。


基础知识

详细可参阅: “正则表达式基础知识”

猜你喜欢

转载自www.cnblogs.com/lovesqcc/p/12443108.html