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

使用 headtail 的组合:

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,1081),对于一个 n n n 位数,其最高位只有 9 9 9 种取法,而后面 n − 1 n-1 n1 位的每一位都有 10 10 10 种取法,所以总共会有 9 ⋅ 1 0 n − 1 9\cdot 10^{n-1} 910n1 种情况,而每种情况都占 n + 1 n+1 n+1 个字节,因此一个 n n n 位数占的总字节数为 9 ⋅ 1 0 n − 1 ⋅ ( n + 1 ) 9\cdot 10^{n-1}\cdot (n+1) 910n1(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=18910n1(n+1)

我们可以做一个推广。假设写入到文件中的数字范围是 [ 0 , 1 0 N − 1 ) [0,10^N-1) [0,10N1),那么总字节数就是

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) N10N+(10N1)98O(N10N)


回到正题,我们可以用 split 命令来切分一个文件,既可以按行数也可以按大小。

切分后的每个文件都有100行:

split -l 100 filename prefix

其中 filename 是待分割的文件,prefix 是生成的每个小文件的前缀,若不指定,则默认为 x。每个小文件的后缀则以 aaab、…等进行编排。

切分后的每个文件都有100MB:

split -b 100M filename prefix

-b 可以接受的单位有 KMGT 等。不指定单位就按bytes进行切分。

注意,-b 可能会导致某些行的内容不完整,这对于诸如 jsonl 的文件是致命的。如需按大小切分的同时保持行的完整性,可使用 --line-bytes(简写为 -C):

split -C 100M filename prefix

我们也可以指定 -a 来控制后缀的长度(默认是2):

split -a 4 -C 100M 1.txt out

该命令将对 1.txt 按大小进行切分,输出的每个文件的大小近乎是100M(最后一个可能不是),同时输出的文件名形如 outaaaaoutaaab、…。

如果觉得字母作为后缀不够直观,我们可以使用数字作为后缀:

split -d -C 100M 1.txt

例如,如果 C4 的大小为400M,我们想将其分割成 C4_1C4_2C4_3C4_4 四个文件,每个文件的大小为100M,则可以这样做:

split -d -a 1 -C 100M C4 C4_

因为在 split 的时候会指定 prefix,所以我们可以根据 prefix 逆向地合并文件:

cat prefix* > merged_file
10-02 01:57