Python 爬虫必备杀器,xpath 解析 HTML

最近工作上写了个爬虫,要爬取国家标准网上的一些信息,这自然离不了 Python,而在解析 HTML 方面,xpath 则可当仁不让的成为兵器谱第一。

你可能之前听说或用过其它的解析方式,像 Beautiful Soup,用的人好像也不少,但 xpath 与之相比,语法更简单,解析速度更快,就像正则表达式一样,刚上手要学习一番,然而用久了,那些规则自然而然的就记住了,熟练之后也很难忘记。

安装 lxml

xpath 只是解析规则,其背后是要有相应的库来实现功能的,就像正则表达式只是规则,而 Python 内置的 re 库,则是提供了解析功能。在 Python 中,lxml 就是 xpath 解析的实现库。

安装 lxml 非常简单,pip install lxml 就搞定了。

下面我们来看一下,在我这次真实的项目中,该如何发挥出它的威力。

加载 HTML 内容,应该用 etree.parse()、etree.fromstring() 还是 etree.HTML() ?

首先,把 lxml 库导进来:from lxml import etree

HTML 内容的加载,是通过 etree 的方法载入的,具体有 3 个方法:parse()、fromstring() 和 HTML()。

  • parse() 是从文件加载。
  • fromstring() 是从字符串加载。
  • HTML() 也是从字符串加载,但是以 HTML 兼容的方式加载进来的。

那我们应该选哪个方法呢?别犹豫,选 etree.HTML(),即使你的 HTML 内容来自文件。这是为何?

首先要说的一点是,HTML 也是 XML 的一种,而 XML 的标准规定,其必须拥有一个根标签,否则,这段 XML 就是非法的。而我们加载进来的 HTML 内容,可能本身就不是完整的,只是个片段,且没有根标签;或是加载进来的 HTML 从头到脚看起来都是完整的,但是中间的节点,有的缺少结束标签,这些情况,其实都是非法的 XML。那么,在用 parse() 或 formstring() 加载这种缺胳膊少腿的 HTML 的时候,就会报错;而用 etree.HTML() 则不会。

这是因为 etree.HTML() 加载方式,有很好的 HTML 兼容性,它会补全缺胳膊少腿的 HTML,把它变成一个完整的、合法的 HTML。

下面是一个从文件加载 HTML 的例子:

from lxml import etreewith open('test.html', 'r') as f:html = etree.HTML(f.read())print(html, type(html))

打印出来的结果是:<Element html at 0x7f7efa762040> <class 'lxml.etree._Element'>,加载进来的 HTML 字符串,已经变成了 Element 对象。

后面我们通过 xpath 找 HTML 节点,全都是在这个 Element 对象上操作的。

找到你需要的 HTML 节点

下面是我想要找的 HTML 节点

在这个 table 表格中,第一个 tbody 是表头,第二个 tbody 是表内容,我们要如何定位到第二个 tbody ?

我们通常是调用上面获得的 Element 对象的 xpath() 方法,通过传入的 xpath 路径查找的。而路径有两种写法:一种是 / 开头,从 html 根标签,沿着子节点一个个找下来;另一种是 // 开头,即不论我们要找的节点在什么位置,找到就算,这种方式是最常用的。

比如,我们现在要找的 tbody 节点,它在 table 节点下,我们就可以这样写:html.xpath('//table/tbody')。这里的 html 是上面获得的 Element 对象,然后去找 HTML 内容中的、不管在任何位置的所有的 table,找到后再继续找它们下面的直接子节点 tbody,于是就匹配出来了。

可是这里有 2 个 tbody,我需要的是第二个,我们可以在 [] 中写条件表达式:html.xpath('//table/tbody[2]'),注意这里的序号是从 1 开始的。

强大的属性选择器

你可能有个疑问,如果 HTML 内容中不只有一个 table 表格,那我们通过 html.xpath('//table/tbody[2]') 岂不是找到了 2 个 table 里的第二个 tbody,而我需要的只是其中之一。没错,是存在这样一个问题。此时,我们就可以用属性选择器,来更精确的定位元素。

观察一下上面的 HTML 结构,table 表格的最外层有一个 div,它还有个 class 属性:table-responsive,假设这个 div 的 class 属性是整个 HTML 里独一无二的,那么我们就可以很放心的去查找 div.table-responsive 下的 table,进而精确定位我们想要的元素。

那么,要怎样写 class = "table-responsive" 这个条件呢?看看上面写条件表达式的 [],那里面除了可以写数字来指定位置以外,也可以写其它各式各样的条件,比如:

html.xpath('//div[@class="table-responsive"]/table/tbody[2]'),这里我们就把 class = "table-responsive" 这个条件写进去了,从而定位到想要的元素。注意,在 xpath 中,所有的 HTML 属性匹配都是以 @ 打头的,比如有这样一个 <a id="show_me" href="#">Click Me</a> 元素,我们想要通过 id 定位它,可以这样写://a[@id="show_me"],是不是很简单。

假设很遗憾,我们这里的 table-responsive 不是唯一的,可能还有其它地方的 div 的 class = "table-responsive",这该怎么办?没关系,我们可以找其它具有唯一 class 值的元素,比如:最外层 div 下的 table.result_list 这个元素,这个是唯一的。好了,下面开始写定位代码:html.xpath('//table[@class="result_list"]/tbody[2]'),但是运行后,发现找不到元素,这是为什么?

其实仔细观察一下就能发现,这个元素的 class 里不只有 result_list,它还包括其它一长串的内容:class = "table result_list table-striped table-hover",所以匹配失败了。那要如何指定 class 包含某个属性呢?其实可以在条件表达式中,用 contains() 函数,无需精确匹配,而是模糊匹配,只要包含指定的字符串就可以了。比如:html.xpath('//table[contains(@class, "result_list")]/tbody[2]') 这样就可以实现了。

需要提一点的是,xpath 定位到的元素,不管是不是全局唯一的,它的返回值都是一个列表,需要通过下标获取其中的元素。

相对定位

我最终的目标,是要遍历表格中所有的内容行,获取其中的标准号和标准名称,于是我初步完成了如下代码:

from lxml import etreewith open('test.html', 'r') as f:html = etree.HTML(f.read())rows = html.xpath('//table[contains(@class, "result_list")]/tbody[2]/tr')for row in rows:td_list = row.xpath('...')

现在我能够成功地定位到每一行,下面需要再基于每一行,找到我需要的列:

此时,我在 for 循环的内部,已经拿到了每一行 row,再通过 row.xpath('//td') 继续往下定位 td 就好了。

可是,当你运行这段代码的时候,你会发现不对劲,一行里面总共只有 8 个 td,为什么出来了 80 个【一行 8 个,总共 10 行】?这是把 HTML 中所有的 td 都找出来了吧,可是我明明是用上面获取的 row 对象来查找的呀,不是应该只基于当前行往下找吗?

这就牵扯到了 绝对定位相对定位

其实,我们上面讲到的 ///,都是绝对定位,也就是从 HTML 内容的根节点往下查找。一个 HTML 内容的根节点是什么呢,它是 html,再往下是 body,再再往下才是自定义的标签。所以,上面代码的执行结果是那种情况,也就不足为奇了,因为它不是在当前所在的 row 节点查找的,而是从根节点 /html/body/xxx/xxx/td 往下查找的呀。

所以,在这里不能用 绝对定位 了,要用 相对定位,那要如何用?很简单,用 ... 即可,这个我们可太熟悉了,. 就代表了当前节点 row,而 .. 则代表了当前节点的上一层父节点 tbody

好了,我们修正上面的代码:

from lxml import etreewith open('test.html', 'r') as f:html = etree.HTML(f.read())rows = html.xpath('//table[contains(@class, "result_list")]/tbody[2]/tr')for row in rows:td_list = row.xpath('./td')i = 1for td in td_list:if i == 2:passelif i == 4:passi += 1

这样就可以正常地找到每一行里面的 8 个 td,然后再单独处理第 2 个和第 4 个单元格,获取其中的信息就好了。

通过已知节点获取属性和文本

到目前为止,我们能拿到第 2 个和第 4 个 td 节点了,只要再获取里面的 a 标签的属性和文本就可以了。

我们先获取 onclick 属性,通过 td.xpath('./a'),可以找到此 td 节点下面的 a 标签,然后调用 a 节点的 get() 方法,即可获得对应的属性值,代码如下:

a1 = td.xpath('./a')[0]
onclick = a1.get('onclick')

注意哦,xpath() 方法的返回值,始终是一个列表,所以我们用下标 [0] 先把它从列表中取出来,然后再获取其属性。 至于属性内的值,我实际想取的是里面的一串 ID 字符串,这个再用正则表达式取一下就可以了。

要获取节点内的文本,也很简单,获得到的节点有一个 text 属性,可以直接得到节点的文本内容:a1.text

好用的兄弟节点选择器

上面的代码逻辑有点挫,我们先是获取到一行里的所有 td,然后循环遍历它,在遍历的过程当中,只取其中的 2 个 td,着实有些浪费。假设一行里有 1000 个 td,那这里岂不是要循环 1000 次,就只为了取 2 个?

虽然从实际运行速度上来讲,影响微乎其微,但对于有代码洁癖和强迫症的人来说,是不可接受的,所以,我们要改造它。

重新观察一下 HTML 结构,我发现第 4 个单元格有个明显的特征,它的 class = "mytxt"

我们可以很容易地找到它:title_td = tr.xpath('./td[@class="mytxt"]')[0],然后再基于刚找到的 title_td,查找从它往上数第 2 个兄弟节点,这样就省略了一个循环,只要查找两次就完成了。

那么,怎么查找上面的兄弟节点呢?用 preceing-sibling,比如:title_td.xpath('./preceding-sibling::td[2]'),这就代表要查找 title_td 上面的、从它这里往上数、排在第 2 位的 td 节点。

除了 preceding-sibling 之外,还有 following-sibling,顾名思意,是往下查找兄弟节点。

以上我只介绍了这 2 个,其实还有很多类似的选择器,具体可以参考下面的速查手册。

最后,我改造的代码如下:

from lxml import etreewith open('test.html', 'r') as f:html = etree.HTML(f.read())rows = html.xpath('//table[contains(@class, "result_list")]/tbody[2]/tr')for row in rows:title_td = row.xpath('./td[@class="mytxt"]')[0]title_link = title_td.xpath('./a')[0]title_onclick = title_link.get('onclick')print(title_onclick, title_link.text)id_td = title_td.xpath('./preceding-sibling::td[2]')[0]id_link_text = id_td.xpath('./a/text()')[0]print(id_link_text)

速查手册

xpath 的规则并不复杂,常用的也就那些,用熟了自然就记住了。但像正则表达式一样,它还有许多不常用却很好用的特性,你还是需要偶尔查一下具体的作用和用法。

这里有一个非常好的速查手册,虽然里面的内容看起来不够丰富、很简单,但是可以一目了然,并且它用 css 的语法来作类比,就能够更好地理解每一个 xpath 规则的实际用途。

速查手册:https://devhints.io/xpath

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/843440.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

视频调色 LUT 教程 All In One

视频调色 LUT 教程 All In One Lookup tables (LUTs) 在图像处理中,查找表通常称为 LUT(或 3DLUT),并为一系列索引值中的每一个提供输出值。一种常见的 LUT,称为颜色图或调色板,用于确定特定图像将显示的颜色和强度值。在计算机断层扫描中,“窗口化”是指用于确定如何显…

探索中国风水学与AI人工智能的融合之旅

在古老的东方智慧中,风水学一直是中国传统文化的重要组成部分。它不仅是一种哲学思想,更是一种生活方式,指导人们如何与自然和谐共存,寻求生活的平衡与和谐。随着科技的发展,人工智能(AI)技术的兴起为风水学带来了新的解读和应用方式。本文将带您走进中国风水学与AI结合…

『玩转Streamlit』--可编辑表格

之前介绍过两个数据展示的组件,st.dataframe和st.table。 今天介绍的st.data_editor组件,除了展示数据的功能更加强大之外,还可以编辑数据。 1. 概要 st.data_editor组件在数据展示和编辑中都发挥着独特且重要的作用。 首先,在数据展示方面,它的优势在于:直观性:以表格形…

【验证码逆向专栏】某多多验证码逆向分析

声明 本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术…

使用静态html绘制流程图

方案一使用svg<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Flowchart Example</title&g…

Vulnhub-Earth靶机笔记

Earth 靶机笔记 概述 这是一台 Vulnhub 的靶机,主要是 Earth 靶机地址:https://vulnhub.com/entry/the-planets-earth,755/#download 一、nmap 扫描 1、端口扫描 -sT 以 TCP 全连接扫描,--min-rate 10000 以最低 10000 速率进行扫描,-p-进行全端口扫描,-o ports 结果输出到…

hhdb数据库介绍(10-2)

集群管理 计算节点集群 集群管理主要为用户提供对计算节点集群的部署、添加、启停监控、删除等管理操作。 集群管理记录 集群管理页面显示已部署或已添加的计算节点集群信息。可以通过左上角搜索框模糊搜索计算节点集群名称进行快速查找。同时也可以通过右侧展开展开/隐藏更多按…

如何查看CUDA版本

在安装pytorch或TensorFlow等包时,需要和cuda版本匹配,此时需要查看cuda版本: 在终端输入命令nvidia-sim

hhdb数据库介绍(10-17)

配置 服务器 服务器菜单可配置集群中所有服务器的SSH信息,方便管理平台对服务器进行各种状态监控。此外也支持添加集群外的服务器到管理平台中进行监控。 自动获取服务器IP 服务器页面会自动显示集群内所有的服务器IP以及服务器中关联的服务程序。 单节点集群模式 管理平台自动…

hhdb数据库介绍(10-16)

配置 存储节点参数 存储节点参数通过可视化方式将部分无需重启的参数展示在管理平台上,方便运维人员进行管理。目前支持存储节点实例和计算节点配置库实例的参数管理。参数列表 参数列表展示存储节点信息、版本信息、参数名称、参数当前值、参数默认值、参数有效值范围、参数生…

无线AC AP监控运维方案,保障无线网络稳定运行

智和信通无线网络运维方案通过统一管理跨区域、跨厂商、跨型号的AC/AP设备,对关键性能指标和运行态势进行监控管理。提供常见无线设备品牌支持和资源监测点及指标,实现对不同时期、不同品牌、不同型号无线AC控制器、瘦AP、胖AP的管控。 当前,无线网络已经成为企业信息…

hhdb数据库介绍(10-11)

配置 逻辑库 功能说明: 逻辑库是客户端程序连接计算节点服务器后,可以访问的数据库,描述数据库表的集合,类似于直接连接存储节点实例后,看到的一个数据库。 功能入口: 在关系集群数据库可视化管理平台页面中选择配置->逻辑库。 在逻辑库页面,输入逻辑库名称,点击“搜…