学习爬虫之前必知必会(http原理,爬虫原理,进程和线程详解)

5 篇文章 0 订阅

学习爬虫之前必知必会

​ 如果你有数据收集的需求,而且觉得传统的数据收集方法太笨重、繁琐,又或者是想提高python的编程水平,那么来学习爬虫就对了!


神魔是爬虫?

爬虫:一段自动抓取互联网信息的程序,从互联网上抓取对于我们有价值的信息。

其实就是一段可以自动收集特定数据的python代码。

爬虫所涉及的知识面也非常广,计算机网络、编程基础、前端开发、后端开发、App开发与逆向、数据分析、机器学习、运维、数据库、网络安全等。

爬虫的现状

​ 企业为了保护自己的数据不被轻易的爬取,采取了很多反爬虫措施如:JavaScript混淆加密,App加密,增强验证码,封锁IP,封锁账号等,爬虫爬取数据的难度在不断增高。

2e5a6e7e2bd40234197c8121742160da

​ 难度的增加意味着对各位的技术水平就有了更高的要求,JavaScript、App的逆向等几乎已经是爬虫工程师必备的技能。当然如果只是浅尝辄止的了解一下,也就不必过分关注了。

基础知识

http的基本原理

  • URI和URL

    image-20201104203707209

    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请求过程

    image-20201104211419710

    ​ 当在浏览器中输入一个URL并按下回车时,就会发生上图的过程。浏览器向该URL所在的服务器发送了一个请求,网站的服务器接收到这个请求后进行处理和解析,然后以HTML的形式返回到浏览器并呈现出来。

    ​ 为了更好的理解这个过程,请打开你的浏览器,右键任何地方然后点击“检查”,会出现以下界面。

    image-20201104213859241

    请求组成:请求方法、请求的网址(URL)、请求头(request headers)、请求体(request body)

    请求方法:常见有GET和POST

    • GET:在浏览器中直接输入URL并回车,便发起了一个GET请求,请求的参数会直接包含在URL中,长度最大1024B。
    • POST:请求大多在表单提交时发起,包含在请求体中,不会出现在URL中,长度没有限制。

    响应组成响应状态码(response status code)、响应头(response headers)、响应体(response body)

    • 响应体:包含响应的正文数据,例请求网页时,它的响应体是html代码;请求一张图片时,它的响应体是图片的二进制数据。

    --------------------响应头

    image-20201105161845591

    connection: 当网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接是否关闭。keep-alive不会关闭(客户端再次访问这个服务器上的网页,会使用这一条已经建立的连接);close表示关闭(客户端再次访问这个服务器上的网页,需要重新建立连接)

    content-Type:告知客户端服务器本身响应的对象的类型和字符集

    image-20201106172824092

    strict-Transport-Security:max-age=172800:基于安全考虑而需要发送的参数详见

    Transfer-Encoding:chunked:表示输出的内容长度不能确定详见

    -----------------------请求头

    image-20201106165512525

    Sec-Fetch*请求头详见

    Upgrade-Insecure-Requests详见

爬虫基本原理

获取网页源代码-------->提取有效信息---------->保存数据

  • 获取网页源代码,关键就是构造一个请求并发送给服务器,然后接收到响应并将其解析出来(python的urlib、requests库可以实现此操作)
  • 提取有效信息,最通用的方法就是采用正则表达式提取,但构造正则表达式时比较复杂且容易出错;也可以用根据网页节点属性、css选择器、XPath来提取网页信息的库(例如:Beautiful Soup、pyquery、lxml等)
  • 保存数据,保存形式多样,如保存为txt文本或json文本,也可以保存到数据库如mysql和MongDB 等,也可以保存至远程服务器,如借助SFTP操作等。

Session和Cookies

  • Session(会话):本身含义是指有始有终的一系列动作。如打电话,从拿起电话拨号到挂断电话这中间的一系列过程可以称为一个Session。在web中存在于服务器端,网站的服务器保存用户的session信息
  • Cookies:在客户端,浏览器在下次访问页面时会自动附带上它发送给服务器,服务器通过识别cookies并鉴定出是哪个用户,然后再判断用户是否是登录状态,进而返回对应的响应。换句话说,是指某些网站为了辨别用户身份、进行Session跟踪而存储在用户本地终端上的数据。

在成功登录某个网站时,服务器(set-cookie字段)会告诉客户端设置哪些Cookies信息,在后续访问页面时客户端会把Cookies(携带了Session ID信息)发送给服务器,服务器再找到对应的session加以判断,若session中的某些设置登录状态的变量是有效的,就证明用户处于登录状态,此时直接返回登录之后才可以查看的网页内容。

image-20201107103122112

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')
    

    image-20201107141847950

    ​ 可以看出上述代码创建了三个进程分别是主线程MainThread,两个子线程Thread-1、Thread-2。join方法的作用是阻塞,等待子线程结束,join方法有一个参数是timeout,即如果主线程等待timeout,子线程还没有结束,则主线程强制结束子线程。如果不加入t.join()则主线程就会提前结束,如下图:

    image-20201107143240440

    • 守护线程

      ​ 之所以是守护,是因为永远当不了主角。守护线程会随着主线程的结束而结束,不管守护线程是否运行完。当然,如果用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')
      

      image-20201107151514422

      ​ 如上图所示,当主线程结束后,并没有打印出守护线程(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}')
      

      image-20201107162717977

      按常理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}')
        

        image-20201107165656439

        ​ 这时结果就正常了,互斥锁的原理换句话说就是,上了互斥锁的程序块就变成了一个互斥资源(即同一时间只有一个线程可以访问),有效避免了多个线程同时读取和修改互斥锁里的同一个数据。

        补充:在python中有GIL(全局解释器锁)的存在,所以在一个python进程的多线程下每个线程的执行方式是:获取GIL------>执行对应线程的代码------>释放GIL。也就是说,在python进程中同一时间只能有一个线程运行,但不会允许一个线程独占系统资源,所以会在多个线程之间来回快速切换(实现伪并行,其实是并发)。

多进程(多路加速)

多进程就是启用多个进程同时运行。

每个python进程中都有一个自己的GIL,所以多个进程同时运行会真正实现并行操作。相比多线程,多进程会更快。但是,多进程之间资源不共享,需要用一个独立的机制来共享全局变量。

  • multiprocessing模块

    image-20201107172654101
    • 直接使用process类方式

      在multiprocessing中,每个进程都用一个Process类来表示。

      API调用:Process(group, target, name, args, kwargs)

      • target: 调用对象,可以传入方法名字
      • args:表示被调用对象的位置参数元组
      • kwargs:表示调用对象的字典
      • name:别名,相当于给该进程取一个名字
      • group:分组
      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()
      

      image-20201107193956430

      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')
      

      image-20201107195043141

    • 继承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()
      

      image-20201107205134015

    • 守护进程

      与守护线程类似,若一个进程被设置为守护进程(通过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')
      

      image-20201107212628430

    • 进程等待

      使用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')
      
      image-20201107213526816

      如图,主进程等待子进程运行完之后才结束。

    • 终止进程

      • 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.join()  
          
          p.terminate()
          print('Terminate:', p, p.is_alive())
          
          p.join()
          print('Joined:', p, p.is_alive())
      
      image-20201107215756830
    • 进程互斥锁

      • 无锁时

        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()
        
        image-20201107222954919

        多个进程同时运行,导致同时输出(同时调用print语句),出现输出不换行。

        • 加锁解决问题

          去掉上方代码的两行注释,结果如下:

          image-20201107223346247

          如图所示输出正常。同线程互斥锁类似,上了互斥锁的程序块就变成了一个互斥资源(即同一时间只有一个进程可以访问),即同一时刻只有一个进程可以输出。

        注:关于multiprocessing的信号量模块将在下一次更新,感谢大家阅读。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值