一、xpath简介
- xpath使用路径表达式在xml和html中进行导航
- xpath包含标准函数库
- xpath是一个w3c的标准(不管什么库,在支持xpath方式的时候,语法是一致的)
二、xpath术语
- 父节点
- 子节点
- 同胞节点
- 先辈节点
- 后代节点
三、xpath语法
(html中的根元素一般是html,xml是可以自定义根元素的)
四、提取单个页面的元素
随意点进某个新闻详情页,修改start_urls中的元素为所点击的新闻详情页的url,例如:
start_urls = ['https://news.cnblogs.com/n/630037/']
之前说过了response会传入到parse函数中。实际上,这个response本身是有xpath方法的,会返回一个SelectorList。
先提取这个页面的标题内容。通过F12审查元素,先自己尝试着写出标题的xpath:
html/body/div[2]/div[2]/div[2]/div[1]/a
然后,通过在F12中所展示的代码中,点击右键,复制XPath,看看和自己写的有没有区别,发现最前方少了一个“/”,即应该为:
/html/body/div[2]/div[2]/div[2]/div[1]/a
编辑parse函数:
def parse(self, response):
xpath_str = '/html/body/div[2]/div[2]/div[2]/div[1]/a'
re_selector = response.xpath(xpath_str)
pass
在pass处打断点,并进行调试。
发现re_selector的类型为SelectorList,为什么不直接返回一个List呢?这是Scrapy为了让我们能够进行进一步的嵌套的selector。
实际上,在有js动态生成html的页面,通过上面的方法,我们极有可能得到一个空的SelectorList。我们再用Chrome浏览器(360浏览器)进行xpath的直接获取,发现它返给我们答案是:
//*[@id="news_title"]/a
html中,任何一个元素的id必须是全局唯一的,所以id就可以标明是某一个元素了。(可以通过在开发者界面中Ctrl+F使用搜索,测试某元素是否是全局唯一)
编辑parse为:
def parse(self, response):
# xpath_str = '/html/body/div[2]/div[2]/div[2]/div[1]/a'
xpath_str = '//*[@id="news_title"]/a'
re_selector = response.xpath(xpath_str)
pass
发现也得到了答案:
我们关心的是a标签中的标题内容,只需要在xpath_str的后面加上:
/text()
即:
xpath_str = '//*[@id="news_title"]/a/text()'
调试,这样就得到了标签之外的字符串内容,获得了标题:
为什么我们自己分析出的的Firefox直接给我们的xpath有时不能正常获取到内容呢?
是因为,我们通过F12观察到的源码,是整个页面运行完,包括html和js,才得到的,它有时和我们Ctrl+U看到的源码是不一样的。比如说写了一段js,在html中插入了一个div,则F12看到的是运行完js之后的,而Ctrl+U看到的是运行之前的。
而我们通过scrapy获取到的源码,实际上是Ctrl+U看到的源码。
而Ctrl+U提供的源码对于观察路径不太友好,可以复制到编辑器中进行分析。
同时,我们也要知道,xpath不一定是要写出完整的路径,只要符合语法,我们可以写出很简洁的xpath。
在提取页面的其他内容之前,我们要解决一个效率上的问题。我们可以发现,在启动scrapy的时候,它的效率实际上是比较慢的,因为每一次启动实际上都是去请求了一边url的。
为了解决这个问题,scrapy提供了一个shell模式,我们可以在shell模式下进行调试。
命令行输入:
scrapy shell https://news.cnblogs.com/n/630037/
我们获得了以下可以直接使用的object:
我们关心的是response。
我们在此处进行调试:
>>> # 标题
>>> title_xpath = '//*[@id="news_title"]/a/text()'
>>> title_selector = response.xpath(title_xpath)
>>> title_selector
[<Selector xpath='//*[@id="news_title"]/a/text()' data='VMware斥资近50亿美元收购两家软件公司'>]
>>> title_list = title_selector.extract()
>>> title_list
['VMware斥资近50亿美元收购两家软件公司']
>>> title = title_list[0]
>>> title
'VMware斥资近50亿美元收购两家软件公司'
需要注意的是,当我们对一个Selector对象使用了extract方法,返回的就是一个list类型变量了,list类型是不能继续进行xpath的。
其他内容的获取:
>>> # 发布日期
>>> create_date_xpath = '//*[@id="news_info"]/span[2]/text()'
>>> create_date_selector = response.xpath(create_date_xpath)
>>> create_date_selector
[<Selector xpath='//*[@id="news_info"]/span[2]/text()' data='发布于 2019-08-23 07:54'>]
>>> create_date_list = create_date_selector.extract()
>>> create_date_list
['发布于 2019-08-23 07:54']
>>> create_date_before_re = create_date_list[0]
>>> create_date_before_re
'发布于 2019-08-23 07:54'
>>> import re
>>> create_date_list = re.findall('\d{4}-\d{2}-\d{2}', create_date_before_re)
>>> create_date_list
['2019-08-23']
>>> create_date = create_date_list[0]
>>> create_date
'2019-08-23'
有两个字符串处理函数对于去除无用字符非常有用:strip()和replace(),你可以自行学习使用。
此外,如果一个标签的class属性有多个值,直接写“='某一个类名称'”是不能获取到内容的,即使所等于的类是全局唯一的。例如:
response.xpath("//span[@class='vote-post-up']")
我们需要使用xpath中的函数contains:
response.xpath("//span[contains(@class, 'vote-post-up')]")
这个页面,与数据相关的内容(收藏数、点赞数、评论数等)都是动态生成的,源码中没有,故我们不做提取,下一章将讲解如何让提取这些内容。但有两点是需要说明的:
一是例如收藏数、点赞数等整数类型值,最终应使用int函数将获取到的字符串转为整型再保存。
二是提供一种由"32 收藏"获取到"32"的正则策略:
import re
match_re = re.match(".*?(\d+).*", "32 收藏")
if match_re:
fav_nums = match_re.group(1)
正文内容的提取:
# 正文
>>> content_xpath = '//*[@id="news_body"]'
>>> content_selector = response.xpath(content_xpath)
>>> content_selector
[<Selector xpath='//*[@id="news_body"]' data='<div id="news_body">\n ...'>]
>>> content_list = content_selector.extract()
>>> content = content_list[0]
>>> content
'<div id="news_body">\n \n<p style="text-align: center;"><img src="//img2018.cnblogs.com/news/34358/201908/34358-20190823075419470-1064669537.jp
eg;%20charset=UTF-8" alt=""></p>\r\n<p>\u3000\u3000原标题:VMware Agrees to Buy Carbon Black, Pivotal for $4.8 Billion</p>\r\n<p>\u3000\u3000网易科技讯,8 月 2
3 日消息,据国外媒体报道,VMware 周四同意斥资近 50 亿美元收购两家软件公司,以便扩大其在开发工具和网络安全领域的影响力。</p>\r\n<p>\u3000\u3000VMware 表示,它将
以 27 亿美元收购销售云软件和服务的 Pivotal Software Inc.,并以 21 亿美元收购网络安全公司 Carbon Black Inc.。</p>\r\n<p>\u3000\u3000该公司称,在完成合并后,公司
将提供软件来构建、运行、管理、连接和保护云上或任何设备上的任何应用程序。收购这两家公司将加速 VMware 推进提供安全且多云的应用程序开发的计划。</p>\r\n<p>\u3000\u
3000VMware 首席执行官帕特·盖尔辛格(Pat Gelsinger)指出,这两笔收购“将显著增强我们驱动客户数字化转型的能力”。</p>\r\n<p>\u3000\u3000“这些收购解决了当今所有企
业的两个关键技术问题——构建现代化的企业级应用程序,以及保护企业的工作负载和客户。”他说道。</p>\r\n<p>\u3000\u3000VMware 表示,一旦 Carbon Black 的交易完成,
它将通过大数据、行为分析和人工智能提供“高度差异化的、具有固有安全性的云”。</p>\r\n<p>\u3000\u3000预计这两笔交易都将在 VMware 当前财年的下半年完成,该财年将于
明年 1 月 31 日结束。(乐邦)<!--EndFragment--></p>\r\n<!-- 作者 --><!-- 声明 --> </div>'
这里我们保存的是正文的源码,对正文的处理是一个比较复杂的课题。
此外我们要保存两个今后在进行搜索时要用到的内容,就是标签和新闻来源。
>>> # 标签
>>> tags_xpath = '//*[@id="news_more_info"]/div/a/text()'
>>> tags_selector = response.xpath(tags_xpath)
>>> tags_selector
[<Selector xpath='//*[@id="news_more_info"]/div/a/text()' data='VMware'>]
>>> tags_list = tags_selector.extract()
>>> tags_list
['VMware']
>>> tags = ','.join(tags_list)
>>> tags
'VMware'
>>> # 来源
>>> source_xpath = '//*[@id="link_source2"]/text()'
>>> source_selector = response.xpath(source_xpath)
>>> source_selector
[<Selector xpath='//*[@id="link_source2"]/text()' data='网易科技'>]
>>> source_list = source_selector.extract()
>>> source_list
['网易科技']
>>> source = source_list[0]
>>> source
'网易科技'
再补充一个,对于一个列表,去除其内部以“评论”结尾的元素:
tag_list = ['职场', ' 1 评论 ', 'fuck两点水']
tag_list = [element for element in tag_list if not element.strip().endwith('评论')]
最终,我们的parse函数:
def parse(self, response):
# 标题
title_xpath = '//*[@id="news_title"]/a/text()'
title_selector = response.xpath(title_xpath)
title_list = title_selector.extract()
title = title_list[0]
# 发布日期
create_date_xpath = '//*[@id="news_info"]/span[2]/text()'
create_date_selector = response.xpath(create_date_xpath)
create_date_list = create_date_selector.extract()
create_date_before_re = create_date_list[0]
import re
create_date_list = re.findall('\d{4}-\d{2}-\d{2}', create_date_before_re)
create_date = create_date_list[0]
# 正文
content_xpath = '//*[@id="news_body"]'
content_selector = response.xpath(content_xpath)
content_list = content_selector.extract()
content = content_list[0]
# 标签
tags_xpath = '//*[@id="news_more_info"]/div/a/text()'
tags_selector = response.xpath(tags_xpath)
tags_list = tags_selector.extract()
tags = ','.join(tags_list)
# 来源
source_xpath = '//*[@id="link_source2"]/text()'
source_selector = response.xpath(source_xpath)
source_list = source_selector.extract()
source = source_list[0]
pass
或者简洁一点:
def parse(self, response):
"""
1. 获取文章列表页中的文章url并交给scrapy下载后并进行解析
2. 获取下一页的url并交给scrapy进行下载, 下载完成后交给parse
"""
# 获取列表页所有文章的url
# 标题
title = response.xpath('//*[@id="news_title"]/a/text()').extract_first("Untitled")
# 发布日期
create_date = re.findall('\d{4}-\d{2}-\d{2}', response.xpath('//*[@id="news_info"]/span[2]/text()').extract_first("0000-00-00"))[0]
# 正文
content = response.xpath('//*[@id="news_body"]').extract_first("No content")
# 标签
tags = ','.join(response.xpath('//*[@id="news_more_info"]/div/a/text()').extract())
# 来源
source = response.xpath('//*[@id="link_source2"]/text()').extract_first('Unknown')
pass
对于list按索引值提取某元素,需要异常处理。我们使用extract_first()函数,免去了异常处理的编写,并可以传入一个默认值。