Оптимизация поиска и замены больших файлов в Python

Яполный новичок на Python или любой серьезный язык программирования в этом отношении. Я наконец получил прототип кода для работы, но я думаю, что он будет слишком медленным.

Моя цель - найти и заменить некоторые китайские иероглифы во всех файлах (они являются csv) в каталоге с целыми числами согласно имеющемуся у меня csv-файлу. Файлы хорошо нумеруются по году-месяцу, например 2000-01.csv, и будут единственными файлами в этом каталоге.

Я буду перебирать около 25 файлов по 500 МБ каждый (и около миллиона строк). Словарь, который я буду использовать, будет иметь около 300 элементов, и я буду менять юникод (китайский символ) на целые числа. Я попытался выполнить тестовый прогон, и, предполагая, что все масштабируется линейно (?), Похоже, что для его запуска потребуется около недели.

Заранее спасибо. Вот мой код (не смейтесь!):

# -*- coding: utf-8 -*-

import os, codecs

dir = "C:/Users/Roy/Desktop/test/"

Dict = {'hello' : 'good', 'world' : 'bad'}

for dirs, subdirs, files in os.walk(dir):
    for file in files:
        inFile = codecs.open(dir + file, "r", "utf-8")
        inFileStr = inFile.read()
        inFile.close()
        inFile = codecs.open(dir + file, "w", "utf-8")
        for key in Dict:
            inFileStr = inFileStr.replace(key, Dict[key])
        inFile.write(inFileStr)
        inFile.close()
 Tim McNamara27 сент. 2010 г., 01:01
В Python принято называть переменные экземпляра строчными буквами. Я бы тоже заменил словоDict с чем-то отличным от типа, чтобы избежать путаницы в будущем.
 rallen27 сент. 2010 г., 02:17
@John: у меня есть еще 35 файлов, в которых эта информация уже закодирована целыми числами, и я буду проводить анализ в Stata, которая не читает юникод. Мне нужно читать несколько символов одновременно, а не только 1.
 John Machin27 сент. 2010 г., 01:46
Ваши словарные ключи состоят ровно из 1 китайского символа каждый, или возможно несколько символов на клавишу? Почему вы хотите заменить китайские символы на целые числа?

Ответы на вопрос(4)

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

r +') и избегайте двойного открытия / закрытия (и, вероятно, связанной с этим очистки буфера). Также, если это возможно, не записывайте обратно весь файл, ищите и записывайте только измененные области после замены содержимого файла. Читайте, заменяйте, пишите измененные области (если есть).

Это все еще не поможет производительноститоже хотя многое: я бы профилировал и определял, где на самом деле достигнут результат, а затем переходил к его оптимизации. Это может быть просто медленное чтение данных с диска, и вы мало что можете сделать с этим в Python.

 Matthew Iselin27 сент. 2010 г., 00:54
@ Томас: Ах да. Всегда попадитесь на флаги open (). Слишком много C :). В любом случае, я предложил сначала полностью прочитать файл, а затем только записывать изменения, а не записывать изменения во время чтения.
 Thomas Wouters27 сент. 2010 г., 00:51
«rw» - это не «чтение / запись». Это просто «прочитано», так как «w» полностью игнорируется. Режимы «чтения / записи» - это «r +», «w +» и «a +», где каждый делает что-то свое. Переписать файл во время чтения сложно, так как вам нужно искать между чтениями и записью, и вы должны быть осторожны, чтобы не перезаписать то, что вы еще не прочитали.
 Thomas Wouters27 сент. 2010 г., 01:17
Строка, которую вы передаете open () - это то, что вы передаетеfopen() в C (и почему у него такая отстойная семантика), так что «слишком много C» вряд ли оправдание :-)
 Matthew Iselin27 сент. 2010 г., 03:06
@Thomas: я больше думаю о прямом переводе с O_RDWR сopen() (который я использую гораздо больше, чемfopen() в среде я пишу код в). Кроме того, некоторые платформы принимают «rw» дляfopen() тоже - это просто не стандартизировано.

dir + file должно бытьos.path.join(dir, file)

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

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

 aaronasterling27 сент. 2010 г., 01:02
У вас есть совет по многопоточности. В Python поток, чтобы обойти границы ввода-вывода. Это связано с глобальной блокировкой интерпретатора. Вы используете подпроцессы для приложений, связанных с процессором / памятью, что и есть. (только 50 операций ввода-вывода в неделю;)
 babbitt27 сент. 2010 г., 02:06
Хорошая точка зрения. Я знал о глобальной блокировке, но на самом деле не думал о подпроцессах и потоках. Узнавать что-то новое каждый день.
 intuited27 сент. 2010 г., 02:22
@AaronMcSmooth: Я ожидал бы, что это будет связано с вводом / выводом, поскольку поиск строки и ее замена из словаря довольно просты для современного процессора. Но в этом случае многопоточность не поможет, если некоторые файлы не находятся на отдельных физических дисках или нет возможности найти переведенные файлы на другом физическом диске.
Решение Вопроса

ак это файлы размером 500 МБ, это означает, что строки размером 500 МБ. И затем вы выполняете их повторную замену, что означает, что Python должен создать новую строку размером 500 МБ с первой заменой, затем уничтожить первую строку, затем создать вторую строку размером 500 МБ для второй замены, а затем уничтожить вторую строку и так далее, за каждую замену. Это оказывается довольно много копирования данных назад и вперед, не говоря уже об использовании большого количества памяти.

Если вы знаете, что замены всегда будут содержаться в строке, вы можете читать файл построчно, перебирая его. Python буферизует чтение, что означает, что оно будет довольно оптимизировано. Вам следует открыть новый файл под новым именем для одновременной записи нового файла. Выполняйте замену по очереди в каждой строке и сразу же записывайте ее. Это значительно уменьшит объем используемой памяти.а также объем памяти копируется туда и обратно при замене:

for file in files:
    fname = os.path.join(dir, file)
    inFile = codecs.open(fname, "r", "utf-8")
    outFile = codecs.open(fname + ".new", "w", "utf-8")
    for line in inFile:
        newline = do_replacements_on(line)
        outFile.write(newline)
    inFile.close()
    outFile.close()
    os.rename(fname + ".new", fname)

Если вы не можете быть уверены, что они всегда будут на одной линии, все становится немного сложнее; вам придется читать в блоках вручную, используяinFile.read(blocksize)и внимательно следите за тем, возможно ли частичное совпадение в конце блока. Это не так просто сделать, но обычно все же стоит избегать строк 500 Мб.

Другим большим улучшением было бы, если бы вы могли сделать замены за один раз, вместо того, чтобы пробовать целую кучу замен по порядку. Есть несколько способов сделать это, но то, что подходит лучше всего, полностью зависит от того, что вы заменяете и на что. Для перевода отдельных символов во что-то еще,translate Метод объектов Юникод может быть удобным. Вы передаете его в соответствии с кодировкой Unicode (в виде целых чисел) в строки Unicode:

>>> u"\xff and \ubd23".translate({0xff: u"255", 0xbd23: u"something else"})
u'255 and something else'

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

>>> import re
>>> d = {u'spam': u'spam, ham, spam and eggs', u'eggs': u'saussages'}
>>> p = re.compile("|".join(re.escape(k) for k in d))
>>> def repl(m):
...     return d[m.group(0)]
...
>>> p.sub(repl, u"spam, vikings, eggs and vikings")
u'spam, ham, spam and eggs, vikings, saussages and vikings'
 Jochen Ritzel27 сент. 2010 г., 02:08
+1 для перевода, +1 для регулярных выражений и +1 для чтения кусков .. если бы я мог.
 Thomas Wouters27 сент. 2010 г., 01:29
Я собираюсь добавить к вашему ответу, что строка размером 500 Мб не только встраивается в оперативную память или в swap, но и в то, как большинство архитектур лучше справляются с повторяющимися операциями над меньшим набором данных (то, что вписывается в Процессор хорошо кэшируется, хотя Python быстро заполняет кеш своими собственными компонентами. Кроме того, Python также оптимизирует распределение небольших объектов больше, чем крупных, что особенно важно в Windows (но все платформы в некоторой степени выигрывают от этого .)
 aaronasterling27 сент. 2010 г., 01:25
Я забыл о неизменяемой строке. Гораздо приятнее, чем мой ответ.
 intuited27 сент. 2010 г., 01:53
Размещение выходных файлов на другом физическом диске, вероятно, ускорит выполнение всей процедуры, поскольку узким местом будет чтение и запись на диск. Вероятно, вы могли бы еще больше повысить производительность, выполняя запись в отдельном потоке и передавая ему каждую строку черезQueue.Queue, Я думаю, что полезность этой последней меры будет зависеть от эффективности кэша чтения дисков чтения в сочетании с любым кэшированием записи на диске записи. Но это также может быть слишком тяжело для начинающего Python.
 Thomas Wouters27 сент. 2010 г., 02:04
Потоки не будут делать ничего существенного; Любая выгода от параллельного чтения или записи в значительной степени сводится на нет всеми издержками, которые являются сегментами. Запись на другой шпиндель, вероятно, будет иметь значение, но это будет означать, что вы не можете сделатьos.rename() в конце.
 Thomas Wouters27 сент. 2010 г., 02:26
@intuited: Python не делает много с точки зрения отложенных записей, но не будет потоков. Как я уже сказал, накладные расходы на потоки будут затмевать любые преимущества, которые вы можете извлечь из ОС. Интеллектуальное использование дисков - вплоть до буферизации и кеширования ОС, которые в большинстве своем весьма агрессивны.
 intuited27 сент. 2010 г., 02:18
@Tommas Wouters: Извините, я не уверен, что вы подразумеваете под "шпиндель". Если под «другим шпинделем» вы подразумеваете «другой жесткий диск», то да, именно это я и предлагал. Насколько я понимаю, как работает Python, так это то, что если чтение и запись не выполняются в отдельных потоках, он не сможет одновременно читать и писать. В основном это (IIUC) сводит на нет преимущества записи на отдельный диск, если только диски не могут одновременно читать и записывать данные в / из своих кэшей.

Ваш ответ на вопрос