
要对文件按行处理,行数算出来不对的时候,第一反应往往是怀疑文件内容。多数情况下内容没错,是「行」这个字在两个地方的含义不一样:一种指换行符的个数,一种指肉眼看到的行。
准备了三个文件,两个都是三行内容,区别只在最后一个字节:
== 1. 两个文件都是三行内容,只有一个末尾带换行符 ==
文件 字节 wc-l wc-c wc-m grep-c '' awk NR sed $=
with-nl.txt 6 3 6 6 3 3 3
no-nl.txt 5 2 5 5 3 3 3
六个命令摆在一起看,前五个数字分成两派:wc -l 说 no-nl.txt 是 2 行,wc -c、wc -m、grep -c ''、awk 'END{print NR}' 和 sed -n '$=' 都说 3 行。同一个文件、同一台机器,答案差 1。
wc -l 数的是换行符
wc -l 数的从来不是「行」,是文件里 0x0a 的个数。POSIX 对文本文件的定义就是每一行以换行符结束,所以一个末尾没有换行符的文件严格来说不算完整的文本文件,wc -l 报 2 是照定义算的,没有 bug。
== 2. 末尾那个字节到底是什么 ==
with-nl.txt 末尾 1 字节的十六进制:0a
no-nl.txt 末尾 1 字节的十六进制:63
0x0a 是换行符,0x63 是字符 c
两个文件末尾的字节一个 0x0a,一个 0x63。后者就是字符 c,文件到那里直接结束了。
这一类文件在真实项目里出现的原因通常很朴素:日志的最后一行是进程被 kill 时写了一半的,导出脚本用了 printf 而不是 echo,或者有人手工 echo -n 追加了一句。上游给的文件里混进这种结尾,按 wc -l 分配的任务数就会少一个,最后一行永远轮不到处理。
grep -c 数的是行,不是次数
第二个容易混的数字是出现次数。grep -c 的名字看着像 count 命中,实际数的是命中的行数:
== 4. grep -c 数的是行,不是命中次数 ==
aaa
aaa aaa
bbb
grep -c aaa -> 2 行(文件里有 3 处 aaa)
grep -o aaa | wc -l -> 3 处
命中所在行号 -> 1,2
那个文件里 aaa 出现在 1、2 两行,第 2 行有两处。grep -c aaa 给的是 2,grep -o aaa 才把三处拆成三行,接一个 wc -l 得到 3。
要统计的到底是「有多少行涉及」还是「总共出现几次」,在写统计脚本的时候就得定下来。少定这一步,报表上的数字会在数据分布变化时慢慢飘走:命中均匀分布时两种算法结果一样,一旦某一行挤进多处,两个数字就分叉了。
-c 和 -m 之间隔着编码
wc -c 数字节,wc -m 数字符。全 ASCII 的文件上两者永远相等,中文一进来就分开:
== 3. wc -m 数的是字符,wc -c 数的是字节 ==
文件 字节 wc-c wc-m LC_ALL=C wc -m
第三个数字是加 LC_ALL=C 之后 wc -m 的结果,它退化成了字节数。wc -m 要按字符数得先知道当前 locale 认不认多字节字符,C 这个 locale 只认单字节,于是它把每个字节当一个字符。写中文内容的脚本,wc -m 前面不加 locale 就直接用,很可能得到一个既不是字节数也不是字符数的东西。
追加内容时最容易造出半行
半行是写出来的,不是天生的。用不带换行符的输出追加上去,两段内容会粘成一行:
== 5. 追加一行之后,两种数法差多少 ==
printf 不带换行符追加一个 d 之后:
文件内容:a|b|cd
wc -l -> 2
awk END{print NR}-> 3
按换行符切出来的段数 -> 3
追加一个 d 之后,文件内容变成 a、b、cd 三行,wc -l 报 2。这种文件用 while read 逐行读时,cd 会作为一个整体变量进来,本该是两个值的地方变成一个。
要避免也简单:追加之前先确认上一个文件以换行符结尾,或者干脆用 printf '%s\n' 这种自带换行的写法。真要在一个非空文件后面接内容,先跟一个 echo >> file 把行补齐。
每个数字该配哪个命令
把上面几件事收成一张对照:
| 想要的东西 | 命令 | 注意 |
|---|---|---|
| 换行符个数 | wc -l |
末尾无换行符时比可见行少 1 |
| 可见行数 | awk 'END{print NR}' 或 grep -c '' |
最后那半行也算一行 |
| 命中行数 | grep -c PATTERN |
一行里多处只算一次 |
| 命中次数 | grep -o PATTERN \| wc -l |
会输出每一处,文件大时慢 |
| 字节数 | wc -c |
跟编码有关 |
| 字符数 | LC_ALL=zh_CN.UTF-8 wc -m |
不指定 locale 会退化成字节数 |
表格里那几条里只有第一条和第二条是要长期分清的:给程序分配任务用哪个都行,只要前后一致;给人看的报表、给下游的行数校验,一旦和 wc -l 混用就会差一个数。
我给批量脚本定的一条规矩是按「可见行」算,统一用 awk 'END{print NR}',因为它跟编辑器里看到的一致,少一个需要解释的例外。代价是空文件上它和 wc -l 都给 0,这也符合预期。
最后一条经验:核对行数的时候别只对一个命令,至少跑两个。两个数字不一致时先看最后一个字节,十次里有九次问题就在那里。