使用多行单元格拆分 CSV

使用多行单元格拆分 CSV

我正在处理 YouTube 生成的一些 CSV 文件(因此我无法更改源结构)。在 CSV 文件中,某些记录跨越多行。为了简洁起见,省略了许多其他列的假设示例如下:

video_id, upload_time, title, policy
oHg5SJYRHA0, 2007/05/15, "RickRoll'D", "Monetize in all countries except: CU, IR, KP, SD, SY
Track in countries: CU, IR, KP
Block in countries: SD, SY"
dQw4w9WgXcQ, 2009/10/24, "Rick Astley - Never Gonna Give You Up", "Monetize in all countries except: CU, IR, KP, SD, SY
Track in countries: CU, IR, KP, SD, SY"

一个典型的文件包含数十万条记录,甚至数百万条记录(一个文件大小为29.57GB),这太大了,无法一次性处理,所以我想将它们分成更小的块,以便在不同的机器上处理。我之前曾在其他报告文件中使用过splitwith -l,当单元格中没有换行符时,它的效果非常好。在这种情况下,如果分割发生在坏行上(例如:示例的第 4 行),那么我在两个文件中就有损坏的记录。除了解析 CSV 文件然后将其重建为多个文件之外,是否有一种有效的方法可以像这样分割 CSV?

答案1

您将需要解析 CSV 文件,以便按照您想要的方式以较小的块重新发出它。在此操作期间,也许您甚至想以不同的、更严格的、定义明确的格式重新发出它(例如,哦,我不知道,json)。

您的输入文件的格式非常不寻常。Python的csv模块,一方面,无法解析它,因为它有一个多字符分隔符:(,逗号空格)而不是更常见的,.否则,您可以使用 5 行 Python 代码轻松解析并重新发出该文件。

您必须找到另一个可以工作的解析器,或者编写一个小的解析器。首先,尝试找出您手头的格式的具体情况,例如引用规则是什么(例如,当用"contains引用的字段时会发生什么"。)

答案2

您可能必须解析它。下面是一个grep通过管道传输到三个sed命令的示例命令,它将把多行带引号的字符串组合到一行上(您可以split -l在末尾添加一个管道):

  grep -Eoz "((([^\",[:space:]]+|\"[!#-~[:space:]]+\"),? ?){4}[[:space:]]){1}" csvtest |  
  sed -e ':a' -e 'N' -e '$!ba' -e 's/\n\n/XXX new record XXX/g' |
  sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/ /g' |
  sed -e "s/XXX new record XXX/\n/g"  

分解一下:

  • grep-E选项允许扩展正则表达式。
  • grep选项-o仅输出匹配的项目
  • grep-z选项将换行符视为\0
  • [^\",[:space:]]+在模式中匹配未加引号的项目
  • \"[!#-~[:space:]]+\"在模式中匹配引用的项目
  • quoted items对于带引号的字符串包含引号"或非标准字符范围的任何特殊情况,您可能需要更新模式。只需在后面添加其他字符范围即可~
  • 第一条sed语句将两个换行符替换为XXX new record XXX。的输出grep在匹配之间生成两个换行符。
  • 第二条sed语句用空格替换每个剩余的单个换行符。
  • 最后的sed替换了之前添加XXX new record XXX回单个换行符的内容

您可以split -l在其末尾添加一个管道。

答案3

对于 CSV 解析,最好建议您使用实际的 CSV 解析器。使用 Perl 的最新版本文本::CSV模块中,您可以指定多字符字段分隔符

#!/usr/bin/env perl
use strict;
use warnings;
use Text::CSV;
use Data::Dump; # just for this demonstration

# the "binary" option allows newlines in field values
my $csv = Text::CSV->new({binary=>1, sep=>", "})
  or die Text::CSV->error_diag;

open my $fh, "<", "test.csv";

while (my $row = $csv->getline($fh)) {
    print "next row:\n";
    dd $row; # or do something more interesting
}

close $fh;

答案4

我通过使用解决了这个问题csvkit的流命令将 CSV 行转换为 JSON 对象(转义换行符),对split转换后的 JSON 流进行 -ing,然后将分割的 JSON 文件转换回 CSV。

带有可运行脚本的要点在这里:https://gist.github.com/vergenzt/d717bbad096dcf4be2151c66af47bf3a

总体结构:

FILE="..."
BASE="$(basename "$FILE" | cut -d. -f1)"

cat "$FILE" \
  | csvjson --stream --no-inference --snifflimit 0 \
  | gsplit -d --additional-suffix=.json -l $ROWS_PER_FILE -u - "${BASE}_"

for chunk_json in ${BASE}_*.json; do
  chunk_csv="$(basename "$chunk_json" .json).csv"
  in2csv -f ndjson --no-inference "$chunk_json" > "$chunk_csv"
  rm "$chunk_json"
done

根据csvkit 文档

  • csvjson--stream --no-inference --snifflimit 0如果“已设置且未--skip-lines设置”,则使用流式传输,并且
  • in2csv--format ndjson --no-inference如果“设置...”则使用流式传输

就规模而言,在我的机器(8GB RAM、1.8GHz 处理器)上,大约需要两分半钟的时间来分割大约 300MB 的 CSV 文件,其中包含大约 450 万行,但只有大约 18 万行(未加引号的换行符)。

相关内容