从字符串中提取带括号的Python表达式

时间:2008-10-16 03:02:28

标签: python parsing

我一直想知道编写一些Python代码来搜索字符串以查找${ expr }形式的子字符串索引的难度。例如, expr 意味着是Python表达式或类似的表达式。鉴于这样的事情,可以很容易地想象继续使用compile()检查表达式的语法,使用eval()针对特定范围进行评估,甚至可能将结果替换为原始字符串。人们必须一直做着非常相似的事情。

我可以想象使用第三方解析器生成器[oof]解决这样的问题,或者通过手工编写某种状态机[eek],或者通过说服Python自己的解析器以某种方式完成繁重的工作[嗯]。也许在某个地方有一个第三方模板库可以做到这一点。也许限制 expr 的语法在简单性或执行时间或减少外部依赖性方面可能是一个值得妥协的方法 - 例如,我真正需要的只是与任何 expr 具有平衡的花括号。

你有什么感觉?

更新

非常感谢您迄今为止的回复!回顾昨天我写的内容,我不确定我是否已经清楚地知道我在问什么。模板替换确实是一个有趣的问题,对于更多人来说可能比我想知道的表达式提取子问题更有用,但是我提出它只是作为一个简单的例子来说明我的问题的答案在实际中是如何有用的生活。其他一些潜在的应用程序可能包括将提取的表达式传递给语法高亮显示器;将结果传递给真正的Python解析器并查看或使用解析树进行查找;或者使用提取的表达式序列来构建一个更大的Python程序,可能与从周围文本中获取的一些信息一起使用。

我提到的${ expr }语法也是一个例子,事实上我想知道我是否应该使用$( < em> expr )作为我的例子,因为它使re.finditer(r'$\{([^}]+)\}', s)的明显方法的潜在缺点更容易看到。 Python表达式可以(通常也包含))(或})字符。处理任何这些案件似乎可能比它的价值更麻烦,但我还不相信。请随意尝试这个案例!

在发布这个问题之前,我花了很多时间研究Python模板引擎,希望有人可能会暴露我正在询问的那种低级功能 - 即可以找到多种表达式的东西上下文并告诉我它们在哪里而不是仅限于使用单个硬编码语法查找嵌入的表达式,始终对它们进行评估,并始终将结果替换回原始字符串。我还没弄清楚如何使用它们中的任何一个来解决我的问题,但我非常感谢有关更多内容的建议(不能相信我错过了维基上那个精彩的列表!)。这些东西的API文档往往是相当高级的,我对它们中的任何内部都不太熟悉,所以我确信我可以使用帮助来查看这些并找出如何让它们去做这种事情。

感谢您的耐心等待!

4 个答案:

答案 0 :(得分:2)

我认为您所要求的是能够将Python代码插入要评估的文本文件中。有几个模块已经存在以提供这种功能。您可以查看Python.org Templating wiki page以获取完整列表。

一些谷歌搜索也发现了一些您可能感兴趣的其他模块:

如果你真的只是为了自己编写这个问题,你也可以深入研究这个Python食谱解决方案Yet Another Python Templating Utility (YAPTU)

  

“Templating”(将输入文件复制到输出,即时插入Python   表达式和语句)是经常需要的,而YAPTU是一个很小的但是   完整的Python模块;表达和陈述被识别出来   通过任意用户选择的正则表达式。

编辑:只是为了它,我为此掀起了一个非常简单的代码示例。我确信它有错误,但它至少说明了该概念的简化版本:

#!/usr/bin/env python

import sys
import re

FILE = sys.argv[1]

handle = open(FILE)
fcontent = handle.read()
handle.close()

for myexpr in re.finditer(r'\${([^}]+)}', fcontent, re.M|re.S):
    text = myexpr.group(1)
    try:
        exec text
    except SyntaxError:
        print "ERROR: unable to compile expression '%s'" % (text)

根据以下文字进行测试:

This is some random text, with embedded python like 
${print "foo"} and some bogus python like

${any:thing}.

And a multiline statement, just for kicks: 

${
def multiline_stmt(foo):
  print foo

multiline_stmt("ahem")
}

More text here.

输出:

[user@host]$ ./exec_embedded_python.py test.txt
foo
ERROR: unable to compile expression 'any:thing'
ahem

答案 1 :(得分:1)

我认为你最好的选择是匹配所有花括号的条目,然后检查Python本身是否是有效的Python,compiler会有用。

答案 2 :(得分:1)

如果你想处理像{'{spam': 42}["spam}"]这样的任意表达式,如果没有完整的解析器,你就无法逃脱。

答案 3 :(得分:0)

发布此内容后,到目前为止阅读回复(感谢大家!),并暂时考虑问题,这是我能够提出的最佳方法:

  1. 找到第一个${
  2. 在此之后找到下一个}
  3. 将两者之间的任何内容提供给compile()。如果它有效,请在其中插入叉子,我们就完成了。
  4. 否则,通过查找}的后续出现来继续扩展字符串。只要有东西编译,就把它归还。
  5. 如果我们用完}而无法编译任何内容,请使用上次编译尝试的结果来提供有关问题所在的信息。
  6. 这种方法的优点:

    • 代码非常简短易懂。
    • 它非常高效 - 即使在表达式不包含}的情况下也是最佳的。最糟糕的情况似乎也不会太糟糕。
    • 适用于包含${和/或}
    • 的许多表达式
    • 没有外部依赖项。事实上,无需导入任何东西。 (这让我感到惊讶。)

    缺点:

    • 有时它抓得太多或太少。请参阅下面的后者示例。我可以想象一个可怕的例子,你有两个表达式,第一个是巧妙的错误,算法最终错误地抓住整个事物和介于两者之间的所有内容并将其返回为有效,尽管我无法证明这一点。也许事情并不像我担心的那么糟糕。我不认为一般可以避免误解 - 问题定义有点滑 - 但似乎应该可以做得更好,特别是如果一个人愿意交易简单或执行时间。
    • 我没有做任何基准测试,但我可以想象有更快的替代方案,特别是在表达式中涉及大量}的情况下。如果想要将这种技术应用于相当大的Python代码块而不仅仅是非常短的表达式,这可能是一个大问题。

    这是我的实施。

    def findExpr(s, i0=0, begin='${', end='}', compArgs=('<string>', 'eval')):
      assert '\n' not in s, 'line numbers not implemented'
      i0 = s.index(begin, i0) + len(begin)
      i1 = s.index(end, i0)
      code = errMsg = None
      while code is None and errMsg is None:
        expr = s[i0:i1]
        try: code = compile(expr, *compArgs)
        except SyntaxError, e:
          i1 = s.find(end, i1 + 1)
          if i1 < 0: errMsg, i1 = e.msg, i0 + e.offset
      return i0, i1, code, errMsg
    

    这里是带有doctest格式的插图的文档字符串,我没有插入上面函数的中间只是因为它很长而且我觉得没有它就更容易阅读代码。

    '''
    Search s for a (possibly invalid) Python expression bracketed by begin
    and end, which default to '${' and '}'.  Return a 4-tuple.
    
    >>> s = 'foo ${a*b + c*d} bar'
    >>> i0, i1, code, errMsg = findExpr(s)
    >>> i0, i1, s[i0:i1], errMsg
    (6, 15, 'a*b + c*d', None)
    >>> ' '.join('%02x' % ord(byte) for byte in code.co_code)
    '65 00 00 65 01 00 14 65 02 00 65 03 00 14 17 53'
    >>> code.co_names
    ('a', 'b', 'c', 'd')
    >>> eval(code, {'a': 1, 'b': 2, 'c': 3, 'd': 4})
    14
    >>> eval(code, {'a': 'a', 'b': 2, 'c': 'c', 'd': 4})
    'aacccc'
    >>> eval(code, {'a': None})
    Traceback (most recent call last):
      ...
    NameError: name 'b' is not defined
    
    Expressions containing start and/or end are allowed.
    
    >>> s = '{foo ${{"}": "${"}["}"]} bar}'
    >>> i0, i1, code, errMsg = findExpr(s)
    >>> i0, i1, s[i0:i1], errMsg
    (7, 23, '{"}": "${"}["}"]', None)
    
    If the first match is syntactically invalid Python, i0 points to the
    start of the match, i1 points to the parse error, code is None and
    errMsg contains a message from the compiler.
    
    >>> s = '{foo ${qwerty asdf zxcvbnm!!!} ${7} bar}'
    >>> i0, i1, code, errMsg = findExpr(s)
    >>> i0, i1, s[i0:i1], errMsg
    (7, 18, 'qwerty asdf', 'invalid syntax')
    >>> print code
    None
    
    If a second argument is given, start searching there.
    
    >>> i0, i1, code, errMsg = findExpr(s, i1)
    >>> i0, i1, s[i0:i1], errMsg
    (33, 34, '7', None)
    
    Raise ValueError if there are no further matches.
    
    >>> i0, i1, code, errMsg = findExpr(s, i1)
    Traceback (most recent call last):
      ...
    ValueError: substring not found
    
    In ambiguous cases, match the shortest valid expression.  This is not
    always ideal behavior.
    
    >>> s = '{foo ${x or {} # return {} instead of None} bar}'
    >>> i0, i1, code, errMsg = findExpr(s)
    >>> i0, i1, s[i0:i1], errMsg
    (7, 25, 'x or {} # return {', None)
    
    This implementation must not be used with multi-line strings.  It does
    not adjust line number information in the returned code object, and it
    does not take the line number into account when computing the offset
    of a parse error.
    
    '''