【问题标题】:How can I split a string of a mathematical expressions in python?如何在python中拆分一串数学表达式?
【发布时间】:2017-09-09 10:09:56
【问题描述】:

我制作了一个在 python 中将中缀转换为后缀的程序。问题是当我介绍这些论点时。 如果我介绍这样的东西:(这将是一个字符串)

( ( 73 + ( ( 34 - 72 ) / ( 33 - 3 ) ) ) + ( 56 + ( 95 - 28 ) ) )

它会用 .split() 分割它,程序将正常工作。 但我希望用户能够介绍这样的内容:

((73 + ( (34- 72 ) / ( 33 -3) )) + (56 +(95 - 28) ) )

如您所见,我希望空格可以是微不足道的,但程序继续用括号、整数(不是数字)和操作数分割字符串。

我尝试用 for 解决它,但我不知道如何捕获整个数字 (73 , 34 ,72) 而不是逐个数字 (7, 3 , 3 , 4 , 7 , 2)

总而言之,我想要的是将((81 * 6) /42+ (3-1)) 之类的字符串拆分为:

[(, (, 81, *, 6, ), /, 42, +, (, 3, -, 1, ), )]

【问题讨论】:

  • 你正在做的事情是“标记化” - 我已经用tokenize替换了你的list标签。
  • 正则表达式不适用于嵌套括号。 grako 很好,但如果您只需要这样做,它可能是重量级的。我喜欢 grako,因为你最终得到了可读的代码。
  • 感谢您的提问,它帮助我获得了银色 Python 徽章!
  • @MichaelGrazebrook 使用正则表达式来获取token还是合理的,然后你可以在后期验证括号(可能使用解析器库,可能使用手写递归下降或分流场)解析器)

标签: python string python-3.x split tokenize


【解决方案1】:

ast 的树

您可以使用ast 来获取表达式树:

import ast

source = '((81 * 6) /42+ (3-1))'
node = ast.parse(source) 

def show_children(node, level=0):
    if isinstance(node, ast.Num):
        print(' ' * level + str(node.n))
    else:
        print(' ' * level + str(node))
    for child in ast.iter_child_nodes(node):
        show_children(child, level+1)

show_children(node)

它输出:

<_ast.Module object at 0x7f56abbc5490>
 <_ast.Expr object at 0x7f56abbc5350>
  <_ast.BinOp object at 0x7f56abbc5450>
   <_ast.BinOp object at 0x7f56abbc5390>
    <_ast.BinOp object at 0x7f56abb57cd0>
     81
     <_ast.Mult object at 0x7f56abbd0dd0>
     6
    <_ast.Div object at 0x7f56abbd0e50>
    42
   <_ast.Add object at 0x7f56abbd0cd0>
   <_ast.BinOp object at 0x7f56abb57dd0>
    3
    <_ast.Sub object at 0x7f56abbd0d50>
    1

正如@user2357112 在 cmets 中所写:ast.parse 解释 Python 语法,而不是数学表达式。 (1+2)(3+4) 将被解析为函数调用,并且列表推导将被接受,即使它们可能不应该被视为有效的数学表达式。

使用正则表达式列出

如果你想要一个扁平的结构,一个正则表达式可以工作:

import re

number_or_symbol = re.compile('(\d+|[^ 0-9])')
print(re.findall(number_or_symbol, source))
# ['(', '(', '81', '*', '6', ')', '/', '42', '+', '(', '3', '-', '1', ')', ')']

它会寻找:

  • 多位数字
  • 或任何不是数字或空格的字符

一旦你有了一个元素列表,你就可以检查语法是否正确,例如使用stack 来检查括号是否匹配,或者每个元素是否都是已知的。

【讨论】:

  • 当心 - ast.parse 解析 Python 语法,而不是数学表达式。例如,(1+2)(3+4) 被解析为函数调用,3^4 被视为 XOR,[x**2 for x in range(5)] 被视为完全普通的输入,而不是应该被拒绝的废话。
  • 我见过一些计算器,他们非常挑剔地将* 与括号分开,但我不记得任何具体的例子。 AFAIK,^ 也不是每个计算器都使用的。不过,你提出了一个很好的观点。我试图将它整合到答案中。谢谢
【解决方案2】:

您需要为您的输入实现一个非常简单的分词器。您有以下类型的令牌:

  • (
  • )
  • +
  • -
  • *
  • /
  • \d+

您可以在输入字符串中找到它们,用各种空格分隔。

所以第一步是从头到尾处理字符串,提取这些标记,然后对标记进行解析,而不是对字符串本身进行解析。

一个很好的方法是使用以下正则表达式:'\s*([()+*/-]|\d+)'。然后你可以:

import re

the_input='(3+(2*5))'
tokens = []
tokenizer = re.compile(r'\s*([()+*/-]|\d+)')
current_pos = 0
while current_pos < len(the_input):
  match = tokenizer.match(the_input, current_pos)
  if match is None:
     raise Error('Syntax error')
  tokens.append(match.group(1))
  current_pos = match.end()
print(tokens)

这将打印['(', '3', '+', '(', '2', '*', '5', ')', ')']

您也可以使用re.findallre.finditer,但是您会跳过不匹配项,在这种情况下这是语法错误。

【讨论】:

  • @EricDuminil 哎呀。我应该在更新 current_pos 时使用 = 而不是 +=,因为 match.end() 是相对于整个字符串,而不是 current_pos。我还添加了一个示例。
  • 现在可以正常使用了。不过,input 不应用作变量名。
【解决方案3】:

快速正则表达式答案: re.findall(r"\d+|[()+\-*\/]", str_in)

演示:

>>> import re
>>> str_in = "((81 * 6) /42+ (3-1))"
>>> re.findall(r"\d+|[()+\-*\/]", str_in)
['(', '(', '81', '*', '6', ')', '/', '42', '+', '(', '3', '-', '1', 
')', ')']

对于嵌套括号部分,可以使用堆栈来跟踪关卡。

【讨论】:

    【解决方案4】:

    如果你不想使用re模块,你可以试试这个:

    s="((81 * 6) /42+ (3-1))"
    
    r=[""]
    
    for i in s.replace(" ",""):
        if i.isdigit() and r[-1].isdigit():
            r[-1]=r[-1]+i
        else:
            r.append(i)
    print(r[1:])
    

    输出:

    ['(', '(', '81', '*', '6', ')', '/', '42', '+', '(', '3', '-', '1', ')', ')']
    

    【讨论】:

    • @EricDuminil 感谢您的提醒,我通过添加一个空字符串来修复索引错误。
    【解决方案5】:

    手动滚动一个简单的表达式标记器实际上是非常简单的。我想你也会通过这种方式学到更多。

    所以为了教育和学习,这里有一个可以扩展的简单的表达式标记器实现。它基于"maximal-much" 规则工作。这意味着它表现得“贪婪”,试图消耗尽可能多的字符来构造每个令牌。

    事不宜迟,这里是分词器:

    class ExpressionTokenizer:
        def __init__(self, expression, operators):
            self.buffer = expression
            self.pos = 0
            self.operators = operators
    
        def _next_token(self):
            atom = self._get_atom()
    
            while atom and atom.isspace():
                self._skip_whitespace()
                atom = self._get_atom()
    
            if atom is None:
                return None
            elif atom.isdigit():
                return self._tokenize_number()
            elif atom in self.operators:
                return self._tokenize_operator()
            else:
                raise SyntaxError()
    
        def _skip_whitespace(self):
            while self._get_atom():
                if self._get_atom().isspace():
                    self.pos += 1
                else:
                    break
    
        def _tokenize_number(self):
            endpos = self.pos + 1
            while self._get_atom(endpos) and self._get_atom(endpos).isdigit():
                endpos += 1
            number = self.buffer[self.pos:endpos]
            self.pos = endpos
            return number
    
        def _tokenize_operator(self):
            operator = self.buffer[self.pos]
            self.pos += 1
            return operator
    
        def _get_atom(self, pos=None):
            pos = pos or self.pos
            try:
                return self.buffer[pos]
            except IndexError:
                return None
    
        def tokenize(self):
            while True:
                token = self._next_token()
                if token is None:
                    break
                else:
                    yield token
    

    这是一个用法演示:

    tokenizer = ExpressionTokenizer('((81 * 6) /42+ (3-1))', {'+', '-', '*', '/', '(', ')'})
    for token in tokenizer.tokenize():
        print(token)
    

    产生输出:

    (
    (
    81
    *
    6
    )
    /
    42
    +
    (
    3
    -
    1
    )
    )
    

    【讨论】:

      【解决方案6】:

      这并不能提供您想要的结果,但可能会引起其他查看此问题的人的兴趣。它利用 pyparsing 库。

      # Stolen from http://pyparsing.wikispaces.com/file/view/simpleArith.py/30268305/simpleArith.py
      # Copyright 2006, by Paul McGuire
      # ... and slightly altered
      
      from pyparsing import *
      
      integer = Word(nums).setParseAction(lambda t:int(t[0]))
      variable = Word(alphas,exact=1)
      operand = integer | variable
      
      expop = Literal('^')
      signop = oneOf('+ -')
      multop = oneOf('* /')
      plusop = oneOf('+ -')
      factop = Literal('!')
      
      expr = operatorPrecedence( operand,
          [("!", 1, opAssoc.LEFT),
           ("^", 2, opAssoc.RIGHT),
           (signop, 1, opAssoc.RIGHT),
           (multop, 2, opAssoc.LEFT),
           (plusop, 2, opAssoc.LEFT),]
          )
      
      print (expr.parseString('((81 * 6) /42+ (3-1))'))
      

      输出:

      [[[[81, '*', 6], '/', 42], '+', [3, '-', 1]]]
      

      【讨论】:

      【解决方案7】:

      提供更详细的正则表达式方法,您可以轻松扩展:

      import re
      
      solution = []
      pattern = re.compile('([\d\.]+)')
      
      s = '((73 + ( (34- 72 ) / ( 33 -3) )) + (56 +(95 - 28) ) )'
      
      for token in re.split(pattern, s):
          token = token.strip()
          if re.match(pattern, token):
              solution.append(float(token))
              continue
          for character in re.sub(' ', '', token):
              solution.append(character)
      

      这会给你结果:

       solution = ['(', '(', 73, '+', '(', '(', 34, '-', 72, ')', '/', '(', 33, '-', 3, ')', ')', ')', '+', '(', 56, '+', '(', 95, '-', 28, ')', ')', ')']
      

      【讨论】:

        【解决方案8】:

        使用 grako:

        start = expr $;
        expr = calc | value;
        calc = value operator value;
        value = integer | "(" @:expr ")" ;
        operator = "+" | "-" | "*" | "/";
        integer = /\d+/;
        

        grako 转换为 python。

        对于本例,返回值如下所示:

        ['73', '+', ['34', '-', '72', '/', ['33', '-', '3']], '+', ['56', '+', ['95', '-', '28']]]
        

        通常您会使用生成的语义类作为模板进行进一步处理。

        【讨论】:

          【解决方案9】:

          类似于@McGrady 的回答,您可以通过基本的队列实现来做到这一点。 作为一个非常基本的实现,您的 Queue 类如下所示:

          class Queue:
          
              EMPTY_QUEUE_ERR_MSG = "Cannot do this operation on an empty queue."
          
              def __init__(self):
                  self._items = []
          
              def __len__(self) -> int:
                  return len(self._items)
          
              def is_empty(self) -> bool:
                  return len(self) == 0
          
              def enqueue(self, item):
                  self._items.append(item)
          
              def dequeue(self):
                  try:
                      return self._items.pop(0)
                  except IndexError:
                      raise RuntimeError(Queue.EMPTY_QUEUE_ERR_MSG)
          
              def peek(self):
                  try:
                      return self._items[0]
                  except IndexError:
                      raise RuntimeError(Queue.EMPTY_QUEUE_ERR_MSG)
          

          使用这个简单的类,你可以实现你的解析函数:

          def tokenize_with_queue(exp: str) -> List:
              queue = Queue()
              cum_digit = ""
              for c in exp.replace(" ", ""):
                  if c in ["(", ")", "+", "-", "/", "*"]:
                      if cum_digit != "":
                          queue.enqueue(cum_digit)
                          cum_digit = ""
                      queue.enqueue(c)
                  elif c.isdigit():
                      cum_digit += c
                  else:
                      raise ValueError
              if cum_digit != "": #one last sweep in case there are any digits waiting
                  queue.enqueue(cum_digit)
              return [queue.dequeue() for i in range(len(queue))]
          

          如下测试:

          exp = "((73 + ( (34- 72 ) / ( 33 -3) )) + (56 +(95 - 28) ) )"
          print(tokenize_with_queue(exp)")
          

          会给你令牌列表:

          ['(', '(', '73', '+', '(', '(', '34', '-', '72', ')', '/', '(', '33', '-', '3', ')', ')', ')', '+', '(', '56', '+', '(', '95', '-', '28', ')', ')', ')']
          

          【讨论】:

            猜你喜欢
            • 2019-02-27
            • 2014-07-25
            • 1970-01-01
            • 1970-01-01
            • 2020-09-02
            • 2015-05-02
            • 1970-01-01
            • 2011-03-23
            • 1970-01-01
            相关资源
            最近更新 更多