使用 UNIX 進(jìn)行文本處理
Unix® 起源于簡單的文本處理,并且在它的命令行環(huán)境中保留了功能最強(qiáng)大的文本處理工具之一。通過將一系列簡單的命令組合在一起,可以完成復(fù)雜的文本轉(zhuǎn)換,UNIX 提供的工具允許您構(gòu)建幾乎任何所需的文本處理引擎。
引言
在 UNIX® 誕生之初,人們不大熟悉這種新的操作系統(tǒng),但他們很快找到了適當(dāng)?shù)那腥朦c,大學(xué)中的研究人員需要一種像樣的文本處理環(huán)境。因為在那個時候,計算機(jī)的處理速度和內(nèi)存容量有限,所以程序必須很小,并且相對比較簡單。這樣就產(chǎn)生了 UNIX 中著名的設(shè)計思想:“一組工具協(xié)同工作,以便完成一項任務(wù)。通過 UNIX 管道將幾種很小的、但功能強(qiáng)大的文本處理工具組合在一起,可以對文本進(jìn)行各種方式的轉(zhuǎn)換和操作。
在本文中,您將簡要了解從文件和程序中獲得文本、使用 tr 命令對其進(jìn)行簡單的轉(zhuǎn)換、使用 sed 命令進(jìn)行復(fù)雜的搜索和替換操作。然后,您將使用 Perl 編程和腳本語言再次完成這些操作,這樣一來您就可以認(rèn)識到,Perl 的功能非常強(qiáng)大,它可以替代 tr 和 sed 命令。
開始之前
如果您希望按照本文中的示例進(jìn)行實驗,請確保您可以使用 UNIX 命令行環(huán)境。這可能是本地計算機(jī)中的終端模擬程序(在現(xiàn)代桌面中通常稱為 終端,如果您習(xí)慣使用 Windows®,那么可以使用 Cygwin)、或通過 SSH 訪問的遠(yuǎn)程系統(tǒng)。
本文的示例所使用的 Shell 語法適用于 GNU Bash,有關(guān)需要使用的特定語法,請參考您的 Shell 手冊(或者可以考慮使用 Bash)。
對文本進(jìn)行各種操作
在開始使用 UNIX 的各種文本實用程序操作文本之前,需要了解如何獲得文本。并且在進(jìn)行這項工作之前,需要了解 UNIX 的標(biāo)準(zhǔn)輸入/輸出 (I/O) 流。
標(biāo)準(zhǔn) C 庫(因而,每個 UNIX 程序)定義了三種標(biāo)準(zhǔn)流:輸入、輸出和錯誤。有時將它們稱為 stdin、stdout 和 stderr,這是在所有 C 程序中用來表示它們的全局變量。
當(dāng)您在 Shell 中使用 > 操作符將程序輸出重定向到文件時,就可以將它的標(biāo)準(zhǔn)輸出 (stdout) 流發(fā)送到這個文件。例如:ls > this-dir 將 ls 的輸出發(fā)送到一個名為 this-dir 的文件。
當(dāng)您在 Shell 中使用 < 操作符將程序輸入重定向到文件時,就可以將該文件中的內(nèi)容輸入到該程序的標(biāo)準(zhǔn)輸入 (stdin) 流。例如:sort < this-dir 可以從名為 this-dir 的文件中讀取內(nèi)容,并將其作為 sort 命令的輸入。
另一個常用于重定向標(biāo)準(zhǔn)流的操作符是“|(管道)操作符,它可以將左側(cè)程序的標(biāo)準(zhǔn)輸出流連接到右側(cè)程序的標(biāo)準(zhǔn)輸入流。例如:ls | sort 和前面的兩個示例完成相同的任務(wù),并且無需臨時文件,ls 的輸出直接進(jìn)入 sort 命令。
如果您仔細(xì)觀察,那么可能會發(fā)現(xiàn),前面的這些示例中并沒有出現(xiàn)標(biāo)準(zhǔn)錯誤 (stderr) 流。與標(biāo)準(zhǔn)輸出流一樣,可以對 stderr 進(jìn)行重定向或使用管道進(jìn)行傳輸,但是您需要告訴 Shell 您希望處理 stderr 而不是 stdout。
可以使用 2> 操作符將標(biāo)準(zhǔn)錯誤流重定向到文件。在處理生成有用的錯誤輸出的命令時,您經(jīng)常會看到這個操作符,比如用于編譯 Unix 程序的 make 工具:make 2> build-errors。
這個命令運行了 make,并將任何錯誤信息發(fā)送到 build-errors 文件。與之類似,您可以使用 2| 將 stderr 通過管道傳遞到另一個程序。
如果您對具體的細(xì)節(jié)感興趣,那么其他的流也有與之對應(yīng)的數(shù)字,盡管很少使用到它們(0 表示標(biāo)準(zhǔn)輸入,1 表示標(biāo)準(zhǔn)輸出),除了在一個非常常見的操作符中。在清單 1 所示的示例中,2>&1 操作符將標(biāo)準(zhǔn)錯誤流連接 到標(biāo)準(zhǔn)輸出流。與 > 操作符組合在一起,您可以使 stderr 和 stdout 輸出到相同的文件中。
清單 1. 將標(biāo)準(zhǔn)錯誤流連接到標(biāo)準(zhǔn)輸出流
make > build-output 2>&1命令
有兩個常用來生成文本輸出的標(biāo)準(zhǔn) Unix 命令:cat 和 echo。
cat 命令讀取參數(shù)中指定的每個文件,并將這些文件的內(nèi)容寫入到 stdout。echo 命令將其參數(shù)寫入到 stdout。您常常會發(fā)現(xiàn)它們作為更復(fù)雜的命令管道中的一部分(請參見清單 2)。
清單 2. 使用 cat 和 echo
cat file1 file2 ... filenecho arguments...但如果您只需要文件中開頭的部分或結(jié)尾的部分,那又應(yīng)該如何呢?cat 有兩種可用來完成這種任務(wù)的變種,稱為 head 和 tail(請參見清單 3),它們分別可以顯示開頭的或結(jié)尾的 10 行內(nèi)容,您可以使用 -n 選項為它們指定不同的行數(shù)。
清單 3. 使用 head 和 tail
head file1 file2 ... filentail file1 file2 ... filentail 命令還有一個有用的選項 -f (follow)。該選項告訴 tail 打印指定文件的最后 10 行,但是它不僅打印已有的內(nèi)容,還會等待該文件中將要出現(xiàn)的更多內(nèi)容,并對其進(jìn)行打印。您可以使用該選項接著 顯示錯誤日志中的輸出,例如,要在將錯誤寫入到日志的同時查看這些錯誤。
轉(zhuǎn)換文本
既然您已經(jīng)了解了至少 5 種生成文本的方式,下面讓我們來看一些進(jìn)行簡單文本轉(zhuǎn)換的示例。
tr 命令允許您將一個集合中的字符轉(zhuǎn)換為另一個集合中相應(yīng)的字符。讓我們來看一些示例(清單 4),以了解其工作方式。
清單 4. 使用 tr 對字符進(jìn)行轉(zhuǎn)換
echo "a test" | tr t pecho "a test" | tr aest 1234echo "a test" | tr -d techo "a test" | tr '[:lower:]' '[:upper:]'研究這些命令的輸出結(jié)果(請參見清單 5),可以看出 tr 的工作方式(提示:它直接使用第二個集合中相應(yīng)的字符來代替第一個集合中的字符)。
清單 5. tr 進(jìn)行了哪些工作?
chrish@dhcp3 [199]$ echo "a test" | tr t pa pespchrish@dhcp3 [200]$ echo "a test" | tr aest 12341 4234chrish@dhcp3 [201]$ echo "a test" | tr -d ta eschrish@dhcp3 [202]$ echo "a test" | tr '[:lower:]' '[:upper:]'A TEST第一個和第二個示例都很簡單,將一個字符替換為另一個字符。第三個示例使用了 -d 選項 (delete),它從輸出中徹底刪除了指定的字符。這個選項通常用來從 Dos 文本文件中刪除回車,以將其轉(zhuǎn)換為 Unix 文本文件(請參見清單 6)。最后一個示例使用了字符類([: :] 中的名稱),以將所有的小寫字母轉(zhuǎn)換為大寫字母。可移植操作系統(tǒng)接口標(biāo)準(zhǔn)(POSIX 標(biāo)準(zhǔn))字符類包括:
alnum:字母數(shù)字字符 alpha:字母字符 cntrl:控制(非打印)字符 digit:數(shù)字字符 graph:圖形字符 lower:小寫字母字符 print:可打印字符 punct:標(biāo)點符號 space:空白字符 upper:大寫字符 xdigit:十六進(jìn)制字符清單 6. 將 DOS 文本文件轉(zhuǎn)換為 UNIX 文本文件
tr -d '' < input_dos_file.txt > output_unix_file.txt盡管 tr 命令表示了 C locale 環(huán)境變量(有關(guān)這些環(huán)境變量更多的信息,可以使用 man locale),但是不要指望它能夠?qū)?UTF-8 文檔進(jìn)行任何合理的操作,如能夠使用合適的大寫字符替換小寫重音字符。tr 命令最適合于 ASCII 和其他標(biāo)準(zhǔn) C 區(qū)域設(shè)置。
使用 sed 進(jìn)行復(fù)雜的搜索和替換
tr 命令所提供的單字符替換(或刪除)功能非常適用于特定的解決方案,但是這些功能并不是很靈活。如果您需要將一個單詞替換為另一個單詞,或?qū)⑦B續(xù)的空格和制表符替換為一個空格,那又應(yīng)該怎么辦呢?
幸運的是,您可以使用 sed 命令 (Stream EDitor),它提供了功能強(qiáng)大的正則表達(dá)式 匹配和替換。正則表達(dá)式是使用各種構(gòu)件構(gòu)建的復(fù)雜模式規(guī)范,并且隨著模式變得越來越復(fù)雜,它看起來就像是調(diào)制解調(diào)器的線路噪聲。本文并不打算詳細(xì)地介紹正則表達(dá)式,但是在本文中,您將簡單了解 sed 所使用的一些有用的模式。
在清單 7 中,您可以看到 sed 命令的基本格式。模式是用來匹配輸入(通常可以使用管道從另一個程序輸入,或者重定向于文本文件)的正則表達(dá)式,替換是指插入某些文本并用其代替那些與模式相匹配的文本。標(biāo)志是用來控制替換行為的單個字符。最常用的標(biāo)志是 g(將替換應(yīng)用于所有匹配模式的非重疊實例,而不僅僅是第一個匹配項)。
實際上,模式和替換可以是各種各樣的內(nèi)容,并且它們之間不需要像在 tr 命令中那樣具有 1:1 的關(guān)系。
清單 7. sed 命令
sed -e s/pattern/replacement/flags最簡單的模式是一個或多個字符組成的字符串。如清單 8 所示,例如將單詞 one 替換為單詞 another。
清單 8. 最簡單的正則表達(dá)式
chrish@dhcp3 [334]$ echo "Replace one Word" | sed -e s/one/another/Replace another word可以使用方括號將一個或多個字符括起來,以創(chuàng)建一個集合,該集合中的任何字符都可以匹配。如清單 9 所示,讓我們將所有的元音字母替換為下劃線。
清單 9. 匹配集合中的任何字符
chrish@dhcp3 [338]$ echo "This is a test" | sed -e s/[aeiouy]/_/gTh_s _s _ t_st請注意,示例中使用了 g 標(biāo)志,以便將模式/替換應(yīng)用于所有的匹配項,而不僅僅是第一個匹配項。
sed 命令也可以理解 tr 命令所支持的那些命名字符類,POSIX 對這些字符類進(jìn)行了定義,但是本文中的語法稍有不同。清單 10 顯示了如何替換任何空白字符(制表符、空格等等):
清單 10. 根據(jù)命名字符類匹配內(nèi)容
chrish@dhcp3 [345]$ echo -e 'hellothere' hello therechrish@dhcp3 [346]$ echo -e 'hellothere' | sed -e 's/[[:space:]]/, /'hello, thereecho 命令的 -e 標(biāo)志用來告訴該命令擴(kuò)展 C 風(fēng)格的轉(zhuǎn)義字符,在本示例中,它會把 轉(zhuǎn)換為制表符。
您還可以使用“.(點號)匹配任何單個的字符。如果您需要處理一些略有變化的數(shù)據(jù),或者包含難以進(jìn)行轉(zhuǎn)義的特殊字符的數(shù)據(jù),那么使用這個符號是非常方便的。例如,在匹配引號時,我經(jīng)常使用 .,所以我不需要在 Shell 中對引號進(jìn)行轉(zhuǎn)義。清單 11 顯示了一個正則表達(dá)式初學(xué)者在使用這個模式時出現(xiàn)的問題。
清單 11. 這可能并不是想要的結(jié)果
chrish@dhcp3 [339]$ echo "This is a test" | sed -e s/./_/g______________既然您已經(jīng)了解了這些非常基本的內(nèi)容,下面介紹一些附加模式修飾符,要使用高級 正則表達(dá)式,您現(xiàn)在還可以使用 -E 選項代替 -e。? 字符表示匹配前面模式元素的零個或一個實例,* 字符表示匹配前面元素的零個或多個實例。+ 字符表示匹配一個或多個前面的元素。^ 字符匹配行首,而 $ 則匹配行尾。清單 12 顯示了實際應(yīng)用中的情況。
清單 12. 實際應(yīng)用中的多個匹配項
chrish@dhcp3 [356]$ echo "hellooooo" | sed -E 's/o?$/_/g'helloooo_chrish@dhcp3 [357]$ echo "hellooooo" | sed -E 's/o*$/_/g'hell_chrish@dhcp3 [358]$ echo "hellooooo" | sed -E 's/o+$/_/g'hell_如果使用圓括號將模式元素括起來,您可以在替換字符串中使用匹配的內(nèi)容。這些元素稱為組,它們使得正則表達(dá)式搜索和替換操作的功能變得非常強(qiáng)大,但是卻很難理解。例如,在清單 13 中,您匹配一個或多個 l (el) 字符,并且后面跟著零個或多個 o 字符。依次使用第二組和第一組中的內(nèi)容對其進(jìn)行替換,實際上是對它們進(jìn)行交換。請注意這個模式中各個組的引用方法,即反斜杠加上該組的序號。
清單 13. 匹配組
chrish@dhcp3 [361]$ echo "hellooooo" | sed -E 's/(l+)(o*)$/21/g'heoooooll通過在大括號中指定匹配的數(shù)目,您可以匹配特定數(shù)目的模式。例如,模式 o{2} 將匹配兩個(僅僅兩個)o 字符。
對了,還有最后一個內(nèi)容,通過使用 字符對其進(jìn)行轉(zhuǎn)義,您可以在模式中使用這些特殊字符的字面內(nèi)容(即作為其本身)。
將其組合在一起
既然已經(jīng)向您介紹了一些非常簡單的正則表達(dá)式,那么讓我們來嘗試一些有用的內(nèi)容。給定 ls -l(文件長 清單)的輸出,您將從中提取權(quán)限信息、大小和名稱。清單 14 顯示了要進(jìn)行處理的 ls -l 輸出示例。
清單 14. ls -l 的典型輸出
chrish@dhcp3 [365]$ ls -l | taildrwx------ 3 chrishwheel 102 Jun 14 21:38 gsrvdir501drwxr-xr-x 2 chrishwheel68 Jun 16 16:01 hsperfdata_chrishdrwxr-xr-x 3 root wheel 102 Jun 14 23:38 hsperfdata_root-rw-r--r-- 1 root wheel 531 Jun 14 10:17Illustrator_activation.plist-rw-r--r-- 1 root wheel 531 Jun 14 10:10 indesign_activation.plist-rw------- 1 nobodywheel24 Jun 16 16:01 objc_sharing_ppc_4294967294-rw------- 1 chrishwheel 132 Jun 16 23:50 objc_sharing_ppc_501-rw------- 1 security wheel24 Jun 16 10:04 objc_sharing_ppc_92-rw-r--r-- 1 root wheel 531 Jun 14 10:05 Photoshop_activation.plist-rw-r--r-- 1 root wheel 928 Jun 14 10:17 serialinfo.plist正如您所看到的,這里一共有 7 列:
權(quán)限 鏈接的數(shù)目 屬主 組 大小 最后的修改時間 名稱讓我們來建立一些正則表達(dá)式,以匹配其中的每一列:
.([r-][w-][x-]){3}—權(quán)限(使用 . 匹配第一個字符,因為它可能是幾個不同的特殊字符中的任何一個。) [[:digit:]]+—鏈接的數(shù)目 [A-Za-z0-9_-.]+ -—屬主(您還可以使用這個模式進(jìn)行組匹配。) [[:digit:]]+—大小 .{3} [0-9 ]{2} [0-9 ][0-9]:[0-9][0-9]—修改時間(您可以對這個模式進(jìn)行一些簡化,因為所有的文件都在 6 月份進(jìn)行的修改,所以您可以確切地指定月份的名稱。) .+$—名稱(在這些內(nèi)容之后,您需要匹配所有的字符,直到行尾。)在上述模式之間,必須使用 [[:space:]]+ 對它們進(jìn)行連接,因為您并不知道這些列之間究竟是使用空格或制表符,還是兩者的組合進(jìn)行分隔。您還需要將權(quán)限、大小和名稱放到組中,以便可以在替換中使用它們。如清單 15 所示,正則表達(dá)式很快就變得難以理解。
清單 15. 完成后的正則表達(dá)式實在難以理解!
(.([r-][w-][x-]){3})[[:space:]]+[[:digit:]]+[[:space:]]+([A-Za-z0-9_-.]+[[:space:]]+){2}([[:digit:]]+)[[:space:]]+.{3} [0-9 ]{2} [0-9][0-9]:[0-9][0-9][[:space:]]+(.+)$如果您仔細(xì)研究這個可怕的正則表達(dá)式模式,您將發(fā)現(xiàn) 5 個組:
完整的權(quán)限塊 權(quán)限塊中最后匹配的 rwx 組 組(該模式的屬主/組部分中最后匹配的內(nèi)容) 大小 名稱在清單 16 中,您將更改 ls -l 的輸出以顯示文件名、權(quán)限和大小。
清單 16. 對輸出進(jìn)行重組
chrish@dhcp3 [382]$ ls -l | tail | sed -E's/(.([r-][w-][x-]){3})[[:space:]]+[[:digit:]]+[[:space:]]+([A-Za-z0-9_-.]+[[:space:]]+){2}([[:digit:]]+)[[:space:]]+.{3} [0-9 ]{2} [0-9][0-9]:[0-9][0-9][[:space:]]+(.+)$/5 (1) has 4 bytes of data/'gsrvdir501 (drwx------) has 102 bytes of datahsperfdata_chrish (drwxr-xr-x) has 68 bytes of datahsperfdata_root (drwxr-xr-x) has 102 bytes of dataIllustrator_activation.plist (-rw-r--r--) has 531 bytes of dataindesign_activation.plist (-rw-r--r--) has 531 bytes of dataobjc_sharing_ppc_4294967294 (-rw-------) has 24 bytes of dataobjc_sharing_ppc_501 (-rw-------) has 132 bytes of dataobjc_sharing_ppc_92 (-rw-------) has 24 bytes of dataPhotoshop_activation.plist (-rw-r--r--) has 531 bytes of dataserialinfo.plist (-rw-r--r--) has 928 bytes of data成功了!您已經(jīng)完成了對輸出結(jié)果的轉(zhuǎn)換。
使用 Perl 完成相應(yīng)的工作
Perl 編程和腳本語言(請參見參考資料部分)的功能非常強(qiáng)大,通常可用來取代前面介紹的 tr 和 sed 命令。通常可以在命令行中直接輸入簡短的 Perl 程序,有時它可以完成比 tr 或 sed 命令行更多的操作。
Perl 的 -p 選項告訴它讀取和處理標(biāo)準(zhǔn)輸入中的每行內(nèi)容,并將結(jié)果打印到標(biāo)準(zhǔn)輸出。-e 選項允許您在命令行中指定一個 Perl 表達(dá)式(實際上是一個程序)。
清單 17 顯示了如何使用 Perl 完成清單 5 中的示例。
清單 17. 使用 Perl 完成 tr 的工作
chrish@dhcp3 [248]$ echo a test | perl -p -e 'tr/t/p/;'a pespchrish@dhcp3 [249]$ echo a test | perl -p -e 'tr/aest/1234/;'1 4234chrish@dhcp3 [250]$ echo a test | perl -p -e 'tr/t//d;'a eschrish@dhcp3 [251]$ echo a test | perl -p -e 'tr/a-z/A-Z/;'A TESTPerl 的 tr 語句具有不同的語法,它更像 sed 的搜索和替換表達(dá)式。另請注意,您在最后一個示例中指定了小寫和大寫字符的范圍。
Perl 中的正則表達(dá)式支持非常優(yōu)秀,并且上面的 sed 示例可以作為有效的 Perl 語句正常工作。清單 18 使用 Perl 顯示了清單 16 中的 ls -l 示例,除了 Perl 命令行語法之外,不需要對其他的內(nèi)容進(jìn)行更改。
清單 18. 使用 Perl 重組 ls 的輸出
chrish@dhcp3 [384]$ ls -l | tail | perl -p -e's/(.([r-][w-][x-]){3})[[:space:]]+[[:digit:]]+[[:space:]]+([A-Za-z0-9_-.]+[[:space:]]+){2}([[:digit:]]+)[[:space:]]+.{3} [0-9 ]{2} [0-9][0-9]:[0-9][0-9][[:space:]]+(.+)$/5 (1) has 4 bytes of data/'gsrvdir501 (drwx------) has 102 bytes of datahsperfdata_chrish (drwxr-xr-x) has 68 bytes of datahsperfdata_root (drwxr-xr-x) has 102 bytes of dataIllustrator_activation.plist (-rw-r--r--) has 531 bytes of dataindesign_activation.plist (-rw-r--r--) has 531 bytes of dataobjc_sharing_ppc_4294967294 (-rw-------) has 24 bytes of dataobjc_sharing_ppc_501 (-rw-------) has 132 bytes of dataobjc_sharing_ppc_92 (-rw-------) has 24 bytes of dataPhotoshop_activation.plist (-rw-r--r--) has 531 bytes of dataserialinfo.plist (-rw-r--r--) has 928 bytes of data這樣做的優(yōu)點在于,您可以使用 sed 或 Perl 完善正則表達(dá)式,并且在只包含其中某一個的系統(tǒng)中,您仍然可以它們。使用 Perl,您可以獲得全方位的編程結(jié)構(gòu),可以充分地利用它們進(jìn)行更復(fù)雜的文本處理。
總結(jié)
使用像 sed 和 Perl 這樣功能強(qiáng)大的工具,以及神奇的正則表達(dá)式,您可以直接通過 Unix 命令行輕松地完成復(fù)雜的文本處理任務(wù)。這使得您可以有效地將多個命令組合在一起,以正確地完成文本處理工作。
