Vim 對特定行處理常用方法(四):刪除、壓縮重複行

目錄

  • 奇偶行刪除

  • 合併奇偶行

  • 奇偶行分離(及寄存器入門)

  • 刪除、壓縮重複行(本文)

  • 跨行操作及長文本分割基礎

4. 刪除重複行

在這一篇中,我們將會來看看如何用 vim 來處理文本中的重複行。

注意,對於大部分高度格式化的文本,比如從手機導出的通訊錄(常見為 cvs 格式)、一些數據文件、配置文件,如果只有單一的「刪除重複行」需求,我都建議各位使用 uniq、awk、sed 等專門的工具來處理。 vim 更適合處理格式化程度低、需求多樣的文本。或者是你懶得保存然後刪除一個文件的時候。

另外,本篇中我們將要了解 vim 的 pattern(模式),vim 是根據 pattern 來匹配文本的,而「正則表達式」是這其中的核心之一。我並不會在例子中使用什麼複雜的正則表達式,但也不會對正則表達式作太多說明。在實際使用中,各位會碰到需要使用更複雜的正則表達式的情況,這需要大家自行學習解決。

另外,在本篇的 4.3 和 4.4 節,我們會開始接觸到一些 vim 的高級功能,比如表達式替換、內部函數、外部命令等等。這些內容對於初學者,或者是的編程毫無概念的朋友難於理解。但各位不必糾結,我提及到這些內容,是希望大家知道 vim 還有這樣的可能性,當在將來遇到更高的需求時,可以回想起還有這樣的解決方案。

** 本篇命令 **nn4.1 無序重複行nn:sort u n 對所有行進行排序,並且只保留完全相同的行的第一行。是 vim n 自帶的一個去除重複行命令,對行的順序沒有要求的時候可以直接n 使用。但是多數情況下這個簡單的命令都無法滿足我們的需求。nn4.2 連續重複行nn:g/^$n^$/dn 刪除連續重複的空白行,最終只保留 1 個空白行。nn:%s/(^$n){2,}/11n 壓縮 2 個以上的練連續空白行為 2 個空白行。nn:g/(^[[:space:] ]*n){2,0}/dn 刪除混有空白字元的空行,最終保留 1 個行。nn:%s/v(^[[:space:] ]*n)+/r n 刪除混有空白字元的空行,最終保留 1 行,同時把不規範的空行n 換為只有換行符的空行。nn:g/(^.*)n1/dn 利用捕獲組和引用來刪除連續的重複行,只保留最後一行。n 通過不同的 {pattern} ,就可以對特定的行進行處理。nn:g/%(^1n)@<=(.*)$/dn 利用了零寬斷言的逆序環視 @<= ,和上述 g 命令的區別在於這         個命令會保留連續重複行的第一行。對於不熟悉正則表達式的朋友n 來說這個寫法並不容易理解,同時,經過我的實測,在 MacVim n 7.4 中這個寫法並不能運作,問題出在逆序環視上面,所以不推薦n 使用這個方法。nn:%s/(^.*)(n1)+/1n 與上面的 g 命令效果一致,不同之處在於稍經改造就能用於壓縮n 重複行,而不光是刪除重複行。同樣可以改用 v 的寫法。nn:%s/(^.*)(n1)+/1r1n 把兩個以上的重複行壓縮為兩個重複行nn4.3 不連續重複行nn:%s/^/=line(.). 在每一行前面添加序號n:sort /d+ / 跳過序號,對文本本身進行排序( 4.1 節)n:g/^d+ (.*)n^d+ 1/d 把重複行去除( 4.2 節)n:sort n 最後剩下的行按序號排列n:%s/^d{-} // 把序號刪除掉nn4.4 外部命令及替代nn:%!awk !a[$0]++ 調用 awkn:%!perl -ne print if !$a{$_}++ 調用 Perln 對外部命令中的 ! 注意用 轉義nn:let a={} 新建空的字典變數 ann:g/^/exe has_key(a,getline(".")) ? "d" : "let a[".getline(.)."]="n Vim Language 的替代方法n 這條命令很長,有 71 個字元,注意n 拖動滾動條查看完整的命令。nn ** 本篇幫助 **nn4.1n:h :sortnn4.2n:h /^ n:h /$n:h /nn:h /()n:h /{n:h /1n:h /{n:h /multin:h /rn:h /[]n:h /.nn4.3n:h :s=n:h line()n:h expr-.nn4.4n:h range!n:h Dictionaryn:h :executen:h has_key()n:h getline()n:h expr1n

4.1 無序重複行

這種屬於純去重的需求,通常而言是不需要特意打開文本編輯器來處理的。但是這對於更複雜的去重來說是基礎知識之一,應該知道要怎麼做,在處理一些更複雜的需求時會用到。

下面我們來看一個簡單的實例:

王朗n來者可是諸葛孔明?n諸葛亮n正是。n王朗n久聞公之大名,今日有幸相會!公既知天命,識時務,為何要興無名之師?犯我疆界?n諸葛亮n我奉詔討賊,何謂之無名?n王朗n天數有變,神器更易,而歸有德之人,此乃自然之理。n諸葛亮n曹賊篡漢,霸佔中原,何稱有德之人?n

這是從第一篇中的實例修改過來的例子,形式上是一個劇本。雖然實際上只有 2 個角色,但請大家想像這是一個有很多角色的劇本。現在,我們需要得到一個角色清單,該怎麼做呢?

第一步,我們用上第一章的知識,刪除掉偶數行,只保留角色。

王朗n諸葛亮n王朗n諸葛亮n王朗n諸葛亮n

如果角色清單是無關順序的,那麼你就可以簡單地使用一個命令來處理:

:sort un:h :sortn

sort 是 vim 的一個排序命令,這個命令可以對 [range] 內的行進行排序,默認情況下是對所有行。這個命令有很多參數,u 是其中之一。加上 u 後,vim 在完成排序後只會保留完全相同的行的第一行,從而達到去重的目的。

4.2 連續重複行

這一類的情況多數出現在數據記錄之類的文檔中,文章一類的文檔似乎並不常見連續的重複行,但是其實文章中有一中連續重複行是我們經常看到的,那就是連續的空白行。

由於寫作習慣不規範、多次複製粘貼之類的原因,我們經常能看到連續的空白行:

王朗n來者可是諸葛孔明?nnn諸葛亮n正是。nn王朗n久聞公之大名,今日有幸相會!公既知天命,識時務,為何要興無名之師?犯我疆界?nnn諸葛亮n我奉詔討賊,何謂之無名?nnn王朗n天數有變,神器更易,而歸有德之人,此乃自然之理。nnnn諸葛亮n曹賊篡漢,霸佔中原,何稱有德之人? n

對於空白行 ,我們可以有一個簡單的方法來進行處理:

:g/^$/dnn:h /^ n:h /$n

上述兩個命令都能刪除掉所有空白行。^ 代表一行開始,$ 代表一行的結束一行的開始和結束之間沒有任何內容,自然就是個空行。

然而,這個命令會刪除掉所有的空行,無法只去除掉重複的空行,保留獨立的空行。

這就是本篇的重點了,怎樣來通過 {pattern} 來識別連續行。

先觀察這兩行:

窗前明月光, n疑是地上霜。 n

就這樣看,他們之間是獨立分隔的,但是,兩行之間實際上只是相隔了一個換行符:

床前明月光,n疑是地上霜。nn:h /nnn"注意:把 n 描述為一個「換行符」實際上是不準確的,關於換行符的問題並沒有那n么簡單,這裡有很多技術上的細節和不同系統間的差異。作為一個入門的應用,各位沒n有必要做那麼深入的了解。有興趣的朋友可以自行 google「vim n r 區別」來n作進一步了解。n

換行符的意義在於告訴程序,在這個地方需要進行換行,因此程序顯示出來的時候,就是獨立的兩行了。

換言之,當我們需要匹配到兩行的時候,只有在 {pattern} 中加入 n 就可以了。以下這個 {pattern} 就能匹配到上面的例子:

/床前明月光,n疑是地上霜。 n

回到空白行,我們就可以知道怎樣匹配連續的空白行了:

:g/^$n^$/dn

上面的那個命令會首先匹配到任意兩個空白行,然後刪除掉第一個空白行。所以不管是連續兩個的空白行,還是連續 n 個空白行,最後都會只保留一個。具體是如何實現的,可以參照第一篇中關於 global 的標記的說明。

但是,如果我希望保留的不是一行,而是兩行、三行或者其他,那麼上面這個簡單粗暴的命令就沒法用了。我們需要用 substitue 來代替,這個命令通常簡寫為 s

:%s/(^$n){2,}/11 "把所有空行統一變成兩個空行nn:h /()n:h /{n:h /1n

在這條替換命令裡面,我們接觸到了三個新東西,分別是 (){2,}1

()1 是配套的,1 相當於一個寄存器,但是這個寄存器只在這一條命令中有效。()中的內容歸為一個「捕獲組」,第一個捕獲組的內容放到 1 中,第二放到 2 中,如此類推,最多到 9,而 0 表示全部內容,不管是不是在 () 中。

下面的例子幫助了解:

/a((bcd)e)(fg)nn0 -> abcdefgn1 -> bcden2 -> bcdn3 -> fgn

一個簡單的方法是:從左算起,第一個 ( 對應 1,第二個 ( 對應 2 ,如此類推。

{2,} 是用於 {pattern} 的修飾符號,表示倍數項,意思是匹配 2 個以上匹配原,儘可能多:

/a{2,} " 匹配 aa 、aaa、aaaa 等,但不會匹配到 a 。nn:h /{n:h /multin

因此

/(^$n){2,}n

相當於

/^$n^$n^$n^$n^$n^$n^$n...n

當然我們是不可能這樣寫的,所以倍數項是必須的。還有很多不同的倍數項修飾符,可以在幫助中詳細了解。

在上面的命令中,是讓 2 個或以上的空行變成 2 個空行,1 個空行的不變。如果還要匹配到 1 個空行,那麼修飾符就能改為 + ,或者 {1,}

因此,刪除連續空白行的命令

:%s/(^$n)+/1n

的一個等效寫法是

:%s/(^$n){1,}/rnn:h /rn

r 是換行符,使用在替換中相當於 n ,具體的原因上面有提到,比較複雜,有興趣的朋友自行 google 了解。在使用上,只需要記住:

  • 查找換行符用 n
  • 替換為換行符用 r

上面的命令可以讓我們處理真正的空白行,但是,有的空白行並不是只有換行符,而是混雜了一些空白字元:

王朗n來者可是諸葛孔明?nntt" 這行有 2 個 tabn " 這行有 16 個半形空格n        " 這行有 8 個全形空格n諸葛亮n正是。n

這類空白行不光數量不等,而且經常還會混雜有半形空格、全形空格甚至 tab 。嚴格意義上這些行並不是「重複行」,因為它們的內容是不一樣的,所以簡單的方式還處理不了。

這裡我們就要用到 [] 了。

/^[  t]*n " [] 中包含了一個空格、一個全形空格和一個 tab 。n:h /[]n

[] 是正則表達式中的一個元字元,特性上和其他平台的正則表達式基本一致,具體用法幫助寫得很清晰了。匹配一個字元,這個字元必須是 [] 中的任意一個,否則就匹配不到。因為 [] 中可以寫很多內容,所以常常有人誤解以為 [] 可以匹配多個字元。這裡必須記清楚了:一個 [] 只能匹配一個字元。

* 也是個倍數項,表示 0 或更多過匹配原,相當於 {0,} 。這裡的意思就是,n 前面可能有各種空格、tab 的組合,也可能沒有。

一個等效的寫法是:

/^[[:space:] ]*nn

[:space:] 是用於 [] 中的字元類表達式,相當於 s ,代表空格和 tab ,當然還有其他的等效寫法,比如使用 s| 代替 [] ,通過幫助,各位可以改造出適合自己使用的 {pattern}

結合我們前面刪除連續重複空白行的命令,我們就有了刪除這種連續「不重複」空白行的命令:

:g/(^[[:space:] ]*n){2,0}/dn:%s/v(^[[:space:] ]*n)+/r "同時把不規範的空行換為只有換行符的空行n

現在我們已經抓到了處理連續重複行的訣竅:

  1. 寫出匹配重複行的 {pattern}
  2. 用 n 連接兩個 {pattern}
  3. 找到重複的部分,加上倍數項,就能用 substitue 進行壓縮

下面就是處理文本中任意連續重複行的命令:

:g/(^.*)n1/dn:%s/(^.*)(n1)+/1nn:h /.n

通過上面的講解,這兩個命令應該很容易理解了。.* 是正則表達式中常見元字元搭配,由匹配「任意單個字元」的 . 和「 0 或更多個」的 * 倍數項組成,指「任意內容」。

但是,這兩個命令是無法混雜了多種空白字元的重複行的,因為那些重複行實質上並不相同。

4.3 不連續重複行

在這一節,我們要來玩一個高端點的東西——替換表達式,用來解決「有序不連續的重複行」的問題。

在前面兩節,我們了解到了連續重複行的處理方式,這基本上能夠滿足日常的使用需要。但是,在某些特殊的情況下,我們還是需要用到處理不連續重複行的技巧。比較典型的就是在某些內容提取需求上面。

王朗n來者可是諸葛孔明?n諸葛亮n正是。n王朗n久聞公之大名,今日有幸相會!公既知天命,識時務,為何要興無名之師?犯我疆界?n諸葛亮n我奉詔討賊,何謂之無名?n王朗n天數有變,神器更易,而歸有德之人,此乃自然之理。n諸葛亮n曹賊篡漢,霸佔中原,何稱有德之人? n

這是上面在前面出現過的一個劇本例子,現在,我們需要以此為基礎製作一個人物登場列表。

意思是就說,我們需要把劇本中所有出現的人物都提取出來,並且去除重複項,而且順序還不能亂。

我們是無法用簡單的正則表達式來完成這項工作的,因為人物名字的出現是不規律的。

很多朋友可能會想到用 Vim Language 來寫這樣的程序(實際上不需要一個「程序」,後面會提到),或者是通過外部的 Python、Perl 或者 Lua,也能利用 awk 這樣的文本流處理工具來達成,Vim 對這些腳本語言或者工具都有很好的支持,這項工作做起來也不算太難。當面對比較大的文本(視乎你的內存,一般 8G 左右的個人電腦不建議操作超過百兆的文檔),我也更推薦這種方式。

但是,如果只是一個只有幾萬行,甚至只有幾千行,體積不過幾兆的小文本,還要另外寫個腳本就顯得小題大做了,我們可以在 vim 內部就搞定:

:%s/^/=line(.). n:sort /d+ /n:g/^d+ (.*)n^d+ 1/dn:sort nn:%s/^d{-} //nn:h :s=n:h line()n:h expr-.n

上面 5 條命令很簡單:

  1. 在每一行前面添加序號
  2. 跳過序號,對文本本身進行排序( 4.1 節內容)
  3. 把重複行去除( 4.2 節內容)
  4. 最後剩下的行按序號排列
  5. 把序號刪除掉

唯一比較新鮮的就是第一條命令的「替換表達式」。

這個特性的作用就是把匹配到的內容替換為一個表達式的結果。利用這個技巧,可以靈活地添加序號、運用變數、運用運算、調用函數等,為查找替換增加了更大的可能性。

在這裡,我們調用了行數 line() line(.) 會返回當前行的行號。比如當替換命令運行到第 10 行的時候,line(.) 的值就是會是 10 。然後在行號後面,我們添加了一個空格,用點號來鏈接。以點號來鏈接不同的表達式,是固定的寫法。空格是字元串,所以要用兩個引號來包圍。

替換表達式能夠做的事情還有很多很多,我們日後還會遇見到,大家也可以根據需求自由發揮。

4.4 外部命令及其替代

覺得上面的 5 條命令過於繁瑣嗎或者過於緩慢嗎?那麼是時候來利用一下外部命令提速了。

我們前面提到,vim 與 Perl、Python 之類的腳本語言配合良好,而且也能輕易調用 awk、sed 等工具。如果你使用的是 *nix 系統,或者在 Windows 中配置好了以上的工具,那麼你就能在 vim 中通過一句簡單的命令,完成上面的複雜工作,以 awk 和 Perl 為例:

:%!awk !a[$0]++n:%!perl -ne print if !$a{$_}++nn:h range!n

沒錯,就是這麼簡單。各位可以在 vim 中方便地使用平日慣用的工具。

記得在命令中的 ! 前加上反斜杠轉義,不然會出錯。

如果你不幸沒有這些工具,或者覺得暫時沒有必要學習一個新工具,vim 也有一個使用同樣思路的解決方案,只是稍微要多打一點字:

:let a={}n:g/^/exe has_key(a,getline(".")) ? "d" : "let a[".getline(.)."]="nn:h Dictionaryn:h :executen:h has_key()n:h getline()n:h expr1n

這條命令用到了 vim 的字典變數,配合表達式的 if-then-else 語法來作判斷。用中文來表達就是:

  • 創建一個空的字典變數 a 。
  • 對每一行進行掃描,如果當前行沒有在 a 中,那麼就加入到 a 中,設值為空。(可以為任意值,無影響)
  • 如果當前行在 a 中,那麼就刪除掉。

這一部分,對於初學者而言可能比較難理解了。如果你無法理解上面的命令,不需要著急, 4.3 中的命令已經完全可以滿足一般的使用。可以等你對效率或者簡潔性有了更高追求的時候, 再過來看上面的方法。

本篇命令及幫助回顧

** 本篇命令 **nn4.1 無序重複行nn:sort u n 對所有行進行排序,並且只保留完全相同的行的第一行。是 vim n 自帶的一個去除重複行命令,對行的順序沒有要求的時候可以直接n 使用。但是多數情況下這個簡單的命令都無法滿足我們的需求。nn4.2 連續重複行nn:g/^$n^$/dn 刪除連續重複的空白行,最終只保留 1 個空白行。nn:%s/(^$n){2,}/11n 壓縮 2 個以上的練連續空白行為 2 個空白行。nn:g/(^[[:space:] ]*n){2,0}/dn 刪除混有空白字元的空行,最終保留 1 個行。nn:%s/v(^[[:space:] ]*n)+/r n 刪除混有空白字元的空行,最終保留 1 行,同時把不規範的空行n 換為只有換行符的空行。nn:g/(^.*)n1/dn 利用捕獲組和引用來刪除連續的重複行,只保留最後一行。n 通過不同的 {pattern} ,就可以對特定的行進行處理。nn:g/%(^1n)@<=(.*)$/dn 利用了零寬斷言的逆序環視 @<= ,和上述 g 命令的區別在於這         個命令會保留連續重複行的第一行。對於不熟悉正則表達式的朋友n 來說這個寫法並不容易理解,同時,經過我的實測,在 MacVim n 7.4 中這個寫法並不能運作,問題出在逆序環視上面,所以不推薦n 使用這個方法。nn:%s/(^.*)(n1)+/1n 與上面的 g 命令效果一致,不同之處在於稍經改造就能用於壓縮n 重複行,而不光是刪除重複行。同樣可以改用 v 的寫法。nn:%s/(^.*)(n1)+/1r1n 把兩個以上的重複行壓縮為兩個重複行nn4.3 不連續重複行nn:%s/^/=line(.). 在每一行前面添加序號n:sort /d+ / 跳過序號,對文本本身進行排序( 4.1 節)n:g/^d+ (.*)n^d+ 1/d 把重複行去除( 4.2 節)n:sort n 最後剩下的行按序號排列n:%s/^d{-} // 把序號刪除掉nn4.4 外部命令及替代nn:%!awk !a[$0]++ 調用 awkn:%!perl -ne print if !$a{$_}++ 調用 Perln 對外部命令中的 ! 注意用 轉義nn:let a={} 新建空的字典變數 ann:g/^/exe has_key(a,getline(".")) ? "d" : "let a[".getline(.)."]="n Vim Language 的替代方法n 這條命令很長,有 71 個字元,注意n 拖動滾動條查看完整的命令。nn ** 本篇幫助 **nn4.1n:h :sortnn4.2n:h /^ n:h /$n:h /nn:h /()n:h /{n:h /1n:h /{n:h /multin:h /rn:h /[]n:h /.nn4.3n:h :s=n:h line()n:h expr-.nn4.4n:h range!n:h Dictionaryn:h :executen:h has_key()n:h getline()n:h expr1n

推薦閱讀:

如何讓 vim 成為我們的神器
如何使用 Vim ?
如何選購 Vim 腳踏板?
Vim基礎篇——快速移動
在 vim 中使用 git 的一些經驗

TAG:Vim | 文本编辑器 | 文本处理 |