Как улучшить скорость этого цикла readline в python? - PullRequest
2 голосов
/ 12 сентября 2009

Я импортирую несколько частей Databasedump в текстовом формате в MySQL, проблема в том, что перед интересными данными есть очень много неинтересного материала. Я написал этот цикл, чтобы получить необходимые данные:

def readloop(DBFILE):
    txtdb=open(DBFILE, 'r')

sline = ""

# loop till 1st "customernum:" is found
while sline.startswith("customernum:  ") is False: 
    sline = txtdb.readline()

while sline.startswith("customernum:  "):
    data = []
    data.append(sline)
    sline = txtdb.readline()
    while sline.startswith("customernum:  ") is False:
        data.append(sline)
        sline = txtdb.readline()
        if len(sline) == 0:
            break
    customernum = getitem(data, "customernum:  ")
    street = getitem(data, "street:  ")
    country = getitem(data, "country:  ")
    zip = getitem(data, "zip:  ")

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

Большое спасибо заранее!

Ответы [ 5 ]

5 голосов
/ 13 сентября 2009

Пожалуйста, не пишите этот код:

while condition is False:

Булевы условия логические для вслух крика, так что они могут быть проверены (или отрицаны и проверены) напрямую:

while not condition:

Ваш второй цикл while не записан как «тогда как условие истинно:», мне любопытно, почему вы почувствовали необходимость проверить «ложно» в первом.

Вытащив модуль dis, я подумал, что расскажу об этом немного дальше. По моему опыту pyparsing, вызовы функций снижают общую производительность, поэтому было бы неплохо избегать вызовов функций, если это возможно. Вот ваш оригинальный тест:

>>> test = lambda t : t.startswith('customernum') is False
>>> dis.dis(test)
  1           0 LOAD_FAST                0 (t)
              3 LOAD_ATTR                0 (startswith)
              6 LOAD_CONST               0 ('customernum')
              9 CALL_FUNCTION            1
             12 LOAD_GLOBAL              1 (False)
             15 COMPARE_OP               8 (is)
             18 RETURN_VALUE

Здесь происходят две дорогие вещи, CALL_FUNCTION и LOAD_GLOBAL. Вы можете сократить LOAD_GLOBAL, определив локальное имя для False:

>>> test = lambda t,False=False : t.startswith('customernum') is False
>>> dis.dis(test)
  1           0 LOAD_FAST                0 (t)
              3 LOAD_ATTR                0 (startswith)
              6 LOAD_CONST               0 ('customernum')
              9 CALL_FUNCTION            1
             12 LOAD_FAST                1 (False)
             15 COMPARE_OP               8 (is)
             18 RETURN_VALUE

Но что, если мы просто отбросим тест 'is' полностью?:

>>> test = lambda t : not t.startswith('customernum')
>>> dis.dis(test)
  1           0 LOAD_FAST                0 (t)
              3 LOAD_ATTR                0 (startswith)
              6 LOAD_CONST               0 ('customernum')
              9 CALL_FUNCTION            1
             12 UNARY_NOT
             13 RETURN_VALUE

Мы свернули LOAD_xxx и COMPARE_OP с простым UNARY_NOT. «Неверно», безусловно, не способствует производительности.

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

>>> test = lambda t : t[0] != 'c' and not t.startswith('customernum')
>>> dis.dis(test)
  1           0 LOAD_FAST                0 (t)
              3 LOAD_CONST               0 (0)
              6 BINARY_SUBSCR
              7 LOAD_CONST               1 ('c')
             10 COMPARE_OP               3 (!=)
             13 JUMP_IF_FALSE           14 (to 30)
             16 POP_TOP
             17 LOAD_FAST                0 (t)
             20 LOAD_ATTR                0 (startswith)
             23 LOAD_CONST               2 ('customernum')
             26 CALL_FUNCTION            1
             29 UNARY_NOT
        >>   30 RETURN_VALUE

(Обратите внимание, что использование [0] для получения первого символа строки не создает срез - это на самом деле очень быстро.)

Теперь, если предположить, что строк, начинающихся с 'c', немного, фильтр черновой обработки может удалить строку, используя все довольно быстрые инструкции. Фактически, проверяя «t [0]! = 'C'» вместо «not t [0] == 'c'», мы спасаем себя от посторонних UNARY_NOT инструкций.

Итак, используя это обучение по оптимизации ярлыков, я предлагаю изменить этот код:

while sline.startswith("customernum:  ") is False:
    sline = txtdb.readline()

while sline.startswith("customernum:  "):
    ... do the rest of the customer data stuff...

К этому:

for sline in txtdb:
    if sline[0] == 'c' and \ 
       sline.startswith("customernum:  "):
        ... do the rest of the customer data stuff...

Обратите внимание, что я также удалил вызов функции .readline () и просто перебрал файл, используя "for sline in txtdb".

Я понимаю, что Алекс полностью предоставил другой код для поиска этой первой строки 'customernum', но я постараюсь оптимизировать в общих рамках вашего алгоритма, прежде чем вытаскивать большие, но неясные пистолеты для чтения блоков.

1 голос
/ 12 сентября 2009

Общая идея оптимизации состоит в том, чтобы «большими блоками» (в основном игнорируя структуру линий) найти первую интересующую строку, а затем перейти к обработке строк для остальных). Это несколько придирчиво и подвержено ошибкам (по одному и т. Д.), Поэтому оно действительно нуждается в тестировании, но общая идея заключается в следующем ...:

import itertools

def readloop(DBFILE):
  txtdb=open(DBFILE, 'r')
  tag = "customernum:  "
  BIGBLOCK = 1024 * 1024
  # locate first occurrence of tag at line-start
  # (assumes the VERY FIRST line doesn't start that way,
  # else you need a special-case and slight refactoring)
  blob = ''
  while True:
    blob = blob + txtdb.read(BIGBLOCK)
    if not blob:
      # tag not present at all -- warn about that, then
      return
    where = blob.find('\n' + tag)
    if where != -1:  # found it!
      blob = blob[where+1:] + txtdb.readline()
      break
    blob = blob[-len(tag):]
  # now make a by-line iterator over the part of interest
  thelines = itertools.chain(blob.splitlines(1), txtdb)
  sline = next(thelines, '')
  while sline.startswith(tag):
    data = []
    data.append(sline)
    sline = next(thelines, '')
    while not sline.startswith(tag):
      data.append(sline)
      sline = next(thelines, '')
      if not sline:
        break
    customernum = getitem(data, "customernum:  ")
    street = getitem(data, "street:  ")
    country = getitem(data, "country:  ")
    zip = getitem(data, "zip:  ")

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

1 голос
/ 12 сентября 2009

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

Вы можете запустить скрипт один раз, чтобы определить фактические позиции в файле, к которому вы хотите перейти, с помощью print txtdb.tell(). Запишите их и замените код поиска на txtdb.seek( pos ). По сути, это создание индекса для файла; -)

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

0 голосов
/ 12 сентября 2009

Расскажите подробнее о файле.

Можете ли вы использовать file.seek для бинарного поиска? Перейдите к отметке на полпути, прочитайте несколько строк, определите, находитесь ли вы до или после нужной вам части, выполните рекурсию. Это превратит ваш поиск O (n) в O (logn).

0 голосов
/ 12 сентября 2009
...