
本文详解如何在PHP中安全、精准地解析并计算含运算符优先级和括号的数学表达式,全程规避eval()风险,采用调度场算法(Shunting Yard)转为逆波兰表示法(RPN),再通过栈求值,同时涵盖基础算术运算、精度处理与大数支持方案。
本文详解如何在php中安全、精准地解析并计算含运算符优先级和括号的数学表达式,全程规避eval()风险,采用调度场算法(shunting yard)转为逆波兰表示法(rpn),再通过栈求值,同时涵盖基础算术运算、精度处理与大数支持方案。
在PHP开发中,直接对用户输入的字符串(如 "2 + 3 * (4 - 1)")执行数学计算时,若贸然使用 eval(),将导致严重的代码注入漏洞——攻击者可借此执行任意PHP指令,危及服务器安全。因此,构建一个不依赖 eval()、支持四则运算、括号、优先级及类型安全的表达式计算器,是生产环境的必备能力。
一、基础算术运算:简洁可靠,但需警惕隐式转换
最直接的加减乘除可通过原生运算符完成:
<?php $x1 = 150; $x2 = 90; $x = $x2 - $x1; // → -60 echo $x; // 输出: -60 // 支持复合运算符 $total = 100; $total += 25; // 等价于 $total = $total + 25 → 125 $total *= 1.1; // → 137.5(注意:除法/取余结果自动转为float) ?>
⚠️ 关键注意事项:
- 字符串参与运算时会触发隐式类型转换(如
"123abc" + 45→123 + 45 = 168),易引发逻辑错误; -
%取余仅作用于整数部分(10.7 % 3实际计算10 % 3 = 1); - 浮点精度问题普遍存在:
0.1 + 0.2 === 0.3返回false(真实值为0.30000000000000004)。
✅ 推荐实践:对用户输入先用is_numeric()校验,再用(float)或floatval()显式转换;金融场景务必启用 BCMath 扩展:
<?php $result = bcadd('0.1', '0.2', 1); // '0.3' —— 指定1位小数精度
?>
二、安全解析复杂表达式:中缀 → RPN → 栈求值
为支持带括号与优先级的表达式(如 "(10 + 5) * 2 - 3"),需实现两阶段算法:
✅ 阶段1:中缀转逆波兰(RPN)——调度场算法(Shunting Yard)
核心逻辑:维护运算符栈与输出队列,按优先级与括号规则调度。
<?php function infixToRpn($expression) {
$tokens = tokenize($expression); // 预处理:提取数字、运算符、括号
$output = [];
$operators = [];
$precedence = ['+' => 1, '-' => 1, '*' => 2, '/' => 2, '%' => 2];
foreach ($tokens as $token) {
if (is_numeric($token)) {
$output[] = (float)$token;
} elseif ($token === '(') {
$operators[] = $token;
} elseif ($token === ')') {
while (!empty($operators) && end($operators) !== '(') {
$output[] = array_pop($operators);
}
array_pop($operators); // 弹出 '('
} elseif (isset($precedence[$token])) {
while (!empty($operators) &&
end($operators) !== '(' &&
isset($precedence[end($operators)]) &&
$precedence[end($operators)] >= $precedence[$token]) {
$output[] = array_pop($operators);
}
$operators[] = $token;
}
}
while (!empty($operators)) {
$output[] = array_pop($operators);
}
return $output;
}
function tokenize($expr) {
// 简化版分词:支持空格分隔及基础符号
return preg_split('/\s+|([+\-*/%()])/', str_replace(' ', '', $expr), -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
}
?>
✅ 阶段2:RPN求值——单栈遍历
RPN 表达式天然无歧义,从左到右扫描,遇数字入栈,遇运算符弹出两操作数计算后压栈:
<?php function evaluateRpn($rpn) {
$stack = [];
foreach ($rpn as $token) {
if (is_numeric($token)) {
$stack[] = $token;
} else {
if (count($stack) < 2) throw new InvalidArgumentException("Invalid RPN expression");
$b = array_pop($stack);
$a = array_pop($stack);
switch ($token) {
case '+': $stack[] = $a + $b; break;
case '-': $stack[] = $a - $b; break;
case '*': $stack[] = $a * $b; break;
case '/':
if ($b == 0) throw new RuntimeException("Division by zero");
$stack[] = $a / $b;
break;
case '%': $stack[] = (int)$a % (int)$b; break;
default: throw new InvalidArgumentException("Unknown operator: $token");
}
}
}
return count($stack) === 1 ? $stack[0] : throw new InvalidArgumentException("Invalid expression");
}
// 使用示例
$expr = "(10 + 5) * 2 - 3";
$rpn = infixToRpn($expr); // → [10, 5, '+', 2, '*', 3, '-']
$result = evaluateRpn($rpn); // → 27
echo "Result: $result"; // 输出: Result: 27
?>
三、进阶场景:大数运算与精度保障
当涉及超长整数(如区块链哈希、ID 运算)或金融结算时,PHP 原生整型/浮点型会溢出或失真。此时应:
-
大整数相加:模拟竖式加法(避免
intval()截断); - 高精度计算:强制使用 BCMath(需启用扩展):
<?php // 大数加法(字符串安全版)
function bigAdd(string $a, string $b): string {
$a = ltrim($a, '0') ?: '0';
$b = ltrim($b, '0') ?: '0';
$lenA = strlen($a);
$lenB = strlen($b);
$maxLen = max($lenA, $lenB);
$result = '';
$carry = 0;
for ($i = 0; $i < $maxLen; $i++) {
$digitA = $i < $lenA ? (int)$a[$lenA - 1 - $i] : 0;
$digitB = $i < $lenB ? (int)$b[$lenB - 1 - $i] : 0;
$sum = $digitA + $digitB + $carry;
$result .= $sum % 10;
$carry = (int)($sum / 10);
}
if ($carry) $result .= $carry;
return strrev($result);
}
// BCMath 精确除法(保留2位小数)
$amount = bcdiv('100', '3', 2); // '33.33'
?>
总结
- ✅ 基础运算:直接使用
+/-/*///%,但必须校验输入类型、处理浮点误差; - ✅ 复杂表达式:拒绝
eval(),采用 调度场算法 + RPN栈求值,兼顾安全性与标准数学语义; - ✅ 生产就绪:对金额用 BCMath,对超长整数用字符串模拟运算,对用户输入始终执行
is_numeric()+ 显式转换; - ❌ 禁止行为:未经清理的字符串直传
eval()、依赖隐式转换处理关键业务数据、忽略除零与溢出异常。
通过以上分层策略,你可在任何PHP项目中构建健壮、可审计、符合安全规范的数学计算能力。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











