使用 Linux 工具将多行 csv 转换为单行

Posted

技术标签:

【中文标题】使用 Linux 工具将多行 csv 转换为单行【英文标题】:Convert multi-line csv to single line using Linux tools 【发布时间】:2019-12-13 12:15:00 【问题描述】:

我有一个包含双引号多行字段的 .csv 文件。我需要将多行单元格转换为单行。它没有显示在示例数据中,但我不知道哪些字段可能是多行的,因此任何解决方案都需要检查每个字段。我知道我会有多少列。第一行也需要跳过。我不知道有多少数据,所以性能不是考虑因素。

我需要一些可以在 Linux 上通过 bash 脚本运行的东西。最好使用 awk 或 sed 等工具,而不是实际的编程语言。

数据将使用 Logstash 进一步处理,但它不处理双引号多行字段,因此需要进行一些预处理。

我尝试了类似的方法,它在一行上有效,但在多行上失败。

sed -e :0 -e '/,.*,.*,.*,.*,/b' -e N -e '1n;N;N;N;s/\n/ /g' -e b0 file.csv

CSV 示例

First name,Last name,Address,ZIP
John,Doe,"Country

City
Street",12345

我想要的输出是

First name,Last name,Address,ZIP
John,Doe,Country City Street,12345
Jane,Doe,Country City Street,67890
etc.
etc.

【问题讨论】:

如果没有合适的 CSV 解析器,我认为这是不可能的。您需要计算引号,忽略转义的引号,并在奇数个引号之后删除换行符。这不是正则表达式类型的任务。 以下是关于使用 awk 解析 CSV 的两个不同但相关的问题 - 请注意,您的案例是这些问题中讨论的最难的一个:***.com/questions/45420535/…***.com/questions/4205431/… 如果您可以在需要的地方保存 awk 脚本为此,我认为第一个链接可能对您特别有帮助 - 搜索“用空格替换换行符”以找到相关部分。 感谢您的链接。我尝试了一些样本,但它们似乎没有做任何事情。鉴于这似乎是一个相当复杂的问题,我认为最好告诉提供 .csv 的人更改格式。不幸的是,我不能花很多时间在这上面。我希望可能存在更多“即插即用”的东西。 查看“grep” a csv file including multi-lines fields? 以获得使用awk 的建议。 【参考方案1】:

如果Perl 是您的选择,请尝试以下操作:

perl -e '
while (<>) 
    $str .= $_;


while ($str =~ /("(("")|[^"])*")|((^|(?<=,))[^,]*((?=,)|$))/g) 
    if (($el = $&) =~ /^".*"$/s) 
        $el =~ s/^"//s; $el =~ s/"$//s;
        $el =~ s/""/"/g;
        $el =~ s/\s+(?!$)/ /g;
    
    push(@ary, $el);


foreach (@ary) 
    print /\n$/ ? "$_" : "$_,";
' sample.csv

sample.csv:

First name,Last name,Address,ZIP
John,Doe,"Country

City
Street",12345
John,Doe,"Country

City
Street",67890

结果:

First name,Last name,Address,ZIP
John,Doe,Country City Street,12345
John,Doe,Country City Street,67890

【讨论】:

感谢脚本。不幸的是,它不能 100% 处理实际数据。如果他们希望进一步处理数据,我会告诉我获得数据的人更改格式。我试图通过使用 cut 来摆脱有问题的列,但由于多行,这也不起作用。【参考方案2】:

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

sed ':a;s/[^,]\+/&/4;tb;N;ba;:b;s/\n\+/ /g;s/"//g' file

测试每一行以查看它是否包含正确数量的字段(在示例中为 4)。如果没有足够的字段,请追加下一行并重复测试。否则,用空格替换换行符,最后删除"'s。

注意这可能会充满诸如,'s 在"'s 和引用"'s 之间的问题。

【讨论】:

【参考方案3】:

试试cat -v file.csv。使用 Excel 创建文件时,您可能会遇到一些运气:当字段中的换行符是简单的 \n 并且末尾的换行符是 \r\n(看起来像 ^M)时,解析很简单。

# delete all newlines and replace the ^M with a new newline.
        tr -d "\n" < file.csv| tr "\r" "\n"

# Above two steps with one command
        tr "\n\r" " \n" < file.csv

当你想要连接线之间有一个空格时,你需要一个额外的步骤。

tr "\n\r" " \n" < file.csv | sed '2,$ s/^ //'

编辑:@sjaak 评论说这不起作用是他的情况。

如果你的虚线也有^M,你仍然可以是一个幸运的(wo-)人。 当您的损坏字段始终是双引号中的第一个字段并且您拥有 GNU sed 4.2.2 时,当第一行正好有一个双引号时,您可以连接 2 行。

 sed -rz ':a;s/(\n|^)([^"]*)"([^"]*)\n/\1\2"\3 /;ta' file.csv

说明:-z不要使用\n作为行尾:a标签用于替换成功后重复该步骤(\n|^)在a后搜索换行符或第一行([^"]*)没有"的子字符串ta返回标签a并重复

【讨论】:

运行 cat -v 确实显示 ^M。问题是它还在每行的末尾显示 ^M 所以当我运行这些命令时,我最终会得到一行,但我只需要将多行单元格转换为一行。 @sjaak 当第二次尝试也失败时,您应该寻找另一个解决方案(cmets 中给出的其他答案或链接)。当这可行时,您可能希望对将转换为 2 个空格的空行执行某些操作(首先使用 grep . | 删除它们,或者在末尾替换为 s/ / /g)。【参考方案4】:

awk 模式匹配 正在工作。 一句话回答:

  awk '/,"/ORS=" ";/",/ORS="\n"print $0' YourFile

如果你想去掉引号,你可以使用:

  awk '/,"/ORS=" ";/",/ORS="\n"print $0' YourFile | sed 's/"//gw NewFile'

但我更喜欢保留它。

解释代码:

    /模式/:在当前行中查找模式。

    ORS:表示输出行记录。

    $0 : 表示整个当前行。

    's/OldPattern/NewPattern/': 用 NewPattern 替换第一个 OldPattern

    /g : 对所有 OldPattern 执行上一个操作

    /w : 将结果写入新文件

【讨论】:

【参考方案5】:

首先我很抱歉迟到了 7 个月...

我今天遇到了一个和你类似的问题,有多个字段和多行类型。我很高兴找到你的问题,但至少就我而言,我的复杂性在于,由于不止一个字段存在冲突,引号可能会在同一行打开、关闭并再次打开......无论如何,阅读大量并结合答案从不同的帖子我想出了这样的事情:

首先我计算一行中的引号,为此,我取出除引号之外的所有内容,然后使用 wc:

quotes=`echo $line | tr -cd '"' | wc -c` # Counts the quotes

如果您想到一个多行字段,知道引号是 1 还是 2 就足够了。在像我这样的更通用的场景中,我必须知道引号的数量是奇数还是偶数,才能知道该行是否完成了记录或需要更多信息。

要检查偶数或奇数,通常可以使用 mod 操作数 (%):

even % 2 = 0
odd % 2 = 1

第一行:

Odd 表示该行希望下一行有更多信息。 偶数表示该行是完整的。

对于后续行,我必须知道上一行的状态。例如在您的示例文本中:

First name,Last name,Address,ZIP
John,Doe,"Country

City
Street",12345

您可以说第 1 行 (John,Doe,"Country) 有 1 个引号(奇数),这意味着记录的状态不完整或打开。

当您转到第 2 行时,没有引号(偶数)。然而,这并不意味着记录是完整的,您必须考虑以前的状态......所以对于第一个之后的行,它将是:

Odd 表示记录状态切换(未完成到完成)。 Even 表示记录状态保持为上一行。

我所做的是逐行循环,同时将最后一行的状态带到下一行:

incomplete=0
cat file.csv | while read line; do
    quotes=`echo $line | tr -cd '"' | wc -c` # Counts the quotes
    incomplete=$((($quotes+$incomplete)%2))  # Check if Odd or Even to decide status
    if [ $incomplete -eq 1 ]; then
        echo -n "$line " >> new.csv          # If line is incomplete join with next
    else
        echo "$line" >> new.csv              # If line completes the record finish
    fi
done

执行此操作后,您格式的文件会生成一个 new.csv,如下所示:

First name,Last name,Address,ZIP
John,Doe,"Country  City Street",12345

我和每个人一样都喜欢单行,我写这个脚本只是为了清楚起见,你可以 - 可以说 - 把它写成一行:

i=0;cat file.csv|while read l;do i=$((($(echo $l|tr -cd '"'|wc -c)+$i)%2));[[ $i = 1 ]] && echo -n "$l " || echo "$l";done >new.csv

如果您能回到您的示例并查看这是否适用于您的情况(您很可能已经解决),我将不胜感激。希望这仍然可以帮助其他人……

恢复多行字段

每个需求都不同,在我的情况下,我希望将记录放在一行中以进一步处理 csv 以添加一些 bash 提取的数据,但我想保持 csv 原样。为了做到这一点,我没有用空格连接行,而是使用了一个代码 - 可能是唯一的 - 然后我可以搜索和替换:

i=0;cat file.csv|while read l;do i=$((($(echo $l|tr -cd '"'|wc -c)+$i)%2));[[ $i = 1 ]] && echo -n "$l ~newline~ " || echo "$l";done >new.csv

代码是~newline~,当然这完全是任意的。

然后,在完成我的处理之后,我获取了 csv 文本文件并将编码的换行符替换为真正的换行符:

sed -i 's/ ~newline~ /\n/g' new.csv

参考资料:

三元运算符:https://***.com/a/3953666/6316852 计数字符出现次数:https://***.com/a/41119233/6316852 其他特殊情况:https://www.linuxquestions.org/questions/programming-9/complex-bash-string-substitution-of-csv-file-with-multiline-data-937179/

TL;DR

运行这个:

i=0;cat file.csv|while read l;do i=$((($(echo $l|tr -cd '"'|wc -c)+$i)%2));[[ $i = 1 ]] && echo -n "$l " || echo "$l";done >new.csv

...并在 new.csv 中收集结果

希望对你有帮助!

【讨论】:

以上是关于使用 Linux 工具将多行 csv 转换为单行的主要内容,如果未能解决你的问题,请参考以下文章