Python 分布式爬虫框架 Scrapy 4-4 xpath的使用方法以及字段提取

一、xpath简介

  1. xpath使用路径表达式在xml和html中进行导航
  2. xpath包含标准函数库
  3. xpath是一个w3c的标准(不管什么库,在支持xpath方式的时候,语法是一致的)

二、xpath术语

  1. 父节点
  2. 子节点
  3. 同胞节点
  4. 先辈节点
  5. 后代节点

 三、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,看看和自己写的有没有区别,发现最前方少了一个“/”,即应该为:

扫描二维码关注公众号,回复: 8765958 查看本文章
/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()函数,免去了异常处理的编写,并可以传入一个默认值。

发布了101 篇原创文章 · 获赞 26 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/liujh_990807/article/details/100029784