从文本文件中提取结构化数据(awk?):缺少的字段必须获得默认值

Posted

技术标签:

【中文标题】从文本文件中提取结构化数据(awk?):缺少的字段必须获得默认值【英文标题】:extract structured data from text files (awk ?) : missing fields must get default value 【发布时间】:2022-01-19 17:56:19 【问题描述】:

(我正在使用 macos)。

我在子文件夹中有 70k 文本文件,我想以递归方式从中提取一些数据,然后 - 如果可能的话 - 将输出写入一个制表符分隔的文件以供以后电子表格处理。 来自我的 wiki 的文件(我使用 PmWiki,它将数据保存在 text files 中)在完成时以这种方式格式化(为了便于阅读,删除了不需要的数据):

version=
agent=
author=
charset=
csum=
ctime=1041379201
description=
host=
name=Name.12
rev=3
targets=Target.1,OtherTarget.23,Target.90
text=
time=
title=My title
author:
csum:
diff:
host:
author:
csum:
diff:

我想为名为 ctime name rev targets title 的字段(5 个字段)提取以 = 分隔的数据。

我的主要问题是如何获取数据(键 ctime= rev= targets= name= title=),以及在某些缺失时使用默认值?

我认为必须测试每个目标键是否存在;如果缺少,则使用默认值创建它;然后提取想要的字段值,最后将数据制成表格。

预期的输出将是制表符分隔的;丢失的数据将被命名为以后容易捕获的东西。 即,对于示例中给出的完整文件(用制表符代替空格),输出将给出类似 (ctime, rev, name, title, targets) 的内容:

1041379201 3 Name.12 my title Target.1,OtherTarget.23,Target.90

并且,对于不完整的文件(第 1 行中缺少的字段是 rev ;在第 2 行中,rev 和标题):

1041379201 XXX Name.12 my title Target.1,OtherTarget.23,Target.90
1041379201 XXX Name.12 XXX Target.1,OtherTarget.23,Target.90

最终的项目是能够每月提取一次数据,然后在电子表格中易于使用的文本文件,每月更新。

我不太糟糕的尝试是这样的(但根本不起作用,缺少 if/else 条件):

awk 'BEGIN  FS = "=" ; /^ctime=/ 
                print $2
                next
                
/^rev=/ 
                print $2
                next
/^name=/ 
                print $2
                next
/^title=/ 
                print $2
                next
/^targets=/ 
                print $2
                next'

这是一个原始的 PmWiki 文件(在这种情况下,我仍然想提取 ctime name rev targets title(并为缺少的字段设置默认值,ctimetitle):

version=pmwiki-2.2.64 ordered=1 urlencoded=1
author=simon
charset=UTF-8
csum=add summary
name=Main.HomePage
rev=203
targets=PmWiki.DocumentationIndex,PmWiki.InitialSetupTasks,PmWiki.BasicEditing,Main.WikiSandbox
text=(:Summary:The default home page for the PmWiki distribution:)%0aWelcome to PmWiki!%0a%0aA local copy of PmWiki's%0adocumentation has been installed along with the software,%0aand is available via the [[PmWiki/documentation index]].  %0a%0aTo continue setting up PmWiki, see [[PmWiki/initial setup tasks]].%0a%0aThe [[PmWiki/basic editing]] page describes how to create pages%0ain PmWiki.  You can practice editing in the [[wiki sandbox]].%0a%0aMore information about PmWiki is available from [[http://www.pmwiki.org]].%0a
time=1400472661

更新我的问题。

我发布问题的方式可能看起来比实际复杂。 由此,在 70k 文本文件中重复:

word1=line1
word2=line2
word3=line3
...

我想获取一个文件,收集每个line1, line3, lineX(用于针对 word1、word2、wordX 的命令)并且在 word1=line1 或 word2=line2 或 wordX=lineX 根本不存在的情况下具有默认值。

最后,通过 Rick Smith 对Retrieve default value with grep -e?的回答,我发现了一些非常接近我需要的东西

【问题讨论】:

每个文件是否有固定的字段要求,例如每个文件总是有 21 个关键字段,例如“版本”或“代理”,或者每个文件是否出现多次?字段分隔符是“=”还是“:”?您的 2 行示例中的字段键在哪里? 关于by the way I don't know how to run this kind of command recursively, for current folder and subfolders 单独问一个问题,如果你无法弄清楚,一次只问一个问题。 编辑我的问题以便更好地理解:我愿意只收集每个文件中的几个字段(ctime=、rev=、name=、title=、targets=),并且需要有默认值缺失字段的值(在五个字段中)。对于这些字段,分隔符始终为 = 每个输入文件都被折叠成一行输出,对吧?您是否需要使用匹配的输出数据维护输入文件名,如果答案是“是”,您是否可以更新问题以使用(输入)文件名显示预期输出? 我不需要标题,也不需要文件名;我有文件,每个文件都由结构化的行列表组成:a=line1、b=line2、c=line3 等,我想在其中提取 line2、line3(我可以用awk 'print $2' FS='=') 做到这一点,但我不这样做'不知道该怎么做是为缺少 b=line2 的文件设置 linedefaut 值。 【参考方案1】:

我刚刚注意到您说您只想打印特定标签的值,这会使事情变得更容易。将 GNU awk 用于 ENDFILEgensub()

$ cat tst.awk
BEGIN 
    OFS="\t"
    numTags = split("ctime rev targets name title",tags)

    for (tagNr=1; tagNr<=numTags; tagNr++) 
        tag = tags[tagNr]
        printf "%s%s", tag, (tagNr<numTags ? OFS : ORS)
    


match($0,/^([[:alnum:]_]+)[=:](.*)/,a) 
    tag = a[1]
    val = gensub(" ?" OFS " ?"," ","g",a[2])
    tag2val[tag] = val


ENDFILE 
    for (tagNr=1; tagNr<=numTags; tagNr++) 
        tag = tags[tagNr]
        val = ( tag in tag2val ? tag2val[tag] : "_ABSENT_" )
        val = ( val == "" ? "_NULL_" : val )
        printf "%s%s", val, (tagNr<numTags ? OFS : ORS)
    
    delete tag2val

$ awk -f tst.awk file
ctime   rev     targets name    title
1041379201      3       Target.1,OtherTarget.23,Target.90       Name.12 My title

$ awk -f tst.awk file | column -s$'\t' -t
ctime       rev  targets                            name     title
1041379201  3    Target.1,OtherTarget.23,Target.90  Name.12  My title

原答案:

如果每个输入文件中的标签都是唯一的,这听起来可能就是您正在尝试做的事情,需要 GNU awk 进行多个扩展:

$ cat tst.awk
BEGIN  OFS="\t" 
match($0,/^([[:alnum:]_]+)[=:](.*)/,a) 
    tag = a[1]
    val = gensub(" ?" OFS " ?"," ","g",a[2])

    if ( !seen[tag]++ ) 
        tags[++numTags] = tag
    

    key2val[ARGIND,tag] = val

END 
    for (tagNr=1; tagNr<=numTags; tagNr++) 
        tag = tags[tagNr]
        printf "%s%s", tag, (tagNr<numTags ? OFS : ORS)
    

    for ( fileNr=1; fileNr<=ARGIND; fileNr++) 
        for (tagNr=1; tagNr<=numTags; tagNr++) 
            tag = tags[tagNr]
            key = fileNr SUBSEP tag
            val = ( key in key2val ? key2val[key] : "_ABSENT_" )
            val = ( val == "" ? "_NULL_" : val )
            printf "%s%s", val, (tagNr<numTags ? OFS : ORS)
        
    

$ awk -f tst.awk file
version agent   author  charset csum    ctime   description     host    name    rev     targets text    time    title   diff
_NULL_  _NULL_  _NULL_  _NULL_  _NULL_  1041379201      _NULL_  _NULL_  Name.12 3       Target.1,OtherTarget.23,Target.90       _NULL_   _NULL_  My title        _NULL_

查看视觉对齐的列:

$ awk -f tst.awk file | column -s$'\t' -t
version  agent   author  charset  csum    ctime       description  host    name     rev  targets                            text    time    title     diff
_NULL_   _NULL_  _NULL_  _NULL_   _NULL_  1041379201  _NULL_       _NULL_  Name.12  3    Target.1,OtherTarget.23,Target.90  _NULL_  _NULL_  My title  _NULL_

只需在所有文件上一次运行它:

awk -f tst.awk file1 file2 etc.

它会找出所有文件中的所有标签,然后打印一个 TSV,其中包含所有这些文件中所有这些标签的值。

【讨论】:

我忘了说我正在使用 macos ;使提议的 Ed 的脚本失败:>>> match($0,/^([[:alnum:]_]+)[=:](.*)/, 然后安装gawk,很简单(google一下)。 Gawk 已安装(当您知道不该做什么时很容易)。您的脚本经过测试,可以工作,但必须针对某些源文件进行改进(输出可能无法正常工作,具体取决于来源:我怀疑正则表达式不完全是我需要的)。我找到了另一种 awk 解决方案,它看起来与我的项目更兼容。 老实说 - 很抱歉,但对于您正在尝试做的事情,几乎不可能有比这更好的解决方案。您在问题中引用的当然不是因为它会慢几个数量级,以随机顺序产生输出,无法区分空值和缺失标签,如果您的输入包含选项卡或"s,则会失败等。如果我使用的正则表达式(我根据您的示例创建)不是您需要的,则修复它,或者如果您可以在其他答案中使用$1,那么只需像您一样设置 FS并使用$1 而不是a[1] 等。【参考方案2】:

假设:

输入文件至少有一行 field=value 条目不跨越多行(即,fieldvalue 均不包含嵌入式换行符/回车符) fieldvalue 均不包含 = 字符(即,= 在每个输入行仅显示一次) OP 可以创建一个包含所需字段列表及其默认值的新文件 [这消除了对字段名称、它们的顺序和它们的默认值进行硬编码的需要]

示例输入文件:

$ cat 1.txt
version=
agent=
author=
charset=
csum=
ctime=1041379201
description=
host=
name=Name.12
rev=3
targets=Target.1,OtherTarget.23,Target.90
text=
time=
title=My title
author:
csum:
diff:
host:
author:
csum:
diff:

$ cat 2.txt
version=pmwiki-2.2.64 ordered=1 urlencoded=1
author=simon
charset=UTF-8
csum=add summary
name=Main.HomePage
rev=203
targets=PmWiki.DocumentationIndex,PmWiki.InitialSetupTasks,PmWiki.BasicEditing,Main.WikiSandbox
text=(:Summary:The default home page for the PmWiki distribution:)%0aWelcome to PmWiki!%0a%0aA local copy of PmWiki's%0adocumentation has been installed along with the software,%0aand is available via the [[PmWiki/documentation index]].  %0a%0aTo continue setting up PmWiki, see [[PmWiki/initial setup tasks]].%0a%0aThe [[PmWiki/basic editing]] page describes how to create pages%0ain PmWiki.  You can practice editing in the [[wiki sandbox]].%0a%0aMore information about PmWiki is available from [[http://www.pmwiki.org]].%0a
time=1400472661

$ cat 3.txt                # NOTE: no matches with fields in defaults.txt
other=abc
line=def

假设 OP 可以创建一个包含所需字段名称和默认值的文件,例如:

$ cat defaults.txt
ctime=CCCC
name=NNNN
rev=REV
targets=NO_TARGETS
title='BLANK TITLE'

注意:最终输出中的字段顺序与defaults.txt中的字段顺序相同

一个awk想法:

awk -F'=' '

function print_line() 
    pfx=""
    if ( printme )                       # skip the first call to this function
       for ( i=1; i<=ordno; i++ )       # loop through our list of desired fields ...

           printf "%s%s", pfx, ( order[i] in fields ? fields[order[i]] : defaults[order[i]] )
           pfx=OFS
       

    print ""                             # terminate line
    delete fields                        # reset our fields[] array
    printme=1                            # enable printing of fields[] contents on next call


BEGIN           OFS="\t"                # output field delimiter
                 printme=0               # disable printing of fields[] on first function call
               

FNR==NR                                 # process 1st file, ie, our desired fields and their associated default values
                 order[++ordno]=$1       # save order of fields
                 defaults[$1]=$2         # save default values
                 next
               

FNR==1          print_line()            # upon seeing a new file flush the contents of fields[] to stdout
                 print "#### "FILENAME   # remove this line once OP validates output
               

$1 in defaults  fields[$1]=$2          # if field #1 is in our default[] array then save field #2 in our fields[] array

END             print_line()           # flush last file/fields[] to stdout

' defaults.txt 1.txt 2.txt 3.txt

注意:我无权访问 MacOS/awk 安装,因此 OP 需要确定这是否适用于他们的环境

这会生成:

#### 1.txt
1041379201      Name.12 3       Target.1,OtherTarget.23,Target.90       My title
#### 2.txt
CCCC    Main.HomePage   203     PmWiki.DocumentationIndex,PmWiki.InitialSetupTasks,PmWiki.BasicEditing,Main.WikiSandbox 'BLANK TITLE'
#### 3.txt
CCCC    NNNN    REV     NO_TARGETS      'BLANK TITLE'

没有print "#### "FILENAME:

1041379201      Name.12 3       Target.1,OtherTarget.23,Target.90       My title
CCCC    Main.HomePage   203     PmWiki.DocumentationIndex,PmWiki.InitialSetupTasks,PmWiki.BasicEditing,Main.WikiSandbox 'BLANK TITLE'
CCCC    NNNN    REV     NO_TARGETS      'BLANK TITLE'

【讨论】:

脚本测试;确实可以很好地进行演示(我希望打印现有的 ctime,我得到默认值 CCCC ;如果 rev 不存在,标题在目标值中间被融化(叠加)+名称被叠加的默认值删除修订版)。 我的错误:至于演示文稿,以 txt 文件发送的输出确实有效(终端破坏了布局,抱歉)。 ctime 确实像预期的那样工作,当 defaults.txt 被更正(错别字)时:ctime 代替 cctime。

以上是关于从文本文件中提取结构化数据(awk?):缺少的字段必须获得默认值的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 awk 或 grep 从标题中提取电子邮件字段

文本三剑客之awk

linux学习-awk工具

从具有不同结构的表单中提取字段

使用 bash 命令 awk sed 等从脚本中提取参数字段

awk 和 sed 文本操作(从特定组中提取大多数负值)