Shell正则表达式:提取价格

4

在以下价格列表中,我正在尝试找出如何归一化/提取仅数字的方法。

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前缀,但不能有其他东西。
  • *N.NN之前的任何跟随.NN的内容标志着匹配的结尾。
  • 最后,正则表达式需要考虑像1,399.661399.66)这样的逗号,以确定它是否是价格,然后剥离它们。例如1, 399.66并不等于1399.66:它应该是399.66

我正在寻找使用sedgrepawk作为便携式和高效解决方案。我该如何处理这个问题?

我找到了一个类似的问题,但我不知道如何使用sed尝试以下正则表达式:

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

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


说明:本段内容提到了输入格式问题,因为其来源是多个爬取页面的拼接。


2
请参考http://unix.stackexchange.com/a/138937,建议使用`grep -o。您的输入格式非常尴尬 - Z$1 399.33应该匹配空格前面的数字,但是25 1399.88`不应该匹配空格前面的数字?为什么 - 可以用什么规则编码这种区别?程序和数据的其余部分是什么样子的 - 您可以进行清理运行或多次运行吗? - TessellatingHeckler
在RTL(从右到左)的情况下,第4、8、12等位置上的空格/逗号是可以接受的。因此,“1 399.88”、“1 333 399.88”和“1 133 333 399.88”都是可以的。匹配“Z$1 399.33”不应该是一个大问题;数字在$出现时就结束了(再次从右到左阅读)。 - octosquidopus
棘手的情况:.1399.98 不应匹配,而 1 399.98 对应于 1399.98。但是对于 .1 399.98 呢?空格是否很重要,以便匹配并且价格为 399.98?我认为要求应该是通过提取 .1 作为令牌来解决此问题,其中尾随空格终止小数部分。下一个数字令牌是 399.98:好价钱。 - Kaz
好的观点。人类常识告诉我们,.1 399.98单独站着是1399.98,但在其他情况下,这样宽松的规则可能会导致误报,这就是为什么我一开始拒绝了.1399.88。我不确定如何解决这个问题,但.1 399.98也相当不太可能出现。@Kaz curl。我使用curl。 - octosquidopus
我从中得到的信息是最简单的方法是:1. 反转行。2.查找 nn.nnn,nnn,...,nn,查找逗号或空格。例如:\d\d\.((\d){3}(, ))+\d+ ? 它似乎更容易倒着运行。 - TessellatingHeckler
显示剩余2条评论
3个回答

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

对于以空格或,分隔的数字,每三位数字为一组,此解决方案在小数点前9位数字范围内有效。如果您需要提取更大的价格,请添加更多行,并将正则表达式中的数字增加3。;-)

将其放入名为extract_prices的文件中,使其可执行(chmod +x extract_prices)并运行它:./extract_prices < 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

你可以尝试直接剥离最后一个由点号(.)和两个数字(NN)组成的实例之后的所有内容,而不是显式地删除尾随的“#”和“<”吗? - octosquidopus
真的。那部分并不难。 - Patrik
欢迎来到 Stack Overflow,顺便说一句 :) - octosquidopus
好的,找到了解决方案!哈哈。有点取巧,但是它有效。谢谢!终于想要获得一些声誉...哈哈 - Patrik
为什么 echo "$1,399.22" | extract_prices 会产生错误的 399.22,而 echo '$1,399.22' | extract_prices 则会产生正确的 1399.22 - octosquidopus
@octosquidopus 这是因为 shell 在双引号字符串中执行变量扩展。$1 将是脚本或函数给出的第一个参数。在这种情况下,没有给出参数,所以我猜它会在 extract_prices 读取之前将字符串转换为 ",399.22" - Patrik

0
一个使用awk的解决方案,它会在所有不是数字或小数点的字符上进行分割,并打印出最后一个匹配价格的字段。前面的sed脚本处理了第三种异常情况,即我们在千位上有一个空格而不是逗号的情况。
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)逗号只是被剥离/忽略...如果有关于数字逗号分隔的问题,可以通过在前导sed脚本中消除gsub并修复过滤器来解决问题。

这里是一个更复杂的版本,它建立在注释#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 } }'

0
这可能适用于您(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

这个程序可以处理提供的数据,但是一些规格有点模糊。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接