s模式get()使用page_load作为timeout;set_page_load_strategy改用类来表示;修改文档

This commit is contained in:
g1879 2022-12-16 00:12:03 +08:00
parent d8f6487a05
commit 545ce2e2aa
27 changed files with 1188 additions and 64 deletions

View File

@ -28,6 +28,7 @@ class ChromiumBase(BasePage):
self._root_id = None
self._debug = False
self._debug_recorder = None
self.timeouts = Timeout(self)
self._connect_browser(address, tab_id)
def _connect_browser(self, addr_tab_opts=None, tab_id=None):
@ -37,7 +38,6 @@ class ChromiumBase(BasePage):
:return: None
"""
self._root_id = None
self.timeouts = Timeout(self)
self._control_session = Session()
self._control_session.keep_alive = False
self._first_run = True
@ -256,14 +256,10 @@ class ChromiumBase(BasePage):
self._scroll = ChromeScroll(self)
return self._scroll
def set_page_load_strategy(self, value):
"""设置页面加载策略 \n
:param value: 可选'normal', 'eager', 'none'
:return: None
"""
if value not in ('normal', 'eager', 'none'):
raise ValueError("只能选择'normal', 'eager', 'none'")
self._page_load_strategy = value
@property
def set_page_load_strategy(self):
"""返回用于设置页面加载策略的对象"""
return pageLoadStrategy(self)
def set_timeouts(self, implicit=None, page_load=None, script=None):
"""设置超时时间,单位为秒 \n
@ -307,7 +303,7 @@ class ChromiumBase(BasePage):
:param retry: 重试次数
:param interval: 重试间隔
:param timeout: 连接超时时间
:return: 目标url是否可用返回None表示不确定
:return: 目标url是否可用
"""
self._url_available = self._get(url, show_errmsg, retry, interval, timeout)
return self._url_available
@ -810,3 +806,34 @@ class Timeout(object):
@property
def implicit(self):
return self.page.timeout
class pageLoadStrategy(object):
"""用于设置页面加载策略的类"""
def __init__(self, page):
"""
:param page: ChromiumBase对象
"""
self.page = page
def __call__(self, value):
"""设置加载策略 \n
:param value: 可选 'normal', 'eager', 'none'
:return: None
"""
if value.lower() not in ('normal', 'eager', 'none'):
raise ValueError("只能选择 'normal', 'eager', 'none'")
self.page._page_load_strategy = value
def set_normal(self):
"""设置页面加载策略为normal"""
self.page._page_load_strategy = 'normal'
def set_eager(self):
"""设置页面加载策略为eager"""
self.page._page_load_strategy = 'eager'
def set_none(self):
"""设置页面加载策略为none"""
self.page._page_load_strategy = 'none'

View File

@ -101,7 +101,8 @@ class ChromiumBase(BasePage):
@property
def scroll(self) -> 'ChromeScroll': ...
def set_page_load_strategy(self, value: str) -> None: ...
@property
def set_page_load_strategy(self) -> pageLoadStrategy: ...
def set_timeouts(self, implicit: float = ..., page_load: float = ..., script: float = ...) -> None: ...
@ -279,3 +280,16 @@ class Timeout(object):
@property
def implicit(self) -> float: ...
class pageLoadStrategy(object):
def __init__(self, page: ChromiumBase):
self.page: ChromiumBase = ...
def __call__(self, value: str) -> None: ...
def set_normal(self) -> None: ...
def set_eager(self) -> None: ...
def set_none(self) -> None: ...

View File

@ -5,7 +5,7 @@ from requests import Session
from tldextract import extract
from .base import BasePage
from .chromium_base import ChromiumBase
from .chromium_base import ChromiumBase, Timeout
from .chromium_page import ChromiumPage
from .config import DriverOptions, SessionOptions, cookies_to_tuple
from .session_page import SessionPage
@ -32,6 +32,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
self._session = None
self._tab_obj = None
self._is_loading = False
self.timeouts = Timeout(self)
self._set_session_options(session_or_options)
self._set_driver_options(driver_or_options)
self._setting_tab_id = tab_id
@ -102,9 +103,6 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
if self._session is None:
self._set_session(self._session_options)
# if self._proxy:
# self._session.proxies = self._proxy
return self._session
@property
@ -117,7 +115,6 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
"""返回用于控制浏览器的Tab对象会先等待页面加载完毕"""
while self._is_loading:
sleep(.1)
# self._wait_loading()
return self._driver
@property
@ -150,6 +147,8 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
if self._mode == 'd':
return super(SessionPage, self).get(url, show_errmsg, retry, interval, timeout)
elif self._mode == 's':
if timeout is None:
timeout = self.timeouts.page_load if self._has_driver else self.timeout
return super().get(url, show_errmsg, retry, interval, timeout, **kwargs)
def ele(self, loc_or_ele, timeout=None):
@ -291,7 +290,6 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
'name': cookie['name'],
'domain': cookie['domain']}
result_cookies.append(c)
# super(WebPage, self)._wait_driver.Network.setCookies(cookies=result_cookies)
self._tab_obj.Network.setCookies(cookies=result_cookies)
# 添加cookie到session

View File

@ -89,7 +89,8 @@ page = MixPage(mode='s', session_options=so, driver_options=do)
# 传入`Drission`对象创建
在入门指南的基本概念一节里,我们讲过`Drission`对象相当于驱动器的角色。事实上,上述两种方式,`MixPage`都会自动创建一个`Drission`对象用于管理与网站或浏览器的连接,我们当然也可以手动创建并传入`MixPage`
在入门指南的基本概念一节里,我们讲过`Drission`对象相当于驱动器的角色。事实上,上述两种方式,`MixPage`都会自动创建一个`Drission`对象用于管理与网站或浏览器的连接,我们当然也可以手动创建并传入`MixPage`
`Drission`一般是不用手动创建的要手动创建的时候一般是用于i以下几种情况
- 指定使用某个配置文件

View File

@ -10,11 +10,9 @@
由于该工具不依赖 DrissionPage现已独立发布为一个库但仍然可以在 DrissionPage 中导入。
!> 为了便于维护,该工具用法请异步 [FlowViewer](https://gitee.com/g1879/FlowViewer) 查看。
#
#
# 简单示例

View File

@ -1,6 +1,7 @@
获取到须要的页面元素后,可以使用元素对象获取元素的信息。
本库有三种元素对象,分别是`DriverElement``ShadowRootElement``SessionElement`,前两者是 d 模式下通过浏览器页面元素生成,后者由静态文本生成。`DriverElement``SessionElement`的基本属性一致。
本库有三种元素对象,分别是`DriverElement``ShadowRootElement``SessionElement`,前两者是 d 模式下通过浏览器页面元素生成,后者由静态文本生成。`DriverElement`
`SessionElement`的基本属性一致。
`ShadowRootElement`由于是 shadow dom 元素,属性比较其余两种少,下文单独介绍。

View File

@ -0,0 +1,207 @@
在入门指南的快速上手一节,我们已经初步了解如何创建页面对象,本节进一步介绍更多功能。
# ✔️ `WebPage`
`WebPage`对象封装了常用的网页操作,并实现在浏览器和 requests 两种模式之间的切换。
初始化参数:
- mode初始化时模式`'d'``'s'`,默认为`'d'`
- timeout超时时间s 模式时为连接时间d 模式时为查找元素、处理弹出框、输入文本等超时时间
- driver_or_options`Tab`对象或`DriverOptions`对象,为`None`时使用 ini 文件配置,为`False`时不读取 ini 文件
- session_or_options`Session`对象或`SessionOptions`对象,为`None`时使用 ini 文件配置,为`False`时不读取 ini 文件
# ✔️ 直接创建
这种方式代码最简洁,程序会从配置文件中读取配置,自动生成页面对象。可以保持代码简洁。
在基本概念一节我们提到过,本库使用配置文件记录常用配置信息,也可以直接把配置写在代码里。
?>**Tips**<br>默认配置文件中,程序使用 9222 端口启动浏览器,浏览器路径为 Chrome。如路径中没找到浏览器执行文件Windows 系统下程序会在注册表中查找路径,如果还是没找到,则要用下一种方式手动配置路径。
```python
# 默认以 d 模式创建页面对象
page = WebPage('d')
# 指定以 s 模式创建页面对象
page = WebPage('s')
```
# ✔️ 通过配置信息创建
本库有两种管理配置信息的对象,`DriverOptions``SessionOptions`,分别对应 d 模式和 s 模式的配置。须要时,可以创建相应的配置对象进行设置。
## 📍 `DriverOptions`
`DriverOptions`用于管理创建浏览器时的配置,内置了常用的配置,并能实现链式操作。详细使用方法见“启动配置”一节。
初始化参数:
- read_file是否从 ini 文件中读取配置信息
- ini_pathini 文件路径,为`None`则读取默认 ini 文件
!>**注意:**<br>浏览器创建后再修改这个配置是没有效果的。
```python
# 导入 DriverOptions
from DrissionPage import WebPage, DriverOptions
# 创建浏览器配置对象,指定浏览器路径和运行端口
do = DriverOptions().set_paths(chrome_path=r'D:\chrome.exe', local_port=9333)
# 用该配置创建页面对象
page = WebPage(driver_or_options=do)
```
## 📍 `SessionOptions`
`SessionOptions`用于管理创建`Session`对象时的配置,内置了常用的配置,并能实现链式操作。详细使用方法见“启动配置”一节。
初始化参数:
- read_file是否从 ini 文件中读取配置信息
- ini_pathini 文件路径,为`None`则读取默认 ini 文件
!>**注意:**<br> `Session`对象创建后再修改这个配置是没有效果的。
```python
# 导入 SessionOptions
from DrissionPage import WebPage SessionOptions
proxies = {'http': 'http://127.0.0.1:1080',
'https': 'https://127.0.0.1:1080'}
# 创建配置对象,不从 ini 文件读取,并设置代理信息
so = SessionOptions(read_file=False).set_proxies(proxies)
# 用该配置创建页面对象s 模式)
page = WebPage(mode='s', session_options=so)
```
d 模式的配置和 s 模式的配置是可以同时使用的,不会互相影响。
```python
page = WebPage(mode='s', session_options=so, driver_options=do)
```
## 📍 使用其它 ini 文件创建
以上方法是使用默认 ini 文件中保存的配置信息创建对象,你可以保存一个 ini 文件到别的地方,并在创建对象时指定使用它。
```python
from DrissionPage import WebPage, DriverOptinos
do = DriverOptinos(ini_path=r'./config1.ini')
page = WebPage(driver_or_options=do)
```
# ✔️ 传递驱动器
当须要使用多个页面对象共同操作一个页面时,可在对象间传递驱动器。
```python
from DrissionPage import WebPage
page1 = WebPage()
driver = page1.driver
session = page1.session
page2 = WebPage(driver_or_options=driver, session_or_options=session)
```
# ✔️ 接管手动打开的浏览器
如果须要手动打开浏览器再接管,可以这样做:
- 右键点击浏览器图标,选择属性
- 在“目标”路径后面加上` --remote-debugging-port=端口号`(注意最前面有个空格)
- 点击确定
- 在程序中的浏览器配置中指定接管该端口浏览器,如下:
```
# 文件快捷方式的目标路径设置
D:\chrome.exe --remote-debugging-port=9222
```
```python
from DrissionPage import WebPage, DriverOptions
do = DriverOptions().set_paths(local_port=9222)
page = WebPage(driver_options=do)
```
?>**Tips**<br>接管使用 bat 文件打开的浏览器也是一样做法做法。<br>接管浏览器时只有`local_port``debugger_address`参数是有效的。
# ✔️ 多 Chrome 浏览器共存
如果想要同时操作多个 Chrome 浏览器,或者自己在使用 Chrome 上网,同时控制另外几个跑自动化,就须要给这些被程序控制的浏览器设置单独的端口和用户文件夹,否则会造成冲突。具体用`DriverOptions`对象进行设置,示例如下:
```python
from DrissionPage import DriverOptions
do1 = DriverOptions().set_paths(local_port=9111, user_data_path=r'D:\data1')
do2 = DriverOptions().set_paths(local_port=9222, user_data_path=r'D:\data2')
page1 = WebPage(driver_or_options=do1)
page2 = WebPage(driver_or_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 import DriverOptions
do1 = DriverOptions().set_paths(local_port=9111)
do2 = DriverOptions().set_paths(local_port=9222)
page1 = WebPage(driver_or_options=do1)
page2 = WebPage(driver_or_options=do2)
```
?> **Tips**<br>使用 bat 文件打开浏览器再接管操作是一样的。
# ✔️ 一些技巧
事实上,本库默认启动浏览器的方式是先通过程序在 9222或用户指定的端口运行一个浏览器进程然后通过程序接管。这种做法有很多好处
## 📍 可重复使用的浏览器对象
当程序运行完毕,浏览器不会主动关闭,下次再运行的时候可以直接在当前状态下继续运行。于是无须每次打开新的浏览器对象,无须从最开始步骤重新运行整个程序,一些前置条件(如登录)也无须每次运行,大大提高开发效率。
## 📍 简便的调试方式
通过重复使用浏览器对象,用户可以把浏览器页面调整到某个状态再用程序接管,对调试某个特定问题效率极高。比如有些通过很多步骤才能到达的页面,如果每次重新运行会花费大量时间,而将页面固定再由程序接管,测试各种细节非常方便快捷。
## 📍 一行代码传递登录状态到 requests
本库的一个特点是打通了浏览器和`requests`之间的登录状态,我们可以手动登录浏览器,再用程序接管,然后切换到 s 模式,这时 s 模式的`Session`对象即已活动浏览器的登录信息,无须用`requests`
处理登录过程,极大地简化了开发复杂程度。示例:
```python
# 假设已在 9222 端口打开了一个浏览器,并已登录某网站
from DrissionPage import WebPage
page = WebPage() # 用 d 模式创建页面对象,默认接管 9222 端口
page.change_mode() # 切换到 s 模式,即使用 requests 进行操作
page.get('某url...') # 即可已登录状态访问
```
使用其它端口号:
```python
from DrissionPage import WebPage, DriverOptions
do = DriverOptions().set_paths(local_port=9333)
page = WebPage(driver_option=do)
page.change_mode()
page.get('某url...')
```

View File

@ -0,0 +1,164 @@
本节介绍如果访问网页。
# ✔️ d 模式
d 模式下使用`get()`方法访问网页。
## 📍 `get()`方法
该方法用于跳转到一个网址。
当连接失败时,程序默认重试 3 次,每次间隔 2 秒,可以通过参数设置重试次数和间隔。
d 模式下根据浏览器的状态,还可以通过重写`check_page()`方法实现自定义检查方式。
参数:
- url目标 url
- show_errmsg是否显示和抛出异常。默认不抛出连接错误会返回`None`
- retry重试次数与页面对象的设置一致默认 3 次
- interval重试间隔与页面对象的设置一致默认 2 秒
- timeout连接超时时间
返回:`bool`类型,表示是否连接成功
```python
from DrissionPage import WebPage
page = WebPage()
page.get('https://www.baidu.com')
```
## 📍 设置超时时间和重试参数
网络不稳定的时候访问页面不一定成功,`get()`方法内置了超时和重试功能。通过`retry``interval``timeout`三个参数进行设置。
其中,如不指定`timeout`参数,该参数会使用`WebPage``timeouts`属性的`page_load`参数中的值。
```python
from DrissionPage import WebPage
page = WebPage()
page.get('https://www.163.com', retry=1, interval=1, timeout=1.5)
```
## 📍 设置加载策略
通过设置`WebPage`对象的`page_load_strategy`属性,可设置页面停止加载的时机。页面加载时,在到达超时时间,或达到设定的状态,就会停止,可有效节省采集时间。有以下三种模式:
- `'normal'`:常规模式,会等待页面加载完毕。
- `'eager'`:加载完 DOM 即停止加载。
- `'none'`:完成连接即停止加载。
默认设置为`'normal'`
```python
from DrissionPage import WebPage
page = WebPage()
page.set_page_load_strategy.set_eager()
```
# ✔️ s 模式
s 模式基于 requests因此可使用 requests 内置的所有请求方式,包括`get()``post()``head()``options()``put()``patch()``delete()`
。不过本库目前只对`get()``post()`做了封装和优化,其余方式可通过调用`WebPage`内置的`Session`对象使用。
## 📍 `get()`方法
`get()`方法使用语法与 requests 的`get()`方法一致,在此基础上增加了重试设置参数。
参数:
- url目标 url
- show_errmsg是否显示和抛出异常默认不抛出连接错误会返回 None
- retry重试次数与页面对象的设置一致默认 3 次
- interval重试间隔与页面对象的设置一致默认 2 秒
- timeout连接超时时间
- **kwargs连接参数具体见 requests用法
返回:`bool`类型,表示是否连接成功,根据`Response`对象的`status_code`参数决定
s 模式的`**kwargs`参数与 requests 中该参数使用方法一致,但有一个特点,如果该参数中设置了某一项(如`headers`),该项中的每个项会覆盖从配置中读取的同名项,而不会整个覆盖。
就是说,如果想继续使用配置中的`headers`信息,而只想修改其中一项,只需要传入该项的值即可。这样可以简化代码逻辑。
与 requests 不一样,`get()`方法不返回`Response`对象以获取结果,处理返回结果的方式详见后面章节。
实用功能:
- 程序会根据要访问的网址自动在`headers`中加入`Host``Referer`
- 程序会自动从返回内容中确定编码,一般情况无须手动设置
```python
from DrissionPage import WebPage
page = WebPage('s')
url = 'https://www.baidu.com'
headers = {'referer': 'gitee.com'}
cookies = {'name': 'value'}
proxies = {'http': '127.0.0.1:1080', 'https': '127.0.0.1:1080'}
page.get(url, headers=headers, cookies=cookies, proxies=proxies)
```
## 📍 `post()`方法
此方法是用 post 方式请求页面。用法与`get()`一致。调用时,`WebPage`对象会自动切换到 s 模式。
参数:
- url目标 url
- data提交的数据可以是`dict``str`类型
- json提交的数据可以是`dict``str`类型
- show_errmsg是否显示和抛出异常默认不抛出连接错误会返回 None
- retry重试次数与页面对象的设置一致默认 3 次
- interval重试间隔与页面对象的设置一致默认 2 秒
- **kwargs连接参数s 模式专用
```python
from DrissionPage import WebPage
page = WebPage('s')
data = {'username': 'xxxxx', 'pwd': 'xxxxx'}
page.post('http://example.com', data=data)
# 或
page.post('http://example.com', json=data)
```
`data`参数和`json`参数都可接收`str``dict`格式数据,即有以下 4 种传递数据的方式:
```python
# 向 data 参数传入字符串
page.post(url, data='abc=123')
# 向 data 参数传入字典
page.post(url, data={'abc': '123'})
# 向 json 参数传入字符串
page.post(url, json='abc=123')
# 向 json 参数传入字典
page.post(url, json={'abc': '123'})
```
具体使用哪种,按服务器要求而定。
## 📍 其它请求方式
本库只针对常用的 get 和 post 方式作了优化,但也可以通过提取页面对象内的`Session`对象以原生 requests 代码方式执行其它请求方式。当然,它们工作在 s 模式。
```python
from DrissionPage import WebPage
page = WebPage('s')
# 获取内置的 Session 对象
session = page.session
# 以 head 方式发送请求
response = session.head('https://www.baidu.com')
print(response.headers)
```
输出:
```shell
{'Accept-Ranges': 'bytes', 'Cache-Control': 'private, no-cache, no-store, proxy-revalidate, no-transform', 'Connection': 'keep-alive', 'Content-Length': '277', 'Content-Type': 'text/html', 'Date': 'Tue, 04 Jan 2022 06:49:18 GMT', 'Etag': '"575e1f72-115"', 'Last-Modified': 'Mon, 13 Jun 2016 02:50:26 GMT', 'Pragma': 'no-cache', 'Server': 'bfe/1.0.8.18'}
```

View File

@ -0,0 +1,708 @@
本节介绍如何获取元素对象。
无论是数据采集还是页面自动化,定位元素都是重中之重的的技能,浏览器开发者工具虽然可以直接复制绝对 xpath 或 css 路径,但这样做一来代码繁琐,可读性低,二来难以应付动态变化的页面。
本库提供一套简洁易用的语法,用于快速定位元素,并且内置等待功能、支持链式查找,减少了代码的复杂性。
定位元素大致分为三种方法:
- 在页面或元素内查找子元素
- 根据 DOM 结构相对定位
- 根据页面布局位置相对定位
d 模式的元素还有专门用于处理 shadow dom 的`shadow_root`属性。获取到的元素可继续用这些方法获取后代元素,使用方法和普通元素一致。
# ✔️ 示例
## 📍 简单示例
```html
<html>
<body>
<div id="one">
<p class="p_cls" name="row1">第一行</p>
<p class="p_cls" name="row2">第二行</p>
<p class="p_cls">第三行</p>
</div>
<div id="two">
第二个div
</div>
</body>
</html>
```
假设 page 为以上页面的`WebPage`对象,我们可以用这些方法获取其中的元素:
```python
# 获取 id 为 one 的元素
div1 = page.ele('#one')
# 获取 div1 元素内所有 p 元素组成的列表
p_eles = div1.eles('tag:p')
# 获取 name 属性为 row1 的元素
p1 = page.ele('@name=row1')
# 获取 name 属性为 row2 且包含“第二”文本的 p 元素
p_ele = page.ele('tag:p@@text():第二@@name=row2')
# 获取包含“第二个div”文本的元素
div2 = page.ele('第二个div')
# 用 xpath 查找
div2 = page.ele('xpath://div[@id="tow"]')
# 从第一行元素用相对定位获取第三行元素
p3 = p1.next(2)
# 获取 p1 元素的父元素
parent = p1.parent()
# 获取 p1 后面的第一个 div 元素
div2 = p1.after(1, 'tag:div')
```
## 📍 实际示例
复制此代码可直接运行查看结果。
```python
from DrissionPage import WebPage
page = WebPage('s')
page.get('https://gitee.com/explore')
# 获取包含“全部推荐项目”文本的 ul 元素
ul_ele = page.ele('tag:ul@@text():全部推荐项目')
# 获取该 ul 元素下所有 a 元素
titles = ul_ele.eles('tag:a')
# 遍历列表,打印每个 a 元素的文本
for i in titles:
print(i.text)
"""输出:
全部推荐项目
前沿技术
智能硬件
IOT/物联网/边缘计算
车载应用
以下省略……
"""
```
# ✔️ 查找元素方法
以下方法,既可用于在页面中查找元素,也可用于在元素中查找子元素。
## ele()
此方法用于查找并返回第一个匹配的元素d 模式下返回`ChromiumElement`s 模式下返回`SessionElement`,用 xpath 获取元素属性时,直接返回属性文本。查找不到结果则返回`None`
参数:
- loc_or_str元素对象拥有元素的定位信息可以是 loc 元组,或查询字符串
- loc_or_ele页面对象拥有元素的定位信息可以是元素对象loc 元组,或查询字符串
- timeout查找元素超时时间默认与元素所在页面等待时间一致s 模式下无效
返回s 模式下返回`SessionElement`d 模式下返回`DriverElement`,或用 xpath 获取到的属性值
```python
# 在页面内查找元素
ele1 = page.ele('search text')
# 在元素内查找后代元素
ele2 = ele1.ele('search text')
# 使用 xpath 获取后代中第一个 div 元素的 class 属性
class = ele1.ele('xpath://div/@class')
```
## eles()
此方法与`ele()`相似,但返回的是匹配到的所有元素组成的列表,用 xpath 获取元素属性时,返回属性文本组成的列表。
参数:
- loc_or_str元素的定位信息可以是 loc 元组,或查询字符串
- timeout查找元素超时时间默认与元素所在页面等待时间一致s 模式下无效
返回s 模式下返回`SessionElement`组成的列表d 模式下返回`DriverElement`组成的列表,或用 xpath 获取到的属性值组成的列表
```python
# 获取 ele 元素内的所有 p 元素
p_eles = ele.eles('tag:p')
# 打印第一个 p 元素
print(p_eles[0])
```
## s_ele()
此方法用于在一个元素下查找后代元素,以`SessionElement`形式返回结果xpath 获取属性值时依然是返回`str`),也可以直接将一个元素或页面转换为`SessionElement`版本。
这是为了 d 模式处理速度的提升,当爬取复杂页面信息而且不须要和元素进行交互时,生成整个页面或者主要容器元素的`SessionElement`,再在其中获取信息,可以将速度提升几个数量级。
s 模式下这个方法和`ele()`是一样的。
参数:
- loc_or_str元素对象拥有元素的定位信息可以是 loc 元组,或查询字符串。为`None`时直接返回当前元素的`SessionElemnet`版本
- loc_or_ele页面对象拥有元素的定位信息可以是 loc 元组,或查询字符串。为`None`时直接返回当前页面的 `SessionElemnet`版本
返回:`SessionElement`,或用 xpath 获取到的属性值
```python
# 获取元素或页面的的 SessionElement 版本
ele2 = ele1.s_ele()
ele2 = page.s_ele()
# 在 ele1 元素下查找元素,并以 SessionElemnet 返回
ele2 = ele1.s_ele('search text')
# 在页面下查找元素,并以 SessionElemnet 返回
ele = page.s_ele('search text')
```
## s_eles()
此方法与`s_ele()`相似,但返回的是匹配到的所有元素组成的列表,或属性值组成的列表。
参数:
- loc_or_str元素的定位信息可以是 loc 元组,或查询字符串(必填)
返回:`SessionElement`组成的列表,或用 xpath 获取到的属性值组成的列表
## active_ele
该属性返回当前页面焦点所在元素。d 模式独有。
```python
ele = page.active_ele
```
## shadow_root
`DriverElement`元素除了以上方法和属性外,还有`shadow_root`属性,用于获取其内部的 shadow_root 元素。
该属性返回的是一个`ShadowRootElement`,类似于`DriverElement`,功能比`DriverElement`少。但也有`ele()``eles()`方法,可直接搜索其下的元素,返回 `DriverElement`
元素。返回的`DriverElement`和普通的没有区别。
```python
# 获取一个元素下是 shadow root
shadow_ele = ele.shadow_root
# 获取该 shadow root 下的一个元素
ele1 = shadow_ele.ele('search text')
# 点击获取到的元素
ele1.click()
```
# ✔️ 查找语法
我们使用一套简洁高效的语法去定位元素,大大简化了定位元素的代码量,增强了功能,也兼容 css selector、xpath、selenium 原生的 loc 元组s 模式也能用。d 模式和 s
模式定位元素的语法是完全一样的,便于模式切换时平滑过渡。
**匹配模式** 指字符串是否完全匹配,有以下两种:
## 📍 精确匹配符`=`
表示精确匹配,匹配完全符合的文本或属性。
## 📍 模糊匹配符`:`
表示模糊匹配,匹配含有某个字符串的文本或属性。
**关键字** 是出现在定位语句最左边,用于指明该语句以哪种方式去查找元素,有以下这些:
## 📍 id 匹配符`#`
表示`id`属性,只在语句最前面且单独使用时生效,可配合`=``:`
```python
# 在页面中查找 id 属性为 ele_id 的元素
ele1 = page.ele('#ele_id')
# 在 ele1 元素内查找 id 属性包含 ele_id 文本的元素
ele2 = ele1.ele('#:ele_id')
```
## 📍 class 匹配符`.`
表示`class`属性,只在语句最前面且单独使用时生效,可配合`=``:`
```python
# 查找 class 属性为 ele_class 的元素
ele2 = ele1.ele('.ele_class')
# 查找 class 属性包含 ele_class 文本的元素
ele2 = ele1.ele('.:ele_class')
```
## 📍 单属性匹配符 `@`
表示某个属性,只匹配一个属性。
`@`关键字只有一个简单功能,就是匹配`@`后面的内容,不再对后面的字符串进行解析。因此即使后面的字符串也存在`@``@@`,也作为要匹配的内容对待。
!> **注意:**
如果属性中包含特殊字符,如包含`@`,用这个方式不能正确匹配到,须使用 css selector 方式查找。且特殊字符要用`\`转义。
```python
# 查找 name 属性为 ele_name 的元素
ele2 = ele1.ele('@name=ele_name')
# 查找 name 属性包含 ele_name 文本的元素
ele2 = ele1.ele('@name:ele_name')
# 查找有 name 属性的元素
ele2 = ele1.ele('@name')
# 查找没有任何属性的元素
ele2 = ele1.ele('@')
# 查找 emaile 属性为 abc@def.com 的元素,有多个 @ 也不会重复处理
ele2 = ele1.ele('@email=abc@def.com')
# 属性中有特殊字符的情形匹配abc@def属性等于v的元素
ele2 = ele1.ele('css:div[abc\@def="v"]')
```
## 📍 多属性匹配符`@@`
表示某个属性,多属性匹配时使用,个数不限。还能匹配要忽略的元素,匹配文本时也和`@`不一样。
`@@`后跟 - 时,表示 not。如
- `@@-name`表示匹配没有`name`属性的元素
- `@@-name=ele_name`表示匹配`name`属性不为`ele_name`的元素
如有以下情况,不能使用此方式,须改用 xpath 的方式:
- 匹配文本或属性中出现`@@`
- 属性名本身以`-`开头
!> **注意:**
如果属性中包含特殊字符,如包含`@`,用这个方式不能正确匹配到,须使用 css selector 方式查找。且特殊字符要用`\`转义。
```python
# 查找 name 属性为 name 且 class 属性包含 cls 文本的元素
ele2 = ele1.ele('@@name=name@@class:cls')
# 查找没有 class 属性的元素
ele2 = ele1.ele('@@-class')
# 查找 name 属性不包含 ele_name 的元素
ele2 = ele1.ele('@@-name:ele_name')
```
## 📍 文本匹配符`text`
要匹配的文本,查询字符串如开头没有任何关键字,也表示根据传入的文本作模糊查找。
如果元素内有多个直接的文本节点,精确查找时可匹配所有文本节点拼成的字符串,模糊查找时可匹配每个文本节点。
```python
# 查找文本为 some text 的元素
ele2 = ele1.ele('text=some text')
# 查找文本包含 some text 的元素
ele2 = ele1.ele('text:some text')
# 与上一行一致
ele2 = ele1.ele('some text')
```
?> **Tips** <br>若要查找的文本包含`text:` ,可下面这样写,即第一个`text:` 为关键字,第二个是要查找的内容:
```python
ele2 = page.ele('text:text:')
```
## 📍 文本匹配符`text()`
作为查找属性时使用的文本关键字,必须与`@``@@`配合使用。
`@`配合和与`@@`配合使用时,`text()`的意义是有差别的。
`@text()`表示在元素的直接子文本节点中匹配,且多个节点不能合并匹配。 `@@text()`表示在元素内部所有文本中匹配,且会把元素内部所有文本拼成一个总字符串再进行匹配,因此可以模糊匹配元素里面任意文本。
```python
# 查找文本为 some text 的元素
ele2 = ele1.ele('@text()=some text')
# 查找文本包含 some text 的元素
ele2 = ele1.ele('@text():some text')
# 查找文本为 some text 且 class 属性为 cls 的元素
ele2 = ele1.ele('@@text()=some text@@class=cls')
# 查找文本为 some text 且没有任何属性的元素(因第一个 @@ 后为空)
ele2 = ele1.ele('@@@@text():some text')
# 查找直接子文本包含 some text 字符串的元素,和 text 关键字没有区别
ele = page.ele('@text():some text')
ele = page.ele('text:some text')
# 查找元素内部包含 some text 字符串的元素
ele = page.ele('@@text():some text')
```
## 📍 类型匹配符`tag`
表示元素的标签,只在语句最前面且单独使用时生效,可与`@``@@`配合使用。`tag:``tag=`效果一致。
```python
# 定位 div 元素
ele2 = ele1.ele('tag:div')
# 定位 class 属性为 cls 的 div 元素
ele2 = ele1.ele('tag:div@class=cls')
# 定位文本为 text 的 div 元素
ele2 = ele1.ele('tag:div@text()=text')
# 定位 class 属性为 cls 且文本为 text 的 div 元素
ele2 = ele1.ele('tag:div@@class=cls@@text()=text')
# 查找直接文本节点包含 text 字符串的 div 元素
ele2 = ele1.ele('tag:div@text():text')
# 查找内部文本节点包含 text 字符串的 div 元素
ele2 = ele1.ele('tag:div@@text():text')
```
?> **Tips:** <br>
注意, `tag:div@text():text``tag:div@@text():text` 是有区别的,前者只在`div`的直接文本节点搜索,后者搜索`div`的整个内部。
## 📍 css selector 匹配符`css`
表示用 css selector 方式查找元素。`css:``css=`效果一致。
```python
# 查找 div 元素
ele2 = ele1.ele('css:.div')
# 查找 div 子元素元素,这个写法是本库特有,原生不支持
ele2 = ele1.ele('css:>div')
```
## 📍 xpath 匹配符`xpath`
表示用 xpath 方式查找元素。`xpath:``xpath=`效果一致。
该方法支持完整的 xpath 语法,能使用 xpath 直接获取元素属性selenium 不支持这种用法。
```python
# 查找 div 元素
ele2 = ele1.ele('xpath:.//div')
# 和上面一行一样,查找元素的后代时,// 前面的 . 可以省略
ele2 = ele1.ele('xpath://div')
# 获取 div 元素的 class 属性,返回字符串
txt = ele1.ele('xpath://div/@class')
```
?> **Tips:** <br>
查找元素的后代时selenium 原生代码要求 xpath 前面必须加`.`,否则会变成在全个页面中查找。笔者觉得这个设计是画蛇添足,既然已经通过元素查找了,自然应该只查找这个元素内部的元素。所以,用 xpath
在元素下查找时,最前面`//``/`前面的`.`可以省略。
## 📍 selenium 的 loc 元组
查找方法能直接接收 selenium 原生定位元组进行查找s 模式下也支持这种写法。
```python
from selenium.webdriver.common.by import By
# 查找 id 为 ele_id 的元素
loc1 = (By.ID, 'ele_id')
ele = page.ele(loc1)
# 按 xpath 查找
loc2 = (By.XPATH, '//div[@class="ele_class"]'
ele = page.ele(loc2)
```
# ✔️ 等待
d 模式下所有查找元素操作都自带等待,默认为跟随元素所在页面`timeout`属性(默认 10 秒),也可以在每次查找时单独设置,单独设置的等待时间不会改变页面原来设置。
```python
# 页面初始化时设置查找元素超时时间为 15 秒
page = MixPage(timeout=15)
# 设置查找元素超时时间为 5 秒
page.timeout = 5
# 使用页面超时时间来查找元素5 秒)
ele1 = page.ele('some text')
# 为这次查找页面独立设置等待时间1 秒)
ele1 = page.ele('some text', timeout=1)
# 查找后代元素使用页面超时时间5 秒)
ele2 = ele1.ele('some text')
# 查找后代元素使用单独设置的超时时间1 秒)
ele2 = ele1.ele('some text', timeout=1)
```
# ✔️ 相对定位
以下方法可以以某元素为基准,在 DOM 中按照条件获取其兄弟元素、祖先元素、文档前后元素。
除获取元素外,还能通过 xpath 获取任意节点内容,如文本节点、注释节点。这在处理元素和文本节点混排的时候非常有用。
## parent()
此方法获取当前元素某一级父元素,可指定筛选条件或层数。
参数:
- level_or_loc第几级父元素或定位符
返回:
```python
# 获取 ele1 的第二层父元素
ele2 = ele1.parent(2)
# 获取 ele1 父元素中 id 为 id1 的元素
ele2 = ele1.parent('#id1')
```
## next()
此方法返回当前元素后面的某一个兄弟元素,可指定筛选条件和第几个。
参数:
- index查询结果中的第几个
- filter_loc用于筛选元素的查询语法
- timeout查找元素的超时时间
返回:本元素后面某个兄弟元素或节点文本
```python
# 获取 ele1 后面第一个兄弟元素
ele2 = ele1.next()
# 获取 ele1 后面第 3 个兄弟元素
ele2 = ele1.next(3)
# 获取 ele1 后面第 3 个 div 兄弟元素
ele2 = ele1.next(3, 'tag:div')
# 获取 ele1 后面第一个文本节点的文本
txt = ele1.next(1, 'xpath:text()')
```
## nexts()
此方法返回后面全部符合条件的兄弟元素或节点组成的列表,可用查询语法筛选。
参数:
- filter_loc用于筛选元素的查询语法
- timeout查找元素的超时时间
返回:本元素前面符合条件的兄弟元素或节点文本组成的列表
```python
# 获取 ele1 后面所有兄弟元素
eles = ele1.nexts()
# 获取 ele1 后面所有 div 兄弟元素
divs = ele1.nexts('tag:div')
# 获取 ele1 后面的所有文本节点
txts = ele1.nexts('xpath:text()')
```
## prev()
此方法返回当前元素前面的某一个兄弟元素,可指定筛选条件和第几个。
参数:
- index查询结果中的第几个
- filter_loc用于筛选元素的查询语法
- timeout查找元素的超时时间
返回:本元素前面某个兄弟元素或节点文本
```python
# 获取 ele1 前面第一个兄弟元素
ele2 = ele1.prev()
# 获取 ele1 前面第 3 个兄弟元素
ele2 = ele1.prev(3)
# 获取 ele1 前面第 3 个 div 兄弟元素
ele2 = ele1.prev(3, 'tag:div')
# 获取 ele1 前面第一个文本节点的文本
txt = ele1.prev(1, 'xpath:text()')
```
## prevs()
此方法返回前面全部符合条件的兄弟元素或节点组成的列表,可用查询语法筛选。
参数:
- filter_loc用于筛选元素的查询语法
- timeout查找元素的超时时间
返回:本元素前面符合条件的兄弟元素或节点文本组成的列表
```python
# 获取 ele1 前面所有兄弟元素
eles = ele1.prevs()
# 获取 ele1 前面所有 div 兄弟元素
divs = ele1.prevs('tag:div')
```
## after()
此方法返回当前元素后面的某一个元素,可指定筛选条件和第几个。这个方法查找范围不局限在兄弟元素间,而是整个 DOM 文档。
参数:
- index查询结果中的第几个
- filter_loc用于筛选元素的查询语法
- timeout查找元素的超时时间
返回:本元素后面某个元素或节点
```python
# 获取 ele1 后面第 3 个元素
ele2 = ele1.after(3)
# 获取 ele1 后面第 3 个 div 元素
ele2 = ele1.after(3, 'tag:div')
# 获取 ele1 后面第一个文本节点的文本
txt = ele1.after(1, 'xpath:text()')
```
## afters()
此方法返回后面符合条件的全部元素或节点组成的列表,可用查询语法筛选。这个方法查找范围不局限在兄弟元素间,而是整个 DOM 文档。
参数:
- filter_loc用于筛选元素的查询语法
- timeout查找元素的超时时间
返回:本元素后面符合条件的元素或节点组成的列表
```python
# 获取 ele1 后所有元素
eles = ele1.afters()
# 获取 ele1 前面所有 div 元素
divs = ele1.afters('tag:div')
```
## before()
此方法返回当前元素前面的某一个元素,可指定筛选条件和第几个。这个方法查找范围不局限在兄弟元素间,而是整个 DOM 文档。
参数:
- index查询结果中的第几个
- filter_loc用于筛选元素的查询语法
- timeout查找元素的超时时间
返回:本元素前面某个元素或节点
```python
# 获取 ele1 前面第 3 个元素
ele2 = ele1.before(3)
# 获取 ele1 前面第 3 个 div 元素
ele2 = ele1.before(3, 'tag:div')
# 获取 ele1 前面第一个文本节点的文本
txt = ele1.before(1, 'xpath:text()')
```
## befores()
此方法返回前面全部符合条件的元素或节点组成的列表,可用查询语法筛选。这个方法查找范围不局限在兄弟元素间,而是整个 DOM 文档。
参数:
- filter_loc用于筛选元素的查询语法
- timeout查找元素的超时时间
返回:本元素前面符合条件的元素或节点组成的列表
```python
# 获取 ele1 前面所有元素
eles = ele1.befores()
# 获取 ele1 前面所有 div 元素
divs = ele1.befores('tag:div')
```
# ✔️ `ShadowRootElement`相关查找
本库把 shadow-root 也作为元素对象看待,是为`ShadowRootElement`对象。对`ShadowRootElement`对象可与普通元素一样查找下级元素和 DOM 内相对定位,但不能用页面布局相对定位。
`ShadowRootElement`对象进行相对定位时,把它看作其父对象内部的第一个对象,其余定位逻辑与普通对象一致。
!> **注意:** <br>
如果`ShadowRootElement`元素的下级元素中有其它`ShadowRootElement`元素,那这些下级`ShadowRootElement`
元素内部是无法直接通过定位语句查找到的,只能先定位到其父元素,再用`shadow-root`属性获取。
```python
# 获取一个 shadow-root 元素
sr_ele = page.ele('#app').shadow_root
# 在该元素下查找下级元素
ele1 = sr_ele.ele('tag:div')
# 用相对定位获取其它元素
ele1 = sr_ele.parent(2)
ele1 = sr_ele.next(1, 'tag:div')
ele1 = sr_ele.after(1, 'tag:div')
eles = sr_ele.nexts('tag:div')
# 定位下级元素中的 shadow+-root 元素
sr_ele2 = sr_ele.ele('tag:div').shadow_root
```
# ✔️ 简化写法
为进一步精简代码,对语法进行了精简
- 定位语法都有其简化形式。
- 页面和元素对象都实现了`__call__()`方法,可直接调用。
- 所有查找方法都支持链式操作
示例:
```python
# 定位到页面中 id 为 table_id 的元素,然后获取它的所有 tr 元素
eles = page('#table_id').eles('t:tr')
# 定位到 class 为 cls 的元素,然后在它里面查找文本为 text 的元素
ele2 = ele1('.cls1')('tx=text')
# 获取 ele1 的 shadow_root 元素,再在里面查找 class 属性为 cls 的元素
ele2 = ele1.sr('.cls')
# 按xpath 查找元素
ele2 = ele1('x://div[@class="ele_class"]')
```
简化写法对应列表
| 原写法 | 简化写法 |
|:-----------:|:----:|
| text | tx |
| text() | tx() |
| tag | t |
| xpath | x |
| css | c |
| shadow_root | sr |
# Tips
- 从一个`DriverElement`元素获取到的`SessionElement`版本,依然能够使用相对定位方法定位祖先或兄弟元素。
- `SessionElement``SessionPage``ele()``eles()`方法也有`timeout`参数,但它是不生效的,仅用于保持与 d 模式元素书写一致,便于无差别的调用。
- 定位语句内容与关键字重复时,请使用 xpath 或 css selector 代替。

View File

@ -1,48 +1,54 @@
* [⭐️ 简介](README.md)
* [⭐️ 1 简介](README.md)
* [🧭 入门指南](#)
* [🔥 基本概念](入门指南\基本概念.md)
* [👍 快速上手](入门指南\快速上手.md)
* [🍀 特性演示](#)
* [🌿 与 requests 对比](入门指南\特性演示\与requests代码对比.md)
* [🌿 与 selenium 对比](入门指南\特性演示\与selenium代码对比.md)
* [🌿 模式切换](入门指南\特性演示\模式切换.md)
* [🌿 获取并打印元素属性](入门指南\特性演示\获取并打印元素属性.md)
* [🌿 下载文件](入门指南\特性演示\下载文件.md)
* [🧭 2 入门指南](#)
* [🛠 使用方法](#)
* [🔨 创建页面对象](使用方法\创建页面对象.md)
* [🔨 访问网页](使用方法\访问网页.md)
* [🔨 查找页面元素](使用方法\查找页面元素.md)
* [🔨 获取元素信息](使用方法\获取元素信息.md)
* [🔨 元素操作](使用方法\元素操作.md)
* [🔨 获取网页信息](使用方法\获取网页信息.md)
* [🔨 页面操作](使用方法\页面操作.md)
* [🔨 启动配置](#)
* [🔧 概述](使用方法\启动配置\概述.md)
* [🔧 Chrome 启动配置](使用方法\启动配置\Chrome启动配置.md)
* [🔧 Session 启动配置](使用方法\启动配置\Session启动配置.md)
* [🔧 使用配置文件](使用方法\启动配置\使用配置文件.md)
* [🔨 下载文件](使用方法\下载文件.md)
* [🔨 监听浏览器网络数据](使用方法\监听浏览器网络数据.md)
* [🔨 cookies 的使用](使用方法\cookies的使用.md)
* [🔨 Drission 对象](使用方法\Drission对象.md)
* [🔨 对接 selenium 及 requests 代码](使用方法\对接selenium及requests代码.md)
* [🔨 使用其它系统或浏览器](使用方法\使用其它系统或浏览器.md)
* [🔨 DriverPage 和 SessionPage](使用方法\DriverPage和SessionPage.md)
* [🔨 打包程序](使用方法\打包程序.md)
* [🔥 2.1 基本概念](入门指南\基本概念.md)
* [👍 2.2 快速上手](入门指南\快速上手.md)
* [🍀 2.3 特性演示](#)
* [🌿 与 requests 对比](入门指南\特性演示\与requests代码对比.md)
* [🌿 与 selenium 对比](入门指南\特性演示\与selenium代码对比.md)
* [🌿 模式切换](入门指南\特性演示\模式切换.md)
* [🌿 获取并打印元素属性](入门指南\特性演示\获取并打印元素属性.md)
* [🌿 下载文件](入门指南\特性演示\下载文件.md)
* [💖 实用示例](#)
* [🧡 自动登录码云](实用示例\自动登录码云.md)
* [🧡 获取各国疫情排名](实用示例\获取各国疫情排名.md)
* [🧡 下载星巴克产品图片](实用示例\下载星巴克产品图片.md)
* [🧡 同时操作多个浏览器](实用示例\同时操作多个浏览器.md)
* [🛠 3 WebPage 使用方法](#)
* [⚡️ Tips大集合](Tips大集合.md)
* [🔨 3.1 创建页面对象](WebPage使用方法\3.1创建页面对象.md)
* [🔨 3.2 访问网页](WebPage使用方法\3.2访问网页.md)
* [🔨 3.3 查找元素](WebPage使用方法\3.3查找元素.md)
* [🎯️ 版本历史](版本历史.md)
* [🛠 4 MixPage 使用方法](#)
* [🔨 4.1 创建页面对象](MixPage使用方法\创建页面对象.md)
* [🔨 4.2 访问网页](MixPage使用方法\访问网页.md)
* [🔨 4.3 查找页面元素](MixPage使用方法\查找页面元素.md)
* [🔨 4.4 获取元素信息](MixPage使用方法\获取元素信息.md)
* [🔨 4.5 元素操作](MixPage使用方法\元素操作.md)
* [🔨 4.6 获取网页信息](MixPage使用方法\获取网页信息.md)
* [🔨 4.7 页面操作](MixPage使用方法\页面操作.md)
* [🔨 4.8 启动配置](#)
* [🔧 概述](MixPage使用方法\启动配置\概述.md)
* [🔧 Chrome 启动配置](MixPage使用方法\启动配置\Chrome启动配置.md)
* [🔧 Session 启动配置](MixPage使用方法\启动配置\Session启动配置.md)
* [🔧 使用配置文件](MixPage使用方法\启动配置\使用配置文件.md)
* [🔨 4.9 下载文件](MixPage使用方法\下载文件.md)
* [🔨 4.10 监听浏览器网络数据](MixPage使用方法\监听浏览器网络数据.md)
* [🔨 4.11 cookies 的使用](MixPage使用方法\cookies的使用.md)
* [🔨 4.12 Drission 对象](MixPage使用方法\Drission对象.md)
* [🔨 4.13 对接 selenium 及 requests 代码](MixPage使用方法\对接selenium及requests代码.md)
* [🔨 4.14 使用其它系统或浏览器](MixPage使用方法\使用其它系统或浏览器.md)
* [🔨 4.15 DriverPage 和 SessionPage](MixPage使用方法\DriverPage和SessionPage.md)
* [🔨 4.16 打包程序](MixPage使用方法\打包程序.md)
* [💖 5 实用示例](#)
* [🧡 自动登录码云](实用示例\自动登录码云.md)
* [🧡 获取各国疫情排名](实用示例\获取各国疫情排名.md)
* [🧡 下载星巴克产品图片](实用示例\下载星巴克产品图片.md)
* [🧡 同时操作多个浏览器](实用示例\同时操作多个浏览器.md)
* [⚡️ 6 Tips大集合](Tips大集合.md)
* [🎯️ 7 版本历史](版本历史.md)
* [💐 鸣谢](鸣谢.md)

View File

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