Регулярное выражение с группами захвата для подстрок, состоящих из переменного числа слов

С помощью следующего скрипта 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] следует разбить на поля следующим образом:

  • Calcipotriol (ингредиент)
  • Daivonex Cream (название бренда)
  • 50mcg/1g 30 g (прочность)
  • 1 (размер упаковки)
  • (пусто, потому что текст не следует [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]}?

1 ответ

Решение

Как и в случае с вашим предыдущим вопросом, awk предлагает более легкое в обслуживании и гораздо более быстрое решение:

awk это лучший выбор, потому что ваш ввод по существу основан на полях, а разбивка ввода на поля - это то, где awk светит. Узнать о awk см awk POSIX спецификация или запуск man awk или же info awk в вашей системе.

Для простоты и в соответствии с образцом ввода все пробелы внутри строки предполагаются пробелами; заменить случаи в регулярных выражениях с [[:blank:]] если вкладки тоже должны совпадать.

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

    print "Ingredient:", $1
    print "Brand name:", name
    print "Strength:  ", strength
    print "Pack size: ", $3
    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:]]) используются для захвата ингредиента и торговой марки; выражение захватывает переменный список буквенных слов, разделенных пробелами (содержащий в списке одно 2-буквенное слово).

  • Упрощенное регулярное выражение избегает mcg / mg / g Сложность разбора путем сопоставления всего после имени бренда до следующего [ (начало размера пакета) с помощью [^[]+ сколько бы мест в нем не было; так как это включает в себя конечные пробелы, read позже используется, чтобы урезать это.

    • Если вам нужно соответствовать mcg / mg / g явно, чтобы исключить ложные срабатывания:
      • замещать [^[]+ с ([[:digit:].]+(mcg|mg|g)[/0-9a-zA-Z[:space:]]*)
      • замещать $BASH_REMATCH индекс 5 с 6, а также 4 с 5 потому что вышеизложенное вводит новую группу захвата по техническим причинам - см. пояснение ниже.
  • Обратите внимание, как [:blank:] (соответствие табуляции или пробела) используется вместо [:space:] потому что последний также соответствует символам новой строки, которых по определению здесь нет.


Существуют различные проблемы с вашей первоначальной попыткой, некоторые из которых уже были отмечены Benjamin W.. в комментариях к вопросу:

  • [mcg|mg|g] должно быть (mcg|mg|g) или же (mc?)?g, так как [mcg|mg|g] является выражением в скобках: в данном случае, набор символов, любой из которых соответствует одному символу, так что в действительности вы соответствуете одному m, c, |, или же g персонаж.

  • [:space:] использует двоичные двоеточия без ASCII, которые Bash не распознает как часть класса символов.

  • Не проблемы как таковые, но предостережения и возможности упрощения:

    • Вы смешиваете [:alpha:] а также a-zA-Z которые гарантированно работают одинаково в диапазоне ASCII; чтобы соответствовать иностранному письму, придерживайтесь [:alpha:]; наоборот, [:digit:] может гипотетически соответствовать не-ASCII цифрам, поэтому [0-9] может быть более безопасный выбор.
    • Не нужно убегать / внутри [...] в bash, так как / не является метасимволом регулярных выражений и также не используется в качестве разделителя регулярных выражений в bash,
    • [\[] а также [\]] представлять буквально [ а также ] излишне сложно; использование \[ а также \] вместо.
  • Основная проблема заключается в том, что вы, похоже, неправильно понимаете, как работают выражения в скобках. Например, [[:digit:]+[mcg|mg|g][:space:][/0-9a-zA-Z[:space:]]*] является неправильно построенным выражением с одной скобкой, которое должно быть несколькими независимыми подвыражениями:

    • [[:digit:].]+ - выражение в скобках для соответствия серии цифр и / или . (также соответствовать 0.5g, например).

    • (mcg|mg|g) - заключенное в скобки подвыражение (группа захвата) с использованием чередования | соответствовать любому из трех токенов; обратите внимание, что с помощью (...) в bash regex неизменно создает группу захвата, даже если вам нужны только круглые скобки для приоритета, поэтому вы должны учитывать это при индексации в ${BASH_REMATCH[@]},

    • [/0-9a-zA-Z[:space:]]* - другое выражение в скобках, которое соответствует любой (потенциально пустой) серии символов, состоящей из /, десятичные цифры, буквы ASCII и пробельные символы.

    • Присоединение к этим подвыражениям должно соответствовать строке, такой как 50mcg/1g 30 g, который вы можете проверить следующим образом:
      [[ '50mcg/1g 30 g' =~ [[:digit:].]+(mcg|mg|g)[/0-9a-zA-Z[:space:]]* ]] && echo "MATCHED: >>>${BASH_REMATCH[0]}<<<"

  • Есть отличные онлайн-инструменты для визуализации и отладки регулярных выражений, которые также являются отличными инструментами обучения. Одним из примеров является https://regex101.com/.

    • Обратите внимание, что эти инструменты обычно не поддерживают (часто для платформы) диалекты регулярных выражений, найденные в bash и различные утилиты Unix, но выбирая PCRE как диалект обычно обеспечивает надмножество.
      Предупреждение состоит в том, что вам нужно знать, какое подмножество поддерживает ваша конкретная утилита, в противном случае вы можете получить регулярное выражение, которое работает только в онлайн-тестере.

    • Демонстрация того, как [[:digit:].]+(mcg|mg|g)[\/0-9a-zA-Z[:space:]]* Матчи 50mcg/1g 30 g можно найти здесь.

    • Вот полное регулярное выражение из фиксированного bash Решение, приведенное выше, проверено на полной линии ввода образца.

Другие вопросы по тегам