我有一个巨大的文件(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指出的那样,您可以在单个大行中检测到一些重复的字符作为自定义分隔符吗?

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

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

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

您可以使用带有-b选项的split定义块文件大小(以字节为单位)。依次使用sed进行处理,然后重新组装。还有一个风险是,可以分割在两个文件中,不会被发现...

#1 楼

常规的文本处理工具并非旨在处理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一起使用。如果您可以选择文件的最后一个字符作为记录分隔符,则可以避免任何可移植性麻烦。

评论


我没有要测试的文件,但是在Awk中,您可以指定“记录分隔符”和“输出记录分隔符”。因此,假如你在你的文件的逗号体面的一知半解,这是可能的,你可以有解决这个问题:AWK -v RS =,-v ORS =“{GSUB(/ /, “”);打印}'不?

–通配符
17年12月30日在7:33

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

–吉尔斯'所以-不再是邪恶的'
17年12月30日在11:20

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

– beasy
17年12月31日在22:27

@Gilles:但是使用awk避免将流两次传递给tr。那么它还会变慢吗?

–user285259
18年1月1日在22:20

@ user285259通常不会。 tr非常快,甚至可以并行化管道。

–吉尔斯'所以-不再是邪恶的'
18年1月2日在7:24

#2 楼

对于这么大的文件,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


评论


make具有默认的规则,而不是flex / cc,您可以添加%option main作为unk.l的第一行,然后进行unk。我或多或少地快速使用了%option main 8bit,并在我的.bashrc中导出了CFLAGS ='-march = native -pipe -Os'。

– jthill
17年12月30日在17:16

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

–jamesqf
17年12月31日下午4:50

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

– JJoao
18年1月3日在16:49

如您所说,@ jamesqf-很难使它成为一个主题问题-但我也希望看到它

–史蒂芬·潘妮(Steven Penny)
18年1月4日,0:57

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

– Paul Evans
18年1月4日在20:37

#3 楼

因此,您没有足够的物理内存(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?)>

–拉胡尔
17年12月31日在9:04

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

– sourcejedi
17年12月31日在11:12



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

– sourcejedi
17年12月31日在13:50

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

–拉胡尔
18年1月1日在8:07

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

– sourcejedi
18年1月1日在10:30



#4 楼

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

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

#5 楼

我认为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>模式错误。

评论


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

–RiaD
17年12月30日在1:30

代码也不适用于字符串“ << unk>” ideone.com/ncM2yy

–RiaD
17年12月30日在1:31

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

– Peter Cordes
17年12月30日在6:58

从我的头顶开始,getchar和putchar很慢。

– Rui F Ribeiro
17年12月30日在17:20

如果模式以重复的字符序列开头,则对“ << unk>”程序的修复仍然无效(即,如果您尝试用斑马替换aardvark并且输入了aaardvak,则该修复将不起作用,或者您正在尝试替换ababc并输入了abababc)。通常,除非您知道所读取的字符不可能匹配,否则您不能按所读取的字符数前进。

–icarus
17/12/30在21:27

#6 楼

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中跟踪已从文件中复制了多少字节。

#7 楼

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

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


这里,折叠将最多捕获1024个字节,但是-s可以确保它如果自上次中断以来至少有一个中断,则在一个空格上中断。

sed命令由您执行并执行您所期望的操作。

然后tr命令将“展开”该文件会将插入的换行符转换回为空。

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

下面是一个将每个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' 'q4312078q'
test XJS C4JD QADn1 nSBn3 2IDnEn GTUBE STAnDARD AnTI UBE-TEST EMAIL*C.34X test


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

评论


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

–rackandboneman
18年1月2日在15:00

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

–alfreema
18年1月2日在20:53



#8 楼

使用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++;
}


如果您要走这条路


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

评论


如果什么落在块之间的边界?

– liori
18年1月2日在20:59

#9 楼

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

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

#10 楼

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

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


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

评论


我也在考虑这种方法,但是使用了while读取-N 1000块; (以1000个为例)。对于溶液中的,块之间断开,是两次通过该文件:所述第一与100MB块和与“100MB + 5字节”块的第二个。但是对于70GB的文件,这不是最佳解决方案。

– MiniMax
17年12月29日在22:07

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

–roaima
17/12/29在23:23

@MiniMax,即第二遍将不一定帮助作为第一通会加入5个字节为每次出现。

–StéphaneChazelas
17年12月30日在22:32

@roaima,是的,这将是一个涉及更多的解决方案。这里它是一个简单的方法是仅极有可能发生(假设出现远远APPART,如果没有,使用$ / = “>” 和s / \ Z / /克)是正确的。

–StéphaneChazelas
17年12月30日在22:35

#11 楼

这是一个执行任务(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运行。
编辑:
对不起,我没有读到所有内容都在一行中,所以我现在尝试逐字符读取文件。
编辑II:
应用了相同的修复程序关于C程序。

评论


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

–猫
17年12月29日在19:08

它逐字符读取文件,并且永远不会将整个文件(仅单个字符)保存在内存中。

–帕特里克·布彻(Patrick Bucher)
17年12月29日在19:10

Scanner.Split(bufio.ScanRunes)发挥了神奇作用。

–帕特里克·布彻(Patrick Bucher)
17年12月29日在19:27

另外,请检查go doc bufio.MaxScanTokenSize以获取默认缓冲区大小。

–帕特里克·布彻(Patrick Bucher)
17年12月29日在19:39

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

–icarus
17年12月30日在21:59

#12 楼

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

https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html上的官方教程使用Java(极其简单),但是您可以找到Perl或任何您喜欢使用的语言的客户端库。

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

评论


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

– sourcejedi
18年1月4日在17:36

#13 楼

如果我们有最小数量的<unk>(如齐普夫定律所预期),

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


评论


sed每次都将一行读入内存。它将无法适应这条线。

– Kusalananda♦
18 Mar 16 '18在9:34

我找不到任何说明使用该标志时GNU sed不会进行输入/输出缓冲的文档。我看不到它将读取部分行。

– Kusalananda♦
18 Mar 16 '18在9:53

这个awk,而不是sed->赞成

–botkop
20/12/08在14:57



#14 楼

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

1)如果我理解你的具体情况正确是可以接受更换与相同长度的其他一些字符串?

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

我确定您已经解决了今年以来的问题,并且我想知道您使用了哪种解决方案。 >
我提出了一个解决方案(最可能在C中),该方案将读取文件的BLOCKS,并在考虑可能的块交叉的情况下搜索每个字符串。找到后,将字符串替换为SAME长度替代,并仅写入该BLOCK。持续已知的出现次数或直到文件结束。这将需要最少的写入次数,最多需要两次写入(如果每个事件均被分成两个块)。这将不需要额外的空间!