python爬虫基础

爬虫合法性

1
2
3
4
5
6
爬虫只是一个便捷且低成本的获取数据的方式
只是一门技术,只要在使用时确保不触碰一些红线,就不会有问题
1. 公民个人信息
2. 非公开数据
3. 大批量访问,干扰对方正常运营
4. 抢票、抢专家号等侵占公共资源的行为

HTTP 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
概念:
访问网址:当我们在浏览器网址栏输入一串网址后回车访问一个网站的行为就是在发送一个 HTTP 请求,此时我们的浏览器就是客户端,网站的服务器就是服务端
客户端:享受服务的一方
服务端:提供服务的一方
发送请求:就是发送消息的意思,客户端发送消息给服务端,告诉服务端我想要什么,服务端再把我想要的东西发给我,这就是一次 HTTP 请求
HTTP 请求:指的是消息发送的形式,以什么样的方式发送消息,消息用什么样的格式,计算机只能进行重复的、机械性的行为,所以我们发送消息时必须规定消息的格式
不要问这其中是如何实现的,就像你通过微信发消息给别人时不会追究消息是怎样发送过去的,这不是我们的重点

HTTP 请求格式:
分为请求和响应两个部分
请求代表客户端发给服务端的消息
响应代表服务端发给客户端的消息
HTTP 请求一定是一次请求一次响应,我们告诉服务端想要什么,服务端再把想要的东西给我们,服务端无法主动向客户端发送消息,也无法对一次请求进行多次响应

可能的疑问
老师你说服务端不会一次性将我们想要的所有东西发送给我们,而是分为多次发送。
又说一次请求一次响应,服务端无法对一次请求进行多次响应
这不是互相矛盾吗?
这其中真实发生的是:
1. 我们访问一个网址,向服务端发送一个 HTTP 请求
2. 浏览器得到服务端的响应
3. 服务端会在这个响应的内容中,告诉浏览器,你还需要请求哪些资源,和这些资源的网址
4. 浏览器自动向这些网址发送 HTTP 请求,并将得到的响应展示在页面中,这是自动发生的

请求部分包含
1. 网址
2. 请求头
用来放置一些服务端可能会用到的内容,这些内容通常是固定的,类似 Python 字典中的键值对
user-agent:用于标识请求来自什么客户端
cookie:用于传输用户的登陆状态,由服务端自定义,浏览器每次请求都会自动携带
referer:请求来自哪个 url
content-type:指定请求体的格式
3. 请求参数
网址中可能会有一个问号(英文),问号后的东西叫请求参数
部分请求会包含请求参数,不是必须要有
用来放置一些我们需要告诉服务端的信息,这些内容是服务端自定义的,格式为 key=value
多个 key=value 用 & 符号分隔
例如我们使用豆瓣图书中的搜索功能,我们输入的搜索关键字就会被包含在请求参数中
4. 请求方式
用于告诉服务端这次请求的类型
常用的只有两种,GET 和 POST
不需要过于纠结请求方式是什么意思,只是存在这样一个区分
只需要知道 GET 请求没有请求体,POST 请求拥有请求体即可,请求体是什么我们后续再聊
5. 请求体
用来放置一些我们需要告诉服务端的信息
和请求参数的区别是请求体里面的东西不会放在url里面

响应部分包含
1. 状态码
用于告知请求是否成功,通常 200 代表成功,404 代表资源不存在
一般2开头代表成功,4开头代表失败,3开头代表重定向
这个并不重要,想知道某个状态码的含义只需要搜索一下
2. 响应头
很少很少会用到
3. 响应体
真正的响应内容,一般分为两种格式
HTML 和 JSON

网页开发者工具使用

1
2
3
4
5
6
7
8
9
浏览器右上角三个点 -> 更多工具 -> 开发者工具,或者按下 F12 进入
选择网络标签,这里可以看到浏览器和服务端发送的所有 HTTP 请求

为什么能看到非常多的请求?
因为绝大多数情况下,服务端不会一次性将我们想要的所有东西发送给我们,而是分为多次发送。
这样能够提高效率,为什么能提高效率?这不重要。

如果选择网络标签并刷新页面后,看不到任何 HTTP 请求,可能是你误触打开了筛选条件
点击任意一个 HTTP 请求,即可看到请求中包含的内容

requests 库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
使用 requests 第三方库可以让我们使用 Python 发送 HTTP 请求
安装: pip install requests
或者: pip3 install requests
如果安装慢: 更换清华源: pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

发送 GET 请求
response = requests.get(url=网址, headers=字典格式请求头, params=字典格式请求参数, cookies=字典格式cookie, proxies=字典格式代理, timeout=响应超时时间)
发送 POST 请求
response = requests.post(url=网址, data=字典格式请求体)

proxies 格式
{ "http": "http://使用http协议时使用的代理ip", "https": "http://使用https协议时使用的代理ip" }

得到响应状态码
response.status_code

得到响应体
response.text,得到文本类型
response.content,得到二进制类型
response.json(),将json格式响应转为字典

得到响应头
response.headers
得到响应 cookie
response.cookies,可迭代对象,Cookie 类的实例对象
name 属性得到 cookie 键
value 属性得到 cookie 值

伪装请求头
如果没有指定 headers 参数,requests 库会自动为你添加 user-agent 的请求头
使用 requests 库发送请求时请求头的 user-agent 字段值和浏览器的值不一样,可能被服务端拒绝访问
发送请求时手动指定 headers 参数进行伪装

params 参数
用于指定请求参数,你当然可以直接将请求参数写在 url 当中
但使用 params 参数看起来会更好看,也更加规范

网页前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
我们看到的网页由三部分组成
HTML + CSS + JavaScript
在基础阶段,主要接触 HTML
HTML 并不是编程语言,是标记语言
使用标签描述页面元素,也就是描述在屏幕的哪个地方放一个什么东西,标签可以嵌套

在开发者工具的元素标签中,可以查看网站的 HTML 代码

格式:
开头的 <!DOCTYPE html> 用于告诉浏览器这是个 HTML 格式的文件
<标签名称>标签内容</标签名称>
<html>代表根元素,所有标签都要写在这里面</html>
<head>用于存放一些信息,比如网页的标题,字符编码等</head>
<body>在这里面编写可见的页面元素</body>

我们不需要去写 HTML,只需要认识几个常用的标签类型即可
<p>用于展示文本</p>
<br> 用于换行,相当于我们字符串当中的 \n,并且该标签只有一个起始标签,没有闭合标签
<img src="图片链接"> 用于展示图片
<a href="要跳转的链接">用于点击后跳转至对应链接</a>
<div>容器标签,用于划分一块区域,再将需要展示的东西写在这个区域中</div>
<span>容器标签,和 div 一样,他们的区别不是我们的重点</span>

常用标签属性
所有标签都可以具有 class 属性和 id 属性,用来帮助我们找到这个标签
格式:
<div class="value1 value2">一个标签可以有多个 class 属性值,用空格分隔</div>
<div id="hd"></div>
区别:可以有多个标签具有同一个 class 属性,但一个 id 属性只能对应一个标签

bs4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
安装:pip install bs4 lxml
导入:from bs4 import BeautifulSoup

创建 BeautifulSoup 对象
BeautifulSoup(response.text, "lxml")

标签方法:
find(name=根据标签名查找, attrs={"根据属性名": "和属性值查找"})
查找第一个匹配的标签
find_all
查找所有匹配的标签
find_next
得到下一个兄弟节点

标签属性:
children,获得子标签,还有获得父标签、兄弟标签等,需要用的时候查一下即可
attrs,获得标签属性,或者直接中括号取标签属性
text,获得标签内的文本,包括子标签内的文本

xpath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
安装:pip install lxml
导入:from lxml import etree

实例化 HTML 对象
etree.HTML(response.text)

方法:
xpath(xpath语法字符串)
返回一个标签对象,返回得到的对象可以继续使用 xpath 方法

语法:
/ 代表根节点
/body/div 代表根节点下的body标签的子标签中的div标签
//div 代表根节点下的所有子孙标签中的div标签
//div[@class="hd"] 定位class属性为hd的div标签
//div[@id="hd"] 定位id属性为hd的div标签
//img/@src 得到img标签的src属性
//div[1] 定位第一个div标签,索引从1开始
//div/text() 得到div标签下的子节点中的字符
//div//text() 得到div标签下的子孙节点中的字符
//div/* 得到所有子元素
. 代表当前节点
.. 代表父节点
* 代表任意节点
//div[text()="HD"] 得到标签内容为 HD 的div标签

正则表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
一种语法,用途广泛,基本上所有编程语言通用
对于数据解析,是最麻烦的一种手段,没有办法了才用这个,但我们还是需要会这个,在其他的很多七七八八的事情上都会用到它
匹配一个文本当中符合要求的字符
. 任意字符
^ 开头
$ 结束
\ 转义
* 匹配前一个字符0或n次
+ 匹配前一个字符至少1次
? 含义1:匹配前一个字符0或1次
含义2:非贪婪匹配,匹配尽可能短的字符
{n} 前一个字符重复n次
{n,} 前一个字符重复n或更多次
{n,m} 前一个字符重复n到m次
() 括号内的字符进行分组
a|b 匹配字符a或b
\d 匹配数字
\s 匹配空白字符
\w 匹配字母、数字、下划线
.* 匹配任意字符
.*? 匹配任意字符,非贪婪匹配
[123] 将括号中的所有字符当做一个字符

导入模块:
import re
返回匹配的第一个字符:
re.search(pattern=正则表达式, string=被匹配的字符)
返回匹配的所有字符:
re.findall(pattern=正则表达式, string=被匹配的字符)
替换匹配的所有字符:
re.sub(pattern=正则表达式, string=被匹配的字符, repl=要替换的字符)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HTTP 请求是无状态的
每次请求都是独立的,服务端无法区分某个请求来自哪一个用户。(因为无论是谁发送的请求,样子都一模一样)

Cookie 用于解决这一问题
当你进行登陆成功时,服务端会返回一串独一无二的字符放在 Cookie 中
后续当你发送 HTTP 请求时,浏览器会自动携带 Cookie,这样不同的人发送的请求就会有不同的 Cookie,服务端就能够区分请求来自哪个用户
Cookie 的格式为 key=value; key=value
Cookie 是服务端返回的,内容也是服务端完全自定义的
浏览器会持久的保存你的 cookie

模拟登陆
得到服务端返回到cookie
放在响应头里面
得到响应头:
response.headers
得到响应头中的cookie:
response.headers['set-cookie']
得到cookie:
response.cookies,可迭代对象

用于标识一个用户,使用场景并不只局限于登陆。

接口数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
JavaScript:由服务端编写的一串浏览器能够执行的代码,能够修改页面元素
你页面中见到的标签都是经过 js 处理之后展示给你的
所以,所见非所得

查看 js 处理之前的页面的三种方式:
1. 在抓包中的预览界面查看
2. 开发者工具右上角三个点 -> 设置 -> 首选项 -> 禁用JavaScript
3. 右键 -> 查看页面源代码

接口数据是指网站利用 js 代码发送请求得到数据后,再将数据渲染到页面中

寻找接口数据的技巧:
ctrl + f 进入搜索
搜索具有特征的数据
搜索中文时,可以转为 Unicode 编码后再搜索

JSON

1
2
3
4
5
6
7
8
9
10
11
一种常用的数据格式,类似于 Python 中的字典
唯一的区别就是不能使用单引号
JSON 中的 null 等价于 Python 中的 None

import json

JSON 转字典:
json.loads(json格式字符串)

字典转 JSON:
json.dumps(字典)

重定向状态码

3开头的状态码表示重定向,意思是请求的资源移动到了新的网址,你应该向新的网址发送请求
新的网址在响应头中的 Location 字段
当状态码为重定向时,浏览器会删除响应内容

requests 库会自动帮我们重定向,可以通过一个参数来指定不要自动重定向(有时候可能需要用到)

IP 地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
对于日常使用,当你连接到互联网时,会为你分配一个 ip
一般来说,只要你不重新进行连接,这个 ip 就不会变了
服务端可以得到客户端的 ip 信息
每个 ip 都是独一无二的
所以服务端也可以使用 ip 来分辨请求来自谁,因为不同的人具备不同的 ip
如果你的 IP 被服务端认为存在异常行为,可能会封禁你的 IP,也就是不返回正常的响应

代理 IP:
无代理 IP:
发送请求:客户端 --> 服务端
得到响应:服务端 --> 客户端
有代理 IP:
发送请求:客户端 --> 代理 IP --> 服务端
得到响应:服务端 --> 代理 IP --> 客户端
在这种情况下,服务端会认为 代理 IP 是它的客户端
低匿名代理:
1. 会告诉服务端请求实际上来自哪个 IP
2. 会告诉服务端请求来自代理 IP,但不会透露真实的 IP
高匿名代理:
不会暴露任何信息

和 cookie 的关系:
在已经登陆的前提下,cookie 是绝对可靠的用于识别用户的手段
ip 则并不可靠

多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
启动多个线程同时执行某一段代码

线程是什么:
操作系统能够进行运算调度的最小单位
被包含在进程中,一个进程可以包含多个线程

导入模块:
import concurrent
from concurrent.futures import ThreadPoolExecutor

启动线程池:
with ThreadPoolExecutor(max_workers=线程数量) as executor:
# 创建任务列表
futures = [executor.submit(需要多线程执行的函数, 这个函数的参数) for i in range(5)]
# 等待所有任务完成
for future in concurrent.futures.as_completed(futures):
pass

线程安全问题:
多个线程同时读写共享数据
需要使用线程锁确保不会同时进行操作
导入模块:from threading import Lock
得到线程锁:lock = Lock()
使用线程锁:with lock:

GIL锁:
Python 解释器内部的一个东西
导致 Python 的多线程不能称为真正的多线程
对于主要时间花费在CPU计算上的任务,Python 的多线程性能可能比单线程更差
对于爬虫,当请求发出去后,CPU就空闲了,只有得到响应后,才重新开始运作,对响应进行处理

单个线程中的代码如果报错,会结束当前线程,不会影响其他线程
如果你不做异常捕获,那你可能无法察觉到报错

ThreadPoolExecutor是一个线程池,会自动帮我们管理线程
可以学习怎样自己管理线程,以及线程之间如何进行通信,但爬虫不太用得到

多进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
导入模块:
from multiprocessing import Pool

启动进程池:
with Pool(进程数量) as pool:
results = pool.map(需要多进程执行的函数, 可迭代对象参数)

得到 CPU 核数:
multiprocessing.cpu_count()

和多线程的不同之处:
进程数量一般不超过你的 CPU 核数
每个进程拥有完全独立的内存空间
创建子进程时,父进程的内存会被完整复制一份,而非共享
对于主要时间花费在CPU计算上的任务,多进程能做到真正的并发
单个进程中的代码如果报错,整个程序就会退出

进程中可以包含线程,所以你的代码中可以同时存在多进程和多线程

dp 自动化

1
2
3
4
5
6
安装:pip install DrissionPage
导入:from DrissionPage import Chromium
需要安装谷歌浏览器

官方文档:
https://www.drissionpage.cn/browser_control/intro

异步,协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
异步的概念:
异步不是多线程,只有一个线程,但异步可以同时执行多个任务
在请求发送出去,等待响应的期间,CPU 是空闲的
异步就是在 CPU 空闲时,切换执行另一个任务
异步就是在一个任务进入等待状态时,切换执行另一个任务
具体是怎样做到的,不是重点

安装模块:pip install aiohttp

导入模块:
import asyncio
import aiohttp

必须使用支持异步的模块发送请求
async 关键字是因为在异步函数中必须这样写
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
等待服务端的响应
results = await response.text()

代码必须封装成异步函数形式
async def main():
创建任务,异步函数加括号不会立刻执行,而是返回一个协程对象
tasks = [异步任务函数(参数) for i in range(n)]
这一步才真正开始执行所有协程
result = await asyncio.gather(*tasks)

执行主要逻辑,异步函数必须通过事件循环执行
asyncio.run(main())

websocket 连接

1
支持服务端主动向客户端发送消息

补充

1
2
3
f12 抓包分类
请求头
robots.txt

关于 scrapy 爬虫框架

1
2
3
基本上就两个使用场景,我认为大部分人没必要去学
1. 需求量非常大,对爬取速度要求高
2. 大学生作业要求用这个东西

下一步

1
2
js 语言基础
js 逆向