Bash 大文件的条件拆分
我有一个非常大的文件(>5亿行),我想根据其中一列的前3个字符将其拆分为几个较小的文件 看起来是这样的,其中第1列和第2列的每个元素都是唯一的:Bash 大文件的条件拆分,bash,split,Bash,Split,我有一个非常大的文件(>5亿行),我想根据其中一列的前3个字符将其拆分为几个较小的文件 看起来是这样的,其中第1列和第2列的每个元素都是唯一的: A0A023GPI8 A0A023GPI8.1 232300 1027923628 A0A023GPJ0 A0A023GPJ0.2 716541 765680613 A0A023PXA5 A0A023PXA5.1 559292 728048729 A0A023PXB0 A0A023PXB0.1 559292 72
A0A023GPI8 A0A023GPI8.1 232300 1027923628
A0A023GPJ0 A0A023GPJ0.2 716541 765680613
A0A023PXA5 A0A023PXA5.1 559292 728048729
A0A023PXB0 A0A023PXB0.1 559292 728048786
A0A023PXB5 A0A023PXB5.1 559292 728048524
A0A023PXB9 A0A023PXB9.1 559292 728048769
A0A023PXC2 A0A023PXC2.1 559292 728050382
我使用了下面的脚本,认为它会非常快,因为在我看来,它涉及到对整个文件的一次读取。然而,它已经运行了好几天,还远远没有完成。有什么想法可以解释原因,并提出解决方案吗
while read line
do
PREFIX=$(echo "$line" | cut -f2 | cut -c1-3)
echo -e "$line" >> ../split_DB/$PREFIX.part
done < $file
读取行时
做
前缀=$(回显“$行”|切割-f2 |切割-c1-3)
echo-e“$line”>>../split_DB/$PREFIX.part
完成<$file
阅读
效率不高;它必须一次读取一个字符,以避免读取超过下一个换行符。然而,这里的一大开销来源是在每行上调用两次cut
。我们可以通过再次使用read
进行拆分,并使用参数展开提取第二列的第一个字符来避免这种情况
while read -r line; do
read -r _ col2 _ <<< "$line"
prefix=${col2:0:3}
# If the first column has a fixed width, you can forgo the
# previous two lines and use
# prefix=${line:12:3}
printf '%s\n' "$line" >> ../split_DB/$prefix.part
done < "$file"
这可能与以下内容一样简单:
$ awk '{s=substr($2,1,3); print >> s}' file
>
将打印重定向到按给定名称追加文件。名称由第二列的前3个字母组成
while read -r line; do
read -r _ col2 _ <<< "$line"
prefix=${col2:0:3}
# If the first column has a fixed width, you can forgo the
# previous two lines and use
# prefix=${line:12:3}
printf '%s\n' "$line" >> ../split_DB/$prefix.part
done < "$file"
这将比Bash处理这个文件快得多
注: 通常操作系统对同时打开的文件数量有限制。这可能是一个问题,具体取决于第二列前3个字符中可能的字符组合数量。这将影响任何解决方案,其中这些名称的文件在处理给定文件时保持打开状态,而不仅仅是awk 如果您有
000
到999
,则可能打开999个文件;如果你有AAA
到ZZZ
那是17575;如果您有三个大写和小写字母数字,即238327个可能打开的文件。。。如果您的数据只有几个唯一的前缀,您可能不需要担心这一点;如果您陈述了数据的细节,这里建议的解决方案可能会有所不同
(您可以根据3个字符中允许的字母长度,通过将'ZZZ'
转换为十进制来计算可能的组合。('0'..'9','a'..'Z')
是32基('0'..'9','a'..'Z','a'..'Z')
是62基,依此类推。)
如果需要(在合理范围内),您可以提高大多数Unix风格OSs的限制,或者根据需要打开和关闭新文件。将文件限制提高到238327是不切实际的。您还可以对数据进行排序,并在上一个文件停止使用时关闭它。为什么shell脚本速度慢
速度慢的原因是,对于5亿行中的每一行,您都在强制shell创建3个进程,因此您的内核正在努力生成15亿个进程。假设它每秒可以处理10000个进程;你仍然在看15万秒,也就是2天。每秒10k进程速度快;可能比你得到的要好十倍或更多。在我2016年运行macOS High Sierra 10.13.1的15英寸MacBook Pro上,我使用了2.7 GHz Intel Core i7、16 GB 2133 MHz LPDDR3和500 GB闪存(大约150 GB免费),每秒大约有700个进程,因此该脚本名义上需要大约25天才能运行5亿条记录
加速的方法
有一些方法可以加快代码的速度。您可以使用纯shell、Awk、Python或Perl。请注意,如果使用Awk,它需要是GNU Awk,或者至少不是BSD(macOS)Awk—BSD版本只是认为它没有足够的文件描述符
我使用随机数据生成器创建了一个包含100000个随机条目的文件,这些条目与问题中的条目有些类似:
E1E583ZUT9 E1E583ZUT9.9 422255 490991884
Z0L339XJB5 Z0L339XJB5.0 852089 601069716
B3U993YMV8 B3U993YMV8.7 257653 443396409
L2F129EXJ4 L2F129EXJ4.8 942989 834728260
R4G123QWR2 R4G123QWR2.6 552467 744905170
K4Z576RKP0 K4Z576RKP0.9 947374 962234282
Z4R862HWX1 Z4R862HWX1.4 909520 2474569
L5D027SCJ5 L5D027SCJ5.4 199652 773936243
R5R272YFB5 R5R272YFB5.4 329247 582852318
G1I128BMI2 G1I128BMI2.6 359124 404495594
(使用的命令是一个即将进行重写的自制生成器。)前两列在模式X#X###XXX#
(X
表示字母,
表示数字)中具有相同的10个前导字符;唯一的区别在于后缀。
。这在脚本中没有被利用;这一点都不重要。也不能保证第二列中的值是唯一的,也不能保证如果出现.2
项,键就会出现.1
项,等等。这些细节对per来说基本上是无关紧要的性能测量。由于文件名使用字母-数字-字母前缀,26*10*26=6760个可能的文件前缀。对于100000个随机生成的记录,这些前缀中的每一个都存在
我编写了一个脚本来计时处理数据的各种方式。有4个shell脚本变体——一个是由发布者,OP;两个是由发布者(一个作为注释),还有一个是我创建的。还有由创建的Awk脚本,chepner发布的Python3脚本的稍微修改版本,以及我编写的Perl脚本
结果
此表总结了结果(以秒为单位的运行时间或挂钟时间):
原始的shell脚本比Perl慢2.5个数量级;当有足够的可用文件描述符时,Python和Awk的性能几乎相同(如果没有足够的可用文件描述符,Python就会停止;Perl也会停止)。shell脚本的速度大约是Python或Awk的一半
7000表示需要打开的文件数(ulimit-n7000
)。这是因为在生成的数据中有26*10*26=6760个不同的3字符起始代码。如果您有更多的模式,您将需要更多的打开文件描述符以获得保持它们全部打开的好处,或者您将需要编写一个文件描述符缓存算法,有点像GNU Awk必须使用的算法,并使用conseq大量性能损失。请注意,如果数据是按排序顺序显示的,因此每个文件的所有条目都是按顺序显示的,那么您可以调整算法,以便
╔═════════════════╦════╦═════════╦═════════╦═════════╦═════════╗
║ Script Variant ║ N ║ Mean ║ Std Dev ║ Min ║ Max ║
╠═════════════════╬════╬═════════╬═════════╬═════════╬═════════╣
║ Lucas A Shell ║ 11 ║ 426.425 ║ 16.076 ║ 408.044 ║ 456.926 ║
║ Chepner 1 Shell ║ 11 ║ 39.582 ║ 2.002 ║ 37.404 ║ 43.609 ║
║ Awk 256 ║ 11 ║ 38.916 ║ 2.925 ║ 30.874 ║ 41.737 ║
║ Chepner 2 Shell ║ 11 ║ 16.033 ║ 1.294 ║ 14.685 ║ 17.981 ║
║ Leffler Shell ║ 11 ║ 15.683 ║ 0.809 ║ 14.375 ║ 16.561 ║
║ Python 7000 ║ 11 ║ 7.052 ║ 0.344 ║ 6.358 ║ 7.771 ║
║ Awk 7000 ║ 11 ║ 6.403 ║ 0.384 ║ 5.498 ║ 6.891 ║
║ Perl 7000 ║ 11 ║ 1.138 ║ 0.037 ║ 1.073 ║ 1.204 ║
╚═════════════════╩════╩═════════╩═════════╩═════════╩═════════╝
cat "$@" |
while read line
do
PREFIX=$(echo "$line" | cut -f2 | cut -c1-3)
echo -e "$line" >> split_DB/$PREFIX.part
done
cat "${@}" |
while read -r line; do
read -r _ col2 _ <<< "$line"
prefix=${col2:0:3}
printf '%s\n' "$line" >> split_DB/$prefix.part
done
cat "${@}" |
while read -r line; do
prefix=${line:12:3}
printf '%s\n' "$line" >> split_DB/$prefix.part
done
sed 's/^[^ ]* \(...\)/\1 &/' "$@" |
while read key line
do
echo "$line" >> split_DB/$key.part
done
exec ${AWK:-awk} '{s=substr($2,1,3); print >> "split_DB/" s ".part"}' "$@"
import fileinput
output_files = {}
#with open(file) as fh:
# for line in fh:
for line in fileinput.input():
cols = line.strip().split()
prefix = cols[1][0:3]
# Cache the output file handles, so that each is opened only once.
#outfh = output_files.setdefault(prefix, open("../split_DB/{}.part".format(prefix), "w"))
outfh = output_files.setdefault(prefix, open("split_DB/{}.part".format(prefix), "w"))
print(line, file=outfh)
# Close all the output files
for f in output_files.values():
f.close()
#!/usr/bin/env perl
use strict;
use warnings;
my %fh;
while (<>)
{
my @fields = split;
my $pfx = substr($fields[1], 0, 3);
open $fh{$pfx}, '>>', "split_DB/${pfx}.part" or die
unless defined $fh{$pfx};
my $fh = $fh{$pfx};
print $fh $_;
}
foreach my $h (keys %fh)
{
close $fh{$h};
}
#!/bin/bash
#
# Test suite for SO 4747-6170
set_num_files()
{
nfiles=${1:-256}
if [ "$(ulimit -n)" -ne "$nfiles" ]
then if ulimit -S -n "$nfiles"
then : OK
else echo "Failed to set num files to $nfiles" >&2
ulimit -HSa >&2
exit 1
fi
fi
}
test_python_7000()
{
set_num_files 7000
timecmd -smr python3 pyscript.py "$@"
}
test_perl_7000()
{
set_num_files 7000
timecmd -smr perl jlscript.pl "$@"
}
test_awk_7000()
{
set_num_files 7000
AWK=/opt/gnu/bin/awk timecmd -smr sh awkscript.sh "$@"
}
test_awk_256()
{
set_num_files 256 # Default setting on macOS 10.13.1 High Sierra
AWK=/opt/gnu/bin/awk timecmd -smr sh awkscript-256.sh "$@"
}
test_op_shell()
{
timecmd -smr sh opscript.sh "$@"
}
test_jl_shell()
{
timecmd -smr sh jlscript.sh "$@"
}
test_chepner_1_shell()
{
timecmd -smr bash chepner-1.sh "$@"
}
test_chepner_2_shell()
{
timecmd -smr bash chepner-2.sh "$@"
}
shopt -s nullglob
# Setup - the test script reads 'file'.
# The SOQ global .gitignore doesn't permit 'file' to be committed.
rm -fr split_DB
rm -f file
ln -s generated.data file
# Ensure cleanup
trap 'rm -fr split_DB; exit 1' 0 1 2 3 13 15
for function in \
test_awk_256 \
test_awk_7000 \
test_chepner_1_shell \
test_chepner_2_shell \
test_jl_shell \
test_op_shell \
test_perl_7000 \
test_python_7000
do
mkdir split_DB
boxecho "${function#test_}"
time $function file
# Basic validation - the same information should appear for all scripts
ls split_DB | wc -l
wc split_DB/* | tail -n 2
rm -fr split_DB
done
trap 0
time (ulimit -n 7000; TRACEDIR=. Trace bash test-script.sh)