如果你有数据收集的需求,而且觉得传统的数据收集方法太笨重、繁琐,又或者是想提高python的编程水平,那么来学习爬虫就对了!
爬虫:一段自动抓取互联网信息的程序,从互联网上抓取对于我们有价值的信息。
其实就是一段可以自动收集特定数据的python代码。
爬虫所涉及的知识面也非常广,计算机网络、编程基础、前端开发、后端开发、App开发与逆向、数据分析、机器学习、运维、数据库、网络安全等。
企业为了保护自己的数据不被轻易的爬取,采取了很多反爬虫措施如:JavaScript混淆加密,App加密,增强验证码,封锁IP,封锁账号等,爬虫爬取数据的难度在不断增高。
难度的增加意味着对各位的技术水平就有了更高的要求,JavaScript、App的逆向等几乎已经是爬虫工程师必备的技能。当然如果只是浅尝辄止的了解一下,也就不必过分关注了。
URI和URL
URI 统一资源标志符
URL 统一资源定位符
例如:https://www.runoob.com/html/html-tutorial.html
既是一个URL也是一个URI,用URL/URI来唯一指定它的访问方式,其中包括了访问HTTPS、访问路径(即/html)和资源名称html-tutorial.html。
URN 统一资源名称(只命名资源而不指定如何定位资源,现实中用的很少)
例如:urn:isbn:3513213265指定了一本书的ISBN,可以唯一标识这本书,但是没有指定这本书的访问方式。就好像只告诉你有个宝藏,但不给你藏宝图。
超文本
浏览器里看到的网页就是超文本解析而成的,网页的源代码是一系列的HTML代码
HTTP和HTTPS
在一个链接中例如:
https://www.runoob.com/html/html-tutorial.html
你会看到URL开头有http或https,这个就是访问资源需要的协议类型,还有其他例如:ftp,sftp, smb开头的URL,就表示访问该资源的协议类型为ftp, sftp, smb。
HTTP(超文本传输协议)
用于从网络传输超文本数据到本地浏览器的传送协议,能保证高效而准确的传送超文本文档。
HTTPS
是HTTP的安全版,即HTTP下加入SSL层,通过SSL对数据进行加密传输。
作用:1、建立一个信息安全通道,来保证数据传输的安全
2、确认网站的真实性,凡是使用了HTTPS的网站,都可以通过点击浏览器地址栏的锁头标志来查看网站认证之后的真实信息,也可通过CA机构颁发的安全签章来查询
HTTPS的广泛使用已经是大势所趋
HTTP请求过程
当在浏览器中输入一个URL并按下回车时,就会发生上图的过程。浏览器向该URL所在的服务器发送了一个请求,网站的服务器接收到这个请求后进行处理和解析,然后以HTML的形式返回到浏览器并呈现出来。
为了更好的理解这个过程,请打开你的浏览器,右键任何地方然后点击“检查”,会出现以下界面。
请求组成:请求方法、请求的网址(URL)、请求头(request headers)、请求体(request body)
请求方法:常见有GET和POST
响应组成:响应状态码(response status code)、响应头(response headers)、响应体(response body)
--------------------响应头
connection: 当网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接是否关闭。keep-alive不会关闭(客户端再次访问这个服务器上的网页,会使用这一条已经建立的连接);close表示关闭(客户端再次访问这个服务器上的网页,需要重新建立连接)
content-Type:告知客户端服务器本身响应的对象的类型和字符集
strict-Transport-Security:max-age=172800:基于安全考虑而需要发送的参数详见
Transfer-Encoding:chunked:表示输出的内容长度不能确定详见
-----------------------请求头
Sec-Fetch*请求头详见
Upgrade-Insecure-Requests详见
获取网页源代码-------->提取有效信息---------->保存数据
在成功登录某个网站时,服务器(set-cookie字段)会告诉客户端设置哪些Cookies信息,在后续访问页面时客户端会把Cookies(携带了Session ID信息)发送给服务器,服务器再找到对应的session加以判断,若session中的某些设置登录状态的变量是有效的,就证明用户处于登录状态,此时直接返回登录之后才可以查看的网页内容。
value:为cookies的值,如果是unicode则为字符编码,若是二进制数据则为base64编码。
Expires/Max-Age:cookies实现的时间,Max-Age(单位为秒)若为正数,则cookies在Max-Age秒之后失效,若为负数则关闭浏览器时失效(浏览器也不会保存该cookie)。
Path:设置可以访问该cookie的路径,设置路径后只有该路径可以访问cookie,若设置为跟路径,则本域名下所有页面都可以访问该cookie。
Domain:设置可以访问该cookie的域名。
Size:该cookie的大小
HttpOnly:若为True则只有在http-headers中可以带有此cookie的信息,而不可通过document.cookie来访问此cookie。
常见误区
只要关闭浏览器,Session就消失了?
显然不是,除非程序通知服务器删除一个Session(比如注销操作),否则服务器会一直保留。直到超过设置的Session失效时间,服务器才会删除Session以节省存储空间。
进程:一个可以独立运行的程序单位(例如:打开一个浏览器,就开启了一个浏览器进程)
打开浏览器之后,我们可以同时看视频、听音乐、浏览网页等等,这些任务之间互不干扰,这一个个任务就对应着线程的执行。
线程:是轻量化的进程,是操作系统进行运算调度的最小单位,是进程中的一个最小运行单元。
进程与线程的联系
进程是线程的集合,是由一个或多个线程构成的;线程是进程中的一个最小运行单元。
并发和并行
并发:指同一时刻只能有一条指令执行。但多个线程的对应的指令被快速轮换地执行,宏观上看起来多个线程在同时运行,但微观上只是这个处理器在连续不断的在多个线程之间切换和执行。
并行:指同一时刻有多条指令在多个处理器上同时执行。并行必须依赖多个处理器,不论宏观上还是微观上多个线程都是在同一时刻一起执行。
应用场景
网络爬虫是一个典型的例子,爬虫在向服务器发起请求后,有一段时间必须要等待服务器的响应返回,这种任务属于IO密集型任务,我们可以在等待的时间去运行其他线程(做其他事);还有一种任务的运行一直需要处理器的参与,叫做计算密集型任务。如果不全是计算密集型任务,尤其是IO密集型任务(像爬虫),多线程可以大大提高程序运行效率。
python中的多线程
threading模块的应用
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') #current_thread().name获取当前线程名称
for i in [1,5]: #循环创建两个子线程,依此实现休眠1秒,5秒。
t = threading.Thread(target=target, args=[i])
t.start() #开启线程
t.join() #让主线程等待子线程运行完之后再结束
print(f'Threading {threading.current_thread().name} is ended')
可以看出上述代码创建了三个进程分别是主线程MainThread,两个子线程Thread-1、Thread-2。join方法的作用是阻塞,等待子线程结束,join方法有一个参数是timeout,即如果主线程等待timeout,子线程还没有结束,则主线程强制结束子线程。如果不加入t.join()则主线程就会提前结束,如下图:
守护线程
之所以是守护,是因为永远当不了主角。守护线程会随着主线程的结束而结束,不管守护线程是否运行完。当然,如果用join函数,主线程会等待子线程(包括守护线程)运行完后再结束。
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')
t1 = threading.Thread(target=target, args=[2])
t1.start()
t2 = threading.Thread(target=target, args=[5]) #创建一个守护线程t2
t2.setDaemon(True)
t2.start()
print(f'Threading {threading.current_thread().name} is ended')
如上图所示,当主线程结束后,并没有打印出守护线程(Thread-2)结束的消息。说明Thread-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}')
按常理count的值应该等于1000才对,为什么远小于1000呢?而且每次运行结果都不一样?
这是因为同一个进程中的多个线程是共享资源的,上述代码中多个线程共享全局变量count,而这些线程中有一些线程是并发或并行操作的,即同一时间会有多个线程同时运行(取得同一个count值),所以导致count+1操作失效,从而count值远小于1000 。
解决这个问题-------->加锁
原理:某个线程再对数据进行操作前,需要先加锁,这样其他的线程发现被加锁了之后,就无法继续向下执行,会一直等待锁被释放。只有加锁的线程把锁释放了,其他的线程才能继续加锁并对数据做修改,修改完了再释放锁这样可以确保同一时间只有一个线程操作数据,多个线程不会再同时读取和修改同一个数据,最后的运行结果就是对的了。
import threading
import time
count = 0
class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
global count
lock.acquire() #上锁
temp = count + 1
time.sleep(0.001)
count = temp
lock.release() #解锁
lock = threading.Lock() #创建一个互斥锁
threads = []
for _ in range(1000):
thread = MyThread()
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print(f'Final count: {count}')
这时结果就正常了,互斥锁的原理换句话说就是,上了互斥锁的程序块就变成了一个互斥资源(即同一时间只有一个线程可以访问),有效避免了多个线程同时读取和修改互斥锁里的同一个数据。
补充:在python中有GIL(全局解释器锁)的存在,所以在一个python进程的多线程下每个线程的执行方式是:获取GIL------>执行对应线程的代码------>释放GIL。也就是说,在python进程中同一时间只能有一个线程运行,但不会允许一个线程独占系统资源,所以会在多个线程之间来回快速切换(实现伪并行,其实是并发)。
多进程就是启用多个进程同时运行。
每个python进程中都有一个自己的GIL,所以多个进程同时运行会真正实现并行操作。相比多线程,多进程会更快。但是,多进程之间资源不共享,需要用一个独立的机制来共享全局变量。
multiprocessing模块
直接使用process类方式
在multiprocessing中,每个进程都用一个Process类来表示。
API调用:Process(group, target, name, args, kwargs)
import multiprocessing
def process(index):
print(f'Process: {index}')
if __name__=='__main__':
for i in range(5): #循环创建5个进程
p = multiprocessing.Process(target=process,args=(i,))
p.start()
import multiprocessing
import time
def process(index):
time.sleep(index)
print(f'Process: {index}')
if __name__ == '__main__':
for i in range(5):
p = multiprocessing.Process(target=process, args=[i])
p.start()
print(f'CPU number: {multiprocessing.cpu_count()}') #cpu_count()获取当前机器的cpu核心数量
for p in multiprocessing.active_children(): #active_children()获取当前还在运行的所有进程
print(f'Chlid process name: {p.name} id: {p.pid}')
print('Process Ended')
继承Process类方式
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self, loop):
Process.__init__(self) #创建进程
self.loop = loop #将loop设置为全局变量
def run(self):
for count in range(self.loop): #创建的三个子进程,loop的值依次为2,3,4
time.sleep(1)
print(f'Pid: {self.pid} LoopCount:{count}')
if __name__ == '__main__':
for i in range(2, 5): #循环创建三个进程
p = MyProcess(i)
p.start()
守护进程
与守护线程类似,若一个进程被设置为守护进程(通过daemon属性设置),当父进程结束后,子进程会自动被终止。
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.daemon = True #设置子进程全为守护进程
p.start()
print('Main Process ended')
进程等待
使用join方法,与线程类似。
join()也可传入参数,如join(3)表示主进程最长等待时间为3秒,防止子进程进入死循环等待时间过长。
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__':
processes = []
for i in range(2, 5):
p = MyProcess(i)
processes.append(p)
p.daemon = True
p.start()
for p in processes:
p.join() #设置进程等待
print('Main Process ended')
如图,主进程等待子进程运行完之后才结束。
终止进程
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.join()
p.terminate()
print('Terminate:', p, p.is_alive())
p.join()
print('Joined:', p, p.is_alive())
进程互斥锁
无锁时
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.lockrelease() #解锁
if __name__ == '__main__':
lock = Lock() #创建一个互斥锁
for i in range(10, 13):
p = MyProcess(i, lock)
p.start()
多个进程同时运行,导致同时输出(同时调用print语句),出现输出不换行。
注:关于multiprocessing的信号量模块将在下一次更新,感谢大家阅读。