[学习中] 使用lxml解析HTML
page = etree.HTML(html.lower().decode('utf-8'))
其中decode('utf-8')
是针对编码为utf8
的网站
通常要先判断网页编码, 可以使用chardet第三方库进行判断
import chardet
encoding = chardet.detect(html)['encoding']
htmlEl = etree.HTML(html.lower().decode(encoding, 'ignore'))
遇到UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)
错误
加上.encode('utf-8')
解决
扩展的示例:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from lxml import etree
import chardet
html = '''
<html>
<head>
<meta name="content-type" content="text/html; charset=utf-8" />
<title>友情链接查询 - 站长工具</title>
<!-- uRj0Ak8VLEPhjWhg3m9z4EjXJwc -->
<meta name="Keywords" content="友情链接查询" />
<meta name="Description" content="友情链接查询" />
</head>
<body>
<h1 class="heading">Top News</h1>
<p style="font-size: 200%">World News only on this page</p>
Ah, and here's some more text, by the way.
<p>... and this is a parsed fragment ...</p>
<a href="http://www.cydf.org.cn/" rel="nofollow" target="_blank">青少年发展基金会</a>
<a href="http://www.4399.com/flash/32979.htm" target="_blank">洛克王国</a>
<a href="http://www.4399.com/flash/35538.htm" target="_blank">奥拉星</a>
<a href="http://game.3533.com/game/" target="_blank">手机游戏</a>
<a href="http://game.3533.com/tupian/" target="_blank">手机壁纸</a>
<a href="http://www.4399.com/" target="_blank">4399小游戏</a>
<a href="http://www.91wan.com/" target="_blank">91wan游戏</a>
<div>hello<p>world</p></div>
</body>
</html>
'''
encoding = chardet.detect(html)['encoding']
print encoding
from lxml.html.clean import Cleaner
cleaner = Cleaner(style=False, scripts=False, page_structure=False, safe_attrs_only=False)
print html
print cleaner.clean_html(html)
# page = etree.HTML(html.lower().decode('utf-8'))
page = etree.HTML(html.decode(encoding, 'ignore'))
# 获得所有链接
hrefs = page.xpath(u"//a")
for href in hrefs:
print href.attrib
if href.text is not None:
print href.text.encode('utf-8')
# 输出:
# {'href': 'http://www.cydf.org.cn/', 'target': '_blank', 'rel': 'nofollow'} 青少年发展基金会
# {'href': 'http://www.4399.com/flash/32979.htm', 'target': '_blank'} 洛克王国
# {'href': 'http://www.4399.com/flash/35538.htm', 'target': '_blank'} 奥拉星
# {'href': 'http://game.3533.com/game/', 'target': '_blank'} 手机游戏
# {'href': 'http://game.3533.com/tupian/', 'target': '_blank'} 手机壁纸
# {'href': 'http://www.4399.com/', 'target': '_blank'} 4399小游戏
# {'href': 'http://www.91wan.com/', 'target': '_blank'} 91wan游戏
# 获得标题
title = page.find(".//title").text.encode('utf-8')
print "title:", title # title: 友情链接查询 - 站长工具
# 获得description
# description = page.find(u".//meta[@name='description']")
description = page.xpath("//meta[translate(@name, 'ABCDEFGHJIKLMNOPQRSTUVWXYZ', 'abcdefghjiklmnopqrstuvwxyz')='description']")
if description is not None:
# description = description.attrib["content"].encode('utf-8')
description = description[0].attrib["content"].encode('utf-8')
print "description:", description # description: 友情链接查询
# 获得keywords
# keywords = page.find(".//meta[@name='keywords']")
keywords = page.xpath("//meta[translate(@name, 'ABCDEFGHJIKLMNOPQRSTUVWXYZ', 'abcdefghjiklmnopqrstuvwxyz')='keywords']")
if keywords is not None:
# keywords = keywords.attrib["content"].encode('utf-8')
keywords = keywords[0].attrib["content"].encode('utf-8')
print "keywords:", keywords # keywords: 友情链接查询
ps = page.xpath(u"/html/body/p[@style='font-size: 200%']")
for p in ps:
print p.text.encode('utf-8')
div = page.xpath(u"//div[text()='hello']")
print div[0].text
XPATH是用一种类似目录树的方法来描述在XML文档中的路径. 比如用/
来作为上下层级间的分隔. 第一个/
表示文档的根节点(注意, 不是指文档最外层的tag节点, 而是指文档本身). 比如对于一个HTML文件来说, 最外层的节点应该是"/html".
定位某一个HTML标签, 可以使用类似文件路径里的绝对路径, 如page.xpath(u"/html/body/p")
,它会找到body这个节点下所有的p
标签; 也可以使用类似文件路径里的相对路径, 可以这样使用: page.xpath(u"//p"), 它会找到整个html代码里的所有p标签:
<p style="font-size: 200%">World News only on this page</p>
Ah, and here's some more text, by the way.
<p>... and this is a parsed fragment ...</p>
注意:XPATH返回的不一定就是唯一的节点, 而是符合条件的所有节点. 如上所示, 只要是body里的p标签, 不管是body的第一级节点, 还是第二级, 第三级节点, 都会被取出来.
如果想进一步缩小范围, 直接定位到<p style="font-size: 200%">World News only on this page</p>
要怎么做呢? 这就需要增加过滤条件. 过滤的方法就是用[]
把过滤条件加上. lxml里有个过滤语法:
p = page.xpath(u"/html/body/p[@style='font-size: 200%']")
或者:
p = page.xpath(u"//p[@style='font-size:200%']")
这样就取出了body
里style
为font-size:200%
的p
节点, 注意: 这个p
变量是一个lxml.etree._Element
对象列表, p[0].text
结果为World News only on this page
, 即标签之间的值: p[0].values()
结果为font-size: 200%
, 即所有属性值. 其中 @style
表示属性style
,类似地还可以使用如@name
, @id
, @value
, @href
, @src
, @class
...
如果标签里面没有属性怎么办? 那就可以用text()
, position()
等函数来过滤, 函数text()
的意思则是取得节点包含的文本. 比如:<div>hello<p>world</p></div>
中, 用div[text()='hello']
即可取得这个div
, 而world
则是p
的text()
. 函数position()
的意思是取得节点的位置。比如li[position()=2]
表示取得第二个li节点, 它也可以被省略为li[2]
.
不过要注意的是数字定位和过滤条件的顺序. 比如ul/li[5][@name='hello']
表示取ul
下第5项li
, 并且其name
必须是hello
, 否则返回空. 而如果用ul/li[@name='hello'][5]
的意思就不同, 它表示寻找ul
下第5个name
为hello
的li
节点.
此外, *
可以代替所有的节点名, 比如用/html/body/*/span
可以取出body下第二级的所有span, 而不管它上一级是div还是p或是其它什么东东.
而descendant::
前缀可以指代任意多层的中间节点, 它也可以被省略成一个/
. 比如在整个HTML文档中查找id
为leftmenu
的div
,可以用/descendant::div[@id='leftmenu']
,也可以简单地使用//div[@id='leftmenu']
。
text = page.xpath(u"/descendant::*[text()]")
表示任意多层的中间节点下任意标签之间的内容, 也即实现蜘蛛抓取页面内容功能. 以下内容使用text属性是取不到的:
<div class="news">
1. <b>无流量站点清理公告</b> 2013-02-22<br/>
取不到的内容
</div>
<div class="news">
2. <strong>无流量站点清理公告</strong> 2013-02-22<br/>
取不到的内容
</div>
<div class="news">
3. <span>无流量站点清理公告</span> 2013-02-22<br/>
取不到的内容
</div>
<div class="news">
4. <u>无流量站点清理公告</u> 2013-02-22<br/>
取不到的内容
</div>
这些“取不到的内容”使用这个是取不到的. 怎么办呢? 别担心, lxml还有一个属性叫做“tail”, 它的意思是结束节点前面的内容, 也就是说在<br/>
与</div>
之间的内容. 它的源码里面的意思是“text after end tag”
至于following-sibling::
前缀就如其名所说, 表示同一层的下一个节点. following-sibling::*
就是任意下一个节点, 而following-sibling::ul
就是下一个ul节点.
如果script与style标签之间的内容影响解析页面, 或者页面很不规则, 可以使用lxml.html.clean
模块. 模块lxml.html.clean
提供一个Cleaner类来清理HTML页. 它支持删除嵌入脚本内容、特殊标记、CSS样式注释或者更多.
cleaner = Cleaner(style=True, scripts=True, page_structure=False, safe_attrs_only=False)
print cleaner.clean_html(html)
注意, page_structure
, safe_attrs_only
为False
时保证页面的完整性, 否则, 这个Cleaner会把你的html结构与标签里的属性都给清理了. 使用Cleaner类要十分小心, 小心擦枪走火.
这里有详细的Cleaner类初始化参数说明:http://lxml.de/api/lxml.html.clean.Cleaner-class.html
忽略大小写可以:
page = etree.HTML(html)
keyword_tag = page.xpath("//meta[translate(@name, 'ABCDEFGHJIKLMNOPQRSTUVWXYZ', 'abcdefghjiklmnopqrstuvwxyz')='keywords']")