使用 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 转换为单行的主要内容,如果未能解决你的问题,请参考以下文章
如何将单个工作表中的多行(在 excel 中)转换为多个 CSV 文件
使用 Pandas DataFrame 将列表等列值转换为多行