【问题标题】:Best way to replace a list of tokens in a text file替换文本文件中标记列表的最佳方法
【发布时间】:2020-10-07 23:58:45
【问题描述】:

我有一个文本文件(没有标点符号),文件大小约为 100MB - 1GB,下面是一些示例行:

please check in here
i have a full hd movie
see you again bye bye
press ctrl c to copy text to clipboard
i need your help
...

还有一个替换标记列表,如下所示:

check in -> check_in
full hd -> full_hd
bye bye -> bye_bye
ctrl c -> ctrl_c
...

在文本文件上替换后我想要的输出如下:

please check_in here
i have a full_hd movie
see you again bye_bye
press ctrl_c to copy text to clipboard
i need your help
...

我目前的做法

replace_tokens = {'ctrl c': 'ctrl_c', ...} # a python dictionary
for line in open('text_file'):
  for token in replace_tokens:
    line = re.sub(r'\b{}\b'.format(token), replace_tokens[token])
    # Save line to file

此解决方案有效,但对于大量替换标记和大型文本文件来说这非常慢。有没有更好的解决方案?

【问题讨论】:

  • 你所有的token都是用空格隔开的两个词吗?
  • 可能是两三个字,差不多就是两个。
  • 如果您有大量计算要做,请使用迭代器
  • 这能回答你的问题吗? Python: words replacing in huge text

标签: python token


【解决方案1】:

您至少可以通过以下方式来消除内部循环的复杂性:

import re 

tokens={"check in":"check_in", "full hd":"full_hd",
"bye bye":"bye_bye","ctrl c":"ctrl_c"}

regex=re.compile("|".join([r"\b{}\b".format(t) for t in tokens]))

with open(your_file) as f:
    for line in f:
        line=regex.sub(lambda m: tokens[m.group(0)], line.rstrip())
        print(line)

打印:

please check_in here
i have a full_hd movie
see you again bye_bye
press ctrl_c to copy text to clipboard
i need your help

【讨论】:

  • Ir 似乎加入正则表达式已经过测试here。 Ir 对性能没有太大的好处。
  • 文件只有 1GB,尝试一次读取整个文件,而不是逐行读取也是值得的。
  • 尝试使用re2 库?根据正则表达式,它可能比内置的要快得多...
  • 我使用带有正则表达式组的内置 re lib 进行了测试,例如dawg 答案,建议阅读sabik 的整个文本文件。哇,它比我的解决方案快得多。
  • @nguyenvanhieuvn — 请问使用rere2 有什么区别吗?我真的很好奇它是否有帮助(以及有多少)......
【解决方案2】:
  • 正如其他人所建议的,制作单个正则表达式将消除内部循环。

    regex = re.compile("|".join(r"\b{}\b".format(t) for t in tokens))
    
  • re2 库可以比内置的 re 快​​得多,尤其是在有大量标记和/或大量文本的情况下。

    regex = re2.compile("|".join(r"\b{}\b".format(t) for t in tokens))
    
  • 根据内存量和文件的可能大小,尝试一次读取整个内容而不是逐行读取可能是值得的。特别是如果行很短,即使您实际上并没有进行任何面向行的处理,处理这些行也可能会花费大量时间。

    text = f.read()
    text = regex.sub(lambda m: tokens[m.group(0)], text)
    

    进一步的改进将使用findall/finditer 而不是sub,然后使用start/end 偏移输出原始文件的片段,与替换交错;这样可以避免在内存中有两个文本副本。

    text = f.read()
    pos = 0
    for m in regex.finditer(text):
        out_f.write(text[pos:m.start(0)])
        out_f.write(tokens[m.group(0)])
        pos = m.end(0)
    out_f.write(text[pos:])
    
  • 如果您的文本是换行的,您可能还希望考虑是否需要替换短语被换行的实例;这可以通过“将整个文本读入内存”方法轻松完成。如果您需要这样做,但文件太大而无法读入内存,则可能需要执行面向字的方法——一个函数读取文件并产生单个字,另一个函数执行面向字的有限状态机。

【讨论】:

    【解决方案3】:

    使用二进制文件和字符串替换如下

    • 将文件处理为二进制文件以减少文件转换的开销
    • 使用字符串替换而不是正则表达式

    代码

    def process_binary(filename):
        """ Replace strings using binary and string replace
            Processing follows original code flow except using
            binary files and string replace """
    
        # Map using binary strings
        replace_tokens = {b'ctrl c': b'ctrl_c', b'full hd': b'full_hd', b'bye bye': b'bye_bye', b'check in': b'check_in'}
    
        outfile = append_id(filename, 'processed')
    
        with open(filename, 'rb') as fi, open(outfile, 'wb') as fo:
            for line in fi:
                for token in replace_tokens:
                    line = line.replace(token, replace_tokens[token])
                fo.write(line)
    
    def append_id(filename, id):
        " Convenience handler for generating name of output file "
        return "{0}_{2}.{1}".format(*filename.rsplit('.', 1) + [id])
    

    性能比较

    在 124 MB 文件上(通过复制发布的字符串生成):

    • 发布的解决方案:82.8 秒
    • 避免正则表达式中的内循环(DAWG 帖子):28.2 秒
    • 当前解决方案:9.5 秒

    当前解决方案:

    • 比已发布的解决方案改进了约 8.7 倍,并且
    • ~3X 超过 Regex(避免内循环)

    总体趋势

    测试代码

    # Generate Data by replicating posted string
    s = """please check in here
    i have a full hd movie
    see you again bye bye
    press ctrl c to copy text to clipboard
    i need your help
    """
    with open('test_data.txt', 'w') as fo:
        for i in range(1000000):  # Repeat string 1M times
            fo.write(s)
    
    # Time Posted Solution
    from time import time
    import re
    
    def posted(filename):
        replace_tokens = {'ctrl c': 'ctrl_c', 'full hd': 'full_hd', 'bye bye': 'bye_bye', 'check in': 'check_in'}
    
        outfile = append_id(filename, 'posted')
        with open(filename, 'r') as fi, open(outfile, 'w') as fo:
            for line in fi:
                for token in replace_tokens:
                    line = re.sub(r'\b{}\b'.format(token), replace_tokens[token], line)
                fo.write(line)
    
    def append_id(filename, id):
        return "{0}_{2}.{1}".format(*filename.rsplit('.', 1) + [id])
    
    t0 = time()
    posted('test_data.txt')
    print('Elapsed time: ', time() - t0)
    # Elapsed time:  82.84100198745728
    
    # Time Current Solution
    from time import time
    
    def process_binary(filename):
        replace_tokens = {b'ctrl c': b'ctrl_c', b'full hd': b'full_hd', b'bye bye': b'bye_bye', b'check in': b'check_in'}
    
        outfile = append_id(filename, 'processed')
        with open(filename, 'rb') as fi, open(outfile, 'wb') as fo:
            for line in fi:
                for token in replace_tokens:
                    line = line.replace(token, replace_tokens[token])
                fo.write(line)
    
    def append_id(filename, id):
        return "{0}_{2}.{1}".format(*filename.rsplit('.', 1) + [id])
    
    
    t0 = time()
    process_binary('test_data.txt')
    print('Elapsed time: ', time() - t0)
    # Elapsed time:  9.593998670578003
    
    # Time Processing using Regex 
    # Avoiding inner loop--see dawg posted answer
    
    import re 
    
    def process_regex(filename):
        tokens={"check in":"check_in", "full hd":"full_hd",
        "bye bye":"bye_bye","ctrl c":"ctrl_c"}
    
        regex=re.compile("|".join([r"\b{}\b".format(t) for t in tokens]))
    
        outfile = append_id(filename, 'regex')
        with open(filename, 'r') as fi, open(outfile, 'w') as fo:
            for line in fi:
                line = regex.sub(lambda m: tokens[m.group(0)], line)
                fo.write(line)
    
    def append_id(filename, id):
        return "{0}_{2}.{1}".format(*filename.rsplit('.', 1) + [id])
    
    t0 = time()
    process_regex('test_data.txt')
    print('Elapsed time: ', time() - t0)
    # Elapsed time:  28.27900242805481
    

    【讨论】:

      【解决方案4】:

      为了获得最佳性能,您应该使用一种旨在同时在文本中搜索多种模式的算法。有几种这样的算法,例如Aho-CorasickRabin-KarpCommentz-Walter

      aho-corasick 算法的实现可以在 on PyPI 找到。

      【讨论】:

        猜你喜欢
        • 2010-09-06
        • 2011-02-19
        • 2015-10-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-04-15
        • 2010-10-25
        相关资源
        最近更新 更多