使用 awk sed 等。从没有结束标签的文件中解析字段

Posted

技术标签:

【中文标题】使用 awk sed 等。从没有结束标签的文件中解析字段【英文标题】:Using awk sed et al. to parse fields from a file having no end tags 【发布时间】:2020-04-05 08:34:30 【问题描述】:

我想要的输出是一个逗号分隔的文件。如果一切都失败了,我知道我可以使用 for/each 逻辑或其他东西编写脚本,但我希望使用 awk 和 sed 找到一个优雅的解决方案,这在我之前曾多次为我服务过。我只是被这个难住了。

下面是数据的表示,后面是期望的结果。注意:每个 GROUPNUMBER 都有一组关联的 GROUPMEMBER、GROUPMEMBERID 和 MEMBERRANK 字段(它们并不总是像在示例数据中那样位于行的第一个字符)。 GROUPNUMBER 周围的其他行和文本不重要,也可能有冒号,并且在此处表示为 lorem ipsum 文本。一些 GROUPNUMBER 根本没有关联的字段(如最后几行的示例数据所示,对于 Group#88888)。此外,在示例数据中,GROUPNUMBER 部分显示为用空行分隔,但情况并非总是如此(有时下一部分与前一部分没有换行符)。

样本数据:

loremipsum: loremipsum?# loremipsum/123: loremipsumxx GROUPNUMBER:111222
loremipsum123:loremispum loremipsumxxxx
GROUPMEMBER:Joe:MEMBERRANK:1
GROUPMEMBERID:1234
GROUPMEMBER:Mike:MEMBERRANK:1
GROUPMEMBERID:2234
loremipsum14e3:loremispum loremipsumxxxx
loremipsum1eer534:loremispum loremipsumxxfgt
GROUPMEMBER:Sue:MEMBERRANK:89
GROUPMEMBERID:3234
GROUPMEMBER:John:MEMBERRANK:323
GROUPMEMBERID:4234:loremipsumaaa_loremipsum

loremipsum: loremipsum..<?# loremipsum/123: loremipsumxx GROUPNUMBER:333444
loremipsum123:loremispum loremipsumxxxx
GROUPMEMBER:Frank:MEMBERRANK:4
GROUPMEMBERID:5234
GROUPMEMBER:Laurie:MEMBERRANK:4
GROUPMEMBERID:6234

loremipsum: loremipsum..<?# loremipsum/123: loremipsumxx GROUPNUMBER:88888
loremipsum123:loremispum loremipsumxxxx

期望的输出:

GROUPNUMBER, MEMBERNAME, MEMBERID, MEMBERRANK

Example from above data:
111222,Joe,1234,1
111222,Mike,2234,1
111222,Sue,3234,89
111222,John,4234,323
333444,Frank,5234,4
333444,Laurie,6234,4
88888,,,

【问题讨论】:

which have served me well so many times before 在这个论坛上,我们鼓励其他人发布他们尝试过的内容。请张贴您尝试过的内容,并说明是什么阻止了您编写它。你的问题到底是什么?虽然我相信它可以在 sed 中完成,但 awk 解决方案会更容易且更具可读性。 edit 你的问题是解释如何从你 loremipsum 编辑的所有其余文本中识别GROUPNUMBER:111222 等字符串。例如,您输入中唯一的字符串是all upper case then colon then digits &lt;input sed 's/GROUPNUMBER:/\n&amp;/g;s/GROUPMEMBER:/\n&amp;/g;s/GROUPMEMBERID:/\n&amp;/g;s/MEMBERRANK/\n&amp;/g' | awk -v OFS=, -F: 'function o(v)if(v)print n,m,i,r;p=m=i=r=""$1=="GROUPNUMBER"o(p);n=$2;p=1$1=="GROUPMEMBER"o(m);m=$2$1=="GROUPMEMBERID"o(i);i=$2$1=="MEMBERRANK"o(r);r=$2n&amp;&amp;m&amp;&amp;i&amp;&amp;ro(1)ENDo(p)' 【参考方案1】:

在 GNU sed 中可能几乎不可能......但最好使用带有哈希表的东西,或者至少比保留空间更多的变量:

sed -nE '/GROUPNUMBER:/s/.*://;h;/GROUPMEMBER:/N;G;s/GROUPMEMBER:(.*):MEMBERRANK:(.*)\nGROUPMEMBERID:([^:]*).*\n(.*)/\4,\1,\2,\3/p;$g;s/$/,,,/p' file

这是对最后一组的粗俗处理...最后一行将始终是最后的组号和三个逗号。

要点:

sed -nE 仅在指定时打印,并允许无反斜杠捕获组 h 持有 GROUPNUMBER N;G 在 GROUPMEMBER 行上附加后续行和组号 捕获组 1-4 重新排序以格式化输出 最后一行 $ 的 hack,它使用 g 获取最后一个 GROUPNUMBER 并用三个逗号打印

【讨论】:

【参考方案2】:

这不是特别容易,但也不是非常困难。所有有趣的信息都在由冒号分隔的字段中,因此一部分是让awk 将输入行拆分为基于冒号的字段(-F:)。然后需要识别组号、组成员、成员等级和成员 ID。任何没有匹配信息的行都会被忽略。可以通过扫描字段来查找与关键字匹配的字段,然后将其后面的字段作为值返回来查找字段值。在下面的代码中,函数extractor 完成了这项工作。还需要跟踪打印组号的次数。在输入结束时,或者识别到新的组号时,如果旧的组号已经打印了零次,则需要打印组信息。函数print_member 打印一个成员;它节省了写出 printf 语句 3 次。

awk -F: '
function extractor(tag,   i)

    for (i = 1; i < NF; i++)
        if ($i ~ tag)
            return $(i + 1)
    return ""

function print_member()

    printf "%s,%s,%s,%s\n", groupnumber, groupmember, groupmemberid, memberrank

    /GROUPNUMBER:[0-9]+/ 
        if (groupnumber != "" && groupcount == 0)
            print_member()
        groupnumber = extractor("GROUPNUMBER")
        groupmember = ""
        memberrank = ""
        groupmemberid = ""
        groupcount = 0
    
    /GROUPMEMBER:[^:]+:MEMBERRANK:[0-9]+/ 
        groupmember = extractor("GROUPMEMBER")
        memberrank = extractor("MEMBERRANK")
    
    /GROUPMEMBERID:[0-9]+/ 
        groupmemberid = extractor("GROUPMEMBERID")
        print_member()
        groupcount++
    
    END 
        if (groupcount == 0)
            print_member()
    ' data

给定问题中的数据文件(名称data),输出为:

111222,Joe,1234,1
111222,Mike,2234,1
111222,Sue,3234,89
111222,John,4234,323
333444,Frank,5234,4
333444,Laurie,6234,4
88888,,,

这似乎是所需的输出。现在考虑一个修改过的输入文件(有许多添加),如下所示:

loremipsum: loremipsum?# loremipsum/123: loremipsumxx GROUPNUMBER:111222:hydrangea
loremipsum123:loremispum loremipsumxxxx
GROUPMEMBER:Joe:MEMBERRANK:1:orchid
GROUPMEMBERID:1234
GROUPMEMBER:Mike:piscatore:MEMBERRANK:1
GROUPMEMBERID:2234
loremipsum14e3:loremispum loremipsumxxxx
loremipsum1eer534:loremispum loremipsumxxfgt
GROUPMEMBER:Sue:MEMBERRANK:89
GROUPMEMBERID:3234
GROUPMEMBER:John:MEMBERRANK:323
GROUPMEMBERID:4234:loremipsumaaa_loremipsum
loremipsum: loremipsum..<?# loremipsum/123: loremipsumxx GROUPNUMBER:333444
loremipsum123:loremispum loremipsumxxxx
GROUPMEMBER:Frank:MEMBERRANK:4
GROUPMEMBERID:5234
GROUPMEMBER:Laurie:MEMBERRANK:4
GROUPMEMBERID:6234
loremipsum: loremipsum..<?# loremipsum/123: loremipsumxx GROUPNUMBER:88888
loremipsum123:loremispum loremipsumxxxx
loremipsum: loremipsum..<?# loremipsum/123: loremipsumxx GROUPNUMBER:222444
loremipsum123:loremispum loremipsumxxxx
GROUPMEMBER:Helen Mary Ann:MEMBERRANK:1
loremipsum: loremipsum..<?# loremipsum/123: loremipsumxx GROUPNUMBER:222555
loremipsum123:loremispum loremipsumxxxx
loremipsum123:loremispum loremipsumxxxx

现在的输出是:

111222,Joe,1234,1
111222,Joe,2234,1
111222,Sue,3234,89
111222,John,4234,323
333444,Frank,5234,4
333444,Laurie,6234,4
88888,,,
222444,Helen Mary Ann,,1
222555,,,

这看起来很合理。 Helen Mary Ann 名字中的空格无关紧要;她没有会员ID。中间没有信息的组也正确显示。

显然,您可以通过将其放入文件并将data 替换为"$@" 来将其转换为可用的shell 脚本,以便它处理命令行中给出的文件名,或者读取标准输入(如果有)没有这样的名字。


如 cmets 中所述,上面的代码假定 GROUPMEMBER 和 MEMBERRANK 字段在一行上是连续的,中间没有任何随机的“ipsum lorem”类型字段。如果实际上两者之间可能存在“ipsum lorem”字段(我在第二个数据集中使用了piscatore),则需要修改脚本以分别识别 GROUPMEMBER 和 MEMBERRANK。这还有一个额外的好处,如果输入包含:

ipsum lorem:MEMBERRANK:1:ipsum lorem:GROUPMEMBER:Hailey:ipsum lorem

ipsum lorem:MEMBERRANK:110:ipsum lorem
hallucination:GROUPMEMBER:Julian:doldrums

然后它会准确地识别信息(在第二个示例中,行按任意顺序排列)。 GROUPMEMBERID 必须是具有给定 GROUPNUMBER 的每个成员的三个条目中的最后一个。

awk -F: '
function extractor(tag,   i)

    for (i = 1; i < NF; i++)
        if ($i ~ tag)
            return $(i + 1)
    return ""

function print_member()

    printf "%s,%s,%s,%s\n", groupnumber, groupmember, groupmemberid, memberrank

    /GROUPNUMBER:[0-9]+/ 
        if (groupnumber != "" && groupcount == 0)
            print_member()
        groupnumber = extractor("GROUPNUMBER")
        groupmember = ""
        memberrank = ""
        groupmemberid = ""
        groupcount = 0
    
    /GROUPMEMBER:[^:]+/ 
        groupmember = extractor("GROUPMEMBER")
    
    /MEMBERRANK:[0-9]+/ 
        memberrank = extractor("MEMBERRANK")
    
    /GROUPMEMBERID:[0-9]+/ 
        groupmemberid = extractor("GROUPMEMBERID")
        print_member()
        groupcount++
    
    END 
        if (groupcount == 0)
            print_member()
    ' data

现在的输出是:

111222,Joe,1234,1
111222,Mike,2234,1
111222,Sue,3234,89
111222,John,4234,323
333444,Frank,5234,4
333444,Laurie,6234,4
88888,,,
222444,Helen Mary Ann,,1
222555,,,

【讨论】:

如果 GROUPNUMBER 在一行中跟在 GROUPMEMBER、GROUPMEMBERID 或 MEMBERRANK 之后,则会中断 @jhnc — 如果示例中的数据不能合理地完全说明可能发生的情况,那么是的,处理可能需要更改。示例数据清楚地表明 GROUPNUMBER 只出现在一行上,没有任何其他项目。如果这在完整数据集上不准确,则需要在问题中显示。我们只需要解决问题显示的内容或可以推断的内容。 问题的最后一行提到部分可能会在没有换行符的情况下继续 @jhnc:由于前言说“在示例数据中,GROUPNUMBER 部分显示为用空行分隔,但情况并非总是如此”,我将其解释为空行(又名“换行符”)并不总是存在,如我的第二个数据文件所示。如果“换行符”真的是“换行符”——一种可能但不一定合理的解释——那么是的,需要做更多的工作。 YMMV。除非 OP 建议所指的换行符是“换行符”而不是“空白行”,否则我不会担心。 很难清楚地解释这一点,很抱歉造成混淆。每行后面都有一个换行符。我是说在示例数据中看到的额外空行(双换行符?)并不总是存在,有时它看起来像 Jonathan 的数据。我之所以提到这一点,是因为我不能依靠“双换行符”来表示 Group 的结束。使用函数有点像编写脚本,但是将它们放在单个 awk 命令中真是太酷了。迫不及待地想明天在工作中试试这个。不过有一件事,您最终输出的第二行显示“Joe”,但应该是“Mike”,这是不是打错字了?

以上是关于使用 awk sed 等。从没有结束标签的文件中解析字段的主要内容,如果未能解决你的问题,请参考以下文章

sed / awk - 使用模式匹配后插入空格

sed 与 awk

用于搜索文本文件的一部分并打印它的通用命令(使用 awk 或 sed)

如何使用 sed、awk 或 gawk 仅打印匹配的内容?

文本处理工具之---sed

sed-awk的简单操作