使用正則表達式抽取所需文本


語言是強大的力量。

引子

“給產品同學解決一個小問題” 一文中,通過 正則表達式和 sed 命令來抽取 total 值。

正則表達式是什么 ? 就像 1+1=2 可以表示 1 個蘋果加 1 個蘋果得到 2 個蘋果一樣,[0-9]{11} 可以表示 11 位數字。正則表達式是一種用於描述文本模式的形式語言,可以從文件里抽取所需的文本和信息。

本文探索用正則表達式來抽取文本的方法。主要使用 Shell 命令演示,但幾乎所有的編程語言都支持正則表達式。為了不至於讓讀者陷入正則語言的瑣碎講解中,將直接進入示例演示,而基礎知識放在文末。閱讀的時候,可以先掃一遍基礎知識,亦可在需要的時候去參考。

目標: 使用正則表達式 + Shell 命令快速抽取所需要的任意文本。


方法與工具

要使用正則表達式進行文本處理,需要了解幾個必要方法和工具:

  • 按行處理模式。指每次從指定文件中讀取一行文本進行處理,處理完成后讀取下一行繼續處理,直到將所有行處理完成。
  • 分解與組合。 匹配一個較長的文本時,會將該文本分割成若干小段,分別匹配每一小段,再組合起來。比如要匹配郵箱 shuqin@163.com ,可以分解為 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 。 . 是萬能通配符,可以匹配任意字符

可以把 boxes, [^0-9]+ 這樣的字符序列稱為”屏障“,因為它們標識出我們要取的文本旁邊有什么。就像打車時說附近有一座支付寶大樓一樣。

應用

通用抽取器

假設有如下單行文本,要析取出其中的所有 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
    regex = args[1]
    file = args[2]
    print 'file: ', file, ' regex: ', regex

    text = ''
    with open(file, 'r') as f:
        text = f.read()

    extract(text, regex)

使用 python reg.py '"total":([0-9]+)' result.txt 即可。

這里將文件參數放在后面是有深意的。 這樣,就可以與 xargs 連用,批量處理文件了。 比如 如下命令,可以批量處理當前目錄下的所有后綴為 .txt 的文件。

ls *.txt | xargs -I {} python reg.py '"total":([0-9]+)' {}

話說回來,正則表達式,通常用來從不規則的文本里抽取信息,對於規則結構的數組、JSON ,使用合適的 JSON 模塊來處理更適宜。


日志解析

下面再給一個從日志中抽取示例。從日志中抽取信息,是正則表達式的一大用武之地。給定一個應用日志文件: app.log , 從中抽取出 state-inconsistency 后面的所有訂單。

———————————————————————————————————————1—————————————————————————————————————————
日志概況: 2020-03-06 00:02:50    bd-prod-trade-xxx-kw5r9/10.232.6.144    warn
日志內容: c76e173b3e2a4E59a0f0c31c222c9323  c.y.t.t.delay_compare-tc_order_compare - state-inconsistency: E20200305235232091500001

———————————————————————————————————————2—————————————————————————————————————————
日志概況: 2020-03-06 00:02:08    bd-prod-trade-xxx-kw5r9/10.232.6.144    warn
日志內容: 24ff9ff23be34d15af44bc7f9c8dfe43   c.y.t.t.delay_compare-tc_order_compare - state-inconsistency: E20200305235121088800001

可以用如下命令來解決。 grep 先拿到不一致的那些行, 然后 cut 用 -d : 指定冒號來分割一行文本,分割出 3 個子串,-f 取到第 3 個字段。 sed 將前面的空格去掉。

grep 'inconsis' app.log | cut -f 3 -d ':'  | sed 's/ //g'

不過,每次記這么長的命令,還得記住不一致的訂單在第幾個冒號后,也挺有負擔的。 使用上述找到所有匹配的方法,可以得到:

grep 'inconsis' app.log | sed -E 's/^.*tency: *(E[0-9]+) *$/\1/g'

不過,即使比較有經驗的人,要寫出 s/xxx/yyy/g 里的 xxx ,也不是那么容易的。 因此,還是用通用解析器:

python reg.py "(E[0-9]+)" app.log

這樣,會解析出 E59 這樣不期望的字符。需要去掉。可以加一個”屏障標識“: \b ,這樣就能得到所需結果。 \b 標識 E 左邊的應是符號而不是單詞。它是一種位置標識。位置標識就好比打車時告訴附近有個什么標志性建築一樣。

python reg.py "\b(E[0-9]+)" app.log

基礎知識

這里給出一些常用的知識點:

  • 普通字符: 除 *, ? , , (, ), [, ], -, . , +, ^, $, {, } , | , 以外的字符匹配它自身, 比如 a 匹配 a

  • 點號 . : 匹配任意不包括換行符的單個字符。比如, sa. 可匹配 sat, sa*, sa[ 等。

  • 字符組[characters] :匹配字符組中指定字符集合中的任意單個字符: 比如 [abc] 將匹配 a 或 b 或 c , ca[ptb] 將匹配 cap, cat 或 cab.

  • 排除性字符組[^characters] : 匹配字符組中指定字符集合之外的任意單個字符: 比如, [^abc] 將匹配除了 a,b,c 之外的任意單個字符。 ca[^ptb],將匹配 caX 的文本,除了 cap, cat, cab, 注意,這里是匹配一個非指定的字符,而不是不匹配。

  • 范圍字符組: [char1-char2] 將匹配從char1 到 char2 之間的任意單個字符(按照ASCII編碼). 比如, [a-z] 匹配任意小寫字符; [A-Z] 匹配任意大寫字符 ; [0-9] 匹配任意數字; [a-zA-Z0-9] 匹配任意大小寫字符或數字

  • 特殊字符: 凡是在正則式中具有特殊含義的字符,要匹配字符本身(將其作為普通文本)都必須使用反斜線 \ 進行轉義;通常需要轉義的字符有: . + * ? { } [ ] ( ) - \ ^ $ | 比如, 匹配 . 的正則式是 . , 匹配 \ 的正則式是 \ , 匹配 ( 的正則式是 ( ;要匹配 (ab) 的正則表達式是 (ab) ; 要匹配 a? 的表達式是 a? ; a? 將匹配空或單個a

  • 匹配空白字符: \s 匹配任意空白符

  • 字符類: <> 等價於; \d <> [0-9] 任意單個數字 ; \w <==> [a-zA-Z0-9_]

  • 順序結構 XY: 匹配 X 后緊跟 Y 的文本,比如 [0-9][a-z] 匹配 數字后跟小寫字母的文本, 7z, 0x 等, 但不匹配 ap, 77

  • 多選分支結構X|Y: 匹配 X 或者 Y ,比如 [0-9]|[a-z] 匹配數字或小寫字母,相當於 [0-9a-z]。

  • 匹配一個或多個 X : X+ ; 例如 s/d+ 匹配 s后跟至少一個數字, s9, s34, s235, ...

  • 匹配零個或多個 X : X* ; 例如 s/d* 匹配 s后跟空或者至少一個數字, s, s9, s34, s235, ...

  • 匹配零個或一個 X : X? ; 例如 https? 匹配 http 或 https

  • 子表達式 (X): 將 X 作為一個子表達式,緊鄰的匹配量詞將作用於 X 整體,而不是 X 中的單個字符。例如 (s\d){3} 匹配 s1s1s1 ,而不匹配 s111

捕獲功能: 使用括號將一個子表達式匹配的文本進行捕獲,后面可在模式或處理中對捕獲的文本進行引用或處理。

((regex1)-(regex2))-(regex3) \n 引用被捕獲的第 n 個文本; n 按左括號出現的順序進行標識 \0 表示匹配的整個文本 ;\1 ref= ((regex1)-(regex2)) ; \2 ref= (regex1) ; \3 ref= (regex2) ; \4 ref= (regex3) 在替換文本時可以使用 $1, $2, $3, $4 分別引用 \1, \2, \3, \4 捕獲的文本。 比如, (\d{3}) mygod(\1) 必須匹配 三位數字mygod三位數字 的模式, 並且,后面三位數字必須與前面三位數字完全相同。


小結

正則表達式是一種用於文本模式匹配和文本處理的靈活有效的語言,也是一種很有趣的小型領域語言。學習正則表達式會是一個有趣的經歷。

關聯文章:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM