使用 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 转换为单行的主要内容,如果未能解决你的问题,请参考以下文章

如何将 CSV 文件转换为多行 JSON?

如何将单个工作表中的多行(在 excel 中)转换为多个 CSV 文件

使用 Pandas DataFrame 将列表等列值转换为多行

使用 LINQ 将多行连接成单行(CSV 属性)

有没有办法使用 ReadFromText 转换(Python)在 Apache Beam 中读取多行 csv 文件?

linux命令--xargs的使用