Самый быстрый способ grep несколько значений из файла в python

  • У меня есть файл из 300 м строк (inputFile), все с двумя столбцами, разделенными вкладкой.
  • У меня также есть список из 1000 уникальных элементов (vals).

Я хочу создать словарь с столбцом 1 как ключ и столбец 2 как значение для всех строк в inputFile, где первые столбцы встречаются в vals. Несколько элементов в vals не появляются в файле, эти значения должны быть сохранены в новом списке. Я могу использовать до 20 потоков, чтобы ускорить этот процесс.

Каков самый быстрый способ достичь этого?

Моя лучшая попытка до сих пор:

newDict = {}
foundVals = []
cmd = "grep \"" + vals[0]
for val in vals:
     cmd = cmd + "\|^"+val+"[[:space:]]"
cmd = cmd + "\" " + self.inputFile
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
for line in iter(p.stdout.readline, ''):
    info = line.split()
    foundVals.append(info[0])
    newDict.update({info[0]:info[1]})
p.wait()
notFound = [x for x in vals if x not in set(foundVals)]

Пример файл_ввод:

2       9913
3       9913
4       9646
...
594592886       32630
594592888       32630
594592890       32630

vals:

[1,2,594592888]
Словарь

:

{2:9913,594592888:32630}

И в notFound:

[1]

Ответ 1

Вы пояснили в комментарии, что каждый ключ встречается не более одного раза в данных. Из этого следует, что существует только 1000 ключей, что количество работы, выполняемой на Python, тривиально; почти все ваше время тратится на выход из grep. Это нормально; ваша стратегия делегирования выделения линии в специализированную утилиту остается разумной. Но это означает, что прирост производительности должен быть найден на стороне извлечения линии.

Вы можете ускорить процесс, оптимизируя ваше регулярное выражение. Например, вместо

^266[[:space:]]\|^801[[:space:]]\|^810[[:space:]]

вы можете использовать:

^\(266\|801\|810\)[[:space:]]

так что привязка не должна быть отдельно согласована для каждой альтернативы. Я вижу примерно 15% улучшение тестовых данных (10 миллионов строк, 25 ключей) с этим изменением.

Дальнейшая оптимизация заключается в объединении общих префиксов в чередовании: 266\|801\|810 можно заменить эквивалентом 266\|8\(01\|10\). Переписывание 25-клавишного регулярного выражения таким образом дает почти 50% ускорение тестовых данных.

В этот момент grep начинает показывать свои пределы. Кажется, что он связан с CPU: iostat показывает, что каждое последующее улучшение в регулярном выражении увеличивает количество запросов IO в секунду, пока выполняется grep. И повторное использование grep с утепленным кешем страницы, а параметр --mmap не ускоряет работу (как это было бы, если бы файл IO был узким местом). Поэтому для большей скорости требуется утилита с более быстрым движком регулярных выражений.

Одним из таких является ag (источник здесь), чья реализация в регулярном выражении также выполняет автоматическую оптимизацию, поэтому вам не нужно много делать, настройка. Хотя мне не удалось получить grep для обработки тестовых данных менее чем за 12 секунд на моей машине, ag заканчивается на ~ 0,5 с для всех вариантов повторного выражения, описанных выше.

Ответ 2

Это не очень эффективно для памяти (для файла с 300 миллионами строк, что может составить проблему). Я не могу придумать способ сохранить не найденные значения в понимании, кроме как сохранить все значения (или дважды прочитать файл). Я не думаю, что потоки будут очень полезны, поскольку файл ввода-вывода, скорее всего, будет узким местом производительности. Я предполагаю, что вкладка является разделительным символом в файле. (Вы не сказали, но в данных примера есть вкладка.)

vals = [1,2,594592888]

with open(self.inputfile,'r') as i_file:
    all_vals = {
        int(t[0]):int(t[1])
        for t in (
                line.strip().split('\t')
                for line in i_file
        )
    }

newDict = {
    t[0]:t[1] for t in filter(lambda t: t[0] in vals, all_vals.items())
}

notFound = list(set(all_vals.keys()).difference(newDict.keys()))

Ответ 3

Если вы правильно поняли, вам не нужна строка файла, которая не соответствует вам vals Поскольку вы говорите о огромных файлах и довольно небольшом количестве желаемых значений, я бы пошел на что-то вроде:

vals_set = set(vals)
found_vals = {}

with open(inputfile,"r") as in_file:
    for line in in_file:
        line = line.split() # Assuming tabs or whitespaces
        if line[0] in vals_set:
            found_vals[line[0]] = line[1]


not_found_vals = vals_set.difference(found_vals)

Это будет память консервативной, и вы будете указывать в found_vals и свой список в not_found_vals. Фактически, использование памяти, AFAIK будет зависеть только от количества vals, которое вы хотите искать, а не от размера файлов.

EDIT:

Я думаю, что самым простым способом распараллеливать эту задачу было бы просто разделение файла и поиск по отдельности в каждой части с помощью другого процесса. Таким образом, вам не нужно иметь дело с общением между потоками (проще и быстрее, я думаю).

Хороший способ сделать это, так как я понимаю, что вы используете BASH (вы использовали grep: P), это то, что упоминается в этом ответе:

split -l 1000000 filename

будет генерировать файлы с 1000000 строк каждый.

Вы можете легко изменить script, чтобы сохранить совпадения в новый файл для каждого процесса, а затем слить другой выходной файл.