• Python爬虫技术系列-02HTML解析-xpath与lxml


    2 XPath介绍与lxml库

    参考连接:
    XPath教程
    https://www.w3school.com.cn/xpath/index.asp
    lxml文档
    https://lxml.de/index.html#support-the-project
    爬虫专栏
    https://blog.csdn.net/m0_38139250/category_12001010.html

    2.1 XPath概述

    XPath的中文名称为XML路径语言(XML Path Language),其最初的设计是用来搜索 XML 文档,但也适用于HTML文档搜索。1996年11月,XPath 成为W3C标准, XQuery 和 XPointer 都构建于 XPath 表达之上。
    XPath有着强大的搜索选择功能,提供了简洁的路径选择表达式, 提供了100+的内建函数,可以完成XML和HTML的绝大部分的定位搜索需求。
    XML和HTML均可通过树形结构的DOM(文档对象模型,Document Object Model)表示,DOM中包含元素节点,文本节点,属性节点三种节点。

    其中元素节点是DOM的基础,元素就是DOM中的标签,
    如<html>是根元素,代表整个文档,其他的元素还包括<head><body><div><ul><span>等,元素节点之间可以相互包含。
    
    文本节点:包含在元素节点中,
    比如<span>文本节点span>。
    
    属性节点:元素节点可以包含一些属性,属性的作用是对元素做出更具体的描述,
    如<span class="属性节点值">文本节点span>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    XPath的核心思想就是写地址,通过地址查找到XML和HTML中的元素,文本,属性等信息。
    获取元素n:

    //标签[@属性1="属性值1"]/标签[@属性2="属性值2"]/.../标签n
    
    • 1

    获取文本:

    //标签[@属性1="属性值1"]/标签[@属性2="属性值2"]/.../text()
    
    • 1

    获取属性n的值:

    //标签[@属性1="属性值1"]/标签[@属性2="属性值2"]/.../@属性n
    
    • 1

    [@属性1=“属性值1”]是谓语,用于过滤相同的标签,如果不需要通过属性过滤标签,可以不加谓语过滤。
    下面介绍XPath的节点类型和常用语法。

    1)节点(Node): XPath包括元素、属性、文本、命名空间、处理指令、注释以及文档(根)等七种类型的节点。XML 文档是被作为节点树来对待的。树的根被称为文档节点或者根节点。节点之间的关系包括父(Parent),子(Children),同胞(Sibling),先辈(Ancestor),后代(Descendant)。

    2)语法:
    XPath中,通过路径(Path)和步(Step)在XML文档中获取节点。
    a.常用的路径表达式
    常见的路径表达式如下表所示:
    XPath表达式与示例
    在这里插入图片描述b.谓语(Predicates)
    为查找特点节点或包含某个指定值的节点,可以使用谓语(Predicates),谓语用方括号[]表示,如:
    //div[@class=‘useful’]
    表示选取所有div 元素,且这些元素拥有值为 useful的 class属性。
    //div[@class=‘useful’]//li[last()]
    表示选取具有class值为useful的div标签下的任意li元素的最后一个li元素。
    c.选取未知节点
    XPath可以通过通配符搜索未知节点,如*表示匹配任何元素,@*表示匹配任何带有属性的节点,node()表示匹配任何类型的节点。如:
    //title[@*]
    表示选取所有带有属性的title元素。
    d.选取若干路径
    XPath可以通过“|”运算符表示选取若干路径。如
    //title | //price
    表示选取文档中的所有 title 和 price 元素

    3)轴与步:
    a.XPath轴(axis)
    轴表示当前节点的节点集XPath轴的名称见表13-2所示:
    表13-2 XPath轴名称与结果
    在这里插入图片描述
    b.步(Step)
    步可以根据当前节点集中的节点来进行计算搜索。
    步的语法:
    轴名称::节点测试[谓语]
    其中,轴(axis)表示所选节点与当前节点之间的关系,节点测试(node-test)表示是某给定轴内部的节点,谓语(predicate)用于搜索特定的节点集。
    步的使用如表13-3所示:
    在这里插入图片描述
    步的使用案例如下:
    //div[@class=“useless”]/descendant::a’)
    获取任意class属性值为useless的div标签下得所有子孙a标签节点。

    2.2 lxml库介绍

    Web数据展示都通过HTML格式,如果采用正则表达式匹配lxml是Python中的第三方库,主要用于处理搜索XML和HTML格式数据。

    2.2.1 lxml库安装

    安装lxml:

    pip install lxml==4.8.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
    
    • 1

    如果安装不成,可以在

    https://www.lfd.uci.edu/~gohlke/pythonlibs/
    
    • 1

    下载对应的whl安装包,然后安装即可。
    如果部分读者还是安装不成,可以把whl包解压,然后把解压后的两个文件夹放在python安装文件夹下的Lib\site-packages目录下即可。

    2.2.2 lxml库基本使用

    lxml的使用首先需要导入lxml的etree模块:

    from lxml import etree
    
    • 1

    etree模块可以对HTML文件进行自动修正,lxml中的相关使用方法如下:
    读取数据:

    etree.HTML(text, parser=None, base_url=None,)
    
    • 1

    第一个参数text为一个字符串,字符串应该可以转换为HTML或XML文档,如果字符串中的标签存在不闭合等问题,本方法会自动修正,并把文本转换成为HTML格式文档。返回结果类型为’lxml.etree._Element’。

    etree.fromstring(text, parser=None, base_url=None)
    
    • 1

    与etree.HTML()类似,但转换过程中,要求text字符串为标准的XML或HTML格式,否则会抛出异常。返回结果类型为’lxml.etree._Element’。

    etree.parse(source, parser=None, base_url=None) 
    
    • 1

    可如果没有解析器作为第二个参数提供,则使用默认解析器。返回一个加载了源元素的ElementTree对象,返回结果类型为’lxml.etree._ElementTree’。

    etree.tostring(element_or_tree, encoding=None,)
    
    • 1

    输出修正后的HTML代码,返回结果为bytes类型。

    搜索数据:
    假定有变量html为etree模块读取数据后返回’lxml.etree._Element’或’lxml.etree._ElementTree’类型,可以调用:

    html.xpath(self, _path, namespaces=None, extensions=None, smart_strings=True, **_variables)
    
    • 1

    _path为xpath中的路径表达式和步,xpath函数可以通过_path参数值实现对文档的搜索。

    2.2.3 lxml案例

    下面根据具体案例来介绍lxml的基本使用。

    a.读取数据并补全
    from lxml import etree
    # 定义一个不规则的html文本
    text = '''
    
    • 01 item
    • ''' html = etree.HTML(text) # etree把不规则文本进行修正 complete_html = etree.tostring(html) # toString可输出修正后的HTML代码,返回结果为bytes print("原数据------->", text) print("修正后的数据--->\n",complete_html.decode('utf-8')) # 输出修正后的html
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

    输出结果如下:

    原数据-------> 
    <html><body><div><ul>
            <li class="item-0">01 item</a></li>
    
    修正后的数据--->
     <html><body><div><ul>
            <li class="item-0">01 item</li>
    </ul></div></body></html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    从输出结果可以看出,etree.toString()可以对缺少闭合标签的HTML文档进行自动修正。

    etree模块可以调用HTML读取字符串,也可以调用parse()方法读取一个HTML格式的文件。把上面代码中的text变量保存在文本文件中,文件命名为lxml.html。

    from lxml import etree
    
    # 读取html文件
    html = etree.parse("./lxml.html",etree.HTMLParser())  # etree把不规则文本进行修正
    complete_html = etree.tostring(html)  # toString可输出修正后的HTML代码,返回结果为bytes
    print(complete_html.decode('utf-8'))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出结果如下:

    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
    <html><body>&#13;
    <div><ul>&#13;
        <li class="item-0"><a href="/link1.html">01 item</a></li></ul></div></body></html>
    
    • 1
    • 2
    • 3
    • 4

    从输出结果看可以看出,etree完成了HTML自动修正,同时还加上了!DOCTYPE标签。

    b.读取数据并选取节点:

    创建Demo11-03.html文件,内容如下:

    <!DOCTYPE html>
    <html>
    <body>
    <div class="useful">
        <ul>
            <li class="cla-0" id="id-0"><a href="/link1">01</a></li>
            <li class="cla-1"><a href="/link2">02</a></li>
            <li><strong><a href="/link3">03</a></strong></li>
            <li class="cla-1"><a href="/link4">04</a></li>
            <li class="cla-0"><a href="/link5">05</a></li>
        </ul>
    </div>
    <div class="useless">
        <ul>
            <li class="cla-0"><a href="/link1">useless-01</a></li>
            <li class="cla-1"><a href="/link2">useless-02</a></li>
        </ul>
    </div>
    </body>
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    导入库,并通过etree读取html文档:

    from lxml import etree
    # 加载HTML文档
    html = etree.parse("./Demo11-03.html",etree.HTMLParser())
    
    • 1
    • 2
    • 3

    00.获取根路径的div元素:

    print('--result00----/div-----')
    result00 = html.xpath('/div')  # 匹配/div节点
    print(result00)
    
    • 1
    • 2
    • 3

    输出如下:

    --result00----/div-----
    []
    
    • 1
    • 2

    因为根路径下标签为,所以无法匹配度根路径下的div标签。

    01.获取任意路径的div元素:

    print('--result01----/div-----')
    result00 = html.xpath('/div')  # 匹配所有div节点
    print(result01)
    
    • 1
    • 2
    • 3

    输出如下:

    --result01----//div-----
    [<Element div at 0x182e1169e80>, <Element div at 0x182e1169e00>]
    
    • 1
    • 2

    匹配到两个div元素。//表示任意路径。

    02.获取任意路径div元素的所以子节点:

    print('--result02----//div/*-----')
    result02 = html.xpath('//div/*')  # 匹配所有div节点的子节点
    print(result02)
    
    • 1
    • 2
    • 3

    输出如下:

    --result02----//div/*-----
    [<Element ul at 0x182e1169f80>, <Element ul at 0x182e1169fc0>]
    
    • 1
    • 2

    *表示匹配任意节点。

    03.匹配所有class属性的值:

    print('--result03----//@class-----')
    result03 = html.xpath('//@class')  # 匹配所有class属性的值
    print(result03)
    
    • 1
    • 2
    • 3

    输出如下:

    --result03----//@class-----
    ['useful', 'cla-0', 'cla-1', 'cla-1', 'cla-0', 'useless', 'cla-0', 'cla-1']
    
    • 1
    • 2

    @class表示获取属性class的值。

    04.获取任意路径下li标签的a标签子节点:

    print('--result04----//li/a-----')
    result04 = html.xpath('//li/a')  # 匹配所有li标签下的子节点a标签
    print(result04)
    
    • 1
    • 2
    • 3

    输出如下:

    --result04----//li/a-----
    [<Element a at 0x182e116a400>, <Element a at 0x182e116a480>, <Element a at 0x182e116a4c0>, <Element a at 0x182e116a500>, <Element a at 0x182e116a540>, <Element a at 0x182e116a5c0>]
    
    • 1
    • 2

    原始数据中一共7个a标签,返回值为6个a标签,是因为如下原始数据

    <li><strong><a href="/link3">03</a></strong></li>
    
    • 1

    a标签不是li标签的子节点。

    05.获取任意路径下li标签的任意a标签子孙节点:

    print('--result05----//li//a-----')
    result05 = html.xpath('//li//a')  # 匹配所有li标签下的所有a标签
    print(result05)
    
    • 1
    • 2
    • 3

    输出如下:

    --result05----//li//a-----
    [<Element a at 0x182e116a400>, <Element a at 0x182e116a480>, <Element a at 0x182e116a600>, <Element a at 0x182e116a4c0>, <Element a at 0x182e116a500>, <Element a at 0x182e116a540>, <Element a at 0x182e116a5c0>]
    
    • 1
    • 2

    原始数据中一共7个a标签,返回值为7个a标签,全部获取到。

    06. 匹配具有herf属性为/link2的元素的父元素的class属性的值:

    print('--result06----//a[@href="/link2"]/../@class-----')
    result06 = html.xpath('//a[@href="/link2"]/../@class')print(result06)
    
    • 1
    • 2

    输出如下:

    --result06----//a[@href="/link2"]/../@class-----
    ['cla-1', 'cla-1']
    
    • 1
    • 2

    …表示当前节点的父元素。

    07.查找所有class="cla-0"的li节点:

    print('--result07----//li[@class="cla-0"]-----')
    result07 = html.xpath('//li[@class="cla-0"]')  # 查找所有class="cla-0"的li节点:
    print(result07)
    
    • 1
    • 2
    • 3

    输出如下:

    --result07----//li[@class="cla-0"]-----
    [<Element li at 0x182e116a140>, <Element li at 0x182e116a2c0>, <Element li at 0x182e116a3c0>]
    
    • 1
    • 2

    //表示匹配任意路径,[]代表谓语,@class="cla-0"代表过滤出class属性值为cla-0的元素。

    08.获取a节点下的文本:

    print('--result08----//li[@class="cla-0"]/a/text()-----')
    result08_1 = html.xpath('//li[@class="cla-0"]/a/text()')  # 先选取a节点,再获取a节点下的文本
    print(result08_1)
    
    • 1
    • 2
    • 3

    输出如下:

    --result08----//li[@class="cla-0"]/a/text()-----
    ['01', '05', 'useless-01']
    
    • 1
    • 2

    text()表示获取匹配节点的文本内容。

    09.获取li节点下a节点的href属性:

    print('--result09----//li/a/@href-----')
    result09 = html.xpath('//li/a/@href')  # 获取li节点下a节点的href属性
    print(result09)
    
    • 1
    • 2
    • 3

    输出如下:

    --result09----//li/a/@href-----
    ['/link1', '/link2', '/link4', '/link5', '/link1', '/link2']
    
    • 1
    • 2

    //li/a/@href表示匹配任意路径下的li元素的a标签子节点的href属性值。

    10.获取li节点下所有a节点的href属性:

    print('--result10----//li//a/@href-----')
    result10 = html.xpath('//li//a/@href')  # 获取li节点下所有a节点的href属性
    print(result10)
    
    • 1
    • 2
    • 3

    输出如下:

    --result10----//li//a/@href-----
    ['/link1', '/link2', '/link3', '/link4', '/link5', '/link1', '/link2']
    
    • 1
    • 2

    相比result9,本次结果匹配到了/link3。

    11.获取class属性值包含-0的li元素下的a标签的文本:

    print('--result11----//li[contains(@class,"-0")]/a/text()-----')
    result11 = html.xpath('//li[contains(@class,"-0")]/a/text()') # 获取class属性值包含-0的li元素下的a标签的文本
    print(result11)
    
    • 1
    • 2
    • 3

    输出如下:

    --result11----//li[contains(@class,"-0")]/a/text()-----
    ['01', '05', 'useless-01']
    
    • 1
    • 2

    contains(@class,“-0”)表示过滤条件为class属性包含-0。于此类似的还有starts-with,starts-with表示以什么开头。

    12.用多个属性获取:

    print('--result12----//li[contains(@class,"-0") and @id="id-0"]/a/text()-----')
    result12 = html.xpath('//li[contains(@class,"-0") and @id="id-0"]/a/text()')  # 多个属性用and运算符来连接
    print(result12)
    
    • 1
    • 2
    • 3

    输出如下:

    --result12----//li[contains(@class,"-0") and @id="id-0"]/a/text()-----
    ['01']
    
    • 1
    • 2

    contains(@class,“-0”) and @id="id-0"表示待匹配的元素需要具有满足以上两种条件。and 操作符也可以替换为or 操作符。由于同时包含两种属性条件的a标签只有一个,所以返回的文本只有01。

    13.按照顺序获取节点:

    print('--result13----//li[last()]/a/text()-----')
    result13 = html.xpath('//li[last()]/a/text()')  # 取最后一个li节点下a节点的文本
    print(result13)
    
    • 1
    • 2
    • 3

    输出如下:

    --result13----//li[last()]/a/text()-----
    ['05', 'useless-02']
    
    • 1
    • 2

    返回结果表示,通过last()返回了两个li列表中的最后一个节点。

    14.通过ancestor轴获取所有的祖先节点:

    print('--result14----//li[1]/ancestor::*-----')
    result14 = html.xpath('//li[1]/ancestor::*') # ancestor轴可以获取所有的祖先节点
    print(result14)
    
    • 1
    • 2
    • 3

    输出如下:

    --result14----//li[1]/ancestor::*-----
    [<Element html at 0x182e0de9c80>, <Element body at 0x182e116abc0>, <Element div at 0x182e1169e80>, <Element ul at 0x182e1169f80>, <Element div at 0x182e1169e00>, <Element ul at 0x182e1169fc0>]
    
    • 1
    • 2

    //li[1]表示获取任意路径的li中的第一个元素,/ancestor::*表示获取当前节点的任意祖先节点。

    15.通过ancestor轴获取祖先div节点:

    print('--result15----//li[1]/ancestor::div-----')
    # 只获取div这个祖先节点
    result15 = html.xpath('//li[1]/ancestor::div')
    print(result15)
    for result15_1 in result15:
        print(result15_1.xpath('.//li[contains(@class,"-0")]/a/text()'))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出如下:

    --result15----//li[1]/ancestor::div-----
    [<Element div at 0x182e1169e80>, <Element div at 0x182e1169e00>]
    ['01', '05']
    ['useless-01']
    
    • 1
    • 2
    • 3
    • 4

    result15的返回结果为div节点,然后对result15进行遍历,在遍历中,通过xpath路径进一步获取a标签的文本。这里需要注意的是循环内的xpath路径以“.”开头,表示相对于当前div元素下,第一次输出为[‘01’, ‘05’],第二次输出为[‘useless-01’]。如果循环内的xpath路径去掉“.”,则循环内的两次输出是一致,应该都为[‘01’, ‘05’, ‘useless-01’]。

    16.获取所有属性值:

    print('--result16----//li[1]/attribute::*-----')
    result16 = html.xpath('.//li[1]/attribute::*')
    print(result16)
    
    • 1
    • 2
    • 3

    输出如下:

    --result16----//li[1]/attribute::*-----
    ['cla-0', 'id-0', 'cla-0']
    
    • 1
    • 2

    输出结果为所有li中的第1个节点的属性值。

    17.获取所有子孙节点a标签:

    print('--result17----//div/descendant::a-----')
    result17 = html.xpath('//div[@class="useless"]/descendant::a')
    print(result17)
    
    • 1
    • 2
    • 3

    输出如下:

    --result17----//div/descendant::a-----
    [<Element a at 0x1f34cf2a540>, <Element a at 0x1f34cf2a5c0>]
    
    • 1
    • 2

    descendant表示匹配子孙节点。
    以上就是lxml的基本操作,更多操作可以自行组合或参考官网,需要说明的是,在浏览器端通过开发者工具–查看器–选择元素–右键复制–选择XPath路径,可以获取选择元素的XPath路径,通过这种方法可以加快XPath路径构建。
    另外需要注意的是,xpath()函数的返回值为列表,可以通过先抓取外层的数据,然后通过遍历或是索引的方式获取节点数据,然后通过相对路径的方式进一步读取内层元素节点。案例如下:

    18.先获取外层元素,再通过相对路径的方式获取内部元素:
    
    • 1
    print('--result18----//li[1]/ancestor::div-----')
    result18 = html.xpath('//li[1]/ancestor::div')
    print(result18)
    print(result18[0].xpath('./ul/li/a/text()'))
    
    • 1
    • 2
    • 3
    • 4

    在上面代码中 ,result18[0]表示获取列表中的第一个Element 类型元素,然后对Element 类型元素进行xpath操作。./ul/li/a/text()中的“.”表示相对当前节点。
    输出为:

    --result18----//li[1]/ancestor::div-----
    [<Element div at 0x28e3b83a000>, <Element div at 0x28e3b83a0c0>]
    ['01', '02', '04', '05']
    
    • 1
    • 2
    • 3

    2.3 urllib整合lxml

    urllib获取百度数据

    from urllib import parse
    from urllib import request
    url='http://www.baidu.com/s?'
    dict_data={'wd':'百度翻译'}
    #unlencode() 将字典{k 1:v 1,k2:v2}转化为k1=v1&k2=v2
    url_data=parse.urlencode(dict_data)
    #urldata:wd=%E7%99%BE%E5%BA%A6%E7%BF%BB%E8%AF%91
    print(url_data)#读取URL响应结果
    response_data=request.urlopen((url+url_data))#用utf 8对响应结果编码
    data=response_data.read().decode('utf-8')
    print(data)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    输出为:

    wd=%E7%99%BE%E5%BA%A6%E7%BF%BB%E8%AF%91
    
    
    
        
        百度安全验证
        
        
        
        
        
        
        
        
        
        
    
    
        
    网络不给力,请稍后重试

    问题反馈

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    基于lxml进行解析百度数据

    from lxml import etree
    # 定义一个不规则的html文本
    
    html = etree.HTML(data)  # etree把不规则文本进行修正
    res = html.xpath("//body[1]//div[@class='timeout-title']/text()")
    print(res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出为:

    [‘网络不给力,请稍后重试’]

  • 相关阅读:
    SpringBoot-28-springSecurity注销和权限控制
    Java面试笔试题大汇总(最全+详细答案)
    离散小波变换(概念与应用)
    上市公司女性高管指标数据1999-2020年(含stata代码)
    flask捕获@app.errorhandler/@app.after_request全局异常总结
    jvm 三 之堆与栈
    ODBC访问达梦数据库Ubuntu18.04 x86-x64(亲测有效)
    OMC IT监控运维管理平台建设方案
    第11章 AOF持久化
    iOS查看汇编代码
  • 原文地址:https://blog.csdn.net/m0_38139250/article/details/133275065