提高bash脚本的性能



我正在对成千上万的CSV文件进行循环,以从中生成更多的文件。要求是提取前1个月,3个月,一个月,1年&2年的数据从每个文件&生成新文件。

我已经写了下面的脚本,完成了工作,但超级慢。这个脚本将需要非常频繁地运行,这使我的生活很麻烦。是否有更好的方法来实现我之后的结果或可能提高这个脚本的性能请?

for k in *.csv; do
sed -n '/'"$(date -d "2 year ago" '+%Y-%m')"'/,$p' ${k} > temp_data_store/${k}.2years.csv
sed -n '/'"$(date -d "1 year ago" '+%Y-%m')"'/,$p' ${k} > temp_data_store/${k}.1year.csv
sed -n '/'"$(date -d "6 month ago" '+%Y-%m')"'/,$p' ${k} > temp_data_store/${k}.6months.csv
sed -n '/'"$(date -d "3 month ago" '+%Y-%m')"'/,$p' ${k} > temp_data_store/${k}.3months.csv
sed -n '/'"$(date -d "1 month ago" '+%Y-%m')"'/,$p' ${k} > temp_data_store/${k}.1month.csv
done

每个CSV要读五遍。最好每个CSV只读取一次。

您多次提取相同的数据。只有一个部分的子集。

  • 2 years ago是1年前、6个月前、3个月前和1个月前的子集。
  • 1 year ago是6个月前、3个月前和1个月前的子集。
  • 6个月前是3个月前和1个月前的子集。
  • 3个月前是1个月前的子集。

表示"2"年内的每一行。也是在"1年。csv"。因此,提取"2年"就足够了。从"1 year.csv"。您可以使用tee级联不同的搜索。

下面的代码假设文件的内容是按时间顺序排列的。(我把引用简化了一点)

sed -n "/$(date -d '1 month ago' '+%Y-%m')/,$p" "${k}" |
tee temp_data_store/${k}.1month.csv |
sed -n "/$(date -d '3 month ago' '+%Y-%m')/,$p" |
tee temp_data_store/${k}.3months.csv |
sed -n "/$(date -d '6 month ago' '+%Y-%m')/,$p" |
tee temp_data_store/${k}.6months.csv |
sed -n "/$(date -d '1 year ago' '+%Y-%m')/,$p" |
tee temp_data_store/${k}.1year.csv |
sed -n "/$(date -d '2 year ago' '+%Y-%m')/,$p" > temp_data_store/${k}.2years.csv

当前与性能相关的问题:

  • 读取每个输入文件5次=>我们希望将其限制为每个输入文件只读取一次
  • 为每个输入文件调用date5次(必要)=>在for k in *.csv循环之前进行5次date调用[注意:与重复读取输入文件相比,重复调用date的开销将显得微不足道]

潜在的操作问题:

sed不是为做数据比较而设计的(例如,寻找>=搜索模式的字符串);考虑这样一个输入文件:

$ cat input.csv
2021-01-25
2021-03-01

如果"今天"是2021-03-14,那么对于1month数据集,当前sed解决方案是:

sed '/2012-02/,$p'

但是因为没有2012-02的条目,所以sed命令返回0行,即使我们应该看到2021-03-01的行。

当然,对于这个特定的问题,我们正在寻找基于月份的日期,应用程序可能每月至少生成一行,所以这个问题可能不会是一个问题,但是,我们需要注意这个问题。

无论如何,回到手头的问题…


假设:

  • 输入文件以逗号分隔(否则需要调整建议的解决方案)
  • 待测日期格式为YYYY-MM-...
  • 待测数据为以逗号分隔的输入文件的第一个字段(否则需要调整提出的解决方案)
  • 输出文件名前缀为输入文件名,.csv

样本输入:

$ cat input.csv
2019-09-01,line 1
2019-10-01,line 2
2019-10-03,line 3
2019-12-01,line 4
2020-05-01,line 5
2020-10-01,line 6
2020-10-03,line 7
2020-12-01,line 8
2021-03-01,line 9
2021-04-01,line 10
2021-05-01,line 11
2021-07-01,line 12
2021-09-01,line 13
2021-09-01,line 14
2021-10-11,line 15
2021-10-12,line 16

我们只需要做一次日期计算,所以我们将在bash和OP的for k in *.csv循环之前执行此操作:

# date as of writing this answer: 2021-10-12
$ yr2=$(date -d "2 year ago" '+%Y-%m')
$ yr1=$(date -d "1 year ago" '+%Y-%m')
$ mon6=$(date -d "6 month ago" '+%Y-%m')
$ mon3=$(date -d "3 month ago" '+%Y-%m')
$ mon1=$(date -d "1 month ago" '+%Y-%m')
$ typeset -p yr2 yr1 mon6 mon3 mon1
declare -- yr2="2019-10"
declare -- yr1="2020-10"
declare -- mon6="2021-04"
declare -- mon3="2021-07"
declare -- mon1="2021-09"

一个awk想法(取代OP当前for k in *.csv循环中的所有sed调用):

# determine prefix to be used for output files ...
$ k=input.csv
$ prefix="${k//.csv/}"
$ echo "${prefix}"
input
awk -v yr2="${yr2}"       
-v yr1="${yr1}"       
-v mon6="${mon6}"     
-v mon3="${mon3}"     
-v mon1="${mon1}"     
-v prefix="${prefix}" 
-F ',' '                                # define input field delimiter as comma
{ split($1,arr,"-")                         # date to be compared is in field #1
testdate=arr[1] "-" arr[2]
if ( testdate >= yr2  ) print $0 > prefix".2years.csv"
if ( testdate >= yr1  ) print $0 > prefix".1year.csv"
if ( testdate >= mon6 ) print $0 > prefix".6months.csv"
if ( testdate >= mon3 ) print $0 > prefix".3months.csv"
if ( testdate >= mon1 ) print $0 > prefix".1month.csv"
}
' "${k}"

注意:awk可以动态地处理输入文件名,以确定文件名前缀(参见FILENAME变量),但仍然需要知道目标目录名(假设写入与输入文件所在目录不同的目录)

生成以下文件:

for f in "${prefix}".*.csv
do
echo "############# ${f}"
cat "${f}"
echo ""
done
############# input.2years.csv
2019-10-01,line 2
2019-10-03,line 3
2019-12-01,line 4
2020-05-01,line 5
2020-10-01,line 6
2020-10-03,line 7
2020-12-01,line 8
2021-03-01,line 9
2021-04-01,line 10
2021-05-01,line 11
2021-07-01,line 12
2021-09-01,line 13
2021-09-01,line 14
2021-10-11,line 15
2021-10-12,line 16
############# input.1year.csv
2020-10-01,line 6
2020-10-03,line 7
2020-12-01,line 8
2021-03-01,line 9
2021-04-01,line 10
2021-05-01,line 11
2021-07-01,line 12
2021-09-01,line 13
2021-09-01,line 14
2021-10-11,line 15
2021-10-12,line 16
############# input.6months.csv
2021-04-01,line 10
2021-05-01,line 11
2021-07-01,line 12
2021-09-01,line 13
2021-09-01,line 14
2021-10-11,line 15
2021-10-12,line 16
############# input.3months.csv
2021-07-01,line 12
2021-09-01,line 13
2021-09-01,line 14
2021-10-11,line 15
2021-10-12,line 16
############# input.1month.csv
2021-09-01,line 13
2021-09-01,line 14
2021-10-11,line 15
2021-10-12,line 16

其他性能改进:

  • [特别是对于大文件]从一个文件系统读取,写到第二个/不同的文件系统;更好的办法是为每一个5倍不同的输出文件提供一个单独的文件系统——这需要对awk解决方案做一个小小的调整
  • 并行处理X个输入文件,例如,awk代码可以放在bash函数中,然后通过<function_name> <input_file> &调用;这可以通过bash循环控件,parallel,xargs等来完成。
  • 如果运行并行操作,则需要限制并行操作的数量,主要基于磁盘子系统的吞吐量,即磁盘子系统在由于读写争用而减慢速度之前可以处理多少并发读/写

如果你有GNU Parallel (version>= 20191022):

do_one() {
cat "$1" | parallel --pipe --tee sed -n '/$(date -d {}" ago" '+%Y-%m')/,$p' "> temp_data_store/$1.{}.csv" 
::: "2 year" "1 year" "6 month" "3 month" "1 month"
}
export -f do_one
parallel -j1 do_one ::: *.csv

如果你的磁盘快:删除-j1.

最新更新