Лучший способ заменить список токенов в текстовом файле - PullRequest
7 голосов
/ 18 июня 2020

У меня есть текстовый файл (без знаков препинания), размер файла составляет примерно 100 МБ - 1 ГБ, вот пример строки:

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

Это решение работает, но это очень медленно для большого количества токенов замены и большого текстового файла. Есть ли лучшее решение?

Ответы [ 4 ]

4 голосов
/ 18 июня 2020

Вы можете, по крайней мере, убрать сложность внутреннего l oop, выполнив следующие действия:

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
3 голосов
/ 18 июня 2020

Используйте двоичные файлы и замену строки следующим образом

  • Обработайте файлы как двоичные, чтобы уменьшить накладные расходы на преобразование файлов
  • Используйте замену строки вместо Regex

Код

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 Мбайт (сгенерировано репликацией опубликованной строки):

  • Размещенное решение: 82,8 секунды
  • Избегайте внутреннего l oop в Regex (сообщение DAWG): 28,2 секунды
  • Текущее решение: 9,5 секунд

Текущее решение :

  • ~ 8,7X улучшение по сравнению с опубликованным решением и
  • ~ 3X по сравнению с Regex (без внутреннего l oop)

Общая тенденция

Curve using Perfplot based upon timeit

Код теста

# 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
1 голос
/ 18 июня 2020
  • Как предлагали другие, создание одного регулярного выражения устранит внутренний l oop.

    regex = re.compile("|".join(r"\b{}\b".format(t) for t in tokens))
    
  • re2* Библиотека 1010 * может быть намного быстрее, чем встроенная 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:])
    
  • Если ваш текст переносится по строкам, вы также можете sh решить, нужно ли вам заменять экземпляры где фраза разбита через строку; это легко сделать с помощью подхода «зачитать весь текст в память». Если вам нужно это сделать, но файл слишком велик для чтения в память, вам может потребоваться использовать словесно-ориентированный подход - одна функция считывает файл и выдает отдельные слова, а другая выполняет ориентированный на слова конечный автомат.

0 голосов
/ 16 июля 2020

Для наилучшей производительности следует использовать алгоритм, предназначенный для одновременного поиска в тексте нескольких шаблонов. Существует несколько таких алгоритмов, например Aho-Corasick , Rabin-Karp и Commentz-Walter .

Реализация aho- алгоритм corasick можно найти на PyPI .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...