Объедините гигабайты текста в один файл, отсортированный по количеству вхождений

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

Он не просто находит уникальные строки, он определяет, как часто каждая уникальная строка появляется во всех файлах.

С помощью этого скрипта нужно обрабатывать МНОГО текста - по крайней мере, около 2 ГБ, поэтому мне нужно, чтобы это было сделано эффективно. Пока я не достиг этой цели.

import os, sys #needed for looking into a directory
from sys import argv #allows passing of arguments from command line, where I call the script
from collections import Counter #allows the lists to be sorted by number of occurrences

#Pass argument containing Directory of files to be combined
dir_string = str((argv[1]))

filenames=[]  

#Get name of files in directory, add them to a list
for file in os.listdir(dir_string):
    if file.endswith(".txt"):
        filenames.append(os.path.join(dir_string, file)) #add names of files to a list

#Declare name of file to be written
out_file_name = dir_string+".txt"

#Create output file
outfile = open(out_file_name, "w")

#Declare list to be filled with lines seen
lines_seen = []

#Parse All Lines in all files
for fname in filenames: #for all files in list
    with open(fname) as infile: #open a given file
        for line in infile: #for all lines in current file, read one by one
                #Here's the problem.
                lines_seen.append(str(line).strip('\n')) #add line to list of lines seen,
                                                         #removing the endline

    #Organizes the list by number of occurences, but produced a list that contains
    # [(item a, # of a occurrences ), (item b, # of b occurrences)...]
    lines_seen = Counter(lines_seen).most_common()

    #Write file line by line to the output file
    for item in lines_seen: outfile.write(str(item[0])+"\n")

outfile.close()

Когда я получаю сообщение об ошибке, речь идет о строке lines_seen.append(str(line).strip('\n')).

Сначала я попытался добавить строки без преобразования в строку и удаления, но это включало бы видимый «\n» в строку, что было для меня неприемлемо. Для меньших списков преобразование в строку и удаление не требовало больших затрат памяти. Я не мог найти более эффективного способа избавиться от символа конца строки

На моем ПК это вызывает MemoryError, на моем Mac это дает мне Killed: 9 — еще не пробовал в Linux.

Нужно ли мне преобразовать в двоичный файл, собрать мой упорядоченный список, а затем преобразовать обратно? Как еще это можно сделать?

EDIT. Стало ясно, что для меня лучший способ сделать это с помощью команд unix

cd DirectoryWithFiles
cat *.txt | sort | uniq -c | sort -n -r > wordlist_with_count.txt
cut  -c6- wordlist_with_count.txt > wordlist_sorted.txt

person berzerk0    schedule 22.03.2017    source источник
comment
вместо того, чтобы хранить List в памяти, почему бы не записать свои строки во временный файл?   -  person Nishanth Matha    schedule 22.03.2017
comment
на момент написания этого я не знал, как отсортировать этот файл, не помещая его в список или набор, что возвращает меня к той же проблеме.   -  person berzerk0    schedule 22.03.2017
comment
в соответствии с этой веткой: stackoverflow.com/questions/41315394/ file-size-limit-for-read вы можете читать файлы размером до 2 ГБ   -  person Nishanth Matha    schedule 22.03.2017
comment
если он больше 2 ГБ, как вы упомянули в сообщении ... вам лучше погрузить его в фрагменты файла или даже меньшие фрагменты списков ... и попробуйте отсортировать каждый фрагмент по отдельности и записать его в один основной выходной файл   -  person Nishanth Matha    schedule 22.03.2017
comment
Я могу, но это может помешать получению общего числа вхождений во всем каталоге.   -  person berzerk0    schedule 22.03.2017
comment
beat the purpose как я удивляюсь, что вы все еще получаете общее количество вхождений по всему каталогу ... это только то, что вы создаете промежуточный буфер для чтения и сортировки по частям, но ваш конечный результат все равно будет таким, как вы хотите   -  person Nishanth Matha    schedule 22.03.2017
comment
Разве тогда мне не пришлось бы сортировать большой выходной файл в конце? То, что наиболее распространено в одном фрагменте, может не быть наиболее распространенным в другом.   -  person berzerk0    schedule 22.03.2017
comment
нет, вы бы не... это больше похоже на бинарный поиск!!! например, если у вас есть 3 фрагмента... вы сортируете sort(1,2), затем sort(2,3), затем снова sort(1,2), что даст вам sort of (1,2,3), вы будете использовать логику, аналогичную этой stackoverflow.com/questions/42893884/   -  person Nishanth Matha    schedule 22.03.2017
comment
Звучит многообещающе, если описанный ниже метод не сработает. И когда я делаю чанк, я сортирую его, но удаляю ли я дубликаты? Если я вас правильно понял, то нет, но в конце концов это достаточно близко. Это правильно?   -  person berzerk0    schedule 22.03.2017


Ответы (3)


Я бы решил эту проблему так

import os, sys #needed for looking into a directory
from sys import argv #allows passing of arguments from command line, where I call the script
from collections import Counter #allows the lists to be sorted by number of occurrences

#Pass argument containing Directory of files to be combined
dir_string = str((argv[1]))


#Get name of files in directory, add them to a list
filenames = []
for file in os.listdir(dir_string):
    if file.endswith(".txt"):
        filenames.append(os.path.join(dir_string, file)) #add names of files to a list


#Declare name of file to be written
out_file_name = os.path.join(dir_string, 'out.txt')


# write all the files to a single file instead of list
with open(out_file_name, "w") as outfile:
    for fname in filenames: #for all files in list
        with open(fname) as infile: #open a given file
              for line in infile: #for all lines in current file, read one by one
                   outfile.write(line)

# create a counter object from outfile
with open(out_file_name, "r") as outfile:
    c = Counter(outfile)



print "sorted by line alphabhitically"
from operator import itemgetter   
print sorted(c.items(),key=itemgetter(0))

print "sorted by count"
print sorted(c.items(), key=itemgetter(1))


def index_in_file(unique_line):
    with open(out_file_name, "r") as outfile:
        for num, line in enumerate(outfile, 1):
            if unique_line[0] in line:
                return num

print "sorted by apperance of line in the outfile"
s= sorted(c.items(),key=index_in_file)
print s

# Once you decide what kind of sort you want, write the sorted elements into a outfile.
with open(out_file_name, "w") as outfile:
    for ss in s:
        outfile.write(ss[0].rstrip()+':'+str(ss[1])+'\n')
person plasmon360    schedule 22.03.2017

Это подход к сокращению потребления памяти, который я предлагал в комментариях под одним из других ответов:

lines_seen = collections.Counter()

for filename in filenames:
    with open(filename, 'r') as file:
        for line in file:
            line = line.strip('\n')
            if line:
                lines_seen.update([line])

with open(out_file_name, "w") as outfile:
    for line, count in lines_seen.most_common():
        outfile.write('{}, {}\n'.format(line, count))

Обратите внимание, что line.strip('\n') удаляет новую строку только в конце каждой прочитанной строки, поэтому line.rstrip('\n') будет более эффективным. Вы также можете удалить начальные и конечные пробелы, используя line.strip(). Избавление от сохраняемых пробелов, возможно значительных, еще больше уменьшит использование памяти.

person martineau    schedule 22.03.2017

Ваша проблема явно в нехватке памяти.

Вы можете удалить лишние строки в lines_seen во время процесса, это может помочь.

from collections import Counter
lines_seen = Counter()

# in the for loop :
lines_seen[ lines_seen.append(str(line).strip('\n')) ] += 1

# at the end:
for item in lines_seen.most_common():
    outfile.write(str(item[0])+"\n")

ИЗМЕНИТЬ

Другим решением было бы, как упоминалось в комментариях:

from collections import Counter
lines_seen = Counter()

# get the files names

for fname in filenames: #for all files in list
    with open(fname) as infile: #open a given file
        lines_seen.update(infile.read().split('\n'))

for item in lines_seen.most_common():
    print( item[0], file=outfile )
person User9123    schedule 22.03.2017
comment
Комментарии не для расширенного обсуждения; этот разговор был перенесено в чат. - person Bhargav Rao; 23.03.2017