头图

【0基础学爬虫】爬虫基础之网页解析库的使用

大数据时代,各行各业对数据采集的需求日益增多,网络爬虫的运用也更为广泛,越来越多的人开始学习网络爬虫这项技术,K哥爬虫此前已经推出不少爬虫进阶、逆向相关文章,为实现从易到难全方位覆盖,特设【0基础学爬虫】专栏,帮助小白快速入门爬虫,本期为网页解析库的使用。

概述

前几期的文章中讲到了网络请求库的使用,我们已经能够使用各种库对目标网址发起请求,并获取响应信息。本期我们会介绍各网页解析库的使用,讲解如何解析响应信息,提取所需数据。

XPath的使用

XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。同样,XPath 也支持HTML文档的解析。

介绍

XPath 使用路径表达式来匹配HTML文档中的节点或节点集,路径表达式基于HTML文档树,因此在学习XPath 时需要对网页结构有一个初步了解,关于网页结构这些在之前的文章《网页基本结构》中已经介绍到了。

安装

使用XPath 需要安装Python的第三方库lxml,可以使用命令pip install lxml进行安装

使用

下文中,我们会通过一个示例来了解xpath的用法。

<div id="box">
    <p name="test">这是一个测试网页1</p>
</div>
<p name="test">这是一个测试网页2</p>
<div id="city">
    <ul>
        <li id="u1">北京</li>
        <li id="u2">上海</li>
        <li id="u3">广州</li>
        <li id="u4"><a name="sz" href="sz.html" target="_self">深圳</a></li>
    </ul>
</div>
<div class="article"><h3>标题</h3></div>
<div class="article"><p>内容1</p></div>
<div class="article"><p>内容2</p></div>
<div class="article"><p>内容3</p></div>

这是一个简单的网页body结构。我们想要提取页面中的信息,就需要先分析它的结构,理清结构后,编写路径表达式就会更加方便。
1
在前文对xpath的介绍中我们了解到xpath是对XML或HTML文档进行解析的功能,但在代码中,示例中的html文本只是一段字符串,所以在使用xpath进行匹配前首先要将字符串转成HTML对象。

from lxml import etree

element = '''
    <div id="box">
        <p name="test">这是一个测试网页1</p>
    </div>
    <p name="test">这是一个测试网页2</p>
    <div id="city">
        <ul>
            <li id="u1">北京</li>
            <li id="u2">上海</li>
            <li id="u3">广州</li>
            <li id="u4"><a name="sz" href="sz.html" target="_self">深圳</a></li>
        </ul>
    </div>
    <div class="article"><h3>标题</h3></div>
    <div class="article"><p>内容1</p></div>
    <div class="article"><p>内容2</p></div>
    <div class="article"><p>内容3</p></div>
'''

html = etree.HTML(element)
print(html)
#输出:<Element html at 0x1b642114388>

将文本转化为html对象后,就可以使用xpath进行匹配了。

路径表达式

表达式描述示例示例描述
nodename选取此节点下的所有子节点head获取当前head节点下的所有子节点
/从根节点选取/html/head从根节点匹配head节点
//从任意位置匹配节点//head匹配任意head节点
.选取当前节点
..选取当前节点的父节点//head/..匹配head节点的父节点
@选取属性//div[@id="box"]匹配任意id值为box的div标签

选取节点

以示例代码为例,我们想要匹配所有的li标签,可以这样实现:

html.xpath("//li")
#输出 [<Element li at 0x24c09d3e5c8>, <Element li at 0x24c09d3e588>, <Element li at 0x24c09d3e648>, <Element li at 0x24c09d3e688>]

谓语

获取id属性值为box的div标签信息

html.xpath('//div[@id="box"]')
#输出 [<Element div at 0x127672dc688>]

获取所有class属性值为article的标签信息

html.xpath('//*[@class="article"]')
#输出 [<Element div at 0x2898696e4c8>, <Element div at 0x2898696e588>, <Element div at 0x2898696e5c8>, <Element div at 0x2898696e608>]

获取所有class属性值为article的标签下h3标签的文本信息

html.xpath('//*[@class="article"]/h3/text()')
#输出 ['标题']

获取所有class属性值为article的标签下p标签的文本信息

html.xpath('//*[@class="article"]/p/text()')
#输出 ['内容1', '内容2', '内容3']

获取第一个li标签的文本信息

html.xpath('//li[1]/text()')
#输出 ['北京']

获取最后一个li标签下的所有文本信息

html.xpath('//li[last()]//text()')
#输出 ['深圳']

获取倒数第二个li标签下的所有文本信息

html.xpath('//li[last()-1]//text()')
#输出 ['广州']

获取前两个li标签下的文本信息

html.xpath('//li[position()<3]//text()')
#输出 ['北京', '上海']

选取多个路径

html.xpath('//div[@class="article"]/h3/text() | //div[@id="box"]/p/text()')
#输出 ['这是一个测试网页1', '标题']

轴可定义相对于当前节点的节点集。

轴名称结果
ancestor选取当前节点的所有先辈(父、祖父等)。
ancestor-or-self选取当前节点的所有先辈(父、祖父等)以及当前节点本身。
attribute选取当前节点的所有属性。
child选取当前节点的所有子元素。
descendant选取当前节点的所有后代元素(子、孙等)。
descendant-or-self选取当前节点的所有后代元素(子、孙等)以及当前节点本身。
following选取文档中当前节点的结束标签之后的所有节点。
namespace选取当前节点的所有命名空间节点。
parent选取当前节点的父节点。
preceding选取文档中当前节点的开始标签之前的所有节点。
preceding-sibling选取当前节点之前的所有同级节点。
self选取当前节点。

获取ul标签下的子li标签下的a标签的href属性

html.xpath('//ul/child::li/a/@href')
#输出 ['sz.html']

获取a标签的所有先辈div标签

html.xpath('//a/ancestor::div')
#输出 [<Element div at 0x23a712de588>]

获取a标签的所有属性

html.xpath('//a/attribute::*')
#输出 ['sz', 'sz.html', '_self']

获取id属性值为u2的li标签之后的所有p标签的文本信息

html.xpath('//li[@id="u2"]/following::p/text()')
#输出 ['内容1', '内容2', '内容3']

获取id属性值为u2的li标签之前的所有p标签的文本信息

html.xpath('//li[@id="u2"]/preceding::p/text()')
#输出 ['这是一个测试网页1', '这是一个测试网页2']

获取id属性值为u2的li标签之后的所有同级标签的文本信息

html.xpath('//li[@id="u2"]/following-sibling::*/text()')
#输出 ['广州']

获取id属性值为u3的li标签之前的所有同级标签的文本信息

html.xpath('//li[@id="u2"]/preceding-sibling::*/text()')
#输出 ['北京']

运算符

获取id属性值为u1或者u2的标签下的文本信息

html.xpath('//li[@id="u1" or @id="u2"]/text()')
#输出 ['北京', '上海']

判断a标签的name属性值是否为sz

html.xpath('//a/@name="sz"')
#输出 True

函数

xpath提供了非常多的内置函数,这些函数可以用于各种值的计算与处理,这里只介绍常用的函数。

获取所有属性值包含test的标签的文本信息

html.xpath('//*[contains(attribute::*,"test")]/text()')
#输出 ['这是一个测试网页1', '这是一个测试网页2']

将id为u1的li标签和id为u2的li标签的文本信息进行拼接

html.xpath('concat(//li[@id="u1"]/text(),//li[@id="u2"]/text())')
#输出 北京上海

获取id属性值以u开头的所有li标签的文本信息

html.xpath('//li[starts-with(@id,"u")]/text()')
#输出 ['北京', '上海', '广州']

上文中讲到的路径表达式的写法只是xpath中比较常用的写法,基本能够覆盖大部分需求。xpath路径也可以通过F12开发者工具直接获取,在 element 中右键需要匹配的节点元素,复制完整xpath即可。复制下来的完整xpath路径如:/html/body/ul/li[1]。这种方法虽然简单,但实际上路径并不准确,而且路径为绝对路径,相对复杂,所以路径表达式推荐自己手动编写。

BeautifulSoup的使用

BeautifulSoup与上文中介绍的xpath一样,都是用于解析XML或HTML标签中的信息。BeautifulSoup 与 xpath 各有优势,使用哪个可以凭个人喜好。

安装

目前流行的 beautifulsoup 版本为beautifulsoup4,下面简称bs4。

pip install beautifulsoup4

使用

与 xpath 不同,bs4 需要自己选择解析器,常用的解析器有:

html.parser:Python内置解析器

lxml HTML:HTML解析器

lxml XML:XML解析器

各解析器之间的区别主要在于文档解析容错能力,对于不规范的 HTML 文本,它们的解析结果并不一致。

这里我们推荐使用 lxml 作为解析器,使用 lxml 作为解析器需要提前安装 lxml 第三方库。

from bs4 import BeautifulSoup

html = '''
    <div id="box">
        <p name="test">这是一个测试网页1</p>
    </div>
    <p name="test">这是一个测试网页2</p>
    <div id="city">
        <ul>
            <li id="u1">北京</li>
            <li id="u2">上海</li>
            <li id="u3">广州</li>
            <li id="u4"><a name="sz" href="sz.html" target="_self">深圳</a></li>
        </ul>
    </div>
    <div class="article"><h3>标题</h3></div>
    <div class="article"><p>内容1</p></div>
    <div class="article"><p>内容2</p></div>
    <div class="article"><p>内容3</p></div>

'''

soup = BeautifulSoup(html,'lxml')
#返回完整的html文本

bs4的写法比较简洁,更人性化。

soup.body :获取body信息
soup.li :获取第一个li标签
soup.div :获取第一个div标签
soup.li['id'] :获取第一个li标签的id属性值
soup.a.attrs :获取第一个a标签的所有属性值,返回类型为字典

获取多个信息

soup.find_all('li') :获取所有li标签
soup.find_all(["p","a"]) :获取所有p标签与a标签
soup.find_all('div','article') :获取所有类名为article的div标签
soup.find_all(id="box") :获取所有id属性值为box的标签

节点

soup.ul.parent :获取ul标签的父节点
soup.find('a').find_parent("li") :获取第一个a标签的父li标签
soup.find('a').find_parents("li") :获取第一个a标签的所有父li标签
soup.find('li').find_next_siblings("li") :获取第一个li标签后的兄弟li标签
soup.find('li').find_next_sibling("li") :获取第一个li标签后的第一个兄弟li标签
soup.find(attrs={'id':'u4'}).find_previous_siblings("li") :获取id属性值为u4的li标签前的所有兄弟li标签
soup.find(attrs={'id':'u4'}).find_previous_sibling("li") :获取id属性值为u4的li标签前的第一个兄弟li标签

CSS选择器

BeautifulSoup 支持大部分的CSS选择器,CSS选择器在之前的文章《网页基本结构》中做了介绍。

soup.select('div h3') :获取div下的h3标签
soup.select('#city #u2') :获取id属性值为city的标签下id值u2的标签
soup.select('a[href="sz.html"]') :获取href属性值为sz.html的a标签
soup.select_one(".article") :获取第一个类名为article的标签

bs4 作用上与 xpath 基本一致,但是 bs4 的优势就在于语句的简洁性,用bs4匹配数据比 xpath 稍微简单一些,但是它在性能上比 xpath 要稍弱。

re正则表达式的使用

正则表达式(Regular Expression,通常简写为“regex”或“regexp”)是一种用来匹配文本字符串的模式。在编程和文本处理中,正则表达式通常被用来进行字符串匹配、搜索、替换等操作。

实际开发中,我们会对一些非结构化数据进行解析,对于这类数据,无论是 xpath 还是 BeautifulSoup 都无法进行解析。这时我们就需要用到正则表达式,正则表达式的强大在于它能够匹配任意类型的文本数据,可以帮助开发者快速的处理文本数据。

安装

Python 中内置了 re 库,无需额外安装

使用

模式描述
\w匹配字母数字及下划线
\W匹配非字母数字及下划线
\s匹配任意空白字符,等价于[\t\n\r\f]
\S匹配任意非空白字符
\d匹配任意数字,等价于[0-9]
\D匹配任意非数字的字符
\A匹配字符串的开头
\Z匹配字符串的结尾,如存在换行,只匹配到换行前的结束字符串
\z匹配字符串的结尾,如存在换行,会匹配换行符
^匹配字符串的开头
$匹配字符串的结尾
.匹配任意字符,除换行符,当re.DOTALL被指定时可以匹配包括换行符的任意字符
[...]匹配一组字符,如[abc],匹配a,b,c
1匹配不在[]中的字符
*匹配0或多个表达式
+匹配1或多个表达式
?对它前面的正则式匹配0到1次
{n}匹配n个之前的正则表达式
{n,m}对表达式进行n到m次匹配,尽量取最多
()匹配括号内的任意表达式

compile函数

re.compile 可以将正则表达式样式的字符串编译为一个正则表达式对象,可以通过这个对象来调用下述方法。

pattern = re.compile("\d")

match函数

re.match 会从字符串的起始位置进行匹配正则表达式,匹配成功后会返回匹配成功的结果,匹配失败则返回None。

import re
#匹配字符a
pattern = re.compile("a") 
#从字符串开头开始匹配
print(pattern.match("cat")) 
#从下标为1的位置开始匹配
print(pattern.match("cat",1)) 

运行结果:

None
<re.Match object; span=(1, 2), match='a'>

表达式匹配:

#以hello开头中间为数字后面是World的字符串
pattern = re.compile("^hello\s(\d+)\sWorld") 
print(pattern.match("hello 123 World!!!")) 

运行结果:

<re.Match object; span=(0, 15), match='hello 123 World'>

可以看到,两次的运行结果都是一个对象,可以使用group()方法获取匹配到的文本信息。

pattern = re.compile("^hello\s(\d+)\sWorld")
result = pattern.match("hello 123 World!!!")
print(result.group())
print(result.group(1))

运行结果:

hello 123 World
123

group(1)会返回第一个被括号包围的匹配结果,示例中被括号包围的是\d+,所以输出的结果为123。

Search函数

match函数是从字符串的开头开始匹配,想要从其它地方开始匹配需要自己传入位置,用这种方法匹配数据局限性很大。我们想要从任意位置开始匹配数据可以使用 re.search 函数。

pattern = re.compile("(\d+)")
result = pattern.search("hello 123 World321!!!")
print(result)
print(result.group(1))

运行结果:

<re.Match object; span=(6, 9), match='123'>
123

可以看到,使用 search 方法我们无需指定位置,它会搜寻整个字符串,返回第一个匹配成功的结果。

findall函数

search 函数可以从任意位置进行匹配,但是它只会返回第一个匹配成功的结果。我们想要获取所有匹配成功的结果就需要用到 findall 函数。

pattern = re.compile("(\d+)")
result = pattern.findall("hello 123 World321!!!")
print(result)

运行结果:

['123', '321']

findall 函数会返回一个列表,因此 findall 函数返回的结果无法使用 group 方法。

通用匹配

在实际开发中,我们往往会遇到非常复杂的文本结构如:

"hello 123 this is a 999Regex Demo!!!World321"

这时如果使用\w,\s进行匹配会显得非常复杂,这时我们就可以使用一个通配组合.*,上文中介绍到了.是匹配任意字符,*是匹配0或多个表达式,.*搭配在一起就是匹配任意多个字符。使用.*可以简单有效的进行数据匹配。

#匹配hello World之间的信息
pattern = re.compile("hello(.*)World")
result = pattern.findall("hello123 World this is a 999Regex Demo!!!World321")
print(result)

运行结果:

['123 World this is a 999Regex Demo!!!']

这里我们可以看到,使用.*后它匹配到了第一个 hello 到最后一个 World 之间的所有文本。但如果我们想要匹配 hello 到第一个 world 之间的信息呢。这时我们就需要了解一下贪婪匹配与非贪婪匹配。

贪婪与非贪婪

顾名思义,贪婪模式表示尽可能多的匹配,非贪婪模式表示尽可能少的匹配。从上文的示例我们可以看到re.compile("hello(.*)World")它会从 hello 开始,匹配到最后一个 World,这显然就是贪婪模式,它会尽可能多的匹配,也就是匹配到最后一个符合规则的位置。正则表达式中,非贪婪模式需要使用到?,前文中讲到了?是匹配0到1次,使用?就能实现尽可能少的匹配。

#匹配hello World之间的信息(非贪婪)
pattern = re.compile("hello(.*?)World")
result = pattern.findall("hello123 World this is a 999Regex Demo!!!World321")
print(result)

运行结果:

['123 ']

可以看到使用.*?后,正则表达式只匹配到第一个 World 就停下了,这样就实现了非贪婪匹配。

Newspaper智能解析库的使用

Newspaper 是 Python 的第三方库,主要用于抓取新闻网页。它能够自动解析网页内容,匹配出新闻的各种信息。而且操作简单,非常容易上手。但是它并不适用于实际开发,因为它不够稳定,存在各种问题,无法应对爬虫开发中可能遇到的问题,如反爬虫等,所以这里对它只做介绍。

安装

命令行安装:pip install newspaper3k

使用

它的使用非常简单,传入目标网址后,调用 download() 方法下载网页源代码,使用 parse() 方法解析源码。

from newspaper import Article
# 目标新闻网址
url = 'https://目标文章'
news = Article(url, language='zh')
news.download()
news.parse()
#获取新闻网页源码
print(news.html)
#获取新闻标题
print(news.title)
#获取新闻正文
print(news.text)
#获取新闻作者
print(news.authors)
#获取新闻发布时间
print(news.publish_date)
#获取新闻关键词
print(news.keywords)
#获取新闻摘要
print(news.summary)
#获取新闻配图地址
print(news.top_image)
#获取新闻视频地址
print(news.movies)

Newspaper 可以与 requests 配合使用,通过 requests 获取源码,由 Newspaper 进行解析提取。Newspaper 库并不能完美的解析出各种信息,适合非专业人士使用。

总结

上文中,讲到了四个爬虫解析库的使用,其中 xpath 与 beautifulSoup 主要用于对 html 文本的解析,正则表达式主要用于对非结构化文本的解析,无法用 xpath 和 beautifulSoup 解析的文本信息通常会使用正则来进行匹配。Newspaper 是智能解析库,使用它可以自动解析新闻信息,无需自己编写表达式,但是缺点也很明显 。

与网络请求库一样,网页解析库的使用是每一个爬虫初学者都应该牢牢掌握的知识点,能够熟练的使用解析库才能更好的完成数据采集工作。


  1. ...

Python网络爬虫、JS 逆向等相关技术研究与分享。

105 声望
64 粉丝
0 条评论
推荐阅读
【K哥爬虫普法】你很会写爬虫吗?10秒抢票、10秒入狱,了解一下?
我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了“K哥爬虫普法”专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识,知晓如何合法合规利用爬...

K哥爬虫阅读 182

又一款眼前一亮的Linux终端工具!
今天给大家介绍一款最近发现的功能十分强大,颜值非常高的一款终端工具。这个神器我是在其他公众号文章上看到的,但他们都没把它的强大之处介绍明白,所以我自己体验一波后,再向大家分享自己的体验。

良许5阅读 1.8k

FastAPI性能碾压Flask?
不止一次的听过,FastAPI性能碾压Flask,直追Golang,不过一直没有测试过,今天闲着没事测试一下看看结果。不知道是哪里出了问题,结果大跌眼镜。

二毛erma02阅读 10.2k评论 3

封面图
一个灵活的 Node.js 多功能爬虫库 —— x-crawl
x-crawl · x-crawl 是一个灵活的 Node.js 多功能爬虫库。灵活的使用方式和众多的功能可以帮助您快速、安全、稳定地爬取页面、接口以及文件。如果你也喜欢 x-crawl ,可以给 x-crawl 存储库 点个 star 支持一下,...

coderhxl2阅读 1.9k评论 2

封面图
Python之如何优雅的重试
为了避免偶尔的网络连接失败,需要加上重试机制,那么最简单的形式就是在对应的代码片段加一个循环,循环体里使用异常捕获,连接成功时退出循环,否则就重复执行相关逻辑,此时修改之后的函数f如下

Harpsichord12073阅读 7.3k

Linux终端居然也可以做文件浏览器?
大家好,我是良许。在抖音上做直播已经整整 5 个月了,我很自豪我一路坚持到了现在【笑脸】最近我在做直播的时候,也开始学习鱼皮大佬,直播写代码。当然我不懂 Java 后端,因此就写写自己擅长的 Shell 脚本。但...

良许1阅读 2.1k

基于 EKS Fargate 搭建微服务性能分析系统
近期 Amazon Fargate 在中国区正式落地,因 Fargate 使用 Serverless 架构,更加适合对性能要求不敏感的服务使用,Pyroscope 是一款基于 Golang 开发的应用程序性能分析工具,Pyroscope 的服务端为无状态服务且性...

亚马逊云开发者阅读 7.8k

Python网络爬虫、JS 逆向等相关技术研究与分享。

105 声望
64 粉丝
宣传栏