共计 12940 个字符,预计需要花费 33 分钟才能阅读完成。
概念
爬虫(spider,⼜叫网络爬虫),是指向⽹站 / 网络发起请求,获取资源后分析并提取有用数据的程序。
通过程序模拟浏览器请求站点的行为,把站点返回的 HTML 代码 /JSON 数据 / ⼆进制数据(图片、
视频)爬到本地,进而提取自己需要的数据,存放起来使用。
步骤
- 发送请求
- 请求方式:GET、POST
- 请求 URL
- 请求头:User-Agent、Host、Cookies 等
- 获取数据
- 响应状态
- 响应头
- 响应体:要获取的数据
- 解析数据
- 正则表达式
- lxml
- BeautifulSoup
- 存储数据
- 文本
- 数据库
- 二进制文件
安装常用包
requests 包、bs4 包和 lxml 包
cmd 执行
conda info -e
pip list
conda install requests
conda install lxml
conda install bs4
数据提取
1. 响应内容的分类
在发送请求获取响应之后,可能存在多种不同类型的响应内容;而且很多时候,我们只需要响应内容中的一部分数据
2. 认识 xml 以及和 html 的区别
要搞清楚 html 和 xml 的区别,首先需要我们来认识 xml
2.1 认识 xml
xml 是一种可扩展标记语言,样子和 html 很像,功能更专注于对传输和存储数据
bookstore>
book category="COOKING">
title lang="en">Everyday Italiantitle>
author>Giada De Laurentiisauthor>
year>2005year>
price>30.00price>
book>
book category="CHILDREN">
title lang="en">Harry Pottertitle>
author>J K. Rowlingauthor>
year>2005year>
price>29.99price>
book>
book category="WEB">
title lang="en">Learning XMLtitle>
author>Erik T. Rayauthor>
year>2003year>
price>39.95price>
book>
bookstore>
上面的 xml 内容可以表示为下面的树结构:
2.2 xml 和 html 的区别
- html:
- 超文本标记语言
- 为了更好的显示数据,侧重点是为了显示
- xml:
- 可扩展标记语言
- 为了传输和存储数据,侧重点是在于数据内容本身
2.3 常用数据解析方法
JSON 模块
JSON 简介
JSON 是⼀种存储和交换数据的语法
JSON 仅仅是⽂本,它能够轻松地在服务器浏览器之间传输
JSON 的数据格式其实就是 python 里面的字典格式
JSON 语法
格式 1:JSON 对象
{"name": "小周", "sex": 女}
格式 2:JSON 数组
{
"student":
[
{"name": "小潇", "sex": "男"},
{"name": "小周", "sex": "女"},
{"name": "小小", "sex": "男"}
]
}
方法
import json
json.dumps()
将 python 数据类型转换为 json 格式的字符串
d = {'a':1, 'b':2}
print(d)
print(type(d))
json_d = json.dumps(d)
print(json_d)
print(type(json_d))
with open('data.json', 'w', encoding='utf-8') as f:
f.write(json_d)
{'a': 1, 'b': 2}
{"a": 1, "b": 2}
json.dump()
将 python 数据类型转换并保存到 json 格式文件内
- sort_keys:是否排序 indent:定义缩进距离
- separators:是一个元组,定义分隔符的类型
- skipkeys:是否允许 json 字串编码字典对象时 字典的 key 不是字符串类型
d = {'a':1,'b':2}
json.dump(d,open('data.json','w'),sort_keys=True,indent=4,separators=(',',':'))
json.loads()
将 json 格式的字符串转换为 python 的类型
d = {'a':1, 'b':2}
print(d)
print(type(d))
json_d = json.dumps(d)
print(json_d)
print(type(json_d))
dic_d = json.loads(json_d)
print(dic_d)
print(type(dic_d))
f = open('data.json', encoding='utf-8')
content = f.read()
python_obj = json.loads(content)
print(python_obj)
print(type(python_obj))
{'a': 1, 'b': 2}
{"a": 1, "b": 2}
{'a': 1, 'b': 2}
json.load()
从 json 格式的文件中读取数据并转换为 python 的类型
python_obj = json.load(open('data.json','r'))
print(python_obj)
print(type(python_obj))
jsonpath 模块
jsonpath 模块的使用场景
如果有一个多层嵌套的复杂字典,想要根据 key 和下标来批量提取 value,这是比较困难的,jsonpath 模块就能解决这个痛点。
jsonpath 可以按照 key 对 python 字典进行批量数据提取
jsonpath 模块的安装
pip install jsonpath -i https://pypi.tuna.tsinghua.edu.cn/simple
jsonpath 模块使用
from jsonpath import jsonpath
jsonpath 语法规则
jsonpath 使用示例
book_dict = {
"store": {
"book": [
{ "category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{ "category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{ "category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{ "category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}
from jsonpath import jsonpath
print(jsonpath(book_dict, '$..author')) # 如果取不到将返回 False # 返回列表,如果取不到将返回 False
jsonpath 练习
我们以拉勾网城市 JSON 文件 http://www.lagou.com/lbs/getAllCitySearchLabels.json 为例,获取所有城市的名字的列表,并写入文件。
参考代码:
import requests
import jsonpath
import json
url = 'http://www.lagou.com/lbs/getAllCitySearchLabels.json'
headers = {"User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"}
response =requests.get(url, headers=headers)
html_str = response.content.decode()
jsonobj = json.loads(html_str)
citylist = jsonpath.jsonpath(jsonobj,'$..name')
with open('c','w') as f:
content = json.dumps(citylist, ensure_ascii=False)
f.write(content)
输出结果:city_name.txt
["安阳", "安庆", "安康", "鞍山", "安顺", "澳门", "阿拉善盟", "阿坝藏族羌族自治州", "阿拉尔", "北京", "保定", "包头", "滨州", "蚌埠", "宝鸡", "北海", "亳州", "毕节", "百色", "保山", "巴音郭楞", "巴中", "本溪", "白银", "巴彦淖尔", "白城", "白山", "北屯", "成都", "长沙", "重庆", "长春", "常州", "沧州", "郴州", "赤峰", "承德", "常德", "潮州", "滁州", "朝阳", "楚雄", "澄迈", "池州", "昌吉", "崇左", "昌都", "东莞", "大连", "德州", "德阳", "大理", "东营", "大同", "达州", "大庆", "丹东", "德宏", "儋州", "定西", "大兴安岭", "鄂尔多斯", "鄂州", "恩施", "佛山", "福州", "阜阳", "抚州", "阜新", "抚顺", "防城港", "广州", "贵阳", "赣州", "桂林", "广安", "贵港", "甘孜藏族自治州", "广元", "甘南", "杭州", "合肥", "惠州", "哈尔滨", "海口", "呼和浩特", "湖州", "邯郸", "菏泽", "衡水", "淮安", "海外", "衡阳", "怀化", "黄石", "黄冈", "淮南", "淮北", "河源", "鹤壁", "汉中", "黑河", "河池", "呼伦贝尔", "红河", "黄山", "葫芦岛", "哈密", "海东", "贺州", "黄南", "济南", "金华", "嘉兴", "江门", "济宁", "揭阳", "九江", "晋中", "荆州", "焦作", "锦州", "景德镇", "吉林", "佳木斯", "吉安", "晋城", "荆门", "金昌", "济源", "酒泉", "嘉峪关", "昆明", "开封", "喀什", "克拉玛依", "廊坊", "兰州", "洛阳", "临沂", "聊城", "柳州", "连云港", "乐山", "六安", "丽水", "临汾", "泸州", "漯河", "龙岩", "吕梁", "丽江", "凉山彝族自治州", "拉萨", "六盘水", "娄底", "辽阳", "临沧", "陵水黎族自治县", "辽源", "临夏", "林芝", "来宾", "陇南", "绵阳", "眉山", "茂名", "梅州", "马鞍山", "牡丹江", "南京", "宁波", "南昌", "南宁", "南通", "南阳", "宁德", "南充", "内江", "南平", "莆田", "盘锦", "濮阳", "平顶山", "萍乡", "普洱", "攀枝花", "平凉", "青岛", "泉州", "清远", "秦皇岛", "曲靖", "衢州", "齐齐哈尔", "钦州", "琼海", "庆阳", "七台河", "黔南", "黔西南", "黔东南", "日照", "日喀则", "上海", "深圳", "苏州", "沈阳", "石家庄", "汕头", "绍兴", "宿迁", "商丘", "三亚", "上饶", "韶关", "宿州", "十堰", "汕尾", "遂宁", "邵阳", "绥化", "随州", "三门峡", "三明", "四平", "松原", "朔州", "石嘴山", "石河子", "商洛", "山南", "神农架林区", "天津", "太原", "台州", "唐山", "泰州", "泰安", "通辽", "铜仁", "通化", "铜陵", "铁岭", "天水", "台湾", "铜川", "天门", "铁门关", "武汉", "无锡", "温州", "潍坊", "芜湖", "乌鲁木齐", "威海", "渭南", "梧州", "乌兰察布", "武威", "文山", "万宁", "文昌", "乌海", "五家渠", "五指山", "西安", "厦门", "徐州", "新乡", "襄阳", "邢台", "咸阳", "香港", "许昌", "西宁", "孝感", "信阳", "新余", "湘潭", "咸宁", "宣城", "西双版纳", "仙桃", "忻州", "湘西土家族苗族自治州", "兴安盟", "锡林郭勒盟", "烟台", "扬州", "银川", "盐城", "宜宾", "宜昌", "阳江", "玉林", "岳阳", "宜春", "运城", "益阳", "营口", "榆林", "玉溪", "雅安", "云浮", "永州", "阳泉", "鹰潭", "延边", "伊犁", "伊春", "延安", "郑州", "珠海", "中山", "淄博", "株洲", "漳州", "湛江", "肇庆", "镇江", "遵义", "周口", "枣庄", "驻马店", "张家口", "长治", "舟山", "张掖", "资阳", "昭通", "自贡", "张家界", "中卫"]
http 协议复习
http 以及 https 的概念和区别
HTTPS 比 HTTP 更安全,但是性能更低
- HTTP:超文本传输协议,默认端口号是 80
- 超文本:是指超过文本,不仅限于文本;还包括图片、音频、视频等文件
- 传输协议:是指使用共用约定的固定格式来传递转换成字符串的超文本内容
- HTTPS:HTTP + SSL(安全套接字层),即带有安全套接字层的超本文传输协,默认端口号:443
- SSL 对传输的内容(超文本,也就是请求体或响应体)进行加密
- 可以打开浏览器访问一个 url,右键检查,点击 net work,点选一个 url,查看 http 协议的形式
爬虫特别关注的请求头和响应头
特别关注的请求头字段
http 请求的形式如上图所示,爬虫特别关注以下几个请求头字段
- Content-Type
- Host (主机和端口号)
- Connection (链接类型)
- Upgrade-Insecure-Requests (升级为 HTTPS 请求)
- User-Agent (浏览器名称)
- Referer (页面跳转处)
- Cookie (Cookie)
- Authorization(用于表示 HTTP 协议中需要认证资源的认证信息,如前边 web 课程中用于 jwt 认证)
加粗的请求头为常用请求头,在服务器被用来进行爬虫识别的频率最高,相较于其余的请求头更为重要,但是这里需要注意的是并不意味这其余的不重要,因为有的网站的运维或者开发人员可能剑走偏锋,会使用一些比较不常见的请求头来进行爬虫的甄别
特别关注的响应头字段
http 响应的形式如上图所示,爬虫只关注一个响应头字段
- Set-Cookie(对方服务器设置 cookie 到用户浏览器的缓存)
常见的响应状态码
- 200:成功
- 302:跳转,新的 url 在响应的 Location 头中给出
- 303:浏览器对于 POST 的响应进行重定向至新的 url
- 307:浏览器对于 GET 的响应重定向至新的 url
- 403:资源不可用;服务器理解客户的请求,但拒绝处理它(没有权限)
- 404:找不到该页面
- 500:服务器内部错误
- 503:服务器由于维护或者负载过重未能应答,在响应中可能可能会携带 Retry-After 响应头;有可能是因为爬虫频繁访问 url,使服务器忽视爬虫的请求,最终返回 503 响应状态码
服务器给我的相关反馈,我们在学习的时候就被教育说应该将真实情况反馈给客户端,但是在爬虫中,可能该站点的开发人员或者运维人员为了阻止数据被爬虫轻易获取,可能在状态码上做手脚,也就是说返回的状态码并不一定就是真实情况,比如: 服务器已经识别出你是爬虫,但是为了让你疏忽大意,所以照样返回状态码 200,但是响应体重并没有数据。
所有的状态码都不可信,一切以是否从抓包得到的响应中获取到数据为准
浏览器的运行过程
http 请求的过程
- 浏览器在拿到域名对应的 ip 后,先向地址栏中的 url 发起请求,并获取响应
- 在返回的响应内容(html)中,会带有 css、js、图片等 url 地址,以及 ajax 代码,浏览器按照响应内容中的顺序依次发送其他的请求,并获取相应的响应
- 浏览器每获取一个响应就对展示出的结果进行添加(加载),js,css 等内容会修改页面的内容,js 也可以重新发送请求,获取响应
- 从获取第一个响应并在浏览器中展示,直到最终获取全部响应,并在展示的结果中添加内容或修改————这个过程叫做浏览器的 渲染
注意:
但是在爬虫中,爬虫只会请求 url 地址,对应的拿到 url 地址对应的响应(该响应的内容可以是 html,css,js,图片等)
浏览器渲染出来的页面和爬虫请求的页面很多时候并不一样,是因为爬虫不具备渲染的能力(当然后续课程中我们会借助其它工具或包来帮助爬虫对响应内容进行渲染)
- 浏览器最终展示的结果是由多个 url 地址分别发送的多次请求对应的多次响应共同渲染的结果
- 所以在爬虫中,需要以发送请求的一个 url 地址对应的响应为准来进行数据的提取
requests 包
import requests
常用方法
方法 | 描述 |
---|---|
requests.request(url) | 构造一个请求,支持以下各种方法 |
requests.get() | 发送一个 Get 请求 |
requests.post() | 发送一个 Post 请求 |
requests.head() | 获取 HTML 的头部信息 |
requests.put() | 发送 Put 请求 |
requests.patch() | 提交局部修改的请求 |
requests.delete() | 提交删除请求 |
常用属性或方法
属性或方法 | 描述 |
---|---|
.status_code | 响应状态码 |
.content | 返回⼆进制结果 |
.text | 返回编码解析的结果 |
.encoding | 定义编码 |
.cookie | 获取请求后的 cookie |
.url | 获取请求网址 |
.json() | 返回字典,可能抛出异常 |
.raw | 返回原始 socket response,需要加参数 stream=True |
content 和 text 区别
content 中间存的是字节码,而 text 中存的是 Beautifulsoup 模块根据猜测的编码方式将 content 内容编码成字符串后的结果。
直接输出 content,会发现前面存在 b’这样的标志,这是字节字符串的标志,而 text 是没有前面的 b, 对于纯 ascii 码,这两个可以说一模一样,对于其他的文字,需要正确编码才能正常显示。
有中文的时候,直接使用 text 会显示乱码,所以需要使用 content 对象来手动进行解码后才能正常显示。
响应状态码
分类 | 分类描述 |
---|---|
1** | 信息,服务器收到请求,需要请求者继续执⾏操作 |
2** | 成功,操作被成功接收并处理 |
3** | 重定向,需要进⼀步的操作以完成请求 |
4** | 客户端错误,请求包含语法错误或⽆法完成请求 |
5** | 服务器错误,服务器在处理请求的过程中发⽣了错误 |
GET 请求
requests.get()
不带参数的请求
res = requests.get("http://www.baidu.com")
print(r.status_code)
print(r.text)
print(r.content)
print(r.json())
带参数的请求
res = requests.get("http://httpbin.org/getname=gemey&age=22")
print(res.text)
params={
"name":"tony",
"age":20
}
res = requests.get("http://httpbin.org/get",params=params)
print(res.text)
headers = {
"User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
"referer": "https://www.abidu.com",
"Cookies": "1234565678"
}
res = requests.get("http://httpbin.org/get",headers = headers)
print(res.text)
POST 请求
requests.post():POST 请求一般用于提交参数,所以直接进行有参数的 POST 请求测试。
import requests
params = {
"name": "tony",
"age": 20
}
url = 'http://httpbin.org/post'
headers = {
"User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
"referer": "https://www.abidu.com",
"Cookies": "1234565678"
}
res = requests.post(url = url,params = params,headers = headers)
print(res.text)
BeautifulSoup 包
Beautiful Soup 是一个用于从 HTML 和 XML 文件中提取数据的 Python 模块。
from bs4 import BeautifulSoup
import lxml
解释器
soup=beautifulsoup(解析内容 , 解析器)
解析器 | 使用方法 | 优势 | 劣势 | |
---|---|---|---|---|
Python 标准库 | BeautifulSoup(markup,"html.parser") |
Python 的内置标准库、执行速度适中、文档容错能力强 | Python 2.7.3 or 3.2.2)前 的版本中文档容错能力差 | |
lxml HTML 解析器 | BeautifulSoup(markup,“lxml”) |
速度快、文档容错能力强 | 需要安装 C 语言库 | |
lxml XML 解析器 | BeautifulSoup(markup, ["lxml-xml"]) 、BeautifulSoup(markup, "xml") |
速度快、唯一支持 XML 的解析器 | 需要安装 C 语言库 | |
html5lib | BeautifulSoup(markup, "html5lib") |
最好的容错性、以浏览器的方式解析文档、生成 HTML5 格式的文档 | 速度慢、不依赖外部扩展 |
用法
soup.title
soup.title.name
soup.title.parent.name
soup.p
soup.p['class']
soup.p.get('class')
soup.a
soup.find_all('a')
soup.find(id="link3")
soup.a['class'] = "newClass"
del bs.a['class']
soup.find('a').get('id')
soup.title.string
1、获取拥有指定属性的标签
方法一:获取单个属性
soup.find_all('div',id="even")
soup.find_all('div',attrs={'id':"even"})
方法二:
soup.find_all('div',id="even",class_="square")
soup.find_all('div',attrs={"id":"even","class":"square"})
2、获取标签的属性值
for link in soup.find_all('a'):
print(link['href']) //等同于 print(link.get('href'))
for link in soup.find_all('a'):
print(link.attrs['href'])
3、获取标签中的内容
divs = soup.find_all('div')
for div in divs:
a = div.find_all('a')[0]
print(a.string)
如果结果没有正确显示,可以转换为 list 列表
4、stripped_strings
divs = soup.find_all('div')
for div in divs:
infos = list(div.stripped_strings)
bring(infos)
5. 输出
soup = BeautifulSoup(markup)
print(soup.prettify())
soup.get_text()
soup.i.get_text()
案例:爬取豆瓣电影 Top250
import requests
from bs4 import BeautifulSoup
class Douban:
def __init__(self):
self.URL = 'https://movie.douban.com/top250'
self.start_num = []
for start_num in range(0, 251, 25):
self.start_num.append(start_num)
self.header = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
def get_top250(self):
for start in self.start_num:
start = str(start)
html = requests.get(self.URL, params={'start':start}, headers=self.header)
soup = BeautifulSoup(html.text, 'lxml')
names = soup.select('#content > div > div.article > ol > li > div > div.info > div.hd > a > span:nth-child(1)')
for name in names:
print(name.get_text())
if __name__ == "__main__":
cls = Douban()
cls.get_top250()
lxml 包
from lxml import etree
初始化
text = '''这是测试的 div
这是嵌套的 div 标签
这是嵌套的 p 标签
这是测试的 p
'''
html = etree.HTML(text)
result = etree.tostring(html)
语法
1. 寻找节点
语法 | 含义 |
---|---|
nodename(节点名字) | 直接根据写的节点名字查找节点, 如:div |
// | 在当前节点下的子孙节点中寻找, 如://div |
/ | 在当前节点下的子节点中寻找, 如:/div |
. | 代表当前节点(可省略不写,就像我们有时候写的相对路径),如:./div |
… | 当前节点的父节点,如:…/div |
result = html.xpath('//div/text()')
print(result)
result = html.xpath('/html/body/div/text()')
print(result)
2. 属性筛选
方法名符号 | 作用 |
---|---|
@ | 获取属性或者筛选属性, 如:@class |
contains | 判断属性中是否含有某个值(用于多值判断),如:contains(@class,‘hello’) |
hello_tag = html.xpath('//div[@class="hello"]')
print(hello_tag)
hello_tags = html.xpath('//div[contains(@class,"hello")]')
print(hello_tags)
3. 按序选择
方法 | 作用 |
---|---|
last() | 获取最后一个标签 |
1 | 获取第一个标签 |
position() num | 筛选多个标签 |
from lxml import etree
text = '''
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
'''
html = etree.HTML(text)
first_tag = html.xpath('//li[1]')
print(first_tag)
last_tag = html.xpath('//li[last()]')
print(last_tag)
li_tags = html.xpath('//li[position() )
print(li_tags)
tags = html.xpath('//li[position() = 1 or position() = 2]')
print(tags)
4. 获取属性和文本
方法 | 作用 |
---|---|
@ | 获取属性或者筛选属性 |
text() | 获取文本 |
div_class = html.xpath('//div/@class')
print(div_class)
content = html.xpath('//div/p/text()')
print(content)
content_two = html.xpath('//div[position() = 2]/text()')
print(content_two)
content_three = html.xpath('//div[position() = 2]//text()')
print(content_three)
5. 节点修饰语法
路径表达式 | 结果 |
---|---|
//title[@lang=“eng”] | 选择 lang 属性值为 eng 的所有 title 元素 |
/bookstore/book[1] | 选取属于 bookstore 子元素的第一个 book 元素。 |
/bookstore/book[last()] | 选取属于 bookstore 子元素的最后一个 book 元素。 |
/bookstore/book[last()-1] | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
/bookstore/book[position()>1] | 选择 bookstore 下面的 book 元素,从第二个开始选择 |
//book/title[text()=‘Harry Potter’] | 选择所有 book 下的 title 元素,仅仅选择文本为 Harry Potter 的 title 元素 |
/bookstore/book[price>35.00]/title | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
关于 xpath 的下标
- 在 xpath 中,第一个元素的位置是 1
- 最后一个元素的位置是 last()
- 倒数第二个是 last()-1
爬虫实战——静态网页
爬取某配乐网站 mp3 音效
import os.path
import random
import time
import requests
import lxml.etree
page_n = int(input("请输入你想要爬取的网页数量"))
for i in range(page_n):
url = f'https://www.tuke88.com/yinxiao/zonghe_0_{i}.html'
response = requests.get(url)
html_parser = lxml.etree.HTMLParser()
html = lxml.etree.fromstring(response.text,parser=html_parser)
titles = html.xpath("//div[@class='lmt']//div[@class='audio-list']//a[@class='title']/text()")
mp3_urls = html.xpath("//div[@class='lmt']//div[@class='audio-list']//source/@src")
if not os.path.exists('pymp3'):
os.mkdir('pymp3')
for title,mp3_url in zip(titles,mp3_urls):
mp3_stream = requests.get(mp3_url,stream=True)
with open(os.path.join('pymp3',title+".mp3"),'wb+') as writer:
writer.write(mp3_stream.raw.read())
print(f'【INFO】{title}.mp3 下载成功')
time.sleep(random.uniform(0.1,0.4))
原文地址: 【python】爬虫基础——JSON、requests、BeautifulSoup、lxml、爬取静态网页