2017-02-17 7 views
11

В Python я хотел бы быстро вычислить хеш-инвариант порядка для строк файла как способ идентифицировать «уникально» его содержимое. Этими файлами являются, например, вывод select ... from table, и, таким образом, порядок строк является случайным.Order-инвариантный хеш в Python

Вот пример, который достигает того, что я хочу (используя один из хэширов в хэшлибе), но за счет необходимости сортировки строк. Обратите внимание, что сортировка строк - это просто способ достижения цели, то есть получить хэш, который не зависит от упорядочения строк в файле. Но ясно, что я бы хотел избежать стоимости O (n * log (n)), особенно. когда файлы намного дольше.

def get_hexdigest(filename, hasher, blocksize=65536, order_invariant=False): 
    if not os.path.isfile(filename): 
     return None 
    if order_invariant: 
     with open(filename, 'r') as f: 
      for line in sorted(f): 
       hasher.update(line.encode()) 
    else: 
     with open(filename, 'rb') as f: 
      while True: 
       buf = f.read(blocksize) 
       hasher.update(buf) 
       if len(buf) < blocksize: 
        break 
    return hasher.hexdigest() 

Так, например, 1MB, 50K строк файла:

%%time 
get_hexdigest('some_file', hashlib.sha1()) 
# Wall time: 1.71 ms 

Но:

%%time 
get_hexdigest('some_file', hashlib.sha1(), order_invariant=True) 
# Wall time: 77.4 ms 

Что лучше/быстрее способ сделать это?

Как отмечено in this answer, Scala имеет хеш-инвариант порядка на основе Murmurhash, но я предполагаю, что это 32-разрядная версия mmh3 (слишком подверженная конфликтам для моего использования), а также я бы предпочел использовать некоторую стандартную библиотеку доступный в Python, вместо того, чтобы внедрять что-то в C или в Cython. Murmurhash3 имеет 128-битную версию, но ее выход отличается от x64 vs x86. Я хотел бы получить независимые от машины результаты.

Итак, в заключении, я хотел бы:

  • результатов различных машинных архитектур
  • низкой скорости столкновения, то есть по крайней мере, 128 бит с хорошей дисперсией (но мне не нужен хэш быть криптографический)
  • достаточно быстрая, т.е. не менее 5 мс для 1 МБ, 50 К строк файла.
  • легкодоступный, если возможно, как библиотека на PyPi или Conda.
  • подходит для файлов с повторяющимися строками (так что хеширование XORing для строк не является стартером, так как любая пара идентичных строк будет отменять друг друга).

редактирует и примечание: Благодаря несколько комментариев, приведенный выше код обновляется для сортировки строк в памяти. Оригинальная версия для order_invariant is True был:

with os.popen('sort {}'.format(filename)) as f: 
     for line in f: 
      hasher.update(line.encode(encoding='utf-8')) 
    return hasher.hexdigest() 

ассоциируемое время стены (для файла, используемого выше) затем 238 мс. Теперь это уменьшено до 77 мс, но все еще медленнее, чем не сортировка строк. Сортировка добавит стоимость n * log (n) для n строк.

Кодирование (до UTF-8) и чтение в режиме 'r' или 'rb' необходимо при чтении строк, так как тогда мы получаем строки не байтов. Я не хочу полагаться на предположение, что файлы содержат только данные ASCII; чтение в 'rb' может привести к тому, что линии не будут правильно разделены. У меня нет такой же озабоченности, когда order_invariant False, потому что тогда мне не нужно разделить файл, и, таким образом, самым быстрым способом является сокращение фрагментов двоичных данных для обновления хэширования.

+2

первого улучшение: вы, вероятно, 'sort' строки питона с помощью' для строк в отсортированном (е) 'вместо вызова внешнего процесса ... –

+0

Как примечание стороны, если вы работаете с линиями (основанный на _'order-инвариантном хеше для строк файла__), почему вы используете буфер в «упорядочивающем» случае? Просто прокатите 'hasher' с' .readline() 'output. – zwer

+0

Кроме того: возможно, намного быстрее закодировать весь файл и * затем разбить строки, а затем вызвать '.encode' в каждой строке:' open (...) как f: для строки в сортировке (f.read () .encode ('utf-8'). split ('\ n')): hasher.update (строка) '(да, он загружает весь файл в память, но сортировка * требует * этого). – Bakuriu

ответ

2

Я думаю, вам следует отсортировать файл перед (select ... from table order by ...) или придумать другое решение для вашей реальной проблемы.

В любом случае, возможный подход в Python с использованием frozenset:

#!/usr/bin/python 

lines1 = ['line1', 'line2', 'line3', 'line4'] 
lines2 = ['line2', 'line1', 'line3', 'line4'] # same as lines1 but different order 
lines3 = ['line1', 'line1', 'line3', 'line4', 'line5'] 


for lines in [lines1, lines2, lines3]: 
    print(lines) 
    print(hash(frozenset(lines))) 
    print('') 

Выход

['line1', 'line2', 'line3', 'line4'] 
8013284786872469720 

['line2', 'line1', 'line3', 'line4'] 
8013284786872469720 

['line1', 'line1', 'line3', 'line4', 'line5'] 
7430298023231386903 

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

+0

Frozenset подходит для этой задачи. И поскольку дублирующие линии устраняются при построении фенизона, нет риска дублирования двух повторяющихся строк. –

+0

Недостатком является то, что хеш-значение составляет всего 32 или 64 бит. –

+0

Прохладный, +1 за то, что думает о морозе. Тем не менее, я сделал некоторые измерения для увеличения размеров и обнаружил, что время, затраченное на этот подход, более чем линейно с количеством строк. Тем не менее, общие константы низкие. Например, он занимает 15 мс для 65536 строк, лучше всего я видел до сих пор. Но тогда он занимает 7,6 с для линий 11,8 М, а не самый лучший. –

-1

Спасибо всем за интересные комментарии и ответы.

В настоящее время лучшим ответом для больших файлов (> 350K строк) является (a) ниже. Он основан на Murmurhash3, добавляя mmh3.hash128() каждой строки. Для небольших файлов это (b) ниже: вариант the frozenset approach proposed by Rolf, который я адаптировал для создания 128-битного хэша (хотя я не стал бы ручаться за качество этих 128 бит).

а) mmh3.hash128() для каждой строки и добавить

import mmh3 
def get_digest_mmh3_128_add(filename): 
    a = 0 
    with open(filename, 'rb') as f: 
     for line in f: 
      a += mmh3.hash128(line) 
    return '{:032x}'.format(a & 0xffffffffffffffffffffffffffffffff) 

В моей установке: постоянная 0,4 секунды на миллион строк.

б) два frozenset хэша

def get_digest_hash_frozenset128(filename): 
    with open(filename, 'rb') as f: 
     frz = frozenset(f.readlines()) 
    return '{:032x}'.format(((hash(frz) << 64) + hash(frz.union('not a line'))) & 0xffffffffffffffffffffffffffffffff) 

В моих настройках: от 0,2 до 0,6 секунд на миллион строк.

Примечание

  1. После рассмотрения, я решил, что это было нормально читать строки файла в двоичном режиме, даже если они потенциально содержат UTF-8 текст. Причина в том, что если какой-то символ Юникода содержит '\n', линия будет случайно разбита на эту точку. Затем файл будет иметь тот же дайджест, что и другой, где две части этой строки были расположены по-разному (или даже раздроблены и помещены в другое место через файл), но вероятность этого чрезвычайно медленная, и я могу жить с Это.

  2. Добавление всех 128-битовых хэшей в (a) выполняется с использованием произвольных прецизионных цепей Python. Во-первых, я попытался сохранить сумму в 128 бит (путем повторного использования с константой 0xfff...fff). Но это оказывается медленнее, чем позволить Python использовать произвольную точность и делать маскировку один раз в конце.

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

Полные результаты

Полный ноутбук доступен here. Он создает псевдослучайные файлы произвольных размеров и пробует несколько подходов к дайджесту, измеряя время, затрачиваемое каждым из них. Это выполняется на экземпляре EC2 (r3.4xlarge, с использованием тома EBS для хранения псевдослучайного файла) и ноутбука Jupyter iPython и Python 3.6.

Для 46341 линий, мы получаем

fun        lines millis 
get_digest_xxh64_order_sensitive 46341 0.4 * 
get_digest_sha1     46341 1.7 * 
get_digest_hash_frozenset64  46341 8.7 
get_digest_hash_frozenset128  46341 10.8 
get_digest_sha1_by_lines   46341 14.1 * 
get_digest_mmh3_128_add_cy  46341 18.6 
get_digest_mmh3_128_add   46341 19.7 
get_digest_sha1_sort_binary  46341 44.3 
get_digest_sha1_sort    46341 65.9 

*: Это порядка зависит, только здесь для сравнения.

get_digest_hash_frozenset64 не подходит, поскольку он дает только 64 бит.

get_digest_mmh3_128_add_cy - это cythonized версия функции, приведенной выше в (a), но есть небольшая разница.

get_digest_xxh64_order_sensitive очень быстро, но зависит от порядка. Мои попытки (не перечисленные здесь) для получения версии, не зависящей от порядка, дали довольно медленные результаты. Причина, по-моему, - это, по-видимому, высокая стоимость инициализации и завершения хеширования.

Для больших файлов выигрывает get_digest_mmh3_128_add_cy. Вот для линий 11,8:

fun         lines millis 
get_digest_xxh64_order_sensitive 11863283  97.8 * 
get_digest_sha1     11863283  429.3 * 
get_digest_sha1_by_lines   11863283 3453.0 * 
get_digest_mmh3_128_add_cy  11863283 4692.8 
get_digest_mmh3_128_add   11863283 4956.6 
get_digest_hash_frozenset64  11863283 6418.2 
get_digest_hash_frozenset128  11863283 7663.6 
get_digest_sha1_sort_binary  11863283 27851.3 
get_digest_sha1_sort    11863283 34806.4 

Сосредоточение на двух ведущих соперников (порядка инвариантно, а не другие), вот сколько времени они принимают в зависимости от размера (количество строк). У-ось - микросекунды/линия, а ось x - это количество строк файла. Обратите внимание, как get_digest_mmh3_128_add_cy проводит постоянное время (0,4 us) в строке.

time of two order-invariant digests in function of size

Следующие шаги

Извините за многословно ответ. Это лишь промежуточный ответ, как я мог (если позволять время) позже попытаться продолжить эксперименты с numba или Cython (или C++) прямой реализации Murmurhash3.

0

Есть ли причина, по которой это решение в стиле мерклеров не будет работать?

import hashlib 

def hasher(data): 
    hasher = hashlib.sha1() 
    hasher.update(data.encode('utf-8')) 
    return hasher.hexdigest() 


def get_digest_by_line(filename, line_invariant=False, hasher=hasher): 
    with open(filename, 'r') as f: 
     hashes = (hasher(line) for line in f) 
     if line_invariant: 
      hashes = sorted(hashes) 
     return hasher(''.join(hashes))