10,000 英尺概览
您需要使用小型自定义解析器来执行此操作:代码获取此表单的输入并将其转换为您想要的表单。
在实践中,我发现根据复杂性将此类解析问题归为以下三个类别之一很有用:
-
琐碎:可以通过几个循环和人性化的正则表达式解决的问题。这个类别很诱人:如果您甚至有点不确定问题是否可以通过这种方式解决,一个好的经验法则是确定它不能。
-
简单: 需要自己构建一个小型解析器的问题,但仍然足够简单,因此拿出大炮没有什么意义。如果您需要编写超过 100 行代码,请考虑升级到下一个类别。
-
涉及的问题:正式化并使用已经存在的、经过验证的解析器生成器¹是有意义的问题。
我将此特定问题归为第二类,这意味着您可以这样处理它:
编写一个小型解析器
定义语法
为此,您必须首先定义——至少是非正式的,并附上一些简短的注释——你要解析的语法。请记住,大多数语法都是在某些时候递归定义的。所以假设我们的语法是:
- 输入是一个序列
-
序列是一系列零个或多个标记
- 标记可以是单词、字符串或数组
- 标记由一个或多个空格字符分隔
-
单词是一系列字母字符 (a-z)
-
字符串是用双引号括起来的任意字符序列
-
数组是由逗号分隔的一系列一个或多个标记
你可以看到我们在一个地方有递归:一个序列可以包含数组,一个数组也是根据一个序列定义的(所以它可以包含更多的数组等)。
像上面那样非正式地处理这个问题作为介绍更容易,但是如果你这样做formally,对语法的推理会更容易。
构建词法分析器
掌握了语法,您知道需要将输入分解为标记,以便对其进行处理。接受用户输入并将其转换为语法定义的各个部分的组件称为 lexer。词法分析器是愚蠢的;他们只关心输入的“外观”,并不试图检查它是否真的有意义。
这是我为解析上述语法而编写的一个简单的词法分析器(不要将它用于任何重要的事情;可能包含错误):
$input = 'all ("hi there", (this, that) , other) another';
$tokens = array();
$input = trim($input);
while($input) {
switch (substr($input, 0, 1)) {
case '"':
if (!preg_match('/^"([^"]*)"(.*)$/', $input, $matches)) {
die; // TODO: error: unterminated string
}
$tokens[] = array('string', $matches[1]);
$input = $matches[2];
break;
case '(':
$tokens[] = array('open', null);
$input = substr($input, 1);
break;
case ')':
$tokens[] = array('close', null);
$input = substr($input, 1);
break;
case ',':
$tokens[] = array('comma', null);
$input = substr($input, 1);
break;
default:
list($word, $input) = array_pad(
preg_split('/(?=[^a-zA-Z])/', $input, 2),
2,
null);
$tokens[] = array('word', $word);
break;
}
$input = trim($input);
}
print_r($tokens);
构建解析器
完成此操作后,下一步是构建一个 parser:一个检查词法输入并将其转换为所需格式的组件。解析器很聪明;在转换输入的过程中,它还确保输入符合语法规则。
解析器通常被实现为state machines(也称为有限状态机或有限自动机),其工作方式如下:
- 解析器有一个状态;这通常是一个适当范围内的数字,但每个状态也用更人性化的名称来描述。
- 有一个循环,每次读取一个读取 lexed 标记。根据当前状态和令牌的值,解析器可以决定执行以下一项或多项操作:
- 采取一些影响其输出的操作
- 将其状态更改为其他值
- 判断输入格式错误并产生错误
¹ 解析器生成器是一种程序,其输入是形式语法,输出是词法分析器和解析器,您可以“加水”到:只需扩展代码以根据标记的类型执行“采取一些行动”;其他一切都已经处理好了。快速搜索这个主题给出了 led PHP Lexer and Parser Generator?