【问题标题】:How to use regex to match Uppercase words that are non-consecutive duplicates of lowercase words else where in a file如何使用正则表达式匹配文件中其他位置小写单词非连续重复的大写单词
【发布时间】:2019-05-14 05:22:08
【问题描述】:

我是正则表达式的菜鸟,需要一些帮助。我对正则表达式字符类、锚点和外观有基本的了解,但事实证明这个特定的用例对我来说很难。

我正在尝试解析每月运行的脚本的输出,该脚本输出用于库存目的的用户列表。我想使用正则表达式来解析文件以匹配这些条件:

  1. 正则表达式查找作为大写单词重复的小写单词。这些大写单词不在同一行,不连续出现,可以在新行或文件中的其他位置。
  2. 我需要一个能够显示重复的大写匹配项的正则表达式
  3. 另一个删除重复大写匹配的正则表达式

这是我尝试解析的文件输出示例:

"hello","2018-11-19","unitelife"
"world","2018-11-09","unitelife"
"foo","2018-11-16","unitelife"
"bar","2018-10-05","unitelife"
"hello123","2018-09-06","unitelife"
"HELLO123","2018-11-18","unitelife"
"FOO","2018-11-20","unitelife"
"WOWMUCHHAPPY","2018-10-20","unitelife"
"suchjoy","2017-11-28","unitelife"

我正在寻找的期望匹配是:

HELLO123
FOO

我尝试了下面引用的 URL 讨论所建议的多种组合,但似乎对我没有任何用处。要么我尝试了不正确的组合,要么这个功能是不可能的。此外,大多数主题都在讨论连续的单词或字母/字符。

如果之前讨论过这个问题或者它的分类有误,我想提前道歉。请让我知道而不是投反对票,以便我可以编辑、关闭或重新分类问题以符合相应的标准。

此致,

谦虚的学生

【问题讨论】:

  • 您在什么环境下工作?
  • 抱歉,如果可能的话,我希望能够在多个环境中应用它。特别是:稍后使用 grep -E、notepad++ 和/或 python 进行 bash。
  • 谢谢,您尝试匹配的单词是否总是像示例中那样被"s 包围?
  • 是的。我试图用 \b 和 \S 设置一些基于它们的锚点,并且能够让第一列匹配但没有超过。

标签: python regex bash grep regex-lookarounds


【解决方案1】:

你可以使用模式

(?sm)^"([a-z\d]+)"(?i)(?=(?:[^\n]*\n)+?"(?=\1")(?-i)[A-Z\d]+")

https://regex101.com/r/nM3iBH/2

这个想法是,从区分大小写开始(没有i),并匹配引号内行首的小写字符串。然后,启用 case-insensitive 标志,并在行首的引号内查找相同的字符串。重新启用区分大小写,并在前瞻中再次匹配相同的字符串,只允许大写字母(和数字)。

请注意,这(与此问题的任何正则表达式算法一样)具有O(N^2) 复杂性,因为对于任何匹配项,您都需要检查整个剩余子字符串的(大写)匹配项。

另请注意,使用 "s 而不是您想的 \bs - 考虑到这种输入,使用 "s 会更精确,并且总体上会显着减少步骤。

为了减少步骤,该模式非常严格。分开:

  • (?sm)^"([a-z\d]+)" - 初始标志,捕获行首引用的小写单词
  • (?i) - 启用不区分大小写,以便将来的 \1 反向引用能够正常工作
  • (?=(?:[^\n]*\n)+?"(?=\1")(?-i)[A-Z\d]+") 大前瞻:
    • (?:[^\n]*\n)+ - 匹配非换行符,后跟换行符
    • "(?=\1") 在行首的引号内反向引用原始匹配的单词
    • (?-i) 重新启用区分大小写,以便检查大小写
    • [A-Z\d]+" - 匹配大写字母和数字,后跟 "

为了用空字符串替换大写单词行,而不是使用大前瞻,匹配组中小写单词和大写单词之间的所有内容(所以你有两组,小写单词,以及它后面直到大写单词的所有内容),然后匹配大写单词,并仅替换前两组(从而替换掉大写单词的行) :

(?sm)^("[a-z\d]+")(?i)((?:[^\n]*\n)*[^\n]*)\n(?=\1)(?-i)"[A-Z\d]+"[^\n]*

替换为

\1\2

(或您环境中的等价物)

https://regex101.com/r/nM3iBH/3

请注意,如果您有重叠的匹配项,则必须反复执行此操作,直到没有匹配项为止。

【讨论】:

  • 很棒的工作@CertainPerformance!但是,当我检查 URL 时,它看起来与小写匹配,当我尝试将大写匹配以稍后删除、替换等时。我正在尝试使用组合来反转结果。我会看看我能想出什么。
  • 哦,你的标题写着to match lower case words,我也跟着跑了。鉴于大写单词出现在小写单词之后,反过来匹配大写而不是小写会更加尴尬,因为你必须在某个地方寻找backwards 在字符串中进行匹配,并且大多数正则表达式环境不支持此类操作。将 everything 从小写部分匹配到大写部分,然后用所需的替换替换大写子字符串怎么样?
  • 我改了标题,谢谢指正。由于我是正则表达式的新手,因此我可能完全错误地处理了这一切。我记得遇到过向前看和向后看,反之亦然,但它确实开始听起来非常复杂。当然,在这一点上,我愿意接受任何建议:)
  • 好的,查看编辑,使用第二个捕获组而不是前瞻,并替换为\1\2
  • 对,这就是我说if you have overlapping matches, you'll have to do this iteratively until no matches remain的原因(同样的模式,继续执行)
【解决方案2】:

不是使用正则表达式,而是使用awks toupper() 和 tolower() 函数

$ awk -F, '{lower=tolower($1)} lower==$1 {a[$1];next} 
      toupper($1)==$1 && lower in a{print $1}' file

"HELLO123"
"FOO"

如果字段是小写的则添加到集合中,如果大写和小写在集合中打印。

这里的顺序很重要(小写应该出现在大写之前),就像你的例子一样。如果没有,需要转换为两遍版本。如果不需要,也很容易删除引号。

【讨论】:

  • 您好,谢谢@karakfa,我什至没有考虑过 awk。我也可以尝试使用它。我会及时通知你结果。
【解决方案3】:

这是一个只使用bash 的解决方案,没有正则表达式:

> cat filter.sh
#!/bin/bash

declare -A lower=()
declare -A upper=()

while IFS= read -r line; do
  eval "words=( $(tr ',' ' ' <<< "$line") )"
  for w in "${words[@]}"; do
    [[ "${w^^}" = "$w" ]] && upper["$w"]=1 || lower["$w"]=1
  done
done

for u in "${!upper[@]}"; do
  exists=${lower["${u,,}"]+foo}
  [[ -n "$exists" ]] && echo "$u"
done

我在这里使用了一些技巧。

首先,我使用关联数组来清除重复。例如,如果"HELLO123" 在文件中出现多次,则只会计算一次。

其次,我通过使用 tr 将逗号替换为空格,然后使用 eval 将字符串解析为数组来解析 CSV,从而利用单个单词始终包含在双引号。

最后,我使用[[ "${w^^}" = "$w" ]] 作为测试来检查单词是否包含所有大写字母。 ${w^^} 语法是将变量转换为大写的 bash-ism。我还在第二个循环中使用了${u,,},它将$u 转换为小写。请注意,如果您的单词混合了大写和小写字母,则将其计为小写单词。如果这不符合您的期望,您可以更改逻辑。

第一个循环只是从stdin 读取数据,将每一行拆分为单独的单词,然后将每个单词分类为大写或小写。同样,我使用关联数组,这样每个单词(无论大小写)只计算一次。

第二个循环只是循环通过 upper 关联数组 ({${!upper[@]}) 的 ,它们只是输入中遇到的所有大写单词。对于每个单词,它都会检查是否也遇到了匹配的小写单词。 ${lower["${u,,}"]+foo} 语法只是检查lower 数组中是否存在小写单词。 foo 部分只是一个任意字符串。您也可以使用barexistsabc。这是您在bash 的关联数组中检查键是否存在的方法。如果键存在于数组中,则表达式将计算为"foo",否则将计算为空字符串。这就是后续的-n 测试要检查的内容。

例子:

> cat input.txt
"hello","2018-11-19","unitelife"
"world","2018-11-09","unitelife"
"foo","2018-11-16","unitelife"
"bar","2018-10-05","unitelife"
"hello123","2018-09-06","unitelife"
"HELLO123","2018-11-18","unitelife"
"FOO","2018-11-20","unitelife"
"WOWMUCHHAPPY","2018-10-20","unitelife"
"suchjoy","2017-11-28","unitelife"

> cat input.txt | ./filter.sh
FOO
HELLO123

注意:请不要在生产代码中使用eval。由于输入中出现了意想不到的事情,它很容易受到各种滥用和事故的影响。例如,考虑一下如果您在输入中插入以下行会发生什么:

"); rm -rf *; foo=("

然后eval 将最终评估字符串"words=(); rm -rf *; foo=()"。肯定不好。我在这里只使用 eval 作为解析 CSV 的一种快速而肮脏的方法。在bash 中解析 CSV 有很多更好(并且更安全)的方法。该解决方案的重点是使用关联数组来跟踪大小写单词,同时过滤掉重复项。

编辑: 另请注意,FOOHELLO123 在输出中出现乱序。这是因为关联数组不会按照您创建它们的顺序存储键。因此,当您执行 ${!hash[@]} 时,密钥的顺序是一个废话。如果这对您来说是个问题,您可以保留一个单独的常规数组来保留顺序。

【讨论】:

  • 谢谢@Mike Holt,我将在隔离服务器上尝试 bash 方法。不过,您提出了一个很好的观点,并且可能必须找到 eval 方法的替代方法,因为 rm -rf 在生产服务器上发生的远程机会总是要避免的(几乎就像给孩子命名为“DROP TABLE students;”) .我也会尝试这个解决方案。
  • @unitelife 我的回答只是对通用算法的想法的快速而肮脏的演示。我认为 Python 更适合解析 CSV,因为它有(可能有几个)库可以这样做,而且读取输入的速度也会更快,尤其是在有很多输入的情况下。此外,Python dicts 比 bash 关联数组更易于使用。
猜你喜欢
  • 2013-12-29
  • 2014-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多