Обработка больших текстовых файлов

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

Мое предлагаемое решение. Разделите огромный файл на несколько файлов, и каждый разделенный файл будет содержать отсортированные слова. Например, все слова, начинающиеся с "a", будут храниться в файле "_a.dic". Таким образом, в любой момент времени мы не превысим более 26 файлов.

Проблема в этом подходе заключается в том,

Я могу использовать потоки для чтения файла, но хотел использовать потоки для чтения определенных частей файла. Например, прочитайте 0-1024 байта с помощью отдельного потока (по крайней мере, 4-8 потоков в зависимости от количества процессоров, существующих в коробке). Это возможно или мне снится?

Есть ли лучший подход?

Примечание. Это должно быть чистое решение на основе C++ или C. Никакие базы данных и т. д. не допускаются.


person asyncwait    schedule 26.10.2009    source источник
comment
Можете ли вы уточнить, как будет выполняться поиск текстового файла? Является ли файл относительно статичным, и вам нужно выполнить много поисков в статическом файле? Потребуется ли вам выполнять поиск по множеству разных слов или важно, чтобы поиск одного слова завершился как можно быстрее? Будет ли в словах, которые вы ищете, обычно есть закономерность - т.е. несколько слов составляют большую часть ваших поисков.   -  person jthg    schedule 26.10.2009
comment
Вы хотите избежать загрузки его в память сразу, потоки были созданы для вашей ситуации.   -  person Matthieu M.    schedule 26.10.2009
comment
Какова цель использования потоков для чтения разных частей файла? Предполагая, что ваш файл находится на обычном жестком диске, потоковая передача напрямую через файл — это самый быстрый способ. Если у вас есть несколько потоков, запрашивающих несколько частей файла одновременно, головка вашего жесткого диска будет прыгать повсюду, что более чем компенсирует любое преимущество, которое вы, возможно, получили от многопоточности.   -  person StriplingWarrior    schedule 26.10.2009
comment
R textmining package (tm), создание корпуса   -  person Nicholas Hamilton    schedule 04.07.2015


Ответы (10)


Вам нужно взглянуть на "Практика программирования" Кернигана и Пайка, и конкретно глава 3.

В C++ используйте карту на основе строк и количества (std::map<string,size_t>, IIRC). Прочитайте файл (один раз — он слишком большой, чтобы читать его более одного раза), разбивая его на слова по мере продвижения (для некоторого определения «слова») и увеличивая количество в записи карты для каждого найденного слова.

В C вам придется создать карту самостоятельно. (Или найдите «C интерфейсы и реализации" Дэвида Хэнсона.)

Или вы можете использовать Perl, или Python, или Awk (все они имеют ассоциативные массивы, эквивалентные карте).

person Jonathan Leffler    schedule 26.10.2009
comment
В зависимости от содержимого 3-гигабайтного файла и объема имеющейся у вас памяти, чтение всего этого в карту может быть слишком большим, чтобы поместиться в память, когда добавляются накладные расходы памяти карты. - person jthg; 26.10.2009
comment
В английском языке около 100 000 слов. Предположим, что определение «слово» не выполняет сопоставление регистра и улавливает знаки препинания, так что для каждого слова имеется 5 вариантов. Давайте предположим, что в среднем слово состоит из 10 символов (избыток), а служебные данные карты составляют, ох, 22 байта. Тогда у нас есть 5 * 100 000 * 32 = 16 МБ. Какой размер компьютера будет иметь проблемы с этим? - person Jonathan Leffler; 26.10.2009
comment
Я не уверен, что это произойдет. В случае, когда слов всего несколько тысяч, карта будет не такой большой. Так что это действительно зависит от количества отдельных слов, а не от размера файла. - person Matthieu M.; 26.10.2009
comment
Имеющийся у меня под рукой список английских слов (думаю, это список Моби, но точно не помню) содержит около трехсот тысяч слов, но с этим все еще не так уж сложно иметь дело. Однако я бы не стал учитывать пунктуацию — я бы убрал ее перед подсчетом слов (за исключением, может быть, сокращений). Большинство людей не хотят, чтобы x и x, считались двумя разными словами. - person Jerry Coffin; 26.10.2009
comment
У вас, ребята, есть очень хорошая идея. Я не уверен, что я думал. Может быть, меня немного смутило то, что ОП хочет каждый раз выполнять поиск по всему файлу, и у меня сложилось впечатление, что файл на самом деле содержит порядка 1 ГБ уникальных слов. - person jthg; 26.10.2009
comment
@ Джонатан, я полностью согласен, и я благодарен вам за то, что вы представили эти две книги. Единственная проблема в вашем ответе заключается в том, как быстро я могу прочитать файл. Ваш ответ, вероятно, подходит для подсчета уникальных слов с использованием карты или лайков. Но реальный вопрос, как быстро я могу перемещаться по файлу? Это причина, по которой мое предлагаемое решение использует потоки, но это не похоже на ответы ниже. - person asyncwait; 27.10.2009
comment
Я сделал очень похожую вещь, за исключением одной детали. Мой первоначальный код действительно использовал std::map<std::string, T>, но как только я избавился от ошибок, я изменил его на std::map<StringShim, T>. StringShim был простым 4-байтовым классом, обертывающим char*; фактическими строками управлял один StringPool. Это было значительно эффективнее. - person MSalters; 27.10.2009
comment
@Vadi: потоки не помогут вам читать файл быстрее совсем, а наоборот. Многопоточность имеет смысл только тогда, когда вы можете обрабатывать работу параллельно на нескольких процессорах. Но по-прежнему есть только одна считывающая головка диска для чтения данных с жесткого диска. - person Konrad Rudolph; 27.10.2009
comment
@Konrad: Но я могу использовать потоки для обхода буфера, как упоминал Джерри Кофин ниже. По-видимому, мне нужно подсчитывать повторяющиеся слова, чтобы потоки с более чем одним выделенным буфером могли быть более эффективными. - person asyncwait; 27.10.2009
comment
FWIW: на машине с одним диском я выполнил 'dd if=/dev/zero of=./junk bs=64k count=49152' за 39 секунд и 'cat ./junk ›/dev/null' за 46 секунд. примерно так же просто, с точки зрения «без обработки», как вы можете получить, и показывает скорость передачи 67-79 МБ/с. За это время вы можете сделать много обработки, не сталкиваясь с проблемами. Рассмотрите возможность сопоставления памяти всего файла, если у вас 64-битная машина (на 32-битной машине это, вероятно, не сойдет с рук, хотя, возможно, стоит попробовать). В отсутствие лучших дисков обработка не будет узким местом. - person Jonathan Leffler; 27.10.2009

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

В случае, если ЦП действительно загружен в однопоточной версии, возможно потенциальное ускорение. Один поток мог считывать данные большими порциями и помещать их в очередь ограниченной емкости. Группа других рабочих потоков может работать каждый со своим фрагментом и считать слова. После завершения подсчета рабочих потоков вам необходимо объединить счетчики слов.

person sellibitze    schedule 26.10.2009
comment
Я бы назвал это почти наверняка. ЦП должен обрабатывать байты намного быстрее, чем диск может извлечь их из пластины, поэтому распараллеливать действительно нечего. - person jprete; 26.10.2009
comment
Я согласен. Я мог бы даже сделать еще один шаг и сказать, что даже если весь файл находится в памяти, ЦП все равно будет обрабатывать слова быстрее, чем они могут быть прочитаны из памяти. - person jthg; 26.10.2009
comment
Не согласен с последним утверждением. Чтение текста из памяти вызовет предварительную выборку процессора. Это чертовски быстро. Узким местом будет поиск произвольного доступа O(log N) для счетчика слов. Вряд ли они все поместятся в кэш L2. - person MSalters; 27.10.2009
comment
Да, предварительная выборка очень помогает, но я уже исходил из идеальной предварительной выборки. Просто для того, чтобы убедиться, что то, что я сказал, нелепо, я сделал грубый расчет по порядку величины: пропускная способность памяти составляет около 10 гигабайт в секунду. Предполагая, что процессор представляет собой ядро ​​2, которое может выполнять одно сравнение за цикл (для одного ядра), получается примерно (8 байт/цикл) * (2e9 циклов/секунду) = 16 гигабайт/секунду. Порядок величины получается примерно таким же. Кто-нибудь хочет указать более точные цифры? - person jthg; 27.10.2009

Во-первых, определитесь со структурой данных для сохранения слов.

Очевидный выбор — карта. Но, возможно, вам лучше подойдет Trie. В каждом узле вы сохраняете счет для слова. 0 означает, что это только часть слова. Вы можете вставить в дерево, используя поток и чтение вашего файла на основе символов.

Второе - многопоточность да или нет? Ответить на этот вопрос непросто. В зависимости от размера структуры данных растет и то, как вы распараллеливаете ответ, может отличаться.

  1. Однопоточный — простой и простой в реализации.
  2. Многопоточный с несколькими потоками чтения и одной структурой данных. Затем вы должны синхронизировать доступ к структуре данных. В Trie вам нужно только заблокировать узел, в котором вы фактически находитесь, чтобы несколько читателей могли получить доступ к структуре данных без особых помех. Самобалансирующееся дерево может быть другим, особенно при перебалансировке.
  3. Многопоточный с несколькими потоками чтения, каждый со своей собственной структурой данных. Каждый поток строит свою собственную структуру данных при чтении части файла. После завершения каждого из них результаты должны быть объединены (что должно быть легко).

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

person Tobias Langner    schedule 26.10.2009
comment
Хороший обзор возможностей и +1 за упоминание о трие как неочевидном решении. - person Konrad Rudolph; 27.10.2009

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

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

Проблема довольно проста: файл, который вы читаете, (потенциально) больше доступного места в кэше, но вы ничего не выиграете от кэширования, потому что вы не собираетесь снова перечитывать куски файла ( по крайней мере, если вы делаете вещи разумно). Таким образом, вы хотите, чтобы система обходила любое кэширование и просто переносила данные как можно напрямую с диска в вашу память, где вы можете их обработать. В Unix-подобной системе это, вероятно, open() и read() (и вы не получите многого). В Windows это CreateFile и ReadFile, передача флага FILE_FLAG_NO_BUFFERING в CreateFile -- и это, вероятно, примерно удвоит вашу скорость, если вы все сделаете правильно.

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

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

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

person Jerry Coffin    schedule 26.10.2009

Как указывали другие, узким местом будет дисковый ввод-вывод. Поэтому я предлагаю вам использовать перекрывающийся ввод-вывод. Это в основном инвертирует логику программы. Вместо того, чтобы ваш код пытался определить, когда выполнять ввод-вывод, вы просто говорите операционной системе вызывать ваш код всякий раз, когда он завершает часть операций ввода-вывода. Если вы используете порты завершения ввода-вывода, вы даже можете указать ОС использовать несколько потоков для обработки фрагментов файла.

person MSalters    schedule 27.10.2009

решение на основе c?

Я думаю, Perl был создан именно для этой цели.

person Oren Mazor    schedule 26.10.2009
comment
Я согласен. Такая обработка текстовых файлов в Perl вполне естественна. - person Martin York; 27.10.2009
comment
Опять же, написать это решение на C++ просто и легко (несмотря на многопоточность, которая, вероятно, создаст те же проблемы в C++ и Perl). - person Konrad Rudolph; 27.10.2009
comment
идея о том, что вам нужно использовать С++ для подсчета экземпляров слов в файле, каким бы большим он ни был, мне кажется странной. Я имею в виду без обид. Я уверен, что представленные здесь решения вполне приемлемы для некоторых людей, но я старомоден. 10 строк Perl будут готовы. - person Oren Mazor; 27.10.2009
comment
Я не знаю, почему за этот пост проголосовали. Просто потому, что это не решение C? Как сказал Орен, если вначале использовался perl (или awk,...), исходная проблема уже была решена. Скорость может быть не такой быстрой, как у усовершенствованной версии C, но они в основном на том же уровне сложности. - person Codism; 27.10.2009

поток имеет только один курсор. Если вы обращаетесь к потоку более чем с одним потоком одновременно, вы не будете уверены, что будете читать там, где хотите. Чтение выполняется с позиции курсора.

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

Например:

  • Поток #i готов и попросите основной поток дать ему следующую часть,
  • Основной поток читает следующие 1 Мб и передает их потоку 1,
  • Тема #я читаю 1Мб и считаю слова сколько хочешь,
  • Поток #i заканчивает свою работу и снова просит следующий 1Mb.

Таким образом, вы можете отделить потоковое чтение от потокового анализа.

person Patrice Bernassola    schedule 26.10.2009
comment
Я не думаю, что есть смысл возиться с потоками. Такая задача будет абсолютно связана с вводом-выводом. Ваш жесткий диск не сможет передавать данные достаточно быстро, чтобы загрузить даже ядро. - person divegeek; 26.10.2009

То, что вы ищете, это RegEx. Этот поток Stackoverflow на механизмах регулярных выражений С++ должен помочь:

C++: какую библиотеку регулярных выражений следует использовать?

person ryber    schedule 26.10.2009
comment
Я даже не могу себе представить ужасы поиска 3-гигабайтного файла через RegEx. - person jthg; 26.10.2009
comment
Если только... механизм регулярных выражений не оптимизирован для потоковой обработки. - person jthg; 26.10.2009
comment
У меня есть программа, которая регулярно обрабатывает столько данных, и она довольно быстрая. - person ryber; 26.10.2009

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

Но, учитывая ваши ограничения, вот что я бы сделал.

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

индекс = 0

цифры = 0

mysplitfile = открытый файл (index-split.txt)

в то время как (большой файл >> слово)

mysplitfile << word

numwords ++

if (numwords > 5000)

    mysplitfile.close()

    index++

    mysplitfile = openfile(index-split.txt)

2) Используйте общую структуру данных карты и потоки для создания новых потоков для чтения каждого из подфайлов. Опять псевдокод:

maplock = create_pthread_lock()

общая карта = std::map()

для каждого файла index-split.txt:

spawn-new-thread(myfunction, filename, sharedmap, lock)

dump_map (общая карта)

void myfunction (имя файла, общая карта) {

localmap = std::map<string, size_t>();

file = openfile(filename)

while (file >> word)

    if !localmap.contains(word)
         localmap[word] = 0

    localmap[word]++

acquire(lock)
for key,value in localmap
    if !sharedmap.contains(key)
         sharedmap[key] = 0

    sharedmap[key] += value
release(lock)

}

Извините за синтаксис. В последнее время я много пишу на питоне.

person spitzanator    schedule 26.10.2009
comment
Использование замка определенно не является хорошей идеей. Вы убиваете параллелизм. Гораздо проще, если вы хотите перейти на MT, заставить каждый поток играть со своей собственной картой и просто объединить их в конце. - person Matthieu M.; 26.10.2009
comment
hay spitzanator, вы читали обработку естественного языка с помощью python? - person zeroin23; 26.10.2009
comment
Может ли кто-нибудь пролить свет на то, почему за это проголосовали? Является ли это подходящим ответом или, как упоминалось ранее, чтение диска с несколькими потоками неэффективно? или только из-за pythonicpseudocode? - person asyncwait; 27.10.2009

Не C, и немного УЖАСНО, но на это ушло всего 2 минуты:

perl -lane '$h{$_}++ for @F; END{for $w (sort {$h{$b}<=>$h{$a} || $a cmp $b} keys %h) {print "$h{$w}\t$w"}}' file > freq

Зациклить каждую строку с помощью -n
Разбить каждую строку на @F слов с помощью -a
Каждое $_ слово увеличивает хэш %h
Как только будет достигнуто END из file,
sort хэш с частотой $h{$b}<=>$h{$a}
> Если две частоты совпадают, отсортировать по алфавиту $a cmp $b
Вывести частоту $h{$w} и слово $w
Перенаправить результаты в файл 'freq'

Я запустил этот код в текстовом файле размером 3,3 ГБ, содержащем 580 000 000 слов.
Perl 5.22 выполнился за 173 секунды.

В моем входном файле уже были удалены знаки препинания, а заглавные буквы были преобразованы в строчные с помощью этого фрагмента кода:
perl -pe "s/[^a-zA-Z \t\n']/ /g; tr/A-Z/a-z/" file_raw > file
(время выполнения 144 секунды)


В качестве альтернативы сценарий подсчета слов можно было бы написать на awk:
awk '{for (i=1; i<=NF; i++){h[$i]++}} END{for (w in h){printf("%s\t%s\n", h[w], w)}}' file | sort -rn > freq

person Chris Koknat    schedule 12.10.2015