使用 wc
命令:
wc -l filename | awk '{print $1}'
使用 awk
命令:
awk 'END {print NR}' filename
使用 grep
命令:
grep -c '' filename
使用 sed
命令:
sed -n '$=' filename
使用 perl
命令:
perl -lne 'END { print $. }' filename
使用 nl
命令:
nl filename | tail -n 1 | awk '{print $1}'
速度测试( 1 0 8 10^8 108 行数据,取 5 5 5 次平均),其中数据由以下代码生成
with open('1.txt', 'w') as w:
for i in range(10**8):
w.write(str(i) + '\n')
先在macOS上实验,使用 time
命令计时,取 total
对应的结果
命令(Apple M1 Pro) | 第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 |
---|---|---|---|---|---|---|
wc | 0.388 | 0.369 | 0.372 | 0.374 | 0.374 | 0.375 |
awk | 25.003 | 25.219 | 25.361 | 25.129 | 25.326 | 25.208 |
grep | 1.330 | 1.333 | 1.324 | 1.315 | 1.313 | 1.323 |
sed | 14.832 | 14.784 | 14.815 | 14.825 | 14.892 | 14.830 |
perl | 6.746 | 6.746 | 6.789 | 6.799 | 6.941 | 6.804 |
nl | 32.039 | 32.015 | 32.039 | 32.022 | 32.014 | 32.026 |
速度:wc
> grep
> perl
> sed
> awk
> nl
。
然后在Linux上实验,使用 time
命令计时,取 real
对应的结果
命令(Intel® Xeon® CPU E5-2698 v4) | 第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 |
---|---|---|---|---|---|---|
wc | 0.765 | 0.761 | 0.765 | 0.761 | 0.740 | 0.758 |
awk | 2.876 | 2.861 | 2.871 | 2.884 | 2.875 | 2.873 |
grep | 3.187 | 3.160 | 3.159 | 3.156 | 3.192 | 3.171 |
sed | 5.513 | 5.528 | 5.499 | 5.505 | 5.513 | 5.512 |
perl | 12.507 | 12.887 | 12.526 | 12.510 | 12.503 | 12.587 |
nl | 16.355 | 16.375 | 16.354 | 16.382 | 16.616 | 16.416 |
速度:wc
> awk
> grep
> sed
> perl
> nl
。
补充实验:
命令(Intel® Xeon® Gold 6133 CPU) | 第一次 |
---|---|
wc | 11.588 |
awk | 8.880 |
grep | 2.562 |
sed | 4.811 |
perl | 11.693 |
nl | 15.177 |
总结:即使是同一个平台,不同CPU下命令的执行时间也会有所不同。
⚠️ 注意,以上六个命令中,只有
wc
命令会存在少统计一行的情况。例如当某个txt文件的最后一行没有以换行符结尾时,最后的结果就会少1,这是因为wc
命令只会统计换行符的数量。
以第100行为例。
使用 sed
命令:
sed -n '100p' filename
注意,虽然能够立即从终端看到结果,但终端依然会被 sed
占用一段时间,因为 sed
还会继续处理文件的剩余部分,sed
默认会读取文件的所有内容。
我们可以让 sed
在读取到第100行时立即退出:
sed -n '100{p;q;}' filename
使用 awk
命令:
awk 'NR==100 {print; exit}' filename
使用 perl
命令:
perl -ne 'print && last if $. == 100' filename
使用 head
与 tail
的组合:
head -n 100 filename | tail -n 1
速度测试(依然使用之前的文件,这里抽取第 50000000 50000000 50000000 行)。
先在macOS上实验:
命令(Apple M1 Pro) | 第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 |
---|---|---|---|---|---|---|
sed | 4.840 | 4.948 | 4.949 | 4.943 | 5.024 | 4.941 |
awk | 13.095 | 13.360 | 13.090 | 13.226 | 13.301 | 13.214 |
perl | 4.371 | 4.360 | 4.348 | 4.278 | 4.334 | 4.338 |
head & tail | 8.130 | 8.220 | 8.134 | 8.118 | 8.155 | 8.151 |
速度:perl
> sed
> head & tail
> awk
。
然后在Linux上实验:
命令(Intel® Xeon® CPU E5-2698 v4) | 第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 |
---|---|---|---|---|---|---|
sed | 2.670 | 2.720 | 2.682 | 2.656 | 2.660 | 2.678 |
awk | 2.509 | 2.473 | 2.465 | 2.477 | 2.457 | 2.476 |
perl | 8.277 | 8.285 | 8.275 | 8.280 | 8.252 | 8.274 |
head & tail | 0.981 | 0.957 | 0.956 | 0.960 | 0.958 | 0.962 |
速度:head & tail
> awk
> sed
> perl
。
补充实验:
命令(Intel® Xeon® Gold 6133 CPU) | 第一次 |
---|---|
sed | 2.294 |
awk | 6.907 |
perl | 8.093 |
head & tail | 0.474 |
总结:在macOS上,perl
查看一行的速度最快,在Linux上,head & tail
查看一行的速度最快。
说到随机抽取,最容易想到的就是 shuf
命令。
从文件中随机抽取100行:
shuf -n 100 filename
可以通过指定 -o
来将随机抽取的结果保存到新文件中:
shuf -n 100 filename -o shuffled_filename
或者使用 sort
命令,先对所有行随机排序,然后选择前100行:
sort -R filename | head -n 100
⚠️
-R
是随机排序的意思,sort -R
会将整个文件的所有行读入到内存中以进行排序,所以内存消耗会非常大。而shuf
采用Reservoir Sampling,只需要维护一个100行的缓冲区。
速度测试(依然使用之前的文件,这里随机抽取 10000 10000 10000 行)。
命令(Intel® Xeon® CPU E5-2698 v4) | 第一次 |
---|---|
shuf | 6.043 |
sort | 3:39.93 |
命令(Intel® Xeon® Gold 6133 CPU) | 第一次 |
---|---|
shuf | 5.530 |
sort | 3:45.73 |
📝 macOS上没有
shuf
命令。
之前提到过,head & tail
是Linux上查看一行最快的方法,如果我们预先生成
10000
10000
10000 个随机数,然后根据这些随机数去查看对应的行,并利用多进程加速,那么同样可以实现随机抽取
10000
10000
10000 行的效果。
生成随机数的前提是要知道随机数的范围,而范围取决于文件的总行数,因此这个方法的速度还取决于查看总行数的速度。
要在Python脚本中执行Linux命令,我们需要用到 subprocess
模块:
def execute_cmd(cmd):
res = subprocess.run(cmd, shell=True, text=True, capture_output=True)
return res.stdout if res.returncode == 0 else res.stderr
完整代码:
import subprocess
import multiprocessing
import random
from functools import partial
def execute_cmd(cmd):
res = subprocess.run(cmd, shell=True, text=True, capture_output=True)
return res.stdout if res.returncode == 0 else res.stderr
def _get_line(filename, idx):
return execute_cmd(f"head -n {idx + 1} {filename} | tail -n 1")
if __name__ == '__main__':
filename = './1.txt'
total_lines = int(execute_cmd(f"wc -l {filename}").split()[0])
samples = random.sample(range(total_lines), 10000)
get_line = partial(_get_line, filename)
with multiprocessing.Pool() as pool:
res = list(pool.imap(get_line, samples))
print(res)
但这种方法的效率是最低的,随机抽取 10000 10000 10000 行要花费8:47.22。
之前这段代码
with open('1.txt', 'w') as w:
for i in range(10**8):
w.write(str(i) + '\n')
生成的 1.txt
的大小是847.71MB,我们先来展示一下这个大小是如何计算得到的。
首先,每一行都由一个
n
n
n 位数和一个换行符组成,所以总共
n
+
1
n+1
n+1 个字符。由于 open
默认采用UTF-8编码,而数字和换行符均是ASCII字符,所以均占一个字节,因此每行占
n
+
1
n+1
n+1 个字节。
数字的范围是 [ 0 , 1 0 8 − 1 ) [0, 10^8-1) [0,108−1),对于一个 n n n 位数,其最高位只有 9 9 9 种取法,而后面 n − 1 n-1 n−1 位的每一位都有 10 10 10 种取法,所以总共会有 9 ⋅ 1 0 n − 1 9\cdot 10^{n-1} 9⋅10n−1 种情况,而每种情况都占 n + 1 n+1 n+1 个字节,因此一个 n n n 位数占的总字节数为 9 ⋅ 1 0 n − 1 ⋅ ( n + 1 ) 9\cdot 10^{n-1}\cdot (n+1) 9⋅10n−1⋅(n+1)。
而 1.txt
存放的就是一位数到八位数的总字节数,注意到
1
MB
=
2
10
KB
=
2
20
KB
1\,\text{MB}=2^{10}\,\text{KB}=2^{20}\,\text{KB}
1MB=210KB=220KB,从而 1.txt
的大小为
1 2 20 ∑ n = 1 8 9 ⋅ 1 0 n − 1 ⋅ ( n + 1 ) \frac{1}{2^{20}}\sum_{n=1}^8 9\cdot 10^{n-1}\cdot (n+1) 2201n=1∑89⋅10n−1⋅(n+1)
我们可以做一个推广。假设写入到文件中的数字范围是 [ 0 , 1 0 N − 1 ) [0,10^N-1) [0,10N−1),那么总字节数就是
N ⋅ 1 0 N + ( 1 0 N − 1 ) ⋅ 8 9 ∼ O ( N ⋅ 1 0 N ) N\cdot 10^N+(10^N-1)\cdot \frac89\sim\mathcal{O}(N\cdot 10^N) N⋅10N+(10N−1)⋅98∼O(N⋅10N)
回到正题,我们可以用 split
命令来切分一个文件,既可以按行数也可以按大小。
切分后的每个文件都有100行:
split -l 100 filename prefix
其中 filename
是待分割的文件,prefix
是生成的每个小文件的前缀,若不指定,则默认为 x
。每个小文件的后缀则以 aa
、ab
、…等进行编排。
切分后的每个文件都有100MB:
split -b 100M filename prefix
-b
可以接受的单位有 K
、M
、G
、T
等。不指定单位就按bytes进行切分。
注意,-b
可能会导致某些行的内容不完整,这对于诸如 jsonl
的文件是致命的。如需按大小切分的同时保持行的完整性,可使用 --line-bytes
(简写为 -C
):
split -C 100M filename prefix
我们也可以指定 -a
来控制后缀的长度(默认是2):
split -a 4 -C 100M 1.txt out
该命令将对 1.txt
按大小进行切分,输出的每个文件的大小近乎是100M(最后一个可能不是),同时输出的文件名形如 outaaaa
、outaaab
、…。
如果觉得字母作为后缀不够直观,我们可以使用数字作为后缀:
split -d -C 100M 1.txt
例如,如果 C4
的大小为400M,我们想将其分割成 C4_1
、C4_2
、C4_3
、C4_4
四个文件,每个文件的大小为100M,则可以这样做:
split -d -a 1 -C 100M C4 C4_
因为在 split
的时候会指定 prefix
,所以我们可以根据 prefix
逆向地合并文件:
cat prefix* > merged_file