python框架-爬虫本质以及bs4模块

摘要

本文内容学习过程中记录

路飞爬虫课程第二天学习内容知识点整理

首先bs4是一个处理html很方便的一个模块,之前在处理html文本时使用的是re正则,但是和bs4相比负责程度不是一个等级,恨没有早点学习到这个模块。

之前也有了解过异步非阻塞的相关知识,但是一直没有能完全理解,经过今天的课程,终于知道了什么是异步非阻塞。

  • bs4
  • 爬虫本质
  • 提高爬虫性能
  • web微信
  • io多路复用
  • 异步非阻塞

一些常见的爬虫经验

图片防盗链

  • referer
  • cookies

轮询

浏览器每一段时间向浏览器发一次请求

缺点:不断的向服务器发请求,服务器的内存压力大, 效率低,存在时间差

长轮询

基于HTTP的长连接,是一种通过长轮询方式实现”服务器推”的技术,它弥补了HTTP简单的请求应答模式的不足,极大地增强了程序的实时性和交互性。

优缺点

  • 轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。
    • 优点:后端程序编写比较容易。
    • 缺点:请求中有大半是无用,浪费带宽和服务器资源。
    • 实例:适于小型应用。
  • 长轮询:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
    • 优点:在无消息的情况下不会频繁的请求,耗费资源小。
    • 缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。
    • 实例:WebQQ、Hi网页版、Facebook IM。
  • 长连接:在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。
    • 优点:消息即时到达,不发无用请求;管理起来也相对方便。
    • 缺点:服务器维护一个长连接会增加开销。
    • 实例:Gmail聊天
  • Flash Socket:在页面中内嵌入一个使用了Socket类的 Flash 程序JavaScript通过调用此Flash程序提供的Socket接口与服务器端的Socket接口进行通信,JavaScript在收到服务器端传送的信息后控制页面的显示。
    • 优点:实现真正的即时通信,而不是伪即时。
    • 缺点:客户端必须安装Flash插件;非HTTP协议,无法自动穿越防火墙。
    • 实例:网络互动游戏。

实现原理

所谓长连接,就是要在客户端与服务器之间创建和保持稳定可靠的连接。其实它是一种很早就存在的技术,但是由于浏览器技术的发展比较缓慢,没有为这种机制的实现提供很好的支持。所以要达到这种效果,需要客户端和服务器的程序共同配合来完成。通常的做法是,在服务器的程序中加入一个死循环,在循环中监测数据的变动。当发现新数据时,立即将其输出给浏览器并断开连接,浏览器在收到数据后,再次发起请求以进入下一个周期,这就是常说的长轮询(long-polling)方式。

  • 轮询的建立
    • 建立轮询的过程很简单,浏览器发起请求后进入循环等待状态,此时由于服务器还未做出应答,所以HTTP也一直处于连接状态中。
  • 数据的推送
    • 在循环过程中,服务器程序对数据变动进行监控,如发现更新,将该信息输出给浏览器,随即断开连接,完成应答过程,实现“服务器推”。
  • 轮询的终止
    • 轮询可能在以下3种情况时终止:
    • 有新数据推送
      • 当循环过程中服务器向浏览器推送信息后,应该主动结束程序运行从而让连接断开,这样浏览器才能及时收到数据。
    • 没有新数据推送
      • 循环不能一直持续下去,应该设定一个最长时限,避免WEB服务器超时(Timeout),若一直没有新信息,服务器应主动向浏览器发送本次轮询无新信息的正常响应,并断开连接,这也被称为“心跳”信息。
    • 网络故障或异常
      • 由于网络故障等因素造成的请求超时或出错也可能导致轮询的意外中断,此时浏览器将收到错误信息。
  • 轮询的重建
    • 浏览器收到回复并进行相应处理后,应马上重新发起请求,开始一个新的轮询周期。

bs4(beautifulsoup4)

beautifuilsoup4: 用于解析html标签

安装

1
2
3
4
pip install beautifulsoup4
# 安装其他解释器
pip install lxml
pip install html5lib

Beautiful Soup支持Python标准库中的HTML解析器,还支持一些第三方的解析器,如果我们不安装它,则 Python 会使用 Python默认的解析器,lxml 解析器更加强大,速度更快,推荐安装。

解释器

  • html.parser
    • Python的内置标准库
    • 执行速度适中
    • 文档容错能力强
    • BeautifulSoup(markup, “html.parser”)
    • Python 2.7.3 or 3.2.2)前 的版本中文档容错能力差
  • lxml
    • 速度快
    • 文档容错能力强
    • BeautifulSoup(markup, “lxml”)
    • 需要安装C语言库
  • xml
    • 速度快
    • 唯一支持XML的解析器
    • BeautifulSoup(markup, [“lxml”, “xml”])
    • BeautifulSoup(markup, “xml”)
    • 需要安装C语言库
  • html5lib
    • 最好的容错性
    • 以浏览器的方式解析文档
    • 生成HTML5格式的文档
    • BeautifulSoup(markup, “html5lib”)
    • 速度慢
    • 不依赖外部扩展

使用方法

1
2
3
4
5
6
7
8
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, 'htmp.parser')

# 格式化输出
print(soup.prettify())

tag = soup.find(id='li') # 查找第一个id为li的标签
tag = soup.find(name='div' id='item', attrs={'class': 'list'}, recursive=True, text='Lacie', class_='item)
  • name: 标签名
  • id:标签的id属性
  • attrs:标签的属性字典
  • recursive:是否递归的去查找,false 只在子集里找, true 递归查找
  • text:标签的文本信息
  • class_:标签的class属性
1
2
3
tag_list = soup.find_all('div) # fing_all 查找所有div 返回一个list 参数名同find
# select 同css的选择器
tag = soup.select('#li')
  • 通过标签名查找
    • soup.select(‘a’)
    • soup.select(‘title’)
  • 通过类名查找
    • soup.select(‘.sister’)
  • 通过 id 名查找
    • soup.select(‘#link1’)
  • 组合查找
    • soup.select(‘p #link1’)
  • 直接子标签查找
    • soup.select(“head > title”)
  • 属性查找

其他方法

  • tag.text # 返回标签的文本
  • tag.string # 返回标签的内容
  • tag.stripped_string # 递归循环获取内部所有标签文本
  • tag.name # 返回标签名称
  • tag.attrs # 返回标签的属性 字典类型
  • tag.clear() # 清空内部所有标签
  • tag.decompose() # 清空所有标签包含自己
  • tag.extract() # 同decompose 但是会返回删除的标签
  • tag.encode() # 返回标签的字节类型
  • tag.decode() # 返回标签的字符串类型
  • tag.encode_contents() # 返回标签的内部内容字节类型
  • tag.decode_contents() # 返回标签的内部内容字符串类型
  • tag.attre = {‘id’: ‘hello’} # 重新赋值
  • tag.get(‘id’) # 获取标签的id属性
  • tag.has_attr(‘id’) # 判断当前标签是否有id这个属性
  • tag.get_text() # 获取当前标签的文本
  • tag.index(tag,find(‘div’)) # 查找标签在某个标签中的索引位置
  • tag.is_empty_element() 判断是否是自闭和标签:
    • 如br,hr,input,img,meta,soacer,link,frame,base
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 当前标签的关联标签,同jquery的链式调用
tag.next
tag.next_element
tag.next_elements
tag.next_sibling
tag.next_siblings
tag.previous
tag.previous_element
tag.previous_elements
tag.previous_sibling
tag.previous_siblings
tag.parend
tag.parents
tag.children # 当前标签的所有子标签,返回一个列表
tag.descendants # 当前元素的所有子(递归)标签

find_all( name , attrs , recursive , text , **kwargs )

find_all() 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件

  • name 参数
    • 字符串:soup.find_all(‘b’)
    • 正则:soup.find_all(re.compile(“^b”)
      • 如果传入正则表达式作为参数,Beautiful Soup会通过正则表达式的 match() 来匹配内容
    • 列表:soup.find_all([“a”, “b”])
    • True:soup.find_all(True)
      • True 可以匹配任何值,下面代码查找到所有的tag,但是不会返回字符串节点
    • 传方法:
1
2
3
4
5
6
7
8
9
# 如果没有合适过滤器,那么还可以定义一个方法,
# 方法只接受一个元素参数
# 如果这个方法返回 True 表示当前元素匹配并且被找到,如果不是则反回 False
# 下面方法校验了当前元素,如果包含 class 属性却不包含 id 属性,那么将返回 True:

def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')

soup.find_all(has_class_but_no_id)
  • keyword 参数
    • soup.find_all(id=’link2’)
    • soup.find_all(href=re.compile(“elsie”))
    • soup.find_all(href=re.compile(“elsie”), id=’link1’)
    • soup.find_all(“a”, class_=”sister”)
    • data_soup.find_all(data-foo=”value”)
    • data_soup.find_all(attrs={“data-foo”: “value”})
  • text 参数
    • soup.find_all(text=”Elsie”)
    • soup.find_all(text=[“Tillie”, “Elsie”, “Lacie”])
    • soup.find_all(text=re.compile(“Dormouse”))
  • limit 参数
    • soup.find_all(“a”, limit=2)
  • recursive 参数: 递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 其他的一些find方法,用法和find_all类似
find( name , attrs , recursive , text , **kwargs )
# 它与 find_all() 方法唯一的区别是 find_all() 方法的返回结果是值包含一个元素的列表,而 find() 方法直接返回结果

find_parents()
find_parent()
# find_all() 和 find() 只搜索当前节点的所有子节点,孙子节点等. find_parents() 和 find_parent() 用来搜索当前节点的父辈节点,搜索方法与普通tag的搜索方法相同,搜索文档搜索文档包含的内容

find_next_siblings()
find_next_sibling()
# 这2个方法通过 .next_siblings 属性对当 tag 的所有后面解析的兄弟 tag 节点进行迭代, find_next_siblings() 方法返回所有符合条件的后面的兄弟节点,find_next_sibling() 只返回符合条件的后面的第一个tag节点

find_previous_siblings()
find_previous_sibling()
# 这2个方法通过 .previous_siblings 属性对当前 tag 的前面解析的兄弟 tag 节点进行迭代, find_previous_siblings() 方法返回所有符合条件的前面的兄弟节点, find_previous_sibling() 方法返回第一个符合条件的前面的兄弟节点

find_all_next()
find_next()
# 这2个方法通过 .next_elements 属性对当前 tag 的之后的 tag 和字符串进行迭代, find_all_next() 方法返回所有符合条件的节点, find_next() 方法返回第一个符合条件的节点

find_all_previous()
find_previous()
# 这2个方法通过 .previous_elements 属性对当前节点前面的 tag 和字符串进行迭代, find_all_previous() 方法返回所有符合条件的节点, find_previous()方法返回第一个符合条件的节点

如何提高爬虫的并发

当爬取大量类似信息、数据、新闻、图片等信息时,串行的爬行方式会消耗大量的时间,这时可以通过并行的方式来提高爬虫的效率以节省时间。

  • 多进程
    • 消耗资源太多,资源浪费
    • 没有大量的cpu操作,多为io操作
  • 使用多线程
    • 相对单线程方式每个线程发送请求的等待时间也有资源浪费
  • 使用单线程(gevent、twisted、tornado、asyncio)
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
#!/usr/bin/env python
# -*- coding: utf-8 -*-


import sys

sys.setrecursionlimit(100000)
import requests
from bs4 import BeautifulSoup
from gevent import monkey
import gevent


headers = {
"User-agent" : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36'
}
data = {
'phone':'86176xxxxxxxx',
'password':"xxxx",
'oneMonth':'1'
}

# 第一次访问获取未授权的cookie
r0 = requests.get(url='https://dig.chouti.com/', headers = headers)
r0_cookie = r0.cookies.get_dict()
# 发送用户名密码认证
ret = requests.post(url='https://dig.chouti.com/login',data= data, headers = headers,cookies = r0_cookie)
print(ret.text)

index = requests.get(url='https://dig.chouti.com/', headers = headers)
soup = BeautifulSoup(index.text, 'html.parser')
div = soup.find(name='div',id='content-list')
items = div.find_all(attrs={'class':'item'})

monkey.patch_all()

def makeLink(nid):
r1 = requests.post(url='https://dig.chouti.com/link/vote?linksId=%s' %nid,headers = headers, cookies=r0_cookie)
print(r1.text)

tasks = []
for item in items:
tag = item.find(name='div',attrs={'class':'part2'})
if not tag:
continue
nid = tag.get('share-linkid')
print(nid)
tasks.append(gevent.spawn(makeLink, nid))
gevent.joinall(tasks)

IO多路复用(select、poll、epoll)

http://www.cnblogs.com/wupeiqi/articles/6229292.html

目标:用于监听多个socket对象是否发生变化

1
2
3
4
5
import select 

while True:
# 让select模块帮助我们检测sk1、sk2两个socket对象是否发生变化
r,w,e = select.select([sk1,sk2],[sk1,sk2],[],0.5)
  • r=[],如果r有值了
    • r=[sk1,],表示sk1已经获取到响应的内容了,本次请求可以终止了
    • r=[sk1,sk2],表示两个socket都获取到响应的内容了
  • w=[],如果w有值了
    • w=[sk1,],表示sk1这个socket已经连接成功,下一步接收数据
    • w=[sk1,sk2],表示两个socket都连接成功了

非阻塞

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
import select


class AsyncHttp(object):
def __init__(self):
self.fds = []
self.conn = []

def add(self, url):
sk = socket.socket()
# 加上这个connect就不阻塞了
sk.setblocking(False)
try:
sk.connect((url,80))
except Exception as e:
pass
self.fds.append(sk)
self.conn.append(sk)

def run(self):
while True:
r, w, e = select.select(self.fds, self.conn, [], 1)

for client in w:
client.sendall(b'GET /index HTTP/1.1\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36\r\n\r\n')

self.conn.remove(client)

for cli in r:
data = cli.recv(8096)
print("data==>",data)
# 断开连接:短链接,无状态
cli.close()
self.fds.remove(cli) # 不再监听
if not self.fds:
break

def main():
a = AsyncHttp()
a.add("www.baidu.com")
a.add("c.isme.pub")
a.run()

if __name__ == "__main__":
main()

异步回调

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
58
59
60
61
62
63
64
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
import select

class Request(object):
def __init__(self, sk, callback):
self.sk = sk
self.callback = callback

def fileno(self):
return self.sk.fileno()

class AsyncHttp(object):
def __init__(self):
self.fds = []
self.conn = []

def add(self, url, callback):
sk = socket.socket()
# 加上这个connect就不阻塞了
sk.setblocking(False)
try:
sk.connect((url,80))
except Exception as e:
pass
req = Request(sk, callback)
self.fds.append(req)
self.conn.append(req)

def run(self):
while True:
r, w, e = select.select(self.fds, self.conn, [], 1)

for client in w:
client.sk.sendall(b'GET /index HTTP/1.1\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36\r\n\r\n')

self.conn.remove(client)

for req in r:
data = req.sk.recv(8096)
req.callback(data)
# 断开连接:短链接,无状态
req.sk.close()
self.fds.remove(req) # 不再监听
if not self.fds:
break
def callback1(data):
print("111")
print("data==>",data)

def callback2(data):
print("222")
print("data==>",data)

def main():
a = AsyncHttp()
a.add("www.baidu.com",callback1)
a.add("c.isme.pub", callback2)
a.run()

if __name__ == "__main__":
main()

史上最牛逼的异步IO模块

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import select
import socket
import time


class AsyncTimeoutException(TimeoutError):
"""
请求超时异常类
"""

def __init__(self, msg):
self.msg = msg
super(AsyncTimeoutException, self).__init__(msg)


class HttpContext(object):
"""封装请求和相应的基本数据"""

def __init__(self, sock, host, port, method, url, data, callback, timeout=5):
"""
sock: 请求的客户端socket对象
host: 请求的主机名
port: 请求的端口
port: 请求的端口
method: 请求方式
url: 请求的URL
data: 请求时请求体中的数据
callback: 请求完成后的回调函数
timeout: 请求的超时时间
"""
self.sock = sock
self.callback = callback
self.host = host
self.port = port
self.method = method
self.url = url
self.data = data

self.timeout = timeout

self.__start_time = time.time()
self.__buffer = []

def is_timeout(self):
"""当前请求是否已经超时"""
current_time = time.time()
if (self.__start_time + self.timeout) < current_time:
return True

def fileno(self):
"""请求sockect对象的文件描述符,用于select监听"""
return self.sock.fileno()

def write(self, data):
"""在buffer中写入响应内容"""
self.__buffer.append(data)

def finish(self, exc=None):
"""在buffer中写入响应内容完成,执行请求的回调函数"""
if not exc:
response = b''.join(self.__buffer)
self.callback(self, response, exc)
else:
self.callback(self, None, exc)

def send_request_data(self):
content = """%s %s HTTP/1.0\r\nHost: %s\r\n\r\n%s""" % (
self.method.upper(), self.url, self.host, self.data,)

return content.encode(encoding='utf8')


class AsyncRequest(object):
def __init__(self):
self.fds = []
self.connections = []

def add_request(self, host, port, method, url, data, callback, timeout):
"""创建一个要请求"""
client = socket.socket()
client.setblocking(False)
try:
client.connect((host, port))
except BlockingIOError as e:
pass
# print('已经向远程发送连接的请求')
req = HttpContext(client, host, port, method, url, data, callback, timeout)
self.connections.append(req)
self.fds.append(req)

def check_conn_timeout(self):
"""检查所有的请求,是否有已经连接超时,如果有则终止"""
timeout_list = []
for context in self.connections:
if context.is_timeout():
timeout_list.append(context)
for context in timeout_list:
context.finish(AsyncTimeoutException('请求超时'))
self.fds.remove(context)
self.connections.remove(context)

def running(self):
"""事件循环,用于检测请求的socket是否已经就绪,从而执行相关操作"""
while True:
r, w, e = select.select(self.fds, self.connections, self.fds, 0.05)

if not self.fds:
return

for context in r:
sock = context.sock
while True:
try:
data = sock.recv(8096)
if not data:
self.fds.remove(context)
context.finish()
break
else:
context.write(data)
except BlockingIOError as e:
break
except TimeoutError as e:
self.fds.remove(context)
self.connections.remove(context)
context.finish(e)
break

for context in w:
# 已经连接成功远程服务器,开始向远程发送请求数据
if context in self.fds:
data = context.send_request_data()
context.sock.sendall(data)
self.connections.remove(context)

self.check_conn_timeout()


if __name__ == '__main__':
def callback_func(context, response, ex):
"""
:param context: HttpContext对象,内部封装了请求相关信息
:param response: 请求响应内容
:param ex: 是否出现异常(如果有异常则值为异常对象;否则值为None)
:return:
"""
print(context, response, ex)

obj = AsyncRequest()
url_list = [
{'host': 'www.google.com', 'port': 80, 'method': 'GET', 'url': '/', 'data': '', 'timeout': 5,
'callback': callback_func},
{'host': 'www.baidu.com', 'port': 80, 'method': 'GET', 'url': '/', 'data': '', 'timeout': 5,
'callback': callback_func},
{'host': 'www.bing.com', 'port': 80, 'method': 'GET', 'url': '/', 'data': '', 'timeout': 5,
'callback': callback_func},
]
for item in url_list:
print(item)
obj.add_request(**item)

obj.running()

协程

协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程。

  • 微线程不存在的,认为创造控制程序先执行某段代码,后跳到某处执行某段代码
  • 遇到非IO请求切换,性能更低
  • 遇到IO请求切换,性能高,能够实现并发(利用IO等待的过程,再去干其他的事)
  • 协程拥有自己的寄存器上下文和栈
  • 线程切换的时候是保存在cpu的寄存器
  • 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈
  • 因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合)
  • 每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

优点

  • 无需线程上下文切换的开销
  • 无需原子操作锁定及同步的开销
  • 原子操作(atomic operation)是不需要synchronized,所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
  • 方便切换控制流,简化编程模型
  • 高并发+高扩展性+低成本,一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

浏览器、爬虫发送请求的本质

  • 本质是socket客户端
    • socket客户端建立tcp连接的过程是阻塞的
    • 等待服务器返回内容是阻塞的
  • socket客户端遵循HTTP协议
    • 通过\r\n分割的格式规范
    • 请求相应之后断开连接
  • http协议建立在tcp协议之上
    • http协议规定了发送的数据格式
    • 请求数据用\r\n分割
    • 请求头和请求体用\r\n\r\n分割
    • 响应头和响应体也是\r\n分割
    • 请求相应之后断开连接
    • 短连接,无状态

面试题

  • 什么是异步
    • 体现是回调
  • 什么是非阻塞
    • 体现是不等待
    • 报错就捕捉
    • sk.setblocking(False)
  • 自定义异步非阻塞模块
    • 基于socket设置setblocking和io多路复用
    • 爬虫发送http请求本质创建socket对象
    • io多路复用循环监听socket是否发生变化,一旦发生变化,我们可以自定义操作(触发某个函数的执行)
    • 本质socket+IO多路复用
  • http协议是什么
  • 如何提高爬虫的并发
    • 进程
    • 线程
    • 利用异步非阻塞模块实现单线程并发请求
  • io多路复用的作用
    • select,内部循环检测socket是否发生变化,1024个
    • poll,没有个数限制
    • epoll,回调的方式,没有个数限制