• 用Xpath选择器解析网页(lxml)


    《爬虫基础以及一个简单的实例》一文中,我们使用了正则表达式来解析爬取的网页。但是正则表达式有些繁琐,使用起来不是那么方便。这次我们试一下用Xpath选择器来解析网页。

    首先,什么是XPath?XPathXML路径语言(XML Path Language),用于在XML文档中查找信息(在XML文档中对元素和属性进行遍历),也适用于HTML文档。

    那么,怎样来选择我们想要的内容呢?常用的规则如下:(以下摘自:https://cuiqingcai.com/2621.html

    选取节点:使用路径表达式

    表达式描述
    nodename 选取此节点的所有子节点。
    / 从根节点选取。
    // 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
    . 选取当前节点。
    .. 选取当前节点的父节点。
    @ 选取属性。

    查找某个特定的节点或者包含某个指定的值的节点:使用谓语(注:谓语被嵌在方括号中)

    路径表达式结果
    /bookstore/book[1] 选取属于 bookstore 子元素的第一个 book 元素。
    /bookstore/book[last()] 选取属于 bookstore 子元素的最后一个 book 元素。
    /bookstore/book[last()-1] 选取属于 bookstore 子元素的倒数第二个 book 元素。
    /bookstore/book[position()<3] 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。
    //title[@lang] 选取所有拥有名为 lang 的属性的 title 元素。
    //title[@lang=’eng’] 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。
    /bookstore/book[price>35.00] 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。
    /bookstore/book[price>35.00]/title 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。

    选取未知节点:使用通配符

    通配符描述
    * 匹配任何元素节点。
    @* 匹配任何属性节点。
    node() 匹配任何类型的节点。

    Xpath运算符

    运算符描述实例返回值
    | 计算两个节点集 //book | //cd 返回所有拥有 book 和 cd 元素的节点集
    + 加法 6 + 4 10
    减法 6 – 4 2
    * 乘法 6 * 4 24
    div 除法 8 div 4 2
    = 等于 price=9.80 如果 price 是 9.80,则返回 true。如果 price 是 9.90,则返回 false。
    != 不等于 price!=9.80 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。
    < 小于 price<9.80 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。
    <= 小于或等于 price<=9.80 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。
    > 大于 price>9.80 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。
    >= 大于或等于 price>=9.80 如果 price 是 9.90,则返回 true。如果 price 是 9.70,则返回 false。
    or price=9.80 or price=9.70 如果 price 是 9.80,则返回 true。如果 price 是 9.50,则返回 false。
    and price>9.00 and price<9.90 如果 price 是 9.80,则返回 true。如果 price 是 8.50,则返回 false。
    mod 计算除法的余数 5 mod 2 1

    节点之间的关系:这部分比较简单,稍微看一下https://cuiqingcai.com/2621.html上的例子就明白了。

    1, 父(Parent)

    2. 子(Children) 

    3. 同胞(Sibling)

    4. 先辈(Ancestor) --- 包括父和父的父

    5. 后代(Descendant) --- 包括子和子的子


    一些路径表达式的例子:(摘自:https://www.jianshu.com/p/89c10770d72c

    使用绝对路径:/html/body/div/form/input

    绝对路径是从网页起始标签开始一直到要定位的元素的路径,如果要定位的元素在页面最下面,则这个Xpath路径会非常长。如果在要定位的元素与页面开始之间的元素有任何增减,元素定位就会失败。

    使用相对路径://input

    相对路径一般只包含与被定位元素关系最近的几层元素,相对路径写的好的话,页面变动影响最小,而且定位准确。

    使用索引定位元素,索引的初始值为1://input[2]

    如果一个页面中有多个相似的元素,或是一个层下面有多个同样的元素的时候,需要用索引的方法来定位,否则无法区分。

    结合属性值来定位元素://input[@id='username']

    属性定位也是比较常用的方法,如果元素中没有常见的id,name,class等直接有方法可调用的属性,也可以查找元素中是否有其他能唯一标识元素的属性,如果有,就可以用此方法定位。

    使用多个属性定位元素://input[@id='username' and @name='userID']

    多个属性联合定位,更能准确定位到元素。(注意:匹配多个属性:用and连接;  匹配属性的多个值:contains(..., ...)

    使用属性名来定位元素://input[@button]

    此方法可以区分同一种标签,含有不同属性名的元素。定位相对简单一些儿,但也同样存在着无法区分同种标签含有同种属性名的多个元素,这个时候要配合索引定位才行。

    使用部分属性值匹配元素,用starts-with(),ends-with(),contains()://input[stars-with(@id,'user')]; //input[ends-with(@id,'name')]; //input[contains(@id,"ernam")]

    此方法更加灵活,可以定位属性值不太规律,或是部分变动,中间有空格的情况。

    使用任意属性值匹配元素://input[@*='username']

    此方法相当于模糊查询,只要欲定位的标签,如input中任何属性值等于‘username’,就能匹配成功。缺点是可能会匹配含有这个属性值的其他元素,所以我们在定位的时候要查看一下这个元素值在页面中是否唯一。

    使用文本匹配元素://input[contains(text(),'text')]

    (注:获取元素的内容用text())

    总结:用Xpath定位时,先看这个元素是否有明显的,唯一的属性值。如果有,我们就用相对路径加属性值定位,这是最简单准确的定位方法。如果要定位的元素不符合这个特征,例如:元素属性是动态的,无法区分这个元素,属性值中间有空格,等等。那么应该从此元素的上一层开始查找。当遇到了一个符合条件的元素时,对其写Xpath。然后从这个元素开始,一级级往下写,直到要定位的元素为止。


    在python中使用Xpath选择器,我们需要安装lxml库。下面是经常用到的一些语法:

    导入lxml的etree库: from lxml import etree

    读取需要进行解析的网页

    1. 从字符串读取:html=etree.HTML(text)

    2. 从文件读取:html=etree.parse(file_path)

    输出修正后的html:result=etree.tostring(html)

    选取所需的节点:result=html.xpath(...)


    了解了以上的知识后,我们就可以开始进行实际操练了。还是用之前的那个例子,实例网址:https://maoyan.com/board/4

    实例目标:用requests库爬取猫眼电影网上top100的电影(排名,图片,电影名称,上映时间,评分),用Xpath进行解析,然后把数据保存到MongoDB。

    首先,导入requests库,lxml的etree库和pymongo库:

    from lxml import etree
    import requests
    import pymongo

    爬取单个网页还是用原来的代码:

    def get_one_page(url):
        try:
            headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) 
                     AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'}
            response=requests.get(url, headers=headers)
            if response.status_code==200:
                return response.text
            return None
        except requests.RequestException:
            print("Fail")

    接下来用浏览器打开网页,然后在浏览器里面选择开发者工具,在Network里查看网页源代码。下面截取一部分:

    <div class="content">
        <div class="wrapper">
            <div class="main">
                <p class="update-time">2018-12-30<span class="has-fresh-text">已更新</span></p>
                <p class="board-content">榜单规则:将猫眼电影库中的经典影片,按照评分和评分人数从高到低综合排序取前100名,每天上午10点更新。相关数据来源于“猫眼电影库”。</p>
                <dl class="board-wrapper">
                    <dd>
                            <i class="board-index board-index-1">1</i>
        <a href="/films/1203" title="霸王别姬" class="image-link" data-act="boarditem-click" data-val="{movieId:1203}">
          <img src="//ms0.meituan.net/mywww/image/loading_2.e3d934bf.png" alt="" class="poster-default" />
          <img data-src="https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c" alt="霸王别姬" class="board-img" />
        </a>
        <div class="board-item-main">
          <div class="board-item-content">
                  <div class="movie-item-info">
            <p class="name"><a href="/films/1203" title="霸王别姬" data-act="boarditem-click" data-val="{movieId:1203}">霸王别姬</a></p>
            <p class="star">
                    主演:张国荣,张丰毅,巩俐
            </p>
    <p class="releasetime">上映时间:1993-01-01</p>    </div>
        <div class="movie-item-number score-num">
    <p class="score"><i class="integer">9.</i><i class="fraction">5</i></p>   

    可以看到,电影的排名在一个dd节点下面,紧接着还有一个i节点,我们需要以"board-index"开头的class属性的文本:

     <dd>
                            <i class="board-index board-index-1">1</i>

    因此,相应的路径可以写为://dd/i[starts-with(@class,'board-index')]/text()

    接下来,我们发现图片在一个a节点下面,但是有两张图片。经过检查,第二个img节点下的data-src属性是图片的链接:

     <img data-src="https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c" alt="霸王别姬" class="board-img" />

    因此,相应的路径可以写为://a/img[2]/@data-src

    再接下来,电影的名称,在一个p节点下面,class为"name",下面还有一个a节点:

    <p class="name"><a href="/films/1203" title="霸王别姬" data-act="boarditem-click" data-val="{movieId:1203}">霸王别姬</a></p>

    相应的路径可以写为://p[@class='name']/a/@title

    上映时间,在一个p节点下面,class为"releasetime":

    <p class="releasetime">上映时间:1993-01-01</p>

    相应的路径可以写为://p[@class='releasetime']/text()

    评分,在一个p节点下面,class为"score",下面还有一个i节点:

    <p class="score"><i class="integer">9.</i><i class="fraction">5</i></p>

    相应的路径可以写为://p[@class='score']/i/text()

    完整的路径如下(用|连接):

    //dd/i[starts-with(@class,'board-index')]/text()|//a/img[2]/@data-src|//p[@class='name']/a/@title|//p[@class='releasetime']/text()|//p[@class='score

    下面,我们再定义一个解析网页的方法:

    def parse_one_page(html):
        result=html.xpath("//dd/i[starts-with(@class,'board-index')]/text()|//a/img[2]/@data-src|//p[@class='name']/a/@title|//p[@class='releasetime']/text()|//p[@class='score']/i/text()")
        return result

    输出的匹配结果如下:

    ['1', 'https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', '霸王别姬', '上映时间:1993-01-01', '9.', '5', '2', 'https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@160w_220h_1e_1c', '肖申克的救赎', '上映时间:1994-09-10(加拿大)', '9.', '5', '3', 'https://p0.meituan.net/movie/289f98ceaa8a0ae737d3dc01cd05ab052213631.jpg@160w_220h_1e_1c', '罗马假日', '上映时间:1953-09-02(美国)', '9.', '1', '4', 'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@160w_220h_1e_1c', '这个杀手不太冷', '上映时间:1994-09-14(法国)', '9.', '5', '5', 'https://p1.meituan.net/movie/b607fba7513e7f15eab170aac1e1400d878112.jpg@160w_220h_1e_1c', '泰坦尼克号', '上映时间:1998-04-03', '9.', '5', '6', 'https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@160w_220h_1e_1c', '唐伯虎点秋香', '上映时间:1993-07-01(中国香港)', '9.', '1', '7', 'https://p0.meituan.net/movie/46c29a8b8d8424bdda7715e6fd779c66235684.jpg@160w_220h_1e_1c', '魂断蓝桥', '上映时间:1940-05-17(美国)', '9.', '2', '8', 'https://p0.meituan.net/movie/223c3e186db3ab4ea3bb14508c709400427933.jpg@160w_220h_1e_1c', '乱世佳人', '上映时间:1939-12-15(美国)', '9.', '1', '9', 'https://p1.meituan.net/movie/ba1ed511668402605ed369350ab779d6319397.jpg@160w_220h_1e_1c', '天空之城', '上映时间:1992', '9.', '1', '10', 'https://p0.meituan.net/movie/b0d986a8bf89278afbb19f6abaef70f31206570.jpg@160w_220h_1e_1c', '辛德勒的名单', '上映时间:1993-12-15(美国)', '9.', '2']

    可以看出,上述的格式还是有些杂乱,让我们修改一下解析网页的方法,使其变为整齐的结构化数据:

    def parse_one_page(html):
        result=html.xpath("//dd/i[starts-with(@class,'board-index')]/text()|//a/img[2]/@data-src|//p[@class='name']/a/@title|//p[@class='releasetime']/text()|//p[@class='score']/i/text()")  
        for i in range(0,55,6):
            yield {"index": result[i], "movie_name": result[i+2],
                    "pic": result[i+1], "release": result[i+3],
                    "score": result[i+4]+result[i+5]}

    现在匹配结果变成了字典格式:

    {'index': '1', 'movie_name': '霸王别姬', 'pic': 'https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', 'release': '上映时间:1993-01-01', 'score': '9.5'}
    {'index': '2', 'movie_name': '肖申克的救赎', 'pic': 'https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@160w_220h_1e_1c', 'release': '上映时间:1994-09-10(加拿大)', 'score': '9.5'}
    {'index': '3', 'movie_name': '罗马假日', 'pic': 'https://p0.meituan.net/movie/289f98ceaa8a0ae737d3dc01cd05ab052213631.jpg@160w_220h_1e_1c', 'release': '上映时间:1953-09-02(美国)', 'score': '9.1'}
    {'index': '4', 'movie_name': '这个杀手不太冷', 'pic': 'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@160w_220h_1e_1c', 'release': '上映时间:1994-09-14(法国)', 'score': '9.5'}
    {'index': '5', 'movie_name': '泰坦尼克号', 'pic': 'https://p1.meituan.net/movie/b607fba7513e7f15eab170aac1e1400d878112.jpg@160w_220h_1e_1c', 'release': '上映时间:1998-04-03', 'score': '9.5'}
    {'index': '6', 'movie_name': '唐伯虎点秋香', 'pic': 'https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@160w_220h_1e_1c', 'release': '上映时间:1993-07-01(中国香港)', 'score': '9.1'}
    {'index': '7', 'movie_name': '魂断蓝桥', 'pic': 'https://p0.meituan.net/movie/46c29a8b8d8424bdda7715e6fd779c66235684.jpg@160w_220h_1e_1c', 'release': '上映时间:1940-05-17(美国)', 'score': '9.2'}
    {'index': '8', 'movie_name': '乱世佳人', 'pic': 'https://p0.meituan.net/movie/223c3e186db3ab4ea3bb14508c709400427933.jpg@160w_220h_1e_1c', 'release': '上映时间:1939-12-15(美国)', 'score': '9.1'}
    {'index': '9', 'movie_name': '天空之城', 'pic': 'https://p1.meituan.net/movie/ba1ed511668402605ed369350ab779d6319397.jpg@160w_220h_1e_1c', 'release': '上映时间:1992', 'score': '9.1'}
    {'index': '10', 'movie_name': '辛德勒的名单', 'pic': 'https://p0.meituan.net/movie/b0d986a8bf89278afbb19f6abaef70f31206570.jpg@160w_220h_1e_1c', 'release': '上映时间:1993-12-15(美国)', 'score': '9.2'}

    接下来将结果保存到MongoDB,先写一个保存到mongo数据库的方法:

    def write_to_mongo(result):
        query=result
        collection.update_one(query,{'$set':result},upsert=True)

    注:为了避免保存重复的数据,这里把upsert改为True。

    其他步骤还和以前一样,完整代码如下:

    from lxml import etree
    import requests
    import pymongo
    import time
    
    def get_one_page(url):
        try:
            headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) 
                     AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'}
            response=requests.get(url, headers=headers)
            if response.status_code==200:
                return response.text
            return None
        except requests.RequestException:
            print("Fail")
    
    def parse_one_page(html):
        result=html.xpath("//dd/i[starts-with(@class,'board-index')]/text()|//a/img[2]/@data-src|//p[@class='name']/a/@title|//p[@class='releasetime']/text()|//p[@class='score']/i/text()")  
        for i in range(0,55,6):
            yield {"index": result[i], "movie_name": result[i+2],
                    "pic": result[i+1], "release": result[i+3],
                    "score": result[i+4]+result[i+5]}
    
    def write_to_mongo(result):
        query=result
        collection.update_one(query,{'$set':result},upsert=True)
    
    def main(offset):
        url="https://maoyan.com/board/4?offset={}".format(offset)
        html=get_one_page(url)
        html=etree.HTML(html)
        result=parse_one_page(html)
        for i in result:
            write_to_mongo(i)
            
    if __name__=='__main__':
        client=pymongo.MongoClient(host='localhost',port=27017)
        db=client['test']
        collection=db['top100_movies']
        for i in range(10):
            main(offset=i*10)
            time.sleep(1)
  • 相关阅读:
    tiny4412 硬件解码
    orb slam2 双目摄像头
    hi3516a arm-hisiv300-linux-gcc jrtplib交叉编译
    第12章_异常
    第10章_内部类:
    IO流深入总结
    实现对存放了Map集合的ArrayList的排序(按照map中某个字段比较)
    UML各图用处

    File类:
  • 原文地址:https://www.cnblogs.com/HuZihu/p/10219912.html
Copyright © 2020-2023  润新知