我正在处理 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),这太大了,无法一次性处理,所以我想将它们分成更小的块,以便在不同的机器上处理。我之前曾在其他报告文件中使用过split
with -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 万行(未加引号的换行符)。