使用上节的urllib,可以简单地模拟用户请求,获取完整的网页内容,但大多数时候,我们可能只需要其中的一部分,这就涉及到数据解析了。本节讲解一波「XPath语法」及Python中支持此语法的「解析库lxml」。
lxml库安装及使用
lxml是Python中的一个解析库,底层由C语言实现,支持XML及HTML的解析,支持XPath解析方式,解析效率非常高。
安装方法非常简单,直接通过pip命令安装即可:
pip install lxml
利用lxml写个简单的例子提取百度Logo地址:
from urllib import requestfrom lxml import etree
resp = request.urlopen("https://www.baidu.com").read().decode('utf-8')# 传入html文本,得到一个可进行xpath的对象
selector = etree.HTML(resp) # 编写提取图标路径的XPath表达式
xpath_reg = '/html/head/link[@rel="icon"]/@href'
results = selector.xpath(xpath_reg)
print(results[0])# 运行结果如下:
//www.baidu.com/img/baidu_85beaf5496f291521eb75ba38eacbd87.svg# 如果是从本地文件中读取
local_selector = etree.parse('hello.html')
XPath概念与简介
// 概念 //
全称XML Path Language,XML路径语言,一门在XML文档中查找信息的语言,起初是用来搜寻XML文档的,但同样适用于HTML文档的搜索。
// 简介 //
XPath选择功能强大,提供非常简明的「 路径选择表达式
」,除此之外还提供了超过100个的内建函数,用于字符串、数值、时间的匹配以及节点、序列的处理等。
XPath于1999年11月16日成为W3C标准,它被设计为供XSLT、XPointer以及其他XML解析软件使用,想了解更多可移步至官方网站:https://www.w3.org/TR/xpath/
XPath语法详解
// 结点关系 //
在Xpath中,有七种类型的节点:元素、属性、文本、命名空间、处理指令、注释及文档节点(或称根节点)。说完节点类型,再来说下节点关系,比如下面的XML文档:
<executions><execution><id>make-assemblyid><phase>packagephase><goals><goal>singlegoal>goals>execution>executions>
有如下这些节点关系:
- 父(Parent) → 每个元素及属性都有一个父:execution是id、phase、goals及goal的父;
- 子(Children) → 元素节点可能有零个或多个子:id、phase、goals及goal是execution的子;
- 同胞(Sibling) → 拥有相同的父的节点:id、phase、goals及goal是execution的同胞;
- 先辈(Ancestor) → 某节点的父、父的父,等等:goals的先辈是execution及executions;
- 后代(Descendant) → 某节点的子,子的子,等等:execution的后代是id、phase、goals及goal;
// 选取节点 //
XPath使用路径表达式在XML文档中选取节点,节点是 沿着路径
或 Step
来选取的(上述子元素从上往下找节点),接着说下具体的路径表达规则: ----------------
①
: 最有用
表达式 | 描述 |
nodename | 此节点的所有子节点 |
/ | 绝对路径,从根节点选取 |
// | 相对路径,从匹配选择的当前节点选择文档中的节点,不考虑位置 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
----------------
②
: 谓语(Predicates)
谓语嵌在方括号中,用来查找某个特定结点或包含某个指定值的节点。
表达式 | 描述 |
/a/b[1] | 选取a子元素的 第一个 b元素 |
/a/b[last()] | 选取 a 子元素的 最后一个 b元素 |
/a/b[last()-1] | 选取 a 子元素的 倒数第二个 b元素 |
/a/b[postion()<3] | 选取 a 子元素的 最前面两个 b元素 |
//a[@id] | 选取所有拥有属性id的a元素 |
/a[@id='test'] | 选取所有拥有属性id,且值为test的a元素 |
/a/b[size>10] | 选取a子元素中所有b元素,且其中的size元素值须大于10 |
/a/b[size>10]/c | 选取a子元素中所有b元素(size元素值大于10) 的所有c元素 |
----------------
③
: 选取未知节点
表达式 | 描述 |
* | 匹配任何元素节点 |
@* | 匹配任何属性节点 |
node() | 匹配任何类型的结点 |
----------------
④
: 选取若干路径
可在路径表达式中使用" |
" 运算符
来选取若干个路径,如: //a/b | //a/c → 选取a元素中所有的b和c元素 ----------------
⑤:
轴
当上述操作都不能定位时,可以考虑根据节点的父节点或兄弟结点来定位,此时就会用到Xpath轴,利用轴可定位某个相对于当前节点的节点集,语法: 轴名称::元素名
,具体规则如下表所示:
轴名称 | 描述 |
child | 选取当前节点的所有子元素 |
parent | 选取当前节点的父节点 |
descendant | 选取当前节点的所有后代元素(子、孙等) |
descendant-or-self | 选取当前节点的所有后代元素(子、孙等)以及当前节点本身 |
ancestor | 选取当前节点的所有先辈(父、祖父等) |
ancestor-or-self | 选取当前节点的所有先辈(父、祖父等)以及当前节点本身 |
preceding | 选取文档中当前节点的开始标签之前的所有节点 |
preceding-sibling | 选取当前节点之前的所有同级节点 |
following | 选取当前节点的结束标签之后的所有节点 |
following-sibling | 选取当前节点之后的所有同级节点 |
self | 选取当前节点 |
attribute | 选取当前节点的所有属性 |
namespace | 选取当前节点的所有命名空间节点 |
使用代码示例如下:
# 先定位到图标,然后获取同级结点,然后定位到name为description的meta结点
xpath_reg = '//link[@rel="icon"]/preceding::meta[@name="description"]/@content'# 运行结果如下:
全球最大的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,可以瞬间找到相关的搜索结果。
----------------
⑥
: 运算符
除了上面介绍的 "|" 运算符外,还有其他的XPATH运算符,此处罗列下:
运算符 | 描述 |
+-* | 加减乘 |
div | 除 |
= != | 等于、不等于 |
< <= > >= | 小于、小于等于、大于、大于等于 |
or and | 或、与 |
mod | 计算除法余数 |
----------------
⑦
: 函数
除了上面的last()、postion()函数外,还有:
- text():获得元素文本内容;
- contains(string1,string2):前后是否匹配,匹配返回True,否则返回False;
- starts-with():从起始位置匹配字符串;
更多XPath函数及用法,可移步至下述网站自行查阅:
https://www.w3school.com.cn/xpath/xpath_functions.asp
// 调试小技巧 //
可在Chrome浏览器直接测试编写的XPath表达式,看下是否定位成功,在开发者工具的Elements选项卡按下Ctrl+F,然后在底部输入XPath表达式即可定位,示例如下:
你也可以在此测试编写的xpath表达式能否 定位到目标元素,而不用上来就写程序测试。另外,你还可以右键目标元素,直接获取Xpath,当然不一定是最优的。
实战:爬取小说站点
目标站点:
爬取内容:
爬取某部小说的所有章节,把小说内容保存为本地txt文件(以小说元尊为例);
// ① 提取章节数据列表 //
如图,需提取正文卷所有章节,打开Chrome开发者工具,点击左上角的箭头
然后点击网页上的《元尊》正文卷,快速定位到下述结点:
需要获得《元尊》正文卷后所有dd节点里的a节点,不难写出这样的XPath表达式:
//dt[text()="《元尊》正文卷"]/following-sibling::dd/a
把表达式放到Chrome浏览器试试看:
也可以写下代码简单验证下:
from urllib import request, parsefrom lxml import etreeimport ssl# 取消SSL的证书验证
ssl._create_default_https_context = ssl._create_unverified_context
base_url = "https://www.biqukan.com"
novel_url = base_url + "/0_790/" # 小说《元尊》的链接
novel_headers = {'Host': parse.urlparse(base_url).netloc,'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ''Chrome/86.0.4240.80 Safari/537.36 '
}# 提取章节def fetch_chapter():
req = request.Request(novel_url, headers=novel_headers)
resp = request.urlopen(req).read().decode('gbk')
selector = etree.HTML(resp)
chapter_xpath_reg = '//dt[text()="《元尊》正文卷"]/following-sibling::dd/a'
results = selector.xpath(chapter_xpath_reg)for result in results:
print(result.text, result.attrib.get('href'))# 提取内容if __name__ == '__main__':
fetch_chapter()
运行结果如下图所示:
// ② 提取章节内容 //
提取完章节列表,接着提取章节内容,打开某个章节的链接,如:
https://www.biqukan.com/0_790/63024951.html
快速定位到:
这个XPath表达式也很好写,直接定位到
//div[@id="content"]/text()
写下简单的代码验证下:
def fetch_content(novel_url):
req = request.Request(novel_url, headers=novel_headers)
resp = request.urlopen(req).read().decode('gbk')
selector = etree.HTML(resp)
content_xpath_reg = '//div[@id="content"]/text()'
results = selector.xpath(content_xpath_reg)for result in results:
print(result)if __name__ == '__main__':
fetch_content('https://www.biqukan.com/0_790/63024951.html')
运行结果如下图所示:
可以,提取到小说内容了,接着加上保存txt,完善下小细节,写出完整代码。
// ③ 编写完整代码 //
# 爬取小说站点from urllib import request, parse, errorfrom lxml import etreeimport sslimport osimport timefrom random import randint# 取消SSL的证书验证
ssl._create_default_https_context = ssl._create_unverified_context
base_url = "https://www.biqukan.com"
novel_url = base_url + "/0_790/" # 小说《元尊》的链接
novel_list = []
novel_save_dir = os.path.join(os.getcwd(), 'novel_cache/') # 小说的保存文件夹
novel_headers = {'Host': parse.urlparse(base_url).netloc,'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ''Chrome/86.0.4240.80 Safari/537.36 '
}# 保存小说信息的类class Novel:
chapter_name = ''
chapter_url = ''def __init__(self, chapter_name, chapter_url):
self.chapter_name = chapter_name
self.chapter_url = base_url + chapter_url# 提取小说章节列表def fetch_chapter():
print("爬取:", novel_url)
req = request.Request(novel_url, headers=novel_headers)
resp = request.urlopen(req).read().decode('gbk')
selector = etree.HTML(resp)
chapter_xpath_reg = '//dt[text()="《元尊》正文卷"]/following-sibling::dd/a'
results = selector.xpath(chapter_xpath_reg)
data_list = []for result in results:
data_list.append(Novel(result.text, result.attrib.get('href')))return data_list# 提取小说内容def fetch_content(novel):
print("爬取:", novel.chapter_url, end="\t")
req = request.Request(novel.chapter_url, headers=novel_headers)
resp = request.urlopen(req).read().decode('gbk')
selector = etree.HTML(resp)
content_xpath_reg = '//div[@id="content"]/text()'
results = selector.xpath(content_xpath_reg)
content = ''for result in results[:-2]:
content = content + result + "\n"
save_novel(content, novel.chapter_name)# 将章节内容写入txtdef save_novel(content, novel_name):try:with open(novel_save_dir + novel_name + '.txt', 'w+', encoding='utf-8') as f:
f.write(content)except (error.HTTPError, OSError) as reason:
print(str(reason))else:
print("下载完成:" + novel_name)# 提取内容if __name__ == '__main__':# 判断存储小说的目录是否存在,不存在创建if not os.path.exists(novel_save_dir):
os.mkdir(novel_save_dir)
novel_list = fetch_chapter()for n in novel_list:# 随机休眠0-3s,防止ip被封
time.sleep(randint(0, 3))
fetch_content(n)
运行结果如下所示: