Python 计算器 - 使用十进制模块评估语句字符串
Python calculator - evaluating string of statements using decimal module
我正在编写一个 Discord 机器人,它将接受用户作为字符串的输入,然后计算其中的表达式,没有什么特别的,只是简单的算术运算。我有两个问题 - 安全和小数。首先我使用了 simpleeval 包,它工作正常但它有小数问题,例如
0.1 + 0.1 + 0.1 - 0.3
会 return 5.551115123125783e-17
。经过大量谷歌搜索后,我找到了一个有效的答案,但它使用内置的 eval() 函数,显然使用它是一个很大的问题。
是否有 better/safer 处理方法?这实现了 https://docs.python.org/3/library/tokenize.html#examples decistmt() 方法,该方法用小数代替语句字符串中的浮点数。但最后我使用了 eval() 并且通过所有这些检查我仍然不确定它是否安全并且我宁愿避免它。
这就是 decistmt() 的作用:
from tokenize import tokenize, untokenize, NUMBER, STRING, NAME, OP
from io import BytesIO
def decistmt(s):
"""Substitute Decimals for floats in a string of statements.
>>> from decimal import Decimal
>>> s = 'print(+21.3e-5*-.1234/81.7)'
>>> decistmt(s)
"print (+Decimal ('21.3e-5')*-Decimal ('.1234')/Decimal ('81.7'))"
The format of the exponent is inherited from the platform C library.
Known cases are "e-007" (Windows) and "e-07" (not Windows). Since
we're only showing 12 digits, and the 13th isn't close to 5, the
rest of the output should be platform-independent.
>>> exec(s) #doctest: +ELLIPSIS
-3.21716034272e-0...7
Output from calculations with Decimal should be identical across all
platforms.
>>> exec(decistmt(s))
-3.217160342717258261933904529E-7
"""
result = []
g = tokenize(BytesIO(s.encode('utf-8')).readline) # tokenize the string
for toknum, tokval, _, _, _ in g:
if toknum == NUMBER and '.' in tokval: # replace NUMBER tokens
result.extend([
(NAME, 'Decimal'),
(OP, '('),
(STRING, repr(tokval)),
(OP, ')')
])
else:
result.append((toknum, tokval))
return untokenize(result).decode('utf-8')
# example user input: "(20+5)^4 - 550 + 8"
@bot.command()
async def calc(context, *, user_input):
#this is so user can use both ^ and ** for power and use "," and "." for decimals
equation = user_input.replace('^', "**").replace(",", ".")
valid_operators: list = ["+", "-", "/", "*", "%", "^", "**"]
# checks if a string contains any element from a list, it will also return false if the iterable is empty, so this covers empty check too
operator_check: bool = any(
operator in equation for operator in valid_operators)
# checks if arithmetic operator is last or first element in equation, to prevent doing something like ".calc 2+" or ".calc +2"
def is_last_or_first(equation: str):
for operator in valid_operators:
if operator == equation[-1]:
return True
elif operator == equation[0]:
if operator == "-":
return False
else:
return True
#isupper and islower checks whether there are letters in user input
if not operator_check or is_last_or_first(equation) or equation.isupper() or equation.islower():
return await context.send("Invalid input")
result = eval(decistmt(equation))
result = float(result)
# returning user_input here so user sees "^" instead of "**"
async def result_generator(result: int or float):
await context.send(f'**Input:** ```fix\n{user_input}```**Result:** ```fix\n{result}```')
# this is so if the result is .0 it returns an int
if result.is_integer():
await result_generator(int(result))
else:
await result_generator(result)
这是用户输入后发生的事情
user_input = "0.1 + 0.1 + 0.1 - 0.3"
float_to_decimal = decistmt(user_input)
print(float_to_decimal)
print(type(float_to_decimal))
# Decimal ('0.1')+Decimal ('0.1')+Decimal ('0.1')-Decimal ('0.3')
# <class 'str'>
现在我需要评估这个输入,所以我使用 eval()。我的问题是 - 这安全吗(我假设不安全)还有其他方法可以评估 float_to_decimal?
编辑。根据要求,更深入的解释:
整个应用程序是一个聊天机器人。这个“计算”功能是用户可以使用的命令之一。它是通过在聊天中输入“.calc”来调用的,“.calc”是一个前缀,后面的任何内容都是参数,它将被连接成一个字符串,最终我将得到一个字符串作为参数。我执行了一系列检查以限制输入(删除字母等)。检查后,我留下了一个由数字、算术运算符和括号组成的字符串。我想评估该字符串的数学表达式。我将该字符串传递给 decistmt 函数,该函数将该字符串中的每个浮点数转换为 Decimal 对象,结果是一个看起来像这样的字符串:“Decimal ('2.5') + Decimal ('-5.2')”。现在我需要评估该字符串中的表达式。我为此使用了 simpleeval 模块,但它与 Decimal 模块不兼容,因此我正在使用内置的 eval() 方法进行评估。我的问题是,是否有一种更安全的方法来评估字符串中的数学表达式?
尝试按照@Shiva 的建议对数字使用 Decimal
。
还有 here's the answer from user @unutbu,它包括使用 PyParsing 库和自定义包装器来评估数学表达式。
在 Python 或系统表达式(例如 import <package>
或 dir()
)的情况下将抛出 ParseException
错误。
pyparsing 最近的一个衍生包是 plusminus, a wrapper around pyparsing specifically for this case of embedded arithmetic evaluation. You can try it yourself at http://ptmcg.pythonanywhere.com/plusminus. Since plusminus uses its own parser, it is not subject to the common eval attacks, as Ned Batchelder describes in this still-timely blog post: https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html。 Plusminus 还包括对表达式的处理,这些表达式虽然有效且“安全”,但非常耗时,可用于拒绝服务攻击 - 例如 "9**9**9"
.
我正计划对 plusminus 进行重构,这将稍微改变 API,但您可以按原样将其用于您的 Discord 插件。 (您会发现 plusminus 还支持一些超出为 Python 定义的标准运算符和函数的附加运算符和函数,例如 "|4-7|"
用于绝对值,或 "√42"
用于 sqrt(42)
- 单击该页面上的示例按钮以获取更多支持的表达式。您还可以将值保存在变量中,然后在以后的表达式中使用该变量。(这可能不适用于您的插件,因为变量状态可能是共享的由多个 Discordian 编写。)
Plusminus 还设计用于支持子类化以定义新的运算符,例如将评估 "3d6+d20"
为“3 卷 6 面骰子,加上 1 卷 20 面骰子”的掷骰子",包括每次评估时的随机掷骰。
我正在编写一个 Discord 机器人,它将接受用户作为字符串的输入,然后计算其中的表达式,没有什么特别的,只是简单的算术运算。我有两个问题 - 安全和小数。首先我使用了 simpleeval 包,它工作正常但它有小数问题,例如
0.1 + 0.1 + 0.1 - 0.3
会 return 5.551115123125783e-17
。经过大量谷歌搜索后,我找到了一个有效的答案,但它使用内置的 eval() 函数,显然使用它是一个很大的问题。
是否有 better/safer 处理方法?这实现了 https://docs.python.org/3/library/tokenize.html#examples decistmt() 方法,该方法用小数代替语句字符串中的浮点数。但最后我使用了 eval() 并且通过所有这些检查我仍然不确定它是否安全并且我宁愿避免它。
这就是 decistmt() 的作用:
from tokenize import tokenize, untokenize, NUMBER, STRING, NAME, OP
from io import BytesIO
def decistmt(s):
"""Substitute Decimals for floats in a string of statements.
>>> from decimal import Decimal
>>> s = 'print(+21.3e-5*-.1234/81.7)'
>>> decistmt(s)
"print (+Decimal ('21.3e-5')*-Decimal ('.1234')/Decimal ('81.7'))"
The format of the exponent is inherited from the platform C library.
Known cases are "e-007" (Windows) and "e-07" (not Windows). Since
we're only showing 12 digits, and the 13th isn't close to 5, the
rest of the output should be platform-independent.
>>> exec(s) #doctest: +ELLIPSIS
-3.21716034272e-0...7
Output from calculations with Decimal should be identical across all
platforms.
>>> exec(decistmt(s))
-3.217160342717258261933904529E-7
"""
result = []
g = tokenize(BytesIO(s.encode('utf-8')).readline) # tokenize the string
for toknum, tokval, _, _, _ in g:
if toknum == NUMBER and '.' in tokval: # replace NUMBER tokens
result.extend([
(NAME, 'Decimal'),
(OP, '('),
(STRING, repr(tokval)),
(OP, ')')
])
else:
result.append((toknum, tokval))
return untokenize(result).decode('utf-8')
# example user input: "(20+5)^4 - 550 + 8"
@bot.command()
async def calc(context, *, user_input):
#this is so user can use both ^ and ** for power and use "," and "." for decimals
equation = user_input.replace('^', "**").replace(",", ".")
valid_operators: list = ["+", "-", "/", "*", "%", "^", "**"]
# checks if a string contains any element from a list, it will also return false if the iterable is empty, so this covers empty check too
operator_check: bool = any(
operator in equation for operator in valid_operators)
# checks if arithmetic operator is last or first element in equation, to prevent doing something like ".calc 2+" or ".calc +2"
def is_last_or_first(equation: str):
for operator in valid_operators:
if operator == equation[-1]:
return True
elif operator == equation[0]:
if operator == "-":
return False
else:
return True
#isupper and islower checks whether there are letters in user input
if not operator_check or is_last_or_first(equation) or equation.isupper() or equation.islower():
return await context.send("Invalid input")
result = eval(decistmt(equation))
result = float(result)
# returning user_input here so user sees "^" instead of "**"
async def result_generator(result: int or float):
await context.send(f'**Input:** ```fix\n{user_input}```**Result:** ```fix\n{result}```')
# this is so if the result is .0 it returns an int
if result.is_integer():
await result_generator(int(result))
else:
await result_generator(result)
这是用户输入后发生的事情
user_input = "0.1 + 0.1 + 0.1 - 0.3"
float_to_decimal = decistmt(user_input)
print(float_to_decimal)
print(type(float_to_decimal))
# Decimal ('0.1')+Decimal ('0.1')+Decimal ('0.1')-Decimal ('0.3')
# <class 'str'>
现在我需要评估这个输入,所以我使用 eval()。我的问题是 - 这安全吗(我假设不安全)还有其他方法可以评估 float_to_decimal?
编辑。根据要求,更深入的解释:
整个应用程序是一个聊天机器人。这个“计算”功能是用户可以使用的命令之一。它是通过在聊天中输入“.calc”来调用的,“.calc”是一个前缀,后面的任何内容都是参数,它将被连接成一个字符串,最终我将得到一个字符串作为参数。我执行了一系列检查以限制输入(删除字母等)。检查后,我留下了一个由数字、算术运算符和括号组成的字符串。我想评估该字符串的数学表达式。我将该字符串传递给 decistmt 函数,该函数将该字符串中的每个浮点数转换为 Decimal 对象,结果是一个看起来像这样的字符串:“Decimal ('2.5') + Decimal ('-5.2')”。现在我需要评估该字符串中的表达式。我为此使用了 simpleeval 模块,但它与 Decimal 模块不兼容,因此我正在使用内置的 eval() 方法进行评估。我的问题是,是否有一种更安全的方法来评估字符串中的数学表达式?
尝试按照@Shiva 的建议对数字使用 Decimal
。
还有 here's the answer from user @unutbu,它包括使用 PyParsing 库和自定义包装器来评估数学表达式。
在 Python 或系统表达式(例如 import <package>
或 dir()
)的情况下将抛出 ParseException
错误。
pyparsing 最近的一个衍生包是 plusminus, a wrapper around pyparsing specifically for this case of embedded arithmetic evaluation. You can try it yourself at http://ptmcg.pythonanywhere.com/plusminus. Since plusminus uses its own parser, it is not subject to the common eval attacks, as Ned Batchelder describes in this still-timely blog post: https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html。 Plusminus 还包括对表达式的处理,这些表达式虽然有效且“安全”,但非常耗时,可用于拒绝服务攻击 - 例如 "9**9**9"
.
我正计划对 plusminus 进行重构,这将稍微改变 API,但您可以按原样将其用于您的 Discord 插件。 (您会发现 plusminus 还支持一些超出为 Python 定义的标准运算符和函数的附加运算符和函数,例如 "|4-7|"
用于绝对值,或 "√42"
用于 sqrt(42)
- 单击该页面上的示例按钮以获取更多支持的表达式。您还可以将值保存在变量中,然后在以后的表达式中使用该变量。(这可能不适用于您的插件,因为变量状态可能是共享的由多个 Discordian 编写。)
Plusminus 还设计用于支持子类化以定义新的运算符,例如将评估 "3d6+d20"
为“3 卷 6 面骰子,加上 1 卷 20 面骰子”的掷骰子",包括每次评估时的随机掷骰。