带有捕获组的正则表达式,用于由可变数量的单词组成的子字符串

Regex with capture groups for substrings made up of a variable number of words

使用以下 Bash 脚本(改编自 ):

#!/bin/bash

while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^([[:alpha:]]+)[[:space:][:punct:]]+([[:alpha:][:space:]]+)[[:space:]]([[:digit:]+[mcg|mg|g][:space:][\/0-9a-zA-Z[:space:]]*])[\[]([[:digit:]]+)[\]]([[:alpha:]]*)$ ]]
then
 printf "Ingredient: %s\n" "${BASH_REMATCH[1]}"
 printf "Brand name: %s\n" "${BASH_REMATCH[2]}"
 printf "Strength: %s\n" "${BASH_REMATCH[3]}"
 printf "Pack size: %s\n" "${BASH_REMATCH[4]}"
 printf "Form: %s\n" "${BASH_REMATCH[5]}"
fi  
done < "${1:-/dev/stdin}"

我想匹配如下行(通过标准输入或通过作为第一个参数传递的文件提供):

Calcipotriol - Daivonex Cream 50mcg/1g 30 g [1]
Candesartan cilexetil - Atacand 4mg [30] capsule
Danazol - Azol 100mg [100] 
Dexamethasone - Dexmethsone 0.5g [1] tablet

并将它们解析为 4-5 个字段。

例如,行 Calcipotriol - Daivonex Cream 50mcg/1g 30 g [1] 应按如下方式分成字段:

但是,当我 运行 我的脚本时,没有 匹配。

这是独立的正则表达式(换行只是为了便于阅读):
^([[:alpha:]]+)[[:space:][:punct:]]+([[:alpha:][:space:]]+)[[:space:]]([[:digit:]+[mcg|mg|g][:space:][\/0-9a-zA-Z[:space:]]*])[\[]([[:digit:]]+)[\]]([[:alpha:]]*)$

你能告诉我如何匹配50mcg/1g 30 g这样的字符串并在${BASH_REMATCH[4]}中捕获它吗?

与您的 一样,awk 提供更易于维护且速度更快的解决方案:

awk是最好的选择,因为你的输入本质上是字段-based,将输入分解为字段是awk 闪耀的地方。要了解 awk,请参阅系统上的 awk POSIX spec 或 运行 man awkinfo awk

为简单起见,并与样本输入一致,假设所有intra-line whitespace为spaces;如果制表符也应该匹配,则将正则表达式中的 </code> 实例替换为 <code>[[:blank:]]

awk -F' +- +|[][]' '
  { 
    name = ; sub(" +[0-9.]+(mc?)?g.*", "", name)
    strength = substr(, 1 + length(name)); sub("^ +", "", strength)
    form = ""
    if (NF > 3) { form = $NF; sub("^ +", "", form) }

    print "Ingredient:", 
    print "Brand name:", name
    print "Strength:  ", strength
    print "Pack size: ", 
    print "Form:      ", form
    print "---"
  }
' <<'EOF'
Calcipotriol - Daivonex Cream 50mcg/1g 30 g [1]
Candesartan cilexetil - Atacand 4mg [30] capsule
Danazol - Azol 100mg [100] 
Dexamethasone - Dexmethsone 0.5g [1] tablet
EOF

产量:

Ingredient: Calcipotriol
Brand name: Daivonex Cream
Strength:   50mcg/1g 30 g 
Pack size:  1
Form:       
---
Ingredient: Candesartan cilexetil
Brand name: Atacand
Strength:   4mg 
Pack size:  30
Form:       capsule
---
Ingredient: Danazol
Brand name: Azol
Strength:   100mg 
Pack size:  100
Form:       
---
Ingredient: Dexamethasone
Brand name: Dexmethsone
Strength:   0.5g 
Pack size:  1
Form:       tablet
---

这里有一个 bash 尝试的固定和简化版本:

while IFS= read -r line || [[ -n "$line" ]]; do
  if [[ "$line" =~ ^([[:alpha:]][[:alpha:][:blank:]]*[[:alpha:]])[[:blank:][:punct:]]+([[:alpha:]][[:alpha:][:blank:]]*[[:alpha:]])[[:blank:]]+([^[]+)\[([0-9]+)\][[:blank:]]*([[:alpha:]]*)$ ]]
  then    
    printf "Ingredient: %s\n" "${BASH_REMATCH[1]}"
    printf "Brand name: %s\n" "${BASH_REMATCH[2]}"
    read -r strength <<<"${BASH_REMATCH[3]}"
    printf "Strength: %s\n" "$strength"
    printf "Pack size: %s\n" "${BASH_REMATCH[4]}"
    printf "Form: %s\n" "${BASH_REMATCH[5]}"
  fi  
done < "${1:-/dev/stdin}"
  • ([[:alpha:]][[:alpha:][:blank:]]*[[:alpha:]]) 的实例用于捕获成分和品牌名称;该表达式捕获由白色 space 分隔的 letter-only 个单词的可变列表(在列表中包含一个 2 个字母的单词)。

  • 简化的正则表达式避免了 mcg / mg / g 解析困难,匹配品牌名称后的所有内容,直到以下 [ (包大小的开始)使用 [^[]+,无论它包含多少 space;因为它包括尾随的白色space,read 后来被用来 trim 那。

    • 如果您确实需要明确匹配 mcg / mg / g 以排除误报:
      • [^[]+替换为([[:digit:].]+(mcg|mg|g)[/0-9a-zA-Z[:space:]]*)
      • $BASH_REMATCH 索引 5 替换为 6,并将 4 替换为 5,因为上面出于技术原因引入了一个新的捕获组 - 请参阅解释如下。
  • 请注意如何使用 [:blank:](匹配制表符或 space)代替 [:space:],因为后者也匹配换行符,由此处没有定义。


您最初的尝试存在各种问题,其中一些问题已被Benjamin W.在问题评论中指出:

  • [mcg|mg|g] 应该是 (mcg|mg|g)(mc?)?g,因为 [mcg|mg|g] 是一个 bracket expression:在这种情况下,一个 set 个字符,其中任何一个匹配一个 单个 字符,因此实际上你匹配一个 单个 mc|g 个字符。

  • [:space:] 使用 non-ASCII fullwidth colons,Bash 无法识别为一个字符 class.

  • 本身不是问题,而是注意事项和简化机会:

    • 您混合了 [:alpha:]a-zA-Z,它们只能保证在 ASCII 范围内工作相同;要匹配外文字母,请坚持使用 [:alpha:];相反,[:digit:] 可以假设匹配 non-ASCII 个数字,因此 [0-9] 可能是更安全的选择。
    • 无需在 bash 中的 [...] 内转义 /,因为 / 不是正则表达式元字符,也不用作正则表达式 定界符 in bash.
    • [\[][\]] 来表示文字 [] 不必要地复杂;使用 \[\] 代替。
  • 主要问题是您似乎对括号表达式的工作原理有误解。例如,[[:digit:]+[mcg|mg|g][:space:][/0-9a-zA-Z[:space:]]*] 是一个 ill-constructed 单个 括号表达式,应该是多个独立的子表达式:

    • [[:digit:].]+ - 匹配 运行 个数字 and/or . 的括号表达式(也匹配 0.5g,对于实例).

    • (mcg|mg|g) - 带括号的子表达式(捕获组)使用交替 | 来匹配三个标记中的任何一个;请注意,在 bash 正则表达式 中使用 (...) 总是 创建一个捕获组,即使您只需要 precedence[=186= 的括号],因此在索引 ${BASH_REMATCH[@]}.

    • 时需要考虑到这一点
    • [/0-9a-zA-Z[:space:]]* - 匹配任何(可能为空)运行 由 /、十进制数字、ASCII 字母和 whitespace个字符.

    • 加入这些子表达式应该会匹配到一个字符串如50mcg/1g 30 g,可以如下验证:
      [[ '50mcg/1g 30 g' =~ [[:digit:].]+(mcg|mg|g)[/0-9a-zA-Z[:space:]]* ]] && echo "MATCHED: >>>${BASH_REMATCH[0]}<<<"

  • 用于可视化和调试正则表达式的出色在线工具,它们也是出色的教学工具。一个例子是 regex101.com.

    • 请注意,这些工具通常不直接支持(通常 platform-specific)在 bash 和各种 Unix 实用程序中发现的正则表达式方言,但选择 PCRE因为方言通常提供 superset.
      需要注意的是,您需要知道您的特定实用程序支持哪些子集,否则您最终可能会得到一个仅适用于在线测试器的正则表达式。

    • 可以找到 [[:digit:].]+(mcg|mg|g)[\/0-9a-zA-Z[:space:]]* 如何匹配 50mcg/1g 30 g 的演示 here

    • [=113=Here 是上面针对完整示例输入行测试的固定 bash 解决方案的完整正则表达式。