Как работать с очень большим текстовым файлом?
В настоящее время я пишу что-то, что нужно обрабатывать очень большие текстовые файлы (по крайней мере, несколько гигабайт). Здесь необходимо (и это исправлено):
- CSV-based, после RFC 4180, за исключением встроенных разрывов строк
- случайный доступ для чтения к линиям, хотя в основном по строкам и ближе к концу
- добавляющие строки в конце
- (изменение строк). Очевидно, что призывы к тому, чтобы остальная часть файла была переписана, это также редко, поэтому не особенно важно на данный момент.
Размер файла запрещает его полностью хранить в памяти (что также нежелательно, так как добавление изменений должно быть как можно скорее сохранено).
Я подумал о том, чтобы использовать область с отображением памяти в виде окна в файл, который перемещается, если запрашивается строка за пределами ее диапазона. Конечно, на этом этапе у меня все еще нет абстракции выше байтового уровня. Чтобы действительно работать с содержимым, у меня есть CharsetDecoder
, дающий мне CharBuffer
. Теперь проблема заключается в том, что я могу иметь дело с текстами строк, которые, возможно, очень хороши в CharBuffer
, но мне также нужно знать смещение байта этой строки в файле (чтобы сохранить кеш строк и смещений, t нужно снова отсканировать файл с начала, чтобы найти определенную строку).
Есть ли способ сопоставить смещения в CharBuffer
с смещениями в совпадающем ByteBuffer
вообще? Это явно тривиально с ASCII или ISO-8859- *, тем более, что с UTF-8 и с ISO 2022 или BOCU-1 все становится совершенно безобразным (не то, что я действительно ожидаю, что последние два, но UTF-8 должен быть по умолчанию здесь - и все еще создает проблемы).
Я думаю, я мог бы просто преобразовать часть CharBuffer
в байты снова и использовать длину. Либо он работает, либо у меня возникают проблемы с диакритикой, и в этом случае я мог бы, вероятно, поручить использование NFC или NFD, чтобы гарантировать, что текст всегда однозначно кодируется.
Тем не менее, мне интересно, это даже путь сюда. Есть ли лучшие варианты?
ETA: Некоторые ответы на общие вопросы и предложения здесь:
Это хранилище данных для симуляционных запусков, предназначенных для небольшой локальной альтернативы полномасштабной базе данных. У нас также есть базы данных, и они используются, но для случаев, когда они недоступны или не применимы, мы этого хотим.
Я также поддерживаю только подмножество CSV (без встроенных разрывов строк), но это нормально. Проблемные моменты здесь в значительной степени таковы, что я не могу предсказать, сколько строк и, следовательно, необходимо создать грубую карту файла.
Что касается того, что я изложил выше: проблема, о которой я размышлял, заключалась в том, что я могу легко определить конец строки на уровне символов (U + 000D + U + 000A), но я не хотел предполагать, что это выглядит как 0A 0D
на байтовом уровне (который уже не работает для UTF-16, например, где он либо 0D 00 0A 00
, либо 00 0D 00 0A
). Мои мысли состояли в том, что я мог бы сделать кодировку символов меняющейся, а не жестко-кодирующую информацию о кодировке, которую я сейчас использую. Но я думаю, я мог бы просто придерживаться UTF-8 и все остальное. Похоже, что-то не так.
Ответы
Ответ 1
Очень сложно поддерживать сопоставление 1:1 между последовательностью Java-символов (что эффективно UTF-16) и байтами, которые могут быть любыми в зависимости от кодировки вашего файла. Даже с UTF-8 "очевидное" отображение 1 байт в 1 char работает только для ASCII. Ни UTF-16, ни UTF-8 не гарантируют, что символ Юникода может быть сохранен в одной машине char
или byte
.
Я бы сохранил свое окно в файле в виде байтового буфера, а не буфера char. Затем, чтобы найти окончания строки в байтовом буфере, я бы кодировал строку Java "\r\n"
(или, возможно, просто "\n"
) в качестве последовательности байтов, используя ту же кодировку, что и файл. Я бы тогда использовал эту последовательность байтов для поиска окончаний строк в байтовом буфере. Положение строки, заканчивающейся в буфере + смещение буфера от начала файла, точно соответствует позиции байта в файле окончания строки.
Добавление строк - это всего лишь случай поиска конца файла и добавления новых строк. Изменение линий более сложно. Я думаю, что я бы сохранил список или карту позиций байтов с измененными строками и какими изменениями были. Когда вы готовы записать изменения:
- сортировать список изменений по позиции байта
- прочитайте исходный файл до следующего изменения и напишите его во временный файл.
- записать измененную строку во временный файл.
- пропустить измененную строку в исходном файле.
- вернитесь к шагу 2, если вы не достигли конца исходного файла
- переместить временный файл поверх исходного файла.
Ответ 2
Можно ли разбить файл в "подфайлах" (разумеется, вы не должны разбить его в пределах одного Utf-8 char)? Затем вам понадобятся метаданные для каждого из подфайлов (общее количество символов и общее количество строк).
Если у вас есть это, а "подфайлы" являются относительно маленькими, так что вы всегда можете загрузить один из них, тогда обработка становится легкой.
Даже редактирование становится легким, потому что вам нужно только обновить "подфайл" и его метаданные.
Если вы поместите его в край: тогда вы можете использовать базу данных и хранить одну строку на каждой строке базы данных. - Если это хорошая идея, сильно зависит от вашего использования.
Ответ 3
CharBuffer предполагает, что все символы UTF-16 или UCS-2 (возможно, кто-то знает разницу)
Проблема с использованием правильного текстового формата заключается в том, что вам нужно прочитать каждый байт, чтобы узнать, где находится n-й символ или где находится n-я строка. Я использую текстовые файлы с несколькими GB, но предполагаю данные ASCII-7, и я только читаю/записываю последовательно.
Если вам нужен произвольный доступ к текстовому файлу без объявления, вы не можете ожидать его выполнения.
Если вы хотите купить новый сервер, вы можете получить его с 24 ГБ за 1800 и 64 ГБ за около 4200 фунтов стерлингов. Это позволит вам загружать в память файлы с несколькими ГБ.
Ответ 4
Если у вас есть фиксированные линии ширины, то использование RandomAccessFile
может решить многие ваши проблемы. Я понимаю, что ваши строки, вероятно, не имеют фиксированной ширины, но вы можете искусственно навязать это, добавив индикатор конца строки, а затем строки дополнений (например, с пробелами).
Это, очевидно, лучше всего работает, если ваш файл в настоящее время имеет довольно равномерное распределение длин строк и не имеет некоторых строк, которые очень и очень длинные. Недостатком является то, что это искусственно увеличит размер вашего файла.
Ответ 5
Stick с UTF-8 и \n, обозначающий конец строки, не должен быть проблемой. В качестве альтернативы вы можете разрешить UTF-16 и распознавать данные: он должен быть указан (например), имеет N commans (точки с запятой) и другой конец строки. Может прочитать заголовок, чтобы узнать, сколько столбцов структуры.
может быть достигнуто путем резервирования некоторого пространства в конце/начале каждой строки.
- добавляющие строки в конце
Это тривиально, пока файл заблокирован (как и любые другие модификации)
Ответ 6
В случае фиксированного количества столбцов я разделил файл логически и/или физически на столбцы и реализовал некоторые оболочки/адаптеры для задач ввода-вывода и управления файлом в целом.
Ответ 7
Как насчет таблицы смещений через несколько регулярных интервалов в файле, чтобы вы могли перезапустить парсинг где-то рядом с местом, которое вы ищете?
Идея заключалась бы в том, что это будут байтовые смещения, где кодирование будет в его исходном состоянии (т.е. если данные были закодированы по ISO-2022, то это место будет в режиме, совместимом с ASCII). Любой индекс в данные будет состоять из указателя в эту таблицу плюс все, что требуется для поиска фактической строки. Если вы поместите точки перезапуска так, чтобы каждая из них находилась между двумя точками, вписывающимися в окно mmap, тогда вы можете опустить код проверки/переназначения/перезапуска из уровня синтаксического анализа и использовать парсер, предполагающий, что данные будут последовательно отображаться.