最近在写一个web数据通用采集器。简单讲,就是对某个目标网站编写特定的规则,然后采集器可以根据这个规则去目标网站采集相应的数据。
比如我有条规则就是针对http://xiaohua.zol.com.cn/这个网站的。
然而在遇到一个特定的页面时,采集器报错。这个页面的地址是:http://xiaohua.zol.com.cn/lengxiaohua/34.html
其实这个页面和其他页面除了内容有所不同外,结构是一样的。而且这个页面的代码也没有语法错误。但奇怪的是,其他的页面,比如http://xiaohua.zol.com.cn/lengxiaohua/33.html或者http://xiaohua.zol.com.cn/lengxiaohua/35.html都不会报错,正常采集。
由于我用的laravel框架,采用了querylist这个专门针对数据采集的包。通过调试发现只要我用到setHtml()方法,得到的html代码就会被破坏。html代码结构被破坏后,就无法得到正常的domdocument,就无法进行解析了。
... $listurl = 'http://xiaohua.zol.com.cn/lengxiaohua/34.html'; $listtaskpid = $v['id']; echo "采集列表页任务::".$listurl."\n"; $listql = $ql->myGet($listurl,[],$options); $listql->use(AbsoluteUrl::class); if($charset != 'utf-8'){ $listhtml = Http::get($listurl,[],$options)->body(); //dump($listhtml); $listhtml = mb_convert_encoding($listhtml,'UTF-8','GBK'); //dump($listhtml); $listhtml = preg_replace('/<meta[^<>]*charset[^<>]*?>/i', '', $listhtml); $listhtml = preg_replace('/<!--.*?-->/i', '', $listhtml); //dump($listhtml);//这里打印的html内容还是正常的 $listql->setHtml($listhtml); } if ($listql->getHtml() == ''){ continue; } //dump($listql);//在这一步,可以发现打印出的结果就和预期的html内容不一样了,也就是被破坏了 //由于html结构已经被破坏,下一步已经无法解析出正确的数据了 $listdata = $listql->absoluteUrl($listurl)->rules($listrule)->range($listrange)->query()->getData();
进一步研究发现querylist用的是phpquery的包。于是我采用原生php编写了专门的代码进行测试,发现phpquery也同样会遇到问题。
<?php ini_set('display_errors',1); //错误信息 ini_set('display_startup_errors',1); //php启动错误信息 error_reporting(-1); require('phpQuery/phpQuery.php'); $listurl = 'http://xiaohua.zol.com.cn/lengxiaohua/34.html'; $listhtml = file_get_contents($listurl); $listhtml = mb_convert_encoding($listhtml,'UTF-8','GBK'); $listhtml = preg_replace('/<meta[^<>]*charset[^<>]*?>/i', '<meta http-equiv="Content-Type" content="text/html;charset=utf-8">', $listhtml); $listhtml = preg_replace('/<!--.*?-->/i', '', $listhtml); //以下是为方便直接照搬的phpQueryCli的代码 $argv = array("./cli/phpquery","--find",".article-list","--contents"); phpQueryCli($listhtml, array_slice($argv, 1)); function phpQueryCli($markup, $callQueue) { $pq = phpQuery::newDocument($markup); $method = null; $params = array(); foreach($callQueue as $param) { if (strpos($param, '--') === 0) { if ($method) { $pq = call_user_func_array(array($pq, $method), $params); } $method = substr($param, 2); // delete -- $params = array(); } else { $param = str_replace('\n', "\n", $param); $params[] = strtolower($param) == 'null' ? null : $param; } } if ($method) $pq = call_user_func_array(array($pq, $method), $params); if (is_array($pq)) foreach($pq as $v) print $v; else print $pq."\n"; //var_dump($pq); }
再进一步,phpquery是利用了php domdocument来处理html代码的。于是我直接用原生的PHP写了代码,发现domdocument也无法解决问题。
<?php ini_set('display_errors',1); //错误信息 ini_set('display_startup_errors',1); //php启动错误信息 error_reporting(-1); require('phpQuery/phpQuery.php'); $listurl = 'http://xiaohua.zol.com.cn/lengxiaohua/34.html'; $listhtml = file_get_contents($listurl); $listhtml = mb_convert_encoding($listhtml,'UTF-8','GBK'); $listhtml = preg_replace('/<meta[^<>]*charset[^<>]*?>/i', '<meta http-equiv="Content-Type" content="text/html;charset=utf-8">', $listhtml); $listhtml = preg_replace('/<!--.*?-->/i', '', $listhtml); $doc = new DOMDocument(); $doc->validateOnParse = true; $doc->loadHTML($listhtml,LIBXML_HTML_NODEFDTD); $doc->encoding = 'UTF-8'; echo $doc->saveHTML(); //这里echo的结果和预期的html内容不一致
最后问题指向这里:即DOMDocument的loadHtml()方法。这个方法,只能在PHP的源码里找了。
同时我验证了一下是什么原因导致DOMDocument出现异常的。我把http://xiaohua.zol.com.cn/lengxiaohua/34.html这个页面的html代码存在本地,将第378行的一个尖括号删除,再测试就正常了。如图:
为方便,我直接用phpQuery的客户端模式进行测试,如下:
cat 34utf8.html | php ./cli/phpquery –find ‘.article-list’ –contents
我用的PHP版本是7.3.9,解压源码,vim ext/dom/document.c,看看具体的实现:
staticvoid dom_load_html(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{ */{zval *id;xmlDoc *docp = NULL, *newdoc;dom_object *intern;dom_doc_propsptr doc_prop;char *source;size_t source_len;int refcount, ret;zend_long options = 0;htmlParserCtxtPtr ctxt;id = getThis();if (zend_parse_parameters(ZEND_NUM_ARGS(), “s|l”, &source, &source_len, &options) == FAILURE) {return;}if (!source_len) {php_error_docref(NULL, E_WARNING, “Empty string supplied as input”);RETURN_FALSE;}if (ZEND_LONG_EXCEEDS_INT(options)) {php_error_docref(NULL, E_WARNING, “Invalid options”);RETURN_FALSE;}if (mode == DOM_LOAD_FILE) {if (CHECK_NULL_PATH(source, source_len)) {php_error_docref(NULL, E_WARNING, “Invalid file source”);RETURN_FALSE;}ctxt = htmlCreateFileParserCtxt(source, NULL);} else {source_len = xmlStrlen((xmlChar *) source);if (ZEND_SIZE_T_INT_OVFL(source_len)) {php_error_docref(NULL, E_WARNING, “Input string is too long”);RETURN_FALSE;}ctxt = htmlCreateMemoryParserCtxt(source, (int)source_len);}
虽然C语言这种太底层的语言理解起来有点麻烦,但是基本看得出,loadHtml主要是由这一段代码来执行的:
source_len = xmlStrlen((xmlChar *) source);if (ZEND_SIZE_T_INT_OVFL(source_len)) {php_error_docref(NULL, E_WARNING, “Input string is too long”);RETURN_FALSE;}ctxt = htmlCreateMemoryParserCtxt(source, (int)source_len);


发表回复