将字符串替换为一个巨大的(70GB)一行文本文件


126

我有一个巨大的(70GB),一行,文本文件,我想替换其中的一个字符串(令牌)。我想<unk>用另一个虚拟令牌替换该令牌(手套发行)。

我试过了sed

sed 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

但是输出文件corpus.txt.new有零字节!

我也尝试使用perl:

perl -pe 's/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

但是我遇到了内存不足的错误。

对于较小的文件,以上两个命令均有效。

如何替换这样的文件字符串? 是一个相关的问题,但是没有一个答案对我有用。

编辑:将文件分成10GB(或其他大小)的块,然后分别应用于sed每个文件,然后与它们合并,该cat怎么办?那有意义吗?有没有更优雅的解决方案?


正如@Gilles指出的那样,您可以在单个大行中检测到一些可以用作自定义分隔符的重复字符吗?
RomanPerekhrest

我认为仅能搜索和替换而不能处理任何更复杂的正则表达式的工具会更快。一次也不会受益于一行,因此不会阻塞该文件。不幸的是,尽管编写起来并不困难,但我不知道这种工具的存在。如果不是,那么用答案之一中的换行符替换可能是最简单的。
ctrl-alt-delor

您的文件中是否包含ASCII以外的其他内容?如果是这样,则可以省略所有的unicode处理,并且可以处理原始字节。
帕特里克·布彻

我同意@PatrickButcher的观点。除了需要立即替换此文本外,该文件还应用于什么?如果它是某种日志,那么没人会有效地使用它。如果它是某些应用程序使用的数据文件,则该应用程序应负责维护该文件中的数据。
托马斯·卡莱尔

2
您可以使用split带有-b选项定义字节块的文件大小。依次使用进行处理,然后sed重新组装。有一个危险,就是<unk>可以分为两个文件,并且找不到......
Vladislavs Dovgalecs

Answers:


106

普通的文本处理工具并非旨在处理RAM中不适合的行。他们倾向于通过读取一条记录(一行),对其进行操作并输出结果,然后进行下一条记录(一行)来工作。

如果某个ASCII字符经常出现在文件中,而没有出现在<unk>或中<raw_unk>,则可以将其用作记录分隔符。由于大多数工具不允许自定义记录分隔符,因此请在该字符和换行符之间进行交换。tr处理字节而不是行,因此它不关心任何记录大小。假设可行;

<corpus.txt tr '\n;' ';\n' |
sed 's/<unk>/<raw_unk>/g' |
tr '\n;' ';\n' >corpus.txt.new

您还可以锚定要搜索的文本的第一个字符,前提是该字符不会在搜索文本中重复出现并且显示频率很高。如果文件开头为unk>,请更改sed命令sed '2,$ s/…以避免虚假匹配。

<corpus.txt tr '\n<' '<\n' |
sed 's/^unk>/raw_unk>/g' |
tr '\n<' '<\n' >corpus.txt.new

或者,使用最后一个字符。

<corpus.txt tr '\n>' '>\n' |
sed 's/<unk$/<raw_unk/g' |
tr '\n>' '>\n' >corpus.txt.new

请注意,该技术假定sed在不以换行符结尾的文件上无缝运行,即,它处理最后的部分行而不会被截断,也不会添加最终的换行符。它与GNU sed一起使用。如果您可以选择文件的最后一个字符作为记录分隔符,则可以避免任何可移植性麻烦。


8
我没有要测试的文件,但是在Awk中,您可以指定“记录分隔符”和“输出记录分隔符”。因此,假设您的文件中有少量逗号,则可以使用以下方法解决此问题:awk -v RS=, -v ORS=, '{gsub(/<unk>/, "<raw_unk>"); print}' 不?
通配符

4
@Wildcard是的,这是另一种解决方案。Awk往往比sed慢,这就是为什么我不提供它作为大文件的首选解决方案的原因。
吉尔斯

您可以使用命令行选项-0和char的八进制值在Perl中设置记录分隔符,也可以在脚本中使用特殊变量设置记录分隔符$/
beasy

@Gilles:但请awk避免将流两次传递给tr。那么它还会变慢吗?
user285259'1

2
@ user285259通常不会。tr速度非常快,甚至可以并行化管道。
吉尔斯

110

对于这么大的文件,Flex就是一种可能。令unk.l是:

%%
\<unk\>     printf("<raw_unk>");  
%%

然后编译并执行:

$ flex -o unk.c  unk.l
$ cc -o unk -O2 unk.c -lfl
$ unk < corpus.txt > corpus.txt.new

5
make对此具有默认规则,而不是flex / cc,您可以%option main在unk.l的第一行添加一个,然后添加make unk。我或多或少地反身使用%option main 8bit fast,并且拥有export CFLAGS='-march=native -pipe -Os'我的.bashrc
jthill

1
@undercat:如果不是那么离题,我可以向您展示许多非编译器前端应用程序,从解决水位问题到专用输入解析。如果您在框外稍微想一想,您可以使用它做的事真是太神奇了:-)
jamesqf

@jthill,谢谢:%option main+ make+ CFLAGS是一个非常好的技巧!是-march=native默认行为吗?
JJoao

1
@jamesqf如你所说-将很难作出这样的一个话题上的问题-但我想看到它也
史蒂芬竹篙

1
@jamesqf uni的一个教授使用flex来构建一种可以识别工厂面料类型的工具!问一下类似的问题:“ flex似乎是一个非常强大的工具,但我不太可能编写任何编译器/解析器-flex是否还有其他用例?”
Paul Evans

41

因此,您没有足够的物理内存(RAM)来一次容纳整个文件,但是在64位系统上,您有足够的虚拟地址空间来映射整个文件。在这种情况下,虚拟映射可用作简单的技巧。

所有必需的操作都包含在Python中。有一些烦人的微妙之处,但确实避免了编写C代码的麻烦。特别是,需要注意避免将文件复制到内存中,这将完全破坏这一点。从好的方面来说,您可以免费获得错误报告(python“ exceptions”):)。

#!/usr/bin/python3
# This script takes input from stdin
# (but it must be a regular file, to support mapping it),
# and writes the result to stdout.

search = b'<unk>'
replace = b'<raw_unk>'


import sys
import os
import mmap

# sys.stdout requires str, but we want to write bytes
out_bytes = sys.stdout.buffer

mem = mmap.mmap(sys.stdin.fileno(), 0, access=mmap.ACCESS_READ)
i = mem.find(search)
if i < 0:
    sys.exit("Search string not found")

# mmap object subscripts to bytes (making a copy)
# memoryview object subscripts to a memoryview object
# (it implements the buffer protocol).
view = memoryview(mem)

out_bytes.write(view[:i])
out_bytes.write(replace)
out_bytes.write(view[i+len(search):])

如果我的系统在8 GB内存中有大约4 GB可用内存,那么mem = mmap.mmap(sys.stdin.fileno(),0,access = mmap.ACCESS_READ)意味着将数据放在该空间中吗?还是会更低(1gb?)>
Rahul

1
@Rahul“因此您没有足够的RAM,但是在64位系统上,您有足够的虚拟地址空间来映射整个文件。” 它按需(或缺少)分页进出物理内存。该程序无需大量物理RAM即可运行。64位系统的虚拟地址空间比最大物理内存大得多。此外,每个运行的进程都有自己的虚拟地址空间。这意味着整个系统用尽虚拟地址空间不是问题,也不是一个有效的概念。
sourcejedi

4
@Rahul是的!python mmap.mmap()是围绕C函数mmap()的相当薄的包装器。mmap()与运行可执行文件以及共享库中的代码使用的机制相同。
sourcejedi

2
@jamesqf我可能是错的,但我觉得这只是个人选择。由于性能损失可以忽略不计(因为他说过,该函数实际上确实调用了c函数),因此开销浪费非常低,因为在这之间没有其他事情发生。C会更好一些,但是此解决方案的目的不是为了优化,而只是为了解决更大,更困难的70gb问题。
拉胡尔

1
通常,用python编写会更紧凑。在这种情况下,事实证明python版本中有一些细节,而C版本可能更适合编写。(尽管search可以包含NUL字符并不是那么简单。我注意到这里的其他C版本不支持。中的NUL字符replace。)非常欢迎您派生C版本以进行比较。但是请记住,我的版本包括有关其执行的操作的基本错误报告。当包含错误报告时,C版本至少会更讨厌阅读 IMO。
sourcejedi '18

17

我认为C版本的性能可能会更好:

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

#define PAT_LEN 5

int main()
{
    /* note this is not a general solution. In particular the pattern
     * must not have a repeated sequence at the start, so <unk> is fine
     * but aardvark is not, because it starts with "a" repeated, and ababc
     * is not because it starts with "ab" repeated. */
    char pattern[] = "<unk>";          /* set PAT_LEN to length of this */
    char replacement[] = "<raw_unk>"; 
    int c;
    int i, j;

    for (i = 0; (c = getchar()) != EOF;) {
        if (c == pattern[i]) {
            i++;
            if (i == PAT_LEN) {
                printf("%s", replacement);
                i = 0;
            }
        } else {
            if (i > 0) {
                for (j = 0; j < i; j++) {
                    putchar(pattern[j]);
                }
                i = 0;
            }
            if (c == pattern[0]) {
                i = 1;
            } else {
                putchar(c);
            }
        }
    }
    /* TODO: fix up end of file if it ends with a part of pattern */
    return 0;
}

编辑:根据评论的建议进行了修改。还修复了模式错误<<unk>


2
您可以打印(pattern [j])而不是(buf [j])(此时它们是相等的,所以您不需要缓冲
RiaD

3
字符串“ << unk>” ideone.com/ncM2yy
RiaD '17

10
0.3秒内30 MB?仅90 MB /秒。 memcpy在最近的x86 CPU(例如Skylake)上,速度(即内存瓶颈)约为12GB /秒。即使有stdio +系统调用开销,对于磁盘高速缓存中30MB的文件仍很热的情况,我希望可能有1GB /秒的高效实现。您是否在禁用优化的情况下进行编译,还是一次一字符的I / O真的那么慢? getchar_unlocked/ putchar_unlocked也许会有帮助,但绝对更好的阅读中,也许128kiB(在大多数x86 CPU的二级缓存大小的一半,所以你主要是在L2命中,而循环后读)块/写
彼得·柯德斯

2
从我的头顶开始,getchar和putchar 慢。
Rui F Ribeiro

3
fix该方案"<<unk>",如果仍然不能正常工作pattern与字符的重复序列开始(即它不会工作,如果你试图用斑马取代土豚,你不得不aaardvak的输入,或者你想取代ababc和输入了abababc)。通常,除非您知道所读取的字符不可能匹配,否则您不能按所读取的字符数前进。
icarus

16

replacemariadb-server / mysql-server软件包中有一个实用程序。它替换了简单的字符串(不是正则表达式),并且与grep / sed / awk不同,replace它不关心\nand \0。内存消耗对于任何输入文件都是恒定的(在我的机器上约为400kb)。

当然,您不需要运行mysql服务器即可使用replace,它仅以这种方式打包在Fedora中。其他发行版/操作系统可能会将其单独包装。


14

GNU grep可以向您显示“二进制”文件中的匹配偏移量,而无需将整行读入内存。然后,您可以使用dd来读取此偏移量,跳过匹配项,然后继续从文件中复制。

file=...
newfile=...
replace='<raw_unk>'
grep -o -b -a -F '<unk>' <"$file" |
(   pos=0
    while IFS=$IFS: read offset pattern
    do size=${#pattern}
       let skip=offset-pos
       let big=skip/1048576
       let skip=skip-big*1048576
       dd bs=1048576 count=$big <&3
       dd bs=1 count=$skip <&3
       dd bs=1 count=$size of=/dev/null <&3
       printf "%s" "$replace"
       let pos=offset+size
    done
    cat <&3
) 3<"$file" >"$newfile"

为了提高速度,我将其dd拆分为大块读取的块大小1048576和一次较小的读取的1字节,但是对于如此大的文件,此操作仍然会有点慢。的grep输出是,例如,13977:<unk>,这是由读取到变量对结肠分割offsetpattern。我们必须跟踪pos已经从文件中复制了多少字节。


11

这是另一个UNIX命令行,它可能比其他选项性能更好,因为您可以“寻找”性能良好的“块大小”。为了使这种方法更可靠,您需要知道每个X字符中至少有一个空格,其中X是您的任意“块大小”。在下面的示例中,我选择了1024个字符的“块大小”。

fold -w 1024 -s corpus.txt | sed 's/<unk>/<raw_unk>/g' | tr '/n' '/0'

在这里,fold 最多可以抓取1024个字节,但是-s可以确保它自上次中断以来至少有一个中断在一个空格上中断。

sed命令是您的命令,可以执行您期望的操作。

然后,tr命令将“展开”文件,将插入的换行符恢复为空。

您应该考虑尝试更大的块大小,以查看其执行速度是否更快。对于fold的-w选项,您可以尝试使用10240和102400和1048576而不是1024。

这是一个按步骤分解的示例,该步骤将所有N都转换为小写:

[root@alpha ~]# cat mailtest.txt
test XJS C4JD QADN1 NSBN3 2IDNEN GTUBE STANDARD ANTI UBE-TEST EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt
test XJS C4JD QADN1
NSBN3 2IDNEN GTUBE
STANDARD ANTI
UBE-TEST
EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt | sed 's/N/n/g'
test XJS C4JD QADn1
nSBn3 2IDnEn GTUBE
STAnDARD AnTI
UBE-TEST
EMAIL*C.34X test

[root@alpha ~]# fold -w 20 -s mailtest.txt | sed 's/N/n/g' | tr '\n' '\0'
test XJS C4JD QADn1 nSBn3 2IDnEn GTUBE STAnDARD AnTI UBE-TEST EMAIL*C.34X test

如果文件末尾有换行符,则需要在该文件的末尾添加换行符,因为tr命令将删除它。


1
如何确保在空白空间不足的情况下不会破坏模式?
rackandboneman

1
如上所述,要使其健壮,需要每个X字符至少有一个空格。您可以通过选择的任何块大小来轻松进行分析:fold -w X mailtest.txt | grep -v“” | wc -l返回的数字是具有潜在边缘情况的折叠线的数量。如果为零,则保证解决方案有效。
alfreema '18年

10

使用 perl

管理自己的缓冲区

您可以使用IO::Handlesetvbuf管理默认的缓冲区,或者你可以管理自己的缓冲区sysreadsyswrite。检查perldoc -f sysreadperldoc -f syswrite获得更多信息,从本质上讲,它们跳过了缓冲的io。

在这里,我们滚动了自己的缓冲区IO,但是我们手动且任意地对1024个字节进行操作。我们还打开了RW文件,因此我们一次在同一FH上完成所有操作。

use strict;
use warnings;
use Fcntl qw(:flock O_RDWR);
use autodie;
use bytes;

use constant CHUNK_SIZE => 1024 * 32;

sysopen my $fh, 'file', O_RDWR;
flock($fh, LOCK_EX);

my $chunk = 1;
while ( sysread $fh, my $bytes, CHUNK_SIZE * $chunk ) {
  if ( $bytes =~ s/<unk>/<raw_unk>/g ) {
    seek( $fh, ($chunk-1)* CHUNK_SIZE, 0 );
    syswrite( $fh, $bytes, 1024);
    seek( $fh, $chunk * CHUNK_SIZE, 0 );
  }
  $chunk++;
}

如果你要走这条路

  1. 确保<unk><raw_unk>相同的字节大小。
  2. CHUNKSIZE如果要替换的字节数超过1个字节,则可能需要确保我们的缓冲方法不会越界。

2
如果<unk>落在块之间的边界上怎么办?
liori

8

您可以尝试bbe二进制块编辑器),“ sed用于二进制文件”。

我在没有EOL字符的7GB文本文件上使用它取得了成功,并用不同长度的字符串替换了多次出现的字符串。在不进行任何优化的情况下,它提供的平均处理吞吐量> 50MB / s。


5

使用perl,您可以处理固定长度的记录,例如:

perl -pe 'BEGIN{$/=\1e8}
          s/<unk>/<raw_unk>/g' < corpus.txt > corpus.txt.new

并希望<unk>这些100MB记录中的两个不会跨越。


我也在考虑这种方法,但是使用while read -N 1000 chunk;1000以示例为例)。解决方案(在<unk>块之间拆分)是通过文件的两次遍历:第一次使用100MB块,第二次使用'100MB + 5字节'块。但是对于70GB的文件,它不是最佳解决方案。
MiniMax

3
您甚至不需要两次通过。读取块A。不是EOF块,则读取块B。在A + B中搜索/​​替换。A:=B。循环。复杂性确保您在替换过程中不进行替换。
roaima

@MiniMax,第二遍并不一定会有所帮助,因为第一遍会为每次出现添加5个字节<unk>
斯特凡Chazelas

1
@roaima,是的,这将是一个涉及更多的解决方案。在这里,这是一种简单的方法,只有在很可能的情况下(假设<unk>发生的情况很正确,如果不是,则使用$/ = ">"s/<unk>\z/<raw_unk>/g)是正确的。
斯特凡Chazelas

5

这是一个执行任务(unk.go)的小型Go程序:

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
)

func main() {
    const (
        pattern     = "<unk>"
        replacement = "<raw_unk>"
    )
    var match int
    var char rune
    scanner := bufio.NewScanner(os.Stdin)
    scanner.Split(bufio.ScanRunes)
    for scanner.Scan() {
        char = rune(scanner.Text()[0])
        if char == []rune(pattern)[match] {
            match++
            if match == len(pattern) {
                fmt.Print(replacement)
                match = 0
            }
        } else {
            if match > 0 {
                fmt.Print(string(pattern[:match]))
                match = 0
            }
            if char == rune(pattern[0]) {
                match = 1
            } else {
                fmt.Print(string(char))
            }
        }
    }
    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

只需使用构建它go build unk.go并以身份运行它即可./unk <input >output

编辑:

抱歉,我没有读到所有内容都在一行中,所以我现在尝试逐字符读取文件。

编辑二:

应用与C程序相同的修复程序。


1
这样可以避免将整个文件读入内存吗?

1
它逐个字符地读取文件,并且永远不会将整个文件(仅单个字符)保存在内存中。
帕特里克·布彻

1
scanner.Split(bufio.ScanRunes)做魔术。
帕特里克·布彻

还要检查go doc bufio.MaxScanTokenSize默认缓冲区大小。
帕特里克·布彻

像您的C程序一样,这对于用aaardvark的输入用斑马替换aardvark无效。
icarus

1

对于70GB的文件和简单的搜索与替换来说,这可能是过大的杀伤力,但是Hadoop MapReduce框架现在可以免费解决您的问题(将其设置为在本地运行时选择“单节点”选项),并且可以将来可以扩展到无限容量,而无需修改代码。

https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html上的官方教程使用(极其简单)Java,但是您可以找到Perl或无论您使用哪种语言。

因此,如果以后发现您要对7000GB的文本文件执行更复杂的操作-并且每天必须执行100次此操作-您可以将工作负载分布在您配置的或由云自动配置的多个节点上,基于Hadoop集群。


1
是的,是的。 “不要使用Hadoop-您的数据不是那么大”。这是一个非常简单的流IO问题。
sourcejedi

0

之前的所有建议都需要读取整个文件并写入整个文件。这不仅需要很长时间,而且还需要70GB的可用空间。

1)如果我明白你特定情况下正确地会是可接受的,以取代<UNK>具有相同的长度的一些其它字符串?

2a)是否有多​​次出现?2b)如果是这样,您知道多少吗?

我确定您已经解决了今年以来的问题,并且我想知道您使用了哪种解决方案。

我提出了一个解决方案(最有可能在C中),该方案将读取文件的BLOCKS,并在每个字符串中搜索字符串,同时考虑到可能的块交叉。找到后,将字符串替换为SAME长度备用,并仅写入该BLOCK。持续已知的出现次数或直到文件结束。这将需要最少次数的写入,而最多需要两次写入(如果每个事件都被分成两个块)。这将不需要额外的空间!


-1

如果我们有最低金额<unk>(如齐普夫定律所预期),

awk -v RS="<unk>" -v ORS="<raw_unk>" 1

1
编号sed每次都将一行读入内存。它将无法适应这条线。
库沙兰丹

1
除了使用该标志时,GNU sed不会进行输入/输出缓冲外,我找不到其他任何文档。我看不到它将读取部分行。
库沙兰丹
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.