【问题标题】:Read file, multiple lines at a time (separated by delimiter)读取文件,一次多行(用分隔符分隔)
【发布时间】:2020-11-21 21:44:57
【问题描述】:

我有大量 .bib BibTeX 条目保存在一个文件中。我想读取文件,将每篇文章的数据(本质上由@分隔)存储到变量中,提取特定字段,最后将字段(制表符分隔)输出到清理后的文件中。

输入:

@article{Author1_2020,
    year = 2020,
    month = {feb},
    publisher = {Wiley},
    ...
}
@article{Author2_2010,
    year = 2010,
    month = {jul},
    publisher = {Journal},
    ...
}

输出:

Wiley   2020    feb
Journal 2010    jul

代码:

while IFS='@' read -r entry; do
    p=$(grep "publisher =" <<< "$entry" | cut ...)
    y=$(grep "year =" <<< "$entry" | awk ...)
    m=$(grep "month =" <<< "$entry" | cut ...)
    echo "$p    $y  $m" >> cleaned_up.bib
done < global.bib
```sh

Is there a way to make the `while read` command in bash operate on delimited chunks of text at a time, instead of single lines? `sed`/`awk` solutions would be more than welcome.

【问题讨论】:

  • IIRC 有一个可用于 python 的 bibtex 解析器,你为什么不使用它呢?用 shell 语言编写解析器将是重新发明轮子。
  • 这可能会回答你的问题:[ tex.stackexchange.com ] Command line tool to extract .bib entry by key

标签: regex bash awk sed bibtex


【解决方案1】:

另一种使用awk 的方法应该可以在所有awk 变体中移植,可以使用'=' 作为字段分隔符,例如:

awk -F= '
    $1~/[ ]*year/       { year = substr($2,2,match($2,/,/)-2) }
    $1~/[ ]*month/      { month = substr($2,3,match($2,/,/)-4) }
    $1~/[ ]*publisher/  { pub = substr($2,3,match($2,/,/)-4) }
    FNR>1 && $1~/^@/    { print pub"\t"year"\t"month }
    END                 { print pub"\t"year"\t"month }
' list.bib

每个规则都提取yearmonthpublisher,并使用substr()match() 从所需字符串的任一端修剪其他字符。 END 规则用于打印收集到的最终值集。

使用/输出示例

使用list.bib 中的示例数据,执行命令将导致:

awk -F= '
    $1~/[ ]*year/       { year = substr($2,2,match($2,/,/)-2) }
    $1~/[ ]*month/      { month = substr($2,3,match($2,/,/)-4) }
    $1~/[ ]*publisher/  { pub = substr($2,3,match($2,/,/)-4) }
    FNR>1 && $1~/^@/    { print pub"\t"year"\t"month }
    END                 { print pub"\t"year"\t"month }
' list.bib
Wiley   2020    feb
Journal 2010    jul

【讨论】:

  • 我选择了这个答案,因为很清楚每一行发生了什么,我对 awk 的基本熟悉。此命令也很容易修改/扩展(例如,包含更多字段、更改列顺序)。
  • 如何从substr($2,3,match($2,/,/)-4) 的输出中获取最后一个单词?这是一个“作者”字段。我不确定如何将典型的{print $NF} 命令合并到这个序列中。
  • 不要使用=作为FS,否则当任何数据字符串包含=时它会失败,例如。在title = {foo = bar} 之类的块中给出一行。当面对 tag=value 数据时,请改为 tag=val=$0; sub(/[[:space:]]*=.*/,"",tag); sub(/[^=]+=[[:space:]]*/,"",val) 以保证只有第一个 = 将被视为分隔符,并且 tag 和 value 都不会被破坏。如果任何值可以包含逗号,您的脚本也会出现问题,例如publisher = {O'Reilley, Ltd.}
  • 非常感谢 Ed。我在这里考虑了'=' 的唯一性,并且不得不假设在随后的{...} 字段中没有更多内容。注意到sub 的使用会更好。 (我实际上问自己“Ed 会做什么......”,我确实记得你使用 sub 而不是使用 substr(),但我的想法当时被锁定在 :)
【解决方案2】:

使用 GNU awk:

awk '{print $17, $6, $11}' RS='}\n' FS='( +|{|}|,)' OFS='\t' global.bib

输出:

威利 2020 年 2 月 杂志 2010 年 7 月

我将输入记录分隔符 (RS) 设置为 },后跟换行符。默认是换行符。

输入字段分隔符(FS)我设置为至少一个空格( +)或{},OFS 是输出字段分隔符。

具有相同输出的不同表示法:

awk 'BEGIN{RS="}\n"; FS="( +|{|}|,)"; OFS="\t"} {print $17, $6, $11}' global.bib

【讨论】:

  • $17等指的是什么?
  • 在第一块$17(第17列)指的是Wiley(与FS='( +|{|}|,)')。
  • 参见:awk '{for(i=1; i&lt;=NF; i++) print "column:",i,$i}' RS='}\n' FS='( +|{|}|,)' OFS='\t' global.bib NF 是最后一列的编号。
  • 如果你修改了字段分隔符 (FS) 它会变得更加清晰:awk '{for(i=1; i&lt;=NF; i++) print "column:",i,$i}' RS='}\n' FS='(\n?@article{|\n | = {?|}?,?\n )' OFS='\t' global.bib
  • 答案中的FS 依赖于未定义的行为,因为{ 是重复的开始(像a{3} 这样的RE 间隔规范),因此它的含义是未定义的前面有一个要重复的表达式。您需要将|{ 更改为|[{] 以使其健壮且可移植(这不是什么大问题,因为您说它只是gawk-only)。如果任何字段包含空格或逗号,您就会遇到问题,例如publisher = {Sue and Bob, Ltd.}
【解决方案3】:

每当输入数据有标签-值对时,我发现最好先创建一个该映射的数组(下面的f[]),然后您可以按标签(名称)按您喜欢的任何顺序打印您喜欢的任何字段:

$ cat tst.awk
BEGIN {
    OFS="\t"
    numTags = split(flds,tags)
}
/^}/ {
    for (tagNr=1; tagNr<=numTags; tagNr++) {
        tag = tags[tagNr]
        printf "%s%s", f[tag], (tagNr<numTags ? OFS : ORS)
    }
    delete f
    next
}
{
    gsub(/^[[:space:]]+|[[:space:],]+$/,"")
    tag = val = $0
    if ( sub(/^@/,"",tag) ) {
        sub(/\{.*/,"",tag)
        sub(/[^{]+\{|/,"",val)
    }
    else {
        sub(/[[:space:]]*=.*/,"",tag)
        sub(/[^=]+=[[:space:]]*/,"",val)
        gsub(/^\{|\}$/,"",val)
    }
    f[tag] = val
}

.

$ awk -v flds='publisher year month' -f tst.awk file
Wiley   2020    feb
Journal 2010    jul

.

$ awk -v flds='month year article publisher' -f tst.awk file
feb     2020    Author1_2020    Wiley
jul     2010    Author2_2010    Journal

鉴于上述方法,您可以轻松地将比较添加到 /^}/ { ... } 块内的代码,例如

if ( (f["publisher"] == "Wiley") && (f["year"] == 2020) ) {
    do whatever you like
}

或者您可以对其进行调整,以便将您的输入转换为 CSV 或 JSON 或您喜欢的任何其他输出格式。

【讨论】:

  • 我非常喜欢这个答案。我只想获取特定标签 (author = {John Ford and James Smith and ... and Tom Williams} -&gt; Williams) 的最后一个单词。我知道如何使用后续的 sed 命令执行此操作,但是如何在同一个 awk 块中实现此操作?
  • sub(/.* /,"",f["author"])
【解决方案4】:

使用 GNU edcolumn

给定文件global.bib

@article{Author1_2020,
    year = 2020,
    month = {feb},
    publisher = {Wiley},
    mama = {foo},
    papa = {bar},
}
@article{Author2_2010,
    year = 2010,
    month = {jul},
    publisher = {Journal},
    mama = {foo},
    papa = {bar},
}
@article{Author3_2010,
    year = 2010,
    month = {aug},
    publisher = {Josh},
    mama = {foo},
    papa = {bar},
}
@article{Author4_2030,
    year = 2030,
    month = {dec},
    publisher = {Jetchisel},
    mama = {foo},
    blah = {qux},
    papa = {bar},
}

ed 脚本,我们就叫它script.ed

g/./s/^@.*//\
s/^}.*//
v/^.*publisher =.*$\|^.*year =.*$\|^.*month =.*$\|^$/d
,s/^.*publisher = \|^.*year = \|^.*month = //
g/./s/}//\
s/{//\
s/,//
g/./s/$/ /
g/./;/^$/j
,s/\([^ ]*\) \([^ ]*\) \([^ ]*\)/\3 \1 \2/
g/^$/d
,p
Q

现在对文件运行ed 脚本并将其通过管道传送到带有-t 标志的列。

ed -s global.bib < script.ed | column -t

输出

Wiley      2020  feb
Journal    2010  jul
Josh       2010  aug
Jetchisel  2030  dec

简要说明。

  • 第 1 行和第 2 行,搜索整个文件 g 表示全局,将所有以 @} 开头的行替换为空行,使其为空行。

  • \ 是续行。所以第 1 行和第 2 行只有一行,用新行分隔。

  • 第 3 行,v 表示与 / /(在本例中为 publisheryearmonth 内匹配的任何内容相反)加上一个空/空白行,删除它,@ 987654342@表示删除。

  • 第 4 行,,s 也是g 的全局替代方案。删除/ / 中的所有内容,而不是删除包含它的行,只需将其删除即可。

  • 第5行到第7行也连上了,尾随\,去掉/ /里面匹配的所有,也就是{},

  • 第 8 行在文件上添加一个尾随空格。

  • 第 9 行,从文件开头开始加入一个非空行,g 表示全局,直到它遇到一个空行。

  • 第 10 行,反向引用所有字段,并按照需要的顺序打印出来。

  • 第 11 行删除所有空行。

  • 第 12 行 ,p 将所有输出打印到标准输出。

  • 第 13 行,Q 退出时即使修改了缓冲区也不会出错,如果需要就地编辑文件,请将其更改为 w

  • 您可以逐行运行 ed 脚本,只需包含由 \ 分隔的所有行及其后的下一行,因为它只是一次 ed 调用。


bash4+grepcolumn

#!/usr/bin/env bash

limit=3

while mapfile -n "$limit" -t array; (( ${#array[*]} )); do
  array=("${array[@]//[\}\{,]}")
  array=("${array[@]#*= }")
  printf '%s %s %s\n' "${array[2]}" "${array[0]}" "${array[1]}"
done < <(
  grep -E '^[[:space:]]*(publisher|year|month) = ' global.bib
) | column -t

【讨论】:

  • 哇......这肯定是一个涉及的ed 脚本。 A代表努力和坚持:)
  • @DavidC.Rankin,起初有点令人困惑,但如果你能掌握它,对我来说 awked 有点难:-)
  • 我必须学习ed,因为对我来说,恰恰相反。我看着ed,我的眼睛在我的脑海里翻滚,但是awk我通常可以应付:)
  • 您很快就会变得精通。要克服的心理障碍是使用字段和记录分隔符来确定将使用的分隔符。然后,就像ed 一样,每行只有一个规则将应用于每个输入记录(行)。幸运的是,这些规则是用类似于 C 的直接过程语法编写的。我发现 GNU Awk Users Guide 非常有用,尤其是 9.1.3 字符串操作函数。它还告诉您awk 的 GNU 扩展是什么。
  • @Jetchisel awk 语言本质上只是 C(或您喜欢的任何其他基于 Algol 的语言)的一个小子集,经过优化,仅在一个隐式的 while-read 循环中进行文本处理,该循环将每个在字段中输入行/记录。一旦你习惯了这种范式,语法就会变得微不足道,因为实际上只有十几个内置函数以及我们都知道和喜爱的常用比较运算符。令人震惊的是工具/语言的强大程度。
【解决方案5】:

使用 GNU awk:

awk 'match($0, /\s*(\S+)\s*=\s*\{?([^},]*)/, a) { r[a[1]]=a[2] }
     /^\}$/ { print r["publisher"], r["year"], r["month"] }
    ' OFS='\t' global.bib

【讨论】:

    【解决方案6】:

    这可能对你有用(GNU sed):

    sed '/^@/{:a;N;/^}/M!ba;s/.*year = \(....\).*month = {\(...\)}.*publisher = {\([^}]*\)}.*/\3\t\1\t\2/}' file
    

    收集以@ 开头和以} 开头的另一行之间的行。

    使用模式匹配提取必填字段并用制表符分隔结果。

    注意在多行正则表达式上使用M 标志,因为这些行被收集在模式空间中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-12-07
      • 2012-12-23
      • 1970-01-01
      • 2013-12-11
      • 1970-01-01
      • 2015-05-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多