1. 统计文件的总行数
使用 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
对应的结果
速度:wc
> grep
> perl
> sed
> awk
> nl
。
然后在Linux上实验,使用 time
命令计时,取 real
对应的结果
速度:wc
> awk
> grep
> sed
> perl
> nl
。
补充实验:
总结:即使是同一个平台,不同CPU下命令的执行时间也会有所不同。
2. 查看文件中的某一行
以第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上实验:
速度:perl
> sed
> head & tail
> awk
。
然后在Linux上实验:
速度:head & tail
> awk
> sed
> perl
。
补充实验:
总结:在macOS上,perl
查看一行的速度最快,在Linux上,head & tail
查看一行的速度最快。
3. 从文件中随机抽取若干行
说到随机抽取,最容易想到的就是 shuf
命令。
从文件中随机抽取100行:
shuf -n 100 filename
可以通过指定 -o
来将随机抽取的结果保存到新文件中:
shuf -n 100 filename -o shuffled_filename
或者使用 sort
命令,先对所有行随机排序,然后选择前100行:
sort -R filename | head -n 100
速度测试(依然使用之前的文件,这里随机抽取 10000 10000 10000 行)。
之前提到过,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。
4. 划分文件&合并文件
之前这段代码
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