Удаление повторяющихся строк из большого файла в Python

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

каждая строка содержит 15 полей и несколько сотен символов, и все поля, необходимые для определения уникальности. Вместо того, чтобы сравнивать всю строку, чтобы найти дубликат, я сравниваю hash(row-as-a-string) в попытке сохранить память. Я устанавливаю фильтр, который разбивает данные на примерно равное количество строк (например, дни недели), и каждый раздел достаточно мал, чтобы таблица поиска хэш-значений для этого раздела поместилась в память. Я прохожу через файл один раз для каждого раздела, проверяя уникальные строки и записывая их во второй файл (псевдокод):

import csv

headers={'DayOfWeek':None, 'a':None, 'b':None}
outs=csv.DictWriter(open('c:dedupedFile.csv','wb')
days=['Mon','Tue','Wed','Thu','Fri','Sat','Sun']

outs.writerows(headers)

for day in days:
    htable={}
    ins=csv.DictReader(open('c:bigfile.csv','rb'),headers)
    for line in ins:
        hvalue=hash(reduce(lambda x,y:x+y,line.itervalues()))
        if line['DayOfWeek']==day:
            if hvalue in htable:
                pass
            else:
                htable[hvalue]=None
                outs.writerow(line)

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

for day in days: 

и

if line['DayOfWeek']==day:

у нас есть

for i in range(n):

и

if len(reduce(lambda x,y:x+y,line.itervalues())%n)==i:

где 'n' настолько мало, насколько позволит память. Но при этом используется тот же метод.

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

P. S. Я ограничен в Python 2.5.

6 ответов


Если вы хотите действительно простой способ сделать это, просто создайте базу данных sqlite:

import sqlite3
conn = sqlite3.connect('single.db')
cur = conn.cursor()
cur.execute("""create table test(
f1 text,
f2 text,
f3 text,
f4 text,
f5 text,
f6 text,
f7 text,
f8 text,
f9 text,
f10 text,
f11 text,
f12 text,
f13 text,
f14 text,
f15 text,
primary key(f1,  f2,  f3,  f4,  f5,  f6,  f7,  
            f8,  f9,  f10,  f11,  f12,  f13,  f14,  f15))
"""
conn.commit()

#simplified/pseudo code
for row in reader:
    #assuming row returns a list-type object
    try:
        cur.execute('''insert into test values(?, ?, ?, ?, ?, ?, ?, 
                       ?, ?, ?, ?, ?, ?, ?, ?)''', row)
        conn.commit()
    except IntegrityError:
        pass

conn.commit()
cur.execute('select * from test')

for row in cur:
    #write row to csv file

тогда вам не придется беспокоиться о какой - либо логике сравнения самостоятельно-просто позвольте sqlite позаботиться об этом за вас. Вероятно, это будет не намного быстрее, чем хеширование строк, но это, вероятно, намного проще. Конечно, вы можете изменить тип, хранящийся в базе данных, если хотите, или нет, в зависимости от случая. Конечно, поскольку вы уже конвертируете данные в строку, вы можете просто вместо этого есть одно поле. Здесь много вариантов.


вы в основном делаете сортировку слиянием и удаляете дублированные записи.

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

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

input = open(infilename, 'rb')
output = open(outfile, 'wb')

for key,  group in itertools.groupby(sorted(input)):
    output.write(key)

ваш текущий метод не гарантированно работает должным образом.

во-первых, существует небольшая вероятность того, что две строки, которые на самом деле разные, могут производить одно и то же значение хэша. hash(a) == hash(b) не всегда означает a == b

во-вторых, вы делаете вероятность выше с вашим каперсом" уменьшить/лямбда":

>>> reduce(lambda x,y: x+y, ['foo', '1', '23'])
'foo123'
>>> reduce(lambda x,y: x+y, ['foo', '12', '3'])
'foo123'
>>>

кстати, не "".присоединиться (['foo', '1', '23']) быть несколько яснее?

BTW2, почему бы не использовать set вместо dict на htable?

вот практическое решение: получите пакет "core utils" из GnuWin32 site и установите его. Затем:

  1. напишите копию своего файла без заголовков (скажем) infile.csv
  2. c:\gnuwin32\bin\sort --unique -ooutfile.csv infile.csv
  3. читать outfile.csv и напишите копию с добавленными заголовками

для каждого из шагов 1 и 3, можно использовать скрипт Python, или некоторые другие Gnuwin32 утилиты (голова, хвост, тройник, кошка,...).


ваше исходное решение немного неверно: у вас могут быть разные строки, хэширующие одно и то же значение (хэш-столкновение), и ваш код оставит один из них.

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

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


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

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

Почему B-Деревья? Они требуют меньше чтения диска, когда вы можете (или хотите) читать их части в память. Степень (число детей) на каждом узле зависит от доступной памяти и количество линий, но вы не хотите иметь слишком много узлов.

Как только у нас есть эти B-деревья на диске, мы сравниваем самый низкий элемент из каждого из них. Мы удаляем самый низкий из всех, из всех B-деревьев, у которых он есть. Мы объединяем их наборы строк, что означает, что у нас нет дубликатов для этих строк (а также что у нас больше нет строк, хэширующих это значение). Затем мы записываем строки из этого слияния в выходную структуру csv.

мы можем разделить половину памяти для чтения B-деревьев и половину, чтобы сохранить выходной csv в памяти в течение некоторого времени. Мы смываем csv на диск, когда его половина заполнена, добавляя к тому, что уже было написано. Сколько из каждого B-дерева, которое мы читаем на каждом шаге, можно грубо вычислить по (available_memory / 2) / number_of_btrees, округленным, чтобы мы читали полные узлы.

в псевдо-Python:

ins = DictReader(...)
i = 0
while ins.still_has_lines_to_be_read():
    tree = BTree(i)
    while fits_into_memory:
        line = ins.readline()
        tree.add(line, key=hash)
    tree.write_to_disc()
    i += 1
n_btrees = i

# At this point, we have several (n_btres) B-Trees on disk
while n_btrees:
    n_bytes = (available_memory / 2) / n_btrees
    btrees = [read_btree_from_disk(i, n_bytes)
              for i in enumerate(range(n_btrees))]
    lowest_candidates = [get_lowest(b) for b in btrees]
    lowest = min(lowest_candidates)
    lines = set()
    for i in range(number_of_btrees):
        tree = btrees[i]
        if lowest == lowest_candidates[i]:
            node = tree.pop_lowest()
            lines.update(node.lines)
        if tree.is_empty():
        n_btrees -= 1

    if output_memory_is_full or n_btrees == 0:
        outs.append_on_disk(lines)

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

или вы можете поймать первое слово в строке и разделить файл на куски. Затем вы можете прочитать линий (возможно сделать.join (line.split ()) для унификации интервалов / вкладок в строке, если можно изменить интервал) в наборе в алфавитном порядке очистка набора между частями (набор удаляет дубликаты), чтобы получить вещи наполовину сортировка (набор не в порядке, если вы хотите, вы можете прочитать в куче и выписать, чтобы получить сортированный порядок, последнее вхождение в набор, заменяющее старые значения, как вы идете.) В качестве альтернативы вы также можете отсортировать кусок и удалить дубликаты строк с помощью решения groupby Джо Коберга. Наконец, вы можете объединить части вместе (вы можете, конечно, сделать запись, как вы идете кусок за куском в окончательный файл во время сортировки частей)