Разбить очень большой текстовый файл по максимуму строк
Я хочу разбить огромный файл, содержащий строки, на набор новых (меньших) файлов и попытался использовать nio2.
Я не хочу загружать весь файл в память, поэтому я попробовал это с BufferedReader.
Текстовые файлы меньшего размера должны быть ограничены количеством текстовых строк.
Решение работает, однако я хочу спросить, знает ли кто-нибудь решение с лучшей производительностью, используя usion java 8 (может быть, lamdas с stream()-api?) И nio2:
public void splitTextFiles(Path bigFile, int maxRows) throws IOException{
int i = 1;
try(BufferedReader reader = Files.newBufferedReader(bigFile)){
String line = null;
int lineNum = 1;
Path splitFile = Paths.get(i + "split.txt");
BufferedWriter writer = Files.newBufferedWriter(splitFile, StandardOpenOption.CREATE);
while ((line = reader.readLine()) != null) {
if(lineNum > maxRows){
writer.close();
lineNum = 1;
i++;
splitFile = Paths.get(i + "split.txt");
writer = Files.newBufferedWriter(splitFile, StandardOpenOption.CREATE);
}
writer.append(line);
writer.newLine();
lineNum++;
}
writer.close();
}
}
2 ответа
Остерегайтесь разницы между прямым использованием InputStreamReader
/ OutputStreamWriter
и их подклассы и тому Reader
/ Writer
заводские методы Files
, В то время как в первом случае системная кодировка по умолчанию используется, когда явная кодировка не указана, во втором всегда по умолчанию UTF-8
, Поэтому я настоятельно рекомендую всегда указывать нужную кодировку, даже если она Charset.defaultCharset()
или же StandardCharsets.UTF_8
задокументировать свое намерение и избежать неожиданностей, если вы переключаетесь между различными способами создания Reader
или же Writer
,
Если вы хотите разделить границы строк, то нет смысла просматривать содержимое файла. Таким образом, вы не можете оптимизировать его так, как при слиянии.
Если вы готовы пожертвовать переносимостью, вы можете попробовать некоторые оптимизации. Если вы знаете, что кодировка charset однозначно отобразится '\n'
в (byte)'\n'
как это имеет место для большинства однобайтовых кодировок, а также для UTF-8
Вы можете сканировать разрывы строк на уровне байтов, чтобы получить позиции файлов для разделения и избежать какой-либо передачи данных из вашего приложения в систему ввода-вывода.
public void splitTextFiles(Path bigFile, int maxRows) throws IOException {
MappedByteBuffer bb;
try(FileChannel in = FileChannel.open(bigFile, READ)) {
bb=in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
}
for(int start=0, pos=0, end=bb.remaining(), i=1, lineNum=1; pos<end; lineNum++) {
while(pos<end && bb.get(pos++)!='\n');
if(lineNum < maxRows && pos<end) continue;
Path splitFile = Paths.get(i++ + "split.txt");
// if you want to overwrite existing files use CREATE, TRUNCATE_EXISTING
try(FileChannel out = FileChannel.open(splitFile, CREATE_NEW, WRITE)) {
bb.position(start).limit(pos);
while(bb.hasRemaining()) out.write(bb);
bb.clear();
start=pos;
lineNum = 0;
}
}
}
Недостатки в том, что он не работает с такими кодировками, как UTF-16
или же EBCDIC
и, в отличие от BufferedReader.readLine()
это не поддержит одинокого '\r'
в качестве ограничителя строки, как в старой MacOS9.
Кроме того, он поддерживает только файлы размером менее 2 ГБ; предел, вероятно, еще меньше для 32-битных JVM из-за ограниченного виртуального адресного пространства. Для файлов, размер которых превышает предел, необходимо выполнить итерации фрагментов исходного файла и map
их один за другим.
Эти проблемы можно исправить, но это повысит сложность этого подхода. Учитывая тот факт, что повышение скорости составляет всего около 15% на моей машине (я не ожидал намного большего, поскольку здесь преобладает ввод / вывод) и будет еще меньше, когда сложность возрастает, я не думаю, что это того стоит.
Суть в том, что для этой задачи Reader
/ Writer
подход достаточно, но вы должны позаботиться о Charset
используется для операции.
Я сделал небольшое изменение в коде @nimo23, учитывая возможность добавления заголовка и нижнего колонтитула для каждого из разделенных файлов, а также выводит файлы в каталог с тем же именем, что и исходный файл, с добавленным к нему _split, код ниже:
public static void splitTextFiles(String fileName, int maxRows, String header, String footer) throws IOException
{
File bigFile = new File(fileName);
int i = 1;
String ext = fileName.substring(fileName.lastIndexOf("."));
String fileNoExt = bigFile.getName().replace(ext, "");
File newDir = new File(bigFile.getParent() + "\\" + fileNoExt + "_split");
newDir.mkdirs();
try (BufferedReader reader = Files.newBufferedReader(Paths.get(fileName)))
{
String line = null;
int lineNum = 1;
Path splitFile = Paths.get(newDir.getPath() + "\\" + fileNoExt + "_" + String.format("%03d", i) + ext);
BufferedWriter writer = Files.newBufferedWriter(splitFile, StandardOpenOption.CREATE);
while ((line = reader.readLine()) != null)
{
if(lineNum == 1)
{
writer.append(header);
writer.newLine();
}
writer.append(line);
writer.newLine();
lineNum++;
if (lineNum > maxRows)
{
writer.append(footer);
writer.close();
lineNum = 1;
i++;
splitFile = Paths.get(newDir.getPath() + "\\" + fileNoExt + "_" + String.format("%03d", i) + ext);
writer = Files.newBufferedWriter(splitFile, StandardOpenOption.CREATE);
}
}
if(lineNum <= maxRows) // early exit
{
writer.append(footer);
}
writer.close();
}
System.out.println("file '" + bigFile.getName() + "' split into " + i + " files");
}