本笔记是学习崔庆才老师的网络爬虫课程的总结
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
网页的html代码。
HTTP: Hyper Text Transfer Protocol,中文名叫作超文本传输协议,HTTPS 的全称是 Hyper Text Transfer Protocol over Secure Socket Layer。
这是两个协议头,https是在http上做的优化。其目的就是加强安全性,在HTTP下加入了SSL层,所以称为HTTPS。在有SSL的安全基础上,通过它传输的内容都是经过SSL加密的,其作用 如下:
客户端发送请求(Request)给服务器,服务器响应(Response)客户端请求的这么一个过程,在这个过程中,会请求许多的资源用于页面的渲染,功能的完善等。在电脑端可以通过f12中的network查看请求与响应,其中包含了各种资源的请求和响应。
主要说明说Web网页的三大基础技术html、css、js,这三个部分主要记录下选择器,不常使用总容易忘。
了解html结构的应该挺容易理解这块,所有标签定义的内容都是节点,可以被不断的包裹起来,形成一棵html dom树。形成这样的规范其实就是说明了页面中的每一个节点都能够被做出响应的修改。因此通过选择器,选择到了这个节点后,可以通过代码对网页做出修改。
实际上就是一个获取页面并提取和保存信息的自动化程序。
就像之前提到的,我们需要去解析的是响应回来的信息,我们可以通过模拟浏览器的请求然后获得网页信息
我们获取数据后,我们需要去进行保存。保存的形式有各种各样的,可以是txt或者json文件。当然我们也可以保存到数据库中。
静态网页:网页的内容是 HTML 代码编写的,文字、图片等内容均通过写好的 HTML 代码来指定,这种页面叫作静态网页。其优点就是速度快,简单,但是可维护性很差,而且不美观。
动态网页:动态解析 URL 中参数的变化,关联数据库并动态呈现不同的页面内容,非常灵活多变。
无状态的含义是:指 HTTP 协议对事务处理是没有记忆能力的,也就是说服务器不知道客户端是什么状态。
此前我们了解到,客户端和服务器通过请求和响应进行联系,由于http协议是无状态的,因此它的记忆仅是一次请求和响应,此后就什么也不记得了。为了保持他的记忆功能,应运而生的就是Session和Cookies。
Cookies 指某些网站为了辨别用户身份、进行 Session 跟踪而存储在用户本地终端上的数据。
客户端第一次访问服务器时,服务器会返回一个响应头中带有Set-Cookie的字段给客户端,用来标记一些用户信息,这里面就带有了关于Session的相关信息。下一次客户端请求这个服务器的时候,客户端的请求头就会带上Cookies交给服务器,服务器读取了其中的SeesionID信息,在服务器端就可以判断用户的状态,到底时正常访问还是过期。通过Cookies和Session的搭配,Cookies在客户端,Session在服务端,两者协作负责两头,这样就实现了Session控制。
在浏览器中按f12,进入Application选项卡,在下方就能看到Cookies的标志,里面的一条条就是Cookie。
有分类指Cookie有会话和持久之分,即会话Cookie就是浏览器关闭后就失效,而持久Cookie会把信息保存在客户端的磁盘中,以此达到持久化的效果。实际上Cookie的时间是通过Max Age和Expires字段决定过期时间的。通过设置很久的过期时间达到永久生效的效果。
常说Session就存活于一次会话中,浏览器关闭后就消失了,但是用户信息这种事情服务器怎么可能会轻易删除。服务器通过Cookies来保存SessionID信息,这样客户端和服务器断开连接后,当再此连接的时候,请求头带有Cookies信息,里面的SessionID对应上服务器信息后就再次识别出这个Session了。Session的消失也是有一个失效时间的,超过了时间才会删除,以此来节省服务器的空间。
在python中多线程的模块是threading,是python自带的模块。
#例子来自于催庆才老师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方法创建了线程,传入的参数为了让线程休眠,达到不同的执行顺序。随后通过start方法进行了线程的启动。从代码知道,这种方式会产生主线程结束,子线程还没有结束的情况,因为子线程被休眠后没有主线程结束的快。结果如下:
如果要达到子线程在主线程之前结束,我们就要使用到线程中的join方法。即让线程有了顺序。
join() 方法的功能是在程序指定位置,优先让该方法的调用者使用 CPU 资源。实际上就是可以控制执行顺序,只要把线程进行了join操作就会优先执行这个线程。
在例子中,我们把线程一加入,在加入线程二到threads这个列表中。在后续遍历的时候先拿到了Thread1,执行join方法,因此线程一先执行,再执行线程二。
结果如下:
当然,如果不想一直等一个线程结束,那么join()是可以添加参数控制一个线程执行的时间长短的:timeout参数,默认不设置。如果设置了线程没有在规定的时间结束的话,那么就会被强制结束。
这样就实现了主线程再最后才退出。
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()方法进行运行。其他就是同直接创建类似。
守护线程的作用就是:如果主线程结束了而该守护线程还没有运行完,那么它将会被强制结束。
这个在扩展Thread()方法的时候就提及,也可以通过后续setDaemon方法进行设置。
在一个例子中对线程二设置为守护线程后,执行结果如下:
即原本主线程结束后,线程1和线程2会继续执行,但是由于线程2为守护线程,因此也退出了。
当然,如果加入了join方法,那么还是会按顺序先执行线程1和线程2才会结束主线程。
用一个代码来介绍这个知识点:
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()方法进行锁的释放。
python的多线程受制于GIL(Global Interpreter Lock,即全局解释器锁),目的是为了安全,这就导致不论是在单核还是多核条件下,在同一时刻只能运行一个线程,导致 Python 多线程无法发挥多核并行的优势。
这就相当于给python的线程上锁了,python要执行多线程,线程需要先获取GIL,然后直线代码,然后释放GIL。但是这个点对爬虫这种IO密集其实影响不大。
前面提到,python的多线程其实会受GIL影响。一个进程包含多线程,每一个进程都有一个自己的GIL。所以在多核的情况下,多进程的运行是不受GIL影响的。虽然爬虫这种IO密集型任务其实多线程和多进程差别不大,但是如果能用多进程就多用多进程。
线程的实现依靠threading库,进程的实现也是python中有内置库:multiprocessing。其提供了一些组件:如 Process(进程)、Queue(队列)、Semaphore(信号量)、Pipe(管道)、Lock(锁)、Pool(进程池)等。这些都可以在操作系统这门课学习到。
多进程的实现和线程类似,也是有两种实现方式,一种直接使用,
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.BaseProcess,所以它的init初始化方法在BaseProcess中,自己并没有设置:
这个类内部自己就说了进程类似与线程。因此里面的参数其实都差不多,不再赘述,这里args必须是元组,在底层也会帮忙进行转换。
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里重新进行一次初始化,否则会报参数丢失的错误。
和线程一样,进程也可以设置守护进程,因为进程也有子与夫的关系。父进程结束后,子进程也会被强制结束。
语法为:进程.daemon = True。也可以直接在初始化的时候设置为True。
进程的等待其实就是想要按顺序的执行进程,设置了守护进程就会导致子进程无法执行就结束,那么想要子进程运行完结束,或者就是控制进程执行的顺序。是不是和线程的一个方法很类似?
是的,使用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')
目前我们知道的线程终止可以通过守护进程,或者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操作给进程提供了回收的时间,因此进程更新后最后反映的结果就是终止效果。
进程和线程一样,运行的时候都会出现操作共享资源导致不能按预想的顺序进行运行的情况。因此和线程一样也是可以通过锁机制进行操作,再线程中使用的是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()
在运行处,对于需要控制的资源进行加锁操作,这样就能保证按顺序执行代码。(原文作者想达到每行输出一句的效果,如果不加锁则会出现一行里有两个结果的情况。)
考过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')
Queue,这个队列是进程中的共享队列,用于进程之的资源共享。
很重要的一点是如果换成平常的那种list有用吗?当然是否,因为进程之间的资源是不共享的,就算是声明为全局变量都无济于事,因为根本不是一个进程中的,这一点就对线程这种资源共享有用,因此队列的地位是不可取代的。 例子可以参考上一篇中的生产者和消费者问题。
主要使用的两个方法就是队列中的put和get方法,put方法是向队列放入一个元素,get是获取队列的元素,队列的结构就是先进先出的,具体的队列结构详情可以参考网上笔记。
队列用于进程之间资源的共享,那么进程之间的通信,比如进程的收发信息。就可以用到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方法进行参数的接收。
模拟一个场景,有几千的任务来了,但是我只想几个人去做这个任务,那么这几个人就构成了进程池。当然这个池里的负责人是可以增加的。这就是我们进程池的概念,用来控制并发执行的数量。
当然这个任务可以用我们之前的信号量来解决,因为信号量可以规定资源数,以此来控制进程,但是设计的过程非常繁琐。因此在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讲爬虫课的第一张,希望对大家有帮助。