Python: найти regexp в файле

Есть:

f = open(...)  
r = re.compile(...)

Потребность:
Найти позицию (начало и конец) первого совпадающего регулярного выражения в большом файле?
(начиная с current_pos=...)

Как я могу это сделать?


Я хочу иметь эту функцию:

def find_first_regex_in_file(f, regexp, start_pos=0):  
   f.seek(start_pos)  

   .... (searching f for regexp starting from start_pos) HOW?  

   return [match_start, match_end]  

Ожидается, что файл 'f' будет большим.

Ответ 1

Один из способов поиска по большим файлам - использовать библиотеку mmap, чтобы отобразить файл в большой фрагмент памяти. Затем вы можете выполнить поиск по нему без явного чтения.

Например, что-то вроде:

size = os.stat(fn).st_size
f = open(fn)
data = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)

m = re.search(r"867-?5309", data)

Это хорошо работает для очень больших файлов (я сделал это для файла размером 30+ ГБ, но вам понадобится 64-разрядная ОС, если ваш файл больше, чем один GB или два).

Ответ 2

Следующий код работает достаточно хорошо с тестовыми файлами размером около 2 ГБ.

def search_file(pattern, filename, offset=0):
    with open(filename) as f:
        f.seek(offset)
        for line in f:
            m = pattern.search(line)
            if m:
                search_offset = f.tell() - len(line) - 1
                return search_offset + m.start(), search_offset + m.end()

Обратите внимание, что регулярное выражение не должно содержать несколько строк.

Ответ 3

ПРИМЕЧАНИЕ: это было проверено на python2.7. Возможно, вам придется настраивать вещи в python 3 для обработки строк и байтов, но это не должно быть слишком болезненно, надеюсь.

Файлы с отображением памяти могут быть не идеальны для вашей ситуации (32-разрядный режим увеличивает вероятность того, что недостаточно непрерывной виртуальной памяти, не может читать из труб или других нефайлов и т.д.).

Вот решение, которое читает 128k блоков за раз, и пока ваше регулярное выражение соответствует строке, меньшей этого размера, это будет работать. Также обратите внимание, что вы не ограничены использованием однострочных регулярных выражений. Это решение работает очень быстро, хотя я подозреваю, что он будет немного медленнее, чем использование mmap. Вероятно, это зависит от того, что вы делаете с совпадениями, а также от размера/сложности регулярного выражения, которое вы ищете.

Метод будет содержать только максимум 2 блока в памяти. Возможно, вы захотите принудительно выполнить как минимум 1 совпадение на блок в качестве проверки на работоспособность в некоторых случаях использования, но этот метод будет усечен, чтобы сохранить максимум 2 блока в памяти. Он также гарантирует, что любое совпадение регулярных выражений, которое ест до конца текущего блока, НЕ ДОПУСКАЕТСЯ, и вместо этого последняя позиция сохраняется, когда либо истинный вход исчерпан, либо у нас есть еще один блок, с которым регулярное выражение совпадает до конца, в чтобы лучше сочетать шаблоны типа "[^\n] +" или "xxx $". Вы все еще можете сломать вещи, если у вас есть взгляд в конце регулярного выражения, например xx (?! Xyz), где yz находится в следующем блоке, но в большинстве случаев вы можете работать с такими шаблонами.

import re

def regex_stream(regex,stream,block_size=128*1024):
    stream_read=stream.read
    finditer=regex.finditer
    block=stream_read(block_size)
    if not block:
        return
    lastpos = 0
    for mo in finditer(block):
        if mo.end()!=len(block):
            yield mo
            lastpos = mo.end()
        else:
            break
    while True:
        new_buffer = stream_read(block_size)
        if not new_buffer:
            break
        if lastpos:
            size_to_append=len(block)-lastpos
            if size_to_append > block_size:
                block='%s%s'%(block[-block_size:],new_buffer)
            else:
                block='%s%s'%(block[lastpos:],new_buffer)
        else:
            size_to_append=len(block)
            if size_to_append > block_size:
                block='%s%s'%(block[-block_size:],new_buffer)
            else:
                block='%s%s'%(block,new_buffer)
        lastpos = 0
        for mo in finditer(block):
            if mo.end()!=len(block):
                yield mo
                lastpos = mo.end()
            else:
                break
    if lastpos:
        block=block[lastpos:]
    for mo in finditer(block):
        yield mo

Чтобы протестировать/исследовать, вы можете запустить это:

# NOTE: you can substitute a real file stream here for t_in but using this as a test
t_in=cStringIO.StringIO('testing this is a 1regexxx\nanother 2regexx\nmore 3regexes')
block_size=len('testing this is a regex')
re_pattern=re.compile(r'\dregex+',re.DOTALL)
for match_obj in regex_stream(re_pattern,t_in,block_size=block_size):
    print 'found regex in block of len %s/%s: "%s[[[%s]]]%s"'%(
        len(match_obj.string),
        block_size,match_obj.string[:match_obj.start()].encode('string_escape'),
        match_obj.group(),
        match_obj.string[match_obj.end():].encode('string_escape'))

Вот результат:

found regex in block of len 46/23: "testing this is a [[[1regexxx]]]\nanother 2regexx\nmor"
found regex in block of len 46/23: "testing this is a 1regexxx\nanother [[[2regexx]]]\nmor"
found regex in block of len 14/23: "\nmore [[[3regex]]]es"

Это может быть полезно в сочетании с быстрым анализом большого XML, где его можно разделить на мини-DOM на основе подэлемента как root, вместо того, чтобы погружаться в обработку обратных вызовов и состояний при использовании синтаксического анализа SAX. Он также позволяет вам фильтровать через XML быстрее. Но я использовал его и для тонны других целей. Я очень удивлен, что такие рецепты не доступны в Интернете!

Еще одно: анализ в unicode должен работать до тех пор, пока переданный поток генерирует строки unicode, и если вы используете классы символов, такие как \w, вам нужно добавить флаг re.U в re.compile рисунок конструкция. В этом случае block_size фактически означает количество символов, а не количество байтов.