爬虫

  • 1:爬虫基础入门
    • 1-1:爬虫简介
    • 1-2:爬虫是违法的吗
    • 1-3:http和https协议
    • 1-4:第一个爬虫程序-requests模块
    • 1-5:乱码问题
  • 2:requests模块深入学习
    • 2-1:图片和视频数据的爬取
    • 2-2:传参
    • 2-3:UA伪装
    • 2-4:UA伪装头部信息模块UserAgent
    • 2-5:cookie
    • 2-6:代理
    • 2-7:验证码
  • 3:巩固练习
    • 3-1:爬取豆瓣电影分类排行榜 - 情色片
    • 3-2:获取淘宝首页的搜索提示
    • 3-3:获取我房网新房列表的价格走势数据
    • 3-4:获取爱奇艺用户的播放记录
  • 4:数据解析
    • 4-1:json数据
      • 4-1-1:json简介
      • 4-1-2:json模块的使用
        • 4-1-2-1:dumps和loads
        • 4-1-2-2:dump和load
        • 4-1-2-3:json的中文显示
      • 4-1-3:注意事项
    • 4-2:正则
    • 4-3:bs4
    • 4-4:xpath
    • 4-5:pyQuery
  • 5:爬虫进阶篇-多进程与多线程
    • 5-1:先看一个深刻的例子,爬取某视频的音乐排行榜视频(反爬虫套路特别多)
    • 5-2:进程和线程的简介
    • 5-3:进程和线程的区别
    • 5-4:提高爬虫效率的方式
      • 5-4-1:多进程
      • 5-4-2:进程池
        • 5-4-2-1:方式1:from multiprocessing import Pool
        • 5-4-2-2:方式2:from concurrent.futures import ProcessPoolExecutor
      • 5-4-2:多线程
      • 5-4-4:线程池
  • 6:爬虫进阶篇-协程
    • 6-1:协程的概念
    • 6-2:实现协程的方式
      • 6-2-1:greenlet
      • 6-2-2:yield
        • 6-2-2-1:什么是generator
        • 6-2-2-2:为什么要用generator
        • 6-2-2-3:yield到底是什么
        • 6-2-2-4:自定义生成器generator
        • 6-2-2-5:yield实现协程
      • 6-2-3:asyncio装饰器
      • 6-2-4:async、await关键字
        • 6-2-4-2:async关键字
        • 6-2-4-3:await关键字
        • 6-2-4-4:Task对象
        • 6-2-4-5:Future对象
    • 6-3:aiohttp,基于异步网络请求的模块
  • 7:爬虫进阶篇-selenium
    • 7-1:selenium入门例子
    • 7-2:selenium其他自动化操作
    • 7-3:iframe
    • 7-4:动作链,如滑块验证码
    • 7-5:无头浏览器界面+规避检测
  • 8:爬虫高级篇-scrapy爬虫框架

 

前提:本篇文章的前提是你要会python基本语法,并且已经安装好了pycharm,我使用的是python3.8版本

声明:本篇博客纯属教学,都是合法数据,涉及的网站例子如介意,请联系删除

1:爬虫基础入门

1-1:爬虫简介

       网络爬虫(又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。

       所谓爬虫,就是通过编写程序,模拟浏览器上网,然后让其去互联网上抓取数据的过程

       抓取互联网上的数据,为我所用,有了大量的数据,就如同有了一个数据银行一样,可以使用这些数据做一些产品,不如最漂亮的美女图片网,技术文章网,搞笑故事APP等等

       爬虫可分为3类

  • 通用爬虫:抓取系统重要组成部分,抓取的是一整张页面数据
  • 聚焦爬虫:是建立在通用爬虫的基础上,抓取的是页面中特定的局部内容
  • 增量式爬虫:检测网站中数据更新的更新,只会抓取网站中最新 更新出来的数据

1-2:爬虫是违法的吗

       在探究爬虫额合法性之前,我们先介绍一个君子协议:robots.txt协议,这个协议几乎每个网站都有。

       robots.txt文件是一个文本文件,使用任何一个常见的文本编辑器,比如Windows系统自带的Notepad,就可以创建和编辑它。robots.txt是一个协议,而不是一个命令。robots.txt是搜索引擎中访问网站的时候要查看的第一个文件。robots.txt文件告诉蜘蛛程序在服务器上什么文件是可以被查看的。

       当一个搜索蜘蛛访问一个站点时,它会首先检查该站点根目录下是否存在robots.txt,如果存在,搜索机器人就会按照该文件中的内容来确定访问的范围;如果该文件不存在,所有的搜索蜘蛛将能够访问网站上所有没有被口令保护的页面。百度官方建议,仅当您的网站包含不希望被搜索引擎收录的内容时,才需要使用robots.txt文件。如果您希望搜索引擎收录网站上所有内容,请勿建立robots.txt文件。

       如果将网站视为酒店里的一个房间,robots.txt就是主人在房间门口悬挂的“请勿打扰”或“欢迎打扫”的提示牌。这个文件告诉来访的搜索引擎哪些房间可以进入和参观,哪些房间因为存放贵重物品,或可能涉及住户及访客的隐私而不对搜索引擎开放。但robots.txt不是命令,也不是防火墙,如同守门人无法阻止窃贼等恶意闯入者。

       首先要认识User-agent、Disallow、Allow是什么意思:

  • User-agent表示定义哪个搜索引擎,如User-agent:Baiduspider,定义百度蜘蛛;
  • Disallow表示禁止访问;
  • Allow表示运行访问;

       一个网站的robots.txt打开方式是其官网域名地址加上robots.txt

       比如CSDN
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析
       又如百度
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_02

为什么说是君子协议

       因为robots.txt只是一个文本,只是明确指出网站上那些 东西可爬,那些不可爬,但是不可爬的那些也无法阻止你真正的去爬取里面的数据,所以说是君子协议,防君子不防小人

       所以就有了反爬机制和反反爬机制

反爬机制:

  • 为了防止爬虫爬取到关键的隐私数据,各个网站制定的策略

反反爬机制

  • 为了破解各个网站的反爬机制而制定的各种策略

       了解了君子协议之后,我们明白了,爬虫在法律中是不被禁止的

  • 君子协议允许爬取的数据,我们爬取之后不违法
  • 我们不能恶意攻击,干扰了被访问网站的正常运营
  • 君子协议不允许爬取的数据,涉及到商业机密或者是受法律保护的特性类型的数据或信息,爬取之后是违法的,要负相应的法律责任

1-3:http和https协议

http协议

       http是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII码形式给出;而消息内容则具有一个类似MIME的格式。这个简单模型是早期Web成功的有功之臣,因为它使开发和部署非常地直截了当。

https

       HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性 。HTTPS 在HTTP 的基础下加入SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。 HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP 之间)。这个系统提供了身份验证与加密通讯方法。它被广泛用于万维网上安全敏感的通讯,例如交易支付等方面。

https协议的加密方式(想深入的了解可以自行查找资料)

  • 对称密钥加密
  • 非对称密钥加密
  • 证书密钥加密

       无论是http协议还是https协议,都是请求与响应

常用的请求头信息:

  • User-Agent:请求载体的身份标识
  • Connection:请求完毕后,是否断开连接还是保持连接

常用的响应头信息:

  • Content-Type:服务器响应回客户端的数据类型,如:text/plain;application/json等等

       比如我们使用谷歌浏览器访问百度首页
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_03
       再如我们使用火狐浏览器访问CSDN的周排行
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_04

1-4:第一个爬虫程序-requests模块

       从上面的简介中,我们已经知道爬虫本质就是使用程序编码替代浏览器访问网络资源获取响应的数据信息

       python为我们提供了2个模块来抓取网络数据,urllib模块和requests模块,相比之下,requests模块的使用简单的要多,所以我们采用requests模块来演示

       要使用requests模块,首先得安装这个模块

  • pip install requests

使用requests模块的简单编码流程

  • 自动url
  • 发起请求
  • 获取响应数据
  • 持久化存储

       下面我们使用requests模块来访问百度首页

# -*- coding: utf-8 -*-

# 导入requests模块
import requests

if __name__ == "__main__":
   # 指定url
   get_url="https://www.baidu.com"
   # 发起请求,返回一个响应对象
   response = requests.get(url=get_url)
   # 设置响应编码,免得中文乱码
   response.encoding = 'utf-8'
   # 获取响应数据,
   page_text = response.text
   # 把数据保存在my_bai_du.html中
   with open('./my_bai_du.html', 'w', encoding='utf-8') as fp:
       fp.write(page_text)

python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_05

1-5:乱码问题

response.text和response.content的 区别

  • response.text返回的类型是str
  • response.content返回的类型是bytes,可以通过decode()方法将bytes类型转为str类型
  • 推荐使用:response.content.decode()的方式获取相应的html页面

扩展理解

response.text

  • 解码类型:根据HTTP头部对响应的编码做出有根据的推测,推测的文本编码
  • 如何修改编码方式:response.encoding = ‘gbk’

response.content

  • 解码类型:没有指定
  • 如何修改编码方式:response.content.decode(‘utf8’)

       如我们上面的例子可以改为

# -*- coding: utf-8 -*-

# 导入requests模块
import requests

if __name__ == "__main__":
   # 指定url
   get_url="https://www.baidu.com"
   # 发起请求,返回一个响应对象
   response = requests.get(url=get_url)
   # 获取响应数据,
   page_content = response.content.decode('utf8')
   # 把数据保存在my_bai_du.html中
   with open('./my_bai_du2.html', 'w', encoding='utf-8') as fp:
       fp.write(page_content)

       结果是一样的
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_06

       如果以上的问题都没有解决,那么就对我们特定的乱码数据data=data.encode(‘iso-8859-1’).decode(‘gbk’)

2:requests模块深入学习

       我们可以看到requests模块的请求中,最终的方法里可以传递各种参数
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_07

2-1:图片和视频数据的爬取

       我们知道,网页上的图片也是一个链接,所以我们爬取图片数据,其实也是访问图片链接下载下来

       我们网上找一个美女图片
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_08

       接着就开弄吧

# -*- coding: utf-8 -*-

import requests

if __name__ == "__main__":

   # 图片链接
   url="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1608264646866&di=c80756b05ed2147e5162e07f1a97b785&imgtype=0&src=http%3A%2F%2Finews.gtimg.com%2Fnewsapp_match%2F0%2F10634033150%2F0.jpg"
   response = requests.get(url=url)
   # 因为图片是二进制形式的,所以使用content
   img_data = response.content
   # wb表示以二进制的形式写入
   with open('../image/mei_nv.jpg','wb') as wp:
       wp.write(img_data)

       爬取后的图片就在/image/下的mei_nv.jpg
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_09

       同样的,视频也是一样的原理,我们找一个视频,不如我找冯提莫唱的《他不爱我》视频
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_10
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_11

       开搞

# -*- coding: utf-8 -*-

import requests

if __name__ == "__main__":

   # 视频链接
   url="https://ugcws.video.gtimg.com/uwMROfz2r57IIaQXGdGnCmdeB3eAEbQczPtowErLZ6kvPmTO/szg_41485149_50001_faf5f8ef210447878f98212a76d0c39c.f622.mp4?sdtfrom=v1010&guid=3a4bd3278692a44ca0baaf89bc1826ec&vkey=B5759EBA404F5A4E738F42D51D518A37C3B35D0D22BC09129DDAA8FD9F3076EA86A75391BE94300827476F985EA085854C761BC46BB04C0921C60DD473D6DF289318C473BF399D088566E1CA537D2DE93804D17D39BF753A57AC841D2267FE822A2D6F538071BDC8606F85425EBBE811104A51BA85182223B767351108CD7DDC"
   response = requests.get(url=url)
   # 因为视频是二进制形式的,所以使用content
   img_data = response.content
   with open('../image/视频.mp4','wb') as wp:
       wp.write(img_data)

       结果的得到我们的视频
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_12

2-2:传参

       模拟百度搜索

       我们模拟一下百度搜索的http请求,这个就需要传参,http://www.baidu.com/s?wd=林高禄
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_13

       这个就需要我们传递参数,我可以直接requests.get(’http://www.baidu.com/s?wd=林高禄‘)得到结果,但是如果参数多的时候,我们不建议这样做,我们可以用一个字典封装参数传递进去

# -*- coding: utf-8 -*-

# 导入requests模块
import requests

if __name__ == "__main__":
   # 指定url
   get_url="http://www.baidu.com/s"
   # 封装参数,上面的url中?可以不要,发起请求的时候会自动加上
   params={
       'wd':'林高禄'
   }
   # 发起请求,返回一个响应对象
   response = requests.get(url=get_url,params=params)
   # 获取响应数据,
   page_content = response.content.decode('utf8')
   print(page_content)
   # 把数据保存在my_bai_du.html中
   with open('./my_bai_du.html', 'w', encoding='utf-8') as fp:
       fp.write(page_content)

       得出的结果是一样的
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_14

2-3:UA伪装

       基于上面的模拟百度搜索传参,我们使用的是http协议,如果改成https协议,我么再试一下

       浏览器访问是可以的
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_15

       但是我们使用代码爬取的时候,是不行的

# -*- coding: utf-8 -*-

# 导入requests模块
import requests

if __name__ == "__main__":
   # 指定url
   get_url="https://www.baidu.com/s"
   # 封装参数,上面的url中?可以不要,发起请求的时候会自动加上
   params={
       'wd':'林高禄'
   }
   # 发起请求,返回一个响应对象
   response = requests.get(url=get_url,params=params)
   # 获取响应数据,
   page_content = response.content.decode('utf8')
   print(page_content)
   # 把数据保存在my_bai_du.html中
   with open('./my_bai_du.html', 'w', encoding='utf-8') as fp:
       fp.write(page_content)

       运行看输出结果,得不到真实的数据
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_16

       这就需要我们进行UA伪装,所谓UA伪装就是User-Agent,请求载体的身份标识,让我们的爬虫伪装成正常的请求载体的请求
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_17

       明白的UA伪装后,我们就在我们的代码基础上加入请求头headers ,进行UA伪装

# -*- coding: utf-8 -*-

# 导入requests模块
import requests

if __name__ == "__main__":
   # 指定url
   get_url="https://www.baidu.com/s"
   # UA伪装
   headers = {
       'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) >Chrome/86.0.4240.193 Safari/537.36'
   }
   # 封装参数,上面的url中?可以不要,发起请求的时候会自动加上
   params={
       'wd':'林高禄'
   }
   # 发起请求,返回一个响应对象
   response = requests.get(url=get_url,params=params,headers=headers)
   # 获取响应数据,
   page_content = response.content.decode('utf8')
   print(page_content)
   # 把数据保存在my_bai_du.html中
   with open('./my_bai_du.html', 'w', encoding='utf-8') as fp:
       fp.write(page_content)

2-4:UA伪装头部信息模块UserAgent

       上面的UA伪装头部信息User-Agent是我们从抓包工具中复制出来的,也就是每次请求都是固定的浏览器载体标识,请求次数过多还是会被服务器察觉,所以我们使用伪装模块UserAgent,来生成我们的伪装

       要使用UserAgent模块,得安装fake_useragent模块,因为UserAgent是在fake_useragent下的字模块

  • pip install fake_useragent

       我们演示一下UserAgent模块
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_18

# -*- coding: utf-8 -*-
from fake_useragent import UserAgent

# 实例化 UserAgent 类
ua = UserAgent()
# 对应浏览器的头部信息
print(ua.ie)
print(ua.internetexplorer)
print(ua.opera)
print(ua.chrome)
print(ua.firefox)
print(ua.safari)
# 随机返回头部信息,推荐使用
print('----------分隔符----------')
print(ua.random)

       可见UserAgent为我们提供了很多种请求头,那么这些数据是从哪里来的,我这里只做简单的介绍,具体细节你们可以跟踪源码

       debug看一下
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_19
       我们找一下数据
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_20
       知道是json,打开复制格式化一下
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_21
       这就是数据源
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_22

       了解了UserAgent模块,我们基于上面的例子,使用UserAgent模块改造

# -*- coding: utf-8 -*-

# 导入requests模块
import requests
# 导入UserAgent模块
from fake_useragent import UserAgent

if __name__ == "__main__":
   # 实例化 UserAgent 类
   ua = UserAgent()
   # 指定url
   get_url="https://www.baidu.com/s"
   # UA伪装
   headers = {
       # 随机返回头部信息
       'User-Agent':ua.random
   }
   # 封装参数,上面的url中?可以不要,发起请求的时候会自动加上
   params={
       'wd':'林高禄'
   }
   # 发起请求,返回一个响应对象
   response = requests.get(url=get_url,params=params,headers=headers)
   # 获取响应数据,
   page_content = response.content.decode('utf8')
   print(page_content)
   # 把数据保存在my_bai_du.html中
   with open('./my_bai_du.html', 'w', encoding='utf-8') as fp:
       fp.write(page_content)

2-5:cookie

       我们使用一个例子来说明,获取账号下ProcessOn的最近修改数据
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_23
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_24

       直接爬肯定是不行的
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_25

       所以我们要先登录,再获取账号下文件的修改记录

       标记个人账号的信息的就是cookie
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_26

       而在requests模块模块中,cookie存在在session中
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_27

       所以我们可以使用sesson登录,这样sesson就带有登录后的cookie,再用携带cookie的sesson就能获取到账号下的修改历史数据

       我们抓取到ProcessOn的登录只需要2个参数,post请求
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_28

       这样就可以进行模拟登录,然后再获取想要的数据

# -*- coding: utf-8 -*-

# 导入requests模块
import requests
# 导入UserAgent模块
from fake_useragent import UserAgent

if __name__ == "__main__":
   # 获取session
   session = requests.session();
   # 实例化 UserAgent 类
   ua = UserAgent()
   # 指定登录url
   url="https://www.processon.com/login"
   # UA伪装
   headers = {
       # 随机返回头部信息
       'User-Agent':ua.random
   }
   # 封装登录参数
   params={
       'login_email': '你们的账号',
       'login_password': '你们的密码'
   }
   # 发起登录请求,返回一个响应对象
   response = session.post(url=url,params=params,headers=headers)
   # 获取响应数据
   page_content = response.content.decode('utf8')
   # 把数据保存在my.html中
   with open('./my.html', 'w', encoding='utf-8') as fp:
       fp.write(page_content)
   # 指定修改历史url
   url = "https://www.processon.com/folder/loadfiles"
   # 发起请求,session中已携带账号信息
   response = session.get(url=url,headers=headers)
   print(response.text)
   # 把数据保存在my2.html中
   with open('./my2.html', 'w', encoding='utf-8') as fp:
       fp.write(response.text)

       获取到历史修改数据
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_29

       我们也可以得知结果是json数据,所以我们可以使用response.json()得到json对象
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_30

# -*- coding: utf-8 -*-

# 导入requests模块
import requests
# 导入UserAgent模块
from fake_useragent import UserAgent
import json

if __name__ == "__main__":
   # 获取session
   session = requests.session();
   # 实例化 UserAgent 类
   ua = UserAgent()
   # 指定登录url
   url="https://www.processon.com/login"
   # UA伪装
   headers = {
       # 随机返回头部信息
       'User-Agent':ua.random
   }
   # 封装登录参数
   params={
       'login_email': '15103631910',
       'login_password': '15103631910com'
   }
   # 发起登录请求,返回一个响应对象
   response = session.post(url=url,params=params,headers=headers)
   # 获取响应数据
   page_content = response.content.decode('utf8')
   # 把数据保存在my.html中
   with open('./my.html', 'w', encoding='utf-8') as fp:
       fp.write(page_content)
   # 指定修改历史url
   url = "https://www.processon.com/folder/loadfiles"
   # 发起请求,session中已携带账号信息
   response = session.get(url=url,headers=headers)
   json_data = response.json()
   print(json_data)
   # 把数据保存在my2.json中
   with open('./my2.json', 'w', encoding='utf-8') as fp:
       # 因为是utf-8,所以ascii编码要设为False
       json.dump(json_data,fp=fp,ensure_ascii=False)

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_31

2-6:代理

       在很多时候,我们写好的一个爬虫刚开始还可以正常的运行,也可以正常的去抓取数据,一切看起来都是那么的美好,然而,可能上个厕所回来,可能就会出现相关的错误,比如出现403等等错误,打开页面一看,可能就会出现您的IP访问频率过高等提示,出现这样的现象的原因就是因为这些对应的门户网站采取的某系反爬措施,拒绝了你的IP访问,也就是你的IP被服务器给封掉了

       为了防止以上的事发生,这就需要一个IP来伪装,而不直接向门户网站的服务器暴露我们的IP,这就是代理。

       所有代理,就是代理服务器,也就是我们不直接向门户网站发送请求,而是向代理服务器发送,代理服务器再将请求转发给web服务器,代理服务器拿到web服务器的响应后,再把响应返回给我们,也就是暴露给wen服务器的IP是代理服务器的IP,这样的好处是:

  • 突破自身IP访问的限制
  • 隐藏自身真实的IP

       代理相关的网站

  • 快代理:https://www.kuaidaili.com/
  • 购半价:http://www.goubanjia.com/

       我们输入网址https://www.baidu.com/s?wd=ip,就能显示我们的IP
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_32
       如果我们使用了代理,那么就会显示代理IP,如何使用代理,因为代理需要花钱买,本人穷困潦倒,买不起了

proxies={
   "https":"112.111.77.174:9999"
}

       然后requests(url=url,proxies=proxies)请求传入代理
       https的请求使用https的代理,http的请求使用http的代理
       代码示例如下

# -*- coding: utf-8 -*-

# 导入requests模块
import requests
from fake_useragent import UserAgent

ua = UserAgent()
url="http://www.baidu.com/s?wd=ip"
headers = {
   'User-Agent':ua.random
}
params={
}
# 代理
proxies={
   "http":"112.111.77.174:9999"
}
response = requests.get(url=url,params=params,headers=headers,proxies=proxies)   # >参数传入代理
response.encoding='utf-8'
with open('./百度.html', 'w', encoding='utf-8') as fp:
   fp.write(response.text)

2-7:验证码

       验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。可以防止:恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试,实际上用验证码是现在很多网站通行的方式,我们利用比较简易的方式实现了这个功能。这个问题可以由计算机生成并评判,但是必须只有人类才能解答。由于计算机无法解答CAPTCHA的问题,所以回答出问题的用户就可以被认为是人类。

       所以验证码,实际上也是反爬措施的一种,目前验证码有很多种

  • 图片验证码:即数字、字母、文字、图片物体等形式的传统字符验证码
  • 滑动验证码
  • 图中点选
  • 短信验证码
  • 语音验证码
  • 问题类验证码

       这里面,验证码破解就不是很容易,需要下一番功夫,这就不是本篇博客的主要内容了,至于图片验证码,但是我们可以借助打码平台,比如超级鹰,打码平台会给我们例子请求,然后返回正确的验证内容

       python也提供了图像识别,识别图像中的文字,得到我们的验证码,但是这个识别度不是很高,具体要很高的识别度,还得深度学习机器训练,这就涉及神经网络相关的内容了,不是我们本篇博客的内容

       python提供了pytesseract模块来图像识别,这个pytesseract模块安装起来貌似有点麻烦

  • pip isntall pytesseract,这个不行的话,就是你们环境缺少了的东西,百度查找啊

       如现在有一张图片
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_33
       然后我们图像识别一下

# -*- coding: utf-8 -*-
import pytesseract
from PIL import Image

# 此段注释的代码,可以把你们的图片验证码截图保存,然后就是图像识别里面的内容
# code_element = browser.find_element_by_id('login_cn_form')
# left = code_element.location['x']
# top = code_element.location['y']
# right = left + code_element.size['width']
# bottom = top + code_element.size['height']
# im = Image.open('code.png')
# im = im.crop((left, top, right, bottom))
# im.save('code.png')
img = Image.open('1.png')
img = img.convert('L')
code = pytesseract.image_to_string(img)
print(code)

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_34

3:巩固练习

       在巩固练习之前,先给大家介绍一种便捷方式

       找到你们爬取的链接,到这里应该都会找相关的数据链接了吧,比如这里我使用百度为例
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_35

       然后打开网址https://curl.trillworks.com/#python
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_36

       真香,这个帮我们省了很多时间敲代码,但是这个出来的东西参数很多,这个看个人习惯哈

3-1:爬取豆瓣电影分类排行榜 - 情色片

       我们先看结果页以及相关请求
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_37

       我们把页条数放大为200,

# -*- coding: utf-8 -*-

# 导入requests模块
import requests
# 导入UserAgent模块
from fake_useragent import UserAgent

if __name__ == "__main__":
   # 实例化 UserAgent 类
   ua = UserAgent()
   # 指定url
   url="https://movie.douban.com/j/chart/top_list"
   # UA伪装
   headers = {
       # 随机返回头部信息
       'User-Agent':ua.random
   }
   params = {
       'type':'6',
       'interval_id':'100:90',
       'action':'unwatched',
       'start':'0',
       'limit':'200'
   }
   # 发起请求,返回一个响应对象
   response = requests.get(url=url,headers=headers,params=params)
   # 获取响应数据
   page_json = response.json()
   for index,article in enumerate(page_json):
       print(str(index),article['title'],article['url'])

python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_38

3-2:获取淘宝首页的搜索提示

       我们先看结果页以及相关请求
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_39

       我们先获取结果看一下,并不是json字符串,找到了一点规律,那就试着把’callback’: 'jsonp18’参数去掉
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_40

       变成了json字符串,这样就可以用json解析了
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_41

# -*- coding: utf-8 -*-

# 导入requests模块
import requests
# 导入UserAgent模块
from fake_useragent import UserAgent
import json
if __name__ == "__main__":
   # 实例化 UserAgent 类
   ua = UserAgent()
   # 指定url
   url="https://suggest.taobao.com/sug"
   # UA伪装
   headers = {
       # 随机返回头部信息
       'User-Agent':ua.random
   }
   params = {
       'code': 'utf-8',
       'q': '程序员',
       'extras': '1',
       'area': 'c2c',
       'bucketid': 'atb_search',
       'pid': 'mm_26632258_3504122_32538762',
       'clk1': 'e805deeff45a8341512810a026ac44b8',
       '_': '1607953862548',
   }
   # 发起请求,返回一个响应对象
   response = requests.get(url=url,headers=headers,params=params)
   json_data = json.loads(response.text)
   for data in json_data['result']:
       print(data)

python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_42

3-3:获取我房网新房列表的价格走势数据

       我们先看价格某一楼盘的价格走势数据
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_43
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_44

       开搞

# -*- coding: utf-8 -*-

# 导入requests模块
import requests
# 导入UserAgent模块
from fake_useragent import UserAgent
import json
if __name__ == "__main__":
   # 实例化 UserAgent 类
   ua = UserAgent()
   # 指定url
   url="http://wofang.com/siteMgr/api/buildingHistoryPrice/v1.0/priceTrend"
   # UA伪装
   headers = {
       # 随机返回头部信息
       'User-Agent':ua.random
   }
   params = {
   'buildingType':'1',
   'buildingId':'4482',
   'monthNum':'6',
   'siteId':'5',
   't':'1608025302194',
   }
   # 发起请求,返回一个响应对象
   response = requests.get(url=url,headers=headers,params=params)
   json_data = json.loads(response.text)
   print(json_data)
   for data in json_data.items():
       print(data)

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_45

       上一步的结果,我们得到了某个楼盘的价格走势数据,要想得到所有楼盘的,那就得得到楼盘列表,我们看一下列表的访问链接和参数
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_46
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_47

       盘它

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
import json
if __name__ == "__main__":
   ua = UserAgent()
   url="http://wofang.com/siteMgr/api/building/v1.0/buildingBaseList"
   headers = {
       'User-Agent':ua.random
   }
   params = {  
       'isMainType':'1',
       'regionCode':'460100 ',
       'pageNum':'1',
       'pageSize':'20',
       'siteId':'5',
   }
   response = requests.get(url=url,headers=headers,params=params)
   json_data = json.loads(response.text)
   print(json_data)
   for data in json_data['list']:
       print(data)

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_48

       现在我们得到了楼盘的列表数据,也得到了一个楼盘的价格走势数据,剩下的就是怎么把列表数据的价格走势拿到,那就是拿列表数据获得相关参数传给价格走势数据爬取,这样就能得到每个楼盘的价格走势数据

       我们对比价格走势的传参和列表的数据,不难发现,siteId和楼盘列表的传参一样,buildingType和buildingId可以从爬取的楼盘数据中获取,而monthNum不固定,也不是非必要的,结合数据估计是月数,那么我们就传12,拿12个月的数据
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_49
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_50

       这样就好办了,把爬取价格走势数据的代码封装成一个方法,传入相关参数,然后遍历列表数据,那每一个楼盘的相关数据传入价格走势的方法进行爬取即可

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
import json

def price_trend(building_type,building_id,month_num):
   price_trend_params = {
       'buildingType': building_type,
       'buildingId': building_id,
       'monthNum': month_num,
       'siteId': '5'
   }
   # 发起请求,返回一个响应对象
   price_trend_response = requests.get(url=price_trend_url, headers=headers, params=price_trend_params)
   return json.loads(price_trend_response.text)

if __name__ == "__main__":

   ua = UserAgent()
   url="http://wofang.com/siteMgr/api/building/v1.0/buildingBaseList"
   price_trend_url = "http://wofang.com/siteMgr/api/buildingHistoryPrice/v1.0/priceTrend"
   headers = {
       'User-Agent':ua.random
   }
   params = {  
       'isMainType':'1',
       'regionCode':'460100 ',
       'pageNum':'1',
       'pageSize':'20',
       'siteId':'5',
   }
   response = requests.get(url=url,headers=headers,params=params)
   json_data = json.loads(response.text)
   print(json_data)
   for data in json_data['list']:
       print(data)
       print(price_trend(data['buildingType'],data['id'],12))

       盘的结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_51

3-4:获取爱奇艺用户的播放记录

       我们从前面知道,要获取用户的播放记录,就得获得用户的cookies,带着cookies去访问播放记录的url,才能获得用户的播放记录,获得用户的cookies的我们有2种方法:

  • 一种是模拟用户登录,拿到带有登录后的cookies信息的session再去访问播放记录的url;
  • 一种是手动在浏览器登录后,抓包工具去拿到cookies,复制出来作为参数去访问播放记录的url;

       一般我们优先使用第一种,程序模拟登录,我们先看爱奇艺的登录的相关信息
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_52

       开始盘它

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent

if __name__ == "__main__":

   ua = UserAgent()
   url="https://passport.iqiyi.com/apis/reglogin/login.action"
   headers = {
       'User-Agent':ua.random
   }
   params = {
       'email':'这里输入你们自己的账号',
       'fromSDK':'1',
       'sdk_version':'1.0.0',
       # 这里的密码是加密后的密码,可以用抓包工具去获取加密后的密码,或者是破解 爱奇艺的加密>方式
       >'passwd':'92c06dd9a8cdc300c3a1e533bbfb4fe718c0705fdef2cbfa3cce09cd9d6369ed038520d7c2c4c52a4fb1a5707430ede80064066d0fad224e6bda5ee01a2add34',
       'agenttype':'1',
       '__NEW':'1',
       'checkExist':'1',
       'ptid':'01010021010000000000',
       'nr':'2',
       'sports_account_merge':'1',
       'verifyPhone':'1',
       'area_code':'86',
       'dfp':'a17a45bfe6c8a44222868f7b7a9dc6302a2659653f7f3a718989a381a4c60f659c',
       'qd_sc':'8d0544b122e59d11948ff363da2e302e',
   }
   response = requests.get(url=url,headers=headers,params=params)
   print(response.status_code)
   print(response.text)

python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_53

       能登录成功了,那么我们就先看一下播放记录的相关信息
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_54

开搞开搞

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
import json

def price_get_all_rc(page_num,page_size):
   all_rc_params = {
       'ckuid': '553b04a903789e834f8e0a3e99968013',
       'only_long': '0',
       'vp': '0',
       'pageNum': page_num,
       'pageSize': page_size,
   }
   # 发起请求,返回一个响应对象
   all_rc_response = session.get(url=get_all_rc_url, headers=headers, params=all_rc_params)
   json_data = json.loads(all_rc_response.text)
   print("所有数据",json_data)
   print('遍历数据:')
   for data in json_data['data']['records']:
       print('标题【',data['videoName'],'】数据详情',data)


if __name__ == "__main__":
   session = requests.session()
   ua = UserAgent()
   url="https://passport.iqiyi.com/apis/reglogin/login.action"
   get_all_rc_url = "https://l-rcd.iqiyi.com/apis/qiyirc/getallrc"
   headers = {
       'User-Agent':ua.random
   }
   params = {
       'email':'这里输入你们自己的账号',
       'fromSDK':'1',
       'sdk_version':'1.0.0',
       'passwd':'92c06dd9a8cdc300c3a1e533bbfb4fe718c0705fdef2cbfa3cce09cd9d6369ed038520d7c2c4c52a4fb1a5707430ede80064066d0fad224e6bda5ee01a2add34',
       'agenttype':'1',
       '__NEW':'1',
       'checkExist':'1',
       'ptid':'01010021010000000000',
       'nr':'2',
       'sports_account_merge':'1',
       'verifyPhone':'1',
       'area_code':'86',
       'dfp':'a17a45bfe6c8a44222868f7b7a9dc6302a2659653f7f3a718989a381a4c60f659c',
       'qd_sc':'8d0544b122e59d11948ff363da2e302e',
   }
   response = session.get(url=url,headers=headers,params=params)
   print(response)
   price_get_all_rc(1,30)

       成功拿到用户的播放记录,拿到这个记录是不是可以分析出用户对什么样的视频感兴趣,是不是就可以推送相关类型的数据给用户了
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_55

4:数据解析

       数据解析在我们的爬虫中也扮演着重要的角色,特别是聚焦爬虫中,因为很多时候,我们并不需要整个页面,而是要页面中某一局部的关键数据,这就需要数据解析了

  • 解析的局部文本内容都会在标签之间或者标签对应的属性中进行存储
  • 进行指定标签的定位
  • 标签或者标签对应的属性中存储的数据值进行提取

4-1:json数据

       我们前面的很多例子中,都是json数据,数据交换用到json文件,json是特殊的字符串。下面就介绍python里json模块的使用

       使用json模块就得引入模块包

  • import json

4-1-1:json简介

定义:JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。

特点:简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。

       在 JS 语言中,一切都是对象。因此,任何支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。但是对象和数组是比较特殊且常用的两种类型:

  • 对象表示为键值对
  • 数据由逗号分隔
  • 花括号保存对象
  • 方括号保存数组
  • 序列化简单定义:变成json格式。
  • 反序列化简单定义: json格式变其它

4-1-2:json模块的使用

       JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。JSON的数据格式其实就是python里面的字典格式,里面可以包含方括号括起来的数组,也就是python里面的列表。

       在python中,有专门处理json格式的模块—— json 和 picle模块

  • Json 模块提供了四个方法: dumps、dump、loads、load
  • pickle 模块也提供了四个功能:dumps、dump、loads、load
  • 序列化:将python的值转换为json格式的字符串。
  • 反序列化:将json格式的字符串转换成python的数据类型

       下面我们使用json模块举例

       要使用json模块,就得引入json模块包

  • import json

       json 模块提供了四个方法

  • dumps:将python对象转换为json字符串
  • loads:将json字符串转为python对象
  • dump:将python对象序列化,并写入打开的文件中
  • load:反序列化,从打开的文件中反序列化到python对象

4-1-2-1:dumps和loads

# -*- coding: utf-8 -*-

import json

#  定义一个字典
data ={
  'girlFriend':[
       {
           'name':'lisa',
           'age':'17'
       },
       {
           'name': 'mary',
           'age': '19'
       }
  ]
}
# 字典转为字符串
data_str = json.dumps(data)
print('类型',type(data_str))
print(data_str)
# 字符串转为字典
data_dic = json.loads(data_str)
print('类型',type(data_dic))
print(data_dic)

python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_56

4-1-2-2:dump和load

# -*- coding: utf-8 -*-

import json

#  定义一个字典
data ={
  'girlFriend':[
       {
           'name':'lisa',
           'age':'17'
       },
       {
           'name': 'mary',
           'age': '19'
       }
  ]
}

# 把数据序列化成json到文件中,文件不存在就会生成
with open('data.txt',mode='w',encoding='utf-8') as wf:
   json.dump(data,wf)

# 读取文件数据转化为python对象
rf = open('data.txt',mode='r',encoding='utf-8')
data_dic = json.load(rf)
rf.close()
print(type(data_dic))
print(data_dic)

python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_57

4-1-2-3:json的中文显示

       先看问题,无论是dumps还是dump,涉及中文就会有显示问题,如下
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_58

       解决方案,因为默认是使用ascii编码进行装换,转换的时候设置编码即可
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_59

4-1-3:注意事项

       注意点:

  • 两种语言之间数据类型的差异,用json交换。
  • 外层必须是字典或列表这两个容器类数据类型。
  • 必须是双引号(因为java等其它语言有使用双引号表示字符串,单引号不表示字符串)
  • json是字符串
  • json中不存在元组。序列化元组之后元组变列表;不能是集合,序列化集合报错。序列化支持类型可以进Python官方文件介绍里面有介绍。
  • 以后传值就是传一个也要用字典或列表

       如
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_60
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_61

4-2:正则

       世间有三大神器,流传了世界各地,普通人看不懂的神秘之物

  • 医生的处方
  • 道士的鬼画符
  • 程序员的正则表达式

       正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。

       许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在Perl中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由Unix中的工具软件(例如sed和grep)普及开的。正则表达式通常缩写成“regex”,单数有regexp、regex,复数有regexps、regexes、regexen。

       正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。

       至于python3的正则如何使用,还请大家自己去学习Python3的正则表达式

       要使用Python3的正则表达式,就得带入re模块

  • import re

       下面我们使用一个例子来演示正则的解析

       获取嗅事百科的热图,我们先看一下热图链接,确定其热图url结构为 https://www.qiushibaike.com/imgrank/page/页码/,到时候我们分页获取的时候遍历传入页码即可,我们看一下具体的图片地址在哪
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_62

       找到了图片的地址规律后,我们的正则可以这样写:r’<div class=“thumb”>.?<img src="(.?)" alt.*?</div>’,这样匹配出来的()里的数据就是我们想要的图片url

       开搞

# -*- coding: utf-8 -*-

import requests
import re
from fake_useragent import UserAgent

def get_pne_page_image(page_num):
   data = requests.get(url=url.format(page_num),headers=headers,params=params).text
   if data:
       # 正则表达式
       ex = r'<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
       image_urls = re.compile(ex, re.S).findall(data)
       for image_url in image_urls:
           print('https:{}'.format(image_url))

if __name__ == "__main__":
   ua = UserAgent()
   headers = {
       'User-Agent': ua.random
    }
    params = {
    }
    url = "https://www.qiushibaike.com/imgrank/page/{}/"
    for i in range(1,6):
        get_pne_page_image(i)

       老实人,不说谎,拿到我们的数据,接着就可以拿图片的地址再去爬取图片了
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_63

4-3:bs4

       BeautifulSoup是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.Beautiful Soup会帮你节省数小时甚至数天的工作时间

       BeautifulSoup的环境安装

  • pip install bs4
  • pip install lxml

       bs4数据解析原理:

  • 实例化一个BeautifulSoup对象,并且将页面源码数据加载到该对象中
  • 通过调用BeautifulSoup对象中相关的属性或者方法进行标签定位和数据提取

       bs4的使用要引入包:

  • from bs4 import BeautifulSoup
  • 将本地的html文档中的数据加载到该对象中,lxml是固定写法

fp = open(’文件路径‘,’r‘,encoding=‘utf-8’)
soup = BeautifulSoup(fp,‘lxml’)

  • 将互联网上获取的页面源码加载到该对象中,lxml是固定写法

page_text = response.text
soup = BeautifulSoup(page_text ,‘lxml’)

       BeautifulSoup提供的解析标签的方法,假设soup是BeautifulSoup对象

  • soup.标签名:得到第一个标签名标签
  • soup.find(‘标签名’):得到第一个标签名标签
  • soup.find(‘标签名’,属性1=‘属性值’):得到属性1的值为属性值的第一个标签名标签,如果属性为calss,那么改为calss_
  • soup.find_all(‘标签名’):得到所有的标签名标签
  • soup.select(选择器):通过标签选择器筛选标签,比如类名选择器,id选择器,层级选择器等等,如soup.select(’.className>a img‘),表示选择所有类名为className的标签的下一级a标签的下面的img标签,>表示直系子层级,空格表示往下的所有子层级
  • soup[num]:第num-1标签
  • 更多的标签选择器可以借鉴选择器超级详细

       BeautifulSoup获取标签的值

  • soup.a[‘href’]:获取标签a的属性href值
  • soup.a.text或者soup.a.get_text():获取标签a中所有的文本内容,下下下…的所有都是
  • soup.a.string:获取标签a下面直系的文本内容

       更多bs4的用法请参考https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/

       下面我们就使用一个例子来演示这些方法,我们先建一个本地文本数据

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
</head>
<body>
   <div>
       <span>这是第1个div里的span</span>
   </div>
   <div class="divClass">
       这是第2个div
   </div>
   <div class="divClass">
       这是第3个div
   </div>
   <div id="divId">
       这是一群链接
       <a href="https://www.baidu.com">百度</a>
       <a href="">林高禄博客</a>
       <a href="https://www.runoob.com/python3/python3-tutorial.html">python3</a>
   </div>
</body>
</html>

       代码实例

# -*- coding: utf-8 -*-

from bs4 import BeautifulSoup

if __name__ == "__main__":
   fp = open('../demoData/data.html','r',encoding='utf-8')
   soup = BeautifulSoup(fp,'lxml')
   print('获取第一个div:')
   print(soup.div)
   print('----------------------------------------------------------------')
   print('获取第一个div:')
   print(soup.find('div'))
   print('----------------------------------------------------------------')
   print('获取第一个class="divClass"的div:')
   print(soup.find('div',class_='divClass'))
   print('----------------------------------------------------------------')
   print('获取所有的div:')
   print(soup.find_all('div'))
   print('----------------------------------------------------------------')
   print('通过类名获取div:')
   print(soup.select('.divClass'))
   print('----------------------------------------------------------------')
   print('获取body下的span:')
   print(soup.select('body span'))
   print('----------------------------------------------------------------')
   print('获取第4个div的id:')
   print(soup.select('div')[3]['id'])
   print('----------------------------------------------------------------')
   print('获取第3个div的直系文本:')
   print(soup.select('div')[2].string)
   print('----------------------------------------------------------------')
   print('获取第4个div的直系文本:这里应该是None,因为下面还有标签')
   print(soup.select('div')[3].string)
   print('----------------------------------------------------------------')
   print('获取第4个div的所有文本:')
   print(soup.select('div')[3].text)

       结果

获取第一个div:
<div>
<span>这是第1个div里的span</span>
</div>
----------------------------------------------------------------
获取第一个div:
<div>
<span>这是第1个div里的span</span>
</div>
----------------------------------------------------------------
获取第一个class="divClass"的div:
<div class="divClass">
       这是第2个div
   </div>
----------------------------------------------------------------
获取所有的div:
[<div>
<span>这是第1个div里的span</span>
</div>, <div class="divClass">
       这是第2个div
   </div>, <div class="divClass">
       这是第3个div
   </div>, <div id="divId">
       这是一群链接
       <a href="https://www.baidu.com">百度</a>
<a href='javascript:void(0)'>林高禄博客</a>
<a href="https://www.runoob.com/python3/python3-tutorial.html">python3</a>
</div>]
----------------------------------------------------------------
通过类名获取div:
[<div class="divClass">
       这是第2个div
   </div>, <div class="divClass">
       这是第3个div
   </div>]
----------------------------------------------------------------
获取body下的span:
[<span>这是第1个div里的span</span>]
----------------------------------------------------------------
获取第4个div的id:
divId
----------------------------------------------------------------
获取第3个div的直系文本:

       这是第3个div
   
----------------------------------------------------------------
获取第4个div的直系文本:这里应该是None,因为下面还有标签
None
----------------------------------------------------------------
获取第4个div的所有文本:

       这是一群链接
       百度
林高禄博客
python3

       实战一下,还是嗅事百科的热图https://www.qiushibaike.com/imgrank/page/页码/
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_62

       找到了图片的地址规律后,我们可以选择根据选择器获取,选择器表达式为’.thumb a img’,得到所有的img标签,然后遍历的时候再获取img的连接img[‘src’]

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
from bs4 import BeautifulSoup

def get_pne_page_image(page_num):
   data = requests.get(url=url.format(page_num),headers=headers,params=params).text
   if data:
       soup = BeautifulSoup(data,'lxml')
       image_labels = soup.select('.thumb a img')
       for image_label in image_labels:
           print('https:{}'.format(image_label['src']))

if __name__ == "__main__":
   ua = UserAgent()
   headers = {
       'User-Agent': ua.random
   }
   params = {
   }
   url = "https://www.qiushibaike.com/imgrank/page/{}/"
   for i in range(1,6):
       get_pne_page_image(i)

       一如既往的老实人,不说谎,拿到我们的数据,接着就可以拿图片的地址再去爬取图片了
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_65

4-4:xpath

       XPath即为XML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言

       XPath基于XML的树状结构,提供在数据结构树中找寻节点的能力。起初XPath的提出的初衷是将其作为一个通用的、介于XPointer与XSL间的语法模型。但是XPath很快的被开发者采用来当作小型查询语言。

        XPath 使用路径表达式在 XML 文档中选取节点。节点是通过沿着路径或者 step 来选取的。

        下面列出了最有用的路径表达式:

表达式 描述
nodename 选取此节点的所有子节点
/ 从根节点选取
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性

       在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:

路径表达式 结果
bookstore 选取 bookstore 元素的所有子节点
/bookstore 选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!
bookstore/book 选取属于 bookstore 的子元素的所有 book 元素
//book 选取所有子book元素,而不管它们在文档中的位置
bookstore//book 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置
//@lang 选取名为 lang 的所有属性
/text() 获取的是标签中的直系文本内容
//text() 获取的是标签中的所有文本内容
属性定位://book[@class=‘aa’] 获取class属性为aa的所有子book元素
索引定位://book[2] 获取第2个子book元素

       更多更详细的xpath用法请参考xpath教程

       xpath的环境安装

  • pip install lxml

       xpath数据解析原理:

  • 实例化一个etree的对象,且需要将被解析的页面源数据加载到该对象中
  • 调用etree对象中的xpath方法结合xpath表达式实现标签的定位和内容的捕获

       xpath的使用要引入包:

  • from lxml import etree
  • 将本地的html文档中的源码数据加载到etree对象中

tree = etree.parse(‘文件路径’)
data = tree.xpath(xpath表达式)

  • 将互联网上获取的页面源码加载到etree对象中

page_text = response.text
tree = etree.HTML(page_text)
data = tree.xpath(xpath表达式)

      &nbsp注意,通过xpath表达式得到的结果永远都是一个列表

       下面我们就使用一个例子来演示这些方法,我们先建一个本地文本数据

<!DOCTYPE html>
<html lang="en">
<head>
   <!--这里不能写成<meta charset="UTF-8">,一定要有结束符或者结束标签标签,不然etree解析会报错-->
   <meta charset="UTF-8"/>
   <title>Title</title>
</head>
<body>
   <div>
       <span>这是第1个div里的span</span>
   </div>
   <div class="divClass">
       <div class="divClass">
           这是第2个div
       </div>
   </div>
   <div id="divId">
       这是一群链接
       <a href="https://www.baidu.com">百度</a>
       <a href="">林高禄博客</a>
       <a href="https://www.runoob.com/python3/python3-tutorial.html">python3</a>
   </div>
</body>
</html>

       代码实例

# -*- coding: utf-8 -*-

from lxml import etree

if __name__ == "__main__":
   tree = etree.parse('../demoData/data.html')
   print("获取body下所有直系的div标签,结果是3个:")
   print(tree.xpath('/html/body/div'))
   print("--------------------------------------")
   print("获取html下所有的div标签,结果是4个:")
   print(tree.xpath('/html//div'))
   print("--------------------------------------")
   print("获取body下类名为divClass的所有直系的div标签,结果是1个:")
   print(tree.xpath('/html/body/div[@class="divClass"]'))
   print("--------------------------------------")
   print("获取类名为divClass的所有div标签,结果是2个:")
   print(tree.xpath('//div[@class="divClass"]'))
   print("------------------注意以下2个表达式的区别,xpath的索引是从1开始的--------------------")
   print("获取body下类名为divClass的所有直系的div标签下的第2个a标签的文本:")
   # 这种是先定位到第2个a标签,在获取文本,得到的是一个列表
   print(tree.xpath('/html/body/div[@id="divId"]/a[2]/text()'))
   print(tree.xpath('/html/body/div[@id="divId"]/a[2]/text()')[0])
   # 这种是先定位到所有a标签,在获取所有文本,得到的是一个列表
   print(tree.xpath('/html/body/div[@id="divId"]/a/text()'))
   print(tree.xpath('/html/body/div[@id="divId"]/a/text()')[1])
   print("------------------注意以下2个表达式的区别,xpath的索引是从1开始的--------------------")
   print("获取body下类名为divClass的所有直系的div标签下的第二个a标签的src:")
   print(tree.xpath('/html/body/div[@id="divId"]/a[2]/@href')[0])
   print(tree.xpath('/html/body/div[@id="divId"]/a/@href')[1])

       结果

获取body下所有直系的div标签,结果是3个:
[<Element div at 0xbceba40>, <Element div at 0xbcebcc0>, <Element div at 0xbcebd00>]
--------------------------------------
获取html下所有的div标签,结果是4个:
[<Element div at 0xbceba40>, <Element div at 0xbcebd80>, <Element div at 0xbcebcc0>, <Element div at >0xbcebd40>]
--------------------------------------
获取body下类名为divClass的所有直系的div标签,结果是1个:
[<Element div at 0xbceba40>]
--------------------------------------
获取类名为divClass的所有div标签,结果是2个:
[<Element div at 0xbcebd80>, <Element div at 0xbcebd00>]
------------------注意以下2个表达式的区别,xpath的索引是从1开始的--------------------
获取body下类名为divClass的所有直系的div标签下的第2个a标签的文本:
['林高禄博客']
林高禄博客
['百度', '林高禄博客', 'python3']
林高禄博客
------------------注意以下2个表达式的区别,xpath的索引是从1开始的--------------------
获取body下类名为divClass的所有直系的div标签下的第二个a标签的src:

       下面我们实战一下,还是嗅事百科的热图https://www.qiushibaike.com/imgrank/page/页码/,实战之前给大家介绍一种巧妙方式,谷歌浏览器可以直接复制xpath。如下图
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_66

       这样就能找到每张图片的Xpath,或者可以定位到上一级的Xpath,然后再拼上/img就可以获得所有图片Xpath

       这里还要再介绍一个插件,xpath-helper,自己百度去下载,下载完后放在你想放的文件下下,然后浏览器添加插件,如图。我使用的是Edga浏览器
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_67
       添加插件成功后浏览器会显示X的字样
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_68
       如果快捷键不冲突,那么ctrl+shift+x就能启动插件
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_69
       在左边的框输入输入xpath,右边的框就能显示结果,如我们看一下嗅事百科的热图的源码
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_62
       那么要获取所有的热图src,那么我们的xpath可以写为**//div[@class=“thumb”]/a/img/@src**获取所有类名thumb的为div下的a标签的img标签的属性src
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_71

       来,代码实战一下

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
from lxml import etree

def get_pne_page_image(page_num):
   data = requests.get(url=url.format(page_num),headers=headers,params=params).text
   if data:
       tree = etree.HTML(data)
       image_srcs = tree.xpath('//div[@class="thumb"]/a/img/@src')
       for image_src in image_srcs:
           print('https:{}'.format(image_src))

if __name__ == "__main__":
   ua = UserAgent()
   headers = {
       'User-Agent': ua.random
   }
   params = {
   }
   url = "https://www.qiushibaike.com/imgrank/page/{}/"
   for i in range(1,6):
       get_pne_page_image(i)

       无论何时我都是老实人,不忽悠,结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_72

4-5:pyQuery

       PyQuery库也是一个非常强大又灵活的网页解析库,如果你有前端开发经验的,都应该接触过jQuery,那么PyQuery就是你非常绝佳的选择,PyQuery 是 Python 仿照 jQuery 的严格实现。语法与 jQuery 几乎完全相同,所以不用再去费心去记一些奇怪的方法了。

  • 官网地址:http://pyquery.readthedocs.io/en/latest/
  • jQuery参考文档: http://jquery.cuishifeng.cn/
  • 本人总结的JQuery选择器JQuery选择器超级详细

       环境安装

  • pip install pyquery

       包导入

  • from pyquery import PyQuery

       PyQuery数据解析原理:

  • 实例化一个PyQuery对象,且需要将被解析的页面源数据加载到该对象中
  • PyQuery对象结合选择器实现标签的定位和内容的捕获
  • 将本地的html文档中的源码数据加载到PyQuery对象中

with open("…/demoData/data.html", “r”, encoding=“utf-8”)as f:
content = f.read()
doc = PyQuery(content)
data =doc(选择器)

  • 将互联网上获取的页面源码加载到PyQuery对象中

page_text = response.text
doc = PyQuery(page_text )
data =doc(选择器)

       下面我们就使用一个例子来演示这些方法,我们先建一个本地文本数据

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8"/>
   <title>Title</title>
</head>
<body>
   <div>
       <span>这是第1个div里的span</span>
   </div>
   <div class="divClass">
       <div class="divClass">
           这是第2个div
       </div>
   </div>
   <div id="divId">
       这是一群链接
       <a href="https://www.baidu.com">百度</a>
       <a href="">林高禄博客</a>
       <a href="https://www.runoob.com/python3/python3-tutorial.html">python3</a>
   </div>
</body>
</html>

       代码实例

# -*- coding: utf-8 -*-

from pyquery import PyQuery

if __name__ == "__main__":
   with open("../demoData/data.html", "r", encoding="utf-8")as f:
       content = f.read()
   doc = PyQuery(content)
   print("获取所有的span:")
   print(doc('span'))
   print("--------------------------------------")
   print("获取id为divId的div:")
   print(doc('div[@id="divId"]'))
   print("--------------------------------------")
   print("获取第2个a标签的文本:")
   print(doc('div[@id="divId"]>a')[1].text)
   print("--------------------------------------")
   print("获取第2个a标签的href:注意以下2中写法的区别")
   print("一种是Element对象的attrib")
   print(type(doc('div[@id="divId"]>a')[1]))
   print(doc('div[@id="divId"]>a')[1].attrib['href'])
   print("一种是PyQuery对象的attr")
   print(type(doc('div[@id="divId"]>a:eq(1)')))
   print(doc('div[@id="divId"]>a:eq(1)').attr('href'))

       结果

获取所有的span:
<span>这是第1个div里的span</span>
   
--------------------------------------
获取id为divId的div:
<div id="divId">
       这是一群链接
       <a href="https://www.baidu.com">百度</a>
       <a href='javascript:void(0)'>林高禄博客</a>
       <a href="https://www.runoob.com/python3/python3-tutorial.html">python3</a>
   </div>

--------------------------------------
获取第2个a标签的文本:
林高禄博客
--------------------------------------
获取第2个a标签的href:注意以下2中写法的区别
一种是Element对象的attrib
<class 'lxml.etree._Element'>

一种是PyQuery对象的attr
<class 'pyquery.pyquery.PyQuery'>

       下面我们实战一下,还是嗅事百科的热图https://www.qiushibaike.com/imgrank/page/页码/,
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_62

       找到了图片的地址规律后,我们可以选择根据选择器获取,选择器表达式为’div[@class=“thumb”]>a>img’,得到所有的img标签,然后遍历的时候再获取src属性.attrib[‘src’]

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
from pyquery import PyQuery
def get_pne_page_image(page_num):
   data = requests.get(url=url.format(page_num),headers=headers,params=params).text
   if data:
       doc = PyQuery(data)
       image_labels = doc('div[@class="thumb"]>a>img')
       for image_label in image_labels:
           print('https:{}'.format(image_label.attrib['src']))

if __name__ == "__main__":
   ua = UserAgent()
   headers = {
       'User-Agent': ua.random
   }
   params = {
   }
   url = "https://www.qiushibaike.com/imgrank/page/{}/"
   for i in range(1,6):
       get_pne_page_image(i)

       结果,都说了,我真的是老实人,不忽悠
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_74

5:爬虫进阶篇-多进程与多线程

5-1:先看一个深刻的例子,爬取某视频的音乐排行榜视频(反爬虫套路特别多)

       在介绍进程与线程之前,我们先来看一个例子,爬取某视频的音乐排行榜,这个某视频涉及版权,所以不好明确指出,这个某你可以理解为某种水果,不如苹果啊,香蕉啊,还有某种单个字的啊,哈哈。
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_75

       首先我们得先拿到每个视频的详情url
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_76

       每一个li标签就是一个视频,我们获取第二个,方便拿到视频的名字,这里我们使用pyquery解析,使用什么解析由你们自己选择,然后我们的选择器可以写为’ul[@class=“popular-list”]>li>div>a

       随便点开一个视频,发现我们拿到的href不是视频详情的全链接,还要拼接上**https://www.xxx.com/**某视频的域名。涉及版权,这里不方便给出,这个某视频相信你们可以猜得出来

       于是我们获取视频详情地址的代码为

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
from pyquery import PyQuery

if __name__ == "__main__":
   session = requests.session()
   ua = UserAgent()
   url="https://某视频域名/popular_59"
   headers = {
       'User-Agent':ua.random
   }
   params = {}
   details_url_list = []
   response = session.get(url=url,headers=headers,params=params)
   if response.status_code == 200:
       page_text = response.text
       if page_text:
           doc = PyQuery(page_text)
           details_url_li_labels = doc('ul[@class="popular-list"]>li>div>a')
           for details_url_li_label in details_url_li_labels.items():
               details_url = 'https://某视频域名/{}'.format(details_url_li_label.attr['href'])
               name = details_url_li_label('h2').text()+'.mp4'
               print(name,end='\t')
               print(details_url)

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_77

       接着看我们的视频详情页面
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_78

       视频的地址就在一个class为img prism-player的div的video标签里的src属性里,这样根据上面拿到视频详情连接,接着访问详情连接解析就能拿到每个视频的地址
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_79

       发现都是空的,为什么,我们打印一个page_text看一下,然后查找video标签
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_80

       发现没找到,也就是说,这里的视频链接是异步加载进来的,所以我们抓一下包,还真有异步请求返回一个视频地址
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_81

       浏览器上访问这个地址,发现访问不了
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_82

       这个可怎么办啊,那个真实的地址肯定是通过某种手段弄来的,我们索性列出几个真实的地址和异步返回的地址对比一下
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_83

真实地址 异步返回的地址
https://某视频域名/mp4/third/20201216/cont-1711992-11905023-172124-hd.mp4 https://某视频域名/mp4/third/20201216/1608638648423-11905023-172124-hd.mp4
https://某视频域名/mp4/third/20201217/cont-1712184-12719568-184629-hd.mp4 https://某视频域名/mp4/third/20201217/1608638850141-12719568-184629-hd.mp4
https://某视频域名/mp4/third/20201215/cont-1711749-12478164-121036-hd.mp4 https://某视频域名/mp4/third/20201215/1608642319906-12478164-121036-hd.mp4

       对比后我们发现,其实真实地址和异步返回的地址除了一个地方不同,其他的都一样
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_84

       而cont是固定的,而cont后面的数字不正好是我们视频详情链接后面的那一串数字吗
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_85

       不得不说这个网站太鸡贼了,但是还是让我们找到了规律,那么我们就可以通过异步获取到返回的视频地址,在重新组装成真正的地址,所以真正的视频详情的地址就变为了https://www.某视频域名.com/videoStatus.jsp

python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_86

       我们只需解析出contId传参即可,但是万万没想到
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_87

       这直接崩溃啊,爬出来的竟然是提示下线,也就是说没爬成功,这反爬虫做的一套连一套的,好,我们好好看一下视频链接的请求
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_88

       经过试验和分析,有一个必要的请求头,所以我们的请求头加上这个参数
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_89

       拿到了我们想要的XHR返回的信息,剩下的就是解析返回的数据,拆分替换组装成真>实的视频地址

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
from pyquery import PyQuery

if __name__ == "__main__":
   session = requests.session()
   ua = UserAgent()
   url="https://www.某视频域名.com/popular_59"
   headers = {
       'User-Agent':ua.random
   }
   params = {}
   details_url_list = []
   response = session.get(url=url,headers=headers,params=params)
   if response.status_code == 200:
       page_text = response.text
       if page_text:
           doc = PyQuery(page_text)
           details_url_li_labels = doc('ul[@class="popular-list"]>li>div>a')
           # 详情地址
           details_url = 'https://www.某视频域名.com/videoStatus.jsp'
           for details_url_li_label in details_url_li_labels.items():
               # 参数contId
               contId = details_url_li_label.attr['href'].split('_')[1]
               # 视频名字
               name = details_url_li_label('h2').text()+'.mp4'
               Referer = 'https://www.某视频域名.com/{}'.format(details_url_li_label.attr['href'])
               # 请求头加一个必要的参数
               headers['Referer'] = Referer
               details_params = {
                   'contId':contId,
               }
               response = session.get(url=details_url, headers=headers, params=details_params)
               # 解析拿到XHR返回的视频地址
               srcUrl = response.json()['videoInfo']['videos']['srcUrl']
               # 拆分重新组装拿到真实的视频地址
               replace_str = srcUrl.split('/')[-1].split('-')[0]
               srcUrl = srcUrl.replace(replace_str,'cont-'+contId)
               video_dict = {
                   'name':name,
                   'srcUrl':srcUrl
               }
               details_url_list.append(video_dict)
   print(details_url_list)

       到这里我们的视频的正确地址就能获取到了,接下来就是下载视频了

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
from pyquery import PyQuery
import time

def download_video(video_name,video_url):
   print(video_name,'开始下载')
   video_content = session.get(url=video_url, headers=headers, params=params).content
   with open('./video/'+video_name,'wb') as fs:
       fs.write(video_content)
   print(video_name, '下载成功')

if __name__ == "__main__":
   session = requests.session()
   ua = UserAgent()
   url="https://www.某视频域名.com/popular_59"
   headers = {
       'User-Agent':ua.random
   }
   params = {}
   details_url_list = []
   response = session.get(url=url,headers=headers,params=params)
   if response.status_code == 200:
       page_text = response.text
       if page_text:
           doc = PyQuery(page_text)
           details_url_li_labels = doc('ul[@class="popular-list"]>li>div>a')
           # 详情地址
           details_url = 'https://www.某视频域名.com/videoStatus.jsp'
           for details_url_li_label in details_url_li_labels.items():
               # 参数contId
               contId = details_url_li_label.attr['href'].split('_')[1]
               # 视频名字
               name = details_url_li_label('h2').text()+'.mp4'
               Referer = 'https://www.某视频域名.com/{}'.format(details_url_li_label.attr['href'])
               # 请求头加一个必要的参数
               headers['Referer'] = Referer
               details_params = {
                   'contId':contId,
               }
               response = session.get(url=details_url, headers=headers, params=details_params)
               # 解析拿到XHR返回的视频地址
               srcUrl = response.json()['videoInfo']['videos']['srcUrl']
               # 拆分重新组装拿到真实的视频地址
               replace_str = srcUrl.split('/')[-1].split('-')[0]
               srcUrl = srcUrl.replace(replace_str,'cont-'+contId)
               video_dict = {
                   'name':name,
                   'srcUrl':srcUrl
               }
               details_url_list.append(video_dict)
   start_time = time.time()
   for video_dict in details_url_list:
       download_video(video_dict['name'],video_dict['srcUrl'])
   end_time = time.time()
   print('耗时:',str(end_time-start_time))

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_90
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_91

       大家想一下,我为和要讲这个例子,还特意挑视频下载,把耗时给打印出来。我们只是下载了8个视频,就耗时了一分钟,想象一下,如果我们再分页爬取,获得更多的视频,岂不是要更长的时间?时间是最宝贵的东西,所以我们要节省这些耗时,让下载的时间更短一点,这就需要了多进程或者多线程了。

5-2:进程和线程的简介

       进程

       进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。


       进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。


       CPU是单进程的,而进程是阻塞的,进程不能同时处理2件事,要等一件处理完才能处理另一件。一个CPU只能处理一个进程,平时所说的几核几核电脑通俗的解释实际上就是处理器里面的CPU的个数,比如双核里面有两个,相当于两个单核CPU。既然进程是阻塞,那为何有些单核的电脑能同时运行那么多软件,这是因为电脑运行速度很快,是不停在切换,这个软件执行几秒,然后切换到另一个软件执行几秒,切换的速度很快,我们察觉不到而已。


       拥有2个运算设备的CPU称作双核CPU,拥有4个运算器的CPU称作4核CPU。也就是说,一个CPU中可能包含多个运算设备(核)。核的个数与可同时运行的进程数相同。相反,若进程数超过核数,进程将分时使用CPU资源。但因为CPU运转速度极快,我们会感到所有进程同时运行。当然,核数越多,这总感觉越明显。


       如何知道我们的电脑有几个CPU,电脑->属性->设备管理器->处理器
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_92
       我这是4个CPU,也就是4核处理器

       线程

       线程(英语:Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。


       线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。


       同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。


       一个进程可以有很多线程,每条线程并行执行不同的任务。


       在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

5-3:进程和线程的区别

       一个进程可以有很多线程,每条线程并行执行不同的任务。 好比如一个女孩子可以同时有多个追求者一样,每个追求者并行的不同方式追求

       进程(Process)和线程(Thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握,但是,我读到一篇材料,发现有一个很好的类比,可以把它们解释地清晰易懂。

  • 原材料地址:http://www.qnx.com/developers/docs/6.4.1/neutrino/getting_started/s1_procs.html

       这里我做了简单的整理归纳

       1.计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,一个工厂就是一个CPU,时刻在运行。
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_93

       2.假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_94

       3.进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_95

       4.一个车间里,可以有很多工人。他们协同完成一个任务。
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_96

       5.线程就好比车间里的工人。一个进程可以包括多个线程。
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_97

       6.车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_98

       7.可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_99

       8.一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_100

       9.还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_101

       10.这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_102
       不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

       11.操作系统的设计,因此可以归结为三点:

  • 以多进程形式,允许多个任务同时运行;
  • 以多线程形式,允许单个任务分成不同的部分运行;
  • 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
    python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_103

5-4:提高爬虫效率的方式

       在上面的5-1爬取某视频的例子中,我们发现爬取视频这样的大文件是非常耗时的,所以我们也要解决这样的耗时问题,为了简便,我们不用这个例子举例,而是使用简单的例子举例,后面再改进爬取某视频的例子

       我们使用一个简单例子,某解忧网(专门通过谈心的方式,解答客户的心事,给客户心灵鸡汤开导,排忧解难)开业当天就有3个单,是3个女的,每个女的都有自己的下单预谈时长,但是目前情感导师只有老板林某一个,**单进程单线程情况下,**老板要和一个女的谈完后,再接着和下一个女的谈,不能同时和2个或以上的女的谈,后面的要排队等候

# -*- coding: utf-8 -*-

import time
import os,threading

# 心灵开导
def talk(girl):
  print(f'进程号:{os.getpid()}和{girl["name"]}开始交谈,线程号{threading.current_thread()}交谈时间{girl["time"]}')
  # 谈话的时间
  time.sleep(girl['time'])
  print(f'进程号:{os.getpid()}和{girl["name"]}交谈结束,线程号{threading.current_thread()}')

def talk_list(girl_list):
   for girl in girl_list:
       talk(girl)

if __name__ == '__main__':
  # 客户
  girl_list1 = [{'name':'小丽','time':5},{'name':'小美','time':2},{'name':'小爱','time':5}]
  start_time = time.time()
  # 交谈
  talk_list(girl_list1)
  end_time = time.time()
  print('耗时',end_time-start_time)

       3个人的预谈时长加起来就是5+2+5=12个单位时间
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_104

       并且他们的进程号都是一样的,线程号也都是一样的,单进程单线程

       随着第一批的客户的体验,客户得到的效果极其满意,办理了VIP,公司的声誉大增,在这个灯红酒绿繁杂内心空无而又虚化的年代,越来越多孤独的人需要心灵之间的融合与交谈,来下单的人越来越多,客户越来越多,如果还是按照之前的单进程单线程方式,老板林某可能无暇顾及,周顾不到公司日益壮大的业务,所以需要找到良好的解决方法…

5-4-1:多进程

       随着公司的单流量增多,老板林某决定再招2个情感导师,包括老板林某一共3个人,每个情感导师分别处理数量相同的 单,比如一个6个单,那就每个人处理2个单

       这3个情感导师相当于3个进程,由原来的老板单进程,变为了多进程,使用多进程需要导包

  • from multiprocessing import Process
# -*- coding: utf-8 -*-

import time
from multiprocessing import Process
import os, threading


# 心灵开导
def talk(girl):
   print(f'进程号:{os.getpid()}和{girl["name"]}开始交谈,线程号{threading.current_thread()}交谈时间{girl["time"]}')
   # 谈话的时间
   time.sleep(girl['time'])
   print(f'进程号:{os.getpid()}和{girl["name"]}交谈结束,线程号{threading.current_thread()}')


def talk_list(girl_list):
   for girl in girl_list:
       talk(girl)


if __name__ == '__main__':
   # 客户
   girl_list1 = [{'name': '小丽', 'time': 5}, {'name': '小美', 'time': 2}]
   girl_list2 = [{'name': '小爱', 'time': 5}, {'name': '小花', 'time': 1}]
   girl_list3 = [{'name': '小玉', 'time': 1}, {'name': '小彩', 'time': 1}]
   start_time = time.time()
   # target表示要执行的方法,args表示参数,是一个元组,当然还有很多参数,比如name表示进程名字等等
   p1 = Process(target=talk_list, args=(girl_list1,))
   p1.start()
   p2 = Process(target=talk_list, args=(girl_list2,))
   p2.start()
   p3 = Process(target=talk_list, args=(girl_list3,))
   p3.start()
   # 放入进程列表
   p_list = [p1, p2, p3]
   # join()是让主线程等待p的结束,卡住的是主线程而绝非进程i
   # 进程只要start就会在开始运行了,所以p1-p3.start()时,系统中已经有四个并发的进程了
   # 而我们p1.join()是在等p1结束,没错p1只要不结束主线程就会一直卡在原地,这也是问题的关键
   # join是让主线程等,而p1-p3仍然是并发执行的,p1.join的时候,其余p2,p33仍然在运行,等#p1.join结束,可能p2,p3早已经结束了,这样p2.join,p3.join直接通过检测,无需等待
   # 所以3个join花费的总时间仍然是耗费时间最长的那个进程运行的时间
   [i.join() for i in p_list]
   end_time = time.time()
   print('耗时', end_time - start_time)

       如果按照单进程的时间来算,至少要5+5+1+1+2+1=15个时间单位,但是从结果我们可以看出,即使我们的客户增多了,时间却一共只耗时7.4个时间单位,这个效率高多了,并且从结果可以看出,同一个列表我们给了一个进程,所以他们的进程号都是一样,如图小丽和小美的进程号都是一样的,因为是单线程,所以他们再这个进程里面的线程号也是一样的。其他的客户组也是一样
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_105

5-4-2:进程池

       上面的多线程虽然可以高效率的帮我们处理了客户,但是从上面的运行图你们发现了没有,小丽第一个先交谈,小美最后一个交谈结束,而这2个客户都是第1组的。也就是说,第1组客户中,即使你先交谈,但是其他组的客户都结束了,你还没结束,这是因为第1组客户的中国交谈时间是5+2=7最长的,而第3组的时间是1+1=2最短的,第3组客户的情感导师在谈完第3组的客户后,就没事做了,一直闲着,而第1组客户的情感导师还在一直谈,从人效上来说,这是不够高的,我们假设一下,当第3组的情感导师谈完客户后,接着谈第2组客户的最后一个客户小花,谈完后接着谈第1组客户额最后一个客户小美,这样花费的时间是第3组的(1+1)+第2组的(1)+第1组的(2)=5个时间单位,而这时候第1组的小丽(花费时间5)和第2组的小爱(花费时间5)也正好谈完,这样3个情感导师总共花费5个时间单位就能完成所有客户的交谈,剩下的时间可以做其他的事,或者交谈更多的客户,这样的时效是不是变高了。

       于是老板改变了思路,不指定单给个人了,每个单都是等待状态,任何情感导师都可以接,情感导师有空就可以接,这样最理想的状态就是以上分析的5个时间单位就能接完所有的单

5-4-2-1:方式1:from multiprocessing import Pool

       这种方式要导包

  • from multiprocessing import Pool
# -*- coding: utf-8 -*-

import time
from multiprocessing import Pool
import os, threading

# 心灵开导
def talk(girl):
   print(f'进程号:{os.getpid()}和{girl["name"]}开始交谈,线程号{threading.current_thread()}交谈时间{girl["time"]}')
   # 谈话的时间
   time.sleep(girl['time'])
   print(f'进程号:{os.getpid()}和{girl["name"]}交谈结束,线程号{threading.current_thread()}')


def talk_list(girl_list):
   for girl in girl_list:
       talk(girl)


if __name__ == '__main__':
   # 客户
   girl_list1 = [{'name': '小丽', 'time': 5}, {'name': '小美', 'time': 2},{'name': '小爱', 'time': 5}
       , {'name': '小花', 'time': 1},{'name': '小玉', 'time': 1}, {'name': '小彩', 'time': 1}]
   start_time = time.time()
   # 开3个进程
   pool = Pool(3)
   pool.map(func=talk,iterable=girl_list1)
   # 关闭池子
   pool.close()
   # 阻塞,子线程运行完主线程再退出
   pool.join()
   end_time = time.time()
   print('耗时', end_time - start_time)

       从结果我们可以看出,时间才5.4个单位时间,比多进程快了,并且从输出中,发现进程59844的情感导师一旦接完一个客户,就会去接等待中的客户,而且时间正好其他情感导师接待完客户小丽,进程59844的情感导师就把其他所有等待的客户接待完了。
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_106

5-4-2-2:方式2:from concurrent.futures import ProcessPoolExecutor

       这种方式要导包

  • from concurrent.futures import ProcessPoolExecutor
# -*- coding: utf-8 -*-

import time
from concurrent.futures import ProcessPoolExecutor
import os
import threading

# 心灵开导
def talk(girl):
   print(f'进程号:{os.getpid()}和{girl["name"]}开始交谈,线程号{threading.current_thread()}交谈时间{girl["time"]}')
   # 谈话的时间
   time.sleep(girl['time'])
   print(f'进程号:{os.getpid()}和{girl["name"]}交谈结束,线程号{threading.current_thread()}')


def talk_list(girl_list):
   for girl in girl_list:
       talk(girl)


if __name__ == '__main__':
   # 客户
   girl_list1 = [{'name': '小丽', 'time': 5}, {'name': '小美', 'time': 2},{'name': '小爱', 'time': 5}
       , {'name': '小花', 'time': 1},{'name': '小玉', 'time': 1}, {'name': '小彩', 'time': 1}]
   start_time = time.time()
   # 开3个进程
   pool = ProcessPoolExecutor(max_workers=3)
   # func指要调的方法,iterable指要循环的迭代器,里面的子元素正好就是方法func指要调的方法的参数
   pool.map(talk,girl_list1)
   # 关闭池子
   pool.shutdown()
   end_time = time.time()
   print('耗时', end_time - start_time)

       从结果我们可以看出,时间才5.4个单位时间,比多进程快了,并且从输出中,发现进程69596的情感导师一旦接完一个客户,就会去接等待中的客户,而且时间正好其他情感导师接待完客户小丽,进程69596的情感导师就把其他所有等待的客户接待完了。
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_107

5-4-2:多线程

       经过多年的工作经验,老板林某对情感的开导应对自如,能一心多用,再也不需要招那么多情感导师了,一个人就能应对,如一个人应对三组客户,这就是多线程

       使用多线程需要导包

  • from threading import Thread
# -*- coding: utf-8 -*-

import time
from threading import Thread
import os, threading


# 心灵开导
def talk(girl):
   print(f'进程号:{os.getpid()}和{girl["name"]}开始交谈,线程号{threading.current_thread()}交谈时间{girl["time"]}')
   # 谈话的时间
   time.sleep(girl['time'])
   print(f'进程号:{os.getpid()}和{girl["name"]}交谈结束,线程号{threading.current_thread()}')


def talk_list(girl_list):
   for girl in girl_list:
       talk(girl)


if __name__ == '__main__':
   # 客户
   girl_list1 = [{'name': '小丽', 'time': 5}, {'name': '小美', 'time': 2}]
   girl_list2 = [{'name': '小爱', 'time': 5}, {'name': '小花', 'time': 1}]
   girl_list3 = [{'name': '小玉', 'time': 1}, {'name': '小彩', 'time': 1}]
   start_time = time.time()
   # target表示要执行的方法,args表示参数,是一个元组,当然还有很多参数,比如name表示线程名字等等
   t1 = Thread(target=talk_list, args=(girl_list1,))
   t1.start()
   t2 = Thread(target=talk_list, args=(girl_list2,))
   t2.start()
   t3 = Thread(target=talk_list, args=(girl_list3,))
   t3.start()
   # 放入线程列表
   t_list = [t1, t2, t3]
   # join()是让主线程等待t的结束,卡住的是主线程而绝非线程程i
   # 进程只要start就会在开始运行了,所以p1-p3.start()时,系统中已经3个并发的线程了
   # 而我们t1.join()是在等t1结束,没错t1只要不结束主线程就会一直卡在原地,这也是问题的关键
   # join是让主线程等,而t1-t3仍然是并发执行的,t1.join的时候,其余t2,t3仍然在运行,等t1.join结束,可能t2,t3早已经结束了,这样t2.join,t3.join直接通过检测,无需等待
   # 所以3个join花费的总时间仍然是耗费时间最长的那个进程运行的时间
   [i.join() for i in t_list]
   end_time = time.time()
   print('耗时', end_time - start_time)

       如果按照单线程的时间来算,至少要5+5+1+1+2+1=15个时间单位,但是从结果我们可以看出,即使我们的客户增多了,时间却一共只耗时7.0个时间单位,这个效率高多了,而且还比多进程的快,并且从结果可以看出,都是老板林某接待客户,只有一个进程。所以所有组的进程号都是一样,而每一组的线程不一样,组内的线程却是一样的,如图小丽和小美的线程号都是一样的,而与其他组不一样
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_108

5-4-4:线程池

       这个就不用解释吧,就和进程池替换多进程是一样的

       使用线程池要导包

  • from concurrent.futures import ThreadPoolExecutor
# -*- coding: utf-8 -*-

import time
from concurrent.futures import ThreadPoolExecutor
import os
import threading

# 心灵开导
def talk(girl):
   print(f'进程号:{os.getpid()}和{girl["name"]}开始交谈,线程号{threading.current_thread()}交谈时间{girl["time"]}')
   # 谈话的时间
   time.sleep(girl['time'])
   print(f'进程号:{os.getpid()}和{girl["name"]}交谈结束,线程号{threading.current_thread()}')


def talk_list(girl_list):
   for girl in girl_list:
       talk(girl)


if __name__ == '__main__':
   # 客户
   girl_list1 = [{'name': '小丽', 'time': 5}, {'name': '小美', 'time': 2},{'name': '小爱', 'time': 5}
       , {'name': '小花', 'time': 1},{'name': '小玉', 'time': 1}, {'name': '小彩', 'time': 1}]
   start_time = time.time()
   # 开3个线程
   pool = ThreadPoolExecutor(max_workers=3)
   # func指要调的方法,iterable指要循环的迭代器,里面的子元素正好就是方法func指要调的方法的参数
   pool.map(talk,girl_list1)
   # 关闭池子
   pool.shutdown()
   end_time = time.time()
   print('耗时', end_time - start_time)

       从结果我们可以看出,时间才5.0个单位时间,比多线程快了,也比进程池快了,并且从输出中,发现都是一个进程。线程68712的一旦接完一个客户,就会去接等待中的客户,而且时间正好其他线程接待完客户小丽,线程68712就把其他所有等待的客户接待完了。
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_109

       综上所述,我们可以得出结论,从效率来说,线程池>进程池>多线程>多进程>单进程单线程

       还记得爬取某视频的例子吗,我们花了61秒
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_110

       那么我们使用线程池改造一下

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
from pyquery import PyQuery
import time
from concurrent.futures import ThreadPoolExecutor

def download_video(video_dict):
   print(video_dict['name'],'开始下载')
   video_content = session.get(url=video_dict['srcUrl'], headers=headers, params=params).content
   with open('./video/'+video_dict['name'],'wb') as fs:
       fs.write(video_content)
   print(video_dict['name'], '下载成功')

if __name__ == "__main__":
   session = requests.session()
   ua = UserAgent()
   url="https://www.某视频域名.com/popular_59"
   headers = {
       'User-Agent':ua.random
   }
   params = {}
   details_url_list = []
   response = session.get(url=url,headers=headers,params=params)
   if response.status_code == 200:
       page_text = response.text
       if page_text:
           doc = PyQuery(page_text)
           details_url_li_labels = doc('ul[@class="popular-list"]>li>div>a')
           # 详情地址
           details_url = 'https://www.某视频域名.com/videoStatus.jsp'
           for details_url_li_label in details_url_li_labels.items():
               # 参数contId
               contId = details_url_li_label.attr['href'].split('_')[1]
               # 视频名字
               name = details_url_li_label('h2').text()+'.mp4'
               Referer = 'https://www.某视频域名.com/{}'.format(details_url_li_label.attr['href'])
               # 请求头加一个必要的参数
               headers['Referer'] = Referer
               details_params = {
                   'contId':contId,
               }
               response = session.get(url=details_url, headers=headers, params=details_params)
               # 解析拿到XHR返回的视频地址
               srcUrl = response.json()['videoInfo']['videos']['srcUrl']
               # 拆分重新组装拿到真实的视频地址
               replace_str = srcUrl.split('/')[-1].split('-')[0]
               srcUrl = srcUrl.replace(replace_str,'cont-'+contId)
               video_dict = {
                   'name':name,
                   'srcUrl':srcUrl
               }
               details_url_list.append(video_dict)
   start_time = time.time()
   # 开8个线程
   pool = ThreadPoolExecutor(max_workers=8)
   # func指要调的方法,iterable指要循环的迭代器,里面的子元素正好就是方法func指要调的方法的参数
   pool.map(download_video, details_url_list)
   # 关闭池子
   pool.shutdown()
   end_time = time.time()
   print('耗时:',str(end_time-start_time))

       结果19.8秒,比之前快了是因为爬取的视频不一样,时刻网速不一样
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_111

6:爬虫进阶篇-协程

       我们在上一章节说到了进程和线程,这2个都是计算机操作系统存在的,而协程是由程序员认为创造出来的

6-1:协程的概念

       协程,又称微线程,纤程。英文名Coroutine,是一种用户态的轻量级线程

       子程序,或者称为函数,在所有语言中都是层级调用,比如A调B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的,而协程的调用和子程序不同

       线程时系统级别的,它们由操作系统调度,而协程则是程序级别的,由程序根据需要自己调度。在一个线程中会有很多函数,我们把这些函数称为子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程就称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行。

       协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法,进入上一次离开时所处逻辑流的位置。

       如正常线程调用,输出的是12AB

# -*- coding: utf-8 -*-

def f1():
   print(1)
   print(2)

def f2():
   print('A')
   print('B')

f1()
f2()

python爬虫,看完发小阿水决心去城发展,村花都留不住_python_112

       而协程可实现输出1A2B,这个例子只是为了演示,往下会说到怎么使用

# -*- coding: utf-8 -*-
from  greenlet import greenlet
def f1():
   print(1)
   gre2.switch()
   print(2)
   gre2.switch()

def f2():
   print('A')
   gre1.switch()
   print('B')

# 创建协程1
gre1 = greenlet(f1)
# 创建协程2
gre2 = greenlet(f2)
# 执行协程1
gre1.switch()

python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_113

       看了上面的示例,也许有人疑惑这没意义啊,把print(‘A’)和print(‘2’)的代码调换,普通的线程也可以做到啊,是的,单纯的协程没有任何意义,但是协程+遇到IO就切换,那就大有用处了,比如我们的某视频爬取例子,如果不使用多进程和多线层,我们可以使用协程,每一次发起视频地址的请求都是IO,那么程序就会切换到另一次请求的发起,而不是一直在等待第一次的请求返回,这样就可以实现多次请求同时发起这些我们都会说到,请继续往下一章看

6-2:实现协程的方式

       实现协程的方式有以下4这种

  • greenlet(早期模块,掌握)
  • yield关键字(了解yield关键字和generator迭代器)
  • asyncio装饰器(py3.4版本及以上)(了解即可)
  • async、await关键字(py3.5版本及以上)【推荐使用这种方式】

6-2-1:greenlet

       上一章中我们已经演示了greenlet的例子,下面我们来说怎么使用

       要使用greenlet,得先安装环境

  • pip install greenlet

       greenlet使用例子解释
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_114

       我们已经知道,单纯的协程没有任何意义,所以要结合遇到IO就切换,才体现协程的价值,但是greenlet模块并没有提供遇到IO就切换的功能,所以我们需要一个模块gevent模块,gevent内部依赖greenlet,相当于greenlet+IO

       环境安装

  • pip install gevent

       使用例子

# -*- coding: utf-8 -*-

# 这个包的导入和下一行的monkey.patch_all()必须放在第一第二行,才能import其他模块,不然报错
from gevent import monkey
# 这个很重要,以后代码中遇到IO都会自动执行greenlet的switch进行切换
monkey.patch_all()
import requests
import gevent

# 访问
def get_page(url_dict):
   print(f'{url_dict["name"]}开始访问')
   response = requests.get(url_dict['url'])
   print(len(response.text),url_dict["url"])
   print(f'{url_dict["name"]}访问结束')
   
# 数据列表
url_dict_list=[
   {'name':'林某博客','url':''},
   {'name':'python官网','url':'https://www.python.org'},
   {'name':'百度','url':'https://www.baidu.com'}
]
# 使用列表推导式,将gevent.spawn(get_page,url_dict)列表推导出来
green_let_list = [gevent.spawn(get_page,url_dict) for url_dict in url_dict_list]
gevent.joinall(green_let_list)

       从结果中我们可以看出,第一次访问get_page方法的时候,遇到请求的IO,并没有等待请求返回结果,而是会切换到第二次访问get_page方法,同理,只要遇到IO就会切换,而不是等待请求结束,并且那个IO先返回结果,接着就会切换到哪个方法继续往下执行
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_115

       看这里,我故意注释了一下
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_116
我演示一下不放在首行的错误
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_117

       了解了这种方式后,我们还是一样,拿爬取某视频的例子改造一下

# -*- coding: utf-8 -*-

# 这个包的导入和下一行的monkey.patch_all()必须放在第一第二行,才能import其他模块,不然报错
from gevent import monkey
# 这个很重要,以后代码中遇到IO都会自动执行greenlet的switch进行切换
monkey.patch_all()
import gevent
import requests
from fake_useragent import UserAgent
from pyquery import PyQuery
import time

def download_video(video_dict):
   print(video_dict['name'],'开始下载')
   video_content = session.get(url=video_dict['srcUrl'], headers=headers, params=params).content
   with open('./video/'+video_dict['name'],'wb') as fs:
       fs.write(video_content)
   print(video_dict['name'], '下载成功')

if __name__ == "__main__":
   session = requests.session()
   ua = UserAgent()
   url="https://www.某视频域名.com/popular_59"
   headers = {
       'User-Agent':ua.random
   }
   params = {}
   details_url_list = []
   response = session.get(url=url,headers=headers,params=params)
   if response.status_code == 200:
       page_text = response.text
       if page_text:
           doc = PyQuery(page_text)
           details_url_li_labels = doc('ul[@class="popular-list"]>li>div>a')
           # 详情地址
           details_url = 'https://www.某视频域名.com/videoStatus.jsp'
           for details_url_li_label in details_url_li_labels.items():
               # 参数contId
               contId = details_url_li_label.attr['href'].split('_')[1]
               # 视频名字
               name = details_url_li_label('h2').text()+'.mp4'
               Referer = 'https://www.某视频域名.com/{}'.format(details_url_li_label.attr['href'])
               # 请求头加一个必要的参数
               headers['Referer'] = Referer
               details_params = {
                   'contId':contId,
               }
               response = session.get(url=details_url, headers=headers, params=details_params)
               # 解析拿到XHR返回的视频地址
               srcUrl = response.json()['videoInfo']['videos']['srcUrl']
               # 拆分重新组装拿到真实的视频地址
               replace_str = srcUrl.split('/')[-1].split('-')[0]
               srcUrl = srcUrl.replace(replace_str,'cont-'+contId)
               video_dict = {
                   'name':name,
                   'srcUrl':srcUrl
               }
               details_url_list.append(video_dict)
   start_time = time.time()
   # 使用列表推导式,将gevent.spawn(download_video,details_url)列表推导出来
   green_let_list = [gevent.spawn(download_video, details_url) for details_url in details_url_list]
   gevent.joinall(green_let_list)
   end_time = time.time()
   print('耗时:',str(end_time-start_time))

       结果耗时15.3秒,我惊呆了,比线程池还快,我们使用线程池可是跑了19.8秒
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_118

6-2-2:yield

       在讲yield之前,我们先了解一下generator

6-2-2-1:什么是generator

       我们先看一个例子,定义一个列表num_list,用这个列表生成新的列表new_nums,新列表new_nums的元素是列表num_list元素的平方,我们可以使用列表推导式来处理

num_list = [1,2,3,4,5]
new_nums = [n*n for n in num_list]
print(type(new_nums))
print(new_nums)

       从结果看,new_nums 是一个列表
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_119
       我们改变一下代码,把推导式最外面的方括号[]改为括号()

num_list = [1,2,3,4,5]
new_nums = (n*n for n in num_list)
print(type(new_nums))
print(new_nums)

       结果发现new_nums是一个generator对象
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_120

       那么generator到底是什么呢,generator就是迭代器,也成为生成器,我们遍历一下

num_list = [1,2,3,4,5]
new_nums = (n*n for n in num_list)
for n in new_nums:
   print(n)

       结果看到我们的数字都被遍历出来了,也就是说,generator就是迭代器
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_121

       我们知道,迭代器都可以用next()方法获取其里面的元素

num_list = [1,2,3,4,5]
new_nums = (n*n for n in num_list)
print(next(new_nums))
print(next(new_nums))
print(next(new_nums))
print(next(new_nums))
print(next(new_nums))
print(next(new_nums))

       结果也是输出了每个元素的值,最后一个报错是因为已经输出了5次完了,迭代器已经停止了,下一个元素已经没有,当然会报错
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_122
       不信我们可以调用3次next(),然后再使用for循环看一下

num_list = [1,2,3,4,5]
new_nums = (n*n for n in num_list)
print(next(new_nums))
print(next(new_nums))
print(next(new_nums))
print('-'*20)
for n in new_nums:
   print(n)

       结果可见for循环后面也是只有2个元素,因为之前的3次next()后,迭代器已经定位到了第四个元素
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_123

6-2-2-2:为什么要用generator

       这个问题问得好,要想解答这个问题,还得举例子演示

       我们把初始列表变大一点,比如3千万,然后生成列表看一下内存前后以及占用内存大小,接着再生成generator,看一下内存前后以及占用内存大小,memory_profiler模块可以查看内存

# -*- coding: utf-8 -*-
import memory_profiler as me
num_list = list(range(30000000))
men_1 = me.memory_usage()
print(f'list内存前:{men_1}')
list_nums = [n*n for n in num_list]
men_2 = me.memory_usage()
print(f'list内存后:{men_2}')
print(f'list占用内存:{men_2[0]-men_1[0]}')

print('-'*20)

men_1 = me.memory_usage()
print(f'generator内存前:{men_1}')
generator_nums = (n*n for n in num_list)
men_2 = me.memory_usage()
print(f'generator内存后:{men_2}')
print(f'generator占用内存:{men_2[0]-men_1[0]}')

       结果大跌眼镜,列表占用了1161MB以上,而generator才0.004MB,内存占用如此之少,这就是使用generator的原因
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_124

6-2-2-3:yield到底是什么

       首先,如果你还没有对yield有个初步分认识,那么你先把yield看做“return”,这个是直观的,它首先是个return,普通的return是什么意思,就是在程序中返回某个值,返回之后程序就不再往下运行了。看做return之后再把它看做一个是生成器(generator)的一部分(带yield的函数才是真正的迭代器),好了,如果你对这些不明白的话,那先把yield看做return,然后直接看下面的程序,你就会明白yield的全部意思了:

# -*- coding: utf-8 -*-

def my_fuc():
   print("starting...")
   while True:
       result = yield 6
       print("result:",result)
g = my_fuc()
print(next(g))
print("*"*15)
print(next(g))

       看这几行代码的输出,你应该大致能知道yield是什么,代码的输出这个:

starting...
6
***************
result: None
6

       我直接解释代码运行顺序,相当于代码单步调试:

  • 1、程序开始执行,g = my_fuc(),因为my_fuc()函数中有yield关键字,所以my_fuc函数 并不会真的执行,而是先得到一个生成器对象g
  • 2、往下调用到next(g),my_fuc函数正式开始执行,先执行了my_fuc函数中的print(“starting…”),然后进入while循环
  • 3、程序遇到yield关键字,然后把yield想想成return,return了一个6之后,程序停止,并没有执行赋值给result操作,此时next(g)语句执行完成,所以输出的前两行(第一个是while上面的print的结果,第二个是return出的结果)是执行print(next(g))的结果,也就是6
  • 4、程序执行print("*"15),输出15个
  • 5、又开始执行下面的print(next(g)),这个时候和上面那个差不多,不过不同的是,这个时候是从刚才那个next程序停止的地方开始执行的,也就是要执行result的赋值操作,这时候要注意,这个时候赋值操作的右边是没有值的(因为刚才那个是return出去了,并没有给赋值操作的左边传参数),所以这个时候result赋值是None,所以接着下面的输出就是result:None,
  • 6、程序会继续在while里执行,又一次碰到yield,这个时候同样return 出6,然后程序停止,print函数输出的6就是这次return出的6。
    python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_125

       到这里你可能就明白yield和return的关系和区别了,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。

       我们再看一个例子,带有send()方法的,代码如下

# -*- coding: utf-8 -*-

def my_fuc():
   print("starting...")
   while True:
       result = yield 6
       print("result:",result)
g = my_fuc()
print(next(g))
print("*"*15)
print(g.send(8))

       输出结果如下

starting...
6
***************
result: 8
6

       先大致说一下send函数的概念:此时你应该注意到上一个例子的result的值为什么是None,而这个变成了8,到底为什么,这是因为,send是发送一个参数给result的,因为上面讲到,return的时候,并没有把6赋值给result,下次执行的时候只好继续执行赋值操作,只好赋值为None了,而如果用send的话,开始执行的时候,先接着上一次(return 6之后)执行,先把8赋值给了result,然后执行next的作用,遇见下一回的yield,return出结果后结束。

我直接解释代码运行顺序,相当于代码单步调试:

  • 1、程序开始执行,g = my_fuc(),因为my_fuc()函数中有yield关键字,所以my_fuc函数 并不会真的执行,而是先得到一个生成器对象g
  • 2、往下调用到next(g),my_fuc函数正式开始执行,先执行了my_fuc函数中的print(“starting…”),然后进入while循环
  • 3、程序遇到yield关键字,然后把yield想想成return,return了一个6之后,程序停止,并没有执行赋值给result操作,此时next(g)语句执行完成,所以输出的前两行(第一个是while上面的print的结果,第二个是return出的结果)是执行print(next(g))的结果,也就是6
  • 4、程序执行print("*"15),输出15个
  • 5、又开始执行下面的print(g.send(8)),程序会从yield关键字那一行继续向下运行,send会把8这个值赋值给result变量,此时result已经等于8,由于send方法中包含next()方法,所以程序会继续向下运行执行print方法,输出result: 8
  • 6、程序会继续在while里执行,又一次碰到yield,这个时候同样return 出6,然后程序停止,print函数输出的6就是这次return出的6。程序再次暂停,直到再次调用next方法或send方法。
    python爬虫,看完发小阿水决心去城发展,村花都留不住_python_126

6-2-2-4:自定义生成器generator

       了解了yield,并且知道了带有yield的函数就是生成器函数的时候,我们就可以自定义生成器generator了

       我们先看一个例子,比如收集1到10内,是3的倍数的就除以3,是5的倍数的就乘以10,其余的按原值,按照平常的推导器可能推不出涉及分支判断的东西,那么这就需要写方法了,按照平常的可能是这样做

# -*- coding: utf-8 -*-

def list_nums(min_num,max_num):
   return_list = list()
   while min_num <= max_num:
       if min_num%5==0:
           return_list.append(min_num*10)
       elif min_num%3==0:
           return_list.append(min_num/3)
       else:
           return_list.append(min_num)
       min_num+=1
   return return_list

nums = list_nums(1,10)
print(nums)

python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_127

       但是我们知道生成器比列表省资源,所以我们使用生成器来生成,使用的时候带遍历出来

# -*- coding: utf-8 -*-

def gen_nums(min_num,max_num):
   while min_num <= max_num:
       if min_num%5==0:
           yield min_num*10
       elif min_num%3==0:
           yield min_num // 3
       else:
           yield min_num
       min_num+=1

gens = gen_nums(1,10)
print(gens)
for i in gens:
   print(i)

python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_128

6-2-2-5:yield实现协程

       到这里应该都会写了吧,协程不就是方法1执行一半,跳到方法2执行一半,接着调到方法1,方法之间来回跳嘛,那用yield来实现,那就是你yield一下,我yield一下,我们举一个例子来实现生产者和消费者模式

# -*- coding: utf-8 -*-

def consumer(name):
   while True:
       bao_zi = yield
       print(f'{name}吃了第{bao_zi}个包子')

def producer(name):
   con1.__next__()
   con2.__next__()
   cont = 0
   while cont<6:
       cont += 2
       print(f'{name}制作了{cont}个包子')
       con1.send(cont-1)
       con2.send(cont)

if __name__ == '__main__':
   con1 = consumer('吴某')
   con2 = consumer('徐某')
   producer('林某')

       输出结果

林某制作了2个包子
吴某吃了第1个包子
徐某吃了第2个包子
林某制作了4个包子
吴某吃了第3个包子
徐某吃了第4个包子
林某制作了6个包子
吴某吃了第5个包子
徐某吃了第6个包子

       解释一下
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_129

6-2-3:asyncio装饰器

       asyncio装饰器是在python3.4版本出现的东西,在这里我们只演示和简单说明,具体实际和下一节的 6-2-4:async、await关键字一样,所以在下一节会说具体用法,这里看一眼了解一下即可

# -*- coding: utf-8 -*-
import asyncio  # 导入asyncio模块

@asyncio.coroutine	 #注解@asyncio.coroutine的函数,说明这是一个协程函数
def fun1():
   print(1)
   yield from asyncio.sleep(1)     # 遇到IO耗时操作,自动化切换到>tasks中的其他任务
   print(2)

@asyncio.coroutine
def fun2():
   print('A')
   yield from asyncio.sleep(1)     # 遇到IO耗时操作,自动化切换到tasks中的其他任务
   print('B')
# tasks任务列表
tasks = [
   asyncio.ensure_future(fun1()),
   asyncio.ensure_future(fun2())
]
# 事件循环
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

       结果输出

1
A
2
B

       简单说明一下
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_130

6-2-4:async、await关键字

       衔接着上一节,这一节我们先使用async、await关键字替代上一节的@asyncio.coroutine和yield from,演示看一下效果,后面再逐一讲解

# -*- coding: utf-8 -*-
import asyncio  # 导入asyncio模块

async def fun1():
   print(1)
   await asyncio.sleep(1)     # 遇到IO耗时操作,自动化切换到tasks中的其他任务
   print(2)

async def fun2():
   print('A')
   await asyncio.sleep(1)     # 遇到IO耗时操作,自动化切换到tasks中的其他任务
   print('B')
# tasks任务列表
tasks = [asyncio.ensure_future(fun1()),asyncio.ensure_future(fun2())]
# 事件循环
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_131

###3 6-2-4-1:事件循环

       所谓事件循环,可以认为是一个死循环,用于去检测并执行某些代码

       它的伪代码我们可以如下表示

任务列表 = {任务1,任务2,任务3}
while 任务列表:
	可执行的任务列表 = 去任务列表中检查所有的任务,将可执行的任务返回
	已完成的任务列表 = 去任务列表中检查所有的任务,将已完成的任务返回
	for 就绪任务 in 可执行的任务列表:
		执行已就绪的任务
	for 已完成的任务 in 已完成的任务列表:
		在任务列表中移除 已完成的任务

       所以在执行的过程中,任务列表不断的移除已完成的任务,直到任务列表不存在任何任务,结束循环

       如上面例子中我们的代码

import asyncio
# 去生成或获取一个事件循环
loop = asyncio.get_event_loop()
# 将任务放到任务列表
loop.run_until_complete(任务)

       或者

import asyncio
# tasks任务列表
tasks = [asyncio.ensure_future(fun1()),asyncio.ensure_future(fun2())]
# 去生成或获取一个事件循环
loop = asyncio.get_event_loop()
# 将tasks任务列表放到任务列表
loop.run_until_complete(asyncio.wait(tasks))

6-2-4-2:async关键字

       async有什么用,被async修饰的函数,称为协程函数,比如下面的fun1就是协程函数

async def fun1():
   print(1)

       执行协程函数,函数体并不会运行,而是得到一个协程对象,如下

async def fun1():
   print(1)

f = fun1()
print(type(f))

python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_132

       那么协程函数如何运行呢,需要借助事件循环,将我们的协程对象丢到事件循环的任务列表中,事件循环就会帮我们去执行协程函数。如下

# -*- coding: utf-8 -*-
import asyncio
async def fun1():
   print(1)

f = fun1()
# 去生成或获取一个事件循环
loop = asyncio.get_event_loop()
# 协程对象当做任务,丢到任务列表中
loop.run_until_complete(f)

       这样协程函数就运行了
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_133

       当然这是以前的做法,python3.7版本之后,有更简洁的写法,如下

# -*- coding: utf-8 -*-
import asyncio
async def fun1():
   print(1)

f = fun1()
asyncio.run(f)

       这是因为3.7版本run方法的内部帮我们处理了
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_134
       启动,协程函数执行了
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_135

6-2-4-3:await关键字

       await我们拆开来看就是async+wait,async是协程函数,wait是等待,所以await就是协程等待,await所起的作用,就是在协程函数里面,执行了协程函数遇到await就会等待,如果任务列表还有其他任务,线程就会切换到其他任务先执行。如果任务列表只有当前任务,那么就会等待到等待到当前任务的await等待完毕,继续往下执行,这么说你们可能不理解,我们使用几个例子来说明

       await的用法

  • await+可等待对象(协程对象,Future、Task对象,IO等待),这些对象我们后面会说到,需要注意的就是等待协程对象和等待其他的不一样,有区别。因为Future、Task对象,IO等待会主动线程让步,而await+协程对象则不会

       我们先看一个例子,任务列表只有一个任务

# -*- coding: utf-8 -*-
import asyncio
import time
async def fun1():
  print(1) # 第二步:控制台输出1
  start_time = time.time()
  await asyncio.sleep(2)   # 第三步:await+可等待对象,asyncio.sleep(2)属于IO等待,所以会自动切换,任务列表去找去其他任务,发现没有了别的任务,只好等待2秒
  print(f'耗时{time.time()-start_time}') # 第四步:控制台输出耗时

asyncio.run(fun1()) # 第一步:协程对象fun1()加入到任务列表,协程函数fun1开始执行

       结果说明
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_136

       第二个例子,任务列表只有一个任务,等待的是协程对象,等待协程对象不会让出线程

# -*- coding: utf-8 -*-
import asyncio
import time
async def fun1():
  print(1) # 第二步:控制台输出1
  start_time = time.time()
  await fun2() # 第三步:遇到await+协程对象fun2(),协程对象fun2()不会自动切换,所以线程继续等待fun2()执行
  print(f'耗时{time.time()-start_time}') # 第三步:等待fun2()耗时了7秒,控制台输出耗时7.0

async def fun2():
  print(2)  # 第四步:控制台输出2
  start_time = time.time()
  await asyncio.sleep(3)   # 第五步:遇到await+可等待对象,asyncio.sleep(3)属于IO等待,所以会自动切换,任务列表去找去其他任务,发现没有了别的任务,只好等待3秒
  print(f'耗时{time.time()-start_time}') # 第六步:3秒后,控制台输出耗时3.0
  await asyncio.sleep(4)   # 第七步:遇到IO等待,自动切换,任务列表去找去其他任务,发现没有了别的任务,只好等待4秒后,fun2()执行结束,返回给fun1()上一次等待状态继续执行

asyncio.run(fun1()) #第一步:虽然有2个协程函数,但是只有fun1()这个协程对象加入到任务列表 中,fun1()开始执行

       结果

1
2
耗时3.0
耗时7.0

       结果说明
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_137

       第三个例子,协程对象await过一次后,就不能再重用

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
 print(1)
 f2 = fun2()
 await f2
 print(2)
 await f2
 print(3)

async def fun2():
 print('A')

asyncio.run(fun1())

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_138

       第四个例子,任务列表有2个任务,等待的是协程对象,这个例子因为是多个任务,所以用到了下一节说到的tasks,这个先不用管,下一节就会说到,这个例子就是为了演示await等待时的执行顺序的。

# -*- coding: utf-8 -*-
import asyncio
import time
async def fun1():
   print(1)
   start_time = time.time()
   await fun2()
   print(f'耗时{time.time()-start_time}')

async def fun3():
   start_time = time.time()
   print(f'fun3开始等待的时间{start_time}')
   time.sleep(10)
   print(f'fun3耗时{time.time() - start_time}')

async def fun2():
   start_time = time.time()
   print(f'fun2开始等待的时间{start_time}')
   await asyncio.sleep(3)
   print(f'fun2耗时{time.time() - start_time}')
# tasks任务列表
tasks = [asyncio.ensure_future(fun1()),asyncio.ensure_future(fun3())]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

       结果

1
fun2开始等待的时间1608897055.477
fun3开始等待的时间1608897055.477
fun3耗时10.0
fun2耗时10.0
耗时10.0

       结果说明
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_139

       所以说IO等待才回自动切换,与其说自动切换,不如说是主动让出,主动让出线程给其他程序

6-2-4-4:Task对象

       所谓Task对象,通俗易懂的话来说就是在事件循环中添加多个任务的

       Task对象是指:与任务调度,和并发有关,是指帮助在事件循环中并发的向任务列表,添加多个任务。task用于并发调度协程,3.7版本以前可通过这种方式添加多个任务进入事件循环任务列表

  • loop.create_task(协程对象)
  • asyncio.create_task(协程对象)

       如下例子

# -*- coding: utf-8 -*-
import asyncio  # 导入asyncio模块

async def fun1():
  print(1)
  await asyncio.sleep(1)
  print(2)

async def fun2():
  print('A')
  await asyncio.sleep(1)
  print('B')

# 事件循环
loop = asyncio.get_event_loop()
# tasks任务列表,以下3种都可以,所以还是第三种香,直接放入协程对象
tasks = [asyncio.ensure_future(fun1()),asyncio.ensure_future(fun2())]  # 先跑fun1()
#tasks = [loop.create_task(fun1()),loop.create_task(fun2())]   # 先跑fun1()
#tasks = [fun1(),fun2()]             # 先跑fun2()
loop.run_until_complete(asyncio.wait(tasks))

       但是3.7版本之后提供更简便的方式,如下

# 协程对象列表
tasks = [fun1(),fun2()]            
asyncio.run(asyncio.wait(tasks))

       全代码演示一下

# -*- coding: utf-8 -*-
import asyncio  # 导入asyncio模块

async def fun1():
  print(1)
  await asyncio.sleep(1)
  print(2)

async def fun2():
  print('A')
  await asyncio.sleep(1)
  print('B')
# 协程对象列表
tasks = [fun1(),fun2()]
asyncio.run(asyncio.wait(tasks))

       运行结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_140

       3.7版本之后提供了asyncio.create_task(协程对象)方法,使得协程对象加入任务列表

       例子1------fun2()不等待

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
  print(1)
  f2 = asyncio.create_task(fun2('已加入任务列表2'))
  print(2)

async def fun2(name):
  print(f'{name}A')
  print(f'{name}B')

asyncio.run(asyncio.wait([fun1()]))

       结果说明
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_141

       例子2------fun2()等待协程对象

       我们把例子改一下,fun2()里面等待其他协程对象

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
  print(1)
  f2 = asyncio.create_task(fun2('已加入任务列表2'))
  print(2)

async def fun2(name):
  print(f'{name}A')
  await fun3()
  print(f'{name}B')

async def fun3():
  print('X')

asyncio.run(asyncio.wait([fun1()]))

       结果说明
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_142
       我们可以把f2的信息打出来看一下

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
 print(1)
 global f2
 f2 = asyncio.create_task(fun2('已加入任务列表2'))
 print(2)
 print(f2)

async def fun2(name):
 print(f'{name}A')
 await fun3()
 print(f'{name}B')

async def fun3():
 print('X')

f2 = None
asyncio.run(asyncio.wait([fun1()]))
print(f2)

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_143

       例子3------fun2()进行IO等待

       上面的2个例子或许你们都看明白,那么改造一下,再来一个例子,fun2()不是等待协程对象,而是进行IO等待

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
  print(1)
  f2 = asyncio.create_task(fun2('已加入任务列表2'))
  print(2)

async def fun2(name):
  print(f'{name}A')
  await asyncio.sleep(1)
  print(f'{name}B')

asyncio.run(asyncio.wait([fun1()]))

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_144
       结果说明
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_145
       我们可以把f2的信息打出来看一下

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
 print(1)
 global f2
 f2 = asyncio.create_task(fun2('已加入任务列表2'),name='已加入任务列表2')
 print(f2)
 print(2)

async def fun2(name):
 print(f'{name}A')
 await asyncio.sleep(1)
 print(f'{name}B')

f2 = None
asyncio.run(asyncio.wait([fun1()]))
print(f2)

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_146

       例子4------fun2()进行IO等待,传入0秒

       **如果await asyncio.sleep(0)**呢,那么结果是一样的吗,灵魂拷问啊,我们先去看一下asyncio.sleep()的源码
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_147
       所以从源码上我们可以知道,如果await asyncio.sleep(0),线程并没有退让

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
  print(1)
  f2 = asyncio.create_task(fun2('已加入任务列表2'))
  print(2)

async def fun2(name):
  print(f'{name}A')
  await asyncio.sleep(0)
  print(f'{name}B')

asyncio.run(asyncio.wait([fun1()]))

       结果说明
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_148
       我们可以把f2的信息打出来看一下

># -*- coding: utf-8 -*-
import asyncio

async def fun1():
 print(1)
 global f2
 f2 = asyncio.create_task(fun2('已加入任务列表2'))
 print(2)
 print(f2)

async def fun2(name):
 print(f'{name}A')
 await asyncio.sleep(0)
 print(f'{name}B')

f2 = None
asyncio.run(asyncio.wait([fun1()]))
print(f2)

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_149

       例子5------fun2()进行IO等待,但是fun1()等待了f2

       接下来这个例子,fun2()里面进行了IO等待,但是主线程的fun1()也使用await等待了f2,所以fun2()不结束,fun1()永远不会结束

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
  print(1)
  await fun2('未加入任务列表1')
  f2 = asyncio.create_task(fun2('已加入任务列表2'))
  f3 = asyncio.create_task(fun2('已加入任务列表3'))
  await f2
  print(2)

async def fun2(name):
  print(f'{name}A')
  await asyncio.sleep(1)
  print(f'{name}B')

asyncio.run(asyncio.wait([fun1()]))

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_150
       我们可以把f2和f3的信息打出来看一下
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_151

       但是我们不推荐这样的写法,我们还有更好的写法,那就是

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
  print(1)
  tasks = [asyncio.create_task(fun2('已加入任务列表2')),asyncio.create_task(fun2('已加入任务列表3'))]
  await asyncio.wait(tasks)
  print(2)

async def fun2(name):
  print(f'{name}A')
  await asyncio.sleep(1)
  print(f'{name}B')

asyncio.run(asyncio.wait([fun1()]))

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_152

6-2-4-5:Future对象

       上一节我们已经点过,Future对象就是Task对象的父类
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_153

       我们上一节讲到Task的时候,一直await Task对象,那么这个await 等待的就是Task对象的返回结果,而这个结果就是由Future封装的,Future有一个属性_state,当这个属性_state的值变为finished的时候,await就不需要再等待
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_154

       如实例1,await Future,Future没结果,一直等待

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
 print(1)
 loop = asyncio.get_running_loop()
 future = loop.create_future()
 print(future)
 await future
 print(2)

asyncio.run(asyncio.wait([fun1()]))

       结果
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_155

       实例2,await Future,另开协程给Future赋值结果

# -*- coding: utf-8 -*-
import asyncio

async def fun1():
 print(1)
 loop = asyncio.get_running_loop()
 future = loop.create_future()
 asyncio.create_task(fun2(future))
 print(future)
 await future
 print(future)
 print(2)

async def fun2(future):
  future.set_result('设置结果')

asyncio.run(asyncio.wait([fun1()]))

       结果说明
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_156
       我们还可以把等待的结果打印出来
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_157

6-3:aiohttp,基于异步网络请求的模块

       再介绍aiohttp之前,我们先看一个例子的几种方式对比,爬取我房网新房列表的价格走势数据,爬取150个新房数据的24个月价格数据

       单进程单线程爬取,耗时约13秒

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
import json
import time

def price_trend(building_data):
  price_trend_params = {
      'buildingType': building_data['buildingType'],
      'buildingId': building_data['id'],
      'monthNum': 24,
      'siteId': '5'
  }
  price_trend_response = requests.get(url=price_trend_url, params=price_trend_params)
  text = json.loads(price_trend_response.text)
  print('楼盘id【',building_data['id'],'】',text)

if __name__ == "__main__":
  ua = UserAgent()
  url="http://wofang.com/siteMgr/api/building/v1.0/buildingBaseList"
  price_trend_url = "http://wofang.com/siteMgr/api/buildingHistoryPrice/v1.0/priceTrend"
  headers = {
      'User-Agent':ua.random
  }
  params = {
      'isMainType':'1',
      'regionCode':'460100 ',
      'pageNum':'1',
      'pageSize':'150',
      'siteId':'5',
  }
  response = requests.get(url=url,headers=headers,params=params)
  json_data = json.loads(response.text)
  start_time = time.time()
  for data in json_data['list']:
      price_trend(data)
  print(f'耗时{time.time()-start_time}')

       结果,耗时13秒
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_158

       进程池爬取,耗时约3.2秒

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
import json
import time
from concurrent.futures import ProcessPoolExecutor

def price_trend(building_data):
  price_trend_url = "http://wofang.com/siteMgr/api/buildingHistoryPrice/v1.0/priceTrend"
  price_trend_params = {
      'buildingType': building_data['buildingType'],
      'buildingId': building_data['id'],
      'monthNum': 24,
      'siteId': '5'
  }
  price_trend_response = requests.get(url=price_trend_url,params=price_trend_params)
  text = json.loads(price_trend_response.text)
  print('楼盘id【',building_data['id'],'】',text)

if __name__ == "__main__":
  ua = UserAgent()
  url="http://wofang.com/siteMgr/api/building/v1.0/buildingBaseList"
  headers = {
      'User-Agent':ua.random
  }
  params = {
      'isMainType':'1',
      'regionCode':'460100 ',
      'pageNum':'1',
      'pageSize':'150',
      'siteId':'5',
  }
  response = requests.get(url=url,headers=headers,params=params)
  json_data = json.loads(response.text)
  start_time = time.time()
  pool = ProcessPoolExecutor(max_workers=8)
  pool.map(price_trend, json_data['list'])
  pool.shutdown()
  print(f'耗时{time.time()-start_time}')

       结果,耗时约3.2秒
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_159

       线程池爬取,耗时约1.7秒

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
import json
import time
from concurrent.futures import ThreadPoolExecutor

def price_trend(building_data):
  price_trend_url = "http://wofang.com/siteMgr/api/buildingHistoryPrice/v1.0/priceTrend"
  price_trend_params = {
      'buildingType': building_data['buildingType'],
      'buildingId': building_data['id'],
      'monthNum': 24,
      'siteId': '5'
  }
  price_trend_response = requests.get(url=price_trend_url,params=price_trend_params)
  text = json.loads(price_trend_response.text)
  print('楼盘id【',building_data['id'],'】',text)

if __name__ == "__main__":
  ua = UserAgent()
  url="http://wofang.com/siteMgr/api/building/v1.0/buildingBaseList"
  headers = {
      'User-Agent':ua.random
  }
  params = {
      'isMainType':'1',
      'regionCode':'460100 ',
      'pageNum':'1',
      'pageSize':'150',
      'siteId':'5',
  }
  response = requests.get(url=url,headers=headers,params=params)
  json_data = json.loads(response.text)
  start_time = time.time()
  pool = ThreadPoolExecutor(max_workers=8)
  pool.map(price_trend, json_data['list'])
  pool.shutdown()
  print(f'耗时{time.time()-start_time}')

       结果,耗时约1.7秒
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_160

       基于gevent协程爬取,耗时约0.6秒

# -*- coding: utf-8 -*-

from gevent import monkey
monkey.patch_all()
import gevent
import requests
from fake_useragent import UserAgent
import json
import time

def price_trend(building_data):
  price_trend_params = {
      'buildingType': building_data['buildingType'],
      'buildingId': building_data['id'],
      'monthNum': 24,
      'siteId': '5'
  }
  price_trend_response = requests.get(url=price_trend_url,params=price_trend_params)
  text = json.loads(price_trend_response.text)
  print('楼盘id【',building_data['id'],'】',text)

if __name__ == "__main__":
  ua = UserAgent()
  url="http://wofang.com/siteMgr/api/building/v1.0/buildingBaseList"
  price_trend_url = "http://wofang.com/siteMgr/api/buildingHistoryPrice/v1.0/priceTrend"
  headers = {
      'User-Agent':ua.random
  }
  params = {
      'isMainType':'1',
      'regionCode':'460100 ',
      'pageNum':'1',
      'pageSize':'150',
      'siteId':'5',
  }
  response = requests.get(url=url,headers=headers,params=params)
  json_data = json.loads(response.text)
  start_time = time.time()
  green_let_list = [gevent.spawn(price_trend, data) for data in json_data['list']]
  gevent.joinall(green_let_list)
  print(f'耗时{time.time()-start_time}')

       结果,耗时约0.6秒
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_161

       基于asyncio协程爬取,耗时约13秒

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
import json
import time
import asyncio

async def price_trend(building_data):
  price_trend_params = {
      'buildingType': building_data['buildingType'],
      'buildingId': building_data['id'],
      'monthNum': 24,
      'siteId': '5'
  }
  price_trend_response = requests.get(url=price_trend_url,params=price_trend_params)
  text = json.loads(price_trend_response.text)
  print('楼盘id【', building_data['id'], '】', text)

if __name__ == "__main__":
  ua = UserAgent()
  url="http://wofang.com/siteMgr/api/building/v1.0/buildingBaseList"
  price_trend_url = "http://wofang.com/siteMgr/api/buildingHistoryPrice/v1.0/priceTrend"
  headers = {
      'User-Agent':ua.random
  }
  params = {
      'isMainType':'1',
      'regionCode':'460100 ',
      'pageNum':'1',
      'pageSize':'150',
      'siteId':'5',
  }
  response = requests.get(url=url,headers=headers,params=params)
  json_data = json.loads(response.text)
  start_time = time.time()
  tasks = [price_trend(data) for data in json_data['list']]
  asyncio.run(asyncio.wait(tasks))
  print(f'耗时{time.time()-start_time}')

       结果,耗时约0.6秒
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_162

       发现没有,基于asyncio协程爬取,和单进程单线程的时间一样,根本就没有变化,为什么呢,因为基于asyncio协程是异步的,而其协程函数内部不能有同步的代码,requests模块是同步的,所以不起作用,这就需要aiohttp模块

       安装环境

  • pip install aiohttp

       使用

import asyncio
import aiohttp
async def 函数名(参数):
	async with aiohttp.ClientSession() as aio_session:
		async with aio_session.request(method='GET', url=url,params=params,data=data,headers=headers,proxy=proxy) as res:
			data = await res.read()	# 获得二进制
			data = await res.text()		# 获得源码
			data = await res.json()		# 获得json

其中代理proxy不是一字典,而是字符串,更多的参数如下
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_163

       我们的例子如下,基于asyncio协程和aiohttp爬取,耗时约0.47秒

# -*- coding: utf-8 -*-

import requests
from fake_useragent import UserAgent
import json
import time
import asyncio
import aiohttp

async def price_trend(building_data):
   async with aiohttp.ClientSession() as aio_session:
       price_trend_params = {
           'buildingType': building_data['buildingType'],
           'buildingId': building_data['id'],
           'monthNum': 24,
           'siteId': '5'
       }
       async with aio_session.request(method='GET', url=price_trend_url,params=price_trend_params) as are:
           text = await are.text()
           print('楼盘id【', building_data['id'], '】', text)

if __name__ == "__main__":
  ua = UserAgent()
  url="http://wofang.com/siteMgr/api/building/v1.0/buildingBaseList"
  price_trend_url = "http://wofang.com/siteMgr/api/buildingHistoryPrice/v1.0/priceTrend"
  headers = {
      'User-Agent':ua.random
  }
  params = {
      'isMainType':'1',
      'regionCode':'460100 ',
      'pageNum':'1',
      'pageSize':'150',
      'siteId':'5',
  }
  response = requests.get(url=url,headers=headers,params=params)
  json_data = json.loads(response.text)
  start_time = time.time()
  tasks = [price_trend(data) for data in json_data['list']]
  asyncio.run(asyncio.wait(tasks))
  print(f'耗时{time.time()-start_time}')

       结果,耗时约0.47秒
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_164

7:爬虫进阶篇-selenium

       Selenium (WEB自动化工具)Selenium [1] 是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。这个工具的主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。测试系统功能——创建回归测试检验软件功能和用户需求。支持自动录制动作和自动生成 .Net、Java、Perl等不同语言的测试脚本。

       通俗的 话来说,就是selenium可以自动化操作浏览器

       为何selenium会和我们的爬虫相关,因为有些数据是异步动态加载进来的,如果通过requests模块了获取数据的话,是不能直接获取的,,还得通过抓包获取其异步的连接,再获取数据,但是如果selenium自动化操作浏览器的话,还是可以直接获取的

7-1:selenium入门例子

       环境安装

  • pip install selenium

       除了上述环境,还需要浏览器驱动包,毕竟你是需要操作浏览器的,不同的浏览器需要不同的相对应驱动,我们基于谷歌浏览器来说明,并且驱动的版本要和浏览器的版本相对应

  • 驱动下载路:https://chromedriver.storage.googleapis.com/index.html或者http://npm.taobao.org/mirrors/chromedriver/
  • 驱动程序和浏览器的映射关系:

       当然你还要会查看自己浏览器的版本chrome://settings/help
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_165
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_166

       使用步骤

# -*- coding: utf-8 -*-
# 引入包
from selenium import webdriver

# 打开浏览器,executable_path是驱动路径,我的驱动路径放在了python路径下了
driver = webdriver.Chrome()
# 浏览器打开网址
driver.get('url地址')
# 获取数据
print(driver.page_source)
# 进行其他操作
# 关闭当前窗口
driver.close()
# 退出驱动关闭所有窗口
driver.quit()

       因为我的驱动已经放在python的路径下了
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_167
       所以调用webdriver.Chrome()的时候,我不需要传executable_path参数,因为默认参数就是python的路径下
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_168

       我们使用一个例子来演示一下,那就药监局吧,http://scxk.nmpa.gov.cn:81/xk/,我们获取药监局里的企业名称等相关数据
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_169

       使用普通爬虫requests模块

# -*- coding: utf-8 -*-

import requests

response = requests.get('http://scxk.nmpa.gov.cn:81/xk/')
print(response.text)

       结果却得不到我们的数据
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_170
       为什么,这是因为表数据是异步加载进来的,所以这里获取不到,当然,通过抓包方式获取其异步的连接,也可以获取到其数据,但是我们不想这么麻烦,那么使用selenium就可以直接拿到我们想要的数据

       selenium方式

# -*- coding: utf-8 -*-
# 引入包
from selenium import webdriver

# 打开浏览器,executable_path是驱动路径,我的驱动路径放在了python路径下了
driver = webdriver.Chrome()
# 浏览器打开网址
driver.get('http://scxk.nmpa.gov.cn:81/xk/')
# 获取数据
print(driver.page_source)
# 关闭当前窗口
driver.close()
# 退出驱动关闭所有窗口
driver.quit()

       结果有我们想要的数据,至于解析就得你们自己去解析了
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_171

7-2:selenium其他自动化操作

       selenium提供了很多的自动化操作,如下

  • 发起请求:get(url),这个我们入门例子已经演示过
  • 标签定位:find系列方法
    python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_172
  • 标签交互:标签.send_keys(‘xxx’),给对应标签设置值
  • 执行js程序:driver.excute_script(‘js代码’)
  • 前进,后退:back(),forward()
  • 关闭当前窗口:driver.close()
  • 退出驱动关闭所有窗口:driver.quit()
  • 更多请参考官网https://www.selenium.dev/documentation/en/

       下面我们使用一个例子来演示一下,登录TAPD,登录地址为https://www.tapd.cn/cloud_logins/login
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_173
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_174
       下面是代码示例,示例中会有很多time.sleep(),主要是为了停顿让你们看到效果

# -*- coding: utf-8 -*-
# 引入包
from selenium import webdriver
import time
LOGIN_NAME = '你的账号'
LOGIN_PASSWORD = '你的密码'  
# 打开浏览器
driver = webdriver.Chrome()
# 等待3秒,让浏览器加载一会
driver.implicitly_wait(3)
# 打开登录页面
driver.get('https://www.tapd.cn/cloud_logins/login')
time.sleep(2)
# 找到用户名标签
username_element = driver.find_element_by_id('username')
# 用户名标签设置值
username_element.send_keys(LOGIN_NAME)
# 找到用密码标签
password_element = driver.find_element_by_id('password_input')
# 密码标签设置值
password_element.send_keys(LOGIN_PASSWORD)
># 执行登录
#driver.find_element_by_id('tcloud_login_button').click()
driver.execute_script('document.getElementById("tcloud_login_button").click()')
# 等待3秒
time.sleep(5)
# 访问百度
driver.get('https://www.baidu.com')
time.sleep(2)
# 退回tapd
driver.back()
time.sleep(2)
# 退出驱动
driver.quit()

       效果,因为录屏软软件的原因,所以切换的时候,右边会是绿色的,这里主要是为了和大家展示效果
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_175

7-3:iframe

       我们先看一个例子,我们想获取这个链接https://www.runoob.com/try/try.php?filename=tryhtml5_draganddrop下的BUNOOB.COM的标签
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_176
       是不是觉得很简单啊,通过id就可以获取是不是,那么我们获取打印来看一下

# -*- coding: utf-8 -*-
# 引入包
from selenium import webdriver

# 打开浏览器
driver = webdriver.Chrome()
# 等待3秒,让浏览器加载一会
driver.implicitly_wait(3)
# 打开页面
driver.get('https://www.runoob.com/try/try.php?filename=tryhtml5_draganddrop')
# 找到run_oob
run_oob = driver.find_element_by_id('drag1')
print(run_oob)
driver.quit()

python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_177
       报错了,说是找不到这个标签,但是我们的id是正确的啊,为何找不到,这就涉及到iframe了,我们看一下,我们所要找额标签是另在一个iframe下
python爬虫,看完发小阿水决心去城发展,村花都留不住_解析_178

       这就涉及作用域的问题,我们刚访问进页面的时候,作用域是在最外层的iframe,而要想访问另外iframe里面的元素,就得切换作用域

  • driver.switch_to.frame(‘iframed’)

       所以我们切换再看一下

# -*- coding: utf-8 -*-
# 引入包
from selenium import webdriver

# 打开浏览器
driver = webdriver.Chrome()
# 等待3秒,让浏览器加载一会
driver.implicitly_wait(3)
# 打开页面
driver.get('https://www.runoob.com/try/try.php?filename=tryhtml5_draganddrop')
# 切换iframe的作用域
driver.switch_to.frame('iframeResult')
# 找到run_oob
run_oob = driver.find_element_by_id('drag1')
print(run_oob)
driver.quit()

       结果拿到了我们的标签
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_179

7-4:动作链,如滑块验证码

       什么叫动作链呢,比如我们需要拖动某一个物件移动,这就要动作链了

       如我们移动这个滑块
python爬虫,看完发小阿水决心去城发展,村花都留不住_协程_180
       而这个滑块出来的前提,是要点击获取验证码
python爬虫,看完发小阿水决心去城发展,村花都留不住_爬虫_181

       这里我们只是演示移动滑块,并没有真正的去破解他啊,至于如何破解,该滑动多长距离,你们可以自己想办法啊,比如找出那2张图的距离等等

# -*- coding: utf-8 -*-
# 引入包
from selenium import webdriver
from selenium.webdriver import ActionChains
import time
# 打开浏览器
driver = webdriver.Chrome()
# 等待3秒,让浏览器加载一会
driver.implicitly_wait(3)
# 打开页面
driver.get('http://www.geetest.com/Register')
# 定位填入手机号
phone = driver.find_element_by_xpath('//*[@id="gt-register-mobile"]/div/div[2]/div[1]/div[2]/div/div[2]/div[1]/input')
phone.send_keys('15103631910')
# 定位验证码单击
code = driver.find_element_by_xpath('//*[@id="gt-register-mobile"]/div/div[2]/div[1]/div[2]/div/div[2]/div[2]/div[1]/div')
code.click()
# 定位到滑块
slider = driver.find_element_by_xpath('/html/body/div[3]/div[2]/div[6]/div/div[1]/div[2]/div[2]')
# 动作链
action = ActionChains(driver)
# # 点击长按指定的标签
action.click_and_hold(slider)
for i in range(4):
   action.move_by_offset(12,0).perform()
action.release()
time.sleep(5)
driver.quit()

       效果
python爬虫,看完发小阿水决心去城发展,村花都留不住_python_182

7-5:无头浏览器界面+规避检测

       前面的例子中,我们控制浏览器的时候,代码执行弹出一哥浏览器,这个看起来不是太友好,所以有没有一种方式是,既可以模拟浏览器执行,但是又不弹出来,有,那就是无头浏览器,下面的自己看着直接用就可以

       导包

# 浏览器
from selenium import webdriver
# 无头浏览器
from selenium.webdriver.chrome.options import Options
# 规避检测
from selenium.webdriver import ChromeOptions

       代码

#实现无可视化界面操作
chrome_options = Options()
chrome_options.add_argument('--headless') #浏览器不提供可视化页面. linux下如果系统不支持可视化不加这条会启动失败
chrome_options.add_argument('--disable-gpu') #谷歌文档提到需要加上这个属性来规避bug
#针对UA请求头的操作,防止因为没有添加请求头导致的访问被栏截了,我这里配的是谷歌浏览器,你们想用的浏览器自己配
chrome_options.add_argument('User-Agent=Mozilla/5.0 (Windows NT 6.1; Win64; x64) >AppleWebKit/537.36 (KHTML, like >Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.57')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--hide-scrollbars') #隐藏滚动条, 应对一些特殊页面
chrome_options.add_argument('blink-settings=imagesEnabled=false') #不加载图片, 提升速度
#实现规避操作
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
browser = webdriver.Chrome(chrome_options=chrome_options, options=option)
#browser = webdriver.Chrome()
``
8:爬虫高级篇-scrapy爬虫框架

       本篇博客到此就结束了,至于scrapy爬虫框架我正在写,还没写完,因为篇幅太多,所以并没有和此篇放在一起,等我写完scrapy爬虫框架篇,会把地址放到这里