如何拆分文件并保留每个部分的第一行?
Posted
技术标签:
【中文标题】如何拆分文件并保留每个部分的第一行?【英文标题】:How to split a file and keep the first line in each of the pieces? 【发布时间】:2010-11-27 13:22:24 【问题描述】:给定:一个大文本数据文件(例如 CSV 格式),第一行有一个“特殊”行(例如,字段名称)。
需要:相当于 coreutils split -l
命令,但额外要求原始文件的标题行出现在每个结果片段的开头。
我猜split
和head
的一些混合物会起作用吗?
【问题讨论】:
有人应该将其添加为split
的内置功能似乎是合理的,不是吗?
可能反对这成为内置的最大因素是您通常通过执行cat a b c > reconstructed
来重建拆分文件。文件中的多余行意味着正常的重建方法不会重现原始文件。
这就是即将推出的 (not) "unsplit --remove-header
" 实用程序的用途!但说真的,split
,如果它有一个“repeat-header”选项,仍应默认为其当前行为。如果你真的想要,你只会使用标题。
是的,我认为--keep-first N
是split
的一个不错的选择,它在行模式和字节模式下都很有用
我认为它是一个好主意——对于分割文件以进行分发而不是重建绝对非常有用.这是一个如此古老的 Unix 实用程序的“如此简单,怎么还不存在”功能之一,我怀疑“负责人”是否出于某种原因拒绝了以前的提议以执行此确切功能或其他。
【参考方案1】:
这个单行将大 csv 拆分为 999 条记录,保留每条顶部的标题行(因此 999 条记录 + 1 条标题 = 1000 行)
cat bigFile.csv | parallel --header : --pipe -N999 'cat >file_#.csv'
基于 Ole Tange 的回答。 (关于 Ole 的回答:你不能在 pipepart 中使用行数)
有关安装并行的一些提示,请参阅 cmets
【讨论】:
请注意,如果我们考虑每个文件中的标题行,那么在此解决方案中每个较小的文件将有 1000 行。 这就是我使用 999 的原因 :) 我必须在 macOS 上brew install parallel
。像魅力一样工作!
这太完美了。非常感谢!
与 MacOS 一样,Ubuntu 20.04 也需要安装 parallel
才能工作。请注意,Ubuntu 建议 sudo apt install moreutils
# 版本 0.63-1 或 sudo apt install parallel
# 版本 20161222-1.1 - 选择后一个建议。第一个建议,moreutils
听起来特别有用,但该软件包中包含的并行版本出错了 (parallel: invalid option -- '-'
)。第二个建议按预期工作 (details)。【参考方案2】:
一个简单但可能不那么优雅的方法:预先切断标题,拆分文件,然后用 cat 或任何正在读取它的文件重新加入每个文件的标题。 所以像:
-
head -n1 file.txt > header.txt
拆分-l file.txt
cat header.txt f1.txt
【讨论】:
【参考方案3】:您可以在 GNU coreutils split >= 8.13 (2011) 中使用新的 --filter 功能:
tail -n +2 FILE.in | split -l 50 - --filter='sh -c " head -n1 FILE.in; cat; > $FILE"'
【讨论】:
我喜欢单行版本。只是为了使它对 bash 更通用,我这样做了:tail -n +2 FILE.in | split -d --lines 50 - --filter='bash -c " head -n1 $FILE%.*; cat; > $FILE"' FILE.in.x
【参考方案4】:
下面是一个 4 行,可用于将 bigfile.csv 拆分为多个较小的文件,并保留 csv 标头。仅使用应该在大多数 *nix 系统上工作的内置 Bash 命令(head、split、find、grep、xargs 和 sed)。如果您安装了 mingw-64 / git-bash,也应该可以在 Windows 上运行。
csvheader=`head -1 bigfile.csv` 拆分 -d -l10000 bigfile.csv smallfile_ 查找 .|grep 小文件_ | xargs sed -i "1s/^/$csvheader\n/" sed -i '1d' smallfile_00逐行解释:
-
将标头捕获到名为 csvheader 的变量中
将 bigfile.csv 拆分为多个带有前缀 smallfile_ 的小文件
查找所有小文件并使用 xargs 和 sed -i 将 csvheader 插入到第一行。请注意,您需要在“双引号”中使用 sed 才能使用变量。
第一个名为 smallfile_00 的文件现在将在第 1 行和第 2 行具有冗余标题(来自原始数据以及第 3 步中插入的 sed 标题)。我们可以使用 sed -i '1d' 命令删除多余的标头。
【讨论】:
【参考方案5】:灵感来自 @Arkady 对单线的评论。
MYFILE 变量只是为了减少样板文件split
不显示文件名,但 --additional-suffix
选项让我们可以轻松控制预期的内容
通过rm $part
删除中间文件(假设没有具有相同后缀的文件)
MYFILE=mycsv.csv && for part in $(split -n4 --additional-suffix=foo $MYFILE; ls *foo); do cat <(head -n1 $MYFILE) $part > $MYFILE.$part; rm $part; done
证据:
-rw-rw-r-- 1 ec2-user ec2-user 32040108 Jun 1 23:18 mycsv.csv.xaafoo
-rw-rw-r-- 1 ec2-user ec2-user 32040108 Jun 1 23:18 mycsv.csv.xabfoo
-rw-rw-r-- 1 ec2-user ec2-user 32040108 Jun 1 23:18 mycsv.csv.xacfoo
-rw-rw-r-- 1 ec2-user ec2-user 32040110 Jun 1 23:18 mycsv.csv.xadfoo
当然还有head -2 *foo
来查看添加的标题。
【讨论】:
【参考方案6】:这是 robhruska 的脚本清理了一下:
tail -n +2 file.txt | split -l 4 - split_
for file in split_*
do
head -n 1 file.txt > tmp_file
cat "$file" >> tmp_file
mv -f tmp_file "$file"
done
我在不需要的地方删除了wc
、cut
、ls
和echo
。我更改了一些文件名以使它们更有意义。我把它分成多行只是为了更容易阅读。
如果你想变得花哨,你可以使用mktemp
或tempfile
来创建一个临时文件名,而不是使用硬编码。
编辑
使用 GNU split
可以做到这一点:
split_filter () head -n 1 file.txt; cat; > "$FILE"; ; export -f split_filter; tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_
为了可读性而分开:
split_filter () head -n 1 file.txt; cat; > "$FILE";
export -f split_filter
tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_
当指定--filter
时,split
为每个输出文件运行命令(在这种情况下为函数,必须导出)并将命令环境中的变量FILE
设置为文件名。
过滤器脚本或函数可以对输出内容甚至文件名进行任何它想要的操作。后者的一个示例可能是输出到变量目录中的固定文件名:例如> "$FILE/data.dat"
。
【讨论】:
这肯定行得通。我只是希望有一些像for $part in (split -l 1000 myfile); cat <(head -n1 myfile) $part > myfile.$part; done
这行不通,因为split
必然不会在stdout
上输出。
split
可以将文件的名称输出到标准输出,不过(只要我们在讨论split
应该做:-)
你是对的。这可能是方便。抱歉,我误读了您的单行字。
@JohnathanElmore:请注意,GNU 实用程序可用于 OS X。例如,使用 Homebrew。【参考方案7】:
使用 GNU 并行:
parallel -a bigfile.csv --header : --pipepart 'cat > #'
如果您需要在每个部分上运行命令,GNU Parallel 也可以帮助您:
parallel -a bigfile.csv --header : --pipepart my_program_reading_from_stdin
parallel -a bigfile.csv --header : --pipepart --fifo my_program_reading_from_fifo
parallel -a bigfile.csv --header : --pipepart --cat my_program_reading_from_a_file
如果您想将每个 CPU 核心分成 2 个部分(例如 24 个核心 = 48 个相同大小的部分):
parallel --block -2 -a bigfile.csv --header : --pipepart my_program_reading_from_stdin
如果你想分成 10 MB 的块:
parallel --block 10M -a bigfile.csv --header : --pipepart my_program_reading_from_stdin
【讨论】:
"--block 10M" - 节省一天!!【参考方案8】:我真的很喜欢 Rob 和 Dennis 的版本,以至于我想改进它们。
这是我的版本:
in_file=$1
awk 'if (NR!=1) print' $in_file | split -d -a 5 -l 100000 - $in_file"_" # Get all lines except the first, split into 100,000 line chunks
for file in $in_file"_"*
do
tmp_file=$(mktemp $in_file.XXXXXX) # Create a safer temp file
head -n 1 $in_file | cat - $file > $tmp_file # Get header from main file, cat that header with split file contents to temp file
mv -f $tmp_file $file # Overwrite non-header containing file with header-containing file
done
区别:
-
in_file 是要拆分维护标头的文件参数
使用
awk
而不是tail
,因为awk
具有更好的性能
分成 100,000 行文件而不是 4 个
拆分文件名将是输入文件名后加下划线和数字(最多 99999 - 来自“-d -a 5”拆分参数)
使用 mktemp 安全处理临时文件
使用单行 head | cat
而不是两行
【讨论】:
建议:将 awk 脚本更改为简单的:'NR > 1',因为 print 是默认操作。 也就是说,在这种情况下,我怀疑 awk 是否比 tail 快(或至少快得多)。 我也可以在循环之前将标头放在变量中,然后在循环中'echo "$header | ...."【参考方案9】:我喜欢 marco 的 awk 版本,从这个简化的单行中采用,您可以轻松地根据需要指定拆分分数:
awk 'NR==1print $0 > FILENAME ".split1"; print $0 > FILENAME ".split2"; NR>1if (NR % 10 > 5) print $0 >> FILENAME ".split1"; else print $0 >> FILENAME ".split2"' file
【讨论】:
我喜欢这个解决方案,但它仅限于两个拆分文件 如果你喜欢它,它有支持它的功能;)它可以很容易地调整到更多文件,但是它不像 split -l 那样灵活 “一个班轮” ...pshh【参考方案10】:你可以使用 [mg]awk:
awk 'NR==1
header=$0;
count=1;
print header > "x_" count;
next
!( (NR-1) % 100)
count++;
print header > "x_" count;
print $0 > "x_" count
' file
100 是每个切片的行数。 它不需要临时文件,可以放在一行中。
【讨论】:
【参考方案11】:这是 Denis Williamson 脚本的更强大的版本。该脚本创建了很多临时文件,如果运行不完整,如果它们被遗弃,那将是一种耻辱。所以,让我们添加信号捕获(见http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html 然后http://tldp.org/LDP/abs/html/debugging.html)并删除我们的临时文件;无论如何,这是一个最佳实践。
trap 'rm split_* tmp_file ; exit 13' SIGINT SIGTERM SIGQUIT
tail -n +2 file.txt | split -l 4 - split_
for file in split_*
do
head -n 1 file.txt > tmp_file
cat $file >> tmp_file
mv -f tmp_file $file
done
用您想要的任何返回码替换“13”。哦,你可能还是应该使用 mktemp(正如一些人已经建议的那样),所以继续并从陷阱行中的 rm 中删除“tmp_file”。有关要捕获的更多信号,请参见信号手册页。
【讨论】:
【参考方案12】:我是 Bash-fu 的新手,但我能够编造出这个双命令怪物。我敢肯定还有更优雅的解决方案。
$> tail -n +2 file.txt | split -l 4
$> for file in `ls xa*`; do echo "`head -1 file.txt`" > tmp; cat $file >> tmp; mv -f tmp $file; done
这是假设您的输入文件是file.txt
,您没有使用split
的prefix
参数,并且您在一个没有任何其他以@987654325 开头的文件的目录中工作@ 的默认 xa*
输出格式。此外,将“4”替换为您想要的分割线大小。
【讨论】:
以上是关于如何拆分文件并保留每个部分的第一行?的主要内容,如果未能解决你的问题,请参考以下文章