Скрипт для анализа миллионов строк

У меня есть файлы с более чем 100 миллионами строк в каждом:

...
01-AUG-2012 02:29:44 important data
01-AUG-2012 02:29:44 important data
01-AUG-2012 02:36:02 important data
some unimportant data
blahblah (also unimportant data)
some unimportant data
01-AUG-2012 02:40:15 important data
some unimportant data
...

Как вы можете видеть, есть важные данные (начиная с даты и времени) и несущественные данные. Также в каждую секунду может быть много строк важных данных.

Моя цель - подсчитать количество «важных данных» за каждую секунду (или минуту или час ...) и форматировать формат даты /времени. Мой скрипт также позволяет мне подсчитывать данные в каждую минуту, час и т. Д., Используя options.dlen:

options.dlen = 10 takes YYYY-MM-DDD
options.dlen = 13 takes YYYY-MM-DDD HH
options.dlen = 16 takes YYYY-MM-DDD HH:MM
options.dlen = 20 takes YYYY-MM-DDD HH:MM:SS

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

DATA = {}

# search for DD-MMM-YYYY HH:MM:SS
# e.g. "01-JUL-2012 02:29:36 important data"
pattern = re.compile('^\d{2}-[A-Z]{3}-\d{4} \d{2}:\d{2}:\d{2} important data')

DATA = defaultdict(int)
i = 0
f = open(options.infilename, 'r')
for line in f:
    if re.match(pattern, line):
        if options.verbose:
            i += 1
            # print out every 1000 iterations
            if i % 1000 == 0:
                print str(i) + '\r',

        # converts data date/time format to YYYY-MM-DD HH:MM:SS format (but still keep it as datetime !)
        d = datetime.strptime( line [0:20], '%d-%b-%Y %H:%M:%S')
        # converts d, which is datetime to string again
        day_string = d.strftime('%Y-%m-%d %H:%M:%S')
        DATA [ str(day_string[0:int(options.dlen)]) ] += 1
f.close()
#L2 = sorted(DATA.iteritems(), key=operator.itemgetter(1), reverse=True)
#L2 = sorted(DATA.iteritems(), key=operator.itemgetter(1))
L2 = sorted(DATA.iteritems(), key=operator.itemgetter(0))

Для обработки более 100 миллионов строк требуется около 3 часов. Можете ли вы предложить улучшения производительности для этого скрипта?

Обновление: я только что использовал PyPy, и одна и та же задача на том же сервере заняла 45 минут. Я попытаюсь добавить статистику профиля.

12 голосов | спросил przemolb 30 PM000000120000005031 2012, 12:38:50

4 ответа


11

1. Сделайте тестовый пример

Первое, что нужно сделать, это установить существование проблемы с производительностью, поэтому давайте сбить некоторые тестовые данные.

def make_test_file(filename, n, t, delta):
    """
    Write `n` lines of test data to `filename`, starting at `t` (a
    datetime object) and stepping by `delta` (a timedelta object) each
    line.
    """
    with open(filename, 'w') as f:
        for _ in xrange(n):
            f.write(t.strftime('%d-%b-%Y %H:%M:%S ').upper())
            f.write('important data\n')
            t += delta

>>> from datetime import datetime, timedelta
>>> make_test_file('data.txt', 10**5, datetime.now(), timedelta(seconds=1))

И затем с кодом OP в функции aggregate1(filename, dlen):

>>> import timeit
>>> timeit.timeit(lambda:aggregate1('data.txt', 16), number = 1)
5.786283016204834

Итак, в реальном файле (в 1000 раз больше), который займет полтора часа на моей машине (или дольше, если временная сложность хуже линейной). Так что да, есть реальная проблема с производительностью.

2. Очистите код

Давайте попробуем кучу очевидных незначительных улучшений и оптимизаций (в основном, как предложено в других ответах):

  1. Преобразуйте dlen в целое число один раз (не каждый для каждой строки).

  2. Напишите day_string[:dlen] вместо str(day_string[0:dlen])

  3. Напишите pattern.match(line) вместо re.match(pattern, line)

  4. Нет необходимости в key = operator.itemgetter(0), потому что сортировка будет продолжаться в первом элементе пары в любом случае.

  5. Переименуйте DATA как count и day_string с помощью s (это действительно строка даты, а не строка дня).

  6. Используйте with, чтобы гарантировать, что файл будет закрыт в случае ошибки.

  7. Импортируйте имя strptime, чтобы его не нужно искать для каждой строки.

Давайте попробуем:

def aggregate2(filename, dlen):
    strptime = datetime.datetime.strptime
    dlen = int(dlen)
    pattern = re.compile(r'^\d{2}-[A-Z]{3}-\d{4} \d{2}:\d{2}:\d{2} important data')
    count = defaultdict(int)
    with open(filename, 'r') as f:
        for line in f:
            if pattern.match(line):
                d = strptime(line[:20], '%d-%b-%Y %H:%M:%S')
                s = d.strftime('%Y-%m-%d %H:%M:%S')
                count[s[:dlen]] += 1
    return sorted(count.iteritems())

>>> timeit.timeit(lambda:aggregate2('data.txt', 10), number = 1)
5.200263977050781

Небольшое улучшение, 10% или около того, но чистый код делает следующий шаг проще.

3. Профиль

>>> import cProfile
>>> cProfile.run("aggregate2('data.txt', 10)")
         2700009 function calls in 6.262 seconds

   Ordered by: standard name
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    6.262    6.262 <string>:1(<module>)
   100000    0.088    0.000    1.020    0.000 _strptime.py:27(_getlang)
   100000    2.098    0.000    4.033    0.000 _strptime.py:295(_strptime)
   100000    0.393    0.000    0.642    0.000 locale.py:339(normalize)
   100000    0.105    0.000    0.747    0.000 locale.py:407(_parse_localename)
   100000    0.119    0.000    0.933    0.000 locale.py:506(getlocale)
   100000    0.067    0.000    0.067    0.000 {_locale.setlocale}
   100000    0.515    0.000    4.548    0.000 {built-in method strptime}
   100000    0.079    0.000    0.079    0.000 {isinstance}
   200000    0.035    0.000    0.035    0.000 {len}
   100000    0.043    0.000    0.043    0.000 {method 'end' of '_sre.SRE_Match' objects}
   300001    0.076    0.000    0.076    0.000 {method 'get' of 'dict' objects}
   100000    0.276    0.000    0.276    0.000 {method 'groupdict' of '_sre.SRE_Match' objects}
   100000    0.090    0.000    0.090    0.000 {method 'index' of 'list' objects}
   100000    0.025    0.000    0.025    0.000 {method 'iterkeys' of 'dict' objects}
   100000    0.046    0.000    0.046    0.000 {method 'lower' of 'str' objects}
   200000    0.553    0.000    0.553    0.000 {method 'match' of '_sre.SRE_Pattern' objects}
   100000    1.144    0.000    1.144    0.000 {method 'strftime' of 'datetime.date' objects}

Я сделал некоторые выводы для ясности. Должно быть ясно, что виновниками являются strptime (73% времени выполнения), strftime (18%) и match (9%). Все остальное либо вызывается одним из них, либо незначительным.

4. Вырвите низко висящие фрукты

Мы можем избежать вызова обоих strptime и strftime, если мы признаем, что единственные вещи, которые мы достигаем, вызывая эти две функции: (a) переводить месяцы от имен (AUG) на номера (08) и (b) изменить порядок компонентов в стандартном стандарте ISO. Итак, давайте сделаем это сами:

def aggregate3(filename, dlen):
    dlen = int(dlen)
    months = dict(JAN = '01', FEB = '02', MAR = '03', APR = '04',
                  MAY = '05', JUN = '06', JUL = '07', AUG = '08',
                  SEP = '09', OCT = '10', NOV = '11', DEC = '12')
    pattern = re.compile(r'^(\d{2})-([A-Z]{3})-(\d{4}) (\d{2}:\d{2}:\d{2}) '
                         'important data')
    count = defaultdict(int)
    with open(filename, 'r') as f:
        for line in f:
            m = pattern.match(line)
            if m:
                s = '{3}-{0}-{1} {4}'.format(months[m.group(2)], *m.groups())
                count[s[:dlen]] += 1
    return sorted(count.iteritems())

>>> timeit.timeit(lambda:aggregate3('data.txt', 10), number = 1)
0.5073871612548828

Там вы идете: ускорение на 90%! Это должно заставить вас отказаться от трех часов до 20 минут или около того. Есть еще несколько вещей, которые можно попробовать (например, выполнить агрегацию для всех разных значений dlen за один проход). Но я думаю, этого достаточно, чтобы продолжать.

ответил Gareth Rees 1 stEurope/Moscowp30Europe/Moscow09bEurope/MoscowSat, 01 Sep 2012 03:09:13 +0400 2012, 03:09:13
4
DATA = {}

Соглашение Python для ALL_CAPS зарезервировано для констант

# search for DD-MMM-YYYY HH:MM:SS
# e.g. "01-JUL-2012 02:29:36 important data"
pattern = re.compile('^\d{2}-[A-Z]{3}-\d{4} \d{2}:\d{2}:\d{2} important data')

DATA = defaultdict(int)
i = 0
f = open(options.infilename, 'r')

Я рекомендую использовать with, чтобы убедиться, что файл закрыт

for line in f:

Вы должны поместить этот цикл в функцию. Код внутри функции работает быстрее, чем код на верхнем уровне

    if re.match(pattern, line):

Вам действительно нужно регулярное выражение? Из списка файлов, которое вы дали, возможно, вы должны проверить line[20:] == 'important data'

Кроме того, используйте pattern.match(line), re.match работает передача прекомпилированного шаблона, но я обнаружил, что он имеет гораздо худшую производительность.

        if options.verbose:
            i += 1
            # print out every 1000 iterations
            if i % 1000 == 0:
                print str(i) + '\r',




        # converts data date/time format to YYYY-MM-DD HH:MM:SS format (but still keep it as datetime !)
        d = datetime.strptime( line [0:20], '%d-%b-%Y %H:%M:%S')
        # converts d, which is datetime to string again
        day_string = d.strftime('%Y-%m-%d %H:%M:%S')
        DATA [ str(day_string[0:int(options.dlen)]) ] += 1

У вас есть хороший шанс, что вам лучше хранить объект datetime, а не строку. С другой стороны, это файл уже в отсортированном порядке? В этом случае все, что вам нужно сделать, это проверить, изменилась ли строка времени, и вы можете не хранить вещи в словаре

f.close()
#L2 = sorted(DATA.iteritems(), key=operator.itemgetter(1), reverse=True)
#L2 = sorted(DATA.iteritems(), key=operator.itemgetter(1))
L2 = sorted(DATA.iteritems(), key=operator.itemgetter(0))

Если входящий файл уже отсортирован, вы сохраните много времени, сохранив этот вид.

ответил Winston Ewert 30 PM000000110000003131 2012, 23:40:31
3

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

ответил 30 PM000000120000001931 2012, 12:51:19
2

Вот несколько идей, ни один из них не тестировался:

  1. Используйте быстрый тест, чтобы пропустить строки, которые не могут соответствовать формату.

    if line[:2].isdigit():
    
  2. Пропустить регулярное выражение целиком и strptime вызвать исключение, если формат неверен.

  3. Пропустить strptime и strftime и используйте исходную строку даты непосредственно в словаре. Используйте второй шаг для преобразования строк перед сортировкой или использования специального ключа сортировки и сохранения исходного формата.
ответил Mark Ransom 30 PM000000110000002831 2012, 23:43:28

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132