Параллельное сопоставление файлов, Python

Я пытаюсь улучшить скрипт, который сканирует файлы для вредоносного кода. У нас есть список шаблонов регулярных выражений в файле, по одному шаблону в каждой строке. Это регулярное выражение для grep, так как наша текущая реализация в основном представляет собой скрипт bash find \ grep combo. Сценарий bash занимает 358 секунд в моем тестовом каталоге. Я смог написать скрипт python, который сделал это за 72 секунды, но хочу улучшить его. Сначала я отправлю базовый код, а затем настройки, которые я пробовал:

import os, sys, Queue, threading, re fileList = [] rootDir = sys.argv[1] class Recurser(threading.Thread): def __init__(self, queue, dir): self.queue = queue self.dir = dir threading.Thread.__init__(self) def run(self): self.addToQueue(self.dir) ## HELPER FUNCTION FOR INTERNAL USE ONLY def addToQueue(self, rootDir): for root, subFolders, files in os.walk(rootDir): for file in files: self.queue.put(os.path.join(root,file)) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) self.queue.put(-1) class Scanner(threading.Thread): def __init__(self, queue, patterns): self.queue = queue self.patterns = patterns threading.Thread.__init__(self) def run(self): nextFile = self.queue.get() while nextFile is not -1: #print "Trying " + nextFile self.scanFile(nextFile) nextFile = self.queue.get() #HELPER FUNCTION FOR INTERNAL UES ONLY def scanFile(self, file): fp = open(file) contents = fp.read() i=0 #for patt in self.patterns: if self.patterns.search(contents): print "Match " + str(i) + " found in " + file ############MAIN MAIN MAIN MAIN################## ############MAIN MAIN MAIN MAIN################## ############MAIN MAIN MAIN MAIN################## ############MAIN MAIN MAIN MAIN################## ############MAIN MAIN MAIN MAIN################## ############MAIN MAIN MAIN MAIN################## ############MAIN MAIN MAIN MAIN################## ############MAIN MAIN MAIN MAIN################## ############MAIN MAIN MAIN MAIN################## fileQueue = Queue.Queue() #Get the shell scanner patterns patterns = [] fPatt = open('/root/patterns') giantRE = '(' for line in fPatt: #patterns.append(re.compile(line.rstrip(), re.IGNORECASE)) giantRE = giantRE + line.rstrip() + '|' giantRE = giantRE[:-1] + ')' giantRE = re.compile(giantRE, re.IGNORECASE) #start recursing the directories recurser = Recurser(fileQueue,rootDir) recurser.start() print "starting scanner" #start checking the files for scanner in xrange(0,8): scanner = Scanner(fileQueue, giantRE) scanner.start() 

Это, очевидно, отлаживает \ уродливый код, не обращая внимания на миллион queue.put (-1), я очищу его позже. Некоторые отступы не отображаются правильно, paticularly в scanFile.

Во всяком случае, некоторые вещи я заметил. Использование 1, 4 и даже 8 потоков (для сканера в xrange (0, ???) 🙂 не имеет значения. Я все равно получаю ~ 72 секунды. Я предполагаю, что это связано с GIL python.

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

Стремясь избежать gyth python, я попытался использовать каждую ветку fork для grep, как в:

 #HELPER FUNCTION FOR INTERNAL UES ONLY def scanFile(self, file): s = subprocess.Popen(("grep", "-El", "--file=/root/patterns", file), stdout = subprocess.PIPE) output = s.communicate()[0] if output != '': print 'Matchfound in ' + file 

Это привело к увеличению времени выполнения.

Любые предложения по повышению производительности.

:::::::::::::РЕДАКТИРОВАТЬ::::::::

Я не могу публиковать ответы на свои вопросы, но вот ответы на несколько поднятых вопросов:

@David Nehme – просто чтобы люди знали, что я знаю, что у меня есть миллион queue.put (-1).

@Blender – отметить нижнюю часть очереди. Мои потоки сканера сохраняют dequeing, пока они не достигнут -1, который находится внизу (в то время как nextFile не -1 :). Ядро процессора 8, однако из-за GIL, использующего 1 нить, 4 потока или 8 потоков, НЕ имеет значения. Истечение 8 подпроцессов привело к значительно более медленному коду (142 сек против 72)

@ed – Да, и это так же медленно, как и find \ grep combo, на самом деле медленнее, потому что он без разбора greps-файл, который не нужен

@Ron – невозможно обновить, это должно быть универсальным. Как вы думаете, это ускорит> 72 секунды? Баш-греппер делает 358 секунд. Мой гигантский метод RE-Python выполняет 72 секунды w \ 1-8 потоков. Метод popen w \ 8 thrads (8 подпроцессов) пробежал 142 секунды. Пока гигантский метод RE python является явным победителем

@intuted

Вот мясо нашего текущего find \ grep combo (Не мой скрипт). Это довольно просто. Есть некоторые дополнительные вещи, такие как ls, но ничего, что должно привести к 5-кратному замедлению. Даже если grep -r немного более эффективен, 5x – ОГРОМНОЕ замедление.

  find "${TARGET}" -type f -size "${SZLIMIT}" -exec grep -Eaq --file="${HOME}/patterns" "{}" \; -and -ls | tee -a "${HOME}/found.txt" 

Код python более эффективен, я не знаю, почему, но я экспериментально протестировал его. Я предпочитаю делать это на питоне. Я уже добился ускорения 5x с помощью python, я бы хотел, чтобы это ускорилось.

::::::::::::: ПОБЕДИТЕЛЬ ПОБЕДИТЕЛЯ ПОБЕДИТЕЛЯ ::::::::::::::::::

Похоже, у нас есть победитель.

Сценарий оболочки intued выходит на 2-е место с 34 секундами, однако @ steveha пришел первым с 24 секундами. Из-за того, что у многих наших ящиков нет python2.6, мне пришлось cx_freeze его. Я могу написать оболочку оболочки оболочки для wget tar и распаковать ее. Однако я люблю интуицию для простоты.

Спасибо вам за все ваши ребята, у меня теперь есть эффективный инструмент для sysadmining

  • Ошибка импорта matplotlib.pyplot
  • Python Glob без всего пути - только имя файла
  • установить права доступа к файлам в файле setup.py
  • Техника удаления общих слов (и их множественных версий) из строки
  • Алгоритм определения победителя Техасского Холдема
  • TypeError: индексы списка должны быть целыми, а не кортежем, что не так
  • Запустить полностью независимый процесс
  • Пример многопроцессорности Python не работает
  • 3 Solutions collect form web for “Параллельное сопоставление файлов, Python”

    Я думаю, что вместо использования модуля threading передачи вы должны использовать модуль multiprocessing для вашего решения Python. Нити Python могут работать с GIL; GIL не проблема, если у вас просто несколько процессов Python.

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

    http://docs.python.org/library/multiprocessing.html

    Если это не быстрее, чем ваша реализация threading , то я не думаю, что проблема с GIL – ваша проблема.

    EDIT: Хорошо, я добавляю рабочую программу Python. Это использует пул рабочих процессов для открытия каждого файла и поиска шаблона в каждом. Когда работник находит имя файла, которое соответствует ему, оно просто выводит его (на стандартный вывод), чтобы вы могли перенаправить вывод этого скрипта в файл, и у вас есть список файлов.

    EDIT: Я думаю, что это немного легче читать версию, ее легче понять.

    Я приурочил это, просматривая файлы в / usr / include на своем компьютере. Он завершает поиск примерно через полсекунды. Используя find xargs через xargs для запуска как можно большего количества процессов grep , требуется около 0,05 секунды, примерно в 10 раз быстрее. Но я ненавижу барочный странный язык, который вы должны использовать, чтобы find работу для правильной работы, и мне нравится версия Python. И, возможно, на действительно больших каталогах разница будет меньше, поскольку часть половины секунды для Python, должно быть, была временем запуска. И, может быть, полсекунды достаточно быстры для большинства целей!

     import multiprocessing as mp import os import re import sys from stat import S_ISREG # uncomment these if you really want a hard-coded $HOME/patterns file #home = os.environ.get('HOME') #patterns_file = os.path.join(home, 'patterns') target = sys.argv[1] size_limit = int(sys.argv[2]) assert size_limit >= 0 patterns_file = sys.argv[3] # build s_pat as string like: (?:foo|bar|baz) # This will match any of the sub-patterns foo, bar, or baz # but the '?:' means Python won't bother to build a "match group". with open(patterns_file) as f: s_pat = r'(?:{})'.format('|'.join(line.strip() for line in f)) # pre-compile pattern for speed pat = re.compile(s_pat) def walk_files(topdir): """yield up full pathname for each file in tree under topdir""" for dirpath, dirnames, filenames in os.walk(topdir): for fname in filenames: pathname = os.path.join(dirpath, fname) yield pathname def files_to_search(topdir): """yield up full pathname for only files we want to search""" for fname in walk_files(topdir): try: # if it is a regular file and big enough, we want to search it sr = os.stat(fname) if S_ISREG(sr.st_mode) and sr.st_size >= size_limit: yield fname except OSError: pass def worker_search_fn(fname): with open(fname, 'rt') as f: # read one line at a time from file for line in f: if re.search(pat, line): # found a match! print filename to stdout print(fname) # stop reading file; just return return mp.Pool().map(worker_search_fn, files_to_search(target)) 

    Я немного смущен относительно того, как ваш скрипт Python оказался быстрее, чем ваш find / grep combo. Если вы хотите использовать grep таким образом, который несколько похож на то, что предложил Рон Смит в его ответе, вы можете сделать что-то вроде

     find -type f | xargs -d \\n -P 8 -n 100 grep --file=/root/patterns 

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

    note : Параметр -d \\n для xargs является расширением GNU, которое не будет работать на всех системах POSIX-ish. Он указывает, что * d * elimiter между именами файлов является новой строкой. Хотя технически имена файлов могут содержать новые строки, на практике никто не делает этого и не сохраняет свою работу. Для совместимости с не-GNU xargs вам нужно добавить параметр -print0 для find и использования -0 вместо -d \\n с xargs . Это приведет к тому, что нулевой бит \0 (hex 0x00 ) будет использоваться как разделитель как для find и для xargs .

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

     NUMFILES="$(find -type f | wc -l)"; 

    и затем используя это число, чтобы получить четное разделение между 8 процессами (предполагая bash как оболочку)

     find -type f | xargs -d \\n -P 8 -n $(($NUMFILES / 8 + 1)) grep --file=/root/patterns 

    Я думаю, что это может работать лучше, потому что дисковый ввод-вывод find не будет мешать дисковым ввода- grep различных grep s. Полагаю, отчасти это зависит от того, насколько велики файлы, и независимо от того, хранятся они постоянно – с небольшими файлами, диск все равно будет искать много, поэтому это не имеет большого значения. Также обратите внимание, что, особенно если у вас есть достаточное количество ОЗУ, последующие прогоны такой команды будут быстрее, потому что некоторые из файлов будут сохранены в вашем кеше памяти.

    Конечно, вы можете параметризовать 8 чтобы было легче экспериментировать с разными количествами одновременных процессов.

    Как описано выше. упоминается в комментариях, вполне возможно, что производительность этого подхода будет по-прежнему менее впечатляющей, чем производительность однопроцессного grep -r . Я думаю, это зависит от относительной скорости вашего диска [array], количества процессоров в вашей системе и т. Д.

    Если вы хотите перейти на версию 3.2 или выше, вы можете воспользоваться concurrent.futures.ProcessPoolExecutor. Я думаю, что это улучшит производительность по сравнению с методом popen, который вы попытались, потому что он предварительно создаст пул процессов, когда ваш метод popen каждый раз создает новый процесс. Вы можете написать свой собственный код, чтобы сделать то же самое для более ранней версии, если по какой-то причине вы не можете перейти на 3.2.

    Interesting Posts

    Перезапись скрипта pymc для оценки параметров в динамических системах в pymc3

    Как установить PyAudio в virtualenv на Mac OS X 10.7

    Скопировать-вставить в Python интерактивный интерпретатор и отступы

    Почему я не могу использовать доходность с возвратом?

    Функция добавления пользовательских входов в список

    Как иметь отрицательный ноль, всегда отформатированный как положительный ноль в строке python?

    Django: Когда настраивать save vs с помощью сигнала после сохранения

    Кудрявые фигурные скобки в Python?

    Предсказание следующего слова с использованием примера тензорного потока модели ptb LSTM

    Как я могу печатать литеральные фигуры в фигурных скобках в строке python, а также использовать .format на нем?

    Использование запросов Python для выбора форм

    Предсказание того, как долго будет проводиться классификация scikit-learn

    Отключить аббревиатуру в argparse

    Использование памяти DBSCAN scikit-learn

    Значения цвета в imshow для matplotlib?

    Python - лучший язык программирования в мире.