10,000 英尺概述
您需要使用小型自定义解析器来完成此操作:代码获取此表单的输入并将其转换为您想要的表单。
在实践中,我发现根据它们的复杂性将这样的解析问题分组到三个类别之一是很有用的:
- 琐碎:可以通过几个循环和人性化的正则表达式解决的问题。这个类别很诱人:如果您甚至有点不确定问题是否可以通过这种方式解决,一个好的经验法则是确定它不能。
- 简单:需要自己构建一个小型解析器的问题,但仍然足够简单,以至于拿出大炮没有什么意义。如果您需要编写超过 100 行代码,请考虑升级到下一个类别。
- 涉及的问题:正式化并使用已经存在的、经过验证的解析器生成器¹是有意义的问题。
我将此特定问题归类为第二类,这意味着您可以像这样处理它:
编写一个小型解析器
定义语法
为此,您必须首先定义——至少是非正式的,用一些简短的注释——你想要解析的语法。请记住,大多数语法都是在某些时候递归定义的。所以假设我们的语法是:
- 输入是一个序列
- 序列是一系列零个或多个令牌
- 标记可以是单词、字符串或数组
- 标记由一个或多个空格字符分隔
- 单词是一系列字母字符 (az )
- 字符串是用双引号括起来的任意字符序列
- 数组是由逗号分隔的一系列一个或多个标记
你可以看到我们在一个地方有递归:一个序列可以包含数组,一个数组也是根据一个序列定义的(所以它可以包含更多的数组等)。
像上面那样非正式地处理这件事作为介绍更容易,但如果你正式地进行语法推理,则更容易。
构建词法分析器
掌握了语法,您就知道需要将输入分解为标记,以便对其进行处理。接受用户输入并将其转换为语法定义的各个部分的组件称为词法分析器。词法分析器是愚蠢的;他们只关心输入的“外观”,并不试图检查它是否真的有意义。
这是我为解析上述语法而编写的一个简单的词法分析器(不要将它用于任何重要的事情;可能包含错误):
$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);
构建解析器
完成此操作后,下一步是构建解析器:检查词法输入并将其转换为所需格式的组件。解析器很聪明;在转换输入的过程中,它还确保输入符合语法规则。
解析器通常被实现为状态机(也称为有限状态机或有限自动机),并像这样工作:
- 解析器有一个状态;这通常是一个适当范围内的数字,但每个状态也用更人性化的名称来描述。
- 有一个循环,一次读取一个 lexed 标记。根据当前状态和令牌的值,解析器可以决定执行以下一项或多项操作:
- 采取一些影响其输出的行动
- 将其状态更改为其他值
- 确定输入格式错误并产生错误
¹ 解析器生成器是输入是形式语法并且输出是词法分析器和解析器的程序,您可以“只是加水”到:只需扩展代码以执行“采取一些行动”,具体取决于令牌的类型;其他一切都已经处理好了。对这个主题的快速搜索给出了导致PHP Lexer 和 Parser Generator?