在总磁盘空间为1TB,可用空间为300GB的系统上,如何从700GB文本文件中删除前3亿行?
(我的系统有2GB的内存。)
我发现答案使用sed,tail,head:

如何使用删除文本文件的前n行shell命令?
删除大文本文件的前n行

但是我认为(请纠正我)我不能使用它们,因为磁盘空间限制为1TB,并且它们会产生一个新文件和/或在处理过程中有一个tmp文件。
该文件包含JSON格式的数据库记录。

评论

你有多少内存?

如果您可以压缩文件,并且压缩后的空间足以存储压缩文件的第二个副本,那么这将是您的最佳选择。解决问题的管道将通过一些转换流化解压缩的数据,然后压缩处理后的数据。参见例如Terdon的答案。

有关真正有趣的选项,请参阅在Linux上的Stack Overflow上截断文件的前100MB,这实际上是在文件的前面进行原位截断。注意,我尚未尝试过,强烈建议您先在一次性文件上进行测试

这是真实情况还是假设情况?我想不出现实的情况,其中700 GB的文本将包含有意义的内容。寻找背景。

你有备份吧?将其视为您的源并覆盖目标文件。如果您没有备份,那么显然数据并不重要,因此只要删除它并重新开始就没关系

#1 楼

如果您有足够的空间来压缩文件,从而可以释放大量空间,从而可以执行其他操作,则可以尝试以下操作:
gzip file && zcat file.gz | tail -n +300000001 | gzip > newFile.gz

首先将gzip原始输入文件(file )创建file.gz。然后,对新创建的zcat进行file.gz,将其通过tail -n +300000001进行管道传输以删除前3M行,压缩结果以节省磁盘空间并将其另存为newFile.gz&&确保仅在gzip操作成功时继续操作(如果空间不足,操作将失败)。
请注意,文本文件具有很高的可压缩性。例如,我使用seq 400000000 > file创建了一个测试文件,该文件打印了从1到400,000,000的数字,这产生了3.7G的文件。当我使用上面的命令压缩它时,压缩后的文件只有849M,而我创建的newFile.gz只有213M。

评论


seq生成的文本的熵可能很低,这就是为什么此特定文本非常可压缩的原因。随机性更高的东西最有可能表现得更差。

–probably_someone
20/09/21在19:45



我猜一个700GB文本文件(带行)的内容也不是很随机。 OP没有指定,因为我猜它包含诸如记录数据或数据库转储之类的内容。它可能占原始大小的25%以下。如果所有内容均为7位ASCII字符,则已经有大约50%的减少空间。

– AVee
20-09-21在20:02

尾巴gzip避免保存要删除的行的压缩副本。

– Peter Cordes
20-09-22在2:07

@AVee您不是说具有7位ascii的12.5%吗?还是您有不同的想法?

– doetoe
20-09-22在23:35

@terdon最后,我使用了您的解决方案(我敢肯定其他人也可以使用-哦,天哪,这个小问题爆发了)。压缩后的文件只有140GB(具有很多相等字段名称的JSON数据),并且可以放在磁盘上。非常感谢你!

–克里斯
20/09/23在20:50

#2 楼

可以使用dd(或使用循环设备)就地删除前n行(或字节)。它不使用临时文件,没有大小限制。但是,这很危险,因为没有进度跟踪,并且任何错误都会使您的文件损坏。
示例:创建一个包含1000行的示例文件:前300行。它对应多少个字节?
$ seq 1 1000 > 1000lines.txt
$ head -n 3 1000lines.txt
1
2
3
$ tail -n 3 1000lines.txt
998
999
1000

文件为3893个字节,我们要删除前1092个字节,剩下2801​​个字节的新文件。
要删除这些字节,我们使用GNU dd命令,将conv=notrunc用作命令,否则在复制文件内容之前,文件将被删除:文件尚未被截断:
$ stat -c %s 1000lines.txt
3893 # total bytes
$ head -n 300 1000lines.txt | wc -c
1092 # first 300 lines bytes
$ echo $((3893-1092))
2801 # target filesize after removal

这会将文件减小到最终大小,从而删除文件末尾的重复行。
结果:
$ dd conv=notrunc iflag=skip_bytes skip=1092 if=1000lines.txt of=1000lines.txt
5+1 records in
5+1 records out
2801 bytes (2.8 kB, 2.7 KiB) copied, 8.6078e-05 s, 32.5 MB/s

处理过程对于较大的文件则类似。您可能需要设置更大的块大小以提高性能(dd的blocksize选项为bs)。
主要问题是确定确切的行号的正确字节偏移量。通常,只能通过读取和计数来完成。使用这种方法,即使要丢弃很大一部分,您也必须至少读取一次整个文件。

评论


如果我的生活依赖它,我将不会使用该解决方案;-)就地编辑文件可能会导致各种问题,包括完全丢失其内容。

– Artem S. Tashkinov
20-09-21在11:36

@ ArtemS.Tashkinov肯定...就地操作始终很危险。甚至对于非破坏性坏块,lvm pvmove,mdadm增长来说,也是如此……尤其对于那些担心dd的人,我还在另一个答案中添加了循环设备方法。

–frostschutz
20/09/21在15:18



怎么知道dd conv = notrunc iflag = skip_bytes skip = 1092 if = 1000lines.txt of = 1000lines.txt在制作输出时没有创建可观的临时文件(使得该解决方案不适用于OP的情况)?

–chux-恢复莫妮卡
20/09/22在11:18



dd不会创建临时文件。该答案假设您的文件系统运行正常。如果将网络或融合文件系统添加到混合中,则可能无法通过这种方式工作。

–frostschutz
20/09/22在11:33

conv = notrunk在FreeBSD dd上也可用,它不是GNU的版本。

–Rob
20/09/22在17:14



#3 楼

在某些文件系统(例如ext4或xfs)上,您可以使用fallocate()系统调用来实现。

评论


很好,这就是我要建议的。如果您要保留的行不在块边界上开始,则首先执行此操作可能仍然是压缩+解压缩的有用设置。尽管就地dd复制可能是最好的,并且不需要任何额外的空间,所以根本无法从中受益。

– Peter Cordes
20-09-22在2:16

#4 楼

您可以使用losetup进行此操作,以替代此处描述的dd方法。同样,此方法同样很危险。
同样,相同的测试文件和大小(从1000行文件中删除1-300行):
$ seq 1 1000 > 1000lines.txt
$ stat -c %s 1000lines.txt
3893 # total bytes
$ head -n 300 1000lines.txt | wc -c
1092 # first 300 lines bytes
$ echo $((3893-1092))
2801 # target filesize after removal

创建循环设备:
# losetup --find --show 1000lines.txt
/dev/loop0
losetup: 1000lines.txt: \
Warning: file does not fit into a 512-byte sector; \
the end of the file will be ignored.
# head -n 3 /dev/loop0
1 
2 
3 
# tail -n 3 /dev/loop0
921
922
923

。缺少号码。这是怎么回事?
循环设备要求其备份文件必须是扇区大小的倍数。带行的文本文件通常不适合该方案,因此为了不丢失文件末尾(最后一个部分扇区)的内容,只需先附加一些数据,然后重试:
# head -c 512 /dev/zero >> 1000lines.txt
# losetup --find --show 1000lines.txt
/dev/loop1
losetup: 1000lines.txt: \
Warning: file does not fit into a 512-byte sector; \
the end of the file will be ignored.
# tail -n 3 /dev/loop1
999
1000
# losetup --find --show --offset=1092 1000lines.txt
/dev/loop2
losetup: 1000lines.txt: \
Warning: file does not fit into a 512-byte sector; \
the end of the file will be ignored.
# head -n 3 /dev/loop2
301
302
303
# tail -n 3 /dev/loop2
999
1000
cp /dev/loop2 /dev/loop1

警告仍然存在,但是内容现在已经完成,所以没关系。
这次创建另一个,偏移量为300行:您不必担心会意外截断文件。您还可以在执行任何操作之前轻松地验证偏移量确实正确。
最后,只需将其复制,从偏移量设备复制到完整位置:
losetup -d /dev/loop2 /dev/loop1 /dev/loop0

溶解循环设备:
truncate -s 2801 1000lines.txt

(或:losetup -D溶解所有循环设备。)
将文件截断为目标文件大小:
$ head -n 3 1000lines.txt 
301
302
303
$ tail -n 3 1000lines.txt 
998
999
1000

结果:
q4312078q

#5 楼

如果您确实需要任务,则对自定义程序进行另一次投票。 C或任何功能强大的动态语言(如Perl或Python)都可以。我不会在这里写出源代码,但是将描述在移动数据时防止数据丢失的算法:

从末尾计数换行符读取大文件。收集了可以安全地容纳在可用空间中的预定行数之后,将此块写为单独的文件并剪切大文件的尾部。使用块的文件名存储行号。
之后,您将最终删除完全删除的大文件,并用相同的空间存储很多小得多的文件。
计算3亿行-您可以删除所有不必要的块立即知道行,因为您知道什么块包含哪些行。
如果您毕竟需要大文件,并且在删除了不必要的块之后释放了足够的空间来存储剩余块的总和-只需将它们与catcp结合在一起。需要大文件并且没有足够的空间,请编写另一个小程序来完成与步骤1相反的操作:将列表和每个块的单独长度保存到某个列表文件中。逐一读取块并将其附加到新创建的“大文件”中。每次将块添加到大文件后,都将删除一个单独的包含该块的小文件,从而使您可以就地重组文件。如果您随时中断编写块的过程,则可以通过为任何特定的块计算正确的偏移量来重新开始大文件的写入,因为您已经预先保存了每个块的大小。


评论


如果您正在写入的块(包含从原始文件末尾算起的行)不适合设备上剩余的300GB,该怎么办?我的意思是您的原始文件仍然存在,占据700GB?

–amn
20-09-28在13:26

@amn“可以安全地容纳在自由空间中的预定义数量的行”。选择真正安全的行数,或者动态跟踪已读内容的长度,并在达到可用空间限制之前停止几兆字节。

–奥列格·沃尔科夫(Oleg V. Volkov)
20-09-28在14:51

#6 楼

使用ksh93时:
tail -n +300000001 < file 1<>; file

1<>;运算符是标准1<>运算符的ksh93特定变体(在读+写模式下打开时不被截断),该命令在该位置返回命令后截断文件如果该命令成功执行,则该命令将其stdout保留。
对于其他shell,您始终可以使用perl手动进行原位截断,例如:
使用pv的进度条:
{
  tail -n +300000001 &&
    perl -e 'truncate STDOUT, tell STDOUT'
} < file 1<> file

(如果输入和输出指向同一文件,则使用head | pvcat | pv作为pv将无法正常工作。pv -Sls 300000000也将无法正常工作,因为pv不会离开像head一样存在之后文件的第300000000行之后的指针(并且POSIX要求可搜索文件)。pv | cat而不是cat | pv将使pv知道需要读取多少并给您一个ETA,目前在虚假在这种情况下,它没有考虑到从该文件的开头不读取的情况(如此处的情况)。
请注意,这些危险很危险,因为文件被适当覆盖。如果前300M行包含孔(对于有效的文本文件不应该发生),则磁盘空间可能用完,并且文件的其余部分会比FS上的可用空间占用更多的空间。 br />

评论


或仅使用Perl:perl -ne'print $。 > 300_000_000; } {truncate(STDOUT,告诉STDOUT);' <文件1 <>文件。或使用Perl和pv:<文件pv | perl -ne'打印$。 > 300_000_000; } {truncate(STDOUT,告诉STDOUT);' 1 <>文件,该文件应在整个文件通过pv时起作用。

–ilkkachu
20/09/22在20:34



@ilkkachu,是的,尽管perl在这里进行艰苦而漫长的工作可能效率不如头尾。您的光伏变体比我的整洁。如果您不想添加自己的答案,请随时对其进行编辑。

–StéphaneChazelas
20/09/22在20:42



#7 楼

此问题的局限性在于无论位于何处的存储量。不需要大量的RAM,因为从根本上讲,您可以从存储文件的任何地方简单地读取一个字节,然后将该字节[字符]写入或不写入到可能驻留的新文件中。 infile和outfile所在的位置可以完全位于单独的位置上,也可以位于单独的分区,磁盘或整个网络中。您无需读取和写入同一文件夹。因此,对于附带的程序,您只需提供一个完整的路径名即可解决磁盘空间限制。您将受到其他限制(例如磁盘或网络I / O速度)的支配,但是它将起作用。花很长时间工作总比不能完成工作要好。 2048个字符。如果愿意,可以将其设置为1000000,如果文本文件中的行非常长,则需要1MB的RAM。考虑在其上执行LL来创建gzip -9。作为文本文件,可能会压缩到5%的大小,考虑到磁盘I / O速度与cpu速度,这很有用。
我将mytextfile.gz的新文件写到未压缩的文本文件中,因此可能很大。
此程序是用标准C语言编写的,我尽可能地简化了它。
它会检查并且不会损害您的原始文本文件。
您不必压缩原始文本文件为此,压缩是可选的。
您可以将原始文件放在一个磁盘或网络位置,然后将带有N条删除行的输出文件写入其他磁盘或网络位置,只需使用完整的命名约定例如

n_deleted_lines

/*  this file named    delete_n_lines.c

    compile by    gcc -W delete_n_lines.c -o delete_n_lines.x -lz

    have your huge text file already compressed via "gzip -9" to save disk space

    this program will also read a regular uncompressed text file
*/

# include <stdlib.h>
# include <stdio.h>
# include <string.h>
# include <zlib.h>

# define LL  2048   /* line length, number of characters up to '\n' */


int main ( int argc, char *argv[] )
{
   gzFile fin;
   FILE *fout;
   char line[LL];
   long int i, n = 0;
   long int n_lines_to_delete = 0;

   if ( argc != 4 )
   {
      printf("   Usage: %s  <infile> <outfile> <first_N_lines_to_delete>\n\n", argv[0] );
      exit( 0 );
   }

   n = sscanf( argv[3], "%d", &n_lines_to_delete );
   if ( n == 0 )
   {
      printf("\n   Error: problem reading N lines to delete\n\n" );
      exit( 0 );
   }

   if ( strcmp( argv[1], argv[2] ) == 0 )
   {
      printf("\n   Error: infile and outfile are the same.\n" );
      printf("          don't do that\n\n");
      exit( 0 );
   }

   fout = fopen( argv[2], "w" );
   if ( fout == NULL )
   {
      printf("\n   Error: could not write to %s\n\n", argv[2] );
      exit( 0 );
   }

   fin = gzopen( argv[1], "r" );
   if ( fin == NULL )
   {
      printf("\n   Error: could not read %s\n\n", argv[1] );
      fclose( fout );
      exit( 0 );
   }

   n = 0;
   gzgets( fin, line, LL );
   while ( ! gzeof( fin ) )
   {
      if ( n < n_lines_to_delete )
         n++;
      else
         fputs( line, fout );

      gzgets( fin, line, LL );
   }

   gzclose( fin );
   fclose( fout );

   printf("\n   deleted the first %d lines of %s, output file is %s\n\n", n, argv[1], argv[2] );


   return 0;
}


评论


只是实际上识别出一个700GB的文本文件,这确实是一个很大的文件,如果在尝试压缩一个小时后在其上执行gzip失败,则将其放在1TB磁盘上不会令我感到惊讶。因此,需要700gb的空间来保存该文本文件,并假定> 500gb的空间来保存仅删除3m行的结果输出文件,您将不得不在其他地方找到存储空间。除了[购买]另一个磁盘外,您还可以利用某些托管N TB的在线存储,然后以任何可接受的方式将其安装在linux中。

–ron
20-09-21在16:14



gzip压缩它不会有任何问题。唯一的问题是是否有足够的空间来写入压缩的输出。

–Ángel
20-09-21在20:53

#8 楼

我创建了一个可能对您有用的工具:hexpeek是用于处理大型文件的十六进制编辑器,可在任何最新的类似POSIX的系统上运行(已在Debian,CentOS和FreeBSD上进行了测试)。
可以使用hexpeek或外部工具来查找第3亿个换行符。然后,假设X是第3亿个换行符后第一个八位位组的十六进制零索引位置,则可以在hexpeek中打开该文件,并且单个命令0,Xk将删除该文件中的第一个八位位组。 > hexpeek不需要tmpfile即可执行此操作;尽管可选的备份模式确实可以并且可能需要通过-backup标志来禁用(遗憾的是,当前的备份算法无法进行重新排列,从而影响了比备份文件更多的文件空间)。
当然,自定义C程序可以完成相同的操作。

#9 楼

想想河内的塔。
首先,将所需的行移动到新文件中:
find the start of line 3 million and 1
create a new, empty file
repeat {
  read a decent number of blocks from the end of the old file
  append the blocks to the end of the new file
  truncate the old file by that many blocks
} until you get to the start of line 3 million and 1.

您现在应该有了一个仅包含所需行的文件,但顺序不正确。 br />因此,让我们再次做同样的事情,以正确的顺序排列它们:留给读者练习。

评论


但是您不能“附加在新文件的前面”,因此,如果字符串顺序很重要,它将不会执行。

–奥列格·沃尔科夫(Oleg V. Volkov)
20-09-22在14:00

如果顺序很重要,请在写入之前在新文件中寻求适当的偏移量。这将创建一个稀疏文件,并允许您逐个填充它。

– rrauenza
20-09-22在18:57

#10 楼

有多种方法可以删除第一行。我建议您将文件拆分为多个块,进行更改(删除第一行),然后再次连接这些块。如果出现问题,您将没有备用选项!
这是我的有效解决方案(bash)。您可能需要一些改进...
 function split_into_chunks {
    BIG_FILE=

    while [ $(stat -c %s $BIG_FILE) -gt 0 ]
    do
    CHUNK_FILE="chunk.$(ls chunk.* 2>/dev/null | wc -l)"
    tail -10 $BIG_FILE > $CHUNK_FILE
    test -s $CHUNK_FILE && truncate -s -$(stat -c %s $CHUNK_FILE) $BIG_FILE
    done
}

function concat_chunks {
    BIG_FILE=
    test ! -s $BIG_FILE || (echo "ERROR: target file is not empty"; return)

    for CHUNK_FILE in $(ls chunk.* | sort -t . -k2 -n -r)
    do
    cat $CHUNK_FILE >> $BIG_FILE
    rm $CHUNK_FILE
    done
}
 

测试:
$ seq 1000 > big-file.txt 
$ stat -c "%s %n" chunk.* big-file.txt 2>/dev/null | tail -12
3893 big-file.txt
$ md5sum big-file.txt; wc -l big-file.txt 
53d025127ae99ab79e8502aae2d9bea6  big-file.txt
1000 big-file.txt

$ split_into_chunks big-file.txt
$ stat -c "%s %n" chunk.* big-file.txt | tail -12
40 chunk.9
31 chunk.90
30 chunk.91
30 chunk.92
30 chunk.93
30 chunk.94
30 chunk.95
30 chunk.96
30 chunk.97
30 chunk.98
21 chunk.99
0 big-file.txt

$ # here you could change the chunks
$ # the test here shows that the file will be concatenated correctly again

$ concat_chunks big-file.txt
$ stat -c "%s %n" chunk.* big-file.txt 2>/dev/null | tail -12
3893 big-file.txt
$ md5sum big-file.txt; wc -l big-file.txt 
53d025127ae99ab79e8502aae2d9bea6  big-file.txt
1000 big-file.txt

评论


我要说的是,OP应该在可能的情况下修改此大文件的生产者和使用者,以将所有内容都保留为大块(如果这不只是一项)。

–海豚
20-09-28在11:31

#11 楼

怎么用vim进行就地编辑呢?
Vim已经可以推理行了:
因此:

我们禁用备份副本的创建
我们删除了前3亿行(光标在启动时从第0行开始)
我们保存文件

就可以了。过去,我曾以类似的方式使用过vim,但它确实有效。它可能不是安全复制粘贴的,OP应该进行一些测试,并可能使命令适应他们的需求。
要确保,您可能想要删除最后的vim开关,并目视检查文件是否存在正确性。

评论


vim将复制原始文件,因此您将需要OP没有的磁盘上有700G的可用空间。

–StéphaneChazelas
20/09/22在12:49



我添加了另一个开关:-c“:set nobackup nowritebackup”来解决让我注意到的问题

–znpy
20/09/22在13:06



@znpy我使用“:3delete”在一个小文件上对此进行了测试,它仅删除了第三行,而保留了前两行。如何完全摆脱前n行?

–l0k3ndr
20-09-22在14:04

您还需要noswapfile。但是,然后整个文件将被加载到内存中。对于700G大文件,这可能不是一个选择。

–StéphaneChazelas
20-09-22在14:09

正确的删除命令似乎是-c“:1,300000000delete”,但是stil留下了vim尝试将整个文件加载到内存的问题。

– JanKanis
20-09-22在14:34

#12 楼

您可以就地读写文件,然后截断文件。不确定,甚至可以使用cli工具来执行此操作,但这是在Java中(未经测试)。
RandomAccessFile out = new RandomAccessFile("file.txt", "rw");
RandomAccessFile in = new RandomAccessFile("file.txt", "r");
String line = null;
long rows = 0;
while( (line=in.readLine()) != null ){
    if( rows > 300000000 ) {
        out.writeBytes(line);
        out.write('\n');
    }
    rows++;
}
in.close();
out.setLength( out.getFilePointer() );
out.close();


评论


您需要跟踪已经在其他文件中移动的数据-一个简单的文本文件,最后成功写入偏移量即可。如果就地重写过程由于任何原因中断,将需要它,因此可以从同一位置恢复它。

–奥列格·沃尔科夫(Oleg V. Volkov)
20-09-22在17:25

同意,那是谨慎的。

–克里斯·塞琳(Chris Seline)
20-09-23在19:32

#13 楼

我会以
 <?php
$fp1 = fopen("file.txt", "rb");
// find the position of the 3M'th line:
for ($i = 0; $i < 300_000_000; ++ $i) {
    fgets($fp1);
}
// the next fgets($fp1) call will read line 3M+1 :)
$fp2 = fopen("file.txt", "cb");
// copy all remaining lines from fp1 to fp2
while (false !== ($line = fgets($fp1))) {
    fwrite($fp2, $line);
}
fclose($fp1);
// remove every line that wasn't copied over to fp2
ftruncate($fp2, ftell($fp2));
fclose($fp2);
 

进行操作,或者如果出于某些原因需要它快速运行,我会做在具有mmap()内存映射的C ++中,这应该运行得快得多:
 #include <iostream>
#include <fstream>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>


int main(){
    const std::string target_file = "file.txt";
    std::fstream fp1(target_file, std::fstream::binary);
    fp1.exceptions(std::fstream::failbit | std::fstream::badbit);
    fp1.seekg(0, std::fstream::end);
    const std::streampos total_file_size_before_truncation = fp1.tellg();
    fp1.seekg(0, std::fstream::beg);
    const int fd = open(target_file.c_str(), O_RDWR);
    char *content_mmaped = (char *)mmap(NULL, total_file_size_before_truncation, PROT_READ, MAP_PRIVATE, fd, 0);
    const std::string_view content_view(content_mmaped, total_file_size_before_truncation);
    size_t line_no = 0;
    size_t line_pos = 0;
    size_t i = 0;
    for(; i < total_file_size_before_truncation; ++i){
        if(content_mmaped[i] == '\n'){
            ++line_no;
            line_pos = i;
            if(line_no >= (3000000-1)){
                break;
            }
        }
    }
    // idk why i have to do all those casts...
    fp1.write(&content_mmaped[i], std::streamoff(std::streamoff(total_file_size_before_truncation)-std::streamoff(i)));
    fp1.close();
    munmap(content_mmaped, total_file_size_before_truncation);
    ftruncate(fd, i);
    close(fd);
}
 


(但是如果我不需要速度,我可能会使用第一种方法,因为代码更易于阅读,因此可能不太可能包含错误。