将数百万行重新格式化为 CSV 的最快方法

Fastest way to reformat millions of lines to CSV

我有一个包含数百万行的文本文件,应尽快将其导入 MySQL table。据我了解,LOAD DATA 最适合这个。

数据格式如下,括号中的每个大写字母都是一个字符串:

(A)(1-3 tabs)(B)
(3 tabs)(C)
(3 tabs)(D)
(3 tabs)(E)

(F)(1-3 tabs)(G)
(3 tabs)(H)
...

因此需要将数据重新格式化为 CSV,其中每个部分的第一个字符串必须在所有连续行中重复,直到下一个部分:

(A)(tab)(B)
(A)(tab)(C)
(A)(tab)(D)
(A)(tab)(E)
(F)(tab)(G)
(F)(tab)(H)
...

我正在考虑编写 C 程序,但是 Bash 可以同样快速(且简单)吗?这个问题可能是一个经典的问题,有一个非常有效和紧凑的解决方案吗?

试试这个小 awk 脚本

awk -F\t+ -v OFS=\t '==""{next}!=""{a=}{=a}1'

假定第二个字段中没有制表符。

一块一块地看:

-F\t+        Set the column separator to a sequence of one or more tabs
-v OFS=\t    Use a tab to separate columns on output
==""{next}  Skip this line if it just has one field.
!=""{a=}  Save the first field if it is specified
{=a}        Replace the first field with the saved one.
              The assignment forces the line to be recomputed using OFS
              to separate columns, so it's needed even if we just did a=.
1             awk idiom, equivalent to `{print}` (or `{print [=11=]}`).

这是 Perl 脚本的作业;这是给你的。经过简单测试,获取文件名列表作为命令行参数 and/or 从 stdin 读取,写入 stdout。假设选项卡的实际数量无关紧要,并且该行中只有一两个非空字段。 (它将抱怨并跳过任何不符合预期格式的行。)

#! /usr/bin/perl

our $left;
while (<>) {
    chomp;
    if (/^([^\t]+)\t+([^\t]+)$/) {
        $left = ;
        printf("%s\t%s\n", $left, );
    } elsif (/^\t+([^\t]+)$/) {
        if (defined $left) {
            printf("%s\t%s\n", $left, );
        } else {
            warn "$ARGV:$.: continuation line before leader line\n";
        }
    } else {
        warn "$ARGV:$.: line in unrecognized format\n";
    }
} continue {
    close ARGV if eof; # reset line numbering for each input file
}

可能 能够编写速度超过此速度的 C 程序,但这样做的工作量将超过其价值。 shell 脚本(bash 特定或其他)将数量级 慢。

为了完整性,这里有一个非常简单的“C”(实际上是 flex)解决方案,它可能更接近最快的解决方案。

文件:tsv.l

%option noinput nounput noyywrap nodefault
%x SECOND
%%
  char* saved = NULL;
\t+            BEGIN SECOND;
[^\t\n]+       free(saved); saved = malloc(yyleng + 1); strcpy(saved, yytext);
<*>\n          BEGIN INITIAL;
<SECOND>.*     printf("%s\t%s\n", saved, yytext); BEGIN INITIAL;

编译:

flex --batch -8 -CF -o tsv.c tsv.l
gcc -O3 -march=native -Wall -o tsv tsv.c -lfl
# On Mac OS  X, change -lfl to -ll

我测试了 and the perl script in 中的 awk 脚本,样本输入为 1,000,000 行非空行,其中包含 91,073 节,由空行分隔。该文件总共有 201,675,114 字节。 Ubuntu 14.04 系统上的计时:

  • 弹性:0.85 秒
  • awk:1.40 秒
  • perl:3.85 秒

在所有情况下,这是使用 time prog < test.text > /dev/null 报告的用户时间,取最少的五次运行并四舍五入到 0.05 秒的单位。

我修改了 perl 脚本以忽略空行,方法是在 if (length) { ... } 条件中将循环体包含在 chomp; 命令之后。它对执行时间的影响很小,但有必要避免忽略生成的警告。

我通常不在 flex 程序上使用“速度”标志,但在这种情况下它确实产生了显着的差异;没有它们,flex 程序花费了将近 2 秒,明显多于 awk 脚本。

我尝试了 C 实现。 3m 线约 1s。但是,sscanf 显然对空格漠不关心,我的 C 有点生疏了。如何在没有大量代码的情况下正确提取字符串?

if(line[0] == '\t') {
  // TODO: remove preceding `\t{3}` and trailing `\r`
  printf("%s\t%s\n", one, line);
}
else {
  // TODO: split at \t{1,3} and remove trailing `\r`
  sscanf(line, "%s\t%s", one, two);
  printf("%s\t%s\n", one, two);
}