本笔记是学习崔庆才老师的网络爬虫课程的总结
一、HTTP基础原理
1. URI、URL、URN
URI: Uniform Resource Identifier,即统一资源标志符
URL:Universal Resource Locator,即统一资源定位符
URN:Universal Resource Name,即统一资源名称
三者的关系就是URI=URL+URN,现在一般的URI和URL等价。对于https://github.com/favicon.ico来说,就包含协议名https,访问路径(根目录)和资源名称favicon.ico。通过这些找到一个目标资源,这就是URL/URI
2. 超文本: Hypertext
网页的html代码。
3. HTTP和HTTPS
HTTP: Hyper Text Transfer Protocol,中文名叫作超文本传输协议,HTTPS 的全称是 Hyper Text Transfer Protocol over Secure Socket Layer。
这是两个协议头,https是在http上做的优化。其目的就是加强安全性,在HTTP下加入了SSL层,所以称为HTTPS。在有SSL的安全基础上,通过它传输的内容都是经过SSL加密的,其作用 如下:
- 建立一个信息安全通道,来保证数据传输的安全。
- 确认网站的真实性,凡是使用了 HTTPS 的网站,都可以通过点击浏览器地址栏的锁头标志来查看网站认证之后的真实信息,也可以通过 CA 机构颁发的安全签章来查询。
4. HTTP请求过程
客户端发送请求(Request)给服务器,服务器响应(Response)客户端请求的这么一个过程,在这个过程中,会请求许多的资源用于页面的渲染,功能的完善等。在电脑端可以通过f12中的network查看请求与响应,其中包含了各种资源的请求和响应。
扩展:Network中每一列的含义:
- Name :请求的名称,一般会把资源作为名称
- Status :响应状态码,具体数字的含义可以查看百度,常见的200表示响应正常通过
- Type :请求的文件类型,有document表示请求html文档,有png表示请求图片
- Inititator:请求源。用来标记是谁发起的这个请求。
- Size:从服务器下载的文件和请求的资源大小。如果是从缓存中取得的资源,则该列会显示 from cache。
- Time:发起请求到获取响应所用的总时间。
- Waterfall:网络请求的可视化瀑布流。
点击其中一个条目后,其中还有更多详细信息,主要说明下头部里面的信息,整个交互过程是通过请求和响应来进行的,因此以主要的请求和响应信息展开。
请求:包含了请求方法(Request Method)、请求的网址(Request URL)、请求头(Request Headers)、请求体(Request Body) - 请求方法: 常见的就是get和post,一般区别:GET 请求中的参数包含在 URL 里面,数据可以在 URL 中看到,而 POST 请求的 URL 不会包含这些数据,数据都是通过表单形式传输的,会包含在请求体中。GET 请求提交的数据最多只有 1024 字节,而 POST 请求没有限制。
其他的请求方法还包含了HEAD、PUT、DELETE、OPTIONS、CONNECT、TRACE等,共八种请求。都是通过不同的方式去对URL进行请求。 - 请求头: 用来说明服务器要使用的附加信息,比较重要的信息有 Cookie、Referer、User-Agent 等。
- Accept:请求报头域,用于指定客户端可接受哪些类型的信息。
- Accept-Language:说明客户端可接受的语言类型。
- Accept-Encoding:指定客户端可接受的内容编码。
- Host:用于指定请求资源的主机 IP 和端口号,其内容为请求 URL 的原始服务器或网关的位置。从 HTTP 1.1 版本开始,请求必须包含此内容。
- Cookie:也常用复数形式 Cookies,这是网站为了辨别用户进行会话跟踪而存储在用户本地的数据。它的主要功能是维持当前访问会话。信息一般保存在客户端的电脑上。
- Referer:此内容用来标识这个请求是从哪个页面发过来的,服务器可以拿到这一信息并做相应的处理,如做来源统计、防盗链处理等。
- User-Agent:简称 UA,它是一个特殊的字符串头,可以使服务器识别客户使用的操作系统及版本、浏览器及版本等信息。在做爬虫时加上此信息,可以伪装为浏览器;如果不加,很可能会被识别出为爬虫。
- Content-Type:也叫互联网媒体类型(Internet Media Type)或者 MIME 类型,在 HTTP 协议消息头中,它用来表示具体请求中的媒体类型信息。例如,text/html 代表 HTML 格式,image/gif 代表 GIF 图片,application/json 代表 JSON 类型
在使用爬虫时,为了伪装,大部分情况下都需要设置请求头。
- 请求体
这里面的信息一般是post提交的表单信息,此时需要注意 Request Headers 中指定 Content-Type 为 application/x-www-form-urlencoded。只有设置 Content-Type 为 application/x-www-form-urlencoded,才会以表单数据的形式提交。或将 Content-Type 设置为 application/json 来提交 JSON 数据,或者设置为 multipart/form-data 来上传文件。因此在使用爬虫时,如果需要构造post请求,则在Content-Type这一方面就需要做出修改。
响应
响应,由服务端返回给客户端,可以分为三部分:响应状态码(Response Status Code)、响应头(Response Headers)和响应体(Response Body)。 - 响应状态码
即1xx,2xx,3xx,4xx,5xx,这些都是对应服务器返回给客户端的信息,可以让客户端通过状态码判断服务器的响应状态。常见的就是200,说明成功返回数据。具体的许多响应码可以查表。 - 响应头
- Date:标识响应产生的时间。
- Last-Modified:指定资源的最后修改时间。
- Content-Encoding:指定响应内容的编码。
- Server:包含服务器的信息,比如名称、版本号等。
- Content-Type:文档类型,指定返回的数据类型是什么,如 text/html 代表返回 HTML 文档,application/x-javascript 则代表返回 JavaScript 文件,image/jpeg 则代表返回图片。
- Set-Cookie:设置 Cookies。响应头中的 Set-Cookie 告诉浏览器需要将此内容放在 Cookies 中,下次请求携带 Cookies 请求。
- Expires:指定响应的过期时间,可以使代理服务器或浏览器将加载的内容更新到缓存中。如果再次访问时,就可以直接从缓存中加载,降低服务器负载,缩短加载时间。
- 响应体
这是我们主要爬出去的目标,如果爬的是网页,那么返回的就是html代码,图片则是对应的二进制代码。后期主要就是对响应体进行解析。
二、Web网页基础
主要说明说Web网页的三大基础技术html、css、js,这三个部分主要记录下选择器,不常使用总容易忘。
1. 节点树
了解html结构的应该挺容易理解这块,所有标签定义的内容都是节点,可以被不断的包裹起来,形成一棵html dom树。形成这样的规范其实就是说明了页面中的每一个节点都能够被做出响应的修改。因此通过选择器,选择到了这个节点后,可以通过代码对网页做出修改。
2. 选择器
- id选择器:
语法:# id名称,如:#container - 类选择器
语法:.类名,如.wrapper - 标签选择器
语法:标签名,如h2 - 嵌套选择(父子选择器)
语法是使用空格表示,如#container .wrapper p表示先选择了id为container,如何在选择其下类名为wrapper的标签,然后再选择其下的p标签。还有一种是使用>来表示。 - 并列选择器
语法 标签名.属性,比如div.id。那么就会选择这个标签。
更多的内容扩展可以再w3cschool查看。
三、爬虫基本原理
1. 概述
实际上就是一个获取页面并提取和保存信息的自动化程序。
2. 获取网页
就像之前提到的,我们需要去解析的是响应回来的信息,我们可以通过模拟浏览器的请求然后获得网页信息
3. 提取信息
- 在获得网页信息后,我们需要从中提取想要的数据,页面是html语言,有各种标签。这时候就用到了我们之前讲过的选择器,通过表达式或者规律找到我们需要的信息。当然,在现在有各种功能库方便我们去提取信息,比如:Beautiful Soup、pyquery、lxml等。
- 对于数据的抓取并没有太多类型的要求,在浏览器中可以访问到的都可以进行获取,这些的基础就是基于我们的URL即HTTPS和HTTP协议。
- 在获取的过程中有一些难点,比如js的渲染。有的网页可能呈现的源码和页面不符,这就有可能是因为调用了js的文件对网页进行渲染的后果,导致我们无法直接去查看,获得的只是一个空壳,但是我们可以分析后台的接口,或者使用Selenium、Splash这样的库来进行模拟js渲染,达到我们要的效果。
4. 保存数据
我们获取数据后,我们需要去进行保存。保存的形式有各种各样的,可以是txt或者json文件。当然我们也可以保存到数据库中。
Session和Cookuies
1. 静态网页和动态网页
静态网页:网页的内容是 HTML 代码编写的,文字、图片等内容均通过写好的 HTML 代码来指定,这种页面叫作静态网页。其优点就是速度快,简单,但是可维护性很差,而且不美观。
动态网页:动态解析 URL 中参数的变化,关联数据库并动态呈现不同的页面内容,非常灵活多变。
2. 无状态HTTP
无状态的含义是:指 HTTP 协议对事务处理是没有记忆能力的,也就是说服务器不知道客户端是什么状态。
此前我们了解到,客户端和服务器通过请求和响应进行联系,由于http协议是无状态的,因此它的记忆仅是一次请求和响应,此后就什么也不记得了。为了保持他的记忆功能,应运而生的就是Session和Cookies。
3.Session
基本概念:
- Session存在于服务器端,浏览器的下次访问时就自动的附带Session。
- 中文名称为会话。由于存在于服务器端,因此用户第一次访问的时候会创建一个Session,知道过期或者放弃时服务器才会终止该Session。
4. Cookie
1.基本概念
Cookies 指某些网站为了辨别用户身份、进行 Session 跟踪而存储在用户本地终端上的数据。
2.Session维持
客户端第一次访问服务器时,服务器会返回一个响应头中带有Set-Cookie的字段给客户端,用来标记一些用户信息,这里面就带有了关于Session的相关信息。下一次客户端请求这个服务器的时候,客户端的请求头就会带上Cookies交给服务器,服务器读取了其中的SeesionID信息,在服务器端就可以判断用户的状态,到底时正常访问还是过期。通过Cookies和Session的搭配,Cookies在客户端,Session在服务端,两者协作负责两头,这样就实现了Session控制。
3.属性结构
在浏览器中按f12,进入Application选项卡,在下方就能看到Cookies的标志,里面的一条条就是Cookie。
- Name:Cookie的名称,一旦创建,不可修改。
- Value:即Cookie的取值,值为Unicode编码,需要为字符编码。如果为二进制数据,则需要使用BASE64编码。
- MaxAge:Cookie的失效时间,秒为单位。为正,则在正的xx秒后失效,为负,则浏览器关闭失效。
- Path:说明这个Cookie作用的路劲。/path/即页面下的某个路径才能访问,/则为本域名下所有网页都可以访问此cookie。
- Domain:可以使用该Cookie的域名。
- Size:Cookie的大小。
- Http字段:即 Cookie 的 httponly 属性。若此属性为 true,则只有在 HTTP Headers 中会带有此 Cookie 的信息,而不能通过 document.cookie 来访问此 Cookie。
- Secure:即该 Cookie 是否仅被使用安全协议传输。安全协议。安全协议有 HTTPS、SSL 等,在网络上传输数据之前先将数据加密。默认为 false。
4.会话Cookie和持久Cookie
有分类指Cookie有会话和持久之分,即会话Cookie就是浏览器关闭后就失效,而持久Cookie会把信息保存在客户端的磁盘中,以此达到持久化的效果。实际上Cookie的时间是通过Max Age和Expires字段决定过期时间的。通过设置很久的过期时间达到永久生效的效果。
5.常见的误区关于Session
常说Session就存活于一次会话中,浏览器关闭后就消失了,但是用户信息这种事情服务器怎么可能会轻易删除。服务器通过Cookies来保存SessionID信息,这样客户端和服务器断开连接后,当再此连接的时候,请求头带有Cookies信息,里面的SessionID对应上服务器信息后就再次识别出这个Session了。Session的消失也是有一个失效时间的,超过了时间才会删除,以此来节省服务器的空间。
四、python的多线程基本了解
在python中多线程的模块是threading,是python自带的模块。
1.创建线程的例子
Thread直接创建子线程
#例子来自于催庆才老师52讲
import threading
import time
def target(second):
print(f'Threading {threading.current_thread().name} is running')
print(f'Threading {threading.current_thread().name} sleep {second}s')
time.sleep(second)
print(f'Threading {threading.current_thread().name} is ended')
print(f'Threading {threading.current_thread().name} is running')
for i in [1, 5]:
thread = threading.Thread(target=target, args=[i])
thread.start()
print(f'Threading {threading.current_thread().name} is ended')
直接创建线程的方法是threading模块的Thread方法。
扩展:Thread()
- group:这个一般不设置,这是为了以后线程组而预留的
- target(*):这是个回调函数,默认是None,但是我们使用直接创建线程的方式一般会设置这个参数,否则什么函数都不调用
- name:这个线程的名称,默认名字就是Thread-N的形式。
- args(*):这个是target这个函数需要的参数元组,默认为空()。
- kwargs:这就是默认调用的字典,默认为{}
- daemon:表示为是否设置为守护线程,即主线程退出后它就退出,不管运行有没有结束。
继续上述的例子,通过扩展知识我们明白了这个例子就是通过Thread方法创建了线程,传入的参数为了让线程休眠,达到不同的执行顺序。随后通过start方法进行了线程的启动。从代码知道,这种方式会产生主线程结束,子线程还没有结束的情况,因为子线程被休眠后没有主线程结束的快。结果如下:
如果要达到子线程在主线程之前结束,我们就要使用到线程中的join方法。即让线程有了顺序。
扩展:Join()
join() 方法的功能是在程序指定位置,优先让该方法的调用者使用 CPU 资源。实际上就是可以控制执行顺序,只要把线程进行了join操作就会优先执行这个线程。
在例子中,我们把线程一加入,在加入线程二到threads这个列表中。在后续遍历的时候先拿到了Thread1,执行join方法,因此线程一先执行,再执行线程二。
结果如下:
当然,如果不想一直等一个线程结束,那么join()是可以添加参数控制一个线程执行的时间长短的:timeout参数,默认不设置。如果设置了线程没有在规定的时间结束的话,那么就会被强制结束。
这样就实现了主线程再最后才退出。
继承Thread类创建子线程
import threading
import time
class MyThread(threading.Thread):
def __init__(self, second):
threading.Thread.__init__(self)
self.second = second
def run(self):
print(f'Threading {threading.current_thread().name} is running')
print(f'Threading {threading.current_thread().name} sleep {self.second}s')
time.sleep(self.second)
print(f'Threading {threading.current_thread().name} is ended')
print(f'Threading {threading.current_thread().name} is running')
threads = []
for i in [1, 5]:
thread = MyThread(i)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print(f'Threading {threading.current_thread().name} is ended')
利用继承来表明这是个子线程,有两个要点,一个是必须继承于Thread,其二必须有run方法,里面就是线程执行的内容。创建好线程类后,创建这个对象,然后还是通过start()方法进行运行。其他就是同直接创建类似。
2. 守护线程
守护线程的作用就是:如果主线程结束了而该守护线程还没有运行完,那么它将会被强制结束。
这个在扩展Thread()方法的时候就提及,也可以通过后续setDaemon方法进行设置。
在一个例子中对线程二设置为守护线程后,执行结果如下:
即原本主线程结束后,线程1和线程2会继续执行,但是由于线程2为守护线程,因此也退出了。
扩展
当然,如果加入了join方法,那么还是会按顺序先执行线程1和线程2才会结束主线程。
3. 互斥锁
用一个代码来介绍这个知识点:
import threading
import time
count = 0
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global count
temp = count + 1
time.sleep(0.001)
count = temp
threads = []
for _ in range(1000):
thread = MyThread()
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print(f'Final count: {count}')
代码的含义就是设置一个全局变量,每个函数都是让这个全局变量+1,如果按照顺序执行,那么正常这个count最后为1000,但是最终的结果却不是。原因就是当代码执行获取count值时,可能同时获得其他线程还没+1的值,即线程1执行了global count以及temp=count+1后休眠了,还没运行count = temp,即修改变量后存储回去的操作,其他线程就读取了count这个值,导致大家拿到的count值相同。这就出现了数据错误。
为了解决这个问题,提出了锁的概念,学过操作系统应该很容易理解这个概念。因此在需要共享访问的变量前后加锁即可解决这个问题。
python中加锁是通过方法:threading.Lock()
代码如下:
import threading
import time
count = 0
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global count
temp = count + 1
time.sleep(0.001)
count = temp
threads = []
for _ in range(1000):
thread = MyThread()
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print(f'Final count: {count}')
代码中通过threading.Lock()方法获取了一个锁对象,在run方法中利用这个lock对象中的acquire()方法进行加锁,在使用这个变量结束后使用lock.release()方法进行锁的释放。
4.Python中多线程的问题
python的多线程受制于GIL(Global Interpreter Lock,即全局解释器锁),目的是为了安全,这就导致不论是在单核还是多核条件下,在同一时刻只能运行一个线程,导致 Python 多线程无法发挥多核并行的优势。
这就相当于给python的线程上锁了,python要执行多线程,线程需要先获取GIL,然后直线代码,然后释放GIL。但是这个点对爬虫这种IO密集其实影响不大。
五、python中多进程的基本原理
1.多进程优势
前面提到,python的多线程其实会受GIL影响。一个进程包含多线程,每一个进程都有一个自己的GIL。所以在多核的情况下,多进程的运行是不受GIL影响的。虽然爬虫这种IO密集型任务其实多线程和多进程差别不大,但是如果能用多进程就多用多进程。
2.多进程的实现
线程的实现依靠threading库,进程的实现也是python中有内置库:multiprocessing。其提供了一些组件:如 Process(进程)、Queue(队列)、Semaphore(信号量)、Pipe(管道)、Lock(锁)、Pool(进程池)等。这些都可以在操作系统这门课学习到。
多进程的实现和线程类似,也是有两种实现方式,一种直接使用,
直接使用Process类
import multiprocessing
def process(index):
print(f'Process: {index}')
if __name__ == '__main__':
for i in range(5):
p = multiprocessing.Process(target=process, args=(i,))
p.start()
扩展:Process
这个Process类其实是继承自process.BaseProcess,所以它的init初始化方法在BaseProcess中,自己并没有设置:
这个类内部自己就说了进程类似与线程。因此里面的参数其实都差不多,不再赘述,这里args必须是元组,在底层也会帮忙进行转换。
前面代码执行结果如下:
扩展方法
- multiprocessing.cpu_count():获取电脑的CPU数
- multiprocessing.active_children():获得一个目前还存活的进程集合。可以获取当前进程的一些相关信息,比如name和pid等。
通过继承实现进程
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self, loop):
Process.__init__(self)
self.loop = loop
def run(self):
for count in range(self.loop):
time.sleep(1)
print(f'Pid: {self.pid} LoopCount: {count}')
if __name__ == '__main__':
for i in range(2, 5):
p = MyProcess(i)
p.start()
大致的过程和前面线程的实现是一样的,都是继承自Process类,然后重写里面的run方法,切记在init里重新进行一次初始化,否则会报参数丢失的错误。
3. 守护进程
和线程一样,进程也可以设置守护进程,因为进程也有子与夫的关系。父进程结束后,子进程也会被强制结束。
语法为:进程.daemon = True。也可以直接在初始化的时候设置为True。
4. 进程等待
进程的等待其实就是想要按顺序的执行进程,设置了守护进程就会导致子进程无法执行就结束,那么想要子进程运行完结束,或者就是控制进程执行的顺序。是不是和线程的一个方法很类似?
是的,使用join()方法。同线程一样,维护一个执行的列表即可。代码如下:
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self,loop):
Process.__init__(self)
self.loop = loop
def run(self) -> None:
for count in range(self.loop):
time.sleep(1)
print(f'Pid{self.pid},LoopCount:{count}')
if __name__ == '__main__':
processes = []
for i in range(2,5):
process = MyProcess(i)
processes.append(process)
process.daemon = True
process.start()
for process in processes:
process.join()
print('Main Process ended')
结果如下:
5.进程终止
目前我们知道的线程终止可以通过守护进程,或者join()方法中设置超时时间结束。还可以通过terminate方法结束进程。通过is_alive方法判断进程是否还在运行。
示例代码:
import multiprocessing
import time
def process():
print('Starting')
time.sleep(5)
print('Finished')
if __name__ == '__main__':
p = multiprocessing.Process(target=process)
print('Before:', p, p.is_alive())
p.start()
print('During:', p, p.is_alive())
p.terminate()
print('Terminate:', p, p.is_alive())
p.join()
print('Joined:', p, p.is_alive())
结果如下:
通过两个方法对进程存在进行操作和判断,值得注意的是再运行terminate后进程的存活还是True,是因为进程的回收是需要时间的,因此后续的join操作给进程提供了回收的时间,因此进程更新后最后反映的结果就是终止效果。
6. 进程互斥锁
进程和线程一样,运行的时候都会出现操作共享资源导致不能按预想的顺序进行运行的情况。因此和线程一样也是可以通过锁机制进行操作,再线程中使用的是threading的lock对象,在进程中使用的是multiprocessing中的lock对象。
from multiprocessing import Process, Lock
import time
class MyProcess(Process):
def __init__(self, loop, lock):
Process.__init__(self)
self.loop = loop
self.lock = lock
def run(self):
for count in range(self.loop):
time.sleep(0.1)
self.lock.acquire()
print(f'Pid: {self.pid} LoopCount: {count}')
self.lock.release()
if __name__ == '__main__':
lock = Lock()
for i in range(10, 15):
p = MyProcess(i, lock)
p.start()
在运行处,对于需要控制的资源进行加锁操作,这样就能保证按顺序执行代码。(原文作者想达到每行输出一句的效果,如果不加锁则会出现一行里有两个结果的情况。)
7. 信号量
考过408的同学或者学过操作系统的同学都记忆犹新,信号量可以用来控制多个进程访问共享资源,还能够限制访问的数量等。在python中用multiprocessing库中的Semaphore来实现信号量。
以下实现了消费者和生产者的案例:
from multiprocessing import Process, Semaphore, Lock, Queue
import time
buffer = Queue(10)
empty = Semaphore(2)
full = Semaphore(0)
lock = Lock()
class Consumer(Process):
def run(self):
global buffer, empty, full, lock
while True:
full.acquire()
lock.acquire()
buffer.get()
print('Consumer pop an element')
time.sleep(1)
lock.release()
empty.release()
class Producer(Process):
def run(self):
global buffer, empty, full, lock
while True:
empty.acquire()
lock.acquire()
buffer.put(1)
print('Producer append an element')
time.sleep(1)
lock.release()
full.release()
if __name__ == '__main__':
p = Producer()
c = Consumer()
p.daemon = c.daemon = True
p.start()
c.start()
p.join()
c.join()
print('Main Process Ended')
8.队列
Queue,这个队列是进程中的共享队列,用于进程之的资源共享。
很重要的一点是如果换成平常的那种list有用吗?当然是否,因为进程之间的资源是不共享的,就算是声明为全局变量都无济于事,因为根本不是一个进程中的,这一点就对线程这种资源共享有用,因此队列的地位是不可取代的。 例子可以参考上一篇中的生产者和消费者问题。
主要使用的两个方法就是队列中的put和get方法,put方法是向队列放入一个元素,get是获取队列的元素,队列的结构就是先进先出的,具体的队列结构详情可以参考网上笔记。
9.管道
队列用于进程之间资源的共享,那么进程之间的通信,比如进程的收发信息。就可以用到pipe管道。当然在操作系统中有学习到很多进程通信的方式,邮件系统或者低级的PV操作等。
管道,我们可以把它理解为两个进程之间通信的通道。管道可以是单向的,即 half-duplex:一个进程负责发消息,另一个进程负责收消息;也可以是双向的 duplex,即互相收发消息。在python中默认声明pipe是双向的,如果要创建单向的,那么在参数deplex为False即可。
代码示例:
from multiprocessing import Process, Pipe
class Consumer(Process):
def __init__(self, pipe):
Process.__init__(self)
self.pipe = pipe
def run(self):
self.pipe.send('Consumer Words')
print(f'Consumer Received: {self.pipe.recv()}')
class Producer(Process):
def __init__(self, pipe):
Process.__init__(self)
self.pipe = pipe
def run(self):
print(f'Producer Received: {self.pipe.recv()}')
self.pipe.send('Producer Words')
if __name__ == '__main__':
pipe = Pipe()
p = Producer(pipe[0])
c = Consumer(pipe[1])
p.daemon = c.daemon = True
p.start()
c.start()
p.join()
c.join()
print('Main Process Ended')
代码中还是以消费者和生产者作为例子,生产者和消费者都是继承自进程类,因此属于两个进程,在主函数中声明了一个管道,并且把管道的两侧分别传入给了两个进程(即声明pipe后,pipe是一个集合,里面包含了pipe[0]和pipe[1]),达到双方通信的效果。启动两个进程,两个进程就可以通过send方法进行发送,用recv方法进行参数的接收。
运行结果如下:
10.进程池
模拟一个场景,有几千的任务来了,但是我只想几个人去做这个任务,那么这几个人就构成了进程池。当然这个池里的负责人是可以增加的。这就是我们进程池的概念,用来控制并发执行的数量。
当然这个任务可以用我们之前的信号量来解决,因为信号量可以规定资源数,以此来控制进程,但是设计的过程非常繁琐。因此在multiprocessing中有个功能Pool即进程池来解决这个问题。
Pool 可以提供指定数量的进程,供用户调用,当有新的请求提交到 pool 中时,如果池还没有满,就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来执行它。
代码如下:
from multiprocessing import Pool
import time
def function(index):
print(f'Start process: {index}')
time.sleep(3)
print(f'End process {index}', )
if __name__ == '__main__':
pool = Pool(processes=3)
for i in range(4):
pool.apply_async(function, args=(i,))
print('Main Process started')
pool.close()
pool.join()
print('Main Process ended')
代码中,声明了一个大小为3的池大小,如果不指定默认自动根据处理器内核来分配进程数。其中的apply_async()方法相当于Process方法(其也是apply方法的异步版本),把任务放入了进程池,其中第一个参数就是要执行的进程代码,args即函数的参数,要用元组传入。
最后结尾要切记把池关闭,否则会有其他新的任务进入池中,调用 join 方法让主进程等待子进程的退出,等子进程运行完毕之后,主进程接着运行并结束。
结果如下:
可以看到,前三个任务中,0号任务结束后,第四号任务才开始。
扩展优化
在以后的爬虫中,我们更多的是对网址进行解析,不断的把一个个元素放入我们的进程中,因此这就可以利用python中的map映射进行实现。其作用就是把第二个参数的列表或者集合里的元素一个个放入参数一的函数中运行。代码示例如下:
from multiprocessing import Pool
import urllib.request
import urllib.error
def scrape(url):
try:
#用于打开一个远程的url连接,并且向这个连接发出请求,获取响应结果。返回的结果是一个http响应对象,
#这个响应对象中记录了本次http访问的响应头和响应体
urllib.request.urlopen(url)
print(f'URL {url} Scraped')
except (urllib.error.HTTPError, urllib.error.URLError):
print(f'URL {url} not Scraped')
if __name__ == '__main__':
pool = Pool(processes=3)
urls=[
'https://www.baidu.com',
'http://www.meituan.com/',
'http://blog.csdn.net/',
'http://xxxyxxx.net'
]
pool.map(scrape,urls)
pool.close()
这个代码只是进行了map方法运用到pool中的演示,其中的urllib库的使用只是随便写写,实际作用不大,其中urlopen方法就是对url发处一个请求,返回的就是一个响应,具体的使用后续会提及。
运行结果如下:
以上是来自崔庆才老师的52讲爬虫课的第一张,希望对大家有帮助。