sed修煉系列(三):sed高級應用之實現窗口滑動技術

sed系列文章:html

sed修煉系列(一):花拳繡腿之入門篇
sed修煉系列(二):武功心法(info sed翻譯+註解)
sed修煉系列(三):sed高級應用之實現窗口滑動技術
sed修煉系列(四):sed中的疑難雜症正則表達式


1.什麼是滑動窗口(slide window)技術

一圖勝千言。shell

在上圖中,資源管理器的高度固定爲正好裝下10行文件名,若是想要顯示第11行,就要下拉滾動條一行的距離,使得11行正好能顯示出來。但這時,最舊的第一行就會被踢出當前可視窗口。ide

滑動窗口的意思大體就是如此。維護一個窗口,當向窗口添加新數據時,舊的數據就被剔除出去,保證窗口的大小固定。固然,還有動態的大小不固定的窗口,此時根據其餘規則來判斷是否要剔除舊數據以及剔除哪些舊數據。工具

在sed的高級用法中,窗口滑動技術做用很是大,這也是"N"、"D"和"P"重要的緣由。sed以模式空間爲"主戰場",以保持空間爲"副戰場"。因此sed要維護"窗口"只能經過模式空間,由於只有在模式空間中才能決定是否要踢掉舊數據以及踢掉哪些舊數據。但不少時候還會輔以保持空間,將每次操做完的窗口數據暫存到保持空間,並在下一個循環中將數據從保持空間拿回來維護一番。post

下面是一些示例和窗口技術的用法說明。性能

2.實現窗口滑動

假如想要輸出文件a.txt的前10行。這很簡單。ui

sed '10q' a.txt

因爲"q"命令在退出sed程序前會輸出模式空間內容,因此第10行也會被輸出,若是使用的是"Q"命令,則應該爲:url

sed 11Q a.txt

難題來了,想輸出倒數10行要怎麼實現,倒數15行?倒數20行?。再通用化一些,怎麼能實現tail工具的倒數行查看功能呢。spa

2.1 經過"s"命令滑動窗口

因爲sed是"勇往直前,毫不回頭"的流式處理器,任何一份輸入流只要被讀取過絕對不會再次被讀取

再者,sed採用行號計數器即時計數,每讀取一行,計數器就加1。所以sed在讀取到最後一行前,不知道後面還有多少行,也不知道何時纔是最後一行。直到讀取到輸入流的最後一行,sed爲該行打上"$"標記,表示這是最後一行。"$"只是一個標記符號,並不是行號,行號只在計數器中記錄,所以沒法經過"$"來計算出倒數幾行,例如"$-1"是錯誤的寫法。這意味着,sed沒法直接輸出倒數的行。要輸出倒數多少行,必須經過窗口來實現。

既然說到了行號,順便提一提。行號的匹配過程比正則表達式的匹配效率要高的多,由於行號是記錄在內存中的,只要比較下表達式中的行號值和計數器記錄的值就能夠了。而正則表達式匹配的時候要通過編譯、匹配等工做,並且sed的正則表達式引擎匹配的效率並不如想象中的那麼好,特別是使用了".*"結合了其餘表達式的時候。所以,批量處理大量文件,特別是大文件時,能用行號儘可能用行號。

回到正題。例如,要輸出倒數10行,這個窗口就一直維持在10行的固定大小。當讀取到最後一行時,能夠經過"$"符號來判斷這是不是最後一行,是的話就輸出該窗口,不然不輸出該窗口的數據。

一般,這樣的問題會藉助保持空間來臨時存儲窗口的數據,但此處僅依靠模式空間也能維持一個固定行數的窗口。以下:

#!/usr/bin/sed -nf

# 先讀取8行,加上自動讀取的一行共9行
N;N;N;N;N;N;N;N

# 判斷是不是最後一行,若是不是則讀取下一行並踢掉最前面的一行
:1
N
$!s/[^\n]*\n//;t1

# 讀取到最後一行後,輸出窗口中的內容
p

爲了在模式空間中保持固定行數的窗口,只能讓全部動做在一個sed循環內完成(由於SCRIPT循環結束時會清空模式空間),所以必須藉助標籤循環跳轉。在上面的示例腳本中,首先讀取了8行,加上自動讀取的一行,模式空間中共9行,這正是咱們須要維護的窗口。隨後,使用一個循環判斷標籤,先讀取一行,再判斷該行是不是最後一行。若是不是,則剔除掉窗口中的第一行,這樣窗口就一直維護在固定的9行大小。直到讀取最後一行,這時替換命令失敗。最後窗口中的10行內容被輸出。

既然經過窗口能輸出倒數10行,顯然輸出倒數第10行也是很簡單的,只需將上面的"p"改爲大寫的"P"便可。

那要是想輸出倒數15行、20行甚至是50行,也要這麼寫嗎?先不說要寫一大堆的"N"命令,僅僅窗口太大的問題就會致使效率極速降低。假如一個文件有1000行,要輸出倒數20行,從第20幾行開始每次作"$"匹配的時候都要從模式空間中的20多行搜索,這至關於處理了一個1000*20=20000行的文件。固然,行號匹配直接比較計數器的值,沒有這種顧慮,但若是真的是正則表達式匹配,效率必然極速降低。

這時,保持空間就能夠派上用場了。但須要注意的是,雖然使用保持空間能夠簡化處理邏輯,但由於兩個buffer空間的數據交換過程都會對性能有一絲絲影響。因此通常來講,用一個buffer空間實現比藉助兩個buffer空間效率要高那麼一點點,特別是大量處理大文件同時還要交換大量數據的時候,性能差距就比較明顯。

2.2 藉助保持空間暫存窗口

上面的例子的思路是讀取一些初始行數填充窗口,在窗口快要達到目標大小時使用標籤循環判斷功能來維持固定大小的窗口滑動過程。

若是藉助保持空間,能夠將每次滑動後的窗口數據暫存起來,在讀取了下一行時,將其追加回來再處理。

例如上面的例子藉助保持空間實現,語句以下:

#!/usr/bin/sed -nf

H
10,${g;s/[^\n]*\n//;h}
$p

因爲藉助了保持空間,所以整個過程無需在一個sed循環內完成。上面的過程將經過sed循環的自動讀取來填充窗口,並將其追加到保持空間。當填充到10行後,將其從保持空間拉回模式空間,並使用"s"命令滑動該窗口,滑動結束後再次將其放回保持空間。直到最後一行滑動結束後,輸出最終窗口的內容。

注意,上面的數字是10,而不該該是11,這是"H"命令致使的結果,由於每次H執行時都會在保持空間尾部先追加一個"\n",即便最初保持空間爲空時也會追加。這使得從讀取第10行並執行了H後,保持空間將有共11行,其中第一行爲空。

2.3 將窗口維護命令"s"替換成"D"

考慮"D"命令的特性:刪除模式空間的第一行,並進入下一個SCRIPT循環。所以,除非模式空間已經沒有內容了,不然"D"命令會一直SCRIPT循環直到模式空間中沒有能被D命的地址匹配的內容。

爲了方便說明"D",舉個簡單的例子:壓縮連續空行。還有壓縮相鄰重複行,即去除重複行。

echo -e "1\n2\n\n3\n4\n\n\n5" | sed '$!N;/^\n$/!P;D' 
echo -e "1\n2\n3\n3\n4\n4\n4\n4\n4\n5" | sed -r '$!N;/^(.*)\n\1$/!P;D'

這兩個命令的思路都是以窗口爲模型(我是這麼認爲的。自從有了窗口的概念,任何涉及"NDP"的命令我都將其認爲是窗口,這樣容易理解多了)。以"N"命令讀入下一行到窗口中,若是窗口中的兩行不重複,就輸出第一行並執行"D"剔除第一行,因而滑動了窗口,並進入下一個SCRIPT循環。直到窗口中出現重複行,將一直循環滑動這大小爲2行的窗口但不輸出,直到不重複了才輸出前面相鄰重複行的最後一行。

由此也能夠看出,其實即便是單行或雙行的模式空間也都算是一個窗口,只不過這個窗口維護起來比較靈活。

以第二個去除重複行的命令來講,大體流程以下:其中d表示該行未被"P"輸出且被"D"刪除了,p表示被"P"輸出後再被"D"刪除。

1p
2p
3d
3p
4d
4d
4d
4d
4p
5p

回到正題。"D"命令其自身實現了一種特殊的條件式SCRIPT循環,而"s"命令要實現這樣的循環只能經過標籤判斷的方式來實現,除非藉助保持空間。正由於如此,"D"命令也能剔除窗口中的舊數據實現窗口滑動。

使用"D"不少時候能夠簡化腳本的複雜性,但其絕對沒法替代"s"命令的維護行爲。由於D命令的循環範圍固定爲一整個SCRIPT循環(正如上面壓縮重複行的示例,每次都回到SCRIPT的下一個循環從頭開始),而"s"命令藉助標籤跳轉能夠實如今任意大小的範圍內循環。所以,"D"命令實現窗口滑動時,在通用性上不及"s"命令。

仍然以輸出倒數10行數據爲例。藉助"D"實現的語句以下:

#!/usr/bin/sed -nf

1h
2,10H
10g
$p
10,${N;D}

其中前3行只是爲了填充一個10行的窗口放在模式空間中(填充窗口的方法實在不少,因此只要知道這3步是幹啥的就行),從第11行開始這3行就沒有任何做用了。重點在最後一行的10,${N;D},這是"NPD"的絕佳組合之一,經過"N;D"輕鬆地維持了一個固定大小的窗口:讀一行刪一行。直到最後一行被"N"讀取,才被"$p"輸出。之因此"$p"要放在"D"的上一行,是由於"D"老是會回到SCRIPT的頂端,因此它後面的命令是不會執行的。

2.4 真正的大招

呀,原來窗口這麼好用?可是不要鑽入了sed的牛角而矇蔽了雙眼。說實話,經過sed自身實現不少較爲複雜的需求並不那麼簡單,費神又費力,反而結合其餘文本處理工具來實現要簡單的多的多。

就以上面的例子來講,輸出倒數10行,結合shell變量簡單的不得了。

total=`wc -l <filename`
sed -n $((total-9))',$p' filename

這樣想輸出任意哪些行號的行均可以簡單至極。以上只是一個示例,結合其餘可用的工具,一樣能輕鬆地實現sed自身比較複雜的需求。所以,這是最終的"大招"。

另外,上面的示例中引號加的位置很奇怪,這是sed結合shell的難題。不少人可能都遇到過這個問題,在網上也沒有很好的解釋,由於這不是sed的問題,而是shell解析的特性。見sed修煉系列(四):sed中的疑難雜症

2.5 維持窗口方法論

綜合上面幾個示例,維持窗口分爲兩種狀況:

  1. 在模式空間中維持窗口大小。這分爲兩個過程:
    • (1)填充窗口到指定行數;
    • (2)滑動窗口。
  2. 藉助保持空間暫存窗口。
    • (1).不斷填充保持空間的窗口;
    • (2).在填充到指定大小後,將窗口拉回模式空間進行滑動;
    • (3).滑動後將其覆蓋回保持空間暫存下來。
    • (4).繼續讀取並填充保持空間中的窗口,而後拉回模式空間,滑動後再暫存。

看上去,第一種狀況比較容易些。確實如此,第一種狀況不用屢次考慮兩個buffer之間的數據交換。而且,第一種狀況的效率更高,由於在達到窗口大小以後,再也無需和保持空間交換數據。

3.最佳搭檔:"N"、"P"和"D"命令

通過前面窗口滑動的示例,也能發現"N"和"D"是一個絕佳搭檔,它倆配合能實現完美的窗口滑動,並且相比於藉助保持空間暫存窗口的方法,它倆的邏輯很是清晰。

前面說了"N"和"D"的結合,加上"P"呢?單獨的"N"和"P"結合沒什麼好說的,可能出現的情形太多了。

但"P"和"D"的結合卻有一層固定的意義:根據匹配模式判斷是否輸出多行模式中的第一行,而後踢掉該行,並回到SCRIPT循環頂端。這極可能是在維護一個大小爲兩行的窗口。格式一般爲:

[Address]P;D

再同時結合"N",做用就更明顯了,維持一個窗口,並判斷是否要輸出該窗口的第一行,而後滑動窗口。格式一般爲:

[Address1]N;[Address2]P;D

不少時候,Address是省略掉的。P命令前的Address徹底是條件判斷語句,判斷是否要輸出。N命令前的Address1若是存在,最大的多是"$!",這表示當最後一行已被讀取過(不管是sed循環自動讀取的、n命令讀取的仍是N命令讀取的),直接跳過該命令。若是不加"$!",則沒有下一行可供讀取時,將直接輸出模式空間(除非指定了"-n")並退出sed程序。

是否在N前加"$!",對結果的影響很大。但想判斷是否要加,難度仍是挺大的,至少要對sed什麼時候輸出模式空間內容瞭如指掌。能夠閱讀sed修煉系列(一):花拳繡腿之入門篇

最後,須要說的是"N"和奇偶數行的關係。"N"命令在沒法讀取下一行時將輸出模式空間內容並退出sed,萬一讀到的最後一行是奇數行,又或是偶數行,會怎樣影響結果呢?可能不少人(包括我本身)都琢磨過這個問題,也被這個問題困惑了好久而不得其解,這個問題甚至放進了info sed手冊的Bug報告段中進行專門的解釋。

其實根本不用考慮最後一行是奇數行仍是偶數行,不管最後一行是奇數行仍是偶數行,sed根本無論這個邏輯,它只記得最後一行是否被讀取過,若是讀取過,則在N命令處就退出sed。因此,真正須要考慮的是N命令前是否要加"$!",它決定了sed是在N處退出仍是繼續執行後面的命令。但有一種狀況必需要考慮奇偶性,當"N"結合了其它讀取行的操做(命令"n"或sed的自動讀取)時,由於其他任意一個讀取動做都會改變"N"讀取的行的奇偶性。

例如,分別輸出輸入流的奇數行和偶數行。考慮以窗口模型實現的話,這是很簡單的。

seq 1 10 | sed 'N;P;d'       # 輸出奇數行
seq 1 10 | sed '1!{N;P};d'   # 輸出偶數行
seq 1 10 | sed -n '1!{N;P;d}'  # 輸出偶數行,但須要考慮奇偶性
seq 1 10 | sed -n '1!{$!N;P;d}'  # 輸出偶數行,不需考慮奇偶性

前兩個命令都很容易理解,第三個命令卻須要考慮奇偶性。由於窗口是從第二行開始填充的,因此窗口數據被"d"刪除前,其內第2行老是奇數行,例如"(2,3)"是一個窗口,"{4,5}"是一個窗口。當最後一行是奇數時,其一定是被"N"讀取的,這不會影響結果。但若是最後一行是偶數,則此行一定是被sed自動讀取的,使得sed在"N"命令處就結束了,其後的"P"就沒法執行。這時,能夠考慮在"N"前加上"$!",即第四條命令。

固然,更簡單的方法以下:

seq 1 10 | sed 'n;d'
seq 1 10 | sed '1!n;d'

再例如,要刪除文件的倒數第2行。若是知道窗口的概念,這一切都很容易:維持一個2行的窗口(N和D就夠了),當發現最後一行被讀取後,不輸出其前一行便可。如下是實現語句:

sed 'N;$!P;D' filename

注意這裏的"N"在處理最後一行時的做用,它輸出了最後一行,且讓sed程序結束,但這一行是自動輸出的,而非"P"輸出的,因此會受"-n"影響。若是改成"$!N",則最後一行被讀取後,將直接連續執行兩次"D",即同時刪除了倒數2行。

相關文章
相關標籤/搜索