This commit is contained in:
g1879 2022-03-20 23:26:44 +08:00
parent 96e5475c06
commit 366e03027e
10 changed files with 242 additions and 65 deletions

View File

@ -8,7 +8,7 @@
from configparser import RawConfigParser, NoSectionError, NoOptionError
from http.cookiejar import Cookie
from pathlib import Path
from typing import Any, Union
from typing import Any, Union, List
from requests.cookies import RequestsCookieJar
from selenium.webdriver.chrome.options import Options
@ -493,6 +493,51 @@ class DriverOptions(Options):
"""浏览器启动文件路径"""
return self.binary_location
# -------------重写父类方法,实现链式操作-------------
def add_argument(self, argument) -> 'DriverOptions':
"""添加一个配置项 \n
:param argument: 配置项内容
:return: 当前对象
"""
super().add_argument(argument)
return self
def set_capability(self, name, value) -> 'DriverOptions':
"""设置一个capability \n
:param name: capability名称
:param value: capability值
:return: 当前对象
"""
super().set_capability(name, value)
return self
def add_extension(self, extension: str) -> 'DriverOptions':
"""添加插件 \n
:param extension: crx文件路径
:return: 当前对象
"""
super().add_extension(extension)
return self
def add_encoded_extension(self, extension: str) -> 'DriverOptions':
"""将带有扩展数据的 Base64 编码字符串添加到将用于将其提取到 ChromeDriver 的列表中 \n
:param extension: 带有扩展数据的 Base64 编码字符串
:return: 当前对象
"""
super().add_encoded_extension(extension)
return self
def add_experimental_option(self, name: str, value: Union[str, int, dict, List[str]]) -> 'DriverOptions':
"""添加一个实验选项到浏览器 \n
:param name: 选项名称
:param value: 选项值
:return: 当前对象
"""
super().add_experimental_option(name, value)
return self
# -------------重写父类方法结束-------------
def save(self, path: str = None) -> str:
"""保存设置到文件 \n
:param path: ini文件的路径 None 保存到当前读取的配置文件传入 'default' 保存到默认ini文件

View File

@ -13,6 +13,7 @@ from selenium import webdriver
from selenium.common.exceptions import SessionNotCreatedException, WebDriverException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
from tldextract import extract
from .config import _session_options_to_dict, SessionOptions, DriverOptions, _cookies_to_tuple
@ -22,7 +23,7 @@ class Drission(object):
"""Drission类用于管理WebDriver对象和Session对象是驱动器的角色"""
def __init__(self,
driver_or_options: Union[WebDriver, Options, DriverOptions, bool] = None,
driver_or_options: Union[RemoteWebDriver, Options, DriverOptions, bool] = None,
session_or_options: Union[Session, dict, SessionOptions, bool] = None,
ini_path: str = None,
proxy: dict = None):
@ -65,7 +66,7 @@ class Drission(object):
elif driver_or_options is False:
self._driver_options = DriverOptions(read_file=False)
elif isinstance(driver_or_options, WebDriver):
elif isinstance(driver_or_options, RemoteWebDriver):
self._driver = driver_or_options
elif isinstance(driver_or_options, (Options, DriverOptions)):
@ -383,7 +384,7 @@ class Drission(object):
self.close_session()
def user_agent_to_session(driver: WebDriver, session: Session) -> None:
def user_agent_to_session(driver: RemoteWebDriver, session: Session) -> None:
"""把driver的user-agent复制到session \n
:param driver: 来源driver对象
:param session: 目标session对象

View File

@ -6,11 +6,13 @@
"""
from os import sep
from pathlib import Path
from time import time, perf_counter
from time import time, perf_counter, sleep
from typing import Union, List, Any, Tuple
from selenium.common.exceptions import TimeoutException, JavascriptException, InvalidElementStateException
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.common.exceptions import TimeoutException, JavascriptException, InvalidElementStateException, \
NoSuchElementException
# from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait
@ -872,7 +874,7 @@ class ElementsByXpath(object):
self.single = single
self.timeout = timeout
def __call__(self, ele_or_driver: Union[WebDriver, WebElement]) \
def __call__(self, ele_or_driver: Union[RemoteWebDriver, WebElement]) \
-> Union[str, DriverElement, None, List[str or DriverElement]]:
def get_nodes(node=None, xpath_txt=None, type_txt='7'):
@ -921,7 +923,7 @@ class ElementsByXpath(object):
"""
return driver.execute_script(js, node, xpath_txt)
if isinstance(ele_or_driver, WebDriver):
if isinstance(ele_or_driver, RemoteWebDriver):
driver, the_node = ele_or_driver, 'document'
else:
driver, the_node = ele_or_driver.parent, ele_or_driver
@ -969,12 +971,14 @@ class Select(object):
self.inner_ele = ele
self.select_ele = Select(ele.inner_ele)
def __call__(self, text_or_index: Union[str, int, list, tuple]) -> bool:
def __call__(self, text_or_index: Union[str, int, list, tuple], timeout=None) -> bool:
"""选定下拉列表中子元素 \n
:param text_or_index: 根据文本值选或序号择选项若允许多选传入list或tuple可多选
:param timeout: 超时时间不输入默认实用页面超时时间
:return: None
"""
return self.select(text_or_index)
timeout = timeout if timeout is not None else self.inner_ele.page.timeout
return self.select(text_or_index, timeout=timeout)
@property
def is_multi(self) -> bool:
@ -1005,35 +1009,43 @@ class Select(object):
"""清除所有已选项"""
self.select_ele.deselect_all()
def select(self, text_or_index: Union[str, int, list, tuple]) -> bool:
def select(self, text_or_index: Union[str, int, list, tuple], timeout=None) -> bool:
"""选定下拉列表中子元素 \n
:param text_or_index: 根据文本值选或序号择选项若允许多选传入list或tuple可多选
:param timeout: 超时时间不输入默认实用页面超时时间
:return: 是否选择成功
"""
i = 'index' if isinstance(text_or_index, int) else 'text'
return self._select(text_or_index, i, False)
timeout = timeout if timeout is not None else self.inner_ele.page.timeout
return self._select(text_or_index, i, False, timeout)
def select_by_value(self, value: Union[str, list, tuple]) -> bool:
def select_by_value(self, value: Union[str, list, tuple], timeout=None) -> bool:
"""此方法用于根据value值选择项。当元素是多选列表时可以接收list或tuple \n
:param value: value属性值传入list或tuple可选择多项
:param timeout: 超时时间不输入默认实用页面超时时间
:return: None
"""
return self._select(value, 'value', False)
timeout = timeout if timeout is not None else self.inner_ele.page.timeout
return self._select(value, 'value', False, timeout)
def deselect(self, text_or_index: Union[str, int, list, tuple]) -> bool:
def deselect(self, text_or_index: Union[str, int, list, tuple], timeout=None) -> bool:
"""取消选定下拉列表中子元素 \n
:param text_or_index: 根据文本或序号取消择选项若允许多选传入list或tuple可取消多项
:param timeout: 超时时间不输入默认实用页面超时时间
:return: None
"""
i = 'index' if isinstance(text_or_index, int) else 'text'
return self._select(text_or_index, i, True)
timeout = timeout if timeout is not None else self.inner_ele.page.timeout
return self._select(text_or_index, i, True, timeout)
def deselect_by_value(self, value: Union[str, list, tuple]) -> bool:
def deselect_by_value(self, value: Union[str, list, tuple], timeout=None) -> bool:
"""此方法用于根据value值取消选择项。当元素是多选列表时可以接收list或tuple \n
:param value: value属性值传入list或tuple可取消多项
:param timeout: 超时时间不输入默认实用页面超时时间
:return: None
"""
return self._select(value, 'value', True)
timeout = timeout if timeout is not None else self.inner_ele.page.timeout
return self._select(value, 'value', True, timeout)
def invert(self) -> None:
"""反选"""
@ -1046,7 +1058,8 @@ class Select(object):
def _select(self,
text_value_index: Union[str, int, list, tuple] = None,
para_type: str = 'text',
deselect: bool = False) -> bool:
deselect: bool = False,
timeout=None) -> bool:
"""选定或取消选定下拉列表中子元素 \n
:param text_value_index: 根据文本值选或序号择选项若允许多选传入list或tuple可多选
:param para_type: 参数类型可选 'text''value''index'
@ -1056,7 +1069,7 @@ class Select(object):
if not self.is_multi and isinstance(text_value_index, (list, tuple)):
raise TypeError('单选下拉列表不能传入list和tuple')
if isinstance(text_value_index, (str, int)):
def do_select():
try:
if para_type == 'text':
if deselect:
@ -1075,11 +1088,20 @@ class Select(object):
self.select_ele.select_by_index(int(text_value_index))
else:
raise ValueError('para_type参数只能传入"text""value""index"')
return True
except Exception:
except NoSuchElementException:
return False
if isinstance(text_value_index, (str, int)):
t1 = perf_counter()
ok = do_select()
while not ok and perf_counter() - t1 < timeout:
sleep(.2)
ok = do_select()
return ok
elif isinstance(text_value_index, (list, tuple)):
return self._select_multi(text_value_index, para_type, deselect)

View File

@ -13,6 +13,7 @@ from urllib.parse import quote
from selenium.common.exceptions import NoAlertPresentException
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.wait import WebDriverWait
@ -25,7 +26,7 @@ from .session_element import make_session_ele, SessionElement
class DriverPage(BasePage):
"""DriverPage封装了页面操作的常用功能使用selenium来获取、解析、操作网页"""
def __init__(self, driver: WebDriver, timeout: float = 10) -> None:
def __init__(self, driver: RemoteWebDriver, timeout: float = 10) -> None:
"""初始化函数接收一个WebDriver对象用来操作网页"""
super().__init__(timeout)
self._driver = driver
@ -302,7 +303,7 @@ class DriverPage(BasePage):
"""等待元素从dom删除、显示、隐藏 \n
:param loc_or_ele: 可以是元素查询字符串loc元组
:param timeout: 等待超时时间
:return: 等待是否成功
:return: 用于等待的ElementWaiter对象
"""
return ElementWaiter(self, loc_or_ele, timeout)

View File

@ -30,8 +30,6 @@ ele.click(timeout = 10) # 不断重试点击,直到遮罩层消失,或到
ele.click(by_js=True) # 无视遮罩层,直接用 js 点击下方元素
```
## click_at()
此方法用于带偏移量点击元素,偏移量相对于元素左上角坐标。不传入`x``y`值时点击元素中点。可选择是否用 js 方式点击,但不会进行重试。
@ -57,8 +55,6 @@ ele.click_at(x=50)
ele.click_at()
```
## r_click()
此方法实现右键单击元素。
@ -71,8 +67,6 @@ ele.click_at()
ele.r_click()
```
## r_click_at()
此方法用于带偏移量右键点击元素,用法和`click_at()`相似,但没有`by_js`参数。
@ -89,8 +83,6 @@ ele.r_click()
ele.r_click_at(50, 50)
```
## input()
此方法用于向元素输入文本或组合键,也可用于输入文件路径到`input`元素(文件间用`\n`间隔)。可选择输入前是否清空元素。
@ -166,11 +158,11 @@ ele.run_script('arguments[0].click()')
方法:
| 方法 | 参数说明 | 功能 |
| :-------: | :------: | :-----------------: |
| display() | 无 | 等待元素从 DOM 显示 |
| hidden() | 无 | 等待元素从 DOM 隐藏 |
| delete() | 无 | 等待元素从 DOM 删除 |
| 方法 | 参数说明 | 功能 |
|:---------:|:----:|:------------:|
| display() | 无 | 等待元素从 DOM 显示 |
| hidden() | 无 | 等待元素从 DOM 隐藏 |
| delete() | 无 | 等待元素从 DOM 删除 |
这些方法返回布尔值,代表是否等待成功。
@ -186,8 +178,6 @@ ele2 = ele1.ele('#div1')
ele1.wait_ele(ele2).hidden()
```
## screenshot()
此方法用于对元素进行截图。
@ -206,8 +196,6 @@ ele1.wait_ele(ele2).hidden()
path = ele.screenshot(r'D:\tmp', 'img_name')
```
## set_prop()
此方法用于设置元素`property`属性。
@ -309,18 +297,18 @@ ele1.drag_to((50, 50))
此属性用于以某种方式滚动元素中的滚动条。
调用此属性返回一个`Scroll`对象,调用该对象方法实现各种方式的滚动。
| 方法 | 参数说明 | 功能 |
| :---------------: | :----------: | :------------------------------: |
| to_top() | 无 | 滚动到顶端,水平位置不变 |
| to_bottom() | 无 | 滚动到底端,水平位置不变 |
| to_half() | 无 | 滚动到垂直中间位置,水平位置不变 |
| to_rightmost() | 无 | 滚动到最右边,垂直位置不变 |
| to_leftmost() | 无 | 滚动到最左边,垂直位置不变 |
| to_location(x, y) | 滚动条坐标值 | 滚动到指定位置 |
| up(pixel) | 滚动的像素 | 向上滚动若干像素,水平位置不变 |
| down(pixel) | 滚动的像素 | 向下滚动若干像素,水平位置不变 |
| right(pixel) | 滚动的像素 | 向左滚动若干像素,垂直位置不变 |
| left(pixel) | 滚动的像素 | 向右滚动若干像素,垂直位置不变 |
| 方法 | 参数说明 | 功能 |
|:-----------------:|:------:|:----------------:|
| to_top() | 无 | 滚动到顶端,水平位置不变 |
| to_bottom() | 无 | 滚动到底端,水平位置不变 |
| to_half() | 无 | 滚动到垂直中间位置,水平位置不变 |
| to_rightmost() | 无 | 滚动到最右边,垂直位置不变 |
| to_leftmost() | 无 | 滚动到最左边,垂直位置不变 |
| to_location(x, y) | 滚动条坐标值 | 滚动到指定位置 |
| up(pixel) | 滚动的像素 | 向上滚动若干像素,水平位置不变 |
| down(pixel) | 滚动的像素 | 向下滚动若干像素,水平位置不变 |
| right(pixel) | 滚动的像素 | 向左滚动若干像素,垂直位置不变 |
| left(pixel) | 滚动的像素 | 向右滚动若干像素,垂直位置不变 |
```python
# 滚动到底部
@ -367,6 +355,8 @@ ele.hover()
ele.select
```
?> **Tips:** <br>网页操作中经常遇到动态变化的表单,这时需要等待列表项加载。`Select`类内置等待功能,默认为页面等待时间。
## 属性
### is_multi
@ -413,12 +403,13 @@ Select 类的`__call__()`方法直接调用这个方法,因此可以直接`ele
参数:
- text_or_index根据文本或序号择选项若允许多选传入`list``tuple`可多选
- timeout等待列表项出现的时间
返回:是否选择成功
```python
# 根据文本选择下拉列表项
ele.select('text')
# 根据文本选择下拉列表项等待1秒
ele.select('text', timeout=1)
# 选择第一个下拉列表项
ele.select(0)
@ -438,6 +429,7 @@ ele.select(('text1', 2))
参数:
- value`value`属性值,若允许多选,传入`list``tuple`可多选
- timeout等待列表项出现的时间
返回:是否选择成功
@ -458,6 +450,7 @@ ele.select.select_by_value(('value1', 2))
参数:
- text_or_index根据文本、值选或序号择选项若允许多选传入`list``tuple`可多选
- timeout等待列表项出现的时间
返回:是否取消选择成功
@ -485,6 +478,7 @@ ele.select.deselect(('text1', 2))
参数:
- value`value`属性值,若允许多选,传入`list``tuple`可取消多项
- timeout等待列表项出现的时间
返回:是否取消选择成功

View File

@ -16,8 +16,7 @@
- driver_options浏览器设置没传入`drission`参数时会用这个设置新建`Drission`对象中的`WebDriver`对象,传入`False`则不创建
- session_optionsrequests 设置,没传入`drission`参数时会用这个设置新建`Drission`对象中的`Session`对象,传入`False`则不创建
!> **注意:** <br>
有传入`drission`参数时,`driver_options``session_options`参数无效
!> **注意:** <br>有传入`drission`参数时,`driver_options``session_options`参数无效
# 直接创建
@ -159,3 +158,109 @@ d = Drission(driver_or_options=do, session_or_options=so)
page = MixPage(drission=d)
```
# 接管手动打开的浏览器
如果须要手动打开浏览器,手动操作后再接管,可以这样做:
- 右键点击浏览器图标,选择属性
- 在“目标”路径后面加上` --remote-debugging-port=端口号`(注意最前面有个空格)
- 点击确定
- 在程序中的浏览器配置中指定接管该端口浏览器,如下:
```
# 文件快捷方式的目标路径设置
D:\chrome.exe --remote-debugging-port=9222
```
```python
from DrissionPage.configs import DriverOptions
from DrissionPage import MixPage
do = DriverOptions().set_paths(local_port=9222)
page = MixPage(driver_options=do)
```
?>**Tips**<br>接管使用 bat 文件打开的浏览器也是一样做法做法。<br>接管浏览器时只有`local_port``debugger_address``driver_path`参数是有效的。
# 多 Chrome 浏览器共存
如果想要同时操作多个 Chrome 浏览器,或者自己在使用 Chrome 上网,同时控制另外几个跑自动化,就须要给这些被程序控制的浏览器设置单独的端口和用户文件夹,否则会造成冲突。具体用`DriverOptions`对象进行设置,示例如下:
```python
from DrissionPage.confing import DriverOptions
do1 = DriverOptions().set_paths(local_port=9111, user_path=r'D:\data1')
do2 = DriverOptions().set_paths(local_port=9222, user_path=r'D:\data2')
page1 = MixPage(driver_options=do1)
page2 = MixPage(driver_options=do2)
page1.get('https://www.baidu.com')
page2.get('http://www.163.com')
```
如果要接管多个手动打开的浏览器,每个浏览器后面的参数都要添加`--remote-debugging-port``--user-data-dir`参数。示例如下:
```
# 浏览器1目标设置
D:\chrome.exe --remote-debugging-port=9111 --user-data-dir=D:\data1
# 浏览器2目标设置
D:\chrome.exe --remote-debugging-port=9222 --user-data-dir=D:\data2
```
```python
from DrissionPage.confing import DriverOptions
do1 = DriverOptions().set_paths(local_port=9111)
do2 = DriverOptions().set_paths(local_port=9222)
page1 = MixPage(driver_options=do1)
page2 = MixPage(driver_options=do2)
```
?> **Tips**<br>使用 bat 文件打开浏览器再接管操作是一样的。
# 一些技巧
事实上,本库默认启动浏览器的方式是先通过程序在 9222或用户指定的端口运行一个浏览器进程然后通过程序接管。这种做法有很多好处
## 可重复使用的浏览器对象
当程序运行完毕,浏览器不会主动关闭,下次再运行的时候可以直接在当前状态下继续运行。于是无须每次打开新的浏览器对象,无须从最开始步骤重新运行整个程序,一些前置条件(如登录)也无须每次运行,大大提高开发效率。
## 简便的调试方式
通过重复使用浏览器对象,用户可以把浏览器页面调整到某个状态再用程序接管,对调试某个特定问题效率极高。比如有些通过很多步骤才能到达的页面,如果每次重新运行会花费大量时间,而将页面固定再由程序接管,测试各种细节非常方便快捷。
## 一行代码传递登录状态到 requests
本库的一个特点是打通了浏览器和`requests`之间的登录状态,我们可以手动登录浏览器,再用程序接管,然后切换到 s 模式,这时 s 模式的`Session`对象即已活动浏览器的登录信息,无须用`requests`
处理登录过程,极大地简化了开发复杂程度。示例:
```python
# 假设已在 9222 端口打开了一个浏览器,并已登录某网站
from DrissionPage import MixPage
page = MixPage() # 用 d 模式创建页面对象,默认接管 9222 端口
page.change_mode() # 切换到 s 模式,即使用 requests 进行操作
page.get('某url...') # 即可已登录状态访问
```
使用其它端口号:
```python
from DrissionPage.configs import DriverOptions
from DrissionPage import MixPage
do = DriverOptions().set_paths(local_port=9333)
page = MixPage(driver_option=do)
page.change_mode()
page.get('某url...')
```

View File

@ -13,8 +13,6 @@
- read_file是否从默认 ini 文件中读取配置信息
- ini_pathini 文件路径,为`None`则读取默认 ini 文件
## headers
该属性返回`headers`设置信息,可传入字典赋值。
@ -162,4 +160,3 @@ so.set_a_header('Connection', 'keep-alive')
# 以该配置创建页面对象
page = MixPage(mode='s', session_options=so)
```

View File

@ -21,7 +21,8 @@ from DrissionPage import MixPage
s 模式是无须初始化的,导入后就可以直接使用。
d 模式因为使用浏览器,须要配置浏览器和对应的 driver。
!> **注意:** <br> 这里介绍的方法只支持 Windows 系统,使用其它系统请查看 [使用其它系统或浏览器](使用方法\使用其它系统或浏览器.md) 章节。
!> **注意:** <br>这里介绍的方法只适用于 Windows 系统和 Chrome 浏览器,使用其它系统或浏览器请查看 [使用其它系统或浏览器](使用方法\使用其它系统或浏览器.md) 章节。<br>如果您有已打开的 Chrome
浏览器,请先关闭,否则会造成冲突。后面在 [创建页面对象](使用方法\使创建页面对象.md) 章节再介绍多 Chrome 浏览器共存的方法。
## 自动配置方式
@ -99,7 +100,7 @@ for row in rows:
cols = row.eles('tag:td')
# 生成当前行数据列表(跳过其中没用的几列)
data = [td.text for k, td in enumerate(cols) if k not in (2, 4, 6)]
print(data) # 打印行数据
```

View File

@ -1,4 +1,16 @@
# v2.5.3
# v2.5.7
- 列表元素`select()``deselect()`等方法添加`timeout`参数,可等待列表元素加载
- 优化了对消息提示框的处理
- `drag()``drag_to()`不再检测是否拖拽成功改成返回None
- `DriverOptions`对象从父类继承的方法也支持链式操作
- 其它优化和问题修复
# v2.5.5
- `DriverPage`添加`run_cdp()`方法
- `get()``post()`方法删除`go_anyway`参数
@ -260,4 +272,3 @@
# v0.8.4
- 基本完成

View File

@ -6,7 +6,7 @@ with open("README.md", "r", encoding='utf-8') as fh:
setup(
name="DrissionPage",
version="2.5.6",
version="2.5.7",
author="g1879",
author_email="g1879@qq.com",
description="A module that integrates selenium and requests session, encapsulates common page operations.",