【问题标题】:Can this regex be further optimized?这个正则表达式可以进一步优化吗?
【发布时间】:2011-10-30 05:34:52
【问题描述】:

我编写了这个正则表达式来解析 srt 文件中的条目。

(?s)^\d++\s{1,2}(.{12}) --> (.{12})\s{1,2}(.+)\r?$

我不知道这是否重要,但这是使用 Scala 编程语言完成的(Java 引擎,但是是文字字符串,因此我不必加倍反斜杠)。

之所以使用s{1,2},是因为有些文件只有换行符\n,而其他文件则有换行符和回车符\n\r 第一个(?s)启用DOTALL模式,这样第三个捕获组也可以匹配换行符。

我的程序基本上打破了使用\n\r?\n 作为分隔符的 srt 文件,并使用 Scala 良好的模式匹配功能来读取每个条目以进行进一步处理:

val EntryRegex = """(?s)^\d++\s{1,2}(.{12}) --> (.{12})\s{1,2}(.+)\r?$""".r

def apply(string: String): Entry = string match {
  case EntryRegex(start, end, text) => Entry(0, timeFormat.parse(start),
    timeFormat.parse(end), text);
}

示例条目:

一行:

1073
01:46:43,024 --> 01:46:45,015
I am your father.

两行:

160
00:20:16,400 --> 00:20:19,312
<i>Help me, Obi-Wan Kenobi.
You're my only hope.</i>

问题是,分析器告诉我,这种解析方法是迄今为止我的应用程序中最耗时的操作(它会进行密集的时间数学运算,甚至可以比读取和解析文件快几倍)条目)。

那么任何正则表达式向导都可以帮助我优化它吗?或者也许我应该牺牲正则表达式/模式匹配的简洁性并尝试一种老式的java.util.Scanner 方法?

干杯,

【问题讨论】:

    标签: java regex optimization scala


    【解决方案1】:
    (?s)^\d++\s{1,2}(.{12}) --> (.{12})\s{1,2}(.+)\r?$
    

    在 Java 中,$ 表示输入的结束或紧接在输入结束之前的换行符的开始。 \z 表示输入的明确结束,所以如果这也是 Scala 中的语义,那么 \r?$ 是多余的,$ 也可以。如果你真的只想要最后一个 CR 而不是 CRLF,那么\r?\z 可能会更好。

    (?s) 也应该使(.+)\r? 变得多余,因为+ 是贪婪的,. 应该始终扩展以包含\r。如果您不希望 \r 包含在第三个捕获组中,则使匹配变得惰性:(.+?) 而不是 (.+)

    也许

    (?s)^\d++\s\s?(.{12}) --> (.{12})\s\s?(.+?)\r?\z
    

    将在 JVM &| 中运行的正则表达式的其他高性能替代方案CLR 包括JavaCCANTLR。有关仅 Scala 的解决方案,请参阅http://jim-mcbeath.blogspot.com/2008/09/scala-parser-combinators.html

    【讨论】:

    • +1 用于提及非正则表达式选项,以及最后关于 Scala 的出色链接。
    • ++ 有效;这是possessive quantifier,如果有的话,他应该使用更多。 :D
    • 迈克,谢谢你的建议。我尝试了你的正则表达式,但在我有缺陷的微基准测试中它有点慢(对于 10000 次运行,我的正则表达式平均每个文件 26.3 毫秒,你的 31.4 毫秒。我运行了几次基准测试以确保)。至于 Parser Combinators 和其他构建 AST 的编译器工具,这个程序可能会读取巨大的文件,所以它更像是一个读取流 -> 解析 -> 转储 -> 丢弃 cicle。 Jim 实际上是在帮助我处理 scala 用户列表,当我告诉他这个要求时,他也得出结论,解析器组合器不是完成这项工作的最佳工具。
    • @Anthony,很有趣。在您与 Jim 的讨论中,您是特别讨论了“解析器组合器”还是笼统地讨论了“解析器生成器”? ANTLR 和朋友们做了很多前期工作来将语法简化为状态机可以有效处理的东西。正则表达式可以这样实现,但很多不是因为他们想要支持 perl5 正则表达式的非常规行为。因此,对于具有常规(或 ANTLR 中的 CF?)词法语法的事物,解析器生成器可以比普通的正则表达式库做得更好。
    • @Alan,感谢您的解释。如果我理解正确,\d++\s{1,2} 在语义上不应与\d+\s{1,2} 有任何不同,因为\d+{1,2} 中没有字符串具有非空后缀,它是\s{1,2} 中任何字符串的非空前缀,但显然只有基准测试可以确定限定符是否可以改进。
    【解决方案2】:

    我并不乐观,但这里有两件事可以尝试:

    1. 您可以将(?s) 移动到您需要它之前。
    2. 删除 \r?$ 并为文本 .+ 使用贪婪的 .++

      ^\d++\s{1,2}(.{12}) --> (.{12})\s{1,2}(?s)(.++)$

    要真正获得良好的性能,我会重构代码和正则表达式以使用findAllIn。当前代码正在为文件中的每个 Entry 执行正则表达式。我想单一的findAllIn 正则表达式会表现得更好......但也许不会......

    【讨论】:

    • 我非常确定所有格量词不会起作用:D...嗯,它确实起作用了,并且将平均时间进一步提高了大约 5%(很难测量,但我确信它是更快,平均约为 25 毫秒)。关于 findAllin,你知道它的迭代器是惰性的还是严格的?我不想将整个文件读入内存,因为它们可能非常大。
    • 您现在如何拆分文件?您不是通过将其放入列表来将其读入内存吗?无论如何,一个好的正则表达式引擎应该能够比单独拆分和匹配更有效地匹配。
    • 我正在使用 Scala 技巧将 Java 扫描器(\n\r?\n 分隔符)隐藏在惰性迭代器后面。然后我将每个条目映射到一个对象,处理,将条目发送到输出文件并丢弃。懒惰 + 丢弃最后一步的组合保证了不会构建任何列表(实际上,在我能够解决应用程序中的每个默认严格行为之前,我遇到了几个 OutOfMemoryErrors,甚至是一个 GC 错误)。如果您查看我在@Mike answer 上发布的链接,您可以关注整个讨论)。
    • 无论如何,我接受你的答案,因为它是更好地优化正则表达式的答案:D。
    • @Anthony Accioly 与使用 BufferedReader 并自行解析相比,扫描仪的性能明显更高。请在此处查看this post。由于该格式非常易于解析,因此我会尝试一种没有 RE 开销作为基线的格式。
    【解决方案3】:

    看看这个:

    (?m)^\d++\r?+\n(.{12}) --> (.{12})\r?+\n(.++(?>\r?+\n.++)*+)$
    

    这个正则表达式匹配一个完整的 .srt 文件条目就地。您不必先在换行符处拆分内容;这是对资源的巨大浪费。

    正则表达式利用了这样一个事实,即只有一个行分隔符(\n\r\n)分隔条目中的行(多个行分隔符用于将条目彼此分隔)。使用 \r?+\n 而不是 \s{1,2} 意味着当您只想匹配一个时,永远不会意外匹配两个行分隔符 (\n\n)。

    这样,您也不必在(?s) 模式下依赖.。 @Jacob 对此是正确的:它并没有真正帮助你,它正在扼杀你的表现。但是(?m) 模式对正确性和性能很有帮助。

    你提到了java.util.Scanner;这个正则表达式可以很好地与findWithinHorizon(0) 一起使用。但是,如果 Scala 不提供一种很好的惯用方式来使用它,我会感到惊讶。

    【讨论】:

    • 艾伦,非常感谢您提供的替代解决方案。我忘记在我原来的问题中提到我真的不需要一次阅读整个文件。另外,我确实需要时间。不过,由于捕获组内嵌套的非捕获组具有疯狂的正则表达式技能,我仍在投票。这就是将正则表达式向导与我们其他人区分开来的那种东西:D。
    • 您的意思是您需要捕获代表时间的.{12} 部分?我没有故意放弃这些括号,我只是专注于性能而不是副作用。我将它们重新添加了。如果您只是将正则表达式应用于单个条目,我看不出性能有那么大 - 但无论如何我都会避免 DOTALL 模式。
    • 艾伦,我觉得我表达得不好。我的应用程序在这种方法中花费了超过 70% 的时间,这就是我关注它的原因。我没有一次读取整个文件的原因是因为它可能很大(千兆字节),所以我只是一次读取一个条目,解析,处理,写入输出并丢弃。
    • 但是该方法不仅仅是正则表达式匹配。在这两个对timeFormat.parse() 的调用中花费了多少时间?
    • 时间比看起来要短。好的、陈旧的和冗长的java.util.Scanner 实际上会将时间减半(并且仍然使用解析方法)......但我想这就是正则表达式的工作方式。简明扼要的表现。看起来很公平。
    【解决方案4】:

    我不会使用java.util.Scanner 甚至字符串。只要您可以假设文件的 UTF-8 编码(或缺少 unicode),您所做的一切都将在字节流上完美运行。您应该能够将速度至少提高 5 倍。


    编辑:这只是对字节和索引的大量低级摆弄。这是基于我之前做过的事情的一些东西,这似乎快了 2 到 5 倍,具体取决于文件大小、缓存等。我没有在这里进行日期解析,只是返回字符串,我假设文件足够小以适合单个内存块(即

    import java.io._
    abstract class Entry {
      def isDefined: Boolean
      def date1: String
      def date2: String
      def text: String
    }
    case class ValidEntry(date1: String, date2: String, text: String) extends Entry {
      def isDefined = true
    }
    object NoEntry extends Entry {
      def isDefined = false
      def date1 = ""
      def date2 = ""
      def text = ""
    }
    
    final class Seeker(f: File) {
      private val buffer = {
        val buf = new Array[Byte](f.length.toInt)
        val fis = new FileInputStream(f)
        fis.read(buf)
        fis.close()
        buf
      }
      private var i = 0
      private var d1,d2 = 0
      private var txt,n = 0
      def isDig(b: Byte) = ('0':Byte) <= b && ('9':Byte) >= b
      def nextNL() {
        while (i < buffer.length && buffer(i) != '\n') i += 1
        i += 1
        if (i < buffer.length && buffer(i) == '\r') i += 1
      }
      def digits() = {
        val zero = i
        while (i < buffer.length && isDig(buffer(i))) i += 1
        if (i==zero || i >= buffer.length || buffer(i) != '\n') {
          nextNL()
          false
        }
        else {
          nextNL()
          true
        }
      }
      def dates(): Boolean = {
        if (i+30 >= buffer.length) {
          i = buffer.length
          false
        }
        else {
          d1 = i
          while (i < d1+12 && buffer(i) != '\n') i += 1
          if (i < d1+12 || buffer(i)!=' ' || buffer(i+1)!='-' || buffer(i+2)!='-' || buffer(i+3)!='>' || buffer(i+4)!=' ') {
            nextNL()
            false
          }
          else {
            i += 5
            d2 = i
            while (i < d2+12 && buffer(i) != '\n') i += 1
            if (i < d2+12 || buffer(i) != '\n') {
              nextNL()
              false
            }
            else {
              nextNL()
              true
            }
          }
        }
      }
      def gatherText() {
        txt = i
        while (i < buffer.length && buffer(i) != '\n') {
          i += 1
          nextNL()
        }
        n = i-txt
        nextNL()
      }
      def getNext: Entry = {
        while (i < buffer.length) {
          if (digits()) {
            if (dates()) {
              gatherText()
              return ValidEntry(new String(buffer,d1,12), new String(buffer,d2,12), new String(buffer,txt,n))
            }
          }
        }
        return NoEntry
      }
    }
    

    既然您看到了,您不高兴正则表达式解决方案的编码速度如此之快吗?

    【讨论】:

    • 非常感谢,但我真的很想从字节流中“解析”...你介意分享一些代码吗?
    • 哇。那是一些非常低级的解析:)。但实际上,我想这是优化意味着原始的那种情况。出于学习目的,我将尝试深入研究。只是一个简单的问题,为了避免一次加载整个文件,我正在考虑使用 BufferedStream (比如 100k 缓冲区)并让每个方法都返回 Option[Type] 它找到输入, None 它没有。我会用这种策略实现类似的性能吗?
    • @Anthony Accioly - 生成一堆 Option 实例会损失一些性能,但它应该仍然可以正常工作。请注意始终将字节存储在字节数组中并直接访问它们,而不是通过方便的 Scala 集合方法。那些需要拳击,因此会对性能产生相当大的影响。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-13
    • 1970-01-01
    • 2017-12-27
    • 1970-01-01
    • 2012-08-28
    相关资源
    最近更新 更多