本节目标:使用xpath选取器得到所有章节的链接。
本节内容:xpath解析HTML 本节技术点:lxml,xpath
本节阅读需要(20)min。 本节实操需要(20)min。
- 系列文章目录
- 前言
- 一、构建文档树
- 二、xpath语法
- 语法元素
- 内容匹配高级部分
- 根据位置匹配
- 实例讲解
- 实战
- bs4版本
- xpath版本
- 总结
xpath作为一种HTML专用的解析语法。可以快速的定位内容。 相当于HTML的正则表达式。
和前一节的bs4爬取信息的思路是差不多的,但是方式和形式上看上去有不小的差异。
一、构建文档树使用时先安装 lxml 包
pip install lxml
from lxml import etree
# 读取外部文件 index.html
html = etree.parse('./index.html')
result = etree.tostring(html, pretty_print=True) #pretty_print=True 会格式化输出
print(result)
parse读取。 tostring将etree的文档节点对象转化为文本字符串。
不过一般上游不是本地的HTML,而是response返回的text。
html = etree.HTML(html_text)
result = etree.tostring(html,encoding="gbk")
print(str(result,'gbk')) # 相当于decode
根据我们的实例,需要这么处理,不然又回乱码了…
很多教程是utf-8.但是对于我们的实例是不对的.因为其他人的教程都是内部的格式字符串,自然是utf-8
但是更多的中文网页的情形却不是,尤其是文本含量比较多的网站.
二、xpath语法xpath的语法也是递归的.也就是说返回的还是一个可以解析的文本节点对象. 这与前面的bs4和find是一个逻辑.
r_list = parse_html.xpath('xpath表达式')
返回的是一个对象的列表.
语法元素表达式 描述
nodename 选取此节点的所有子节点
/ 从当前节点选取直接子节点
// 从当前节点选取子孙节点
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性
* 通配符,选择所有元素节点与元素名
@* 选取所有属性
[@attrib] 选取具有给定属性的所有元素
[@attrib='value'] 选取给定属性具有给定值的所有元素
[tag] 选取所有具有指定元素的直接子节点
[tag='text'] 选取所有具有指定元素并且文本内容是text节点
contains(string1,string2)
starts-with(string1,string2)
ends-with(string1,string2) #不支持
upper-case(string) #不支持
text()
last()
position()
node()
以上的这些都是通过内容匹配。 与HTML和前端有一点不一样的是,标签里面所有的都会作为属性比如@style,@class 语法和css的还是比较像的. 但是写到一起就看上去很不一样了…
内容匹配高级部分要选择 style属性值 包含 color 字符串的 页面元素 ,可以这样 //*[contains(@style,'color')]
要选择 style属性值 以 color 字符串 开头 的 页面元素 ,可以这样 //*[starts-with(@style,'color')]
要选择 style属性值 以 某个 字符串 结尾 的 页面元素 ,大家可以推测是 //*[ends-with(@style,'color')],
根据位置匹配
xpath使用的时候永远是从后往前,只要能匹配到,或者大致匹配到,就行。不需要完整的xpath表达式, 因为容易造成空匹配。
//div/p[last()-2]
//*[@class='multi_choice']/*[position()10]')
html.xpath('//li/a[@num1>10 and @num2]')
# 获取class的值,不限于class
html.xpath('//li/a[@href="link2.html"]/@class')
# 文本获取
html.xpath('//li/a[@href="link2.html"]/text()')
注意: 如果是直接的子元素html.xpath(‘a’) ,无需再加/.或者写成xpath(‘./a’)。 html.xpath(‘/a’) 指的是孙子节点a。
所以大致分为三个步骤
- 用//大步长跳转,/小步长跳转定位到tag
- 使用属性限定得到目标地元素集合
- 提取文本信息。
我们需要得到每个章节的链接。 其实就是在昨天的基础上更进一步。 也可以用CSS选择器完成。
bs4版本dt_nu = 0
href_list = []
for title in raw_titles:
if not title.find('a') :
print("dt")
dt_nu +=1
if dt_nu >=2 and title.find('a') :
href_str = title.find('a')["href"] # 用CSS选择器完成的
href_list.append(href_str)
with open("href.txt","w",encoding='utf-8') as HREF:
for title in href_list:
if title:
HREF.write(title+"\n")
注意lxml和bs4数据类型不一样,所以互不兼容的。
xpath版本html = etree.HTML(html_text)
# result = etree.tostring(html,encoding="gbk")
# print(result)
# print(str(result,'gbk'))
raw_list = html.xpath('//div[@class="listmain"]/dl/*')
print(len(raw_list))
dt_nu = 0
href_list = []
# print(raw_list[1].xpath('./a'))
for title in raw_list:
if not title.xpath('./a') :
print("dt")
dt_nu +=1
if dt_nu >=2 and title.xpath('./a') :
href_str = title.xpath('./a/@href')[0] # 哪怕只有一个返回的也是列表
href_list.append(href_str)
with open("href.txt","w",encoding='utf-8') as HREF:
for title in href_list:
if title:
HREF.write(title+"\n")
但是我们发现href只是一部分。 如下:
/1_1852/835564.html
/1_1852/835565.html
/1_1852/835566.html
/1_1852/835567.html
/1_1852/835568.html
/1_1852/835569.html
/1_1852/835570.html
/1_1852/835571.html
显然不是完整的url。 所以我们需要url链接
raw_list = html.xpath('//div[@class="listmain"]/dl/*')
print(len(raw_list))
base_url = 'https://www.zhhbq.com/' # 前缀
dt_nu = 0
href_list = []
# print(raw_list[1].xpath('./a'))
for title in raw_list:
if not title.xpath('./a') :
print("dt")
dt_nu +=1
if dt_nu >=2 and title.xpath('./a') :
href_str = title.xpath('./a/@href')[0]
href_str = base_url+href_str[1:] # 去除一个重复的/
href_list.append(href_str)
with open("href.txt","w",encoding='utf-8') as HREF:
for title in href_list:
if title:
HREF.write(title+"\n")
https://www.zhhbqg.com/1_1852/1_1852/835564.html
https://www.zhhbqg.com/1_1852/1_1852/835565.html
https://www.zhhbqg.com/1_1852/1_1852/835566.html
https://www.zhhbqg.com/1_1852/1_1852/835567.html
well done成功!!!
总结注意xpath的语法。 使用*匹配的时候需要注意,有没有干扰项? 能不能用属性去除? 不能的话后面还需要自己写逻辑过滤。
注意,是先看实际的网页是什么url。然后根据自己爬取到的信息自动批量化拼接的。 不是用爬到的信息瞎拼接的。规则不是爬虫工程师定的,是后端路由规则决定的。
延伸阅读:
lxml延伸阅读