工作中遇到的數據很多都是按照字段進行組織的,分隔符通常是制表符\t
,如下所示:
seg_A seg_B seg_C
...
...
...
這種格式可以組織各種數據,比如日志文件,訓練/測試數據,用戶行為數據等,這些數據都是字符串的形式,并且按照每行一個樣本,每列一個字段的形式進行儲存。linux中有很多工具和命令處理此類數據非常方便,這里簡單筆記下。
打印某一列
采用awk
可以做到。
cat your_file | awk -F "\t" '{print $1}' # 打印第一列
cat your_file | awk -F "\t" '{print $NF}' # 打印最后一列
查找字符串
采用sed
, grep
,大殺器awk
當然也可,不過沒必要。
cat your_file | sed -n '/string_pattern/p'
cat your_file | grep "your_string"
cat your_file | grep -E "string_pattern"
新增一列
采用awk
, 假如原本的文件如:
# file: your_file
1 2 3
4 5 6
7 8 9
那么在最后一列添加上字符A
,可以用以下腳本
cat your_file | awk -F "\t" '
BEGIN{string="A"}
{
for(i = 0; i < NF; i++)
{
printf($i);
printf("\t");
}
printf("%s\n", string);
}'
其輸出結果是:
1 2 3 A
4 5 6 A
7 8 9 A
我們發現awk
的可執行代碼非常類似于C語言,并且awk是對每一行為單位進行處理的,這意味著以上的代碼會對文件中的每一行進行相同的操作。
替換字符串
替代字符串這個操作可以由非常多的工具進行,比如sed
, tr
, 萬能的awk
等。個人喜歡用sed
。
cat your_file | sed -n 's/old_string/new_string/p'
大小寫字母轉換
tr
命令適合用于字符串的轉換,壓縮和刪除。
echo "AbC" | tr -t [A-Z] [a-z]
輸出為abc
數據篩選
有時候需要對每一列的某些數值指標(離散的或者連續的)進行篩選,可以采用awk
輕松搞定。原數據如:
# file: your_file
data_a data_b 1.0
data_a data_b 0.5
data_a data_b 0.8
data_a data_b 0.3
data_a data_b None
那么挑選所有最后一列大于0.5的行,可以
cat your_file | awk -F "\t" '$3>0.5'
輸出為:
data_a data_b 1.0
data_a data_b 0.8
data_a data_b None
為了篩出掉缺省值None
,也可以選擇同時篩選多個條件,通過與(&&)或(||)非(?。┻B接起來,如下面的第一條
cat your_file | awk -F "\t" '$3>0.5 && $3!="None"'
cat your_file | awk -F "\t" '$3>0.5 && $3<0.8'
cat your_file | awk -F "\t" '$3<0.5 || $3>0.8'
cat your_file | awk -F "\t" '$3!=0'
求每一行數字的加和并且求平均值
cat your_file | awk -F "\t" '
BEGIN{sumv=0;line=0}
{
sumv+=$2;
line++;
}
END{print sumv/line}
'
awk引用shell環境變量
有時候需要awk訪問shell環境變量,此時不能簡單用${VAR}
進行訪問,可以考慮以下兩種方式:
ENVVAR="..."
cat file | awk -v var="${ENVVAR}" '{print var}'
或者通過訪問內置的ENVIRON
變量實現
export ENVVAR="..."
cat file | awk '{print ENVIRON["ENVVAR"]}'
unset ENVVAR # 為了后續的數據引用安全,unset掉
awk文件間查重
有時候需要對兩個文件之間某個字段的重復部分進行篩選,然后對提取出重復字段的整行部分。比如我們有以下數據:
file_1.data
http://haokan.baidu.com/v?pd=wisenatural&vid=7217158148519997092 A
https://zhidao.baidu.com/question/2054086297537323627/answer/3367935303.html B
http://m.bilibili.com/video/BV1Zw411d7m6 C
https://haokan.baidu.com/v?pd=wisenatural&vid=6619006578881460543 D
http://haokan.baidu.com/v?pd=wisenatural&vid=5651466905742673647 E
file_2.data
https://haokan.baidu.com/v?pd=wisenatural&vid=7217158148519997092 A
http://zhidao.baidu.com/question/2054086297537323627/answer/3367935303.html B
http://v.qq.com/boke/page/t/0/w/t01451r5euw.html C
http://3g.163.com/v/video/V5KLT7ESE.html D
http://haokan.baidu.com/v?pd=wisenatural&vid=5651466905742673647 E
那么可以通過以下awk腳本進行:
awk -F " " 'FNR==NR{a[$1];next} $1 in a {print $1}' file_1.data file_2.data
那么會輸出:
http://haokan.baidu.com/v?pd=wisenatural&vid=5651466905742673647 E
其中的NR (Number of Records)表示程序開始累計讀取的記錄數,而FNR (File Number of Records)表示當前文件讀取的記錄數,當單文件執行時,兩者相同;當存在多文件時,FNR會在讀取新文件的時候重新置位為0,而NR會一直累計,因此可以用FNR==NR來判斷是否在讀取第一份文件。
我們會發現協議頭https://和http://即便不同,其url主體還是一致的,這種情況下需要進行split去除協議頭后進行對比,腳本如下:
awk -F " " -v seg="://" '
FNR==NR{split($1, b, seg);a[b[2]];next} {
split($1, c, seg); if (c[2] in a) print $0
}' file_1.data file_2.data
將會輸出:
https://haokan.baidu.com/v?pd=wisenatural&vid=7217158148519997092 A
http://zhidao.baidu.com/question/2054086297537323627/answer/3367935303.html B
http://haokan.baidu.com/v?pd=wisenatural&vid=5651466905742673647 E
awk文件去重
awk也可以用于當前文件中,對于某個字段進行去重處理,加入目前輸入文件如:
file.data
https://haokan.baidu.com/v?pd=wisenatural&vid=7217158148519997092 A
http://v.qq.com/boke/page/t/0/w/t01451r5euw.html C
http://zhidao.baidu.com/question/2054086297537323627/answer/3367935303.html B
http://v.qq.com/boke/page/t/0/w/t01451r5euw.html C
http://3g.163.com/v/video/V5KLT7ESE.html D
http://haokan.baidu.com/v?pd=wisenatural&vid=7217158148519997092 A
https://zhidao.baidu.com/question/2054086297537323627/answer/3367935303.html B
http://m.bilibili.com/video/BV1Zw411d7m6 C
http://haokan.baidu.com/v?pd=wisenatural&vid=7217158148519997092 A
https://haokan.baidu.com/v?pd=wisenatural&vid=6619006578881460543 D
http://haokan.baidu.com/v?pd=wisenatural&vid=5651466905742673647 E
http://haokan.baidu.com/v?pd=wisenatural&vid=7217158148519997092 A
http://haokan.baidu.com/v?pd=wisenatural&vid=5651466905742673647 E
那么在不考慮協議頭的差別的情況下(也就是只看url主體),去重腳本如下:
awk -F " " -v seg="://" '{
split($1,a,seg); if (a[2] in b) {next} else {b[a[2]]};print $0
}' file.data
輸出為:
https://haokan.baidu.com/v?pd=wisenatural&vid=7217158148519997092 A
http://v.qq.com/boke/page/t/0/w/t01451r5euw.html C
http://zhidao.baidu.com/question/2054086297537323627/answer/3367935303.html B
http://3g.163.com/v/video/V5KLT7ESE.html D
http://m.bilibili.com/video/BV1Zw411d7m6 C
https://haokan.baidu.com/v?pd=wisenatural&vid=6619006578881460543 D
http://haokan.baidu.com/v?pd=wisenatural&vid=5651466905742673647 E
單文件查重
有時候需要進行單文件查重,比如對某個字段進行查重,如下所示
file_1.data
part-00001.attempt_000.gz
part-00001.attempt_001.gz
part-00002.attempt_000.gz
part-00003.attempt_000.gz
part-00004.attempt_000.gz
part-00004.attempt_001.gz
part-00004.attempt_002.gz
其中的attempt_xxx
是失敗重試的次數,那么對其進行查重可以用以下腳本:
cat file_1.data | awk '{
split($0, tmp, ".");
part_name = tmp[1];
if (part_name in part_set) {
print part_name, $0;
} else {
part_set[part_name];
}
}'
由此可以將重復的part打印出來,當然由此也可以選擇未重復的part。
兩文件查重
有時候需要簡單統計兩個文件之間的某個字段重復程度,比如統計兩個文件重復的url數量,那么可以用grep實現,通過-x指定完全字符串匹配,-F將匹配模式指定為固定字符串的列表,用-f指定規則文件,其內容含有一個或多個規則樣式,讓grep查找符合規則條件的文件內容,格式為每行一個規則樣式。通過awk首先將字段進行轉儲,如:
cat url_1.data | awk -F "\t" '{print $1}' > tmp_url_1.data
cat url_2.data | awk -F "\t" '{print $1}' > tmp_url_2.data
grep -xFf tmp_url_1.data tmp_url_2.data
這樣可以統計tmp_url_2.data
中的url有多少是在tmp_url_1.data
出現過的。
更換分割字段的分隔符
有時候需要更改文件的分隔符,比如從"\t"
轉成" "
,那么可以用如下腳本:
# file.data: your_file
1 2 3
4 5 6
7 8 9
cat file.data | awk -F "\t" -v OFS=" " '{$1=$1; print $0}'
這里有個值得注意的就是: $0
是awk中對于輸入record的記錄,不會由于設置了OFS
輸出分隔符(Output Field Seperator )而變化,因此需要通過$1=$1
進行$0
值的重建。
提取括號內的值
有時候遇到的數據如下所示:
# file.data: your_file
date(20220114)time(0419pm)
date(20220114)time(0839pm)
...
需求是提取括號內的內容,那么可以用以下命令:
cat file.data | awk -F"[()]" '{print $2,$4}'
也還有很多命令可以實現這類型的需求,筆者以后繼續整理下
打印特定行的字符串
用awk
可以解決,但是最快的還是采用sed
進行:
sed -n '2,$p' data.file # 第二行到最后一行的所有數據
sed -n '100p' data.file # 第100行數據
sed -n '4,6p' data.file # 第4到第5行數據
字符串替換(正則模板)
通過正則表達式可以實現更為靈活的字符串查找和匹配,以sed
為例子,假如當前文檔如:
# file.data
\mathcal{J} = \sum_{i=0]^{N-1} \mathcal{L}_i
\tag{1-1}
有時候需要把所有\tag{}
的字符串都去除,最好的方法就是采用正則表達式:
cat file.data | sed -e 's/\(.*\)\\tag{.*}\(.*\)/\1\2/p'
其中-e表示擴展正則表達式,s/reg_pattern/replace_str/p表示用replace_str去替換符合reg_pattern的字符串,其中\(\)是對括號的轉義,而()是表示一組字符串(在后續會用\1 \2進行指定),那么除去正則表達式,這個正則表達式的意思是(.*)\tag{.*}(.*) 也就是查找符合該模式的字符串。在replace_str域,\1 \2代表符合正則表達式的字符串組,那么其實\1 = "\mathcal{J} = \sum_{i=0]^{N-1} \mathcal{L}_i ", \2 = “”。[1]
字段挑選
如以下輸入,不同字段用空格隔開,但是由于某些原因,可能并不僅僅是一個空格,其中可能有若干個空格隔開了不同字段,可以考慮結合cut和tr進行字段挑選
NO Name SubjectID Mark 備注
1 longshuai 001 56 不及格
2 gaoxiaofang 001 60 及格
3 zhangsan 001 50 不及格
4 lisi 001 80 及格
5 wangwu 001 90 及格
cat abc.sh | tr -s " " | cut -d " " -f2,4
以上腳本對第2和4列字段進行打印,其中的tr -s
將對重復的空格進行壓縮,輸出結果如:
Name Mark
longshuai 56
gaoxiaofang 60
zhangsan 50
lisi 80
wangwu 90
cut
命令的參數有:
-b:按字節篩選;
-n:與"-b"選項連用,表示禁止將字節分割開來操作;
-c:按字符篩選;
-f:按字段篩選;
-d:指定字段分隔符,不寫-d時的默認字段分隔符為"TAB";因此只能和"-f"選項一起使用。
-s:避免打印不包含分隔符的行;
--complement:補足被選擇的字節、字符或字段(反向選擇的意思或者說是補集);
--output-delimiter:指定輸出分割符;默認為輸入分隔符。
當然,這個字段挑選的功能也可以由大殺器awk完成,但是有時候用cut會更精煉一些。
刪除重復字符有些場景中,可能會出現重復字符,這些字符可能是用戶的不規范輸入,或者其他各種原因產生的,比如最常見的是重復空格,或者重復的制表符等等,可以采用tr -s 命令進行重復字符的去除,如:
echo "sssssss" | tr -s "s"
# 輸出為 s
查看grep結果的上下文
grep經常用于查看文本中出現的字符串內容,有時候需要查看其上下文,可以用以下參數:
grep -5 'parttern' inputfile //打印匹配行的前后5行
grep -C 10 'parttern' inputfile //打印匹配行的前后10行
grep -A 5 'parttern' inputfile //打印匹配行的后5行
grep -B 5 'parttern' inputfile //打印匹配行的前5行
Reference
[1]. https://unix.stackexchange.com/questions/78625/using-sed-to-find-and-replace-complex-string-preferrably-with-regex
[2]. https://article.itxueyuan.com/m9bPp