拆分文本行,同时附加前缀
Splitting text lines, whilst appending prefix
已编辑
我有大约 50x9Gb .mer
个文件,如下所示:
"xxxxx";"123\t123\t123\v234\t234\v234\t224\t234\v"
"yyyyy";"123\t234\t224\v234\t234\v234\t224\t234\v"
"zzzzz";"123\t456\t565\v234\t774"
A uuid
后跟 ";"
,然后可能是额外的制表符条目,然后是垂直制表符分隔的列表 更多选项卡式 条目,全部用引号括起来。我在这里将它们显示为 3 位数字,但实际上它们是可变长度的字符串,其中可以包含双引号 ""
.
我需要把它们变成这样:
xxxxx\t123\t123\t123
xxxxx\t234\t234
xxxxx\t234\t224\t234
yyyyy\t123\t234\t224
yyyyy\t234\t234
yyyyy\t234\t224\t234
zzzzz\t123\t456\t565
zzzzz\t234\t774
也就是说,拆分垂直选项卡上的行,在每行前面加上它来自的行的第一个字段。
目前,我正在使用 noddy 正则表达式,它至少可以工作,但需要多次运行和手动检查。
我如何使用 awk
或 sed
来做到这一点?我已尝试调整下面的当前答案,但我无法找出 ;P 和 ;D 后缀的含义。
(注意:我在 Windows 上使用 GitBash,所以我猜那是 gnu sed
和 awk
?)
您可以将此 awk 命令用于此输出:
awk 'BEGIN{FS=OFS="\t"} n = split(, a, "\x0b") {
for (i=1; i<=n; i++) print , a[i]}' file
195a664e-e0d0-4488-99d6-5504f9178115 1234
195a664e-e0d0-4488-99d6-5504f9178115 1412
195a664e-e0d0-4488-99d6-5504f9178115 1231
195a664e-e0d0-4488-99d6-5504f9178115 4324
195a664e-e0d0-4488-99d6-5504f9178115 1421
195a664e-e0d0-4488-99d6-5504f9178115 3214
a1d61289-7864-40e6-83a7-8bdb708c459e 1412
a1d61289-7864-40e6-83a7-8bdb708c459e 6645
a1d61289-7864-40e6-83a7-8bdb708c459e 5334
a1d61289-7864-40e6-83a7-8bdb708c459e 3453
a1d61289-7864-40e6-83a7-8bdb708c459e 5453
工作原理:
BEGIN{FS=OFS="\t"} # sets input and output field separator as tab
n = split(, a, "\x0b") # splits second field using Hex 0B (ASCII 11) i.e. vertical tab
for (i=1; i<=n; i++) ... # prints pair of field 1 with each item from split array a
这可能适合您 (GNU sed):
sed -r 's/^((\S*\t)\S*)\v/\n/;P;D' file
用换行符、第一个字段和制表符替换每个 \v
。打印并删除第一行并重复。
编辑:根据新问题;
sed -r '/\n/!s/"(")?//g;/\n/!s/;/\t/;s/^((\S*\t)[^\v]*)\v/\n/;/\t$/!P;D' file
删除任何单双引号(将双引号替换为单双引号)并将分号替换为制表符。然后用换行符、第一个字段和制表符替换任何 \v
,然后重复。
awk -F';' -v OFS='\t' #set Field separator is ';',
'{for(i=1;i<=NF;i++) #then we have 2 fields, remove leading and trailing doubled qoutes
gsub(/^"|"$/,"",$i)
c=split(,a,"\v") #split by vertical tab, save result in array 'a'
for(i=1;i<=c;i++) #for each element in a, if it is not empty, print field1 (the uuid)
if(a[i])print ,a[i]}' file #and the element, separated by Tab
解释是内联的。
它输出:
xxxxx 123 123 123
xxxxx 234 234
xxxxx 234 224 234
yyyyy 123 234 224
yyyyy 234 234
yyyyy 234 224 234
zzzzz 123 456 565
zzzzz 234 774
gnu sed
sed 's/"\|..$//g;s/;/\t/;:r;s/^\([^\t]*\)\t\(.*\)\v/\t\n\t/;t r;s/\t/\t/g;' YourFile
首先递归替换 \v "field" + 制表符 + 清除途中多余的字符
另一个解决方案使用 awk
awk '
BEGIN{FS="[\v;]"}
{
gsub("[\"]","");
for(i=2; i<=NF; ++i)
if($i) printf "%s\t%s\n", , $i;
}' file.mer
另一个解决方案使用 sed
sed -r 's/\v\n/\v/g; s/"//g;
:a; s/([^;]*);([^\v]*)\v/;\n;/g; ta;
s/;/\t/g;' file.mer | sed -r '/^[^\t]+\t$/d'
你明白了,
xxxxx 123 123 123
xxxxx 234 234
xxxxx 234 224 234
yyyyy 123 234 224
yyyyy 234 234
yyyyy 234 224 234
zzzzz 123 456 565
zzzzz 234 774
好吧,我故意等到 Kent 的回答被接受并获得赏金,因为问题是关于 awk/sed 的。因此,我的回答可能有点 off-topic,但无论如何,这是我的 Java 解决方案,我只是为了好玩才做的。
MER 输入文件生成器:
我认为生成一些具有随机值的示例输入文件会很好。每行包含
- 一个UUID,
- 0-9 组,由垂直制表符分隔,
- 在每组中,1-4 个字符串,由水平制表符分隔,
- 每个字符串由1-20个字符组成,其中双引号被其他双引号转义,即
""
.
我认为这足够多样化,可以获得一些好的测试数据。
package de.scrum_master.Whosebug;
import org.apache.commons.lang.RandomStringUtils;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Random;
import java.util.UUID;
public class RandomFileGenerator {
private static final int BUFFER_SIZE = 1024 * 1024;
private final static Random RANDOM = new Random();
private final static char VERTICAL_TAB = '\u000b';
private final static char[] LEGAL_CHARS =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzäöüÄÖÜß. -\""
.toCharArray();
public static void main(String[] args) throws IOException {
long startTime = System.currentTimeMillis();
// final long maxOutputSize = 9L * 1024 * 1024 * 1024;
// final String outputFile = "src/main/resources/sample-9gb.mer";
final long maxOutputSize = 1L * 1024 * 1024;
final String outputFile = "src/main/resources/sample-1mb.mer";
long totalOutputSize = 0;
long lineCount = 0;
String line;
try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(outputFile), BUFFER_SIZE))) {
while (totalOutputSize < maxOutputSize) {
line = generateLine();
writer.println(generateLine());
totalOutputSize += line.length() + 1;
lineCount++;
}
}
System.out.println(lineCount);
System.out.println(totalOutputSize);
System.out.println((System.currentTimeMillis() - startTime) / 1000.0);
}
private static String generateLine() {
StringBuilder buffer = new StringBuilder();
buffer
.append('"')
.append(UUID.randomUUID().toString())
.append("\";\"");
int numItems = RANDOM.nextInt(10);
for (int i = 0; i < numItems; i++) {
int numSubItems = 1 + RANDOM.nextInt(4);
for (int j = 0; j < numSubItems; j++) {
buffer.append(
RandomStringUtils.random(1 + RANDOM.nextInt(20), 0, LEGAL_CHARS.length, false, false, LEGAL_CHARS)
.replaceAll("\"", "\"\"")
);
if (j + 1 < numSubItems)
buffer.append('\t');
}
if (i + 1 < numItems) {
buffer.append(VERTICAL_TAB);
}
}
buffer.append('"');
return buffer.toString();
}
}
您可以看到创建所需文件大小的测试文件很容易,例如
- 1 MB:
maxOutputSize = 1L * 1024 * 1024
- 9 GB:
maxOutputSize = 9L * 1024 * 1024 * 1024
我主要使用较小的用于在开发过程中检查算法,而使用真正大的用于性能调整。
4 种不同变体的文件拆分器:
这里显示的变体使用不同的方法,但它们的共同点是它们通过 reader.lines()
从 BufferedReader
和 Java 流中读取。从流切换到简单的 for
循环使其变慢,顺便说一句。所有解决方案将结果写入PrintWriter
.
reader.lines().forEach()
然后正则匹配+拆分。此解决方案在可读性、简洁性和性能之间具有最佳 trade-off。
reader.lines().flatMap()
,即UUID后的vertical-tab-separated组使用sub-streams,同样使用正则匹配+拆分。这个解决方案也非常简短和优雅,但比#1 更难阅读,而且速度也慢了大约 15%。
因为像 replace()
和 split()
这样的正则表达式匹配调用可能非常昂贵,所以我开发了一个解决方案,它迭代字符串并使用 indexOf()
和 substring()
而不是正则表达式。这比 #1 和 #2 快得多,但代码更难阅读,我开始不喜欢这种方式。只有在性能非常重要的情况下才应该这样做,即如果经常使用文件拆分器。对于 one-time 解决方案,或者如果它每月运行一次,我认为从可维护性的角度来看,这并不值得。
#3 的进一步优化版本,它避免了更多的开销并且再次更快了一点,但速度并不快。现在代码确实需要源代码注释,以便向 reader 传达算法的作用。从干净代码的角度来看,这是一场噩梦。 (不要在家里这样做,孩子们!)
package de.scrum_master.Whosebug;
import java.io.*;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FileSplitter {
private static final int BUFFER_SIZE = 1024 * 1024;
private static final Pattern LINE_PATTERN = Pattern.compile("^\"([^\"]+)\";\"(.*)\"$");
private final static char VERTICAL_TAB = '\u000b';
public static void main(String[] args) throws IOException {
long startTime = System.currentTimeMillis();
String inputFile = "src/main/resources/sample-9gb.mer";
String outputFile = inputFile.replaceFirst("mer$", "txt");
try (
BufferedReader reader = new BufferedReader(new FileReader(inputFile), BUFFER_SIZE);
PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(outputFile), BUFFER_SIZE))
) {
// forEachVariant(reader, writer);
// flatMapVariant(reader, writer);
noRegexSimpleVariant(reader, writer);
// noRegexOptimisedVariant(reader, writer);
}
System.out.println((System.currentTimeMillis() - startTime) / 1000.0);
}
private static void forEachVariant(BufferedReader reader, PrintWriter writer) {
Matcher matcher = LINE_PATTERN.matcher("dummy");
reader.lines()
.forEach(line -> {
matcher.reset(line).matches();
for (String record : matcher.group(2).replace("\"\"", "\"").split("\v"))
writer.println(matcher.group(1) + "\t" + record);
});
}
private static void flatMapVariant(BufferedReader reader, PrintWriter writer) {
Matcher matcher = LINE_PATTERN.matcher("dummy");
reader.lines()
.flatMap(line -> {
matcher.reset(line).matches();
return Arrays
.stream(matcher.group(2).replace("\"\"", "\"").split("\v"))
.map(record -> matcher.group(1) + "\t" + record);
})
.forEach(writer::println);
}
private static void noRegexSimpleVariant(BufferedReader reader, PrintWriter writer) {
reader.lines()
.forEach(line -> {
final int lineLength = line.length();
// UUID + '\t'
int indexLeft = 1;
int indexRight = line.indexOf('"', indexLeft);
final String uuid = line.substring(indexLeft, indexRight) + "\t";
indexLeft = indexRight + 3;
String record;
int quoteIndex;
while (indexLeft < lineLength) {
writer.print(uuid);
indexRight = line.indexOf(VERTICAL_TAB, indexLeft);
if (indexRight == -1)
indexRight = lineLength - 1;
while (indexLeft < indexRight) {
quoteIndex = line.indexOf('"', indexLeft);
if (quoteIndex == -1 || quoteIndex >= indexRight)
quoteIndex = indexRight;
else
quoteIndex++;
record = line.substring(indexLeft, quoteIndex);
writer.print(record);
indexLeft = quoteIndex + 1;
}
writer.println();
indexLeft = indexRight + 1;
}
});
}
private static void noRegexOptimisedVariant(BufferedReader reader, PrintWriter writer) throws IOException {
reader.lines()
.forEach(line -> {
// UUID + '\t'
int indexLeft = 1;
int indexRight = line.indexOf('"', indexLeft);
final String uuid = line.substring(indexLeft, indexRight) + "\t";
// Skip '";"' after UUID
indexLeft = indexRight + 3;
final int lineLength = line.length();
String recordChunk;
int quoteIndex;
// If search for '"' has once reached end of line, search no more
boolean doQuoteSearch = true;
// Iterate over records per UUID, separated by vertical tab
while (indexLeft < lineLength) {
writer.print(uuid);
indexRight = line.indexOf(VERTICAL_TAB, indexLeft);
if (indexRight == -1)
indexRight = lineLength - 1;
// Search for '""' within record incrementally, + replace each of them by '"'.
// BTW, if '"' is found, it actually always will be an escaped '""'.
while (indexLeft < indexRight) {
if (doQuoteSearch) {
// Only search for quotes if we never reached the end of line before
quoteIndex = line.indexOf('"', indexLeft);
assert quoteIndex != -1;
if (quoteIndex >= lineLength - 1)
doQuoteSearch = false;
if (quoteIndex >= indexRight)
quoteIndex = indexRight;
else
quoteIndex++;
}
else {
// No more '"' within record
quoteIndex = indexRight;
}
// Write record chunk, skipping 2nd '"'
recordChunk = line.substring(indexLeft, quoteIndex);
writer.print(recordChunk);
indexLeft = quoteIndex + 1;
}
// Do not forget newline before reading next line/UUID
writer.println();
indexLeft = indexRight + 1;
}
});
}
}
更新的 awk 脚本:
此外:每个Java解决方案写出一个没有任何内容的UUID,以防输入文件中有none。这很容易避免,但我是故意的。这是与我用作基准的这个稍微更新的 awk 脚本(基于 Dave 的,但也用 "
替换 ""
)的唯一区别:
#!/usr/bin/awk
{
for(i=1;i<=NF;i++) {
gsub(/^"|"$/,"",$i)
gsub(/""/,"\"",$i)
}
c=split(,a,"\v")
for(i=1;i<=c;i++)
print ,a[i]
}
性能结果:
我测量了解析和写入性能。
- 解析意味着从磁盘读取一个 9 GB 的文件并将其拆分,但将输出写入 /dev/null 或根本不写入。
写入意味着读取相同的 9 GB 文件并将其写回到相同的磁盘分区(混合 HD + SSD 类型),即可以通过写入另一个物理磁盘来进一步优化。输出文件的大小为 18 GB。
读取文件,拆分成行但不解析行:66秒
Awk
- 仅解析:533 秒
- 解析+写入:683秒
reader.lines().forEach()
然后正则匹配+拆分
- 仅解析:212 秒
- 解析+写入:425秒
reader.lines().flatMap()
,即使用 sub-streams
- 仅解析:245 秒
- 解析+写入:未测
不使用正则表达式,但使用 String.replace("\"\"", "\"")
(此处代码中未显示)
- 仅解析:154 秒
- 解析+写入:369秒
无正则表达式,无replace()
,简单版本
- 仅解析:86 秒
- 解析+写入:342秒
无正则,无replace()
,优化版
- 仅解析:84 秒
- 解析+写入:342秒
抱歉冗长的论文,但我想与阅读问题和其他答案的其他人分享我的发现,推测 Java(或 C?)是否可能比 awk 更快 - 是的,它是有相当大的一点,但不是一个数量级,因为磁盘性能也是一个因素。我认为这是对那些为了优化而倾向于 over-optimise 的人的警告。这是不值得的如果你走得太远,只需尝试在努力、可读性和性能之间找到最佳平衡点。阿们。
已编辑
我有大约 50x9Gb .mer
个文件,如下所示:
"xxxxx";"123\t123\t123\v234\t234\v234\t224\t234\v"
"yyyyy";"123\t234\t224\v234\t234\v234\t224\t234\v"
"zzzzz";"123\t456\t565\v234\t774"
A uuid
后跟 ";"
,然后可能是额外的制表符条目,然后是垂直制表符分隔的列表 更多选项卡式 条目,全部用引号括起来。我在这里将它们显示为 3 位数字,但实际上它们是可变长度的字符串,其中可以包含双引号 ""
.
我需要把它们变成这样:
xxxxx\t123\t123\t123
xxxxx\t234\t234
xxxxx\t234\t224\t234
yyyyy\t123\t234\t224
yyyyy\t234\t234
yyyyy\t234\t224\t234
zzzzz\t123\t456\t565
zzzzz\t234\t774
也就是说,拆分垂直选项卡上的行,在每行前面加上它来自的行的第一个字段。
目前,我正在使用 noddy 正则表达式,它至少可以工作,但需要多次运行和手动检查。
我如何使用 awk
或 sed
来做到这一点?我已尝试调整下面的当前答案,但我无法找出 ;P 和 ;D 后缀的含义。
(注意:我在 Windows 上使用 GitBash,所以我猜那是 gnu sed
和 awk
?)
您可以将此 awk 命令用于此输出:
awk 'BEGIN{FS=OFS="\t"} n = split(, a, "\x0b") {
for (i=1; i<=n; i++) print , a[i]}' file
195a664e-e0d0-4488-99d6-5504f9178115 1234
195a664e-e0d0-4488-99d6-5504f9178115 1412
195a664e-e0d0-4488-99d6-5504f9178115 1231
195a664e-e0d0-4488-99d6-5504f9178115 4324
195a664e-e0d0-4488-99d6-5504f9178115 1421
195a664e-e0d0-4488-99d6-5504f9178115 3214
a1d61289-7864-40e6-83a7-8bdb708c459e 1412
a1d61289-7864-40e6-83a7-8bdb708c459e 6645
a1d61289-7864-40e6-83a7-8bdb708c459e 5334
a1d61289-7864-40e6-83a7-8bdb708c459e 3453
a1d61289-7864-40e6-83a7-8bdb708c459e 5453
工作原理:
BEGIN{FS=OFS="\t"} # sets input and output field separator as tab
n = split(, a, "\x0b") # splits second field using Hex 0B (ASCII 11) i.e. vertical tab
for (i=1; i<=n; i++) ... # prints pair of field 1 with each item from split array a
这可能适合您 (GNU sed):
sed -r 's/^((\S*\t)\S*)\v/\n/;P;D' file
用换行符、第一个字段和制表符替换每个 \v
。打印并删除第一行并重复。
编辑:根据新问题;
sed -r '/\n/!s/"(")?//g;/\n/!s/;/\t/;s/^((\S*\t)[^\v]*)\v/\n/;/\t$/!P;D' file
删除任何单双引号(将双引号替换为单双引号)并将分号替换为制表符。然后用换行符、第一个字段和制表符替换任何 \v
,然后重复。
awk -F';' -v OFS='\t' #set Field separator is ';',
'{for(i=1;i<=NF;i++) #then we have 2 fields, remove leading and trailing doubled qoutes
gsub(/^"|"$/,"",$i)
c=split(,a,"\v") #split by vertical tab, save result in array 'a'
for(i=1;i<=c;i++) #for each element in a, if it is not empty, print field1 (the uuid)
if(a[i])print ,a[i]}' file #and the element, separated by Tab
解释是内联的。
它输出:
xxxxx 123 123 123
xxxxx 234 234
xxxxx 234 224 234
yyyyy 123 234 224
yyyyy 234 234
yyyyy 234 224 234
zzzzz 123 456 565
zzzzz 234 774
gnu sed
sed 's/"\|..$//g;s/;/\t/;:r;s/^\([^\t]*\)\t\(.*\)\v/\t\n\t/;t r;s/\t/\t/g;' YourFile
首先递归替换 \v "field" + 制表符 + 清除途中多余的字符
另一个解决方案使用 awk
awk '
BEGIN{FS="[\v;]"}
{
gsub("[\"]","");
for(i=2; i<=NF; ++i)
if($i) printf "%s\t%s\n", , $i;
}' file.mer
另一个解决方案使用 sed
sed -r 's/\v\n/\v/g; s/"//g;
:a; s/([^;]*);([^\v]*)\v/;\n;/g; ta;
s/;/\t/g;' file.mer | sed -r '/^[^\t]+\t$/d'
你明白了,
xxxxx 123 123 123 xxxxx 234 234 xxxxx 234 224 234 yyyyy 123 234 224 yyyyy 234 234 yyyyy 234 224 234 zzzzz 123 456 565 zzzzz 234 774
好吧,我故意等到 Kent 的回答被接受并获得赏金,因为问题是关于 awk/sed 的。因此,我的回答可能有点 off-topic,但无论如何,这是我的 Java 解决方案,我只是为了好玩才做的。
MER 输入文件生成器:
我认为生成一些具有随机值的示例输入文件会很好。每行包含
- 一个UUID,
- 0-9 组,由垂直制表符分隔,
- 在每组中,1-4 个字符串,由水平制表符分隔,
- 每个字符串由1-20个字符组成,其中双引号被其他双引号转义,即
""
.
我认为这足够多样化,可以获得一些好的测试数据。
package de.scrum_master.Whosebug;
import org.apache.commons.lang.RandomStringUtils;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Random;
import java.util.UUID;
public class RandomFileGenerator {
private static final int BUFFER_SIZE = 1024 * 1024;
private final static Random RANDOM = new Random();
private final static char VERTICAL_TAB = '\u000b';
private final static char[] LEGAL_CHARS =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzäöüÄÖÜß. -\""
.toCharArray();
public static void main(String[] args) throws IOException {
long startTime = System.currentTimeMillis();
// final long maxOutputSize = 9L * 1024 * 1024 * 1024;
// final String outputFile = "src/main/resources/sample-9gb.mer";
final long maxOutputSize = 1L * 1024 * 1024;
final String outputFile = "src/main/resources/sample-1mb.mer";
long totalOutputSize = 0;
long lineCount = 0;
String line;
try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(outputFile), BUFFER_SIZE))) {
while (totalOutputSize < maxOutputSize) {
line = generateLine();
writer.println(generateLine());
totalOutputSize += line.length() + 1;
lineCount++;
}
}
System.out.println(lineCount);
System.out.println(totalOutputSize);
System.out.println((System.currentTimeMillis() - startTime) / 1000.0);
}
private static String generateLine() {
StringBuilder buffer = new StringBuilder();
buffer
.append('"')
.append(UUID.randomUUID().toString())
.append("\";\"");
int numItems = RANDOM.nextInt(10);
for (int i = 0; i < numItems; i++) {
int numSubItems = 1 + RANDOM.nextInt(4);
for (int j = 0; j < numSubItems; j++) {
buffer.append(
RandomStringUtils.random(1 + RANDOM.nextInt(20), 0, LEGAL_CHARS.length, false, false, LEGAL_CHARS)
.replaceAll("\"", "\"\"")
);
if (j + 1 < numSubItems)
buffer.append('\t');
}
if (i + 1 < numItems) {
buffer.append(VERTICAL_TAB);
}
}
buffer.append('"');
return buffer.toString();
}
}
您可以看到创建所需文件大小的测试文件很容易,例如
- 1 MB:
maxOutputSize = 1L * 1024 * 1024
- 9 GB:
maxOutputSize = 9L * 1024 * 1024 * 1024
我主要使用较小的用于在开发过程中检查算法,而使用真正大的用于性能调整。
4 种不同变体的文件拆分器:
这里显示的变体使用不同的方法,但它们的共同点是它们通过 reader.lines()
从 BufferedReader
和 Java 流中读取。从流切换到简单的 for
循环使其变慢,顺便说一句。所有解决方案将结果写入PrintWriter
.
reader.lines().forEach()
然后正则匹配+拆分。此解决方案在可读性、简洁性和性能之间具有最佳 trade-off。reader.lines().flatMap()
,即UUID后的vertical-tab-separated组使用sub-streams,同样使用正则匹配+拆分。这个解决方案也非常简短和优雅,但比#1 更难阅读,而且速度也慢了大约 15%。因为像
replace()
和split()
这样的正则表达式匹配调用可能非常昂贵,所以我开发了一个解决方案,它迭代字符串并使用indexOf()
和substring()
而不是正则表达式。这比 #1 和 #2 快得多,但代码更难阅读,我开始不喜欢这种方式。只有在性能非常重要的情况下才应该这样做,即如果经常使用文件拆分器。对于 one-time 解决方案,或者如果它每月运行一次,我认为从可维护性的角度来看,这并不值得。#3 的进一步优化版本,它避免了更多的开销并且再次更快了一点,但速度并不快。现在代码确实需要源代码注释,以便向 reader 传达算法的作用。从干净代码的角度来看,这是一场噩梦。 (不要在家里这样做,孩子们!)
package de.scrum_master.Whosebug;
import java.io.*;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FileSplitter {
private static final int BUFFER_SIZE = 1024 * 1024;
private static final Pattern LINE_PATTERN = Pattern.compile("^\"([^\"]+)\";\"(.*)\"$");
private final static char VERTICAL_TAB = '\u000b';
public static void main(String[] args) throws IOException {
long startTime = System.currentTimeMillis();
String inputFile = "src/main/resources/sample-9gb.mer";
String outputFile = inputFile.replaceFirst("mer$", "txt");
try (
BufferedReader reader = new BufferedReader(new FileReader(inputFile), BUFFER_SIZE);
PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(outputFile), BUFFER_SIZE))
) {
// forEachVariant(reader, writer);
// flatMapVariant(reader, writer);
noRegexSimpleVariant(reader, writer);
// noRegexOptimisedVariant(reader, writer);
}
System.out.println((System.currentTimeMillis() - startTime) / 1000.0);
}
private static void forEachVariant(BufferedReader reader, PrintWriter writer) {
Matcher matcher = LINE_PATTERN.matcher("dummy");
reader.lines()
.forEach(line -> {
matcher.reset(line).matches();
for (String record : matcher.group(2).replace("\"\"", "\"").split("\v"))
writer.println(matcher.group(1) + "\t" + record);
});
}
private static void flatMapVariant(BufferedReader reader, PrintWriter writer) {
Matcher matcher = LINE_PATTERN.matcher("dummy");
reader.lines()
.flatMap(line -> {
matcher.reset(line).matches();
return Arrays
.stream(matcher.group(2).replace("\"\"", "\"").split("\v"))
.map(record -> matcher.group(1) + "\t" + record);
})
.forEach(writer::println);
}
private static void noRegexSimpleVariant(BufferedReader reader, PrintWriter writer) {
reader.lines()
.forEach(line -> {
final int lineLength = line.length();
// UUID + '\t'
int indexLeft = 1;
int indexRight = line.indexOf('"', indexLeft);
final String uuid = line.substring(indexLeft, indexRight) + "\t";
indexLeft = indexRight + 3;
String record;
int quoteIndex;
while (indexLeft < lineLength) {
writer.print(uuid);
indexRight = line.indexOf(VERTICAL_TAB, indexLeft);
if (indexRight == -1)
indexRight = lineLength - 1;
while (indexLeft < indexRight) {
quoteIndex = line.indexOf('"', indexLeft);
if (quoteIndex == -1 || quoteIndex >= indexRight)
quoteIndex = indexRight;
else
quoteIndex++;
record = line.substring(indexLeft, quoteIndex);
writer.print(record);
indexLeft = quoteIndex + 1;
}
writer.println();
indexLeft = indexRight + 1;
}
});
}
private static void noRegexOptimisedVariant(BufferedReader reader, PrintWriter writer) throws IOException {
reader.lines()
.forEach(line -> {
// UUID + '\t'
int indexLeft = 1;
int indexRight = line.indexOf('"', indexLeft);
final String uuid = line.substring(indexLeft, indexRight) + "\t";
// Skip '";"' after UUID
indexLeft = indexRight + 3;
final int lineLength = line.length();
String recordChunk;
int quoteIndex;
// If search for '"' has once reached end of line, search no more
boolean doQuoteSearch = true;
// Iterate over records per UUID, separated by vertical tab
while (indexLeft < lineLength) {
writer.print(uuid);
indexRight = line.indexOf(VERTICAL_TAB, indexLeft);
if (indexRight == -1)
indexRight = lineLength - 1;
// Search for '""' within record incrementally, + replace each of them by '"'.
// BTW, if '"' is found, it actually always will be an escaped '""'.
while (indexLeft < indexRight) {
if (doQuoteSearch) {
// Only search for quotes if we never reached the end of line before
quoteIndex = line.indexOf('"', indexLeft);
assert quoteIndex != -1;
if (quoteIndex >= lineLength - 1)
doQuoteSearch = false;
if (quoteIndex >= indexRight)
quoteIndex = indexRight;
else
quoteIndex++;
}
else {
// No more '"' within record
quoteIndex = indexRight;
}
// Write record chunk, skipping 2nd '"'
recordChunk = line.substring(indexLeft, quoteIndex);
writer.print(recordChunk);
indexLeft = quoteIndex + 1;
}
// Do not forget newline before reading next line/UUID
writer.println();
indexLeft = indexRight + 1;
}
});
}
}
更新的 awk 脚本:
此外:每个Java解决方案写出一个没有任何内容的UUID,以防输入文件中有none。这很容易避免,但我是故意的。这是与我用作基准的这个稍微更新的 awk 脚本(基于 Dave 的,但也用 "
替换 ""
)的唯一区别:
#!/usr/bin/awk
{
for(i=1;i<=NF;i++) {
gsub(/^"|"$/,"",$i)
gsub(/""/,"\"",$i)
}
c=split(,a,"\v")
for(i=1;i<=c;i++)
print ,a[i]
}
性能结果:
我测量了解析和写入性能。
- 解析意味着从磁盘读取一个 9 GB 的文件并将其拆分,但将输出写入 /dev/null 或根本不写入。
写入意味着读取相同的 9 GB 文件并将其写回到相同的磁盘分区(混合 HD + SSD 类型),即可以通过写入另一个物理磁盘来进一步优化。输出文件的大小为 18 GB。
读取文件,拆分成行但不解析行:66秒
Awk
- 仅解析:533 秒
- 解析+写入:683秒
reader.lines().forEach()
然后正则匹配+拆分- 仅解析:212 秒
- 解析+写入:425秒
reader.lines().flatMap()
,即使用 sub-streams- 仅解析:245 秒
- 解析+写入:未测
不使用正则表达式,但使用
String.replace("\"\"", "\"")
(此处代码中未显示)- 仅解析:154 秒
- 解析+写入:369秒
无正则表达式,无
replace()
,简单版本- 仅解析:86 秒
- 解析+写入:342秒
无正则,无
replace()
,优化版- 仅解析:84 秒
- 解析+写入:342秒
抱歉冗长的论文,但我想与阅读问题和其他答案的其他人分享我的发现,推测 Java(或 C?)是否可能比 awk 更快 - 是的,它是有相当大的一点,但不是一个数量级,因为磁盘性能也是一个因素。我认为这是对那些为了优化而倾向于 over-optimise 的人的警告。这是不值得的如果你走得太远,只需尝试在努力、可读性和性能之间找到最佳平衡点。阿们。