用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)
原文地址:https://www.cnblogs.com/HuZihu/p/10219912.html