【问题标题】:Slow parsing with regular expression正则表达式解析慢
【发布时间】:2011-05-27 17:14:10
【问题描述】:

我有以下用于数据验证的正则表达式:

lexer = /(?:
      (.{18}|(?:.*)(?=\s\S{2,})|(?:[^\s+]\s){1,})\s*
      (.{18}|(?:.*)(?=\s\S{2,})|(?:[^\s+]\s){1,})\s*
      (?:\s+([A-Za-z][A-Za-z0-9]{2}(?=\s))|(\s+))\s*
      (Z(?:RO[A-DHJ]|EQ[A-C]|HIB|PRO|PRP|RMA)|H(?:IB[2E]|ALB)|F(?:ER[2T]|LUP2|ST4Q))\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\s+\d{10}|\s+)\s*
      (\d{6})\s*
      (.*)(?=((?:\d{2}\/){2}\d{4}))\s*
      ((?:\d{2}\/){2}\d{4})\s*
      (\S+)
    )/x

问题是我必须遍历一个有 10000 行(平均)的文件,使用正则表达式执行验证,导致解析应用程序速度缓慢。

  filename = File.new(@file, "r")
  filename.each_line.with_index do |line, index|
    next if index < INFO_AT + 1

    lexer = /(?:
      (.{18}|(?:.*)(?=\s\S{2,})|(?:[^\s+]\s){1,})\s*
      (.{18}|(?:.*)(?=\s\S{2,})|(?:[^\s+]\s){1,})\s*
      (?:\s+([A-Za-z][A-Za-z0-9]{2}(?=\s))|(\s+))\s*
      (Z(?:RO[A-DHJ]|EQ[A-C]|HIB|PRO|PRP|RMA)|H(?:IB[2E]|ALB)|F(?:ER[2T]|LUP2|ST4Q))\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\S+)\s*
      (\s+\d{10}|\s+)\s*
      (\d{6})\s*
      (.*)(?=((?:\d{2}\/){2}\d{4}))\s*
      ((?:\d{2}\/){2}\d{4})\s*
      (\S+)
    )/x

    m = lexer.match(line)
    begin
      if (m) then ...

编辑 在这里你可以找到一些我需要解析的行:File

编辑 II @迈克R

我正在解析一个每行包含 25 列的文件,每列可能有自己的验证方式。它可以是空格或完整的字符集。

  • 需要进行验证,因为我必须删除与该部分不匹配的行。
  • 可能没有必要
  • 这是必要的

我不相信它的表达式构造不好,它使用的前瞻,也许在我重复代码的部分(我只是不记得捕获组索引 \1...\n,如果这个就是你的意思!)我也相信这里正在发生灾难性的回溯。

如果您看到该文件,也许您会明白我为什么要这样做!让我们以第一列为例。我必须匹配“零件编号”,并且我没有任何关于如何执行此操作的规则,例如:

  • 123456789
  • 1 555 989
  • 0123456789123456789

简单的 \S+ 或 (\S+\s){1, } 都不能解决这个问题,因为我不会保证数据的完整性。

泰!

有什么改进、建议吗?

~埃德奎尼翁

【问题讨论】:

  • 嗯...写一个真正的解析器,也许吧?那个正则表达式是一个完全不可读的怪物。
  • 哇哇哇。怎么回事?为什么不是所有(\S+)\s* 都折叠成(?:(\S+)\s*)+
  • 那些(\S+)\s* 肯定应该是(\S+)\s+。这将为您节省很多痛苦。但我认为这不是最好的方法。
  • @Eder:你能举一些你想要匹配的例子吗?
  • @rockerest: "(\S+)\s*" 这真的很有必要,因为我必须捕获这么多的匹配项

标签: ruby regex parsing jruby token


【解决方案1】:
  1. 正则表达式在循环中不会改变,因此将lexer = 赋值拉出循环。 ("循环不变代码运动。")

  2. 我不确定这会更快还是更慢,但重复的子表达式可能只是一个外部重复组。

  3. 所以这很明显,我敢肯定,但编写一个真正的解析器可能是有意义的。 Ruby 有一个名为 Treetop 的高级 PEG 解析器生成器。传统方法要么是 Lex + Yacc(例如 flex + bison),要么只是一个实现扫描器和递归下降解析器的 C 程序。是的,老派,但是很容易编写,并且是另一个 CRUD 程序的有趣变化。

  4. 我同意其他响应者的观点,即该表达式不必要地冗余、复杂,并且可能应该至少分成两个单独的更小的表达式。两个表达式可能会很有趣,因为它们都可以相反地锚定。

【讨论】:

  • * 将代码放在循环内是错误的,将正则表达式移到循环外没有任何区别。 * 这对编辑来说是必要的,而且更具可读性 * 这是个好主意,我只是想试试看!
【解决方案2】:

您可能会收到catastrophic backtracking

也就是说,但不仅是因为你贪婪的\S+\s* 序列。它们也可能无效,因为:

(\S+)\s*
(\S+)\s*
(\S+)\s*
(\S+)\s*

将匹配'abcd'(经过痛苦量的回溯)。

另外,正如另一个答案中所指出的,寻求从循环中编译该正则表达式定义。

【讨论】:

    【解决方案3】:

    您的文件是具有固定宽度字段的格式。 Ruby 有一个名为unpack 的字符串方法,专门用于解析这种类型的文件。

    field_widths = [19,41,14,11,11,11,11,11,11,11] #etc
    field_pattern = "A#{fields.join('A')}"
    

    然后在你的线路循环中:

    row = line.unpack(field_pattern)
    

    现在您有了一个包含每个字段内容的数组(行)。然后,您可以对每个应用正则表达式进行验证。这更快、更易于管理,并且还允许显示特定于字段的错误消息。

    【讨论】:

      【解决方案4】:

      您实际上在解析中寻找什么?

      据我所知,只有少数几个重要元素是您要在该行中寻找的。​​p>

      • 您正在寻找以 Z 开头的字母数字。
        (Z(?:RO[A-DHJ]|EQ[A-C]|HIB|PRO|PRP|RMA)|H(?:IB[2E]|ALB)|F(?:ER[2T]|LUP2|ST4Q))\s*

      • 您正在寻找一个 6 位数字
        (\d{6})\s*

      • 您正在寻找约会对象
        (?=((?:\d{2}\/){2}\d{4}))\s*
        ((?:\d{2}\/){2}\d{4})\s*

      (请注意,最后一个日期表达式是一个例子,说明这是多么糟糕的构造。它有一个日期的前瞻,紧随其后的是该完全相同日期的非捕获匹配。因此,前瞻是没有意义的。请参阅有关灾难性回溯的另一个答案,这无疑是在这里发生的。)

      该表达式中的几乎所有其他内容都可以匹配任何我发现很难相信他们正在表达任何有意义形式的规则的内容。 实际数据并没有那么大的变化,或者它会随着可以用一个(或多个正则表达式)表达的某些精确规则而变化。

      所以,彻底抛弃表达式,停止尝试一次完成所有操作。 单独处理(根据需要在 \s 上拆分行)并将其分解为几个小的正则表达式这实际上符合您真正需要验证的内容。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-10-17
        • 1970-01-01
        • 2011-03-20
        • 2012-07-08
        • 2010-10-23
        • 2017-07-10
        • 1970-01-01
        相关资源
        最近更新 更多