如何用 Python 和正則表達式抽取文本結構化資訊?
- 2019 年 10 月 8 日
- 筆記
痛點
很多人的日常工作,都是要和大量的文本打交道的。
例如學者需要閱讀大量的文獻材料,從中找到靈感、數據與論據。
學生需要閱讀很多教科書和論文,然後寫自己的報告或者做幻燈。
財經分析師,需要從大量的新聞報道中,找到行業的發展趨勢和目標企業動態的蛛絲馬跡。
不是所有的文本處理,都那麼新鮮而有趣。
有一項重要但繁瑣的工作,就是從大量的文本當中抽取結構化的資訊。
許多數據分析的場景,都要求輸入結構化的資訊。
例如在咱們之前介紹過的《貸還是不貸:如何用 Python 和機器學習幫你決策?》和《如何用 Python 和深度神經網路鎖定即將流失的客戶?》中,你都看到了,機器模型更喜歡被結構化的表格資訊來餵養。

然而,結構化的資訊,不一定就在那裡,靜候你來使用。很多時候,它蘊藏在以往生成的非結構化文本中。

你可能早已習慣,人工閱讀文本資訊,把關鍵點摘取出來,然後把它們拷貝粘貼放到一個表格中。從原理上講,這樣做無可厚非。但是實際操作裡面,效率太低,而且太麻煩。
大部分人,是不願意從事這種簡單重複的枯燥工作的。
一遍遍機械重複滑鼠劃定文本範圍,「Ctrl+C」、切換到表格文檔、找准輸入位置,再 「Ctrl+V」……
這種事兒做得太多,對你的肩肘關節,甚至是身心健康,都有可能造成不利影響。
想不想嘗試用一種更簡單的自動化方式,替你快速完成這些煩人的操作步驟呢?
讀過本文後,希望你能找到答案。
樣例
這裡,我們舉一個極端簡化的中文文本抽取資訊例子。
之所以這樣做,是為了避免你在解讀數據上花費太多時間。
我更希望,你能夠聚焦於方法,從而掌握新知。
假設一個高中班主任,高考後讓班長統計一下學生們的畢業去向。班長很認真地進行了調查,然後做了如下彙報:
張華考上了北京大學 李萍進了中等技術學校 韓梅梅進了百貨公司 ……
為了讓你對樣例足夠熟悉,甚至有共鳴,這裡我從 1998 年版的新華字典中,「借鑒」 了部分內容。
夠貼心吧?
現實生活中,一個班大概不會只有 3 個人,因此你可以想像這是一個長長的句子列表。
但其實班主任有個隱含的意思沒有表達出來,即:
我想要一張表格!
所以,看到這一長串的句子,你可以想像他的表情。
班長估計也很難堪:
想要表格你早說啊!
這時候,假設你是班長,怎麼辦?
資訊都在文本裡面。但如果需要轉換成表格,就得一個個資訊點去尋找和處理。
其實,對於四五十人的班級來說,手動操作也不是什麼太難的事情。
但是設想一下,如果你需要處理的數據量,是這個例子的十倍、百倍甚至千萬倍呢?
繼續堅持手動處理?
這不僅麻煩,而且不現實。
我們需要找到一種簡單的方法,幫助我們自動抽取相應的資訊。
此處我們使用的方法,是正則表達式。
正則
「正則表達式」 這個名字,初聽起來好像很玄妙。實際上,它是從英文 「regular expression」 翻譯過來的。
如果譯成白話,那就是 「有規律的表述形式」。
這,聽起來,是不是就更加接地氣了?
但是,給你補一下 「假行家 101」 課程:
說別人聽得懂的話,你能唬得住誰?
約定俗成,咱們繼續沿用 「正則表達式」,來稱呼它好了。
從創生之日起,它就給文本處理帶來了高效率。
但是,用它的主要人群,卻不是時常跟文字打交道的作家、編輯、學者、文員,而是……
程式設計師!
程式設計師寫的程式碼,是文本;程式設計師處理的數據,很多也是文本格式。其中便有很多顯著的規律可循。
正是靠著正則表達式這種獨門秘籍,許多別人做起來,需要昏天黑地一整周的任務,程式設計師可以半小時搞定,然後喝著咖啡等下班。
即便到了泛人工智慧的今天,正則表達式依然有許多令你意想不到的應用。
例如人機對話系統。
你可能看了新聞報道,總以為人機對話都是靠著知識圖譜或者深度學習搞出來的。
不能說這裡面,沒有上述炫酷技術的參與。但它們充其量,只佔其中一部分,或許還只是一小部分。
生產實踐裡面,大量的對話規則後面,並不是讓你倍感神奇深奧的神經網路,而是一堆正則表達式。
你可能會擔心,這樣高端的應用技術,自己能掌握嗎?
答案是:
當然!
正則表達式,並不難學。
尤其是當你把它和 Python 結合到一起,那簡直就是效率神器了。
我們這就來看看,正則表達式怎麼幫我們識別出樣例文本裡面 「人名」 和 「去向」 資訊。
試練
請你開啟一個瀏覽器,鍵入這個網址(https://regex101.com/)。
你會看見如下介面。

它可是一個正則表達式實驗的利器。我教 INFO 5731 課程時,學生們就是在掌握了這個工具以後,迅速玩兒轉了正則表達式。
這麼好的工具,一定要價不菲吧?
不,它是免費的。你放心大膽使用就好了。
我們首先把左側的程式語言,從默認的 PHP ,調整為 Python。
之後,把需要進行處理的文本,貼到中間空白的大文本框裡面。

下面我們來嘗試進行「匹配」。
什麼叫做匹配呢?
就是你寫一個表達式,電腦便拿著雞毛當令箭,在每一行文本上,都認認真真地找有沒有符合該表達式的文本段落。
如有,則會高亮顯示出來。
這裡我們觀察一下,發現每個句子裡面,人員去向前面,都有一個 「了」 字。
好,我們就在中部上方小文本框里,把 「了」 字輸入進去。

可以看到,三句話裡面的「了」,全都亮了。
這就是你接觸到的第一種匹配方式 —— 按照字元原本的意思來查找一致的內容。
因為樣例文本的規律性,我們可以把 「了」 當成一個定位符,它後面,到句子結束位置,是 「去向」 資訊。
咱們需要找的一半結構化資訊,不就是這個 「去向」 嗎?
我們嘗試匹配 「去向」。
怎麼匹配呢?這次每一行的字兒都不一樣啊?
沒關係,正則表達式強大之處,此時就顯示出來了。
你可以用一個點號,也就是.
,表示任意字元。
字母、數字、標點…… 甚至是中文,也能涵蓋在內。
然後我們繼續想想看,去向資訊這裡,會有幾個字呢?
不好說。
例子裡面這簡單的三句話,就有 「4 個字」 或者 「6 個字」 兩種情況。
所以,我們無法指定去向資訊裡面字元的長度。
但這也沒關係,我們只需要用一個星號(*
),就可以代表出現次數,從 0 到無窮大都可以匹配。
當然,實際情況中,是不會真出現無窮大的。
我們在剛才輸入的基礎上,加上.*
,結果就成了這個樣子:

不錯嘛!
不過似乎去向資訊和 「了」 字兒都是一樣顏色的高亮。那不就混到了一起嗎?
我們可不想這樣。
怎麼辦?
請你在.*
的兩側,嘗試加入一對小括弧(注意,不要用中文全形符號)試試看。

你會發現,這次 「了」 依然用藍色表示,而後面的去向資訊,已經變成了綠色。
這一對小括弧,很重要,它叫做 「分組」,是提取資訊的基本單位。
我們的任務已經解決了一半了,是吧?
下面我們來嘗試把人名一併抽取出來。
我們來找人名的錨定位置。
細細觀察,你不難發現,每個人名的後面,都有個動詞跟著。
升學的同學,用的是 「考」 字,而就業的同學,用的是 「進」 字。
我們先嘗試一下 「考」 字。
這裡我們嘗試直接把 「考」 字放在 「了」 字以前。但是你會發現,什麼匹配結果也沒有。

為什麼?
回看數據,你會發現,人家用的原詞是 「考上了」。
當然這裡我們可以輸入 「上」 字。不過你要考慮一下更為通用的情況。
好比說,「考取了」 怎麼辦?「考入了」 呢?
更好的方式,是繼續使用我們剛才學會的「大招」,在「考」和「了」之間,插入一個.*
。
這時候,你的正則表達式的樣子是 考.*了(.*)

看,第一行的資訊成功匹配了吧?
但是,那後面還有兩行沒有匹配,怎麼辦?
我們依樣畫葫蘆,就會發現,使用進.*了(.*)
就能正確匹配後兩行。

問題來了:
匹配第一行的,匹配不了後兩行,反之亦然。
這不好。我們希望寫的表達式,能夠更通用。
怎麼辦?
我們看看正則表達式當中 「或」 關係的表示。
這裡,我們可以把兩個字元用豎線隔開,旁邊用中括弧括起來,代表兩者任一出現,都算匹配成功。
也就是,把正則表達式,寫成這樣:[考|進].*了(.*)

太棒了,三行的內容都已經匹配成功。
這裡,動詞片語,和代表時態的 「了」 作為中間錨定資訊,我們可以放心大膽,把之前的人名資訊,提取出來了。
也就是這樣寫:(.*)[考|進].*了(.*)

注意此時,人名分組是綠色,去向分組是紅色的。
我們成功提取了兩組資訊!慶祝一下!
可是,如果你給班主任看這裡的結果,估計他不會滿意。
表格,我要表格!
別著急,該 Python 出場了。
下面我們嘗試在 Python 把數據正式提取出來。
環境
本文的配套源程式碼,我放在了 Github 上。
你可以在我的公眾號 「玉樹芝蘭」(nkwangshuyi)後台回復 「regex」,查看完整的程式碼鏈接。

如果你對我的教程滿意,歡迎在頁面右上方的 Star 上點擊一下,幫我加一顆星。謝謝!
注意這個頁面的中央,有個按鈕,寫著 「在 Colab 打開」(Open in Colab)。請你點擊它。
然後,Google Colab 就會自動開啟。

我建議你點一下上圖中紅色圈出的 「COPY TO DRIVE」 按鈕。這樣就可以先把它在你自己的 Google Drive 中存好,以便使用和回顧。

Colab 為你提供了全套的運行環境。你只需要依次執行程式碼,就可以復現本教程的運行結果了。
如果你對 Google Colab 不熟悉,沒關係。我這裡有一篇教程,專門講解 Google Colab 的特點與使用方式。
為了你能夠更為深入地學習與了解程式碼,我建議你在 Google Colab 中開啟一個全新的 Notebook ,並且根據下文,依次輸入程式碼並運行。在此過程中,充分理解程式碼的含義。
這種看似笨拙的方式,其實是學習的有效路徑。
程式碼
首先,讀入 Python 正則表達式包。
import re
然後,我們把數據準備好。注意為了演示程式碼的通用性,我這裡在最後加了一行文字,區別於之前的文字規律,看看我們的程式碼能否正確處理它。
data = """張華考上了北京大學 李萍進了中等技術學校 韓梅梅進了百貨公司 他們都有光明的前途"""
然後,該寫正則表達式了。你真的需要自己手動來寫嗎?
當然不必。
強大的 regex101 網站,已經幫助我們準備好了。

請你點擊上圖中紅色圈出的按鈕,網站會為你準備好一個初始程式碼的模板,可以匹配你需要的模式。

你不需要完全照搬程式碼。其中有這樣一句,是很重要的,拷貝過來,貼到 Colab Notebook 就好。
regex = r"(.*)[考|進].*了(.*)"
以上就是你的正則表達式,在 Python 裡面應有的樣子。
我們準備一個空列表,用來接收數據。
mylist = []
接著,寫一個循環。
for line in data.split('n'): mysearch = re.search(regex, line) if mysearch: name = mysearch.group(1) dest = mysearch.group(2) mylist.append((name, dest))
我給你解釋一下這個循環裡面,各條語句的含義:
data.split('n')
把文本數據按行來拆分開。這樣我們就可以針對每一行,來獲取數據。mysearch = re.search(regex, line)
這一句嘗試匹配模式到該行內容。if mysearch
這個判斷語句,是讓程式分辨一下,該行是否有我們要找的模式。例如最後一行文字,裡面並沒有咱們前面分析的文字模式。遇到這樣的行,直接跳過。name = mysearch.group(1)
是說匹配的第一組內容,也就是 regex101 網站里綠色代表的人名分組存到name
變數里。下一句依次類推。注意group
對應你正則表達式裡面小括弧出現的順序,從 1 開始計數。mylist.append((name, dest))
把該行抽取到的資訊,存入到咱們之前定義的空列表裡面。
注意,如果不加 mysearch = re.search(regex, line)
這一句,程式會對每一行都嘗試匹配並且抽取分組內容,那麼結果就會報這樣的錯誤:

所以你看,用正則表達式抽取資訊時,不能蠻幹。
此時,我們查看一下 mylist
這個列表裡面的內容:
mylist
結果為:
[('張華', '北京大學'), ('李萍', '中等技術學校'), ('韓梅梅', '百貨公司')]
不錯,一個不多,一個不少,恰好是我們需要的。
我們要把它導出成為表格。方法有很多,但是最簡便順手的,是用 Pandas 數據分析軟體包。
import pandas as pd
只需要利用 pd.DataFrame
函數,我們就能把上面列表和元組(tuple)組成的一個二維結構,變成數據框。
df = pd.DataFrame(mylist) df.columns = ['姓名', '去向']
注意,這裡我們還非常細心地修改了表頭。
看看你的勞動成果吧:
df

有了數據框,轉換成為 Excel ,就是一行程式碼的事情了:
df.to_excel("dest.xlsx", index=False)
進入 Files 標籤頁,刷新並且查看一下當前目錄下的內容:

這個 dest.xlsx
就是輸出的結果了。下載之後我們可以用 Excel 打開查看。

任務完成!
你可以把結果提交給班主任,看他滿意的笑容了。
小結
這篇教程裡面,咱們談了如何利用文本字元規律,藉助 Python 和正則表達式,來提取結構化資訊。
希望你已經掌握了以下本領:
- 了解正則表達式的功用;
- 用 regex101 網站嘗試正則表達式匹配,並且生成初步的程式碼;
- 用 Python 批量提取資訊,並且根據需求導出結構化數據為指定格式。
再次強調一下,對於這麼簡單的樣例,使用上述方法,絕對是大炮轟蚊子。
然而,如果你需要處理的數據是海量的,這個方法給你節省下來的時間,會非常可觀。
希望你能夠舉一反三,在自己的工作中靈活運用它。