热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

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

在《爬虫基础以及一个简单的实例》一文中,我们使用了正则表达式来解析爬取的网页。但是正则表达式有些繁琐,使用起来不是那么方便。这次我们试一下用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里查看网页源代码。下面截取一部分:

2018-12-30已更新

榜单规则:将猫眼电影库中的经典影片,按照评分和评分人数从高到低综合排序取前100名,每天上午10点更新。相关数据来源于“猫眼电影库”。

1 霸王别姬

霸王别姬

主演:张国荣,张丰毅,巩俐

上映时间:1993-01-01

9.5

 

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

 
1

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

 

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

 霸王别姬

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

 

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

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

 

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

上映时间:1993-01-01

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

 

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

9.5

相应的路径可以写为://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://img8.php1.cn/3cdc5/12fff/2be/6e99e781472a90a3.jpeg@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://img8.php1.cn/3cdc5/12fff/2be/6e99e781472a90a3.jpeg@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)

 


推荐阅读
author-avatar
Binggo89
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有