数据解析
本文章中, 介绍三种解析方式:
- re解析 (必修)
- bs4解析
- xpath解析(必修)
以上三种方案都可以从HTML中提取到你需要的内容, 这三种方式可以混合进行使用, 完全以结果做导向, 只要能拿到你想要的数据. 用什么方案并不重要. 当你掌握了这些之后. 再考虑性能的问题.
一. re解析
1.1 正则
Regular Expression, 正则表达式, 一种使用表达式的方式对字符串进行匹配的语法规则.
我们抓取到的网页源代码本质上就是一个超长的字符串, 想从里面提取内容.用正则再合适不过了.
正则的优点: 速度快, 效率高, 准确性高。
正则的缺点: 新手上手难度有点儿高。
不过只要掌握了正则编写的逻辑关系, 写出一个提取页面内容的正则其实并不复杂。
正则的语法: 使用元字符进行排列组合用来匹配字符串,在线测试正则表达式 https://tool.oschina.net/regex/
元字符: 具有固定含义的特殊符号。
常用元字符:
. √匹配除换行符以外的任意字符, 未来在python的re模块中是一个坑.
\w √匹配字母或数字或下划线. 0-9 a-z Z-Z _ 用户名
\s 匹配任意的空白符
\d √匹配数字
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配字符串的开始
$ 匹配字符串的结尾
\W 匹配非字母或数字或下划线
\D 匹配非数字
\S 匹配非空白符
a|b 匹配字符a或字符b
() √匹配括号内的表达式,也表示一个组
[...] √匹配字符组中的字符
[^...] 匹配除了字符组中字符的所有字符
量词: 控制前面的元字符出现的次数
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次
贪婪匹配和惰性匹配(重点)
.* 贪婪匹配, 尽可能多的去匹配结果
.*? 惰性匹配, 尽可能少的去匹配结果 -> 回溯
先看案例
str: 玩儿吃鸡游戏, 晚上一起上游戏, 干嘛呢? 打游戏啊
reg: 玩儿.*?游戏
此时匹配的是: 玩儿吃鸡游戏
reg: 玩儿.*游戏
此时匹配的是: 玩儿吃鸡游戏, 晚上一起上游戏, 干嘛呢? 打游戏
str: <div>胡辣汤</div>
reg: <.*>
结果: <div>胡辣汤</div>
str: <div>胡辣汤</div>
reg: <.*?>
结果:
<div>
</div>
str: <div class="abc"><div>胡辣汤</div><div>饭团</div></div>
reg: <div>.*?</div>
结果:
<div>胡辣汤</div>
<div>饭团</div>
所以我们能发现这样一个规律: .*? 表示尽可能少的匹配, .*表示尽可能多的匹配, 暂时先记住这个规律. 写爬虫会用到
1.2 re模块
那么接下来的问题是, 正则我会写了, 怎么在python程序中使用正则呢? 答案是re模块
re模块中我们只需要记住这么几个功能就够我们使用了.
- findall 查找所有. 返回list
lst = re.findall("m", "mai le fo len, mai ni mei!")
print(lst) # ['m', 'm', 'm']
lst = re.findall(r"\d+", "5点之前. 你要给我5000万")
print(lst) # ['5', '5000']
- search 会进行匹配. 但是如果匹配到了第一个结果. 就会返回这个结果. 如果匹配不上search返回的则是None
ret = re.search(r'\d', '5点之前. 你要给我5000万').group()
print(ret) # 5
- match 只能从字符串的开头进行匹配(再见)
ret = re.match('a', 'abc').group()
print(ret) # a
- finditer 和findall差不多. 只不过这时返回的是迭代器(重点)
it = re.finditer("m", "mai le fo len, mai ni mei!")
for el in it:
print(el.group()) # 依然需要分组
- compile() 可以将一个长长的正则进行预加载. 方便后面的使用
obj = re.compile(r'\d{3}') # 将正则表达式编译成为一个 正则表达式对象, 规则要匹配的是3个数字
ret = obj.search('abc123eeee') # 正则表达式对象调用search, 参数为待匹配的字符串
print(ret.group()) # 结果: 123
- 正则中的内容如何单独提取?
单独获取到正则中的具体内容可以给分组起名字
s = """
<div class='西游记'><span id='10010'>中国联通</span></div>
"""
obj = re.compile(r"<span id='(?P<id>\d+)'>(?P<name>\w+)</span>", re.S)
result = obj.search(s)
print(result.group()) # 结果: <span id='10010'>中国联通</span>
print(result.group("id")) # 结果: 10010 # 获取id组的内容
print(result.group("name")) # 结果: 中国联通 # 获取name组的内容
这里可以看到我们可以通过使用分组. 来对正则匹配到的内容进一步的进行筛选.
- 正则表达式本身是用来提取字符串中的内容的. 也可以用作字符串的替换
import re
r = re.split(r"\d+", "我今年19岁了, 你知道么, 19岁就已经很大了. 周杰伦20岁就得奖了")
print(r) # ['我今年', '岁了, 你知道么, ', '岁就已经很大了. 周杰伦', '岁就得奖了']
# 替换
r = re.sub(r"\d+", "18", "我今年19岁了, 你知道么, 19岁就已经很大了. 周杰伦20岁就得奖了")
print(r) # 我今年18岁了, 你知道么, 18岁就已经很大了. 周杰伦18岁就得奖了
哦了. 正则. 这些东西够用了.
二. bs4解析
估计看到这里. 你应该是半崩溃状态. 用正则提取东西实在太痛苦了. 有没有那种能直接提取标签的方法. 有的. 但是在学习新方案之前. 我们必须要了解和知道, 网页的组成(HTML, CSS). 了解完这两个东西之后. 再学bs4
, xpath
这些就容易很多了.
2.1 HTML基本结构
HTML(Hyper Text Markup Language)超文本标记语言, 是我们编写网页的最基本也是最核心的一种语言. 其语法规则就是用不同的标签对网页上的内容进行标记, 从而使网页显示出不同的展示效果.
<h1>
我爱你
</h1>
上述代码的含义是在页面中显示"我爱你"三个字, 但是我爱你三个字被"<h1>“和”</h1>"标记了. 白话就是被括起来了. 被H1这个标签括起来了. 这个时候. 浏览器在展示的时候就会让我爱你变粗变大. 俗称标题, 所以HTML的语法就是用类似这样的标签对页面内容进行标记. 不同的标签表现出来的效果也是不一样的.
h1: 一级标题
h2: 二级标题
p: 段落
font: 字体(被废弃了, 但能用)
body: 主体
这里只是给小白们简单科普一下, 其实HTML标签还有很多很多的. 我们不需要一一列举(主要介绍爬虫相关, 不主介绍前端).
OK~ 标签我们明白了, 接下来就是属性了.
<h1>
我爱你
</h1>
<h1 align='right'>
我爱你妹
</h1>
有意思了. 我们发现在标签中还可以给出xxx=xxx这样的东西. 那么它又是什么呢? 又该如何解读呢?
首先, 这两个标签都是h1标签, 都是一级标题, 但是下面这个会显示在右边. 也就是说, 通过xxx=xxx这种形式对h1标签进一步的说明了. 那么这种语法在html中被称为标签的属性. 并且属性可以有很多个. 例如:
<body text="green" bgcolor="#eee">
你看我的颜色. 贼健康
</body>
总结, html
语法:
<标签 属性1="值" 属性2="值">
被标记的内容
</标签>
或
<标签 属性1="值" 属性2="值"/>
对于语法层面, 我们知道这么多就够了. 大多数情况下, 我们并不用关心div
和span
有什么区别. 也不用关心section
是什么. 但是有几个标签. 我们是必须要知道的. 因为未来, 我们会高密度的和这几个标签做斗争.
- a 超链接
- img 图片
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="http://www.baidu.com">我是百度</a>
<a href="myself.html">我是自己</a>
<a href="dddd/myself.html">我是自己</a>
<img src="https://mmbiz.qpic.cn/mmbiz/ZKGHmo3MibQUhB9O7E0b05wBDuP1beR86kcXtFsPUZVnanPW2Sjmwiaia0ibICJt4Q8Q7RDyF3bthpciaBuOwcgEWZg/0"/>
</body>
</html>
2.2 CSS选择器
CSS
全称层叠样式表(Cascading Style Sheets), 主要用来定义页面内容展示效果的一门语言.
HTML
: 页面骨架. 素颜
CSS
: 页面效果美化. 美妆+滤镜
2.2.1. css语法规则:
- 通过
style
属性来编写样式 - 通过
style
标签中定义选择器
. 然后使用选择器的来选择页面上的元素, 添加样式 - 在
css
文件中编写样式, 通过link引入该文件
2.2.2. css选择器(重点)
1. id选择器 #id值
2. 标签选择器 标签
3. 类选择器 .
4. 选择器分组 ,
5. 后代选择器 空格
6. 子选择器 父 > 子
7. 属性选择器 [属性=值]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
/*标签选择器, 选择页面上的标签, 添加样式*/
div{
width:400px;
height:300px;
border: 1px solid red;
float: left;
font-size: 1cm;
}
/*id选择器, 根据页面上的id值.选择标签*/
#jay{
color:green;
}
/*类选择器, 根据页面上的class值.选择标签*/
.wlh{
color:yellowgreen;
}
/*类选择器, 根据页面上的class值.选择标签*/
/*
解读: 寻找class是wlh的p标签
<p class='wlh'></p>
*/
p.wlh{
color:lightseagreen;
}
/*标签选择器, 根据页面上的标签名.选择标签*/
article{
color:darkgreen;
}
/*
选择器分组, 符合以下条件的选择器被选择
解读: 多个选择器一起生效
*/
.zu1,.zu2{
color: orange;
}
/*
后代选择器, 符合该结构的页面内容被选择
*/
section span{
color: springgreen;
}
/*
子选择器, 符合该结构的父子关系的被选择
*/
strong > span{
color: lawngreen;
}
/*
符合 属性=值 的标签被选择
解读: code标签中, abc='haha'的被添加样式
*/
code[abc='haha']{
color:mediumspringgreen;
}
</style>
</head>
<body>
<div>
id选择器
<span id="jay">我是周杰伦, 我也是id选择器</span>
</div>
<div>类选择器
<span class="wlh">我是王力宏, 我也是类选择器</span>
<section class="wlh">我是一样也是王力宏, 我也是类选择器</section>
<p class="wlh">我还没那么绿</p>
</div>
<div>标签选择器
<article>我也是article, 我是分组选择器</article>
</div>
<div>选择器分组
<article class="zu1">我是article, 我是标签选择器</article>
<section class="zu2">我是section, 我是标签选择器</section>
</div>
<div>后代选择器
<section><span>一样是吃</span></section>
<span>一样是吃</span>
</div>
<div>子选择器
<strong><span>一样是喝</span></strong>
<strong><i><span>一样是喝</span></i></strong>
</div>
<div>属性选择器
<code abc="haha">代码</code>
<code>代码</code>
<code abc="haha">代码</code>
<code>带带吗</code>
</div>
</body>
</html>
2.3 bs4解析
有了这些基础了. 我们尝试着学学这个叫bs4
的东西. bs4
的逻辑很简单. 直接用标签和属性来选择页面上的标签
bs4是一个第三方模块. 需要单独安装
pip install bs4
2.3.1 通用查询方案
关于bs4
, 本质上我们知道两个东西就好, 一个是find
,另一个是find_all
, 从名字上看. 一个是查找一个
, 另一个是查找所有
.
- find, 在页面中查找一个结果, 找到了就返回
- find_all, 在页面中查找一堆结果. 找完了才返回
这两个功能拥有相同的参数结构. 学一个即可
find(标签, attrs={属性:值})
上个案例试试
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
url = "https://desk.zol.com.cn/pc/"
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.82 Safari/537.36"
}
# 发送请求, 获取页面源代码
resp = requests.get(url, headers=headers)
resp.encoding = 'gbk' # 设置一下字符集
# 1. 创建BeautifulSoup的对象
main_soup = BeautifulSoup(resp.text, "html.parser")
# 2. 找到超链接, 请注意.这里面的属性拼接,多少会和页面稍微有一些细微的差别.
a_list = main_soup.find("ul", attrs={"class": "pic-list2 clearfix"}).find_all("a")
# 3. 循环出每一个超链接
for a in a_list:
# 4.1 拿到href, 也就是子页面的url
href = a.get("href")
# 4.2 获取超链接中的文本信息
content = a.text
print("没啥用,只是给你演示如何获取文本", content)
# 5. 剔除特殊项
if href.endswith(".exe"): # 垃圾客户端. 坑我
continue
# 6. 域名拼接
href = urljoin(url, href)
# 7. 剩下的就是套娃了
child_resp = requests.get(href, headers=headers)
child_resp.encoding = 'gbk'
child_soup = BeautifulSoup(child_resp.text, "html.parser")
# print(child_resp.text) # 适当的打印,可以帮助你调BUG
img = child_soup.find("img", attrs={"id": "bigImg"})
img_src = img.get("src")
# 下载图片
img_resp = requests.get(img_src, headers=headers)
file_name = img_src.split("/")[-1]
with open(file_name, mode="wb") as f:
f.write(img_resp.content)
print("下载完一张图片了")
2.3.2 利用css选择器来获取页面内容
关于选择器. 这里我们讲两个功能
- select_one(选择器) 使用
选择器
获取html文档中的标签, 拿一个 - select(选择器) 使用
选择器
获取html文档中的标签, 拿一堆
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
url = "https://desk.zol.com.cn/pc/"
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.82 Safari/537.36"
}
# 发送请求, 获取页面源代码
resp = requests.get(url, headers=headers)
resp.encoding = 'gbk' # 设置一下字符集
# 1. 创建BeautifulSoup的对象
main_soup = BeautifulSoup(resp.text, "html.parser")
# 2. 找到超链接, 请注意.这里面的属性拼接,多少会和页面稍微有一些细微的差别.
a_list = main_soup.select("ul.pic-list2 a")
# 3. 循环出每一个超链接
for a in a_list:
# 4.1 拿到href, 也就是子页面的url
href = a.get("href")
# 4.2 获取超链接中的文本信息
content = a.text
print("没啥用,只是给你演示如何获取文本", content)
# 5. 剔除特殊项
if href.endswith(".exe"): # 垃圾客户端. 坑我
continue
# 6. 域名拼接
href = urljoin(url, href)
# 7. 剩下的就是套娃了
child_resp = requests.get(href, headers=headers)
child_resp.encoding = 'gbk'
child_soup = BeautifulSoup(child_resp.text, "html.parser")
# print(child_resp.text) # 适当的打印,可以帮助你调BUG
img = child_soup.select_one("#bigImg")
img_src = img.get("src")
# 下载图片
img_resp = requests.get(img_src, headers=headers)
file_name = img_src.split("/")[-1]
with open(file_name, mode="wb") as f:
f.write(img_resp.content)
print("下载完一张图片了")
三. xpath解析
xpath
是一种非常简单好用的页面提取方案.
3.1 xpath语法
我们给出一段测试用的HTML代码.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<div>
<p>一个很厉害的人</p>
<ol>
<li id="10086">周大强</li>
<li id="10010">周芷若</li>
<li class="joy">周杰伦</li>
<li class="jolin">蔡依林</li>
<ol>
<li>阿信</li>
<li>信</li>
<li>信不信</li>
</ol>
</ol>
</div>
<hr />
<ul>
<li><a href="http://www.baidu.com">百度</a></li>
<li><a href="http://www.google.com">谷歌</a></li>
<li><a href="http://www.sogou.com">搜狗</a></li>
</ul>
<ol>
<li><a href="feiji">飞机</a></li>
<li><a href="dapao">大炮</a></li>
<li><a href="huoche">火车</a></li>
</ol>
<div class="job">李嘉诚</div>
<div class="common">胡辣汤</div>
</body>
</html>
from lxml import etree
# 加载HTML
f = open("xpath测试.html", mode='r', encoding='utf-8')
page_source = f.read()
# 和bs4一样, 把HTML交给etree
hm = etree.HTML(page_source) # type:etree._Element
# xpath各种用法
# 节点: 每个HTML标签叫节点
# 最外层节点: 根节点
# 内层节点: 子节点
# 父子节点: <爹><子></子></爹>
html = hm.xpath("/html") # /根
print(html) # 看一眼标签名(测试用, 不用记住)
body = hm.xpath("/html/body") # 第二个/表示子节点
print(body)
# 接下来这句话请记住, xpath提取到的内容不论多少, 都会返回列表
p = hm.xpath("/html/body/div/p/text()") # 想要p里面的文本
print(p) # 列表
print(p[0]) # 要么取0.
print("".join(p)) # 要么用join()进行合并.
# 如果页面结构非常复杂. 这样一层一层数下来. 过年了
# xpath我们还可以用相对定位
p = hm.xpath("//p/text()") # // 表示在页面任意位置找
print(p) # 依然有效
# 我想找到 `周大强`, `周芷若`,`周杰伦`,`周大强`,`蔡依林`
li = hm.xpath("//div/ol/li/text()")
print(li)
# 我想找到 `一个很厉害的人`后面ol中所有的文本
li = hm.xpath("//div/ol//text()") # 第二个//表示所有
# 请注意. 这里多了很多空白,一般我们提取一篇文章的时候,会用这种.
# 结合字符串各种操作. 这些东西应该难不倒各位.
print(li)
print("".join(li).replace(" ", "").replace("\n", ""))
from lxml import etree
# 加载HTML
f = open("xpath测试.html", mode='r', encoding='utf-8')
page_source = f.read()
# 和bs4一样, 把HTML交给etree
hm = etree.HTML(page_source) # type:etree._Element
# 重点:根据位置数元素
# 我想要`周芷若`, 分析角度: 它是ol里第二个li
li = hm.xpath("//ol/li[2]/text()")
print(li) # 这里莫名其妙带出了`信`, 请思考为什么? 请思考怎么干掉`信`
# 我想单独找`信`聊聊
li = hm.xpath("//ol/ol/li[2]/text()")
print(li)
# 重点:根据属性筛选元素
# 我想要id=10086的li标签中的内容0
li = hm.xpath("//li[@id='10086']/text()")
print(li)
# 我想要class是joy的内容
li = hm.xpath("//*[@class='joy']/text()")
print(li)
# 我想要有class的li的内容
li = hm.xpath("//*[@class]/text()")
print(li)
# 我想和`啊信`,`信`,`信不信`单独聊聊
li_list = hm.xpath("//ol/ol/li")
for li in li_list:
print(li.xpath("./text()")) # ./表示当前节点, 也就是li
# 提取`百度`, 谷歌, 搜狗的超链接href属性
li_list = hm.xpath("//ul/li")
for li in li_list:
print(li.xpath("./a/text()")) # ./表示当前节点, 也就是li
print(li.xpath("./a/@href")) # @href 表示提取属性
# 我想要`火车`
li = hm.xpath("//body/ol/li[last()]/a/text()") # last() 拿到最后一个
print(li)
f.close()
3.2 xpath实战案例(重点)
我们选择一个网站. 尝试着进行数据提取
http://www.boxofficecn.com/boxoffice2019 抓取2019年的电影票房数据.
import requests
from lxml import etree
url = "http://www.boxofficecn.com/boxoffice2019"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.82 Safari/537.36"
}
resp = requests.get(url, headers=headers)
tree = etree.HTML(resp.text) # 加载页面源代码
# 提取数据
# 这个xpath不是随便写的. 你要找到你的`单条数据`的边界
trs = tree.xpath("//table/tbody/tr")[1:-1]
for tr in trs:
num = tr.xpath("./td[1]/text()") # 编号
year = tr.xpath("./td[2]//text()") # 年份
name = tr.xpath("./td[3]//text()") # 名称
money = tr.xpath("./td[4]/text()") # 票房
print(num, year, name, money)