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

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

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

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» настолько мало, насколько позволяет память. Но это все еще использует тот же метод.

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

PS Я ограничен Python 2.5.

9
задан Community 23 May 2017 в 10:28
поделиться

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

12
ответ дан 4 December 2019 в 10:02
поделиться

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

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

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

1
ответ дан 4 December 2019 в 10:02
поделиться

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

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

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

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

for key,  group in itertools.groupby(sorted(input)):
    output.write(key)
6
ответ дан 4 December 2019 в 10:02
поделиться

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

Считывать строки ввода в 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)
0
ответ дан 4 December 2019 в 10:02
поделиться

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

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

0
ответ дан 4 December 2019 в 10:02
поделиться

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

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

Во-вторых, вы увеличиваете вероятность с помощью каперса "reduce / lambda":

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

Кстати, разве "" .join (['foo', '1', '23']) не будет более ясным?

Кстати, почему бы не использовать набор вместо ] dict для htable ?

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

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

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

2
ответ дан 4 December 2019 в 10:02
поделиться
Другие вопросы по тегам:

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