Python爬虫|基础知识点详细汇总(requests、urllib、re、bs4、xpath、PyQuery、jsonpath、多线程、协程、数据保存、selenium)

发布于:2023-02-09 ⋅ 阅读:(1231) ⋅ 点赞:(1)

爬虫总结

一、静态页面 html 代码的获取

1. 请求数据

① requests

(1) 基本使用
url = 'http://www.baidu.com'  # 这里不要用https

resp = requests.get(url)  # 请求数据
resp = requests.post(url=url, data=data)  # post方式请求数据(只需要加个字典类型的data即可)
resp.encoding = 'utf-8'  # 根据html的charset来设定编码方式,可以避免中文乱码

print(resp)  # <Response [200]>  -- 是一个Request对象,包含响应头、html之类的

# ★ 拿到网页源代码(html)
html = resp.text  # 拿到网页源代码(html)、数据       --- str类型
html = resp.content  # 拿到网页源代码(html)、数据    --- bytes类型   (下载图片可以如此)

参数

url: 获取页面的url链接

params:url中的额外参数,字典或者字节流等形式,可以选择
        # params(GET参数)是指  url中的额外参数,字典或者字节流格式,是可选参数。可以让url不那么冗长,使代码看起来更加整齐
        params = {
            'version'='0.12',
            'language'='en',
            ...
         }
        resp = requests.get(url, params)  / requests.post(url, data ,params)

data:POST参数     --- ⚠ post方法才有
headers:请求头(反爬)
cookies:Cookies
**kwargs:12个控制访问的参数
...

对响应内容的操作

resp = requests.get(url)  # 请求数据
resp = requests.post(url)  # 请求数据
resp.encoding = 'utf-8'  # 设置数据的编码

resp.text  # 返回响应的内容(拿到页面源代码(html)、数据)
resp.json()  # 如果返回的是json格式,可以直接resp.json()可以正常显示中文
resp.content  # 返回响应的内容(拿到页面源代码(html)、数据)(bytes形式)

resp.request.headers  # 拿到请求头
resp.headers  # 拿到响应头
resp.status_code  # 响应代码,200表示成功
(2) Requests进阶:使用Session

为什么要用 Session?
Session代表服务器与浏览器的一次会话过程,Session对象存储了特定用户会话所需的信息
例如:一定时间内记录账号密码 (自动登录)
可以加快 requests请求速度

import requests

url = 'http://www.baidu.com'
resp = requests.Session().request(method='get', url=url)
resp.encoding = 'utf-8'
html = resp.text
print(html)

需要客户端登录的网站

客户端登录 --> 服务器返回 cookie  给客户端
客户端带着 cookie 再去请求数据 --> 服务器知道你是谁
# 需要登录的网站一般可以这样进行登录
resp = requests.post(url,data={
    'loginName':' xxx',
    'password':' xxx',
})
# 或
resp = requests.post(url,headers={
    'Cookie':'xxxxxxxx'  # 抓包复制来的
})
print(resp.text)
# 这两种方法 缺点是每次都得传入cookie信息


# ★ 使用 Session 可以记录 cookie 信息
url = 'https://h5.17k.com/'
# 登录信息
data = {
    'loginName':'xxx',
    'password':'xxx',
}
# 创建会话
session = requests.Session()
# 1. 登录
resp = session.post(url=url,data=data,)
# print(resp.text)
print(resp.cookies)

# 2. 拿我的书架上的数据
# 刚才的那个Session中是有cookie的,还是使用刚才的Session
resp2 = session.get('https://user.17k.com/ck/author/shelf?platform=4&appKey=1351550300')
print(resp2.text)

(3) 防盗链处理
【★注意】很多 页面源代码 和 开发者工具的Elements页面代码 是存在偏差的
页面源代码是不会改变的
Elements页面代码是实时的网页效果的源代码,很多内容可能是Js渲染出来的内容,在网页源代码中是找不到的!
'''
    防盗链
        Referer验证 --- 溯源
            找当前请求的上一级(上一个页面)是谁,如果找不到,就认为有问题,不会返回需要的相响应内容

'''
import requests

url = 'https://www.pearvideo.com/video_1756772'
contId = url.split('_')[1]

# 抓包,请求视频html相关内容(不在原代码中)
videoStatusUrl = f'https://www.pearvideo.com/videoStatus.jsp?contId={contId}&mrd=0.7303948894191317'
headers={
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36',
    # 防盗链:溯源
    'Referer':url
}
# 请求视频详情页数据
resp = requests.get(videoStatusUrl,headers=headers)
# print(resp.json())  # 因为是json形式的str,直接转为json好处理
dict = resp.json()
srcUrl = dict['videoInfo']['videos']['srcUrl']
systemTime = dict['systemTime']
# 对视频链接处理(替换、拼接成完整的视频地址)
srcUrl = srcUrl.replace(systemTime,f'cont-{contId}')
# print(srcUrl)
resp2 = requests.get(srcUrl)

# 下载视频
with open('myVidoe.mp4','wb') as f:
    f.write(resp2.content)
(4) 代理ip
# 原理:通过第三方的一个机器去发送请求(借用他人ip)
#当需要采集大量数据时,或者有的网站对访问速度特别严格的时候,有的网站就采取封ip,这样就需要使用代理ip。就像马蜂窝一样,,自从被曝数据造假之后,就不好爬了,python使用代理ip的小demo为:
import requests

# ★ 代理ip的使用
# 检测IP地址的网站
url = 'http://httpbin.org/ip'
# 设置代理ip
proxy = {
    'http':'http://218.238.83.182:80',  # 根据类型选择 http 或 https
    'https':'https://172.157.83.198:80',
}
res = requests.get(url,proxies=proxy)
print(res.text)
'''
{
  "origin": "218.238.83.182"
}
'''


# ★ 检测代理ip是否能用
ips = [('http://218.238.83.182:80'),('http://219.146.125.162:9091'),('http://112.80.248.73:80'),('http://125.123.121.223:766'),('http://114.100.3.14:3617'),('http://47.92.113.71')]
url = 'http://httpbin.org/ip'

for i in ips:
    try:
        res = requests.get(url,proxies={'http':i},timeout=1)
        print(res.text)
    except Exception as e:
        print('出现异常',e)

② urllib & urllib3

urllib

import urllib

url = 'https://www.baidu.com'
# 一、简单获取一个get请求 (不修改headers)
resp = urllib.request.urlopen(url)

# 二、简单获取一个post请求 (不修改headers)

# Post请求需要向服务器发送表单内容,用urlopen中的data参数
# data 是 bytes 字节型数据,需要用 bytes() 转换成字节型
# 如果data被赋值,则请求的方式就会由get转为post,而post需要提供一些待处理的数据data
# 这些待处理的数据需要一定的格式,因此就需要 urllib.parse.urlencode() 编码
# 【注】不能对string编码,只能对dict类型编码
import urllib.parse
data = bytes(urllib.parse.urlencode({'name': 'Syy'}), encoding='utf-8')
resp = urllib.request.urlopen(url=url, data=data)

# ★ 拿到网页源代码(html)
# 拿到网页源代码(html)、数据                        --- str类型
html = resp.read().decode('utf-8')  # 以utf-8的方式对获取到的网页源码进行解码,可以更好的解析网页,解析换行符、中文等
html = resp.read()  # 拿到网页源代码(html)、数据    --- bytes类型


# 三、拿到请求头的一些属性值
print(resp.status)  # 200 (状态码)
# 查看所有属性值(有s)
print(resp.getheaders())
# 查看所有属性值(无s)
print(resp.getheader('Server'))


# 四、常用的其他方法
# 下载网页、图片、视频 的已封装函数方法
urllib.request.urlretrieve(url=url, filename=filename)
# 将汉字变成unicode编码
# 转换的unicode码可以 拼接到url后面作为参数 也可以 通过data传参进去(post请求时)
urllib.parse.quote(str)
urllib.parse.urlencode({'key1': 'val1', 'key2': 'val2'})  # 将字典中的键值对用&拼接起来(post传入data需要.encode('utf-8'))


# 五、伪装成浏览器(将User-Agent由爬虫改为浏览器的信息)
# ① Post 请求
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
}
# data = bytes(urllib.parse.urlencode({'name': 'Eric'}), encoding='utf-8')
data_ = urllib.parse.urlencode({'name': 'Eric'}).encode('utf-8')  # 两种写法都可以
# ⭕构建(封装)一个请求对象
req = urllib.request.Request(url=url, headers=headers, data=data, method='POST')
resp = urllib.request.urlopen(req)

# ② Get请求
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
}
# 构建(封装)一个请求对象
req = urllib.request.Request(url=url, headers=headers)
resp = urllib.request.urlopen(req)

urllib3

import urllib3
url = 'http://www.baidu.com'

http = urllib3.PoolManager(n)  # 创建连接池管理对象,参数可填几个,可以实现向多个服务器发送请求
resp = http.request(method='get',url=url)
resp = http.urlopen(method='get',url=url)  # urlopen 跟 request方法效果一样,但无法直接添加headers,因此用的少

print(resp)  # <urllib3.response.HTTPResponse object at 0x0000014DFF9459A0>  是一个响应对象

# ★ 拿到网页源代码(html)
html = resp.data.decode('utf-8')  # 拿到网页源代码(html)、数据       --- str类型
html = resp.data  # 拿到网页源代码(html)、数据                       --- bytes类型   (下载图片可以如此)

③ selenium (webdriver)

from selenium import webdriver

# (注意:驱动需要与浏览器版本兼容)
# 配置Chrome启动时属性的类ChromeOptions
options = webdriver.ChromeOptions()
options.add_argument('--headless')  # 设置 无界面浏览  # 还可以设置UA、IP代理等等

driver = webdriver.Chrome(options=options)  # 创建了 Chrome 实例
driver.get('http://www.baidu.com')  # 发送请求,请求数据

html = driver.page_source  # 拿到网页源代码(html)、数据       --- str类型

2. 节点获取 / 内容匹配

① re

正则表达式

1. 语法
'''
    正则:用来匹配字符串的一门表达式语言
    测试的方法:https://tool.oschina.net/regex/

    1.正则支持所有普通字符
    2.元字符,就一个符号来匹配一堆内容
        \d  --能够匹配一个数字(0-9)
        \D  --匹配除了数字(0-9)的内容
        \w  --能够匹配数字、字母、下划线(0-9,a-z,A-Z,_)
        \W  --除了数字、字母、下划线(0-9,a-z,A-Z,_)以外的内容
        [abc]  -- 匹配字母 a 或 b 或 c 的内容
        [^abc] -- 匹配除了字母 a 或 b 或 c 的内容
        . -- 匹配 除了换行符以外 的所有内容

    3.量词,控制前面元字符出现的频次
        +  -- 前面的元字符出现 1 次或多次
        *  -- 前面的元字符出现 0 次或多次,贪婪匹配的,尽可能多的拿到数据
        ?  -- 前面的元字符出现 0 次或 1 次
        {n}    -- 匹配 n 次
        {n,m}  -- 最少匹配 n 次且最多匹配 m 次
        .  -- 匹配 除了换行符以外 的所有内容
        ^  -- 匹配要检索的文本的开头
        $  -- 匹配文本的结束(限制作用)
        ^xxx$ -- 表示精准匹配,必须完全满足^$之间的模式
        () -- 表示分组(括号中的内容作为一个整体)
        (?P<组名>正则表达式)  -- ?P表示要分组,<>内为组名,后面为匹配模式

    4.惰性匹配  : .*?
        例如: 玩儿吃鸡游戏,晚上一起打游戏,干嘛呢?打游戏啊!
             正则:玩儿.*游戏        .*  -- 找最远的"游戏"   (即 -- 中间尽可能多)
             结果:玩儿吃鸡游戏,晚上一起打游戏,干嘛呢?打游戏

             正则:玩儿.*?游戏        .*? -- 找最近的"游戏"  (即 -- 中间尽可能少)
             结果:玩儿吃鸡游戏
        😊.*? : 惰性匹配     xxx.*?xx-- 匹配xxx 到最近的xx为止内容,😊
'''
'''
re.findall(r'',str)  # 匹配正则表达式匹配的内容  r''--原始字符串,避免转义字符的影响  返回一个装有匹配结果的列表
re.search(r'',str)  # 只拿到第一个结果就返回
re.finditer(r'',str)  # 匹配正则表达式匹配的内容,返回的是一个迭代器对象

# findall 和 finditer  的结果效果相同,选择哪个主要看获取的内容大小,如果太大,选择finditer比较合适,占用内存少
# 结合写法:
re.findall(re.compile(r''),str)

# 预加载
# 提前准备好一个正则对象
# re.compile 用于编译正则表达式,生成一个 Pattern 对象
# 此对象可以使用 findall、finditer、search等等各种re方法
obj = re.compile(r'')

obj.search(str)
obj.finditer(str)
obj.findall(str)

(?P<组名>正则表达式)  -- ?P表示要分组,<>内为组名
item.group()  # 打印当前匹配到的内容(只能用于 re.search和 re.finditer 的结果内容的遍历)
item.group('组名')  # 打印当前匹配到的指定组的内容  # 必须在设定了组名的时候才能使用
item.groupdict()  # 必须在设定了组名的时候才能使用
'''
# 一、findall()
result = re.findall(r'\d+', '今天我有100块,买了2块蛋糕')  # 匹配正则表达式匹配的内容  r''--原始字符串,避免转义字符的影响  返回一个装有匹配结果的列表
print(result)  # ['100', '2']

'''
注意:search 和 finditer中,()括起来的内容是分组内容,是要输出的内容,不用括号括起来的内容至会匹配不会输出!
'''
# 二、search()
result = re.search(r'(?P<text>\d+)', '今天我有100块,买了2块蛋糕')  # 只拿到第一个结果就返回
print(result.group())  # 输出所有分组内容,即()括起来的内容
print(result.group('text'))  # 分组起名后可以按名称查询

# 三、finditer()
result = re.finditer(r'\d+', '今天我有100块,买了2块蛋糕')  # 匹配正则表达式匹配的内容,返回的是一个迭代器对象
for item in result:
    print(item.group())
    # result = re.finditer(r'(?P<text>\d+)', '今天我有100块,买了2块蛋糕')  # 匹配正则表达式匹配的内容,返回的是一个迭代器对象
    print(item.group('text'))  # 分组起名后可以按名称查询
    print(item.groupdict())  # 字典格式打印所有匹配的组

# 四、sub()
# re.sub() 利用正则来实现字符替换
print(re.sub('a','A', 'abcdefghijklmn'))  # 找到a用A替换
# 建议在正则表达式中,在被比较的字符串前面加上r,这样不用担心转义字符的问题
print(re.sub('a','A', r'\abcdefgh\ijklmn\l'))
# ★ 例子2:预加载
# 提前准备好一个正则对象
# re.compile 用于编译正则表达式,生成一个 Pattern 对象
obj = re.compile(r'\d+')   # 此对象可以使用 findall、finditer、search等等各种re方法
result = obj.findall('今天我有100块,买了2块蛋糕')
obj.search('今天我有100块,买了2块蛋糕')
obj.finditer('今天我有100块,买了2块蛋糕')
print(result)
2. 实战
# 测试正则表达式用于爬虫
html = requests.get(url).text
html = '''
<div class="abc">
    <div><a href="baidu.com">我是百度</div>
    <div><a href="qq.com">我是腾讯</div>
    <div><a href="162.com">我是网易</div>
</div>
'''

# ★ finditer 方法
obj = re.compile(r'<div><a href="(?P<url>.*?)">(?P<text>.*?)</div>')
result = obj.finditer(html)
for item in result:
    print(item.group('url'), end='  ')
    print(item.group('text'))
print(item.groupdict())  # 字典格式打印所有匹配的组
'''   
     baidu.com  我是百度
     qq.com  我是腾讯
     162.com  我是网易
     {'url': '162.com', 'text': '我是网易'}
'''


# ★ findall 方法
obj = re.compile(r'<div><a href="(.*?)">(.*?)</div>')
result = obj.findall(html)  # result是一个列表
print(result[0])  # 可以直接输出
# findall 和 finditer  的结果效果相同,选择哪个主要看获取的内容大小,如果太大,选择finditer比较合适,占用内存少

# ★ search 方法
obj = re.compile(r'<div><a href="(?P<url>.*?)">(?P<text>.*?)</div>')
result = obj.search(html)
print(item.group('url'), end='  ')
print(item.group('text'))
print(item.groupdict())  # 字典格式打印所有匹配的组

② bs4

漂亮的汤

1. 语法
'''
BeautifulSoup4将复杂html文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4种:
    - Tag
    - NavigableString
    - BeautifulSoup
    - Comment
'''
from bs4 import BeautifulSoup
html = requests.get(url).text

# 解析内容:html,用的解析器:html.parser
bs = BeautifulSoup(html, 'html.parser')

# 1.拿到整个文档(html)
print(bs)
print(type(bs))  # <class 'bs4.BeautifulSoup'>

# 2.Tag  --标签及其内容:找到的第一个标签及其内容
# bs.标签名
# 找到第一个出现的标签及其所有内容
print(bs.title)  # <title>百度一下,你就知道 </title>
print(bs.a)
# print(bs.div)
print(type(bs.title))  # <class 'bs4.element.Tag'>

# 3.NavigableString  --标签里的内容(字符串)
# bs.标签名.string
print(bs.title.string)  # 百度一下,你就知道

# 4.拿到一个标签的所有属性
# bs.标签名.attrs
print(bs.a.attrs)

# 5. Comment  --注释类型,是一种特殊的NavigatableString,输出的内容不包含注释符号
print(bs.a.string)  # 新闻
print(type(bs.a.string))  # <class 'bs4.element.Comment'>
# 解析内容:html,用的解析器:html.parser
bs = BeautifulSoup(html, 'html.parser')

# 【节点获取】
# 一、文档的遍历
# contents  -- 返回一个列表,里面装的是标签中的内容
print(bs.div.contents)
print(bs.div.contents[0])
print(bs.div.contents[1])
print(type(bs.div.contents[1]))  # <class 'bs4.element.Tag'>

# 二、 文档的搜索  (更重要)
# 1.find(标签,属性=属性值)  --查询第一个符合条件的节点
a = bs.find('a',class_='bri')
aa = bs.find('a',attrs={'class':'bri'})  # 同上
print(a.text)  # 更多产品
# 拿到某个属性值
print(a.get('href'))

# 2.find_all()  --查询所有指定节点,返回的是列表
# ① 字符串过滤:会查找所有与字符串完全匹配的节点内容
t_list = bs.find_all('a')  # ★ 查找所有a标签
print(t_list)

# ② 正则表达式
# 正则表达式搜索,使用search()方法来匹配内容
t_list = bs.find_all(re.compile('a'))  # ★ 查找所有带有'a'字符的节点
print(t_list)


# ③ 方法:传入一个函数:根据函数的要求来搜索
def name_is_exists(tag):
    # 标签中有name属性才会筛选出来(拿到)
    return tag.has_attr('name')

t_list = bs.find_all(name_is_exists)  # 这里只需要写函数名
print(t_list)

# 3.kwargs 参数
# 指定一些参数搜索:class_、id...
t_list = bs.find_all(id='head')
# html的class属性要加个下划线_,为了和Python的内部class区分开来!
t_list = bs.find_all(class_=True)  # 这里是指有class属性的标签,而不是class=True的标签
t_list = bs.find_all(class_="mnav")  # 这里是指class="mnav"的标签
t_list = bs.find_all(href="http://news.baidu.com")
for item in t_list:
    print(item)

# 4.text 参数
# 查找文本
t_list = bs.find_all(text='hao123')
t_list = bs.find_all(text=['hao123', '百度', '贴吧'])
t_list = bs.find_all(text=re.compile('\d'))  # 应用正则表达式查找含有特定文本的内容(标签里的字符串)  # ['hao123']

for item in t_list:
    print(item)

# 5.limit 参数
t_list = bs.find_all('a', limit=3)  # 查找前 3 个 a标签
print(t_list)

# 6.指定属性查找
# ★ 既是某个标签,又有某种属性
t_list = bs.find_all('a', class_='mnav')
t_list = bs.find_all('a', {'class': 'mnav', 'id': '', 'name': 'tj_trnews'})  # 获取带有这些属性的<a>标签
print(t_list)

# ★【css选择器】
t_list = bs.select('title')  # 通过标签来查找
t_list = bs.select('.mnav')  # 通过标class来查找
t_list = bs.select('#u1')  # 通过id来查找
t_list = bs.select('a[class="bri"]')  # 通过属性值来查找
t_list = bs.select('head > title')  # 通过子代选择器来查找
t_list = bs.select('.mnav ~ .bri')  # 兄弟选择器
print(t_list[0].get_text())  # 获取文本

2. 实战
# 2.逐一解析数据
soup = BeautifulSoup(html, "html.parser")

# soup.find_all('div', class_='item')  # 查找指定class='item'的div节点
for item in soup.find_all('div', class_='item'):  # 带有 class = item 的div
    # print(item)  # 测试查看item(筛选后的html内容)
    print(type(item)) # <class 'bs4.element.Tag'> 
    item = str(item)  # 转换成字符串str,方便后面用正则表达式

③ xpath

1. 语法
正则表达式 re模块 适合一长段凌乱的字符串的匹配
xpah lxml-->etree 适合层次鲜明的节点的层层获取
⚠️ xpath 语法提取的结果是一个列表!
'''
    xpath解析
    xpath 是针对 xml 创建的一种表达式语言,可以从 xml 中直接提取数据
    html 是 xml 的子集,因此 xpath 也可以处理 html
'''
'''
    from lxml import etree 从包中导入模块
    
    # etree.HTML 用来解析字符串格式的HTML文档对象,将传进去的字符串转变成_Element对象。
	# 作为_Element对象,可以方便的使用getparent()、remove()、xpath()等方法。
    et = etree.HTML(html)       -- 加载数据,返回element对象
    et.xpath('')                -- 匹配节点

    /html/body/div/span/text()  -- text() : 拿到标签内的文本
    /html/body/div/span/@属性    -- @属性 : 拿到某个属性值
    /html/*/span/@属性           -- * 通配符,任意的字符,啥都行
    et.xpath('//a/@href')                       -- // 表示从任意位置开始找
    et.xpath('//span[@class="title"]/text()')   -- [@属性名xxx=属性值xx] 表示属性上的限定
    href = item.xpath('./a/@href')[0]           -- ./表示当前这个元素
    
    补充:
        ① 标签同时具有两个属性值,使用and连接即可
        '//div[@class="icon" and @id="next"]/@href'
        同一个属性有两个值,可以用contains(@属性名=属性值)来限定,也可以直接写到一起
        .xpath('//div[@id="a" and contains(@class,"b") and contains(@class,"c")]/text()
        .xpath('//div[@class="b c")]/text() 写在一起也是可以的!
        
        ② XPath常用规则
        / (直接)子节点
        // 所有子孙节点  (后代)
        .  选取当前节点
        .. 表示父节点
        *  表示任意节点
        [1] 表示第一个某元素    -- //div[1] 第一个div元素 , ⚠ 不是从0开始计数,从1开始计数!⚠
        [last()] 表示最后一个  -- //div[last()] 最后一个dv  |   //div[last() - 1] 倒数第二个dv
        [position() < 3]  前两个某元素  -- //div[position() < 5]   前4个div
        contains(@属性名,属性值) 某元素的某个属性中包含某个值  -- div[contains(@class,'abc')]class中含有'abc'的div
        starts-with(@属性名,属性值) 选择以 某个属性名=xxx 开头的元素
        string(.)  可以提取()内的所有元素的text(),并且帮你拼起来,这样就不需要用正则先清理一遍标签了(适用于返回的内容既有标签又有文本时进行提取纯文本)
'''

from lxml import etree

# ① 加载准备解析的数据 (爬取数据)
f = open('text.html', mode='r', encoding='utf-8')
html = f.read()
print(html)  # 拿到了html

# ② 加载数据,返回element对象
# et = etree.parse('',etree.HTMLParser())  # 解析内容,解析方式
# etree.HTML 用来解析字符串格式的HTML文档对象,将传进去的字符串转变成_Element对象。
# 作为_Element对象,可以方便的使用getparent()、remove()、xpath()等方法。
et = etree.HTML(html)  # et是一个element对象

# xpath的语法

# 不用//,就必须从/html开始
result = et.xpath('/html')  # /html表示根节点
result = et.xpath('/html/body')  # 表达式中间的/表示一层html节点
result = et.xpath('/html/body/div/span')
result = et.xpath('/html/body/div/span/text()')  # text()表示提取标签中的文本信息
result = et.xpath('/html/body/div/*/li/a/text()')  # * 通配符,任意的字符,啥都行
result = et.xpath('/html/body/div/*/li/a/@href')  # @表示属性
print(type(result))  # <class 'list'>
print(type(result[0]))  # <class 'lxml.etree._Element'>

result = et.xpath('//a/@href')  # // 表示从任意位置开始找  -- 任意位置开始的 a标签的 href属性值

result = et.xpath('//span[@class="title"]/text()')  # [@属性名xxx=属性值xx] 表示属性上的限定

# 满足包含多个属性的元素
result = et.xpath('//div[contains(@class,"pic") and contains(@id,"next")]/span/text()')  
result = et.xpath('//div[@class="pic next"]/span/text()') # 直接写一起也是可以的!
# 包含 @class="pic"并且 @id="next" 的 div节点

# 选择以 某个属性名=xxx 开头的元素
result = et.xpath('//li[starts-with(@name,"navIndex")]/ul/li/a')

# 成对出现(匹配到多个节点)
# 用循环实现
result = et.xpath('/html/body/div[@class="item"]/ul/li')
for item in result:
    href = item.xpath('./a/@href')[0]  # ./表示当前这个元素
    text = item.xpath('./a/text()')[0]
    print(href, text)
    
# 查找一次完毕,基于查找到的节点,继续往下查找(相对某个节点查找)
divs = et.xpath('/html/body/div[@class="item"]')
a = divs[0].xpath('./ul/li/a')  # ./ 表示从当前节点的下一层开始找
print(a[0].text)

# 用 string() 提取纯文本(在 scrapy框架中常用)
print(a[0].xpath('string(.)'))  # 腾讯QQ

# 小技巧:
# 控制台的 Element 中选中想要的节点,右键 Copy --> Copy Xpath!快速拿到 Xpath 写法!

更多内容: https://blog.csdn.net/qq_50854790/article/details/123610184

2. 实战
html = requests.get(url)

# ② 加载数据,返回element对象
et = etree.HTML(html)  # et是一个element对象

res1 = et.xpath('/html/body/div/span/text()')  # text()表示提取标签中的文本信息
print(res1)

result = et.xpath('//div[@class="title"]/span/text()')  # [@属性名xxx=属性值xx] 表示属性上的限定
print(res2)

res3 = et.xpath('//div[contains(@class,"pic") and contains(@id,"next")]/div/ul/li')  
# 包含 @class="pic"并且 @id="next" 的 div节点
print(res3)
res4 = res3[0].xpath('./text()')  # 拿到的节点还能继续使用 xpath 进一步获得节点
print(res4)

# 成对出现(匹配到多个节点)
# 用循环实现
result = et.xpath('/html/body/div[@class="item"]/ul/li')
for item in result:
    href = item.xpath('./a/@href')[0]  # ./表示当前这个元素
    text = item.xpath('./a/text()')[0]
    print(href, text)
    

④ PyQuery

  • JQuery

jQuery 是一个 JavaScript 库,用于处理 DOM。 使用 jQuery,我们可以查找,选择,遍历和操作 HTML 文档的各个部分。

  • PyQuery

Pyquery 是一个 Python 库,是一种网页解析工具,具有与 jQuery 类似的 API。 它使用lxml模块进行快速的 XML 和 HTML 操作。 该 API 尽可能类似于 jQuery。让你使用jquery的风格来遍历xml文档,比 xpath 与 Beautiful Soup 解析库比起来更加灵活与简便,并且增加了添加类和移除节点的操作,这些操作有时会为提取信息时带来极大的便利。

PyQuery 充分利用 css的足够多样化的查找节点的方式(css中的语法都可用)
所以很灵活

PyQuery 不仅能获取节点,还能改变节点!
可以让不那么规整的页面变的完整、规整
1. 语法
from pyquery import PyQuery

# 一、获取节点
html1 = open('./baidu.html', encoding='utf-8').read()
html1 = '''
<html>
<body link="#0000cc">
  <div id="wrapper" class="wocao">
       <a>哈哈哈哈</a>
       <span>阿古茹</span>
       <span>睡吧睡吧</span>
  </div>
  <div id="w" class="wo">
       <a>呵呵呵呵</a>
       <span>啦啦啦</span>
       <span><a>不要不爱我</a></span>
  </div>
</body>
</html>
'''
# 创建了PyQuery对象,html数据将传递给构造函数
p = PyQuery(html1)  # 和 BeautifulSoup 等一样,把 html 交给 PyQuery 对象解析,
# print(p)
print(type(p))  # <class 'pyquery.pyquery.PyQuery'> 是一个 PyQuery对象(类似节点)

# pyquery 选择节点对象: 直接用 pyquery对象('标签名')
a = p('html')('body')('div')('a')  # 一层一层往下找,太复杂不太用
# print(a)
# 1. 用后代选择器等复杂语法
a2 = p('html body div a')
print(a2)
wrapper = p('#wrapper')  # p('.wrapper') 等也同理
print(wrapper)
span2 = p('html > body > div > span:nth-child(2)')  # 第2个span
print(span2)
# 在已经提取出的内容里获取第一个内容: eq(0)
print(span2.eq(0))
'''
    nth-child()是在css进行选择的时候,选择第一个位置的内容
    eq(0)是在css选择完后获取的内容里选择第一个
'''
# 伪类选择器
'''
CSS选择器之所以强大,有一个很重要的原因就是它支持多种多样的伪类选择器。
例如选择第一个节点、最后一个节点、奇偶数节点、包含某一文本的节点等等
'''
first_li = p("li:first")   # 第一个li标签
first_li = p('li:first-child')  # 第一个li标签
last_li = p("li:last")   # 最后一个li标签
last_li = p('li:last-child')  # 最后一个li标签
odd_lis = p("li:odd")    # 奇数li元素
even_lis = p("li:even")  # 偶数li元素
li = p('li:nth-child(2)')  # 节点、第二个
li = p('li:gt(2)')  # 获取序号比2大的标签
li = p('li:nth-child(2n)')  # 获取偶数的标签,2n+1则是获得奇数
li = p('li:contains(second)')  # 包含second文本的

# 2.1 查找后代中的某个节点
item.find("li")
# 2.2 查找子代中的某个节点
item.children('.color')
# 2.3 获取兄弟节点
item.siblings(".active")
# 2.4 获取某个节点的父节点
item.parent()  # 这个parent方法不会找到祖先节点如果想找到祖先节需要用parents()
item.parents('.first')  # 获取符合条件的祖先节点
# 2.5 遍历节点
# 调用items方法后,会得到一个生成器,对其进行遍历,就可以逐个得到li节点对象,类型也是pyquery。
# 还可以调用前面所说的方法对li节点进行选择,继续查询所需节点
lis = p("li").items()
for li in lis:
    print(li, type(li))

# 3. 获取属性内容
wrapper_class = p('#wrapper').attr('class')  # 等效于 p('#wrapper').attr.class
print(wrapper_class)  # wocao
# 4. 拿文本
a2_text = p('html body div a').text()
print(a2_text)  # 哈哈哈哈

# ★ 注意的一个坑
html2 = '''
    <ul>
        <li class = 'wgaahgajgkg'>哈哈</li>
        <li class = 'abew'>曹哈</li>
        <li class = 'abdhara'>黑暗凤凰</li>
        <li class = 'abcghk'>教师轮岗</li>
        <li class = 'ahdf'>爱给</li>
    </ul>
'''
p = PyQuery(html2)
# 可以拿到全部的文本
li_text = p('ul li').text()
print(li_text)
# 如果匹配到多个标签,要同时拿属性,但是attr只有一个属性,调用attr方法,只会得到第一个节点的属性只能拿到第一个的class
li_class = p('ul li').attr('class')  # wgaahgajgkg
print(li_class)
# 多个标签同时都拿属性或文本,需要用items()然后遍历获取属性
lis = p('ul li').items()  # 这是一个生成器 generator
for item in lis:
    print(item.text())
    print(item.attr('class'))

# 5. 迭代文档的li元素。
# items = [item.text() for item in p.items('li')]  # items()方法用于在列表推导式中创建li元素的Python列表
for item in p.items('li'):
    print(item.text())
    

# 快速总结
# 1. Pyquery(选择器)
# 2. items()  当选择器选择的内容很多的时候会,需要一个一个处理的时候
# 3. attr(属性名)   获取属性值
# 4. text()  获取文本

# 5. html()
# 类似于 innerHtml() 和 innertex() 的区别
html3 = '''
        <span>
            <a>窝矮泥</a>
        </span>
'''

p = PyQuery(html3)
text = p('span a').text()
html = p('span a').html()
print(text), print(html)
text = p('span').text()
html = p('span').html()
print(text)  # text方法只能打印字符串文本,会忽略节点内部的所有HTML,只返回纯文字内容
print(html)  # html方法可以打印 html 结构,当然如果只有纯文本,也可以打印纯文本

# 二、PyQuery 操作DOM
html4 = '''
    <ul>
        <li class = 'wgaahgajgkg'>哈哈</li>
        <li class = 'abew'>曹哈</li>
    </ul>
'''

# 1. 添加标签
p = PyQuery(html4)
abew = p('ul li.abew')
# 在某节点后面(同一级)添加节点
abew.after('''<li class = 'abdhara'>黑暗凤凰</li>
        <li class = 'abcghk'>教师轮岗</li>''')
# 在某节点里面添加节点
abew.append('''<a>哈哈哈哈哈哈哈啊哈哈</a>''')

# 2. 更改属性
p('ul li.abew').attr('class','hahahahah')  # attr() 第二个参数就是要修改的值
p('ul li.hahahahah').attr('id','12356')  # attr() 如果没有某个属性,就会添加此属性

# 3. 删除标签
# remove() 删除某标签,并返回该标签
p('ul li.wgaahgajgkg').remove()  # attr() 如果没有某个属性,就会添加此属性

# 4. 删除属性
# remove_attr() 删除某属性,并返回该属性
p('ul li.hahahahah').remove_attr('id')  # attr() 如果没有某个属性,就会添加此属性
print(p)

# 5.1 专门添加 class 属性的方法
li.addClass('active')
# addclass方法和removeclass方法可以动态改变节点的class属性
# 5.2 专门移出 class 属性的方法
li.removeClass('active')

2. 实战
from pyquery import PyQuery

html = open('./baidu.html', 'r', encoding='utf-8').read()

p = PyQuery(html)

li_text = p('div#wrapper > div#head > div.head_wrapper > a').text()
print(li_text)  # 哈哈哈
# 如果匹配到多个标签,要同时拿属性,但是attr只有一个属性,调用attr方法,只会得到第一个节点的属性只能拿到第一个的class
new_li= p('div#wrapper > div#head > div.head_wrapper > a').add_class('new_class')  # wgaahgajgkg
print(new_li)

⑤ jsonpath

jsonpath - 是xpath在json的应用

xml最大的优点就有大量的工具可以分析,转换,和选择性的提取文档中的数据。XPath是这些最强大的工具之一。
如果可以使用xpath来解析json,以下的问题可以被解决:

1,数据不使用特殊的脚本,可以在客户端交互的发现并取并获取。
2,客户机请求的JSON数据可以减少到服务器上的相关部分,这样可以最大限度地减少服务器响应的带宽使用率。
如果我们愿意,这个可以解析json数据的工具会变得有意义。随之而来的问题是它如何工作,jsonpath的表达式看起来怎么样。

事实上,json是由c系统编程语言表示自然数据,有特定语言的特定语法来访问json数据。
xpath的表达式:
/store/book[1]/title
我们可以看作是:
x.store.book[0].title
或
x['store']['book'][0]['title']

在Javascript, Python 和 PHP 中一个变量x表示json数据。经过观察,特定的语言里有内置xpath来解析数据。

JSONPath工具的问题
- 依赖某种特定的语言
- 需要依赖 XPath 1.0
- 减少代码量和内存的消耗

1. 语法
'''
    $    表示最外层对象
    @    表示当前的对象 ,类似于 xpath的.
    ./[] 表示子元素(属性)  例如 $.name
    ..	 表示任意的(后代),类似于 xpath 的 //
    *    表示所有的元素
    (表达式)      脚本表达式,使用在脚本引擎下面
    ?(表达式)     对象属性使用逻辑表达式来过滤
    [num]	数组访问,其中 num 是数字,可以是负数 ,例如 $[-1]	最后元素
    [1,2]   数组多个元素访问
    [start:end:step] 数组切片(数组范围访问),其中start和end是开始小表和结束下标,可以是负数,返回数组中的多个元素。

    JOSNPath 表达式可以使用.  符号如下:
    $.store.book[0].title

    或者使用[] 符号
    $['store']['book'][0]['title']

    【注意】: []在xpath表达式总是从前面的路径来操作数组,索引是从1开始
             使用JOSNPath的[]操作符操作一个对象或者数组,索引是从0开始
'''
import json
import jsonpath


obj = json.load(open('073_尚硅谷_爬虫_解析_jsonpath.json','r',encoding='utf-8'))

# 书店所有书的作者
author_list = jsonpath.jsonpath(obj,'$.store.book[*].author')
print(author_list)

# 所有的作者
author_list = jsonpath.jsonpath(obj,'$..author')
print(author_list)

# store下面的所有的元素
tag_list = jsonpath.jsonpath(obj,'$.store.*')
print(tag_list)

# store里面所有东西的price
price_list = jsonpath.jsonpath(obj,'$.store..price')
print(price_list)

# 第三个书
book = jsonpath.jsonpath(obj,'$..book[2]')
print(book)

# 最后一本书
book = jsonpath.jsonpath(obj,'$..book[(@.length-1)]')
print(book)

# 	前面的两本书
book_list = jsonpath.jsonpath(obj,'$..book[0,1]')
book_list = jsonpath.jsonpath(obj,'$..book[:2]')
print(book_list)

# 条件过滤需要在()的前面添加一个?
# 	 过滤出所有的包含isbn的书。
book_list = jsonpath.jsonpath(obj,'$..book[?(@.isbn)]')
print(book_list)


# 哪本书超过了10块钱
book_list = jsonpath.jsonpath(obj,'$..book[?(@.price>10)]')
print(book_list)

2. 实战
import json
import jsonpath

obj = json.load(open('jsonpath_taopiaopiao.json','r',encoding='utf-8'))
city_list = jsonpath.jsonpath(obj,'$..regionName')
print(city_list)

二、多线程 和 线程池

1. 多线程

'''
    多线程
        让程序能同时执行多个任务
        1.导包 from threading import Thread
        2.创建任务 def func()...
        3.创建线程 t = Thread(target=func,args=(xxx,xxx))  # target=目标函数 , args=(参数1,参数2...)
        4.启动线程 t.start()
'''
from threading import Thread

# 定义函数,用于添加到线程中
def func(name):
    for i in range(0, 100):
        print("子线程_{}".format(name))


if __name__ == '__main__':  # 主线程
    # 创建一个子线程
    t1 = Thread(target=func, args=('张三丰',))  # 容器类型哪怕里面只有一个元素,建议用元组形式,要用逗号隔开!
    t1.start()  # 启动线程,这里只是设置了多线程状态,为可以开始工作状态,具体的执行时间由CPU决定

    t2 = Thread(target=func, args=('李四',))
    t2.start()
    for i in range(0, 100):
        print("主线程%d" % i)
        
# 会发现t1、t2和主线程会有交叉,说明是多线程并发

2. 线程池

处理一些庞大的、比较慢的程序时适合用多线程,如果是秒完成的程序,单单针对速度的话,用多线程提速并不明显,甚至还会变慢

处理一些请求服务器的操作一般用多线程都比较快

'''
    线程池
        自动管理线程
        若指定管理 10 个线程,则在一个线程结束后,自动关闭这个线程,创建下一个线程,保证一直都是 10 个线程在进行
        线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程

        # 把任务提交给线程池,线程任务的调度交给线程池来完成
        t.submit(func,f'线程{i}',18)  # 参数1:执行的函数,参数2-n:函数的传参
'''
from concurrent.futures import ThreadPoolExecutor

# 定义函数,用于添加到线程中
def func(name,age):
    for i in range(0, 100):
        print("线程_{}__{}".format(name,age))

if __name__ == '__main__':
    # 创建线程池
    with ThreadPoolExecutor(10) as t: # 里面可以有10个线程
        # 10个任务
        # 把任务交给线程池
        for i in range(1000):
            t.submit(func,f'线程{i}',18)
	# 用 with 会等待线程池内的任务全部执行完毕,才会向下执行(守护)

3. 线程实战

from concurrent.futures import ThreadPoolExecutor

def func(name, age,url):
    html = requests.get(url).text
    print(html,name,age)
	

if __name__ == '__main__':
    with ThreadPoolExecutor(100) as t:  # 里面可以有100个线程
        # 100000个任务
        # 把任务交给线程池
        for i in range(100000):
            t.submit(func, f'线程{i}', 18,'http://www.baidu.com')



    # 😊测试线程池性能
    # 创建线程池
    st1 = time.time()
    for i in range(100):
        func(f'线程{i}', 18)
    ed1 = time.time()
    print(ed1 - st1)

    # 创建线程池
    st2 = time.time()
    with ThreadPoolExecutor(10) as t:  # 里面可以有10个线程
        # 10个任务
        # 把任务交给线程池
        for i in range(100):
            t.submit(func, f'线程{i}', 18)
            # func(f'线程{i}', 18)
    ed2 = time.time()
    print(ed2 - st2)

    # 创建线程池
    st3 = time.time()
    for i in range(1000):
        func(f'线程{i}', 18)
    ed3 = time.time()
    print(ed3 - st3)

    # 创建线程池
    st4 = time.time()
    with ThreadPoolExecutor(100) as t:  # 里面可以有10个线程
        # 10个任务
        # 把任务交给线程池
        for i in range(1000):
            t.submit(func, f'线程{i}', 18)
            # func(f'线程{i}', 18)
    ed4 = time.time()
    print(ed4 - st4)

    # 创建线程池
    st5 = time.time()
    for i in range(100000):
        func(f'线程{i}', 18)
    ed5 = time.time()
    print(ed5 - st5)

    # 创建线程池
    st6 = time.time()
    with ThreadPoolExecutor(1000) as t:  # 里面可以有10个线程
        # 10个任务
        # 把任务交给线程池
        for i in range(100000):
            t.submit(func, f'线程{i}', 18)
            # func(f'线程{i}', 18)
    ed6 = time.time()
    print(ed6 - st6)

    print(ed1 - st1)  # 单线程,           任务100        0.11509108543395996
    print(ed2 - st2)  # 多线程,线程池10,  任务100        0.4698059558868408
    print(ed3 - st3)  # 单线程,           任务1000       1.6373044967651367
    print(ed4 - st4)  # 多线程,线程池100, 任务1000       3.6064316749572754
    print(ed5 - st5)  # 单线程,           任务100000     208.70198273658752
    print(ed6 - st6)  # 多线程,线程池1000,任务100000     132.52682662010193

三、协程

协程
当线程遇见了IO操作的时候,可以选择性地切换到其他任务上(提高CPU利用率)

(多任务异步操作) 在单线程条件下
在微观上是一个任务一个任务进行切换,切换条件一般就是IO操作
在宏观上,我们能看到的其实是多个任务一起在执行
  • async
    • 协程函数:定义函数时加上async修饰,即async def func()
      • 用 async 可以把普通函数升级为 “异步函数”,注: 一个异步的函数, 有个更标准的称呼, 我们叫它 “协程” (coroutine)
      • 异步函数(协程)的特点是能在函数执行过程中断挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是sleep(5))消失后,也就是5秒到了再回来执行
    • 协程对象:执行协程函数得到的对象
      :执行协程函数得到协程对象,函数内部代码不会执行

  • await
    • await + 可等待对象(协程对象,Future,Task对象(IO等待))
    • 等待到对象的返回结果,才会继续执行后续代码

线程中的不足

def func():
    print('我爱周杰伦')
    time.sleep(3)  # 让当前线程处于阻塞状态,CPU不为此线程工作
    # input()  # 程序也是处于阻塞状态  # 一般的IO操作都会使线程处于阻塞状态
    # requests.get(url)  # 在网络请求返回数据之前,也是处于阻塞状态
    print('我真的爱周杰伦')
if __name__ == '__main__':
    func()

1. 协程程序基本语法

import asyncio
import time
async def func2():
    print('nihao!')


if __name__ == '__main__':
    g = func2()  # 此时的函数是异步协程函数,此时函数调用得到的 g 是一个协程对象
    print(g)  # <coroutine object func at 0x00000225002EA940>
    asyncio.run(g)  # 协程程序运行需要asyncio模块的支持


# 协程案例:
async def func3():
    print('你好,我是武大郎')
    # time.sleep(3)  # time.sleep(3) 是同步操作,当出现了同步操作的时候,异步就会中断了
    await asyncio.sleep(3)  # 异步操作的 sleep,需要用 await 把当前的 sleep 挂起
    print('你好,我是武大郎')

async def func4():
    print('你好,我是黑旋风')
    # time.sleep(2)
    await asyncio.sleep(2)
    print('你好,我是黑旋风')

async def func5():
    print('你好,我是及时雨')
    # time.sleep(4)
    await asyncio.sleep(4)
    print('你好,我是及时雨')

async def main():  # 协程主函数
    # # 第一种写法(不推荐)
    # f3 = func3()
    # await f3  # await 必须在 async 函数中 ,一般 await 挂起操作放在协程对象前面

    # 第二种(推荐)
    task = [
        asyncio.create_task(func3()),
        asyncio.create_task(func4()),
        asyncio.create_task(func5())
    ]
    await asyncio.wait(task)

if __name__ == '__main__':
    st = time.time()
    # 同时执行多个异步操作需要用 wait 管理,然后用 run 调用
    # asyncio.run(main())  # 这样写会时常报错
    loop = asyncio.get_event_loop()  # 创建事件循环,事件循环:去检索一个任务列表的所有任务,并执行所有未执行任务,直至所有任务执行完成。
    loop.run_until_complete(main())  # 将内部代码交给事件循环执行(添加任务,直至所有任务执行完成)

    ed = time.time()
    print(ed - st)  # 3.991 - 4.005
    # 一般的单线程痛不需要 9s+


# 报错记录:  RuntimeError: There is no current event loop in thread 'MainThread'.
# 此时我有两个协程
# 执行这个协程之前还有一个协程调用,下面的 loop = asyncio.get_event_loop() 并没有获取到事件循环,所以导致报错:在主线程没有事件循环。简单说,就是在同一线程中,第二个协程受到第一个的干扰。

2. 协程常用的库

import asyncio   # io 库
import aiohttp   # 类似 requests,用于发送请求
import aiofiles  # 异步 files 管理库
  • asyncio
async def main():
    urls = []
	# 准备异步协程对象列表
    tasks = []
    for url in urls:
        d = asyncio.create_task(aio_download(url))
        tasks.append(d)
    # tasks = [asyncio.create_task(aio_download(url)) for url in urls]  # 这么干也行哦~
    # 一次性把所有任务都执行
    await asyncio.wait(tasks)
    
if __name__ == '__main__':
 	loop = asyncio.get_event_loop()  # 创建事件循环,事件循环:去检索一个任务列表的所有任务,并执行所有未执行任务,直至所有任务执行完成。
    loop.run_until_complete(main())  # 将内部代码交给事件循环执行(添加任务,直至所有任务执行完成)
  • aiohttp
async with aiohttp.ClientSession() as session:  # aiohttp.ClientSession  <==> requests
	async with session.get(url) as resp:
        # resp.content.read()  <==> resp.content
        # resp.text() <==> resp.text
        # resp.json()  <==> resp.json()
  • aiofiles
 async with aiofiles.open(file=f'img/{name}', mode='wb') as f:
        await f.write(await resp.content.read())  # 读取内容试异步的,需要 await 挂起

3. 协程实战

# 在爬虫领域的应用
import time
import asyncio
import aiohttp
import aiofiles


async def aio_download(url):
    # 使用 with 可以自动管理上下文,不用手动关闭打开的资源,异步程序中使用 with 前面必须加 async,这是规定
    async with aiohttp.ClientSession() as session:  # aiohttp.ClientSession  <==> requests
        async with session.get(url) as resp:
            name = url.rsplit('/', 1)[-1]  # 从右边切,切1个,选最后一个
            # resp.content.read()  <==> resp.content
            # resp.text() <==> resp.text
            # resp.json()  <==> resp.json()

            print(f"准备开始下载 {name}")
            # aiofiles是一个用于处理asyncio应用程序中的本地磁盘文件。爬虫过程中用它来进行文件的异步操作。
            async with aiofiles.open(file=f'img/{name}', mode='wb') as f:
                await f.write(await resp.content.read())  # 读取内容是异步的,需要 await 挂起
            # with open(file=f'img/{name}', mode='wb') as f:
            #     f.write(await resp.content.read())  # 读取内容是异步的,需要 await 挂起
            print(f"{name} 下载完成!")



async def main():
    urls = [  # 三张图片地址
        'http://kr.shanghai-jiuxin.com/file/2022/0729/fe832be7e35c8a593e0ce90ab56361d3.jpg',
        "http://kr.shanghai-jiuxin.com/file/2022/0729/efe2fc9725df8915b530fd84ae18c39b.jpg",
        "http://kr.shanghai-jiuxin.com/file/mm/20211130/hvnxp0z2jlc.jpg"
    ]

    # 准备异步协程对象列表
    tasks = []
    for url in urls:
        d = asyncio.create_task(aio_download(url))
        tasks.append(d)
    # tasks = [asyncio.create_task(aio_download(url)) for url in urls]  # 这么干也行哦~
    # 一次性把所有任务都执行
    await asyncio.wait(tasks)


if __name__ == '__main__':
    st = time.time()

    # asyncio.run()会自动关闭循环,并且调用_ProactorBasePipeTransport.__del__报错, 而asyncio.run_until_complete()不会.
    # asyncio.run(main())  # 这样写会时常报错

    loop = asyncio.get_event_loop()  # 创建事件循环,事件循环:去检索一个任务列表的所有任务,并执行所有未执行任务,直至所有任务执行完成。
    loop.run_until_complete(main())  # 将内部代码交给事件循环执行(添加任务,直至所有任务执行完成)

    ed = time.time()
    print(ed - st)

四、保存数据

1. 保存到 Excel xls (xlwt)

语法

import xlwt

# 创建 Workbook 对象 (工作簿)
workbook = xlwt.Workbook(encoding='utf-8')  # 创建Workbook对象
# 创建工作表1
worksheet = workbook.add_sheet('sheet1')  
# 在工作表中写入数据,第一个参数表示行,第二个参数表示列,第三个参数表示内容
worksheet.write(0,0,'hello')  
# 保存数据表
workbook.save('student.xls')  

实战

# 保存数据到excel
def saveData_excel(dataList, savePath):
    # dataList是一个二维数据,每一维是一部电影的的8条数据
    print('saving...')
    # 创建Workbook对象
    workbook = xlwt.Workbook(encoding='utf-8', style_compression=0)  # 创建Workbook对象
    worksheet = workbook.add_sheet('sheet1', cell_overwrite_ok=True)  # 创建工作表1

    col = ("电影详情链接", "图片链接", "影片中文名", "影片外文名", "评分", "评价人数", "概述", "其他相关信息")
    # 写入表头
    for i in range(0, 8):
        # worksheet.write(0,0,'hello')  # 写入数据,第一个参数表示行,第二个参数表示列,第三个参数表示内容
        worksheet.write(0, i, col[i])  # 在第0行写入写入列名
	
    # 写入内容
    for i in range(0, 250):
        print('第%d条数据' % (i + 1))
        data = dataList[i]
        for j in range(0, 8):
            worksheet.write(i + 1, j, data[j])  # 写入每行数据(一部电影信息)

    workbook.save(savePath)  # 保存数据表

2. 保存到 数据库 db (sqlite3)

语法

import sqlite3

# 1.连接数据库
# 若不存在会自动创建一个数据库文件
conn = sqlite3.connect('test.db')  # 打开或创建一个数据库文件
print('成功打开数据库!')

# 2.创建一张数据表
# 数据库游标
cursor = conn.cursor()  # 数据库游标

sql = '''
    create table company
        (id int primary key not null,
        name text not null,
        age int not null,
        address char(50),
        salary real)
'''
cursor.execute(sql)  # 执行sql语句
conn.commit()  # 提交数据库操作
# conn.close()  # 关闭数据库连接
print('成功建表!')

# 3.插入数据
cursor = conn.cursor()  # 数据库游标

# sql1 = '''
#     insert into company(id,name,age,address,salary)
#         values(1,'张三',20,'成都',8000)
# '''
# sql2 = '''
#     insert into company(id,name,age,address,salary)
#         values(2,'李四',30,'重庆',1000),
# '''
# sql3 = '''
#     insert into company(id,name,age,address,salary)
#         values(3,'王武',25,'杭州',16000)
# '''
# SQLite插入多行数据写法
sql1 = '''
    insert into company(id,name,age,address,salary)
        select 1,'张三',20,'成都',8000
        union all
        select 2,'李四',30,'重庆',1000
        union all
        select 3,'王武',25,'杭州',16000
'''
cursor.execute(sql1)  # 执行sql语句
# driver.execute(sql2)
# driver.execute(sql3)
conn.commit()  # 提交数据库操作
# conn.close()  # 关闭数据库连接
print('插入成功!')

# 4.查询数据
cursor = conn.cursor()  # 数据库游标

sql1 = "select id,name,address,salary from company"
res = cursor.execute(sql1)  # 执行sql语句  # res 是 <sqlite3.Cursor object at 0x000002CFF668DF10>

for row in res:
    print('id = ', row[0])
    print('name = ', row[1])
    print('address = ', row[2])
    print('salary = ', row[3], '\n')

conn.commit()  # 提交数据库操作
conn.close()  # 关闭数据库连接
print('查询完毕!')

实战

# 创建初始化数据库
def init_db(db_path):
    sql = '''
        create table movie250
        (
            id integer primary key autoincrement,
            info_link text,
            imgSrc_link text,
            c_name varchar,
            f_name varchar,
            rating numeric,
            judgeNum numeric,
            inq text,
            bd text
        )
    '''
    # 创建数据表
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()  # 游标
    cursor.execute(sql)  # 执行sql语句
    conn.commit()  # 提交数据库操作
    conn.close()  # 关闭数据库连接
    
    
# 保存数据到数据库db
def saveData_db(dataList, db_path):
    init_db(db_path)      # 创建、初始化数据库
    
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
	
    for data in dataList:  # 每条数据(每部电影)
        # 处理数据
        for index in range(len(data)):  # 每部电影的内容(8个指标)
            if index == 4 or index == 5:
                continue  # 索引是4和5的不用变为字符串类型
            data[index] = '"{}"'.format(data[index])  # 把每个内容都变成带有""的字符串 -- 这一步是必须的,因为sql语句带引号,里面要插入的值如果是字符串必须再在字符串外面套一层引号
        # 插入数据(保存数据)
        sql = '''
            insert into movie250(
                info_link,imgSrc_link,c_name,f_name,rating,judgeNum,inq,bd)
            values({})'''.format(','.join(data))
        print(sql)
        cursor.execute(sql)
        conn.commit()
    cursor.close()
    conn.close()



3. 爬虫数据获取的问题

# 用 requests 请求图片数据:拿到 😊字节数据😊 方法: resp.content
resp_detail_img = requests.get(url=res_img_url)
with open(f'D:/img/{img_name}.{img_format}',mode='wb') as f:
    f.write(resp_detail_img.content)  # 保存图片(写入bytes数据)

拿到网页源代码时,script中有一大串 类似 json格式的字符串,可以这样转换成json: json.loads(img_arr_dict)

import json
img_arr_dict = res.group('dict_img')  # 此时是一个很像json的字符串
# 用json模块把字符串转换为json,转换完后里面的一些转义符也会消去
img_json = json.loads(img_arr_dict)
# print(img_json)

五、动态数据的获取操作

在爬取网页数据的时候,有时候会出现获取数据为空的情况(在路径、代码没问题前提下),
这种就很有可能是爬取的数据为动态加载的数据

有很多网页内容是js渲染或者其他手段弄出来的,这些在网页源代码中都是找不到的,
之前的方案数都是抓包获取这些后来数据的网页链接,再拿到这些数据,
但有些数据经过加密等一系列复杂操作,我们解密、加密操作起来十分复杂!
现在想能不能让程序连接到浏览器,让浏览器来完成各种复杂的操作,把那些脚本都跑完了
(即浏览器拿到这些数据,加载完毕后),
我们直接拿到最后的结果数据 ?
可以,那就是使用 selenium !
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
selenium : 本是一种自动化测试工具
selenium 可以打开浏览器,然后像人一样去操作浏览器
★ 程序员可以通过selenium,直接提取网页上的各种信息
因为对于 selenum 来说,各种网页信息都是透明的
selenium 它本身也是浏览器,加密、反爬的东西无论如何也得让浏览器正常运行
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

1. selenium webdriver 驱动

自动化测试框架 selenium 解决了 Ajax异步加载的页面获取问题

chromedriver下载地址: http://npm.taobao.org/mirrors/chromedriver/

from selenium import webdriver

😊【第一部分:创建driver并访问(打开)网页】
# (注意:驱动需要与浏览器版本兼容)

# 配置Chrome启动时属性的类ChromeOptions
options = webdriver.ChromeOptions()
options.add_argument('--headless')  # 设置 无界面浏览      # options.add_argument('')  # 可以设置UA、IP代理等等
options.add_argument('--disable-gpu')  # 禁用GPU加速

driver = webdriver.Chrome(options=options)  # 创建了 Chrome 实例
driver.get('http://www.baidu.com')  # 请求页面(打开此页面)
html = driver.page_source  # 网页源代码  str类型

Chrome 实例常用的配置

选项的添加无非就是分为以下这几种,如下:

options.set_headless()  # 设置启动无界面化
options.binary_location(value)  # 设置chrome二进制文件位置
options.add_argument(arg)   # 添加启动参数
options.add_extension(path)  # 添加指定路径下的扩展应用
options.add_encoded_extension(base64)   # 添加经过Base64编码的扩展应用
options.add_experimental_option(name,value)  # 添加实验性质的选项
options.debugger_address(value)   # 设置调试器地址 
options.to_capabilities()  # 获取当前浏览器的所有信息ptions

虽然选项很多,但是我们真正能用到的不多,一般就是无痕模式或者禁用JavaScript和图片来快速获取到相关信息。虽然我们上面使用的是Options方法,但是在实际应用中建议大家使用的ChromeOptions方法。

# 利用 options 配置 Chrome 实例 driver   
options = webdriver.ChromeOptions()
# 😊添加启动参数 (add_argument)
    # 设置UA
    options.add_argument('user-agent="Mozilla/5.0 (X11; Linux x86_64)   AppleWebKit/537.36 (KHTML, like 							Gecko) Chrome/81.0.4044.122 Safari/537.36"')

    # 设置ip和端口 
    options.add_argument('--proxy-server=http://ip:port')
	options.add_argument("--proxy-server=http://200.130.123.43:3456") # 代理服务器访问
    
    options.add_argument('--headless')  # 浏览器不提供可视化页面
    options.add_argument('--user-agent=""')  # 设置请求头的User-Agent
    options.add_argument('--window-size=1280x1024')  # 设置浏览器分辨率(窗口大小)
    options.add_argument('--start-maximized')  # 最大化运行(全屏窗口),不设置,取元素会报错
    # 去掉chrome正受到自动测试软件的控制的提示
    options.add_argument('--disable-infobars')  # 禁用浏览器正在被自动化程序控制的提示 
    options.add_argument("--disable-blink-features=AutomationControlled")  # 禁用启用Blink运行时的功能
    options.add_argument('--incognito')  # 隐身模式(无痕模式)
    options.add_argument('--hide-scrollbars')  # 隐藏滚动条, 应对一些特殊页面
    options.add_argument('--disable-javascript')  # 禁用javascript
    options.add_argument('--blink-settings=imagesEnabled=false')  # 不加载图片, 提升速度
    options.add_argument('--ignore-certificate-errors')  # 禁用扩展插件并实现窗口最大化
    options.add_argument('--disable-gpu')  # 禁用GPU加速
    options.add_argument('–disable-software-rasterizer')
    options.add_argument('--disable-extensions')  # 禁用扩展
	options.add_argument('lang=zh_CN.UTF-8')  # 设置默认编码为utf-8
	
    # 设置加载策略
	options.page_load_strategy = 'normal'
    options.page_load_strategy = 'eager'
    options.page_load_strategy = 'none'
    # 通过URL导航到新页面时, 默认情况下, Selenium 将等待页面完全加载后再进行响应. 但是在加载大量第三方资源的页面上可能会导致较长的等待时间. 在这种情况下, 使用非默认策略可以使测试的执行速度更快, 但是也可能导致不稳定, 即页面上的元素随元素加载和大小变化而改变位置
    '''
        策略	     准备完成的状态	                 备注
        normal	  complete	            默认情况下使用, 等待所有资源下载完成
        eager	  interactive	   DOM访问已准备就绪, 但其他资源 (如图像) 可能仍在加载中
        none	  Any	                      完全不阻塞WebDriver
        
        normal:等待整个页面加载完毕再开始执行操作
        eager:等待整个dom树加载完成,即DOMContentLoaded这个事件完成,也就是只要 HTML 完全加载和解析完毕就开始执行操作。放			  弃等待图片、样式、子帧的加载。
        none:等待html下载完成,哪怕还没开始解析就开始执行操作。
	'''

# 😊添加扩展应用 (add_extension, add_encoded_extension)
	extension_path = '插件路径'
    options.add_extension(extension_path)

# 😊添加实验性质的设置参数 (add_experimental_option)
    # 禁止图片加载
    prefs = {"profile.managed_default_content_settings.images": 2}
    options.add_experimental_option("prefs", prefs)

    # 禁用保存密码
    prefs = {"": ""}
    prefs["credentials_enable_service"] = False
    prefs["profile.password_manager_enabled"] = False
    options.add_experimental_option ("prefs", prefs)

    # 禁用浏览器弹窗
    prefs = {  
        'profile.default_content_setting_values' :  {  
            'notifications' : 2  
        }  
    } 
    options.add_experimental_option('prefs',prefs) 
     
    # 使用chrome开发者模式
    options.add_experimental_option('excludeSwitches', ['enable-automation'])


# 创建 Chrome 实例对象
# executable_path为chromedriver的路径,如果 python 环境变量下或项目中有,就不用配置
dirver = webdriver.Chrome()  # 默认启动
dirver = webdriver.Chrome(executable_path=r'./chromedriver.exe') # 获取chrome浏览器的驱动,并启动Chrome浏览器
    
#设置隐式等待
driver.implicitly_wait(30)
# 注:隐式等待的好处是不用像固定等待方法一样死等时间N秒,可以在一定程度上提升测试用例的执行效率。不过这种方法也存在一定的弊端,那就是程序会一直等待整个页面加载完成,也就是说浏览器窗口标签栏中不再出现转动的小圆圈,才会继续执行下一步

#设置窗口最大化
driver.maximize_window()

# 设置cookie
driver.delete_all_cookies() # 删除cookie
driver.add_cookie({'name':'ABC','value':'DEF'})  # 携带cookie
driver.get_cookies()  # 获取cookie

防止浏览器识别出 selenium

selenium 做爬虫能解决很多反爬问题,但是 selenium 也有很多特征可以被识别
如:用 selenium 驱动浏览器后,window.navigator.webdriver值是true,而正常运行浏览器该值是未定义的(undefined)
下面的方法可以消除 selenium 启动特征

# 1.使用Chrome开发者模式  (这种方式在高版本的 Chrome 中已经失效了,无法解决问题)
options = webdriver.ChromeOptions()
options.add_experimental_option('excludeSwitches', ['enable-automation'])

# 2.禁用启用 Blink 运行时的功能
options.add_argument("--disable-blink-features=AutomationControlled")

# 3.Selenium执行cdp命令,再次覆盖window.navigator.webdriver的值
driver = webdriver.Chrome(options=options)
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": """
    			navigator.webdriver = undefined
                Object.defineProperty(navigator, 'webdriver', {
                  get: () => undefined
                })
              """
})

2. selenium 匹配节点及其内容

😊【第二部分:匹配节点及其内容】
# ★此后获取节点啥的和以往一样,可以用 driver.page_source 这个str类型的网页源码去匹配内容
# from lxml import etree
# et = etree.HTML(driver.page_source)  # 把这个网页源码字符串解析成了html element对象  -- et是一个element对象
# self_b = et.xpath('//input[contains(@id,"su")]/@value')
# print(self_b)

# ★也可以使用selenium自带的查找节点的方法
'''
    from selenium.webdriver.common.by import By

    driver.find_element(By.XPATH,'XPATH')
    driver.find_element(By.ID,'ID')
    driver.find_element(By.CLASS_NAME,'CLASS_NAME')
    driver.find_element(By.CSS_SELECTOR,'CSS_SELECTOR')
    driver.find_element(By.LINK_TEXT,'LINK_TEXT')
    driver.find_element(By.PARTIAL_LINK_TEXT,'PARTIAL_LINK_TEXT')
    driver.find_element(By.TAG_NAME,'TAG_NAME')

'''
from selenium.webdriver.common.by import By

res = driver.find_elements(By.CLASS_NAME,'nav_bg')  # 查找class='nav_bg'的节点
print(res)  # 是一个节点  [<selenium.webdriver.remote.webelement.WebElement (session="4bbe5ac89086771f788546f40baed3ae", element="99d66601-a9f1-409a-b9e1-205f96e260a1")>]
for item in res:
    print(item.get_attribute('style'))  # item 的获取style属性值

# res = driver.find_element(By.XPATH,'//li[@name="navIndex"]/a/text()')  # 错误写法,selenium的xpath只能匹配元素
res = driver.find_elements(By.XPATH,'//li[starts-with(@name,"navIndex")]/ul/li/a')
print(res)  # 是一个节点列表
for item in res:
    print(item.get_property('text'))  # 这样能拿到文本(?不明白为什么这里text不行?)
    # 获得属性
    print(item.get_attribute('title'))  # NBA直播

res = driver.find_elements(By.XPATH,'//a')
for item in res:
    print(item.text) # 这样text能拿到一些文本

注意:如果某些元素是ajax或者其他方式加载出来的,在selenium匹配节点的时候,很可能因为某些操作(如:点击某按钮后,页面会局部刷新,加载出来一些信息)后,导致节点还没加载出来,还没获取到,代码就已经往下执行了(因为局部刷新需要时间,而代码执行太快),此时最简单的方法就是time.sleep() 让程序睡一会儿,再往下执行,或者采用下文提到的延时等待方法。

3. selenium 执行js操作浏览器

😊【第三部分:可以执行js操作浏览器】  
# 可以让 dricer 执行 js代码,来操作BOM,操作页面
js = '''
	console.log('hello,world!')
'''
driver.execute_script(js)  # 可以让 dricer 执行 js代码,来操作BOM,操作页面

driver.close()  # 关闭当前页面(如果浏览器只剩下一页,则关闭整个dirver)
driver.quit()  # 关闭dirver

常用的js代码

① 窗口下滑

# 窗口下滑
js = '''
                let height = 0
        let interval = setInterval(() => {
            window.scrollTo({
                top: height,
                behavior: "smooth"
            });
            height += 500
        }, 500);
        setTimeout(() => {
            clearInterval(interval)
        }, 7000);
    '''
driver.execute_script(js)
# 时间必须大于等于js的时间,否则会并发执行,混乱
time.sleep(8)  #  给js足够的时间执行完,最好比js中定时器的总时间多一点

② 清理Chrome(driver)缓存

from selenium import webdriver

driver = webdriver.Chrome()
# 设置隐式等待
driver.implicitly_wait(10)

# 清除缓存提示框
driver.get('chrome://settings/clearBrowserData')

# 2S 等待时间
time.sleep(2)

clearButton = driver.execute_script("return document.querySelector('settings-ui').shadowRoot.querySelector('settings-main').shadowRoot.querySelector('settings-basic-page').shadowRoot.querySelector('settings-section > settings-privacy-page').shadowRoot.querySelector('settings-clear-browsing-data-dialog').shadowRoot.querySelector('#clearBrowsingDataDialog').querySelector('#clearBrowsingDataConfirm')")

clearButton.click()

# driver.quit()

4. selenium 浏览器的常用方法

参考1

参考2:非常详细

参考3

① 获取本页面URL

driver.current_url

② 获取日志

driver.log_types  # 获取当前日志类型
driver.get_log('browser')# 浏览器操作日志
driver.get_log('driver') # 设备日志
driver.get_log('client') # 客户端日志
driver.get_log('server') # 服务端日志

③ 窗口操作

driver.maximize_window()  # 最大化
driver.fullscreen_window()  # 全屏
driver.minimize_window()  # 最小化
driver.get_window_position()  # 获取窗口的坐标
driver.get_window_rect()  # 获取窗口的大小和坐标
driver.get_window_size()  # 获取窗口的大小
driver.set_window_position(100,200)  # 设置窗口的坐标
driver.set_window_rect(100,200,32,50)  # 设置窗口的大小和坐标
driver.set_window_size(400,600)  # 设置窗口的大小
driver.current_window_handle  # 返回当前窗口的句柄
driver.window_handles  #返回当前会话中的所有窗口的句柄

④ 设置延时

driver.set_script_timeout(5)  # 设置脚本延时五秒后执行
driver.set_page_load_timeout(5)  # 设置页面读取时间延时五秒

⑤ 关闭

driver.close() #关闭当前标签页
driver.quit() #关闭浏览器并关闭驱动

⑥ 打印网页源代码

driver.page_source

⑦ 屏幕截图操作

driver.save_screenshot('1.png')#截图,只支持PNG格式
driver.get_screenshot_as_png() #获取当前窗口的截图作为二进制数据
driver.get_screenshot_as_base64() #获取当前窗口的截图作为base64编码的字符串

⑧ 前进后退刷新

driver.forward()  # 页面前进
driver.back()  # 页面后退
driver.refresh()  # 页面刷新

⑨ 执行JS代码

在Selenium中也可以自定义JS代码并带到当前页面中去执行,如下:

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

driver = webdriver.Chrome(executable_path=r'./chromedriver.exe')
driver.get('https://www.baidu.com')
kw1 = driver.find_element(By.ID,'kw')
driver.execute_script("alert('hello')")  # 执行js代码
time.sleep(3)
driver.quit()

⑩ Cookies操作

driver.get_cookie('BAIDUID') #获取指定键的Cookies
driver.get_cookies()         #获取所有的Cookies
for y in driver.get_cookies():
   x=y
   if x.get('expiry'):
       x.pop('expiry')     
   driver.add_cookie(x) #添加Cookies  
driver.delete_cookie('BAIDUID') #删除指定键的Cookies内容
driver.delete_all_cookies() #删除所有cookies

⑪ 获取标题内容

driver.title

⑫ 获取当前浏览器名

driver.name

⑬ 全局超时时间

driver.implicitly_wait(5)

5. selenium 元素操作的常用方法

对我们找到的元素进行二次操作,不仅可以再次选择子元素还可以进行其它操作。如下:

kw1.click()        # 点击元素
kw1.text           # 内容,如果是表单元素则无法获取
kw1.get_property('background') # 获取元素的属性的值
kw1.get_attribute('id') # 获取元素的属性的值
kw1.send_keys('')  # 向元素内输入值

kw1.id             # Selenium所使用的内部ID
kw1.clear()        # 清除元素的值
kw1.location       # 不滚动获取元素的坐标
kw1.location_once_scrolled_into_view  # 不滚动且底部对齐并获取元素的坐标
kw1.parent         # 父元素
kw1.size           # 大小
kw1.submit         # 提交
kw1.screenshot('2.png') # 截取元素形状并保存为图片
kw1.screenshot_as_png   # 截取元素形状(获得字节 bytes 数据)
kw1.tag_name       # 标签名
kw1.is_selected()  # 判断元素是否被选中
kw1.is_enabled()   # 判断元素是否可编辑
kw1.is_displayed   # 判断元素是否显示
kw1.value_of_css_property('color') # 获取元素属性的值
kw1._upload('2.png') # 上传文件

6. 键盘鼠标操作 (动作API)

动作API是网上资料比较少的,因为之前的查找元素,调用click等已经可以解决很多的问题了,在翻看官方文档时,发现selenium还支持动作API来模拟动作。
动作API分为四个部分,分别是键盘、鼠标、笔、滚轮。

一些交互动作都是针对某个节点执行的,比如:对于输入框,我们就调用它的输入文字和清空文字方法;对于按钮,就调用它的点击方法,还有另外一些操作,他们没有特定的对象,比如鼠标的拖拽,键盘的按键等,这些动作用另一种方式来执行,那就是动作链

比如:现在实现一个节点的拖拽操作,将某个节点从一处拖拽到另外一处

  • ActionChains 类常用于模拟鼠标、键盘的行为,是自动执行低级交互的一种方式,例如:鼠标单击、移动、拖拽,键盘操作,文本操作等行为
  • 如果在一个用例中只有一两个动作,那么用之前讲过的简单版的就可以了,如果动作很复杂,那么可以使用动作链了。

from selenium.webdriver.common.action_chains import ActionChains

ActionsChains是如何模拟鼠标操作?

1.首先把当前的driver对象赋予给ActionsChains类,让ActionsChains知道是哪个driver实例在操作鼠标,
2.其次再传入需要被定位元素位置,让鼠标对此元素执行click操作,
3.最后借助perform执行上面规划好的动作链

例如:ActionChains(driver).context_click(right_click).perform()

① 鼠标操作

from selenium.webdriver import ActionChains

clickable = driver.find_element(By.ID, "clickable")

find_element(By.ID,'su').click()  # 点击

# 复杂的操作,使用动作链
# 鼠标单击长按id为clickable的元素
ActionChains(driver).click_and_hold(clickable).perform()
# 也可以分开写
ac = ActionChains(driver)
ac.key_down(Keys.SHIFT)
ac.send_keys("abc")
ac.click(on_element=btn)
ac.perform()

## ★常用鼠标操作★ ##
.click(on_element=None)  # 鼠标单击
.click_and_hold(on_element=None)  # 鼠标单击长按
.context_click(on_element=None)  # 右击
.double_click(on_element=None)  # 双击
.drag_and_drop(source,target)  # 拖拽到某个元素然后松开
.drag_and_drop_by_offset(source, xoffset, yoffset)  # 拖拽到某个坐标然后松开
    source: The element to mouse down.
    xoffset: X offset to move to.
    yoffset: Y offset to move to.
.move_by_offset(xoffset, yoffset)  # 鼠标从当前位置移动到某个坐标
.move_to_element(to_element)        # 鼠标移动到某个元素
.move_to_element_with_offset(to_element, xoffset, yoffset) # 移动到距某个元素(左上角坐标)多少距离的位置
.release(on_element=None)       # 在某个元素位置松开鼠标左键

鼠标操作实例

鼠标定义的5种按键

  • 0——鼠标左键
  • 1——鼠标中键
  • 2——鼠标右键
  • 3——X1(后退键)
  • 4——X2(前进键)

鼠标双击

clickable = driver.find_element(By.ID, "clickable")
ActionChains(driver)\
    .double_click(clickable)\
    .perform()

鼠标右击

clickable = driver.find_element(By.ID, "clickable")

ActionChains(driver)\
    .context_click(clickable)\
    .perform()

按下鼠标3键

动作行为构造类ActionBuilder,将行为绑定到WebDriver对象上
selenium 中的鼠标操作类 -- ActionChains类,其操作鼠标行为都是使用的 ActionBuilder类。
action = ActionBuilder(driver)
action.pointer_action.pointer_down(MouseButton.BACK)
action.pointer_action.pointer_up(MouseButton.BACK)
action.perform()

按下鼠标4键

action = ActionBuilder(driver)
action.pointer_action.pointer_down(MouseButton.FORWARD)
action.pointer_action.pointer_up(MouseButton.FORWARD)
action.perform()

鼠标移动到元素上

hoverable = driver.find_element(By.ID, "hover")
ActionChains(driver)\
    .move_to_element(hoverable)\
    .perform()

鼠标位移
就是通过像素点来进行位移操作。

从元素左顶边进行位移

mouse_tracker = driver.find_element(By.ID, "mouse-tracker")
ActionChains(driver)\
    .move_to_element_with_offset(mouse_tracker, 8, 11)\
    .perform()

从当前窗口左上角位移

action = ActionBuilder(driver)
action.pointer_action.move_to_location(8, 12)
action.perform()

从当前鼠标位置位移

ActionChains(driver)\
    .move_by_offset( 13, 15)\
    .perform()

拖拽元素
该方法首先单击并按住源元素,移动到目标元素的位置,然后释放鼠标。

draggable = driver.find_element(By.ID, "draggable")
droppable = driver.find_element(By.ID, "droppable")
ActionChains(driver)\
    .drag_and_drop(draggable, droppable)\
    .perform()

通过位移拖拽

draggable = driver.find_element(By.ID, "draggable")
start = draggable.location
finish = driver.find_element(By.ID, "droppable").location
ActionChains(driver)\
    .drag_and_drop_by_offset(draggable, finish['x'] - start['x'], finish['y'] - start['y'])\
    .perform()

滚轮(只有谷歌内核浏览器生效)
滚动到某元素位置

iframe = driver.find_element(By.TAG_NAME, "iframe")
ActionChains(driver)\
    .scroll_to_element(iframe)\
    .perform()

定量滚动

footer = driver.find_element(By.TAG_NAME, "footer")
delta_y = footer.rect['y']
ActionChains(driver)\
    .scroll_by_amount(0, delta_y)\
    .perform()

从一个元素滚动指定量

iframe = driver.find_element(By.TAG_NAME, "iframe")
scroll_origin = ScrollOrigin.from_element(iframe)
ActionChains(driver)\
    .scroll_from_origin(scroll_origin, 0, 200)\
    .perform()

从一个元素滚动,并指定位移

footer = driver.find_element(By.TAG_NAME, "footer")
scroll_origin = ScrollOrigin.from_element(footer, 0, -50)
ActionChains(driver)\
    .scroll_from_origin(scroll_origin, 0, 200)\
    .perform()

从一个元素的原点位移

ActionChains(driver)\
    .scroll_from_origin(scroll_origin, 0, 200)\
    .perform()

② 键盘操作

键盘代码完整列表

from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys

driver.find_element(By.ID,'kw').send_keys('python') # 输入'python'
driver.find_element(By.ID,'kw').send_keys(Keys.ENTER) # 回车键

# 复杂的操作,使用动作链
# 输入'ABC'
ActionChains(driver).key_down(Keys.SHIFT).send_keys("abc").perform()

## ★常用键盘操作★ ##
.key_down(value,element=None)  # 按下某个键
.key_up(value,element=None)  # 放开某键
		 - value: The modifier key to send. Values are defined in `Keys` class.
         - element: The element to send keys.  If None, sends a key to current focused element.
.send_keys(*keys_to_send)        # 发送某个键或者输入文本到当前焦点的元素
.send_keys_to_element(element, *keys_to_send)  # 发送某个键到指定元素

组合
    .send_keys(Keys.CONTROL,'a') # 全选(Ctrl+A)
    .send_keys(Keys.CONTROL,'c') # 复制(Ctrl+C)
    .send_keys(Keys.CONTROL,'x') # 剪切(Ctrl+X)
    .send_keys(Keys.CONTROL,'v') # 粘贴(Ctrl+V)
非组合
    回车键:Keys.ENTER
    删除键:Keys.BACK_SPACE
    空格键:Keys.SPACE
    制表键:Keys.TAB
    回退键:Keys.ESCAPE
    刷新键:Keys.F5

键盘操作实例

按下某键,以输入shift+abc为例

ActionChains(driver)\
    .key_down(Keys.SHIFT)\
    .send_keys("abc")\
    .perform()

弹起某键,以输入shift+a和shift+b为例

ActionChains(driver)\
    .key_down(Keys.SHIFT)\
    .send_keys("a")\
    .key_up(Keys.SHIFT)\
    .send_keys("b")\
    .perform()

浏览器输入某串字符(不指定元素)

ActionChains(driver)\
    .send_keys("abc")\
    .perform()

指定元素输入字符串

text_input = driver.find_element(By.ID, "textInput")
ActionChains(driver)\
    .send_keys_to_element(text_input, "abc")\
    .perform()

复制和粘贴

cmd_ctrl = Keys.COMMAND if sys.platform == 'darwin' else Keys.CONTROL
ActionChains(driver)\
        .send_keys("Selenium!")\
        .send_keys(Keys.ARROW_LEFT)\
        .key_down(Keys.SHIFT)\
        .send_keys(Keys.ARROW_UP)\
        .key_up(Keys.SHIFT)\
        .key_down(cmd_ctrl)\
        .send_keys("xvv")\
        .key_up(cmd_ctrl)\
        .perform()

③ 其他操作

perform()        # 执行所有操作
pause(seconds)   # 暂停所有输入(指定持续时间以秒为单位)
reset_actions()  # 结束已经存在的操作并重置

实例1:复制粘贴

from selenium import webdriver
from selenium.webdriver.common.by import By
import time
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
driver = webdriver.Chrome(executable_path=r'./chromedriver.exe')
driver.get('https://www.baidu.com')

a = ActionChains(driver)
kw1 = driver.find_element(By.ID,'kw')
tj = driver.find_element(By.ID,'su')

tj.send_keys(Keys.CONTROL,'c')  # 复制
a.drag_and_drop(kw1,tj).perform()  # 从输入框拖动到搜索按钮
kw1.send_keys(Keys.CONTROL,'v')  # 粘贴
tj.send_keys(Keys.ENTER)

time.sleep(3)
driver.close()
driver.quit()

暂停(pause)
光标移动,滚轮滚动期间,会有一些时间空隙,这里可以使用暂停来实现,这里是支持链式调用的,这里贴出官方给出的例子,

clickable = driver.find_element(By.ID, "clickable")

ActionChains(driver)\
        .move_to_element(clickable)\
        .pause(1)\
        .click_and_hold()\
        .pause(1)\
        .send_keys("abc")\
        .perform()

释放所有动作
当前有动作执行时,可以使用以下方法停止这些动作,

ActionBuilder(driver).clear_actions()

7. 选项卡管理 (窗口 window)

通过执行js命令: window.open()实现新开选项卡(新窗口)
不同的选项卡是存在列表 driver.window_handles
通过driver.window_handles[0]就可以操作第一个选项卡
通过driver.window_handles[-1]就可以操作最后一个(最新)选项卡

注意:在 selenium 的浏览器驱动中,新窗口默认是不切换过来的,需要用上述代码控制切换窗口,否则不切换,也拿不到相应网页的内容

import time
from selenium import webdriver
driver = webdriver.Chrome()
driver.get('https://www.baidu.com')

driver.execute_script('window.open()')
print(driver.window_handles)

driver.switch_to_window(driver.window_handles[1])  # 切换到第二个选项卡
driver.get('https://www.taobao.com')
time.sleep(1)

driver.close()  # 关闭当前选项卡(第二个选项卡)  # 注意此时即使被关闭了,selenium 视角还是在第二个选项卡那里,需要手动切换回去
driver.switch_to_window(driver.window_handles[0])  # 切换到第一个选项卡(回到第一个选项卡)
driver.get('https://python.org')

8. 切换 Frame

网页中有一种节点叫作iframe,也就是子Frame,相当于页面的子页面,它的结构和外部网页的结构完全一致。

Selenium打开页面后,它默认是在父级别Frame里面操作,而此时如果页面中还有子Frame,它是不能获取到子Frame里面的节点的,这时就需要使用switch_to.frame() 方法来切换Frame

import time
 
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver import ActionChains
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
 
driver = webdriver.Chrome()
driver.get("https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable")
driver.switch_to.frame("iframeResult")
try:
    logo = driver.find_element_by_class_name("logo")
except NoSuchElementException:
    print("NO LOGO")
    
# driver.switch_to.default_content()  # 切换回默认页面、原页面(即整个窗口)
driver.switch_to.parent_frame()  # 切换回上一层的frame,对于层层嵌套的frame很有用
logo = driver.find_element_by_class_name("logo")
print(logo)
print(logo.text)
 
结果:
NO LOGO
<selenium.webdriver.remote.webelement.WebElement (session="97f699231561624df577fc75ece0866e", element="16ea4a07-35f5-426b-aab8-980a86a7d739")>

9. Alert

在弹窗处理中,我们会遇到三种情况,如下:

浏览器弹出框
新窗口弹出框
人为弹出框

② 浏览器弹出框

首先说说浏览器弹出框,想必大家对JavaScript中的Alert,Confirm,Prompt应该不是很陌生,就是弹出框,确认框,输入框;基本方法如下:

from selenium.webdriver.common.alert import Alert

driver = webdriver.Chrome(executable_path=r'./chromedriver.exe')
driver.implicitly_wait(10)
driver.get('https://www.baidu.com')

a1 = Alert(driver)
a1.accept()  # 确定
a1.dismiss()  # 取消
a1.authenticate(username,password)  # 用户身份验证
a1.send_keys('')  # 输入文本或按键
a1.text  # 获取弹窗内容

这里我们应对每种情况它上面的方法的对应位置都是会有所变化的,所以我们需要根据具体情况来进行操作,而且还可以使用另一种方法,如下:

driver = webdriver.Chrome(executable_path=r'./chromedriver.exe')
driver.implicitly_wait(10)
driver.get('https://www.baidu.com')

a1 = driver.switch_to_alert()
a1.accept()  # 确定
a1.dismiss()  # 取消
a1.authenticate(username,password)  # 用户身份验证
a1.send_keys('')  # 输入文本或按键
a1.text   # 获取弹窗内容

:该类方法必须在有弹框的情况下才有作用,如没有会报错。

② 新窗口弹出框

上面就是浏览器弹出框的处理方法了,如果是 新窗口弹出 的话那么就不一样了,我们需要通过句柄来定位

具体用法如下:

from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome(executable_path=r'./chromedriver.exe')
driver.implicitly_wait(10)
driver.get('https://www.baidu.com')

kw1 = driver.find_element(By.ID,'kw')
tj = driver.find_element(By.ID,'su')
hwnd = driver.window_handles # 所有窗口句柄
for h in hwnd:
   if h != driver.current_window_handle:  # 如果句柄不是当前窗口句柄则切换
	   driver.switch_to_window(h)   # 切换窗口
   else:
       print('无需切换窗口') 
        
time.sleep(3)
driver.close()
driver.quit()

:如果有多个窗口,当你关闭了当前窗口想切换到另一个窗口,你需要把没关闭的窗口切换成当前活动窗口,因为Selenium是不会为你做这件事的。

③ 人为弹出框

这类弹出框是我们自己开发的,一般都是使用 div 包裹一些其它的元素标签然后形成一个整体,当我们触发某个事件的时候就会出现,否则消失。这种弹出框使用我们的众多find前缀的方法就能遍历到,很方便,这里不一一细说。

10. 判断

在Selenium中我们在做自动化测试时常无法判断一个元素是否真的显示出来了,因此会各种报错,接下来我们对这些操作进行判断,如果显示出了我们预期的值,那么就进行下一步操作,否则就关闭或者暂停几秒然后再判断,这里我要跟大家说Selenium中的一个模块-----Expected_Conditions,简称为EC,如下所示:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
import time
driver = webdriver.Chrome(executable_path=r'./chromedriver.exe')
driver.implicitly_wait(10)
driver.get('https://baidu.com')

t = Edriver.title_is('百度一下,你就知道')  # 判断 title 是不是 '百度一下,你就知道'
print(t(driver))  # True

time.sleep(3)
driver.close()
driver.quit()

常用的 Expected_Conditions方法

EC.title_contains('')(driver) # 判断页面标题是否包含给定的字符串
EC.presence_of_element_located('')(driver)  # 判断某个元素是否加载到dom树里,该元素不一定可见
EC.url_contains('')(driver)  # 判断当前url是否包含给定的字符串
EC.url_matches('')(driver)  # 匹配URL
EC.url_to_be('')(driver)   # 精确匹配
EC.url_changes('')(driver)  # 不完全匹配
EC.visibility_of_element_located('')(driver)  # 判断某个元素是否可见,可见代表元素非隐藏元素
EC.visibility_of('')(driver)  # 跟上面一样,不过是直接传定位到的element
EC.presence_of_all_elements_located('')(driver)  # 判断是否至少有1个元素存在于dom树中
EC.visibility_of_any_elements_located('')(driver)  # 判断是否至少一个元素可见,返回列表
EC.visibility_of_all_elements_located('')(driver)  # 判断是否所有元素可见,返回列表
EC.text_to_be_present_in_element('')(driver)  # 判断元素中的text是否包含了预期的字符串
EC.text_to_be_present_in_element_value('')(driver) # 判断元素中value属性是否包含预期的字符串
EC.frame_to_be_available_and_switch_to_it('')(driver)  # 判断该frame是否可以switch进去
EC.invisibility_of_element_located('')(driver)  # 判断某个元素是否不存在于dom树或不可见
EC.element_to_be_clickable('')(driver)  # 判断某个元素中是否可见并且可点击
EC.staleness_of('')(driver)  # 等某个元素从dom树中移除
EC.element_to_be_selected('')(driver)  # 判断某个元素是否被选中了,一般用在下拉列表
EC.element_located_to_be_selected('')(driver)  # 判断元组中的元素是否被选中
EC.element_selection_state_to_be('')(driver)  # 判断某个元素的选中状态是否符合预期
EC.element_located_selection_state_to_be('')(driver)  # 跟上面一样,只不过是传入located
EC.number_of_windows_to_be('')(driver)  # 判断窗口中的数字是否符合预期
EC.new_window_is_opened('')(driver)  # 判断新的窗口是否打开
EC.alert_is_present('')(driver)  # 判断页面上是否存在alert

11. 下拉选择框

selenium的下拉选择框。我们通常会遇到两种下拉框,一种使用的是html的标签select,另一种是使用input标签做的假下拉框。
后者我们通常的处理方式与其他的元素类似,点击或使用JS等。
而对于前者,selenium给了有力的支持,就是Select类。
进行测试的网站:http://sahitest.com/demo/selectTest.htm

参考1

语法

from selenium.webdriver.support.select import Select
from selenium.webdriver.common.by import By

element = driver.find_element(By.ID,'ls_fastloginfield')

s = Select(element)  # 实例化
res = s.all_selected_options  # 全部选中子项
res1 = s.options  # 全部子项

print(res)
print(res1)

下拉选择控件最常用的2种选择方式:Select

方式一:通过下标选择值,下标开始值:0

from selenium.webdriver.support.select import Select

# 单位-默认选择第一个
# 这里的get_visible_element是我封装的函数,类似于 driver.find_element(By.ID,id)
ele = get_visibile_element(driver,'id','inputItemKind.prpCitemKind.targetPriceUnit')
s = Select(ele)
s.select_by_index(0)  # 通过下标选择值

方式二:通过文本内容来选择值

from selenium.webdriver.support.select import Select

# 单位-默认选择第一个
ele = get_visibile_element(driver,"id","opinionType")
s = Select(ele)
s.select_by_visible_text(val)  # 通过文本内容来选择值

所有Select操作

# 选择选项
s.select_by_index(index)  # 根据索引选择
s.select_by_value(value)   # 根据值来选择
s.select_by_visible_text(text)   # 根据选项可见文本

# 取消选择(反选)
s.deselect_by_index(index)   # 取消选择对应index索引的选项
s.deselect_by_value(value)   # 取消选择对应value值的选项
s.deselect_by_visible_text(text)  # 取消选择对应可见文本的选项
s.deselect_all()  # 取消所有选择

# 返回选项
s.first_selected_option   # 返回第一个选中的子项,也是下拉框的默认值
s.all_selected_options  # 返回全部选中的子项
s.options  # 返回全部子项

实战

实例1

from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.select import Select
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
path = r'E:\web\select.html'
driver.get(path)

# 通过显示等待的方法判断元素是否出现
WebDriverWait(driver,10).until(EC.visibility_of_element_located((By.NAME,"anjing")))

anjing = driver.find_element(By.NAME,'anjing')
s = Select(select)
# 根据下标进行选择,从0开始
s.select_by_index(1)
time.sleep(2)
# 根据value的值选择anjing
s.select_by_value('daily')
time.sleep(2)
# 根基text选择
s.select_by_visible_text('关注了吗?')

time.sleep(2)
# 判断选择是否预期
WebDriverWait(driver,20).until(EC.element_located_to_be_selected((By.XPATH,'//*[contains(text(),"关注了")]')))

实例2

import unittest
import time
 
from selenium import webdriver
from selenium.webdriver.support.ui import Select
 
 
class SelectStudy(unittest.TestCase):
 
    def setUp(self):
        # 创建一个Chrome WebDriver的实例
        self.driver = webdriver.Chrome()
 
    # 选择页面第一个下拉框,依次选择值O1-O3
    def test_selectO1ToO3(self):
        driver = self.driver
        driver.get('http://sahitest.com/demo/selectTest.htm')
        # 实例化Select
        s1 = Select(driver.find_element(By.ID,'s1Id'))
        # 查看选择框的默认值
        print s1.first_selected_option.text
        # 选择第二个选项o1
        s1.select_by_index(1)
        time.sleep(3)
        # 为了方便查看效果,可以加上等待时间
        time.sleep(3)
        # 选择value="o2"的项,value是option标签的一个属性值,并不是显示在下拉框中的值
        s1.select_by_value("o2")
        # 查看选中选择框的默认值
        print s1.first_selected_option.text
        time.sleep(3)
        # 选择text="o3"的值,即在下拉时我们可以看到的文本,visible_text是在option标签中间的值,是显示在下拉框的值
        s1.select_by_visible_text("o3")
        time.sleep(3)
 
    # 反选操作,包括取消某个值和全部取消
    def test_cancel_select(self):
        driver = self.driver
        driver.get('http://sahitest.com/demo/selectTest.htm')
        s4 = Select(driver.find_element(By.ID,'s4Id'))
        # 全选
        for option in s4.options:
            if not option.is_selected():
                print option.text
                s4.select_by_visible_text(option.text)
        time.sleep(3)
 
        # 根据index取消选中
        s4.deselect_by_index(0)
        time.sleep(3)
 
        # 根据value取消选中
        s4.deselect_by_value("o1val")
        time.sleep(5)
 
        # 根据标签文本选中
        s4.deselect_by_visible_text("o2")
        time.sleep(5)
 
        # 全选
        for option in s4.options:
            if not option.is_selected():
                s4.select_by_visible_text(option.text)
        time.sleep(3)
 
        # 取消选中所有选项
        s4.deselect_all()
 
    # 查看选中项目
    """
    输出结果为:
    o1
    o2
      With spaces
      With nbsp
    """
    def test_view_selection(self):
        driver = self.driver
        driver.get('http://sahitest.com/demo/selectTest.htm')
        s4 = Select(driver.find_element(By.ID,'s4Id'))
        # 查看选择框的默认值
        s4.select_by_index(1)
        s4.select_by_value("o2val")
        s4.select_by_visible_text("With spaces")
        s4.select_by_value("o4val")
 
        for select in s4.all_selected_options:
            print select.text
 
    def tearDown(self):
        self.driver.close()
 
 
if __name__ == "__main__":
    unittest.main()

12. 延时等待

在Selenium中,get() 方法会在网页框架加载结束后结束执行,此时如果获取page_source,可能并不是浏览器完全加载完成的页面,如果某些页面有额外的Ajax请求,我们在网页源代码中也不一定能成功获取到,这里就需要延时等待一定时间,确保节点已经加载处理好。

显式等待 和 隐式等待

  • 显式等待 就是浏览器在我们设置的时间内每隔一段时间(该时间一般都很短,默认为0.5秒,也可以自定义),执行自定义的程序判断条件,如果判断条件成立,就执行下一步,否则继续等待,直到超过设定的最长等待时间,如果没在规定时间内成立,就抛出TimeOutEcpection的异常信息。

首先指定一个等待条件,并且再指定一个最长等待时间,然后在这个时间段内进行判断是否满足等待条件,如果成立就会立即返回,如果不成立,就会一直等待,直到等待你指定的最长等待时间,如果还是不满足,就会抛出异常,如果满足了就会正常返回

  • 隐式等待 则是我们设置时间,然后程序去找元素,期间会不断刷新页面(轮询查看页面/元素是否加载完成),超时仍没找到就抛异常。

    使用隐式等待执行测试的时候,如果 Selenium 没有在DOM中找到节点,将继续等待,超出设定时间后,则抛出找不到节点的异常, 换句话说,当查找节点而节点并没有立即出现的时候,隐式等待将等待一段时间再查找DOM ,默认的时间是0

    本质上是driver的设置项,设置一次全局生效

    • 缺点:必须等到整个页面加载完成,也就是说浏览器窗口标签栏中不再出现转动的小圆圈,才会进入等待。
  • 强制等待 是用 time.sleep() 强制让浏览器等待,不管当前操作是否完成

  • 这里有个常用的模块专门用来实现显示等待和隐式等待的,它就是”wait“

😊 显式等待使用方法

WebDriverWait(driver, 超时时间, 调用频率, 要忽略的异常).until(要执行的方法, 超时时返回的错误信息)

WebDriverWait 结合 ExpectedCondition 是实现显式等待的一种方式

from selenium.webdriver.support.ui import WebDriverWait
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome(executable_path=r'C:\Users\Administrator\AppData\Local\Google\Chrome\Application\chromedriver.exe')
driver.get('https://www.baidu.com/')

# 显示等待

wait = WebDriverWait(driver, timeout=True, poll_frequency=10)
input = wait.until(EC.presence_of_element_located((By.ID, 'q')))  # 确认元素是否已经出现了
button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.btn-search')))  # 确认元素是否是可点击


time.sleep(3)
driver.close()
driver.quit()

引入WebDriverWait这个对象,指定最长等待时间,然后调用until() 方法,(可以选择传入要等待条件excpeted_conditions比如:这里传入presence_of_element_located这个条件,代表节点出现的意思,其参数就是定位节点元组)也可以自定义函数

解释:

  • driver: 当前实例的对象
  • timeout:最长等待的时间
  • poll_frequency:多少秒检测一次 默认0.5
  • EC.presence_of_element_located ((By.CLASS_NAME, “topic-item”)):判断class_name=topic-item元素是否已经成功加载。
  • 忽略异常:如果在调用until或until_not的过程中抛出这个元组中的异常,则不中断代码,继续等待,如果抛出的是这个元组外的异常,则中断代码,抛出异常。默认只有NoSuchElementException。

上述的例子中的条件:

								EC.presence_of_element_located()是确认元素是否已经出现了

								EC.element_to_be_clickable()是确认元素是否是可点击的

WebDriverWait()类

until()方法:条件成立为真
until_not()方法:条件不成立为真也就是没有指定的元素为真

expected_conditions()类

element_located_selection_state_to_be():判断一个元素的状态是否是给定的选择状态
element_selection_state_to_be():判断给定的元素是否被选中
element_located_to_be_selected():期望某个元素处于被选中状态
element_to_be_selected():期望某个元素处于选中状态
element_to_be_clickable():判断元素是否可见并且能被单击,条件满足返回页面元素对象,否则返回Flase
frame_to_be_available_and_switch_to_it():判断frame是否可用
invisibility_of_element_located():希望某个元素不可见或者不存在DOM中,满足条件返回True,否则返回定位到的元素对象
visibility_of_element_located():希望某个元素出现在DOM中并且可见,满足条件返回该元素的页面元素对象
visibility_of():希望某个元素出现在页面的DOM中,并且可见,满足条件返回该元素的页面元素对象
visibility_of_any_elements_located():希望某个元素出现在DOM中并且可见
presence_of_all_elements_located():判断页面至少有一个如果元素出现,如果满足条件,返回所有满足定位表达式的压面元素
presence_of_element_located(locator):判断某个元素是否存在DOM中,不一定可见,存在返回该元素对象
staleness_of(webelement):判断一个元素是否仍在DOM中,如果在规定时间内已经移除返回True,否则返回Flase
text_to_be_present_in_element():判断文本内容test是否出现在某个元素中,判断的是元素的text
text_to_be_present_in_element_value():判断text是否出现在元素的value属性值中
title_contains():判断页面title标签的内容包含partial_title,只需要部分匹配即可,包含返回True,不包含返回Flase
title_is():判断页面title内容是与传入的title_text内容完全匹配,匹配返回True,否则返回Flase

😊 隐式等待使用方法

隐式等待很简单,就一行代码,如下:

# 隐式等待
driver.implicitly_wait(10)

它的等待时间适用于全局的环境,也就是任何地方找不到某个元素,它都将发挥作用,如果找得到,则不会产生作用。

😊 强制等待使用方法

import time
time.sleep(3)
本文含有隐藏内容,请 开通VIP 后查看