資料預處理

你是否有過變換資料形式的需求?當然有過!這也是此課會講授的內容。 具體來說,我們需要對文字或二進位形式的資料不斷處理,直至獲得我們所需的內容。

在之前部分的課程中,我們已經遇到過一些基礎的資料處理例項,比如當你使用 | 時,已經是在使用基本形式的資料處理了。 考慮這樣一個指令 journalctl | grep -i intel。它會搜尋所有提到Intel(大小寫敏感)的系統日誌。 你或許不認為這是資料處理,但這將資料從一種形式(全部系統日誌)轉換成了另一種更有價值的形式(僅含intel的日誌)。 大部分資料處理工作需要我們理解如何組合並使用工具來達成目的。

讓我們從頭說起。實行資料處理,需要兩個條件:用來處理的資料,以及處理的情境。 日誌處理是一個常見的情景,因為我們常常需要在日誌中尋找資訊,此時閱讀所有日誌是不現實的。 讓我們通過檢視日誌來找出有誰曾試圖登入我們的伺服器:

ssh myserver journalctl

內容太多了,讓我們只看與ssh相關的:

ssh myserver journalctl | grep sshd

請注意我們在此處通過 grep 來使用管道,將 遠端的 檔案傳送至本地端電腦! ssh 非常神奇,我們會在下一課的命令列環境中詳細講授。 此時傳回的內容依然冗長,且不好閱讀。讓我們做點改善:

ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' | less

為什麼要使用雙層引用呢? 我們檢視的日誌非常多,從遠端傳送至本地端再濾掉有些浪費。 作為替代,我們可以在遠端就濾掉一部分,然後將其結果傳送至本地端。 less 建立了一個 「分頁機制」 來允許我們通過滾動頁面來閱讀長文字。 如果想節省傳送流量,我們甚至可以將過濾後的日誌寫入檔案,使得我們不需要再次通過網路傳送:

$ ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' > ssh.log
$ less ssh.log

此時的結果依然有許多無用的部分。 我們有 很多 方法來更優化。首先讓我們熟悉一下最強的工具之一: sed.

sed 是一個基於舊式 ed 文字編輯器的 「流編輯器」 (stream editor)。 在其中,我們可以使用簡短的指令來更改檔案,而非直接編輯檔案內容(雖然我們也可以這樣做)。 指令有很多,但最常用的是 s: 替換。 例如,我們可以:

ssh myserver journalctl
 | grep sshd
 | grep "Disconnected from"
 | sed 's/.*Disconnected from //'

我們剛剛寫了一段 正規表達式 ; 正規表達式允許我們匹配符合特定句法的字串。 s 命令的使用方式是這樣: s/REGEX/SUBSTITUTION/, 其中 REGEX 是我們需要匹配的正規表達式, SUBSTITUTION 是用於替代匹配結果的字串。

正規表達式

正規表達式很常見也很有用,值得用些時間去理解它。 讓我們從上面使用過的字串開始: /.*Disconnected from /. 正規表達式通常 (也有例外) 被 / 包圍。 多數 ASCII 字元代表其自身的含義,有些則擁有「特別的」含義。 由於正規表達式實現方法的不同,這些符號也常常有不同的含義,這讓我們感到有些挫折。 常見的表達式有:

<!–

sed 的正規表達式有些古怪,需要你在這些表達式前使用 \ 來使它們擁有這些特殊含義。 也可以使用 -E 達成同樣效果。

讓我們重回 /.*Disconnected from /, 可以看出它會匹配以任意字元起始,緊接 “Disconnected from” 的字串,這就是我們需要的。 但要小心,正規表達式時而有些棘手。 試想某人嘗試使用 “Disconnected from” 作為使用者名登入,我們將會有:

Jan 17 03:13:00 thesquareplanet.com sshd[2631]: Disconnected from invalid user Disconnected from 46.97.239.16 port 55920 [preauth]

我們會得到怎樣的結果?實際上,*+ 預設是「貪婪的」。它們會匹配所有可能的字元。 所以,上述字串會得出這樣的匹配結果:

46.97.239.16 port 55920 [preauth]

這並不是我們需要的。在一些正規表達式的實現中,我們可以單純給*+ 加入 ? 字尾使其轉變為非貪婪模式。只可惜,sed 並不支援這種方式。 我們 可以 換用 perl 的命令列模式,它 支援 這樣的結構:

perl -pe 's/.*?Disconnected from //'

我們會使用 sed 完成後續的工作,因為它是最常見的工具。 sed 也善於完成類似於印出匹配後的字串,一次使用就完成多次替換等工作。 但是我們課中不會講解太多。 sed 比較萬能,但是對於特定細小功能往往有更優秀的工具。

好了,現在我們依然有需要去除的字尾,我們應該如何處理? 僅匹配使用者名後的文字有些困難,當使用者名字含有空格時更是如此! 我們此時應做的就是匹配 整句話

 | sed -E 's/.*Disconnected from (invalid |authenticating )?user .* [^ ]+ port [0-9]+( \[preauth\])?$//'

讓我們用regex debugger來看看會得出怎樣的結果。 起始的部分和之前是一樣的。然後,我們匹配出所有型別的 「user」 變體(在日誌中有兩種不同字首)。 接下來我們匹配屬於使用者名稱的所有字元。 再之後,匹配任意一個單詞([^ ]+; 匹配由任意非空格組成的非空字串)。 此時,再匹配 「port」 與接續的一串數字,和可能存在的字尾 [preauth], 直至行尾。

採用這種方法,即使使用者名是 「Disconnected from」,我們也可以匹配正確內容,你能理解其原理嗎?

還剩下一個問題需要解決,就是日誌的全部內容都被替換成了空字串。 我們希望能 留下 匹配到的使用者名。 為了達成此目的,我們可以使用 「捕捉群」(capture groups)。 被括號包圍的正規表達式會被按順序存入捕捉群,而捕捉群的內容可以在替換時取用(在有些正規表達式實現中,替換內容甚至支援正規表達式本身!)。 使用 \1, \2, \3 以此類推來取用其內容:

 | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'

你大概可以想象到,我們有時需要 真的非常 複雜的正規表達式。 例如,此處有文章介紹如何匹配電子郵件, 這實際上很困難。 人們對其進行了很多討論, 還寫出了許多測試用例,與test matrices。 我們甚至還能用正規表達式判斷一個數是否為質數

正規表達式可是相當難以確保正確的,但是他們依然十分好用!

回到資料處理

現在我們可以寫出:

ssh myserver journalctl
 | grep sshd
 | grep "Disconnected from"
 | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'

sed 還有能力做到許多有趣的事情,比如注入文字(使用 i ),印出指定列(使用 p ),或者利用index搜尋等。我們可以使用 man sed 檢視更多能做的事情!

總之,我們現在已經獲得了由曾經嘗試登入的使用者名組成的表,不過這還用處不大,讓我們看看有哪些人時常嘗試登入:

ssh myserver journalctl
 | grep sshd
 | grep "Disconnected from"
 | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
 | sort | uniq -c

sort 將會,如同字面意思,對他排序。 uniq -c 會合併連續的行並寫出出現次數。 我們希望能按照出現次數排序,來找出最常登入的使用者:

ssh myserver journalctl
 | grep sshd
 | grep "Disconnected from"
 | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
 | sort | uniq -c
 | sort -nk1,1 | tail -n10

sort -n 將會按照數字排序 (預設是按照字母順序) 。 -k1,1 表示 「按照被空格分隔的第一行排序」。 ,n表示 「僅排序前 n 個部分」 預設是醬所有部分都排序。 在這個 典型的 例子中,排序所有部分沒有任何問題,我們只是為了講授 ,n 的用法才使用了這個!

如果我們想找到 最不常見 的登入者,我們可以使用 head 替換 tail,或者使用 sort -r, 它可以反向排序。

結果已經非常好了,但我們只想要使用者名稱,而且不要一列只寫一個。

ssh myserver journalctl
 | grep sshd
 | grep "Disconnected from"
 | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
 | sort | uniq -c
 | sort -nk1,1 | tail -n10
 | awk '{print $2}' | paste -sd,

paste 允許我們指定一個分隔符(-d)來合併列(-s)。 不過這個 awk 是做什麼的?

awk – 另一種編輯器

awk 是一種非常善於處理文字的程式語言。awk非常多 可以學習的功能,不過此課我們只介紹一點基本知識。   首先,{print $2} 有什麼作用? awk 程式可以接受一個選項串和一個指令區塊,指出當匹配到內容時應該做出什麼動作。 在指令碼塊中 $0 代表整列的文字, $1$n 代表此列中第 n欄位 , 這些 欄位awk 域分隔符劃分(預設是空格, 可以用過 -F 指定)。 在這個例子中,這些程式碼的意思是 印出每列第二個部分,也就是使用者名!

讓我們看看還能做到什麼奇異功能。讓我們找出所有以 c 起始, 以 e 結尾,且只嘗試過一次登入的人:

 | awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }' | wc -l

這裡有許多要講解。首先,請注意我們指定了表達式(也就是 {...} 前面的那些)。 這個表達式要求此列文字的第一部分必須是 1 (這部分是 uniq -c 取得的), 並且第二部分必須匹配給定的表達式。 指令碼塊中的內容則表示印出使用者名。 接下來我們使用 wc -l 統計輸出結果的列數。

別忘記 awk 是一種程式語言:

BEGIN { rows = 0 }
$1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 }
END { print rows }

BEGIN 是一種匹配起始輸入 (and END 匹配結尾)的模式。 然後,對每列第一部分相加(本例中此部分均為 1 ), 再印出最後結果。 實際上,無需 grepsed,因為 awk 可以解決所有問題。 至於如何做到,可以參考課後練習。

分析數據

我們也可以做些計算!例如,將每列數字相加:

 | paste -sd+ | bc -l

也可以使用更加複雜的方法:

echo "2*($(data | paste -sd+))" | bc -l

我們可以透過多種方式獲取統計數據。 st 是個不錯的選擇,不過如果你已經安裝了R:

ssh myserver journalctl
 | grep sshd
 | grep "Disconnected from"
 | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
 | sort | uniq -c
 | awk '{print $1}' | R --slave -e 'x <- scan(file="stdin", quiet=TRUE); summary(x)'

R 是一種 (古怪的) 程式語言,適合用來進行資料分析或繪圖。 我們不會介紹太多,只需知道 summary 會印出關於矩陣的統計結果。 我們從輸入的數字中計算出一個矩陣,R會給出分析結果。

我們可以輕鬆藉助 gnuplot 來繪製圖表:

ssh myserver journalctl
 | grep sshd
 | grep "Disconnected from"
 | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
 | sort | uniq -c
 | sort -nk1,1 | tail -n10
 | gnuplot -p -e 'set boxwidth 0.5; plot "-" using 1:xtic(2) with boxes'

使用資料處理來獲取參數

有時我們需要利用資處理來從一個長表中找出想要安裝或反安裝的東西, 利用我們之前講授的知識配合 xargs 可以輕鬆實現:

rustup toolchain list | grep nightly | grep -vE "nightly-x86" | sed 's/-x86.*//' | xargs rustup toolchain uninstall

處理二進位

雖然截至目前我們都在處理文字,不過管道對於處理二進位資料也十分有效。 例如,我們可以使用ffmpeg來從相機中獲取影象,轉換為灰度,再進行壓縮,最後通過SSH傳送至遠端,在遠端解壓縮後存檔並顯示。

ffmpeg -loglevel panic -i /dev/video0 -frames 1 -f image2 -
 | convert - -colorspace gray -
 | gzip
 | ssh mymachine 'gzip -d | tee copy.jpg | env DISPLAY=:0 feh -'

課後練習

  1. 學習這篇 簡短的交互式正規表達式教學
  2. 統計words文件 (/usr/share/dict/words) 中包含至少三個a 且不以's 結尾的單字個數。這些單詞中,出現頻率最高的末尾兩個字母是什麼? sedy 命令,或者 tr 指令也許可以幫你解決大小寫的問題。共存在多少種詞尾兩字母組合?更進階的問題:哪個組合從未出現過?
  3. 進行原地替換似乎很實用,例如: sed s/REGEX/SUBSTITUTION/ input.txt > input.txt。但是這並不是一個明智的做法,為什麼呢?還是說只有 sed 是這樣的? 查看 man sed 來回答這個問題。
  4. 找出您最近十次開機的開機時間平均數、中位數和最長時間。在Linux上需要用到 journalctl ,而在 macOS 上使用 log show。找到每次起到開始和結束時的時間戳記。在Linux上類似這樣操作:
    Logs begin at ...
    

    systemd[577]: Startup finished in ...
    

    在 macOS, 查找:

    === system boot:
    

    Previous shutdown cause: 5
    
  5. 查看之前三次重新啟動訊息中不同的部分 (參考 journalctl-b 選項)。將這一任務分為幾個步驟,首先獲取之前三次啟動的日誌,也許獲取啟動日誌的命令就有合適的選項可以幫助您提取前三次啟動的日誌,亦或者您可以使用 sed '0,/ STRING/d' 來刪除 STRING 匹配到的字符串前面的全部內容。然後,過濾掉每次都不相同的部分,例如時間戳記。下一步,重複記錄輸入行並對其計數(可以使用 uniq )。最後,刪除所有出現過3次的內容(因為這些內容為重複的部分)。
  6. 在網路上找一個類似 這個這個這個的資料表格。用 curl 獲取表格並提取其中兩行數據,如果您想要獲取的是HTML資料,那麼 pup 可能會更有幫助。對於JSON類型的數據,可以試試 jq 。請使用一條指令來找出其中一行的最大值和最小值,用另外一條指令計算兩行之間差的總和。

Edit this page.

Licensed under CC BY-NC-SA.