在《爬虫基础以及一个简单的实例》一文中,我们使用了正则表达式来解析爬取的网页。但是正则表达式有些繁琐,使用起来不是那么方便。这次我们试一下用Xpath选择器来解析网页。
首先,什么是XPath?XPath即XML路径语言(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)