awk 关于日期条件

awk 关于日期条件

我正在尝试从 person.csv(如下)中删除行,条件是该人不是在过去 1 年出生的:

数据集1:

"Index","User Id","First Name","Last Name","Date of birth","Job Title"
"1","9E39Bfc4fdcc44e","new, Diamond","Dudley","06 Dec 1945","Photographer"
"3","32C079F2Bad7e6F","Ethan","Hanson","08 Mar 2014","Actuary"
"2","aaaaaaa, bbbbbb","Grace","Huerta","21 Jan 2023","Visual merchandiser"

因此,预期的输出如下所示(最后一行在不到一年的时间内被删除):

"Index","User Id","First Name","Last Name","Date of birth","Job Title"
"1","9E39Bfc4fdcc44e","new, Diamond","Dudley","06 Dec 1945","Photographer"
"3","32C079F2Bad7e6F","Ethan","Hanson","08 Mar 2014","Actuary"

我尝试使用 awk 来执行以下操作:

awk -F , '{print $5 ....}' person.csv > output.csv

但是,无法弄清楚如何将每行日期与(今天减去 1 年)进行比较。

数据集2:有时双引号字段内可能有双引号,例如(line1 field4):

"Index","User Id","First Name","Last Name","Date of birth","Job Title"
"1","9E39Bfc4fdcc44e","new, Diamond","Dudley (aka "dud")","03 Oct 2023","Photographer"
"3","32C079F2Bad7e6F","Ethan","Hanson","03 Dec 2022","Actuary"
"2","aaaaaaa, bbbbbb","Grace","Huerta","21 Jan 2023","Visual merchandiser"

如果“sed”可以做到这一点,我也持开放态度。请任何帮助,谢谢!

答案1

假设:

  • 所有列/字段都用双引号引起来
  • 双引号不会显示为数据的一部分(否则我们需要除基本字符之外的其他内容-F'"'作为字段分隔符)
  • OP(操作系统)date支持该-d参数(例如,如果“今天”16 Sep 2023在 OP 的系统上将date -d '-1 year' '+%Y%m%d'生成20220916
  • 由于OP提到截止日期可以是任何东西(例如,-1年,-7天等),我们将使用(操作系统)date以格式生成截止日期YYYYMMDD(否则我们需要添加更多代码awk来能够处理各种条件,如“-1年”、“-7天”等)

一种awk方法:

cutoff=$(date -d '-1 year' '+%Y%m%d')                             # change '-1 year' to the desired condition;
                                                                  # alternatively: manually set to the desired date (in YYYYMMDD format)

awk -v cutoff="${cutoff}" -F'"' '                                 # set awk variable "cutoff" to the value of the OS variable of the same name
                                                                  # field delimiter is double quotes; this means data fields are even-numbered (eg, 5th field is the 10th "-delimited field)
BEGIN { mlist="JanFebMarAprMayJunJulAugSepOctNovDec" }
NR>1  { split($10,a,/[[:space:]]+/)                               # split 5th data field on spaces; a[1]=day a[2]=month a[3]=year
        m=sprintf("%02d", ( (index(mlist,a[2])+2) /3) )           # convert 3-letter month to 2-digit month
        if ( a[3] m a[1] > cutoff) next                           # if new date is greater than the cutoff then skip to the next line of input
      } 
1                                                                 # print the current line
' person.csv

这会生成:

"Index","User Id","First Name","Last Name","Date of birth","Job Title"
"1","9E39Bfc4fdcc44e","new, Diamond","Dudley","06 Dec 1945","Photographer"
"3","32C079F2Bad7e6F","Ethan","Hanson","08 Mar 2014","Actuary"

性能角度...

此答案需要单个操作系统调用,date并需要 1 个文件描述符打开/关闭(如果将输出重定向到另一个文件,则为 2 个)。

dateGilles 的答案需要对每行输入进行操作系统调用需要为每个调用打开/关闭文件描述符的昂贵开销date

测试运行:

100K line file          # per comment from OP
GNU awk 5.1.0
GNU date 8.32
Ubuntu 20.04
i7-1260P

这个答案:

real    0m0.198s        <<< 546 times faster
user    0m0.198s
sys     0m0.000s

吉尔斯的回答:

real    1m48.229s       <<<
user    1m30.598s
sys     0m23.999s

两次运行的输出都保存到文件中;diff两个输出文件中的a显示没有差异(即,两个答案生成相同的结果集)。


在这种情况下,OP 声明所有字段都用双引号引起来。

在某些字段可能没有用双引号引起来的情况下,我们可以使用GNU awk's 'FPAT'并且仍然只执行对 的单个调用date,例如:

cutoff=$(date -d '-1 year' '+%Y%m%d')

awk -v cutoff="${cutoff}" '
BEGIN { FPAT="([^,]+)|(\"[^\"]+\")"
        mlist="JanFebMarAprMayJunJulAugSepOctNovDec"
      }
NR>1  { f5=$5
        gsub(/"/,"",f5)                                           # strip double quotes from 5th data field
        split(f5,a,/[[:space:]]+/)                                # change from 10th field to 5th field
        m=sprintf("%02d", ( (index(mlist,a[2])+2) /3) )
        if ( a[3] m a[1] > cutoff) next
      }
    1
' person.csv

使用与上面相同的测试标准,这个答案的运行时间:

real    0m0.861s        <<<
user    0m0.850s
sys     0m0.009s

基于FPAT(而不是-F'"')解析输入会使运行时间增加约 4 倍,但仍然比 108 秒快得多。

答案2

使用GNU和 GNU和 1 年条件:

awk -v epoch1y=$(date -d '1 year ago' +%s) '
    BEGIN{FPAT="([^,]*)|(\"[^\"]+\")"}
    NR>1{
        cmd="date -d " $5 " +%s"
        epoch=( (cmd | getline line) > 0 ? line : "N/A")
        close(cmd)
        if (epoch > epoch1y) next
    }
    1
' person.csv

输出

"Index","User Id","First Name","Last Name","Date of birth","Job Title"
"1","9E39Bfc4fdcc44e","new, Diamond","Dudley","06 Dec 1945","Photographer"
"3","32C079F2Bad7e6F","Ethan","Hanson","08 Mar 2014","Actuary"

说明

代码应该清晰、简洁、易读。

不明显的部分是使用FPAT特殊变量(按内容划分),能够比简单的 处理更多的用例-F,

笔记某些程序导出的 CSV 数据包含双引号之间嵌入的换行符。 gawk 没有提供处理这个问题的方法。尽管存在 CSV 数据的正式规范,但还没有更多的工作要做;FPAT机制为大多数情况提供了优雅的解决方案,gawk 开发者对此感到满意。


这部分是运行 shell 命令并在之后输入变量getline的技巧。awkgetline


1最后,意味着true,所以在某种true条件下,默认情况下,它意味着print当前行。


关于通话的文档close()https://www.gnu.org/software/gawk/manual/html_node/Close-Files-And-Pipes.html


epoch是 Unix 时间戳:自 以来的秒数1/1/1970

答案3

由于awk不了解 CSV 的引用规则,因此最好使用支持 CSV 的工具来执行此任务。

使用 CSV 处理工具csvkit:

$ csvsql -I --query "SELECT * FROM file WHERE \`Date of birth\` <= date('now', '-1 year')" file
Index,User Id,First Name,Last Name,Date of birth,Job Title
1,9E39Bfc4fdcc44e,"new, Diamond",Dudley,06 Dec 1945,Photographer
3,32C079F2Bad7e6F,Ethan,Hanson,08 Mar 2014,Actuary

这将日期解析和任何日期计算委托给数据库后端 (SQLite)。

请注意,输出仅引用需要引用的字段。如果你想引用全部字段,将结果传递给csvformat -U1(另一个 csvkit 工具)。

编辑:请注意,更新问题中不正确引用的字段将由csvkit 工具"Dudley (aka "dud")"转换(此处未显示)。"Dudley (aka dud"")"""


不使用 csvkit 工具,仅使用 SQLite:直接从 CSV 文件加载数据并查询。 SQLite 包含一个支持 CSV 的读取器和写入器,因此我们可以放心,数据将被适当引用。

rm -f data.db
sqlite3 data.db \
    '.headers on' \
    '.mode csv' \
    '.import file mytable' \
    "SELECT * FROM mytable WHERE \`Date of birth\` <= date('now', '-1 year')"

在我的系统(无风扇“Intel(R) Core(TM) i5-4300U CPU @ 1.90GHz”)上,100k 条记录大约需要 0.75 秒,其中 0.3 秒用于构建 SQLite 数据库。对于 1M 条记录,这需要不到 7 秒的时间,其中 4 秒用于构建数据库。对于10M行,操作大约需要1分15秒,其中45秒用于建库。

编辑:请注意,更新问题中不正确引用的字段将被 SQLite CSV 读取器/编写器"Dudley (aka "dud")"更改为正确引用的字段(此处未显示)。"Dudley (aka ""dud"")"

答案4

IFS=$'\n'
lyear_old_sec=$(date -d "1 year ago" +%s)
echo '"Index","User Id","First Name","Last Name","Date of birth","Job Title"'
for i in $(cat sup.txt|awk 'NR>1')
do
dat_de=$(echo $i | awk -F "," '{print $(NF-1)}'|sed 's/"//g')
date_de_second=$(date -d "$dat_de" +%s)
if [[ $date_de_second -lt $lyear_old_sec ]]
then
echo $i
fi
done

相关内容