【问题标题】:shell regex: Extract pricesshell 正则表达式:提取价格
【发布时间】:2016-03-23 09:39:58
【问题描述】:

鉴于以下价格列表,我试图弄清楚如何仅规范化/提取数字。

INPUT          DESIRED_OUTPUT

CA$1399.00     1399.00
$1399.11   1399.11
$1,399.22<     1399.22
Z$1 399.33     1399.33
$1399.44#      1399.44
C$ 1399.55     1399.55
1,399.66       1399.66
1399.77        1399.77
,1399.88       1399.88
25 1399.88     1399.88
399.99          399.99
88.88 99.99      99.99 (if >2 matches on one line, only the last one matters)
.1399.88         DO NOT MATCH (not a price; too many ".")
666.000          DO NOT MATCH (not a price: too many 0's)

我认为从它们的共同点开始是个好主意:

  • 价格始终包含.NN,但绝不包含.NNN

经过进一步检查,其他规则变得明显:

  • .NN 前面必须有一个或多个 digits
  • NNN.NN 可以在 , 或简单的 digit 前面,但仅此而已。
  • .NN 之后和*N.NN 之前的任何内容都标志着比赛的结束。
  • 最后,正则表达式需要考虑1,399.66 (1399.66) 之类的逗号,以确定它是否是价格,然后去掉它们。 1, 399.66,例如不等于1399.66:应该是399.66

我正在寻找sedgrepawk 寻找可移植且高效的解决方案。我应该如何解决这个问题?

我找到了similar question,但我不知道如何使用sed 尝试以下正则表达式:

^\d+(,\d{1,2})?$

编辑:是的,我的输入格式可能有点奇怪,因为它是抓取页面连接的结果。

【问题讨论】:

  • unix.stackexchange.com/a/138937推荐grep -o。您的输入格式非常尴尬 - Z$1 399.33 应该匹配空格前的数字,但 25 1399.88 不应该匹配空格前的数字?为什么——根据什么规则可以对这种区别进行编码?程序和数据的其余部分是什么样的 - 你可以进行清理运行或多次运行吗?
  • 4812 等位置的空格/逗号 (RTL)。是可以接受的,所以1 399.881 333 399.881 133 333 399.88 都可以。匹配Z$1 399.33应该没什么大不了的;数字一出现 $ 就结束(再次读取 RTL)。
  • 棘手的情况:.1399.98不能匹配,而1 399.98对应1399.98。但是那么.1 399.98 呢?那么空间是否很大,以至于匹配且价格为399.98?我认为要求应该是通过提取.1 作为标记来解决案例,其中尾随空格终止分数。下一个数字标记是399.98:好价钱。
  • 您使用什么严格可移植的 Unix 工具来抓取这些数据?
  • 好点。人类常识告诉我们,.1 399.98 独立存在就是1399.98,但在其他情况下,这样的宽松规则可能会导致误报,这就是我拒绝.1399.88 的原因。我不确定如何解决这个问题,但.1 399.98 无论如何都不太可能。 @Kaz 卷曲。我使用卷曲。

标签: regex shell awk sed grep


【解决方案1】:

您可以使用以下 shell 脚本:

#/bin/sh
grep -v '\.\d\+\.' | # get rid of lines with multiple dots within the same number
grep -v '\.\d\d\d\+' | # get rid of lines with more than 2 digits after .
sed -e 's/\(.*\.[0-9][0-9]\).*$/\1/' | # remove anything after last .NN
sed -e 's/^.* \([0-9][0-9][0-9][0-9]\)\./\1./' | # "* NNNN." => "NNNN."
sed -e 's/^.* \([0-9][0-9]\)\./\1./' | # "* NN." => "NN."
sed -e 's/^.* \([0-9]\)\./\1./' | # "* N." => "N."
sed -e 's/^\(.*\)[ ,]\(\([0-9]\)\{3,\}\)\./\1\2./g' | # "*,NNN." or "* NNN." => "*NNN."
sed -e 's/^\(.*\)[ ,]\(\([0-9]\)\{6,\}\)\./\1\2./g' | # "*,NNNNNN." or "* NNNNNN." => "*NNNNNN."
sed -e 's/^\(.*\)[ ,]\(\([0-9]\)\{9,\}\)\./\1\2./g' | # "*,NNNNNNNNN." or "* NNNNNNNNN." => "*NNNNNNNNN."
grep -o '\d\+\.\d\d' # print only the price

如果数字由空格或, 以 3 位为一组,则此解决方案在 . 之前最多可工作 9 位。如果您需要提取更大的价格,只需添加更多行,将正则表达式中的数字增加 3。;-)

将其放入名为extract_prices 的文件中,使其可执行(chmod +x extract_prices)并运行它:./extract_prices &lt; my_list.txt

使用以下输入在 OS X 上测试:

CA$1399.00
&#36;1399.11
$1,399.22<
Z$1 399.33
Z$12 777 666.34   # <-- additonal monster price
$1399.44#
C$ 1399.55
1,399.66
1399.77
,1399.88
25 1399.88
399.99
88.88 99.99
.1399.88
666.000

生成以下输出:

1399.00
1399.11
1399.22
1399.33
12777666.34
1399.44
1399.55
1399.66
1399.77
1399.88
1399.88
399.99
99.99

【讨论】:

  • 不是显式删除尾随 #&lt;,您不能简单地删除紧跟两个数字的最后一个实例后面的任何内容,前面有一个点 (.NN)?
  • 是的。这部分并不难。
  • 欢迎来到 Stack Overflow,顺便说一句 :)
  • 好的,有办法了!哈哈。哈克,但它的工作原理。谢谢!终于尝试获得一些声誉...大声笑
  • 为什么 echo "$1,399.22" | extract_prices 会产生不正确的 399.22 ,而 echo '$1,399.22' | extract_prices 会产生正确的 1399.22
【解决方案2】:

一种带有 awk 的解决方案,它拆分所有非数字或小数点的字符,并打印与价格匹配的最后一个字段。前面的 sed 脚本处理异常情况 #3,其中我们有一个空格而不是逗号来标记千位。

sed -e 's/  / x /g; :a; s/\(\$[1-9][0-9]*\) /\1/; ta' | awk -F '[^0-9.]' -v p='[0-9]+\\.[0-9][0-9]' '$0 ~ p { gsub(/,/, ""); for (i=NF; i>0; i--) if ($i ~ "^" p "$") { print $i; next } }'

注意事项:

1) sed 脚本使用测试进行迭代;因此,它可以处理数百万、数十亿等。
2) sed 脚本还处理多个空格条件,使得 $1[ ][ ]1000.00 最终不会变为 $11000.00。
3) 逗号被简单地删除/忽略...如果数字逗号分隔存在问题,可以通过删除 awk 脚本中的 gsub 并修复前导 sed 脚本

这是一个更复杂的版本,它基于注释 #3 中的想法,仅当空格或逗号位于千位分隔符处时才将逗号和空格作为数字的一部分。

sed -e ':a; s/\(\$[1-9][0-9]*\) \([0-9][0-9][0-9][ .]\)/\1\2/; ta; :b; s/\([1-9][0-9]*\),\([0-9][0-9][0-9][,.]\)/\1\2/; tb;' | awk -F '[^0-9.]' -v p='[0-9]+\\.[0-9][0-9]' '$0 ~ p { for (i=NF; i>0; i--) if ($i ~ "^" p "$") { print $i; next } }'

如果每一行的成功机会都很高,那么去掉“p”将使脚本更有效率。

sed -e ':a; s/\(\$[1-9][0-9]*\) \([0-9][0-9][0-9][ .]\)/\1\2/; ta; :b; s/\([1-9][0-9]*\),\([0-9][0-9][0-9][,.]\)/\1\2/; tb;' | awk -F '[^0-9.]' '{ for (i=NF; i>0; i--) if ($i ~ /^[0-9]+\.[0-9][0-9]$/) { print $i; next } }'

最后,为了安全起见,我们可以检查 sed 过滤器,以确保在我们进行替换之前我们有一个有效的空格或逗号分隔的数字。

sed -e ':a; /\$[1-9][0-9]\?[0-9]\?\( [0-9][0-9][0-9]\)\+\.[0-9][0-9]/ s/\(\$[1-9][0-9]*\) \([0-9][0-9][0-9][ .]\)/\1\2/; ta; :b; /[1-9][0-9]\?[0-9]\?\(,[0-9][0-9][0-9]\)\+\.[0-9][0-9]/ s/\([1-9][0-9]*\),\([0-9][0-9][0-9][,.]\)/\1\2/; tb;' | awk -F '[^0-9.]' '{ for (i=NF; i>0; i--) if ($i ~ /^[0-9]+\.[0-9][0-9]$/) { print $i; next } }'

【讨论】:

    【解决方案3】:

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

     sed -r '/\n/!s/([^0-9]*\b(([0-9])[ ,]([0-9]{3})|([0-9]+))(\.[0-9]{2})\b)+/\n\3\4\5\6\n/;/^[0-9]+\.[0-9]{2}\b/P;D' file
    

    这适用于提供的数据,但有些规范有点粗略。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-05-01
      • 2018-06-21
      • 2010-12-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-01
      相关资源
      最近更新 更多