[学习中] 使用lxml解析HTML

用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%']")

这样就取出了bodystylefont-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则是ptext(). 函数position()的意思是取得节点的位置。比如li[position()=2]表示取得第二个li节点, 它也可以被省略为li[2].

不过要注意的是数字定位和过滤条件的顺序. 比如ul/li[5][@name='hello']表示取ul下第5项li, 并且其name必须是hello, 否则返回空. 而如果用ul/li[@name='hello'][5]的意思就不同, 它表示寻找ul下第5个namehelloli节点.

此外, *可以代替所有的节点名, 比如用/html/body/*/span可以取出body下第二级的所有span, 而不管它上一级是div还是p或是其它什么东东.

descendant::前缀可以指代任意多层的中间节点, 它也可以被省略成一个/. 比如在整个HTML文档中查找idleftmenudiv,可以用/descendant::div[@id='leftmenu'],也可以简单地使用//div[@id='leftmenu']

text = page.xpath(u"/descendant::*[text()]")表示任意多层的中间节点下任意标签之间的内容, 也即实现蜘蛛抓取页面内容功能. 以下内容使用text属性是取不到的:

<div class="news">
    1. <b>无流量站点清理公告</b>&nbsp;&nbsp;2013-02-22<br/>
    取不到的内容
</div>
<div class="news">
    2. <strong>无流量站点清理公告</strong>&nbsp;&nbsp;2013-02-22<br/>
    取不到的内容
</div>
<div class="news">
    3. <span>无流量站点清理公告</span>&nbsp;&nbsp;2013-02-22<br/>
    取不到的内容
</div>
<div class="news">
    4. <u>无流量站点清理公告</u>&nbsp;&nbsp;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_onlyFalse时保证页面的完整性, 否则, 这个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']")

【记录】Python中尝试用lxml去解析html