重磅開源:TN文本分析語言


tn是desert(沙漠之鷹)和tan共同開發的一種用於匹配,轉寫和抽取文本的語言(DSL)。並為其開發和優化了專用的編譯器。基於遞歸下降方法和正則表達式,能解析自然文本並轉換為樹和字典,識別時間,地址,數量等復雜序列模式。
github地址:https://github.com/ferventdesert/tnpy

0.設計理由

字符串分析和處理幾乎是每個員程序必備的工作,簡單到分割類似"1,2,3,4"這樣的字符串,稍微復雜一些如字符串匹配,再復雜如編譯和分析SQL語法。字符串幾乎具有無窮的表達能力,解決字符串問題,就解決了計算機90%的問題。

雖然字符串處理如此深入人心,但當分割字符時,本來都是按照逗號分割的,突然出現分號,程序就可能出錯。再如日期處理,每個程序員肯定都對各種奇怪詭異的時間表達方式感到頭疼,處理起來非常費時。這些功能,幾乎只能以硬編碼實現。它們是與外界交互的最底層模塊,然而卻如此脆弱。

  • 如何將”一百二十三“轉換為數字?
  • 如何將”2013年12月14日“識別為時間並轉換為時間類型?
  • 如何分析一個XML或JSON文件?

正則表達式雖提供了強大的匹配功能,成為必備的工具,但它有不少局限,我們擴展了正則表達式引擎,使之能力大大增強。
在線演示:http://www.desertlambda.com:81/extracttext.html

1. 如何學習?

基本上程序員都讀過“30分鍾學會正則表達式”這篇文章吧?最后沒幾個人能在30分鍾內就讀完它。不過相信我,TN引擎只需要15分鍾就可以學會。
詳細的語法說明在這里:
[tn基本語法][1]
[使用tn構造自然語言計算器][2]
[tn實現的xml解析器][3]

TN可以實現文本的匹配,轉寫和信息抽取,可以理解為模板引擎的逆運算。簡單的操作用正則表達式更方便,但不少問題是正則無法解決的。這時就需要使用TN了。

TN的解釋器有Python,C#和C三種版本。C#版本已經不再維護。使用C#或Java等語言的,建議使用IronPython或Jython進行跨語言編譯。
tnpy是tn的Python解釋器,Python良好的可讀性讓代碼寫起來非常方便,代碼不超過1000行,單文件,無第三方庫依賴。推薦使用Python3。
tn是解釋型語言,需要編寫規則文件,並使用tnpy加載,再對文本進行處理。

1. 基礎的匹配和替換:

首先我們先編寫一個最簡單的規則文件learn,內容如下:

#%Order% 1
hello= ("你好");

接着,執行下面的python代碼:

from src.tnpy import RegexCore
core = RegexCore('../rules/learn')
matchs=core.Match('領導你好!老婆你好');
for m in matchs:
    print('match',m.mstr, 'pos:',m.pos)

引入tnpy命名空間,之后從learn規則文件初始化引擎,匹配該文本:

success load tn rules:../rules/learn
match 你好 pos: 2
match 你好 pos: 7

上面輸出了文本的匹配結果和位置。當然這一點正則也能做到。
如果我們匹配的是領導你好,老婆您好,並想把所有的你好您好,都轉寫為hello
為此我們添加hello2和hello3兩個子規則:

hello2= $(hello)| ("您好");
#%Order% 1
hello3= $(hello2) : (//:/hello/);

hello2引用了剛才的hello規則,同時添加了“您好”
hello3是主規則,負責將將hello2匹配的內容都轉寫為hello
($代表引用一條規則,|表示將幾個規則並列排列,匹配最長的那個規則,:代表轉寫。)

執行下面的代碼:`

print(core.Rewrite('領導你好!老婆您好'));

結果為:

領導hello!老婆hello

如果我們想替換順序,把“你好”放在前面呢?可以這樣寫:

people= ("老婆") | ("領導");
#%Order% 1
reorder= $(people) $(hello3) : $2 $1;

先用people定義如何描述老婆,領導,然后用reorder來修改順序, 注意reorder是個順序結構,people匹配老婆和領導,hello3匹配您好/你好,並將其轉換為hello$2和$1修改了轉寫順序,執行Rewrite后輸出:

hello領導!hello老婆

我們把類似$(name1) $(name2)的結構,稱為順序表達式,把$(name1) | $(name2) 稱為或表達式。
如果將剛才所有的規則繪制成圖,則是下面的樣子:
![foo.png-34.5kB][4]

2. 正則表達式

僅僅使用文本,表現力太差了。我們引入正則表達式來完成,正則表達式需要放在(//)中,注意和文本("")的區別。
如果要進行轉寫,則標注為(/match/:/rewrite/);
下面的表達式將所有的長空白符轉換為一個空白符:

byte_det_space = (/ */://);

下面將所有字母轉換為空白:

low_letter_to_null = (/[a-z]/ ://);
#或者下面:
low_letter= (/[a-z]/);
translate= $(low_letter) : ("");

覺得沒有挑戰?我們接着看下面的。

3. 復雜組合:中文數字轉阿拉伯數字

二十三如何轉換為23?這種用普通的編程會比較困難。我們嘗試用TN解決,會發現一點都不難。
先定義漢字的一二三到九轉換為1-9,你肯定會寫出這樣的規則:

#定義0-9
int_1 = ("一" : "1");
int_0 =("零" : "0");
int_2  = ("二" : "2") | ("兩" : "2");
int_3_9 = ("三" : "3") | ("四" : "4") | ("五" : "5") | ("六" : "6") | ("七" : "7") | ("八" : "8") | ("九" : "9");
int_1_9 = $(int_1) | $(int_2) | $(int_3_9) | (/\d/);
int_0_9 = $(int_0) | $(int_1_9);
int_del_0 = (/零/ : /0/) |  (// : /0/);
int_0_9_null = $(int_del_0) |  $(int_0_9);

之所以要把0,1,2分開寫,是因為這些數有特殊情況,如兩和二都代表2,需要在后面特殊處理。
上面的int_0_9_null規則,就可以把五七零二轉寫為5702。但沒法處理二十三這樣的情況。

再定義下面的規則,這樣一十三可以轉寫為13

int_del_0 = (/零/ : /0/) |  (// : /0/);
int_0_9_null = $(int_del_0) |  $(int_0_9);
#定義10,十
int_1_decades = (/十/ : /1/) | (/一十/ : /1/);

再加上下面的規則,int_1_9_decades定義了十位數如何轉寫,而int_10_99定義了從十到九十九的轉寫規則。

int_10_99 = $(int_1_9_decades) $(int_0_9_null)  | (/[1-9][0-9]/) ;
int_1_99 = $(int_1_9) | $(int_10_99) ;
int_01_99 =  $(int_1_9) | $(int_10_99) | (/\d{1,2}/);

#%Order% 3
int_0_99 =  $(int_0) | $(int_1_9) | $(int_10_99);

看看下面的例子:
print({r:core.Rewrite(r) for r in ['十','三十七','一十三','68']});
運行結果:
{'一十三': '13', '68': '68', '十': '10', '三十七': '37'}
是不是感到很神奇?三十七是如何被轉寫為37的?

仔細看規則,規則自底向上構造成了一棵規則樹,in_0_99是整棵樹的根節點。結構如下圖:
![foo.png-132.1kB][5]
下面的log文件給出了匹配過程:

int_0_99,Table,Raw  =三十七
  int_0,String,Raw  =三十七
  int_0,String,NG
  int_1_9,Table,Raw  =三十七
    int_1,String,Raw  =三十七
    int_1,String,NG
    int_2,Table,Raw  =三十七
      int_2_merge,Regex,Raw  =三十七
      int_2_merge,Regex,NG
    int_2,Table,NG
    int_3_9,Table,Raw  =三十七
      int_3_9_merge,Regex,Raw  =三十七
      int_3_9_merge,Regex,Match=三
    int_3_9,Table,Match=三
    int_1_9_3,Regex,Raw  =三十七
    int_1_9_3,Regex,NG
  int_1_9,Table,Match=三
  int_10_99,Table,Raw  =三十七
    int_10_99_0,Sequence,Raw  =三十七
      int_1_9_decades,Table,Raw  =三十七
        int_1_decades,Table,Raw  =三十七
          int_1_decades_0,Regex,Raw  =三十七
          int_1_decades_0,Regex,Match=十
          int_1_decades_1,Regex,Raw  =三十七
          int_1_decades_1,Regex,NG
        int_1_decades,Table,Match=十
        int_1_9_decades_1,Sequence,Raw  =三十七
          int_1_9,Table,Raw  =三十七
          int_1_9,Table,Buff =三
          unknown,Regex,Raw  =十七
          unknown,Regex,Match=十
        int_1_9_decades_1,Sequence,Match=三十
      int_1_9_decades,Table,Match=三十
      int_0_9_null,Table,Raw  =七
        int_del_0,Table,Raw  =七
          int_del_0_0,Regex,Raw  =七
          int_del_0_0,Regex,NG
          int_del_0_1,Regex,Raw  =七
          int_del_0_1,Regex,Match=
        int_del_0,Table,Match=
        int_0_9,Table,Raw  =七
          int_0,String,Raw  =七
          int_0,String,NG
          int_1_9,Table,Raw  =七
            int_1,String,Raw  =七
            int_1,String,NG
            int_2,Table,Raw  =七
              int_2_merge,Regex,Raw  =七
              int_2_merge,Regex,NG
            int_2,Table,NG
            int_3_9,Table,Raw  =七
              int_3_9_merge,Regex,Raw  =七
              int_3_9_merge,Regex,Match=七
            int_3_9,Table,Match=七
            int_1_9_3,Regex,Raw  =七
            int_1_9_3,Regex,NG
          int_1_9,Table,Match=七
        int_0_9,Table,Match=七
      int_0_9_null,Table,Match=七
    int_10_99_0,Sequence,Match=三十七
    int_10_99_1,Regex,Raw  =三十七
    int_10_99_1,Regex,NG
  int_10_99,Table,Match=三十七
int_0_99,Table,Match=三十七

引擎從文本的左向右,沿着規則樹尋找最長的文本,如果在一個順序表達式上的任何一步失敗,那么整個順序表達式被拋棄。或表達式會遍歷每個子表達式,直到發現最長的那個,返回結果。具體的匹配原理,以及優化,會在專門的文章中介紹。

4. 由規則構造更復雜的規則

自然而然的,知道怎么定義三十七,就可以定義五百三十七,那不過是int_1_9_hundreds+int_0_99(這個已經定義過了)。

int_1_9_hundreds = $(int_1_9) ("百" : "");
int_100_999 =   $(int_1_9_hundreds) ("" : "00") |  $(int_1_9_hundreds) $(int_10_99);
int_1_999 = $(int_1_99) | $(int_100_999);

int_1_999可以處理類似五百三十七這樣的問題!

進而,我們可以處理幾千,幾萬,這個延伸到萬以后,就可以自然而然地衍生出億,萬億的表達。

如何處理負數?這還不簡單!

signed_symbol0 = ("正" : "") | ("負" : "-") | ("正負" : "±") | ("\+" : "+") | ("\-" : "-") | ("±" : "±") ; 
signed_symbol = $(signed_symbol0) | $(null_2_null);

接下來,我們默認正整數為integer_int,那么,整數(包含正負)就是:
integer_signed = $(signed_symbol) $(integer_int)

5. 屬性提取

沿着剛才的路,我們自然而然地能定義分數,但僅僅是轉寫還不夠,遇到三分之一,我們不僅要將其處理為1/3,還要計算出它的值,這就涉及到屬性抽取。也就是把信息從文本中提取為字典。

分數,不過是整數+分之+整數,可以定義成下面的形式:

fraction_cnv_slash = ("分之" : "/");
fraction2 = ("/" : "/");
percent_transform= ("%" : "100") | ("‰" : "1000");
#%Type% DOUBLE
#%Property%  Denominator,,Numerator| Numerator ,, Denominator | Denominator ,, Numerator 
#%Order% 101
fraction = $(integer_int_extend) $(fraction_cnv_slash) $(integer_int) : $3 $2 $1
    | $(integer_int) $(fraction2) $(integer_int)
    | $(pure_decimal) ("" : "/") $(percent_transform);

這個有點復雜,但容我慢慢講解。分數有三種情況,如剛才的三分之一,或是1/3,或是30%。分別對應上面fraction規則的三個子規則。仔細地看上面的規則,不難理解。

值得注意的是Property這個標簽,該標簽定義了如何抽取信息。也是用豎線分隔,每個名稱對應下面的一個子規則,為空的直接跳過。那么”十三分之二十四“中,“十三”就對應Numerator, 而“二十四”對應Denominator。來測試一下:

print(core.Extract('十三分之二十四',entities=[core.Entities['fraction']]))
我們用Extract函數來抽取文本,返回的是一個字典,entites是可選參數,我們限制只用fraction規則來匹配,獲得輸出:

'#match': '十三分之二十四', 'Denominator': '13', '#pos': 3}]```
是不是很贊?

###6.嵌入Python腳本
有一種需求還沒談到,將所有的大寫字母轉換為小寫字母,你可能會想定義26個字符串規則,並用或表達式來拼接起來吧?這樣太費事了。我們可以直接這樣:
`low_to_up_letter =  (/[A-Z]/) : "str.lower(mt)";`
`[A-Z]`匹配了所有的大寫字母,將匹配結果送到后半段的轉寫,內置的解釋器會執行那段python代碼,將其轉換為小寫,mt代表前面表達式的匹配串,rt代表轉寫串。好在`[A-Z]`不執行轉寫,可以認為`mt==rt`.
這是在轉寫過程中嵌入python的例子,還能在匹配時嵌入轉寫:
`foo = "findsecret" : "print(mt)"`;
前面的findsecret函數負責在字符串中找到“神秘文本”,后面的轉寫代碼打印出來,並將原始的字符返回…

##6. 你在15分鍾內讀完了么?
我相信你沒有,因為讀懂那個匹配規則的日志文件,就需要最少五分鍾,但如果你有編譯原理和正則基礎的話,還是能很快理解的。而從零開發這個引擎,到反復優化和完善,花了一年之久。

定義了各種數字之后,我們就能很快地定義時間,日期,電話號碼,地址…而你看到的只是TN語言的冰山一角。

 - 它能夠分析文本的模式,解析諸如ABCABC這樣的序列,從而發現這是一個重復模式。

 - 不僅能夠順序匹配,還能逆向,甚至亂序匹配,這就能夠抽取類似“學校的校訓”這樣的問題。

 - 規則可以調用自身,配合腳本,因此能夠實現遞歸下降解析。例如30行代碼實現xml解析,或20行規則實現自然語言計算器。

 - 規則可以嵌入腳本,甚至動態生成代碼,因此,甚至在理論上,TN能夠自己編譯自己。

 - TN還能做一個簡單的SQL解釋器,或是中文英文的簡單互相翻譯的工具。

是不是已經激動地顫抖了?唯一限制你能力的就是你的想象力。本博客將會進一步發布一系列有關tn的內容,包括高級語法,tn優化等。
感興趣的可以聯系作者:buptzym@qq.com,或在本文下面留言。


  [1]: http://www.cnblogs.com/buptzym/p/5355827.html
  [2]: http://www.cnblogs.com/buptzym/p/5361121.html
  [3]: http://www.cnblogs.com/buptzym/p/5355920.html
  [4]: http://static.zybuluo.com/buptzym/ksl5ggrfcn1psmdf2f81i8wg/foo.png
  [5]: http://static.zybuluo.com/buptzym/itwhlmz8ua2h3jgbqdq5z48g/foo.png


免責聲明!

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



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