【问题标题】:Tokenization using regexp in Python在 Python 中使用正则表达式进行标记化
【发布时间】:2013-03-07 19:59:12
【问题描述】:

我尝试将"spam bar ds<hai bye>sd baz eggs" 之类的字符串标记为['spam', 'bar', 'ds<hai bye>sd', 'baz', 'eggs'] 列表,即str.split(),但在< ... > 中保留空格。

我的解决方案是使用 re.split(\S*<.*?>\S*)|\s+ 模式。但是我得到以下信息:

>>> re.split('(\S*<.*?>\S*)|\s+', "spam bar ds<hai bye>sd baz eggs")
['spam', None, 'bar', None, '', 'ds<hai bye>sd', '', None, 'baz', None, 'eggs']

不确定那些Nones 和空字符串来自哪里。当然,我可以通过列表理解 [s for s in result if s] 将它们过滤掉,但在我知道原因之前我不太愿意这样做。

那么,(1) 为什么那些Nones 和空字符串,(2) 可以做得更好吗?

【问题讨论】:

    标签: python regex parsing token tokenize


    【解决方案1】:

    None 和空字符串值是因为您在模式中使用了捕获括号,因此拆分包含匹配的文本 - 请参阅 official documentation 以了解这一点。

    如果您将模式修改为r"((?:\S*&lt;.*?&gt;\S*)|\S+")(即转义括号以使其不捕获并将空白更正为非空白)它应该可以工作,但只能通过保留分隔符,然后您需要过滤通过跳过替代项目。我认为你最好这样做:

    ITEM_RE = re.compile(r"(?:\S*<.*?>\S*)|\S+")
    ITEM_RE.findall("spam bar ds<hai bye>sd baz eggs")
    

    如果您不需要实际列表(即一次只浏览一项),那么finditer() 效率更高,因为它一次只生成一项。如果您可能在不查看整个列表的情况下退出,则尤其如此。

    原则上也可能带有否定的lookbehind断言,但实际上我认为创建一个足够灵活的断言是不可能的 - 我尝试r"(?&lt;!&lt;[^&gt;]*)\s+"并得到错误“看-behind 需要固定宽度的模式”,所以我想这是一个禁忌。文档证实了这一点 - 后向断言(正面和负面)都需要固定宽度。

    这种方法的问题在于,如果您期望嵌套尖括号 - 那么您将不会得到您期望的结果。例如,解析ds&lt;hai &lt;bye&gt; foo&gt;sd 将产生ds&lt;hai &lt;bye&gt; 作为一个标记。我认为这是正则表达式无法解决的一类问题——你需要更接近正确解析器的东西。用纯 Python 编写一个一次遍历字符并计算括号嵌套级别的代码并不难,但这会很慢。取决于您是否可以确定您只会在输入中看到一层嵌套。

    【讨论】:

      【解决方案2】:

      我得到了这个正则表达式:

      ss = "spam bar ds<hai bye>sd baz eggs ZQ<boo <abv> foo>WX  "
      
      reg = re.compile('(?:'
                           '\S*?'
                           '<'
                           '[^<>]*?'
                           '(?:<[^<>]*>[^<>]*)*'
                           '[^<>]*?'
                           '>'
                             ')?'
                       '\S+')
      
      print reg.findall(ss)
      

      结果

      ['spam', 'bar', 'ds<hai bye>sd', 'baz', 'eggs',
       'ZQ<boo <abv> foo>WX']
      

      编辑 1

      一个新的正则表达式,更准确,在 Cartroo 的评论之后:

      import re
      
      pat = ('(?<!\S)'  # absence of non-whitespace before
      
             '(?:'
                 '[^\s<>]+'
      
                 '|'  # OR
      
                 '(?:[^\s<>]*)'
                 '(?:'
                     '<'
                     '[^<>]*?'
                     '(?:<[^<>]*?>[^<>]*)*'
                     '[^<>]*?'
                     '>'
                     ')'
                 '(?:[^\s<>]*)'
             ')'
      
             '(?!\S)' # absence of non-whitespace after)
             )
      reg = re.compile(pat)
      
      ss = ("spam i>j bar ds<hai bye>sd baz eggs Z<boo <abv>"
            " foo>W ttt <two<;>*<:> three> ")
      print '%s\n' % ss
      print reg.findall(ss)
      
      ss = "a<b<E1>c>d <b<E2>c>d <b<E3>c> a<<E4>c>d <<E5>>d 
         <<E6>> <<>>"
      print '\n\n%s\n' % ss
      print reg.findall(ss)
      

      结果

      spam i>j bar ds<hai bye>sd baz eggs Z<boo <abv> foo>W 
      ttt <two<;>*<:> three> 
      
      ['spam', 'bar', 'ds<hai bye>sd', 'baz', 'eggs', 
       'Z<boo <abv> foo>W', 'ttt', '<two<;>*<:> three>']
      
      
      a<b<E1>c>d <b<E2>c>d <b<E3>c> a<<E4>c>d <<E5>>d <<E6>> <<>>
      
      ['a<b<E1>c>d', '<b<E2>c>d', '<b<E3>c>', 'a<<E4>c>d', '<<E5>>d',
       '<<E6>>', '<<>>']
      

      上述字符串格式良好,结果一致。
      在格式不正确的文本上(关于括号),它可能会给出非预期的结果:

      ss = """A<B<C>D  
       E<F<G>H 
      I<J>K> 
       L<<M>N
         O<P>>Q
       R<<S>    T<<>"""
      print '\n\n%s\n' % ss
      print reg.findall(ss)
      

      结果

      A<B<C>D  
       E<F<G>H 
      I<J>K> 
       L<<M>N
         O<P>>Q
       R<<S>    T<<>
      
      ['E<F<G>H \nI<J>K>', 'L<<M>N\n   O<P>>Q']
      

      这是因为'(?:&lt;[^&lt;&gt;]*?&gt;[^&lt;&gt;]*)*' 末尾的星号。可以通过移除星号来关闭此行为。这种行为导致难以使用正则表达式来分析 Crtaroo 所称的此类“复杂”文本。

      .

      编辑 2

      当我说结果 'E&lt;F&lt;G&gt;H \nI&lt;J&gt;K&gt;''L&lt;&lt;M&gt;N\n O&lt;P&gt;&gt;Q' 是不想要的结果时,这并不意味着找到的匹配部分不尊重正则表达式的模式(怎么可能?),因为我制作了它;匹配部分确实形成良好:
      两个部分 &lt;G&gt;&lt;J&gt; 在两个括号之间 &lt; &lt;G&gt; &lt;J&gt; &gt;
      两个部分&lt;M&gt;&lt;P&gt; 在两个括号&lt; &lt;M&gt; &lt;P&gt; &gt; 之间

      事实上,这是一种轻描淡写的说法,它意味着找到的每个匹配部分都应该只扩展一行。但是,一旦明确表达了轻描淡写,就会出现可能的解决方案。
      如果不需要在多行上延伸的匹配部分,很容易告诉正则表达式不匹配它们,这与我写的相反。在正则表达式模式的某些地方添加字符 \n 就足够了。

      实际上,这意味着匹配部分不能超过\n 字符,那么这个字符可以被认为是匹配部分的分隔符。因此,可以需要任何其他字符作为同一行上存在的匹配部分之间的分隔符,例如以下代码中的#

      正则表达式不能做饭或从学校接孩子,但它们非常强大。说正则表达式对格式错误的文本的行为是一个问题太短了:必须补充说这是文本的问题,而不是正则表达式的问题。一个正则表达式做它被命令做的事情:吃掉给它的任何文本。它贪婪地吃掉它,也就是说,在没有验证它的任何一致性的情况下,这不是它的预期行为,如果它被喂食了不合饮食的文字,它是不负责任的。说正则表达式在格式错误的文本上的行为是一个问题,听起来好像有人会责备孩子有时会用威士忌和胡椒粉来滋养。

      编码员有责任确保传递给正则表达式的文本格式正确。就像编码员将验证sn-p放入代码中以确保条目是整数以便程序正确运行一样。

      .

      这一点与当试图将标记文本解析为 XML 文本时滥用正则表达式不同。正则表达式无法解析这样的文本,好吧,因为不可能制作出能够对格式错误的标记文本做出正确反应的正则表达式。不尝试这样做也是编码人员的责任。
      这并不意味着如果该文本已经过验证,则不得使用正则表达式来分析标记文本。
      无论如何,如果文本格式错误太多,即使解析器也不会捕获数据。

      我的意思是我们必须区分:

      • 传递给正则表达式的文本的性质(格式错误/格式正确)

      • 使用正则表达式时所追求目标的性质(解析/分析)

      .

      import re
      
      ss = """
       A<:<11>:<12>:>
       fgh
       A<#:<33>:<34>:>
       A#<:<55>:<56>:>
       A<:<77>:<78> i<j>
       A<B<C>D #
       E<F<G>H #
       I<J>K> 
       L<<M>N 
       O<P>>Q  #
       R<<S>  T<<>"""
      print '%s\n' % ss
      
      pat = ('(?<!\S)'  # absence of non-whitespace before
                 '(?:[^\s<>]*)'
                 '(?:<'
                     '[^<>]*?'
                     '(?:<[^<>]*?>[^<>]*)*'
                     '>)'
                 '(?:[^\s<>]*)'
             '(?!\S)' # absence of non-whitespace after)
             )
      reg = re.compile(pat)
      print '------------------------------'
      print '\n'.join(map(repr,reg.findall(ss)))
      
      
      pat = ('(?<!\S)'  # absence of non-whitespace before
                 '(?:[^\s<>]*)'
                 '(?:<'
                     '[^<>\n]*?'
                     '(?:<[^<>\n]*?>[^<>\n]*)*'
                     '>)'
                 '(?:[^\s<>]*)'
             '(?!\S)' # absence of non-whitespace after)
             )
      reg = re.compile(pat)
      print '\n----------- with \\n -------------'
      print '\n'.join(map(repr,reg.findall(ss)))
      
      
      pat = ('(?<!\S)'  # absence of non-whitespace before
                 '(?:[^\s<>]*)'
                 '(?:<'
                     '[^<>#]*?'
                     '(?:<[^<>#]*?>[^<>#]*)*'
                     '>)'
                 '(?:[^\s<>]*)'
             '(?!\S)' # absence of non-whitespace after)
             )
      reg = re.compile(pat)
      print '\n------------- with # -----------'
      print '\n'.join(map(repr,reg.findall(ss)))
      
      
      pat = ('(?<!\S)'  # absence of non-whitespace before
                 '(?:[^\s<>#]*)'
                 '(?:<'
                     '[^<>#]*?'
                     '(?:<[^<>#]*?>[^<>#]*)*'
                     '>)'
                 '(?:[^\s<>]*)'
             '(?!\S)' # absence of non-whitespace after)
             )
      reg = re.compile(pat)
      print '\n------ with ^# everywhere -------'
      print '\n'.join(map(repr,reg.findall(ss)))
      

      结果

       A<:<11>:<12>:>
       fgh
       A<#:<33>:<34>:>
       A#<:<55>:<56>:>
       A<:<77>:<78> i<j>
       A<B<C>D #
       E<F<G>H #
       I<J>K> 
       L<<M>N 
       O<P>>Q  #
       R<<S>  T<<>
      
      ------------------------------
      'A<:<11>:<12>:>'
      'A<#:<33>:<34>:>'
      'A#<:<55>:<56>:>'
      'i<j>'
      'E<F<G>H #\n I<J>K>'
      'L<<M>N \n O<P>>Q'
      
      ----------- with \n -------------
      'A<:<11>:<12>:>'
      'A<#:<33>:<34>:>'
      'A#<:<55>:<56>:>'
      'i<j>'
      
      ------------- with # -----------
      'A<:<11>:<12>:>'
      'A#<:<55>:<56>:>'
      'i<j>'
      'L<<M>N \n O<P>>Q'
      
      ------ with ^# everywhere -------
      'A<:<11>:<12>:>'
      'i<j>'
      'L<<M>N \n O<P>>Q'
      

      【讨论】:

      • 除非此模式要求尖括号外至少有一个字符。例如,在"one &lt;two three&gt; four" 上试一试,它不会达到您的预期。您也许可以通过将最终的 \S+ 更改为 \S* 并添加一个额外的替代来覆盖 \S+ 情况(即没有尖括号)来解决这个问题 - 这是因为如果有尖括号那么你实际上不需要任何其他字符以使其成为有效令牌(我假设)。但是,我还没有测试过。无论哪种方式,这是卷积都表明正则表达式不是这里的最佳解决方案。
      • +1 表示第二个正则表达式,但您为什么要尝试容纳嵌套括号?我在问题或任何 cmets 中都没有看到该要求。
      • @AlanMoore 这不是为了满足 OP 表达的需求,而是为了乐趣和个人挑战,并向 Cartroo 展示正则表达式如何做到这一点。他在回答中说:“这种方法的问题在于,如果您期望嵌套尖括号 - 那么您将不会得到您期望的结果(...)我认为这是一类问题正则表达式无法解决 - 您需要更接近正确解析器的东西” 如果 OP 不想要此功能,他将删除部分正则表达式
      • 你肯定比我用正则表达式取得的成就更多,但格式错误的字符串的行为是一个问题。此外,最小匹配引入了大量回溯,这可能会使正则表达式解决方案的效率大大低于传统的基于状态的词法分析器。尽管如此,还是为这次尝试脱帽致敬。
      • @Cartroo:这不是问题;您可以使用贪婪的量词编写低效的正则表达式,就像使用非贪婪的量词一样容易。如果他一直在使用.*?,我可能会同意你的看法,不是因为他使用了不情愿的量词,而是因为他马虎。事实上,这些不情愿的量词根本没有任何作用; [^&lt;&gt;]* 将在下一个 &lt;&gt; 之前停止。 (但是,最后一个 [^&lt;&gt;]*? 多余的;这已经在前面的 atom 中介绍过:(?:&lt;[^&lt;&gt;]*?&gt;[^&lt;&gt;]*)*。)
      【解决方案3】:

      我相信None 值是由于基于文档中这一行的模式中存在()s:

      如果在模式中使用捕获括号,则模式中所有组的文本也会作为结果列表的一部分返回

      在您的输入中使用Regex Tester 也可能有助于可视化解析:http://regexpal.com/?flags=g&regex=%28\S*%3C.*%3F%3E\S*%29| \s%2B&input=spam%20bar%20ds%3Chai%20bye%3Esd%20baz%20eggs

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-09-06
        • 2017-08-22
        • 1970-01-01
        • 2021-10-16
        • 1970-01-01
        • 2021-11-02
        • 1970-01-01
        相关资源
        最近更新 更多