• 文件操作的常用技巧(持续更新)


    1. 统计文件的总行数

    使用 wc 命令

    wc -l filename | awk '{print $1}'
    
    • 1

    使用 awk 命令:

    awk 'END {print NR}' filename
    
    • 1

    使用 grep 命令:

    grep -c '' filename
    
    • 1

    使用 sed 命令:

    sed -n '$=' filename
    
    • 1

    使用 perl 命令:

    perl -lne 'END { print $. }' filename
    
    • 1

    使用 nl 命令:

    nl filename | tail -n 1 | awk '{print $1}'
    
    • 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')
    
    • 1
    • 2
    • 3

    先在macOS上实验,使用 time 命令计时,取 total 对应的结果

    命令(Apple M1 Pro)第一次第二次第三次第四次第五次平均
    wc0.3880.3690.3720.3740.3740.375
    awk25.00325.21925.36125.12925.32625.208
    grep1.3301.3331.3241.3151.3131.323
    sed14.83214.78414.81514.82514.89214.830
    perl6.7466.7466.7896.7996.9416.804
    nl32.03932.01532.03932.02232.01432.026

    速度:wc > grep > perl > sed > awk > nl

    然后在Linux上实验,使用 time 命令计时,取 real 对应的结果

    命令(Intel® Xeon® CPU E5-2698 v4)第一次第二次第三次第四次第五次平均
    wc0.7650.7610.7650.7610.7400.758
    awk2.8762.8612.8712.8842.8752.873
    grep3.1873.1603.1593.1563.1923.171
    sed5.5135.5285.4995.5055.5135.512
    perl12.50712.88712.52612.51012.50312.587
    nl16.35516.37516.35416.38216.61616.416

    速度:wc > awk > grep > sed > perl > nl

    补充实验:

    命令(Intel® Xeon® Gold 6133 CPU)第一次
    wc11.588
    awk8.880
    grep2.562
    sed4.811
    perl11.693
    nl15.177

    总结:即使是同一个平台,不同CPU下命令的执行时间也会有所不同。

    ⚠️ 注意,以上六个命令中,只有 wc 命令会存在少统计一行的情况。例如当某个txt文件的最后一行没有以换行符结尾时,最后的结果就会少1,这是因为 wc 命令只会统计换行符的数量。

    2. 查看文件中的某一行

    以第100行为例。

    使用 sed 命令:

    sed -n '100p' filename
    
    • 1

    注意,虽然能够立即从终端看到结果,但终端依然会被 sed 占用一段时间,因为 sed 还会继续处理文件的剩余部分,sed 默认会读取文件的所有内容。

    我们可以让 sed 在读取到第100行时立即退出:

    sed -n '100{p;q;}' filename
    
    • 1

    使用 awk 命令:

    awk 'NR==100 {print; exit}' filename
    
    • 1

    使用 perl 命令:

    perl -ne 'print && last if $. == 100' filename
    
    • 1

    使用 headtail 的组合:

    head -n 100 filename | tail -n 1
    
    • 1

    速度测试(依然使用之前的文件,这里抽取第 50000000 50000000 50000000 行)。

    先在macOS上实验:

    命令(Apple M1 Pro)第一次第二次第三次第四次第五次平均
    sed4.8404.9484.9494.9435.0244.941
    awk13.09513.36013.09013.22613.30113.214
    perl4.3714.3604.3484.2784.3344.338
    head & tail8.1308.2208.1348.1188.1558.151

    速度:perl > sed > head & tail > awk

    然后在Linux上实验:

    命令(Intel® Xeon® CPU E5-2698 v4)第一次第二次第三次第四次第五次平均
    sed2.6702.7202.6822.6562.6602.678
    awk2.5092.4732.4652.4772.4572.476
    perl8.2778.2858.2758.2808.2528.274
    head & tail0.9810.9570.9560.9600.9580.962

    速度:head & tail > awk > sed > perl

    补充实验:

    命令(Intel® Xeon® Gold 6133 CPU)第一次
    sed2.294
    awk6.907
    perl8.093
    head & tail0.474

    总结:在macOS上,perl 查看一行的速度最快,在Linux上,head & tail 查看一行的速度最快。

    3. 从文件中随机抽取若干行

    说到随机抽取,最容易想到的就是 shuf 命令。

    从文件中随机抽取100行:

    shuf -n 100 filename
    
    • 1

    可以通过指定 -o 来将随机抽取的结果保存到新文件中:

    shuf -n 100 filename -o shuffled_filename
    
    • 1

    或者使用 sort 命令,先对所有行随机排序,然后选择前100行:

    sort -R filename | head -n 100
    
    • 1

    ⚠️ -R 是随机排序的意思,sort -R 会将整个文件的所有行读入到内存中以进行排序,所以内存消耗会非常大。而 shuf 采用Reservoir Sampling,只需要维护一个100行的缓冲区。


    速度测试(依然使用之前的文件,这里随机抽取 10000 10000 10000 行)。

    命令(Intel® Xeon® CPU E5-2698 v4)第一次
    shuf6.043
    sort3:39.93
    命令(Intel® Xeon® Gold 6133 CPU)第一次
    shuf5.530
    sort3: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
    
    • 1
    • 2
    • 3

    完整代码:

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    但这种方法的效率是最低的,随机抽取 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
    • 2
    • 3

    生成的 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
    
    • 1

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

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

    split -b 100M filename prefix
    
    • 1

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

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

    split -C 100M filename prefix
    
    • 1

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

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

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

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

    split -d -C 100M 1.txt
    
    • 1

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

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

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

    cat prefix* > merged_file
    
    • 1
  • 相关阅读:
    Add the installation prefix of “Qt5“ to CMAKE_PREFIX_PATH or set “Qt5_DIR“解决
    编写一个简单的linux kernel rootkit
    【HCIP】高级 VLAN
    AIGC视频生成/编辑技术调研报告
    Q4还没结束,我已看到2022年全球科技并购已近3000亿美元了
    c语言数据结构 二叉树(三)
    Android描边外框stroke边线、rotate旋转、circle圆形图的简洁通用方案,基于Glide与ShapeableImageView,Kotlin
    js 继承
    数据库应用:CentOS 7离线安装MySQL与Nginx
    芯原商业模式sipaas中第二个s指的是(芯原股份面试题)
  • 原文地址:https://blog.csdn.net/raelum/article/details/133421207