在 PHP 中对 HTML 文件使用 XPath
我最近开始考虑让自己成为PHPZend认证工程师,在做了一些研究之后,我发现标准的PHP字符串和数组函数似乎是考试材料的很大一部分。因此,作为起点(以及将来的修订),我决定为这些功能创建修订表可能是个好主意。
我想要一种自动方式来获取函数定义,而不是手动完成这项工作。可以从PHP网站下载PHP文档作为许多HTML文件的集合。这为我提供了开始提取必要信息所需的文件。我需要的函数声明分布在两种类型的HTML文件中。有一个索引文件,其中包含对函数的单行描述,每行都链接到详细描述该函数的页面。内页包含函数声明,所以我需要的是从函数索引文件中提取所有链接以及每个链接页面顶部的所有声明。
为了做到这一点,我使用了PHP中的DOM类,这些类在PHP版本5中被引入。这些类是解析XML和HTML的好方法,而不会弄乱正则表达式,因为正则表达式可能会很快出错。在学习了如何使用它们之后,DOM类现在是我从HTML文件中提取数据的首选方法。然而,由于HTML作为一堆标签完全没有问题,我们需要首先抑制由于任何HTML异常而产生的错误。这可以使用libxml_use_internal_errors()函数来完成,我们传递了一个true值。
libxml_use_internal_errors(TRUE);
我们可以选择使用该libxml_get_errors()函数来获取文档中的任何错误,但我们只会将这些错误扔掉,因此没有必要这样做。这个函数返回一个错误数组,所以如果你愿意,你可以遍历它们并尝试对它们做一些事情。
然后我获取字符串函数索引页面的内容,并使用该方法将其传递给一个DOMDocument对象loadHTMLFile(),从而创建一个HTMLDOM。这将创建一个基于HTML文件的可用DOM对象,然后将其传递到一个新的DOMXPath对象中,以便我可以使用XPath查询HTMLDOM。
$dom = new DOMDocument(); $dom->loadHTMLFile('ref.strings.html'); $x = new DOMXPath($dom);
XPath是一种XML查询语言,用于在XML文档中查找元素并且非常易于使用。我们本质上需要做的是找到所有锚元素(a),它们是列表元素(li)的子元素,它们是类值为“chunklistchunklist_reference”的无序列表元素(ul)的子元素。所述DOMXPath对象用于运行使用适当命名XPath查询query()方法。该query()方法返回一个可遍历的DOMNodeList对象,其中包含DOMNode对象的列表。这意味着我们可以在foreach()循环中使用它来单独查看每个DOMNode对象。我们可以通过DOMNode的getAttribute()方法来获取锚标签的href对象并询问它的“href”属性。以下代码将打印出链接中找到的所有href属性。
foreach($x->query("//ul[@class='chunklistchunklist_reference']/li/a")as$node){")as$node){")as$node){") as $node) { $href = $node->getAttribute("href"); echo $href . PHP_EOL; }
现在我拥有了所有需要的文件引用,我可以使用新的DOMDocument对象加载内部HTML并运行不同的XPath查询。然而,这一次,我们应该只返回一个结果(即函数定义),因此我们只需要获取该单个项目。这可以通过使用DOMNodeList对象的item()方法来完成。这是在内部函数页面中加载的代码(基于我们在上面的循环中选取的href)并查找包含在具有“methodsynopsisdc-description”类属性的div元素中的函数定义。
$function_dom = new DOMDocument(); $function_dom->loadHTMLFile('php-chunked-xhtml/' . $href); $function_x = new DOMXPath($function_dom); //选择函数定义 $function_node_list = $function_x->query("//div[@class='methodsynopsisdc-description']");");");"); $function_node = $function_node_list->item(0);
我们现在拥有的HTMLDOM在原始页面中包含以下标记,这比我们实际需要的信息多得多。函数定义本身包含许多分隔每个组件的内部标签。我们现在拥有的是一个DOMNode对象,它包含几个子DOMNode对象,我们现在需要将其转换为文本格式。我们可以做的是遍历这棵DOMNode对象树,从每个对象中提取文本内容,一次一个。幸运的是,DOMNode对象有一个名为textContent的属性,它已经包含了这个对象和所有子对象的文本。因此,我们可以像这样提取DOMNode树的内容:
$function_definition=$function_node->textContent;
由于删除HTML标记后留下的空白,这产生的实际文本内容有点混乱。所以定义只需要通过几个清理步骤来整理输出。
$function_definition = trim(preg_replace("/\s{2,}/", ' ', $function_node->textContent)); $function_definition = str_replace(array(' (', '( ', ' )'), array('(', '(', ')'), $function_definition);
我遇到的一个问题是,函数列表中的某些页面实际上是别名,因此与普通函数页面的结构略有不同。这意味着我们之前运行的XPath查询将找不到任何东西。如果发生这种情况,那么DOMXPath对象的query()方法将返回一个NULL值,我们可以很容易地检测到该值。然后我们需要做的就是运行一个稍微不同的查询来挑选别名定义。
if (is_null($function_node)) { //这是一个别名,与功能页面的结构略有不同 $alias_node_list = $function_x->query("//p[@class='refpurpose']"); $function_node = $alias_node_list->item(0); }
我想做的最后一件事是从定义中提取函数名称。这可以通过使用XPath子查询轻松完成。如果您将DOMNode对象作为第二个参数传递给该query()方法,则您运行的查询将与该DOM对象相关。这意味着我可以在函数声明节点中搜索具有特定类属性的跨度,而不必担心相同的东西是否出现在全局DOM中的其他地方。函数和别名所需的XPath查询在这里略有不同,因此我将它们放在下面。
//函数名 $function_name = $function_x->query("./span[@class='methodname']/strong", $function_node)->item(0)->textContent; //别名 $function_name = $function_x->query("./span[@class='refname']", $function_node)->item(0)->textContent;
上面的所有代码都可以组合成一个函数。以下代码将列表页面位置作为输入,并将其中的函数定义提取到单个数组中,即返回的数组。
function get_function_list($href) { //关闭无效的HTML错误 libxml_use_internal_errors(TRUE); $functions = array(); //解析主HTML文档 $dom = new DOMDocument(); $dom->loadHTMLFile($href); $x = new DOMXPath($dom); //获取所有功能页面链接 foreach($x->query("//ul[@class='chunklistchunklist_reference']/li/a")as$node){")as$node){")as$node){") as $node) { $href = $node->getAttribute("href"); //获取函数文件内容并解析 $function_dom = new DOMDocument(); $function_dom->loadHTMLFile($href); $function_x = new DOMXPath($function_dom); //选择函数定义 $function_node_list = $function_x->query("//div[@class='methodsynopsisdc-description']");");");"); $function_node = $function_node_list->item(0); if (is_null($function_node)) { //这是一个别名,与功能页面的结构略有不同 $alias_node_list = $function_x->query("//p[@class='refpurpose']"); $function_node = $alias_node_list->item(0); //查询别名xpath查询结果 $function_name = $function_x->query("./span[@class='refname']", $function_node)->item(0)->textContent; } else { //获取函数名 $function_name = $function_x->query("./span[@class='methodname']/strong", $function_node)->item(0)->textContent; } //将内容提取为字符串,去除一些空格 $function_definition = trim(preg_replace("/\s{2,}/", ' ', $function_node->textContent)); $function_definition = str_replace(array(' (', '( ', ' )'), array('(', '(', ')'), $function_definition); //将函数添加到我们的定义列表中 $functions[$function_name] = $function_definition; } return $functions; }
这是我用来运行上述函数并将输出保存到文件中的代码。正如我之前所说,我只想提取字符串和数组函数,所以我只查看那些索引文件。
$file_contents = ''; $file_contents .= '--STRING FUNCTIONS--' . PHP_EOL; $functions = get_function_list('ref.strings.html'); foreach ($functions as $function) { $file_contents .= $function . PHP_EOL; } file_put_contents('string_functions.txt', $file_contents); $file_contents = ''; $file_contents .= '--ARRAY FUNCTIONS--' . PHP_EOL; $functions = get_function_list('ref.array.html'); foreach ($functions as $function) { $file_contents .= $function . PHP_EOL; } file_put_contents('array_functions.txt', $file_contents);
我现在有两个文件,其中包含PHP中可用的数组和字符串函数,我打印出来并贴在我办公室的墙上作为修订指南。