闲来无事,想做个网页爬虫。用C++来做的话,当然是先要做HTTP下载了,最简单的方式当然是用libcurl实现,然后从网页信息中提取必要的信息即可。这其中,libcurl下载可以支持chunked流及gzip压缩的特殊情况,难点在于HTML的解析。
HTML是从XML来的,我用项目之前的xml封装库做解析,发现HTML文档并不能解析成功,但是在网页浏览器却可以成功,打开开发者工具查看DOM树,发现确实是中途多了一个</div>标签。在XML规范中这个就会失败,看来简单的通过XML解析引擎去解析不是很靠谱。
既然XML不行,直接上HTML解析库,看了下C++的就一个htmlcxx,demo看着就不是很好用。果断在github去看看,确实找了几个不错的,可惜不知为何,clone代码编译之后,自带的测试DEMO都会core,看来稳定性还是有点问题的(应该是兼容性吧)。
好吧,造轮子吧!
HTML文档其实可以看成是一个XML文档,只是有一些特有的地方。解析的话,思想就是先解析一个标签,从中取出属性,然后再找是是否是自关闭,未关闭继续在内容中递归。按照这个思路很容易写出对应的代码,设计一个元素类,内部包含熟悉、值、子元素、父元素。有了这些基本一个HTML文档就串联起来了。
当然如果一切顺利的话,解析起来不难,就如同XML引擎解析HTML失败,大量的HTML网页存在大量不规范的地方,即不符合XHTML规范。
总之不规范的地方还是挺多的,但基本都可以被浏览器支持的,说明浏览器的容错性还是不错的,据说就是因为较早版本的HTML规范没这么严格导致的。实现的时候还是要尽量的兼容一下。
我的实现基本上是以可以解析现有文档并提取有效信息为出发点,找了163等网站主页作为测试页,均已可以测试通过(网易主页也有很多不规范的地方)。同时为了保证内存泄露问题,采用了智能指针,子元素用shared_ptr封装,父元素用weak_ptr封装。
目前是将元素内的所有值(非子元素)全部保持到value中,比如
<html>
<body>
hello<a href="" />world
</body>
</html>
body元素的值会保存为hello world,我觉得应该分别作为子元素存在,目前还没有这么做。
另外在操作DOM的时候Javascript的getElementByXXX系列还是很好用的,有了前面的结构,通过递归也很容易实现,不过class一般可以配置多个,还需要分割。
当然存在的问题还很多,但已经基本够用。已分享到http://github.com/rangerlee/htmlparser。
写个demo测试下,用libcurl下载某网页到内存进行解析,提取美图地址。
HtmlParser parser;
shared_ptr<HtmlDocument> doc = parser.Parse(data.c_str(), data.size());
std::vector<shared_ptr<HtmlElement>> pics;
std::vector<shared_ptr<HtmlElement>> picbox = parser.GetElementByClassName("pic_box");
for (auto box : picbox) {
std::vector<shared_ptr<HtmlElement>> tmp = box->GetElementByTagName("img");
pics.insert(pics.end(), tmp.begin(), tmp.end());
}
for (auto v : pics) {
printf("img: %s\n", v->GetAttribute("src").c_str());
}
当然测试写的是C++11代码,解析器其实也可以自动适配支持非C++11编译器,使用tr1里面的库。
功能更新1
功能更新2