在中缀表示法中解析表达式的算法是什么?

时间:2010-01-19 11:41:28

标签: php algorithm language-agnostic parsing expression-trees

我想在PHP中解析布尔表达式。如:

A and B or C and (D or F or not G)

这些术语可以被视为简单标识符。它们将具有一些结构,但解析器不需要担心。它应该只识别关键字and or not ( )。其他一切都是一个术语。

我记得我们在学校写过简单的算术表达式评估器,但我不记得它是如何完成的。我也不知道在Google / SO中要查找哪些关键字。

一个现成的库会很好,但是我记得算法非常简单,所以自己重新实现它可能很有趣也很有教育意义。

7 个答案:

答案 0 :(得分:15)

递归下降解析器编写易于阅读。第一步是写出你的语法。

也许这就是你想要的语法。

expr        = and_expr ('or' and_expr)*
and_expr    = not_expr ('and' not_expr)*
not_expr    = simple_expr | 'not' not_expr
simple_expr = term | '(' expr ')'

将其转换为递归下降解析器非常简单。只需每个非终端写一个函数。

def expr():
    x = and_expr()
    while peek() == 'or':
        consume('or')
        y = and_expr()
        x = OR(x, y)
    return x

def and_expr():
    x = not_expr()
    while peek() == 'and':
        consume('and')
        y = not_expr()
        x = AND(x, y)
    return x

def not_expr():
    if peek() == 'not':
        consume('not')
        x = not_expr()
        return NOT(x)
    else:
        return simple_expr()

def simple_expr():
    t = peek()
    if t == '(':
        consume('(')
        result = expr()
        consume(')')
        return result
    elif is_term(t):
        consume(t)
        return TERM(t)
    else:
        raise SyntaxError("expected term or (")

这还不完整。您必须提供更多代码:

  • 输入功能。 consumepeekis_term是您提供的功能。使用正则表达式很容易实现它们。 consume(s)读取输入的下一个标记,如果与s不匹配则抛出错误。 peek()只是在不消耗它的情况下返回下一个标记。如果is_term(s)是一个术语,则s会返回true。

  • 输出函数。 ORANDNOTTERM在每次表达式时都会被调用成功解析。他们可以做任何你想做的事。

  • 包装函数。您不想直接调用expr,而是要编写一个初始化consume和{所用变量的包装函数。 {1}},然后调用peek,最后检查以确保没有剩余消耗的剩余输入。

即便如此,它仍然是一小部分代码。 In Python, the complete program is 84 lines,包括一些测试。

答案 1 :(得分:4)

为什么不使用PHP解析器?

 $terms=array('and','or','not','A','B','C','D'...);
 $values=array('*','+','!',1,1,0,0,1....);

 $expression="A and B or C and (D or F or not G)";
 $expression=preg_replace($terms, $values,$expression);
 $expression=preg_replace('^(+|-|!|1|0)','',$expression);
 $result=eval($expression);

实际上,第二个正则表达式是错误的(只有在需要阻止任何代码注入时才需要) - 但是你明白了。

下进行。

答案 2 :(得分:2)

我会选择Pratt解析器。这几乎就像递归下降但更聪明:) Douglas Crockford(JSLint成名)here的一个不错的解释。

答案 3 :(得分:2)

Dijkstra's shunting yard algorithm是从中缀到后缀/图形的传统方法。

答案 4 :(得分:2)

我已经实现了plinth建议的分流码算法。但是,此算法只为您提供后缀表示法,即反向波兰表示法(RNP)。您仍然需要对其进行评估,但是一旦在RNP中使用表达式(例如here所述),这就很容易了。

下面的代码可能不是很好的PHP风格,我的PHP知识有点受限。尽管如此,这应该足够了。

$operators = array("and", "or", "not");
$num_operands = array("and" => 2, "or" => 2, "not" => 1);
$parenthesis  = array("(", ")");

function is_operator($token) {
    global $operators;
    return in_array($token, $operators);
}

function is_right_parenthesis($token) {
    global $parenthesis;
    return $token == $parenthesis[1];
}

function is_left_parenthesis($token) {
    global $parenthesis;
    return $token == $parenthesis[0];
}

function is_parenthesis($token) {
    return is_right_parenthesis($token) || is_left_parenthesis($token);
}

// check whether the precedence if $a is less than or equal to that of $b
function is_precedence_less_or_equal($a, $b) {
    // "not" always comes first
    if ($b == "not")
        return true;

    if ($a == "not")
        return false;

    if ($a == "or" and $b == "and")
        return true;

    if ($a == "and" and $b == "or")
        return false;

    // otherwise they're equal
    return true;
}


function shunting_yard($input_tokens) {
    $stack = array();
    $output_queue = array();

    foreach ($input_tokens as $token) {
        if (is_operator($token)) {
            while (is_operator($stack[count($stack)-1]) && is_precedence_less_or_equal($token, $stack[count($stack)-1])) {
                    $o2 = array_pop($stack);
                    array_push($output_queue, $o2);
            }
            array_push($stack, $token);

        } else if (is_parenthesis($token)) {
            if (is_left_parenthesis($token)) {
                array_push($stack, $token);
            } else {
                while (!is_left_parenthesis($stack[count($stack)-1]) && count($stack) > 0) {
                    array_push($output_queue, array_pop($stack));
                }
                if (count($stack) == 0) {
                    echo ("parse error");
                    die();
                }
                $lp = array_pop($stack);
            }
        } else {
            array_push($output_queue, $token);  
        }
    }

    while (count($stack) > 0) {
        $op = array_pop($stack);
        if (is_parenthesis($op))
            die("mismatched parenthesis");
        array_push($output_queue, $op);
    }

    return $output_queue;
}

function str2bool($s) {
    if ($s == "true")
        return true;
    if ($s == "false")
        return false;
    die('$s doesn\'t contain valid boolean string: '.$s.'\n');
}

function apply_operator($operator, $a, $b) {
    if (is_string($a))
        $a = str2bool($a);
    if (!is_null($b) and is_string($b))
        $b = str2bool($b);

    if ($operator == "and")
        return $a and $b;
    else if ($operator == "or")
        return $a or $b;
    else if ($operator == "not")
        return ! $a;
    else die("unknown operator `$function'");
}

function get_num_operands($operator) {
    global $num_operands;
    return $num_operands[$operator];
}

function is_unary($operator) {
    return get_num_operands($operator) == 1;
}

function is_binary($operator) {
    return get_num_operands($operator) == 2;
}

function eval_rpn($tokens) {
    $stack = array();
    foreach ($tokens as $t) {
        if (is_operator($t)) {
            if (is_unary($t)) {
                $o1 = array_pop($stack);
                $r = apply_operator($t, $o1, null);
                array_push($stack, $r);
            } else { // binary
                $o1 = array_pop($stack);
                $o2 = array_pop($stack);
                $r = apply_operator($t, $o1, $o2);
                array_push($stack, $r);
            }
        } else { // operand
            array_push($stack, $t);
        }
    }

    if (count($stack) != 1)
        die("invalid token array");

    return $stack[0];
}

// $input = array("A", "and", "B", "or", "C", "and", "(", "D", "or", "F", "or", "not", "G", ")");
$input = array("false", "and", "true", "or", "true", "and", "(", "false", "or", "false", "or", "not", "true", ")");
$tokens = shunting_yard($input);
$result = eval_rpn($tokens);
foreach($input as $t)
    echo $t." ";
echo "==> ".($result ? "true" : "false")."\n";

答案 5 :(得分:0)

您可以使用LR解析器构建解析树,然后评估树以获取结果。包含示例的详细说明可在Wikipedia中找到。如果你还没有自己编码,我今晚会写一个小例子。

答案 6 :(得分:0)

最简单的方法是使用正则表达式将表达式转换为php语法中的表达式,然后使用eval,如symcbean所示。但我不确定你是否想在生产代码中使用它。

另一种方法是编写自己的简单recursive descent parser。它并不像听起来那么难。对于像你这样的简单语法(布尔表达式),你可以从头开始轻松编写代码。你也可以使用类似于ANTLR for php的解析器生成器,可能会搜索一个php解析器生成器会发现一些东西。