【问题标题】:Splitting text lines, whilst appending prefix分割文本行,同时附加前缀
【发布时间】:2017-05-24 19:56:37
【问题描述】:

已编辑

我有大约 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"

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 正则表达式,它至少可以工作,但需要多次运行和手动检查。

我将如何使用 awksed 执行此操作?我已尝试调整下面的当前答案,但我无法找出 ;P 和 ;D 后缀的含义。

(注意:我在 Windows 上使用 GitBash,所以我猜这是 gnu sedawk ?)

【问题讨论】:

  • 试试awk -F'\t|\v' -v OFS='\t' '{for(i=2; i<=NF; i++) print $1, $i}' file
  • 感谢肯特,工作就像一个魅力!
  • 提出一个新问题,而不是让现有的答案过时
  • Sundeep - 遗憾的是,现有的答案最初并没有奏效
  • wrt I've shown them as 3-digit numbers here, but in reality they're variable length strings, which can include doubled quotes "" - 你为什么要这样做?在发布示例输入时,重要的是要使其真正代表您的真实输入。让它看起来像是一个 3 位数的数字只是要求一个仅适用于 3 位数的解决方案。

标签: regex awk sed text-processing


【解决方案1】:
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($2,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 $1,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

【讨论】:

  • 如果我把它放在一行,我需要';'还是什么?
  • awk -F';' -v OFS='\t' '{for(i=1;i&lt;=NF;i++)gsub(/^"|"$/,"",$i);c=split($2,a,"\v");for(i=1;i&lt;=c;i++)if(a[i])print $1,a[i]}' file@Dycey
  • 我需要一点 Java 编程 kata 并找到了这个问题。我还没有写答案,因为 Java 是题外话,但如果您有兴趣,我根据您更新的规范创建了一个虚拟数据生成器并创建了一个 9 GB 的测试文件。然后我用这个非常 awk 的脚本解析它:533 s,我发现它对于解释型语言来说非常令人印象深刻。恭喜,肯特!但是 Dycey,以防万一性能是一个主要问题并且您感兴趣,我的 Java 程序只运行 212 秒,即快 2.5 倍(尚未以任何方式优化),两个程序都写入 /dev/null。如果你问我,我可以发布它。
  • @kriegaex Kents 当前的解决方案正在执行多个耗时的步骤,鉴于 OP 当前发布的输入,这些步骤是不必要的,因此它比等效的 java 脚本慢也就不足为奇了。如果/当 OP回答我在 cmets 在他的问题下提出的问题,然后我们可以开始着眼于帮助他为他的真实输入数据创建最佳解决方案。
  • 我认为 awk 脚本并没有做很多不必要的事情,但它也不能替换水平 + 垂直制表符分隔值列表中的"",这是我的解决方案所做的,所以我的解决方案是甚至更多。但公平地说,肯特在 OP 刚刚改变问题之前写下了他的答案。无论如何我都不会过度优化,我喜欢可读的代码,无论是 awk、sed 还是 Java 代码。 (好吧,对于 sed 来说,它更难实现,但无论如何。)
【解决方案2】:

这可能对你有用(GNU sed):

sed -r 's/^((\S*\t)\S*)\v/\1\n\2/;P;D' file

将每个\v 替换为换行符、第一个字段和一个制表符。打印并删除第一行并重复。

编辑:根据新问题;

sed -r '/\n/!s/"(")?/\1/g;/\n/!s/;/\t/;s/^((\S*\t)[^\v]*)\v/\1\n\2/;/\t$/!P;D' file

删除任何单双引号(将双双引号替换为单双引号)并将分号替换为制表符。然后将任何\v 替换为换行符、第一个字段和制表符并重复。

【讨论】:

  • 您更新的 sed 脚本完全删除了 "",而不是用 " 替换它们。这不是更新后的问题所描述的。
  • @kriegaex 谢谢,我没有考虑这方面,请参阅编辑
  • 还是不行。你甚至测试过它吗?它不断删除所有双引号。您可以使用我的 Java 测试数据生成器并在它生成的文件上进行尝试。
【解决方案3】:

您可以将此 awk 命令用于此输出:

awk 'BEGIN{FS=OFS="\t"} n = split($2, a, "\x0b") {
        for (i=1; i<=n; i++) print $1, 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($2, 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

【讨论】:

    【解决方案4】:

    gnu sed

     sed  's/"\|..$//g;s/;/\t/;:r;s/^\([^\t]*\)\t\(.*\)\\v/\1\t\2\n\1\t/;t r;s/\\t/\t/g;'  YourFile
    

    通过第一个“字段”+制表符+在途中清除多余的字符来递归替换\v

    【讨论】:

      【解决方案5】:

      使用awk的另一种解决方案

      awk '
          BEGIN{FS="[\v;]"}
         {
             gsub("[\"]",""); 
             for(i=2; i<=NF; ++i) 
                 if($i) printf "%s\t%s\n", $1, $i;
         }' file.mer
      

      使用sed的另一种解决方案

      sed -r 's/\v\n/\v/g; s/"//g;
          :a; s/([^;]*);([^\v]*)\v/\1;\2\n\1;/g; ta; 
          s/;/\t/g;' file.mer | sed -r '/^[^\t]+\t$/d'
      

      你明白了,

      xxxxxx 123 123 123 xxxxxx 234 234 xxxxxx 234 224 234 年年 123 234 224 年年 234 234 年年 234 224 234 zzzzz 123 456 565 zzzz 234 774

      【讨论】:

      • 您的 awk 和 sed 脚本都完全删除了 "",而不是用 " 替换它们。这不是更新后的问题所描述的。
      【解决方案6】:

      好的,我故意等到肯特的回答被接受并授予赏金,因为问题是关于 awk/sed。因此,我的回答可能有点离题,但无论如何,这是我的 Java 解决方案,我只是为了好玩而做的。

      MER 输入文件生成器:

      我认为生成一些具有随机值的示例输入文件会很好。每行由

      • 一个 UUID,
      • 0-9 组,由垂直制表符分隔,
      • 在每组中,1-4 个字符串,由水平制表符分隔,
      • 每个字符串由 1-20 个字符组成,其中双引号被其他双引号转义,即""

      我认为这足以获得一些好的测试数据。

      package de.scrum_master.stackoverflow;
      
      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() 从带有 Java 流的 BufferedReader 读取。从流切换到简单的for 循环使其变慢,顺便说一句。所有解决方案都将结果写入PrintWriter

      1. reader.lines().forEach() 然后正则表达式匹配+拆分。此解决方案在可读性、简洁性和性能之间取得了最佳平衡。

      2. reader.lines().flatMap(),即在 UUID 之后使用垂直制表符分隔组的子流,也使用正则表达式匹配 + 拆分。这个解决方案也非常简短和优雅,但比 #1 更难阅读,而且速度也慢了大约 15%。

      3. 因为像 replace()split() 这样的正则表达式匹配调用可能非常昂贵,所以我开发了一个解决方案,它宁可遍历字符串并使用 indexOf()substring() 而不是正则表达式。这比 #1 和 #2 快得多,但代码更难以我开始不喜欢的方式阅读。仅当性能非常重要时才应该这样做,即定期使用文件拆分器。对于一次性解决方案,或者如果它每月只运行一次,我认为从可维护性的角度来看它并不值得。

      4. #3 的进一步优化版本,它避免了一些更多的开销,并且再次更快一点,但不是实质性的。现在代码确实需要源代码 cmets 以便向读者传达算法的作用。从干净代码的角度来看,这是一场噩梦。 (孩子们,不要在家里这样做!)

      package de.scrum_master.stackoverflow;
      
      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,以防输入文件中没有任何内容。这很容易避免,但我是故意这样做的。这是与我用作基准的这个稍微更新的 awk 脚本(基于 Dave 的,但也将 "" 替换为 ")的唯一区别:

      #!/usr/bin/awk
      
      {
        for(i=1;i<=NF;i++) {
          gsub(/^"|"$/,"",$i)
          gsub(/""/,"\"",$i)
        }
        c=split($2,a,"\\v")
        for(i=1;i<=c;i++)
          print $1,a[i]
      }
      

      性能结果:

      我测量了解析和写入性能。

      • 解析意味着从磁盘读取一个 9 GB 的文件并将其拆分,但将输出写入 /dev/null 或根本不写入。
      • 写入意味着读取相同的 9 GB 文件并将其写回相同的磁盘分区(混合 HD + SSD 类型),即可以通过写入另一个物理磁盘来进一步优化。输出文件的大小为 18 GB。

        1. 读取文件,分行但不解析行:66 s

        2. Awk

          • 仅解析:533 秒
          • 解析 + 写入:683 秒
        3. reader.lines().forEach() 然后正则匹配+拆分

          • 仅解析:212 秒
          • 解析 + 写入:425 秒
        4. reader.lines().flatMap(),即使用子流

          • 仅解析:245 秒
          • 解析 + 写入:未测量
        5. 不使用正则表达式,但使用String.replace("\"\"", "\"")(此处代码中未显示)

          • 仅解析:154 秒
          • 解析 + 写入:369 秒
        6. 没有正则表达式,没有replace(),简单版

          • 仅解析:86 秒
          • 解析 + 写入:342 秒
        7. 没有正则表达式,没有replace(),优化版

          • 仅解析:84 秒
          • 解析 + 写入:342 秒

      对于冗长的论文感到抱歉,但我想与其他阅读该问题和其他答案的人分享我的发现,推测 Java(或 C?)是否可能比 awk 更快 - 是的,它是相当多的,但是不是一个数量级,因为磁盘性能也是一个因素。我认为这对那些为了优化而倾向于过度优化的人是一个警告。如果你走得太远,那是不值得的,只是试着在努力、可读性和性能之间找到最佳平衡点。阿门。

      【讨论】:

      • 感谢 kriegaex!但可悲的是,Java 在客户的商店中是不可行的。 :-(
      • 在这些情况下,即使它优于 sed/awk 脚本也不行?带有杀手论点的政治。不错的客户。 ;-) 感谢您的反馈。
      猜你喜欢
      • 2013-03-12
      • 1970-01-01
      • 2019-07-05
      • 1970-01-01
      • 2010-10-31
      • 2015-07-28
      • 1970-01-01
      • 2013-11-18
      • 2022-11-11
      相关资源
      最近更新 更多