Merge pull request !45 from g1879/dev
This commit is contained in:
g1879 2024-04-07 11:42:27 +00:00 committed by Gitee
commit 7455314639
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
53 changed files with 1202 additions and 749 deletions

View File

@ -1,3 +1,11 @@
1. 使用上的问题请先查看文档[使用文档](http://g1879.gitee.io/drissionpagedocs)
2. 遇到bug请详细描述如何重现并附上代码
3. 提问前先给本库打个星,谢谢
在提交issue前请确认已经给本库点了星星这对我来说很重要。
使用方法请查看[使用文档](http://drissionpage.cn),文档里都有。
也可在QQ群里提问636361957
请围绕以下内容陈述您的问题:
1. 遇到了什么问题?什么场景下出现的?如何重现?
2. 请附上代码和报错信息(如有)
3. DrissionPage、浏览器、python版本号是多少
4. 有什么意见建议?

View File

@ -14,4 +14,4 @@ from ._configs.chromium_options import ChromiumOptions
from ._configs.session_options import SessionOptions
__all__ = ['ChromiumPage', 'ChromiumOptions', 'SessionOptions', 'SessionPage', 'WebPage', '__version__']
__version__ = '4.0.4.7'
__version__ = '4.0.4.21'

View File

@ -5,6 +5,7 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from os import waitpid
from pathlib import Path
from shutil import rmtree
from time import perf_counter, sleep
@ -72,7 +73,9 @@ class Browser(object):
:param owner: 使用该驱动的对象
:return: Driver对象
"""
d = self._drivers.pop(tab_id, Driver(tab_id, 'page', self.address))
d = self._drivers.pop(tab_id, None)
if not d:
d = Driver(tab_id, 'page', self.address)
d.owner = owner
self._all_drivers.setdefault(tab_id, set()).add(d)
return d
@ -129,7 +132,7 @@ class Browser(object):
return len([i for i in j if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')])
@property
def tabs(self):
def tab_ids(self):
"""返回所有标签页id组成的列表"""
j = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp因为顺序不对
return [i['id'] for i in j if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')]
@ -139,13 +142,12 @@ class Browser(object):
"""返回浏览器进程id"""
return self._process_id
def find_tabs(self, title=None, url=None, tab_type=None, single=True):
"""查找符合条件的tab返回它们的id组成的列表
def find_tabs(self, title=None, url=None, tab_type=None):
"""查找符合条件的tab返回它们组成的列表
:param title: 要匹配title的文本
:param url: 要匹配url的文本
:param tab_type: tab类型可用列表输入多个
:param single: 是否返回首个结果的id为False返回所有信息
:return: tab id或tab列表
:return: dict格式的tab信息列表列表
"""
tabs = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp
@ -156,9 +158,8 @@ class Browser(object):
elif tab_type is not None:
raise TypeError('tab_type只能是set、list、tuple、str、None。')
r = [i for i in tabs if ((title is None or title in i['title']) and (url is None or url in i['url'])
and (tab_type is None or i['type'] in tab_type))]
return r[0]['id'] if r and single else r
return [i for i in tabs if ((title is None or title in i['title']) and (url is None or url in i['url'])
and (tab_type is None or i['type'] in tab_type))]
def close_tab(self, tab_id):
"""关闭标签页
@ -190,6 +191,30 @@ class Browser(object):
"""
return self.run_cdp('Browser.getWindowForTarget', targetId=tab_id or self.id)['bounds']
def new_tab(self, new_window=False, background=False, new_context=False):
"""新建一个标签页
:param new_window: 是否在新窗口打开标签页
:param background: 是否不激活新标签页如new_window为True则无效
:param new_context: 是否创建新的上下文
:return: 新标签页id
"""
bid = None
if new_context:
bid = self.run_cdp('Target.createBrowserContext')['browserContextId']
kwargs = {'url': ''}
if new_window:
kwargs['newWindow'] = True
if background:
kwargs['background'] = True
if bid:
kwargs['browserContextId'] = bid
tid = self.run_cdp('Target.createTarget', **kwargs)['targetId']
while tid not in self._drivers:
sleep(.1)
return tid
def reconnect(self):
"""断开重连"""
self._driver.stop()
@ -205,21 +230,31 @@ class Browser(object):
:param force: 是否立刻强制终止进程
:return: None
"""
pids = [pid['id'] for pid in self.run_cdp('SystemInfo.getProcessInfo')['processInfo']]
for tab in self._all_drivers.values():
try:
self.run_cdp('Browser.close')
except PageDisconnectedError:
pass
self.driver.stop()
drivers = list(self._all_drivers.values())
for tab in drivers:
for driver in tab:
driver.stop()
if force:
from psutil import Process
for pid in pids:
Process(pid).kill()
else:
if not force:
return
try:
pids = [pid['id'] for pid in self.run_cdp('SystemInfo.getProcessInfo')['processInfo']]
except:
return
from psutil import Process
for pid in pids:
try:
self.run_cdp('Browser.close')
self.driver.stop()
except PageDisconnectedError:
self.driver.stop()
Process(pid).kill()
except:
pass
from os import popen
from platform import system
@ -239,6 +274,10 @@ class Browser(object):
if ok:
break
sleep(.05)
if self.process_id:
waitpid(self.process_id, 0)
def _on_disconnect(self):
self.page._on_disconnect()
@ -254,3 +293,4 @@ class Browser(object):
break
except (PermissionError, FileNotFoundError, OSError):
pass
sleep(.05)

View File

@ -40,13 +40,13 @@ class Browser(object):
def tabs_count(self) -> int: ...
@property
def tabs(self) -> List[str]: ...
def tab_ids(self) -> List[str]: ...
@property
def process_id(self) -> Optional[int]: ...
def find_tabs(self, title: str = None, url: str = None,
tab_type: Union[str, list, tuple] = None, single: bool = True) -> Union[str, List[str]]: ...
tab_type: Union[str, list, tuple] = None) -> List[dict]: ...
def close_tab(self, tab_id: str) -> None: ...
@ -56,6 +56,8 @@ class Browser(object):
def get_window_bounds(self, tab_id: str = None) -> dict: ...
def new_tab(self, new_window: bool = False, background: bool = False, new_context: bool = False) -> str: ...
def reconnect(self) -> None: ...
def connect_to_page(self) -> None: ...

View File

@ -30,7 +30,7 @@ class Driver(object):
self.address = address
self.type = tab_type
self.owner = owner
self._debug = False
# self._debug = False
self.alert_flag = False # 标记alert出现跳过一条请求后复原
self._websocket_url = f'ws://{address}/devtools/{tab_type}/{tab_id}'
@ -180,7 +180,6 @@ class Driver(object):
def run(self, _method, **kwargs):
"""执行cdp方法
:param _method: cdp方法名
:param args: cdp参数
:param kwargs: cdp参数
:return: 执行结果
"""
@ -202,7 +201,13 @@ class Driver(object):
try:
self._ws = create_connection(self._websocket_url, enable_multithread=True, suppress_origin=True)
except WebSocketBadStatusException as e:
raise TargetNotFoundError(f'找不到页面:{self.id}') if 'No such target id' in str(e) else e
txt = str(e)
if 'No such target id' in txt:
raise TargetNotFoundError(f'找不到页面:{self.id}')
elif 'Handshake status 403 Forbidden' in txt:
raise RuntimeError('请升级websocket-client库。')
else:
raise e
self._recv_th.start()
self._handle_event_th.start()
return True
@ -224,14 +229,15 @@ class Driver(object):
self._ws.close()
self._ws = None
try:
while not self.event_queue.empty():
event = self.event_queue.get_nowait()
function = self.event_handlers.get(event['method'])
if function:
function(**event['params'])
except:
pass
# try:
# while not self.event_queue.empty():
# event = self.event_queue.get_nowait()
# function = self.event_handlers.get(event['method'])
# if function:
# function(**event['params'])
# sleep(.1)
# except:
# pass
self.event_handlers.clear()
self.method_results.clear()

View File

@ -5,13 +5,14 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from copy import copy
from pathlib import Path
from requests import Session
from requests.structures import CaseInsensitiveDict
from .options_manage import OptionsManager
from .._functions.web import cookies_to_tuple, set_session_cookies
from .._functions.web import cookies_to_tuple, set_session_cookies, format_headers
class SessionOptions(object):
@ -171,6 +172,7 @@ class SessionOptions(object):
self._headers = None
self._del_set.add('headers')
else:
headers = format_headers(headers)
self._headers = {key.lower(): headers[key] for key in headers}
return self
@ -211,8 +213,8 @@ class SessionOptions(object):
return self._cookies
def set_cookies(self, cookies):
"""设置cookies信息
:param cookies: cookies可为CookieJar, list, tuple, str, dict传入None可在ini文件标记删除
"""设置一个或多个cookies信息
:param cookies: cookies可为Cookie, CookieJar, list, tuple, str, dict传入None可在ini文件标记删除
:return: 返回当前对象
"""
cookies = cookies if cookies is None else list(cookies_to_tuple(cookies))
@ -440,7 +442,7 @@ class SessionOptions(object):
:param headers: headers
:return: 当前对象
"""
self._headers = CaseInsensitiveDict(**session.headers, **headers) if headers else session.headers
self._headers = CaseInsensitiveDict(copy(session.headers).update(headers)) if headers else session.headers
self._cookies = session.cookies
self._auth = session.auth
self._proxies = session.proxies

View File

@ -5,13 +5,13 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from http.cookiejar import CookieJar, Cookie
from pathlib import Path
from typing import Any, Union, Tuple, Optional
from requests import Session
from requests.adapters import HTTPAdapter
from requests.auth import HTTPBasicAuth
from requests.cookies import RequestsCookieJar
from requests.structures import CaseInsensitiveDict
@ -49,7 +49,7 @@ class SessionOptions(object):
@property
def headers(self) -> dict: ...
def set_headers(self, headers: Union[dict, None]) -> SessionOptions: ...
def set_headers(self, headers: Union[dict, str, None]) -> SessionOptions: ...
def set_a_header(self, name: str, value: str) -> SessionOptions: ...
@ -60,7 +60,7 @@ class SessionOptions(object):
@property
def cookies(self) -> list: ...
def set_cookies(self, cookies: Union[RequestsCookieJar, list, tuple, str, dict, None]) -> SessionOptions: ...
def set_cookies(self, cookies: Union[Cookie, CookieJar, list, tuple, str, dict, None]) -> SessionOptions: ...
@property
def auth(self) -> Union[Tuple[str, str], HTTPBasicAuth]: ...

View File

@ -6,12 +6,12 @@
@License : BSD 3-Clause.
"""
from json import loads
from os.path import basename, sep
from os.path import basename
from pathlib import Path
from re import search
from time import perf_counter, sleep
from DataRecorder.tools import get_usable_path
from DataRecorder.tools import get_usable_path, make_valid_name
from .none_element import NoneElement
from .session_element import make_session_ele
@ -93,6 +93,14 @@ class ChromiumElement(DrissionElement):
def __eq__(self, other):
return self._backend_id == getattr(other, '_backend_id', None)
def __getattr__(self, item):
"""获取元素属性
:param item: 属性名
:return: 属性值
"""
a = self.attr(item)
return a if a is not None else self.property(item)
@property
def tag(self):
"""返回元素tag"""
@ -117,6 +125,10 @@ class ChromiumElement(DrissionElement):
try:
attrs = self.owner.run_cdp('DOM.getAttributes', nodeId=self._node_id)['attributes']
return {attrs[i]: attrs[i + 1] for i in range(0, len(attrs), 2)}
except ElementLostError:
self._refresh_id()
attrs = self.owner.run_cdp('DOM.getAttributes', nodeId=self._node_id)['attributes']
return {attrs[i]: attrs[i + 1] for i in range(0, len(attrs), 2)}
except CDPError: # 文档根元素不能调用此方法
return {}
@ -447,10 +459,7 @@ class ChromiumElement(DrissionElement):
:param index: 获取第几个从1开始可传入负数获取倒数第几个
:return: SessionElement对象或属性文本
"""
if self.tag in __FRAME_ELEMENT__:
r = make_session_ele(self.inner_html, locator, index=index)
else:
r = make_session_ele(self, locator, index=index)
r = make_session_ele(self, locator, index=index)
if isinstance(r, NoneElement):
if Settings.raise_when_ele_not_found:
raise ElementNotFoundError(None, 's_ele()', {'locator': locator})
@ -464,8 +473,6 @@ class ChromiumElement(DrissionElement):
:param locator: 定位符
:return: SessionElement或属性文本组成的列表
"""
if self.tag in __FRAME_ELEMENT__:
return make_session_ele(self.inner_html, locator, index=None)
return make_session_ele(self, locator, index=None)
def _find_elements(self, locator, timeout=None, index=1, relative=False, raise_err=None):
@ -505,24 +512,27 @@ class ChromiumElement(DrissionElement):
sleep(.1)
src = self.attr('src')
if not src:
raise RuntimeError('元素没有src值或该值为空。')
if src.lower().startswith('data:image'):
if base64_to_bytes:
from base64 import b64decode
return b64decode(src.split(',', 1)[-1])
else:
return src.split(',', 1)[-1]
is_blob = src.startswith('blob')
result = None
end_time = perf_counter() + timeout
while perf_counter() < end_time:
if is_blob:
if is_blob:
while perf_counter() < end_time:
result = get_blob(self.owner, src, base64_to_bytes)
if result:
break
sleep(.05)
else:
else:
while perf_counter() < end_time:
src = self.property('currentSrc')
if not src:
continue
@ -534,7 +544,8 @@ class ChromiumElement(DrissionElement):
result = self.owner.run_cdp('Page.getResourceContent', frameId=frame, url=src)
break
except CDPError:
sleep(.1)
pass
sleep(.1)
if not result:
return None
@ -548,11 +559,12 @@ class ChromiumElement(DrissionElement):
else:
return result['content']
def save(self, path=None, name=None, timeout=None):
def save(self, path=None, name=None, timeout=None, rename=True):
"""保存图片或其它有src属性的元素的资源
:param path: 文件保存路径为None时保存到当前文件夹
:param name: 文件名称为None时从资源url获取
:param timeout: 等待资源加载的超时时间
:param rename: 遇到重名文件时是否自动重命名
:return: 返回保存路径
"""
data = self.src(timeout=timeout)
@ -565,8 +577,13 @@ class ChromiumElement(DrissionElement):
if src.lower().startswith('data:image'):
r = search(r'data:image/(.*?);base64,', src)
name = f'img.{r.group(1)}' if r else None
name = name or basename(self.property('currentSrc'))
path = get_usable_path(f'{path}{sep}{name}').absolute()
path = Path(path) / make_valid_name(name or basename(self.property('currentSrc')))
if not path.suffix:
path = path.with_suffix('.jpg')
if rename:
path = get_usable_path(path)
path.parent.mkdir(parents=True, exist_ok=True)
path = path.absolute()
write_type = 'wb' if isinstance(data, bytes) else 'w'
with open(path, write_type) as f:
@ -602,7 +619,7 @@ class ChromiumElement(DrissionElement):
return self.owner._get_screenshot(path, name, as_bytes=as_bytes, as_base64=as_base64, full_page=False,
left_top=left_top, right_bottom=right_bottom, ele=self)
def input(self, vals, clear=True, by_js=False):
def input(self, vals, clear=False, by_js=False):
"""输入文本或组合键也可用于输入文件路径到input元素路径间用\n间隔)
:param vals: 文本值或按键组合
:param clear: 输入前是否清空文本框
@ -723,10 +740,15 @@ class ChromiumElement(DrissionElement):
self._tag = n['localName']
return n['backendNodeId']
def _refresh_id(self):
"""根据backend id刷新其它id"""
self._obj_id = self._get_obj_id(backend_id=self._backend_id)
self._node_id = self._get_node_id(obj_id=self._obj_id)
def _get_ele_path(self, mode):
"""返获取绝对的css路径或xpath路径"""
if mode == 'xpath':
txt1 = 'var tag = el.nodeName.toLowerCase();'
txt1 = 'let tag = el.nodeName.toLowerCase();'
txt3 = ''' && sib.nodeName.toLowerCase()==tag'''
txt4 = '''
if(nth>1){path = '/' + tag + '[' + nth + ']' + path;}
@ -745,10 +767,10 @@ class ChromiumElement(DrissionElement):
js = '''function(){
function e(el) {
if (!(el instanceof Element)) return;
var path = '';
let path = '';
while (el.nodeType === Node.ELEMENT_NODE) {
''' + txt1 + '''
var sib = el, nth = 0;
let sib = el, nth = 0;
while (sib) {
if(sib.nodeType === Node.ELEMENT_NODE''' + txt3 + '''){nth += 1;}
sib = sib.previousSibling;
@ -1079,7 +1101,7 @@ class ShadowRoot(BaseElement):
return None if r is False else r
else:
eles = make_session_ele(self.html).eles(loc)
eles = make_session_ele(self, loc, index=None)
if not eles:
return None
@ -1093,8 +1115,8 @@ class ShadowRoot(BaseElement):
r = make_chromium_eles(self.owner, _ids=node_id, is_obj_id=False)
return None if r is False else r
else:
node_ids = [self.owner.run_cdp('DOM.querySelector', nodeId=self._node_id, selector=i)['nodeId']
for i in css]
node_ids = [self.owner.run_cdp('DOM.querySelector',
nodeId=self._node_id, selector=i)['nodeId'] for i in css]
if 0 in node_ids:
return None
r = make_chromium_eles(self.owner, _ids=node_ids, index=index, is_obj_id=False)
@ -1376,8 +1398,8 @@ else{return e.singleNodeValue;}'''
# 按顺序获取所有元素、节点或属性
elif type_txt == '7':
for_txt = """
var a=new Array();
for(var i = 0; i <e.snapshotLength ; i++){
let a=new Array();
for(let i = 0; i <e.snapshotLength ; i++){
if(e.snapshotItem(i).constructor.name=="Text"){a.push(e.snapshotItem(i).data);}
else if(e.snapshotItem(i).constructor.name=="Attr"){a.push(e.snapshotItem(i).nodeValue);}
else if(e.snapshotItem(i).constructor.name=="Comment"){a.push(e.snapshotItem(i).nodeValue);}
@ -1392,7 +1414,7 @@ else{a.push(e.snapshotItem(i));}}"""
return_txt = 'return e.singleNodeValue;'
xpath = xpath.replace(r"'", r"\'")
js = f'function(){{var e=document.evaluate(\'{xpath}\',{node_txt},null,{type_txt},null);\n{for_txt}\n{return_txt}}}'
js = f'function(){{let e=document.evaluate(\'{xpath}\',{node_txt},null,{type_txt},null);\n{for_txt}\n{return_txt}}}'
return js
@ -1418,6 +1440,7 @@ def run_js(page_or_ele, script, as_expr, timeout, args=None):
obj_id = page_or_ele._root_id
if obj_id is not None:
break
sleep(.01)
else:
raise RuntimeError('js运行环境出错。')
@ -1518,7 +1541,7 @@ def convert_argument(arg):
if isinstance(arg, ChromiumElement):
return {'objectId': arg._obj_id}
elif isinstance(arg, (int, float, str, bool)):
elif isinstance(arg, (int, float, str, bool, dict)):
return {'value': arg}
from math import inf

View File

@ -56,6 +56,8 @@ class ChromiumElement(DrissionElement):
def __eq__(self, other: ChromiumElement) -> bool: ...
def __getattr__(self, item: str) -> str: ...
@property
def tag(self) -> str: ...
@ -206,7 +208,11 @@ class ChromiumElement(DrissionElement):
def src(self, timeout: float = None, base64_to_bytes: bool = True) -> Union[bytes, str, None]: ...
def save(self, path: [str, bool] = None, name: str = None, timeout: float = None) -> str: ...
def save(self,
path: [str, bool] = None,
name: str = None,
timeout: float = None,
rename: bool = True) -> str: ...
def get_screenshot(self,
path: [str, Path] = None,
@ -215,7 +221,7 @@ class ChromiumElement(DrissionElement):
as_base64: PIC_TYPE = None,
scroll_to_center: bool = True) -> Union[str, bytes]: ...
def input(self, vals: Any, clear: bool = True, by_js: bool = False) -> None: ...
def input(self, vals: Any, clear: bool = False, by_js: bool = False) -> None: ...
def _set_file_input(self, files: Union[str, list, tuple]) -> None: ...
@ -237,6 +243,8 @@ class ChromiumElement(DrissionElement):
def _get_backend_id(self, node_id: int) -> int: ...
def _refresh_id(self) -> None: ...
def _get_ele_path(self, mode: str) -> str: ...

View File

@ -37,18 +37,26 @@ class SessionElement(DrissionElement):
attrs = [f"{k}='{v}'" for k, v in self.attrs.items()]
return f'<SessionElement {self.tag} {" ".join(attrs)}>'
def __call__(self, locator, timeout=None):
def __call__(self, locator, index=1, timeout=None):
"""在内部查找元素
ele2 = ele1('@id=ele_id')
:param locator: 元素的定位信息可以是loc元组或查询字符串
:param index: 第几个元素从1开始可传入负数获取倒数第几个
:param timeout: 不起实际作用
:return: SessionElement对象或属性文本
"""
return self.ele(locator)
return self.ele(locator, index=index)
def __eq__(self, other):
return self.xpath == getattr(other, 'xpath', None)
def __getattr__(self, item):
"""获取元素属性
:param item: 属性名
:return: 属性值
"""
return self.attr(item)
@property
def tag(self):
"""返回元素类型"""
@ -199,12 +207,12 @@ class SessionElement(DrissionElement):
# 若为链接为None、js或邮件直接返回
if not link or link.lower().startswith(('javascript:', 'mailto:')):
return link
else: # 其它情况直接返回绝对url
return make_absolute_link(link, self.owner.url)
return make_absolute_link(link, self.owner.url) if self.owner else link
elif name == 'src':
return make_absolute_link(self.inner_ele.get('src'), self.owner.url)
return make_absolute_link(self.inner_ele.get('src'),
self.owner.url) if self.owner else self.inner_ele.get('src')
elif name == 'text':
return self.text
@ -350,6 +358,10 @@ def make_session_ele(html_or_ele, loc=None, index=1):
html_or_ele = fromstring(html)
html_or_ele = html_or_ele.xpath(xpath)[0]
elif html_or_ele._type == 'ChromiumFrame':
page = html_or_ele
html_or_ele = fromstring(html_or_ele.inner_html)
# 各种页面对象
elif isinstance(html_or_ele, BasePage):
page = html_or_ele

View File

@ -35,6 +35,8 @@ class SessionElement(DrissionElement):
def __eq__(self, other: SessionElement) -> bool: ...
def __getattr__(self, item: str) -> str: ...
@property
def tag(self) -> str: ...

View File

@ -94,12 +94,12 @@ def get_launch_args(opt):
opt.set_user_data_path(path)
result.add(f'--user-data-dir={path}')
if headless is None and system().lower() == 'linux':
from os import popen
r = popen('systemctl list-units | grep graphical.target')
if 'graphical.target' not in r.read():
headless = True
result.add('--headless=new')
# if headless is None and system().lower() == 'linux': # 无界面Linux自动加入无头
# from os import popen
# r = popen('systemctl list-units | grep graphical.target')
# if 'graphical.target' not in r.read():
# headless = True
# result.add('--headless=new')
result = list(result)
opt._headless = headless
@ -176,8 +176,8 @@ def set_flags(opt):
states_dict = load(f)
except JSONDecodeError:
states_dict = {}
flags_list = [] if opt.clear_file_flags else states_dict.setdefault(
'browser', {}).setdefault('enabled_labs_experiments', [])
states_dict.setdefault('browser', {}).setdefault('enabled_labs_experiments', [])
flags_list = [] if opt.clear_file_flags else states_dict['browser']['enabled_labs_experiments']
flags_dict = {}
for i in flags_list:
f = str(i).split('@', 1)

View File

@ -10,6 +10,12 @@ from ..errors import AlertExistsError
class Keys:
"""特殊按键"""
CTRL_A = ('\ue009', 'a')
CTRL_C = ('\ue009', 'c')
CTRL_X = ('\ue009', 'x')
CTRL_V = ('\ue009', 'v')
CTRL_Z = ('\ue009', 'z')
CTRL_Y = ('\ue009', 'y')
NULL = '\ue000'
CANCEL = '\ue001' # ^break
@ -94,35 +100,108 @@ keyDefinitions = {
'7': {'keyCode': 55, 'key': '7', 'code': 'Digit7'},
'8': {'keyCode': 56, 'key': '8', 'code': 'Digit8'},
'9': {'keyCode': 57, 'key': '9', 'code': 'Digit9'},
'Power': {'key': 'Power', 'code': 'Power'},
'Eject': {'key': 'Eject', 'code': 'Eject'},
'\ue001': {'keyCode': 3, 'code': 'Abort', 'key': 'Cancel'},
'\ue002': {'keyCode': 6, 'code': 'Help', 'key': 'Help'},
'\ue003': {'keyCode': 8, 'code': 'Backspace', 'key': 'Backspace'},
'\ue004': {'keyCode': 9, 'code': 'Tab', 'key': 'Tab'},
'\ue005': {'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3},
'\ue006': {'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3},
'\ue007': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'\r': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'a': {'keyCode': 65, 'key': 'a', 'code': 'KeyA'},
'b': {'keyCode': 66, 'key': 'b', 'code': 'KeyB'},
'c': {'keyCode': 67, 'key': 'c', 'code': 'KeyC'},
'd': {'keyCode': 68, 'key': 'd', 'code': 'KeyD'},
'e': {'keyCode': 69, 'key': 'e', 'code': 'KeyE'},
'f': {'keyCode': 70, 'key': 'f', 'code': 'KeyF'},
'g': {'keyCode': 71, 'key': 'g', 'code': 'KeyG'},
'h': {'keyCode': 72, 'key': 'h', 'code': 'KeyH'},
'i': {'keyCode': 73, 'key': 'i', 'code': 'KeyI'},
'j': {'keyCode': 74, 'key': 'j', 'code': 'KeyJ'},
'k': {'keyCode': 75, 'key': 'k', 'code': 'KeyK'},
'l': {'keyCode': 76, 'key': 'l', 'code': 'KeyL'},
'm': {'keyCode': 77, 'key': 'm', 'code': 'KeyM'},
'n': {'keyCode': 78, 'key': 'n', 'code': 'KeyN'},
'o': {'keyCode': 79, 'key': 'o', 'code': 'KeyO'},
'p': {'keyCode': 80, 'key': 'p', 'code': 'KeyP'},
'q': {'keyCode': 81, 'key': 'q', 'code': 'KeyQ'},
'r': {'keyCode': 82, 'key': 'r', 'code': 'KeyR'},
's': {'keyCode': 83, 'key': 's', 'code': 'KeyS'},
't': {'keyCode': 84, 'key': 't', 'code': 'KeyT'},
'u': {'keyCode': 85, 'key': 'u', 'code': 'KeyU'},
'v': {'keyCode': 86, 'key': 'v', 'code': 'KeyV'},
'w': {'keyCode': 87, 'key': 'w', 'code': 'KeyW'},
'x': {'keyCode': 88, 'key': 'x', 'code': 'KeyX'},
'y': {'keyCode': 89, 'key': 'y', 'code': 'KeyY'},
'z': {'keyCode': 90, 'key': 'z', 'code': 'KeyZ'},
' ': {'keyCode': 32, 'key': ' ', 'code': 'Space'},
'*': {'keyCode': 106, 'key': '*', 'code': 'NumpadMultiply', 'location': 3},
'+': {'keyCode': 107, 'key': '+', 'code': 'NumpadAdd', 'location': 3},
'-': {'keyCode': 109, 'key': '-', 'code': 'NumpadSubtract', 'location': 3},
'/': {'keyCode': 111, 'key': '/', 'code': 'NumpadDivide', 'location': 3},
';': {'keyCode': 186, 'key': ';', 'code': 'Semicolon'},
'=': {'keyCode': 187, 'key': '=', 'code': 'Equal'},
',': {'keyCode': 188, 'key': ',', 'code': 'Comma'},
'.': {'keyCode': 190, 'key': '.', 'code': 'Period'},
'`': {'keyCode': 192, 'key': '`', 'code': 'Backquote'},
'[': {'keyCode': 219, 'key': '[', 'code': 'BracketLeft'},
'\\': {'keyCode': 220, 'key': '\\', 'code': 'Backslash'},
']': {'keyCode': 221, 'key': ']', 'code': 'BracketRight'},
'\'': {'keyCode': 222, 'key': '\'', 'code': 'Quote'},
')': {'keyCode': 48, 'key': ')', 'code': 'Digit0'},
'!': {'keyCode': 49, 'key': '!', 'code': 'Digit1'},
'@': {'keyCode': 50, 'key': '@', 'code': 'Digit2'},
'#': {'keyCode': 51, 'key': '#', 'code': 'Digit3'},
'$': {'keyCode': 52, 'key': '$', 'code': 'Digit4'},
'%': {'keyCode': 53, 'key': '%', 'code': 'Digit5'},
'^': {'keyCode': 54, 'key': '^', 'code': 'Digit6'},
'&': {'keyCode': 55, 'key': '&', 'code': 'Digit7'},
'(': {'keyCode': 57, 'key': '(', 'code': 'Digit9'},
'A': {'keyCode': 65, 'key': 'A', 'code': 'KeyA'},
'B': {'keyCode': 66, 'key': 'B', 'code': 'KeyB'},
'C': {'keyCode': 67, 'key': 'C', 'code': 'KeyC'},
'D': {'keyCode': 68, 'key': 'D', 'code': 'KeyD'},
'E': {'keyCode': 69, 'key': 'E', 'code': 'KeyE'},
'F': {'keyCode': 70, 'key': 'F', 'code': 'KeyF'},
'G': {'keyCode': 71, 'key': 'G', 'code': 'KeyG'},
'H': {'keyCode': 72, 'key': 'H', 'code': 'KeyH'},
'I': {'keyCode': 73, 'key': 'I', 'code': 'KeyI'},
'J': {'keyCode': 74, 'key': 'J', 'code': 'KeyJ'},
'K': {'keyCode': 75, 'key': 'K', 'code': 'KeyK'},
'L': {'keyCode': 76, 'key': 'L', 'code': 'KeyL'},
'M': {'keyCode': 77, 'key': 'M', 'code': 'KeyM'},
'N': {'keyCode': 78, 'key': 'N', 'code': 'KeyN'},
'O': {'keyCode': 79, 'key': 'O', 'code': 'KeyO'},
'P': {'keyCode': 80, 'key': 'P', 'code': 'KeyP'},
'Q': {'keyCode': 81, 'key': 'Q', 'code': 'KeyQ'},
'R': {'keyCode': 82, 'key': 'R', 'code': 'KeyR'},
'S': {'keyCode': 83, 'key': 'S', 'code': 'KeyS'},
'T': {'keyCode': 84, 'key': 'T', 'code': 'KeyT'},
'U': {'keyCode': 85, 'key': 'U', 'code': 'KeyU'},
'V': {'keyCode': 86, 'key': 'V', 'code': 'KeyV'},
'W': {'keyCode': 87, 'key': 'W', 'code': 'KeyW'},
'X': {'keyCode': 88, 'key': 'X', 'code': 'KeyX'},
'Y': {'keyCode': 89, 'key': 'Y', 'code': 'KeyY'},
'Z': {'keyCode': 90, 'key': 'Z', 'code': 'KeyZ'},
':': {'keyCode': 186, 'key': ':', 'code': 'Semicolon'},
'<': {'keyCode': 188, 'key': '<', 'code': 'Comma'},
'_': {'keyCode': 189, 'key': '_', 'code': 'Minus'},
'>': {'keyCode': 190, 'key': '>', 'code': 'Period'},
'?': {'keyCode': 191, 'key': '?', 'code': 'Slash'},
'~': {'keyCode': 192, 'key': '~', 'code': 'Backquote'},
'{': {'keyCode': 219, 'key': '{', 'code': 'BracketLeft'},
'|': {'keyCode': 220, 'key': '|', 'code': 'Backslash'},
# '\ue026': {'keyCode': 220, 'key': '|', 'code': 'Backslash'},
'}': {'keyCode': 221, 'key': '}', 'code': 'BracketRight'},
'"': {'keyCode': 222, 'key': '"', 'code': 'Quote'},
'\n': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'\ue007': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'\ue003': {'keyCode': 8, 'code': 'Backspace', 'key': 'Backspace'},
'\ue00d': {'keyCode': 32, 'code': 'Space', 'key': ' '},
# 'PageUp': {'keyCode': 33, 'shiftKeyCode': 105, 'key': 'PageUp', 'code': 'Numpad9', 'shiftKey': '9', 'location': 3},
'\ue00e': {'keyCode': 33, 'code': 'PageUp', 'key': 'PageUp'},
# 'PageDown': {'keyCode': 34, 'shiftKeyCode': 99, 'key': 'PageDown', 'code': 'Numpad3', 'shiftKey': '3', 'location': 3},
'\ue00f': {'keyCode': 34, 'code': 'PageDown', 'key': 'PageDown'},
'\ue008': {'keyCode': 16, 'code': 'ShiftLeft', 'key': 'Shift', 'location': 1},
# 'ShiftRight': {'keyCode': 16, 'code': 'ShiftRight', 'key': 'Shift', 'location': 2},
'\ue009': {'keyCode': 17, 'code': 'ControlLeft', 'key': 'Control', 'location': 1},
# 'ControlRight': {'keyCode': 17, 'code': 'ControlRight', 'key': 'Control', 'location': 2},
'\ue00a': {'keyCode': 18, 'code': 'AltLeft', 'key': 'Alt', 'location': 1},
# 'AltRight': {'keyCode': 18, 'code': 'AltRight', 'key': 'Alt', 'location': 2},
'\ue00b': {'keyCode': 19, 'code': 'Pause', 'key': 'Pause'},
'CapsLock': {'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock'},
'\ue00c': {'keyCode': 27, 'code': 'Escape', 'key': 'Escape'},
'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'},
'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'},
'\ue00d': {'keyCode': 32, 'code': 'Space', 'key': ' '},
# 'PageUp': {'keyCode': 33, 'shiftKeyCode': 105, 'key': 'PageUp', 'code': 'Numpad9', 'shiftKey': '9', 'location': 3},
'\ue00e': {'keyCode': 33, 'code': 'PageUp', 'key': 'PageUp'},
# 'PageDown': {'keyCode': 34, 'shiftKeyCode': 99, 'key': 'PageDown', 'code': 'Numpad3', 'shiftKey': '3', 'location': 3},
'\ue00f': {'keyCode': 34, 'code': 'PageDown', 'key': 'PageDown'},
'\ue010': {'keyCode': 35, 'code': 'End', 'key': 'End'},
# 'Numpad1': {'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3},
'\ue03d': {'keyCode': 91, 'key': 'Meta', 'code': 'MetaLeft'},
'\r': {'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': '\r'},
'\ue011': {'keyCode': 36, 'code': 'Home', 'key': 'Home'},
# 'Numpad7': {'keyCode': 36, 'shiftKeyCode': 103, 'key': 'Home', 'code': 'Numpad7', 'shiftKey': '7', 'location': 3},
'\ue012': {'keyCode': 37, 'code': 'ArrowLeft', 'key': 'ArrowLeft'},
@ -133,6 +212,19 @@ keyDefinitions = {
# 'Numpad6': {'keyCode': 39, 'shiftKeyCode': 102, 'key': 'ArrowRight', 'code': 'Numpad6', 'shiftKey': '6', 'location': 3},
# 'Numpad2': {'keyCode': 40, 'shiftKeyCode': 98, 'key': 'ArrowDown', 'code': 'Numpad2', 'shiftKey': '2', 'location': 3},
'\ue015': {'keyCode': 40, 'code': 'ArrowDown', 'key': 'ArrowDown'},
'\ue001': {'keyCode': 3, 'code': 'Abort', 'key': 'Cancel'},
'\ue002': {'keyCode': 6, 'code': 'Help', 'key': 'Help'},
'\ue004': {'keyCode': 9, 'code': 'Tab', 'key': 'Tab'},
'\ue005': {'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3},
'\ue006': {'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3},
'\ue00b': {'keyCode': 19, 'code': 'Pause', 'key': 'Pause'},
'CapsLock': {'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock'},
'\ue00c': {'keyCode': 27, 'code': 'Escape', 'key': 'Escape'},
'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'},
'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'},
'\ue010': {'keyCode': 35, 'code': 'End', 'key': 'End'},
# 'Numpad1': {'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3},
'Select': {'keyCode': 41, 'code': 'Select', 'key': 'Select'},
'Open': {'keyCode': 43, 'code': 'Open', 'key': 'Execute'},
'PrintScreen': {'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen'},
@ -238,102 +330,17 @@ keyDefinitions = {
'Alt': {'keyCode': 18, 'key': 'Alt', 'code': 'AltLeft'},
'Accept': {'keyCode': 30, 'key': 'Accept'},
'ModeChange': {'keyCode': 31, 'key': 'ModeChange'},
' ': {'keyCode': 32, 'key': ' ', 'code': 'Space'},
'Print': {'keyCode': 42, 'key': 'Print'},
'Execute': {'keyCode': 43, 'key': 'Execute', 'code': 'Open'},
'\u0000': {'keyCode': 46, 'key': '\u0000', 'code': 'NumpadDecimal', 'location': 3},
'a': {'keyCode': 65, 'key': 'a', 'code': 'KeyA'},
'b': {'keyCode': 66, 'key': 'b', 'code': 'KeyB'},
'c': {'keyCode': 67, 'key': 'c', 'code': 'KeyC'},
'd': {'keyCode': 68, 'key': 'd', 'code': 'KeyD'},
'e': {'keyCode': 69, 'key': 'e', 'code': 'KeyE'},
'f': {'keyCode': 70, 'key': 'f', 'code': 'KeyF'},
'g': {'keyCode': 71, 'key': 'g', 'code': 'KeyG'},
'h': {'keyCode': 72, 'key': 'h', 'code': 'KeyH'},
'i': {'keyCode': 73, 'key': 'i', 'code': 'KeyI'},
'j': {'keyCode': 74, 'key': 'j', 'code': 'KeyJ'},
'k': {'keyCode': 75, 'key': 'k', 'code': 'KeyK'},
'l': {'keyCode': 76, 'key': 'l', 'code': 'KeyL'},
'm': {'keyCode': 77, 'key': 'm', 'code': 'KeyM'},
'n': {'keyCode': 78, 'key': 'n', 'code': 'KeyN'},
'o': {'keyCode': 79, 'key': 'o', 'code': 'KeyO'},
'p': {'keyCode': 80, 'key': 'p', 'code': 'KeyP'},
'q': {'keyCode': 81, 'key': 'q', 'code': 'KeyQ'},
'r': {'keyCode': 82, 'key': 'r', 'code': 'KeyR'},
's': {'keyCode': 83, 'key': 's', 'code': 'KeyS'},
't': {'keyCode': 84, 'key': 't', 'code': 'KeyT'},
'u': {'keyCode': 85, 'key': 'u', 'code': 'KeyU'},
'v': {'keyCode': 86, 'key': 'v', 'code': 'KeyV'},
'w': {'keyCode': 87, 'key': 'w', 'code': 'KeyW'},
'x': {'keyCode': 88, 'key': 'x', 'code': 'KeyX'},
'y': {'keyCode': 89, 'key': 'y', 'code': 'KeyY'},
'z': {'keyCode': 90, 'key': 'z', 'code': 'KeyZ'},
'\ue03d': {'keyCode': 91, 'key': 'Meta', 'code': 'MetaLeft'},
'*': {'keyCode': 106, 'key': '*', 'code': 'NumpadMultiply', 'location': 3},
'+': {'keyCode': 107, 'key': '+', 'code': 'NumpadAdd', 'location': 3},
'-': {'keyCode': 109, 'key': '-', 'code': 'NumpadSubtract', 'location': 3},
'/': {'keyCode': 111, 'key': '/', 'code': 'NumpadDivide', 'location': 3},
';': {'keyCode': 186, 'key': ';', 'code': 'Semicolon'},
'=': {'keyCode': 187, 'key': '=', 'code': 'Equal'},
',': {'keyCode': 188, 'key': ',', 'code': 'Comma'},
'.': {'keyCode': 190, 'key': '.', 'code': 'Period'},
'`': {'keyCode': 192, 'key': '`', 'code': 'Backquote'},
'[': {'keyCode': 219, 'key': '[', 'code': 'BracketLeft'},
'\\': {'keyCode': 220, 'key': '\\', 'code': 'Backslash'},
']': {'keyCode': 221, 'key': ']', 'code': 'BracketRight'},
'\'': {'keyCode': 222, 'key': '\'', 'code': 'Quote'},
'Attn': {'keyCode': 246, 'key': 'Attn'},
'CrSel': {'keyCode': 247, 'key': 'CrSel', 'code': 'Props'},
'ExSel': {'keyCode': 248, 'key': 'ExSel'},
'EraseEof': {'keyCode': 249, 'key': 'EraseEof'},
'Play': {'keyCode': 250, 'key': 'Play'},
'ZoomOut': {'keyCode': 251, 'key': 'ZoomOut'},
')': {'keyCode': 48, 'key': ')', 'code': 'Digit0'},
'!': {'keyCode': 49, 'key': '!', 'code': 'Digit1'},
'@': {'keyCode': 50, 'key': '@', 'code': 'Digit2'},
'#': {'keyCode': 51, 'key': '#', 'code': 'Digit3'},
'$': {'keyCode': 52, 'key': '$', 'code': 'Digit4'},
'%': {'keyCode': 53, 'key': '%', 'code': 'Digit5'},
'^': {'keyCode': 54, 'key': '^', 'code': 'Digit6'},
'&': {'keyCode': 55, 'key': '&', 'code': 'Digit7'},
'(': {'keyCode': 57, 'key': '(', 'code': 'Digit9'},
'A': {'keyCode': 65, 'key': 'A', 'code': 'KeyA'},
'B': {'keyCode': 66, 'key': 'B', 'code': 'KeyB'},
'C': {'keyCode': 67, 'key': 'C', 'code': 'KeyC'},
'D': {'keyCode': 68, 'key': 'D', 'code': 'KeyD'},
'E': {'keyCode': 69, 'key': 'E', 'code': 'KeyE'},
'F': {'keyCode': 70, 'key': 'F', 'code': 'KeyF'},
'G': {'keyCode': 71, 'key': 'G', 'code': 'KeyG'},
'H': {'keyCode': 72, 'key': 'H', 'code': 'KeyH'},
'I': {'keyCode': 73, 'key': 'I', 'code': 'KeyI'},
'J': {'keyCode': 74, 'key': 'J', 'code': 'KeyJ'},
'K': {'keyCode': 75, 'key': 'K', 'code': 'KeyK'},
'L': {'keyCode': 76, 'key': 'L', 'code': 'KeyL'},
'M': {'keyCode': 77, 'key': 'M', 'code': 'KeyM'},
'N': {'keyCode': 78, 'key': 'N', 'code': 'KeyN'},
'O': {'keyCode': 79, 'key': 'O', 'code': 'KeyO'},
'P': {'keyCode': 80, 'key': 'P', 'code': 'KeyP'},
'Q': {'keyCode': 81, 'key': 'Q', 'code': 'KeyQ'},
'R': {'keyCode': 82, 'key': 'R', 'code': 'KeyR'},
'S': {'keyCode': 83, 'key': 'S', 'code': 'KeyS'},
'T': {'keyCode': 84, 'key': 'T', 'code': 'KeyT'},
'U': {'keyCode': 85, 'key': 'U', 'code': 'KeyU'},
'V': {'keyCode': 86, 'key': 'V', 'code': 'KeyV'},
'W': {'keyCode': 87, 'key': 'W', 'code': 'KeyW'},
'X': {'keyCode': 88, 'key': 'X', 'code': 'KeyX'},
'Y': {'keyCode': 89, 'key': 'Y', 'code': 'KeyY'},
'Z': {'keyCode': 90, 'key': 'Z', 'code': 'KeyZ'},
':': {'keyCode': 186, 'key': ':', 'code': 'Semicolon'},
'<': {'keyCode': 188, 'key': '<', 'code': 'Comma'},
'_': {'keyCode': 189, 'key': '_', 'code': 'Minus'},
'>': {'keyCode': 190, 'key': '>', 'code': 'Period'},
'?': {'keyCode': 191, 'key': '?', 'code': 'Slash'},
'~': {'keyCode': 192, 'key': '~', 'code': 'Backquote'},
'{': {'keyCode': 219, 'key': '{', 'code': 'BracketLeft'},
'|': {'keyCode': 220, 'key': '|', 'code': 'Backslash'},
# '\ue026': {'keyCode': 220, 'key': '|', 'code': 'Backslash'},
'}': {'keyCode': 221, 'key': '}', 'code': 'BracketRight'},
'"': {'keyCode': 222, 'key': '"', 'code': 'Quote'},
'Power': {'key': 'Power', 'code': 'Power'},
'Eject': {'key': 'Eject', 'code': 'Eject'},
}
modifierBit = {'\ue00a': 1,
'\ue009': 2,
@ -405,10 +412,7 @@ def keyDescriptionForString(_modifiers, keyString): # noqa: C901
def send_key(page, modifier, key):
"""发送一个字,在键盘中的字符触发按键,其它直接发送文本"""
if key not in keyDefinitions:
page.run_cdp('Input.insertText', text=key, _ignore=AlertExistsError)
else:
if key in keyDefinitions:
description = keyDescriptionForString(modifier, key)
text = description['text']
data = {'type': 'keyDown' if text else 'rawKeyDown',
@ -427,6 +431,9 @@ def send_key(page, modifier, key):
data['type'] = 'keyUp'
page.run_cdp('Input.dispatchKeyEvent', **data)
else:
page.run_cdp('Input.insertText', text=key, _ignore=AlertExistsError)
def input_text_or_keys(page, text_or_keys):
"""输入文本也可输入组合键组合键用tuple形式输入

View File

@ -12,6 +12,12 @@ from .._pages.chromium_base import ChromiumBase
class Keys:
"""特殊按键"""
CTRL_A: tuple
CTRL_C: tuple
CTRL_X: tuple
CTRL_V: tuple
CTRL_Z: tuple
CTRL_Y: tuple
NULL: str
CANCEL: str

View File

@ -13,3 +13,4 @@ class Settings(object):
raise_when_wait_failed = False
singleton_tab_obj = True
cdp_timeout = 30
auto_handle_alert = None

View File

@ -10,7 +10,7 @@ from platform import system
from shutil import rmtree
from tempfile import gettempdir, TemporaryDirectory
from threading import Lock
from time import perf_counter
from time import perf_counter, sleep
from .._configs.options_manage import OptionsManager
from ..errors import (ContextLostError, ElementLostError, CDPError, PageDisconnectedError, NoRectError,
@ -107,7 +107,7 @@ def show_or_hide_browser(page, hide=True):
except ImportError:
raise ImportError('请先安装pip install pypiwin32')
pid = page.process_id
pid = page._page.process_id
if not pid:
return None
hds = get_hwnds_from_pid(pid, page.title)
@ -177,6 +177,7 @@ def wait_until(function, kwargs=None, timeout=10):
value = function(**kwargs)
if value:
return value
sleep(.01)
raise TimeoutError
@ -197,7 +198,8 @@ def raise_error(result, ignore=None):
:return: None
"""
error = result['error']
if error in ('Cannot find context with specified id', 'Inspected target navigated or closed'):
if error in ('Cannot find context with specified id', 'Inspected target navigated or closed',
'No frame with given id found'):
r = ContextLostError()
elif error in ('Could not find node with given id', 'Could not find object with given id',
'No node with given id found', 'Node with given id does not belong to the document',
@ -221,9 +223,8 @@ def raise_error(result, ignore=None):
r = RuntimeError(f'你的浏览器可能太旧。\n方法:{result["method"]}\n参数:{result["args"]}')
elif result['type'] in ('call_method_error', 'timeout'):
from DrissionPage import __version__
from time import process_time
txt = f'\n错误:{result["error"]}\n方法:{result["method"]}\n参数:{result["args"]}\n' \
f'版本:{__version__}\n运行时间:{process_time()}\n出现这个错误可能意味着程序有bug请把错误信息和重现方法' \
f'版本:{__version__}\n出现这个错误可能意味着程序有bug请把错误信息和重现方法' \
'告知作者,谢谢。\n报告网站https://gitee.com/g1879/DrissionPage/issues'
r = TimeoutError(txt) if result['type'] == 'timeout' else CDPError(txt)
else:

View File

@ -10,7 +10,8 @@ from pathlib import Path
from threading import Lock
from typing import Union, Tuple
from .._pages.chromium_page import ChromiumPage
from ..errors import BaseError
from .._pages.chromium_base import ChromiumBase
class PortFinder(object):
@ -30,7 +31,7 @@ def port_is_using(ip: str, port: Union[str, int]) -> bool: ...
def clean_folder(folder_path: Union[str, Path], ignore: Union[tuple, list] = None) -> None: ...
def show_or_hide_browser(page: ChromiumPage, hide: bool = True) -> None: ...
def show_or_hide_browser(page: ChromiumBase, hide: bool = True) -> None: ...
def get_browser_progress_id(progress: Union[popen, None], address: str) -> Union[str, None]: ...
@ -45,4 +46,4 @@ def wait_until(function: callable, kwargs: dict = None, timeout: float = 10): ..
def configs_to_here(file_name: Union[Path, str] = None) -> None: ...
def raise_error(result: dict, ignore=None) -> None: ...
def raise_error(result: dict, ignore: BaseError = None) -> None: ...

View File

@ -7,11 +7,10 @@
"""
from datetime import datetime
from html import unescape
from http.cookiejar import Cookie
from http.cookiejar import Cookie, CookieJar
from re import sub
from urllib.parse import urlparse, urljoin, urlunparse
from requests.cookies import RequestsCookieJar
from tldextract import extract
@ -97,7 +96,7 @@ def location_in_viewport(page, loc_x, loc_y):
:param loc_y: 页面绝对坐标y
:return: bool
"""
js = f'''function(){{var x = {loc_x}; var y = {loc_y};
js = f'''function(){{let x = {loc_x}; let y = {loc_y};
const scrollLeft = document.documentElement.scrollLeft;
const scrollTop = document.documentElement.scrollTop;
const vWidth = document.documentElement.clientWidth;
@ -181,17 +180,14 @@ def cookie_to_dict(cookie):
cookie_dict = cookie
elif isinstance(cookie, str):
cookie = cookie.rstrip(';,').split(',' if ',' in cookie else ';')
cookie_dict = {}
for key, attr in enumerate(cookie):
attr_val = attr.lstrip().split('=', 1)
if key == 0:
for attr in cookie.strip().rstrip(';,').split(',' if ',' in cookie else ';'):
attr_val = attr.strip().split('=', 1)
if attr_val[0] in ('domain', 'path', 'expires', 'max-age', 'HttpOnly', 'secure', 'expiry', 'name', 'value'):
cookie_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else ''
else:
cookie_dict['name'] = attr_val[0]
cookie_dict['value'] = attr_val[1] if len(attr_val) == 2 else ''
else:
cookie_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else ''
return cookie_dict
@ -206,17 +202,24 @@ def cookies_to_tuple(cookies):
:param cookies: cookies信息可为CookieJar, list, tuple, str, dict
:return: 返回tuple形式的cookies
"""
if isinstance(cookies, (list, tuple, RequestsCookieJar)):
if isinstance(cookies, (list, tuple, CookieJar)):
cookies = tuple(cookie_to_dict(cookie) for cookie in cookies)
elif isinstance(cookies, str):
cookies = tuple(cookie_to_dict(c.lstrip()) for c in cookies.rstrip(';,').split(',' if ',' in cookies else ';'))
c_dict = {}
for attr in cookies.strip().rstrip(';, ').split(',' if ',' in cookies else ';'):
attr_val = attr.strip().split('=', 1)
c_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else True
cookies = _dict_cookies_to_tuple(c_dict)
elif isinstance(cookies, dict):
cookies = tuple({'name': cookie, 'value': cookies[cookie]} for cookie in cookies)
cookies = _dict_cookies_to_tuple(cookies)
elif isinstance(cookies, Cookie):
cookies = (cookie_to_dict(cookies),)
else:
raise TypeError('cookies参数必须为RequestsCookieJar、list、tuple、str或dict类型。')
raise TypeError('cookies参数必须为Cookie、CookieJar、list、tuple、str或dict类型。')
return cookies
@ -227,8 +230,7 @@ def set_session_cookies(session, cookies):
:param cookies: cookies信息
:return: None
"""
cookies = cookies_to_tuple(cookies)
for cookie in cookies:
for cookie in cookies_to_tuple(cookies):
if cookie['value'] is None:
cookie['value'] = ''
@ -276,16 +278,20 @@ def set_browser_cookies(page, cookies):
cookie['value'] = ''
elif not isinstance(cookie['value'], str):
cookie['value'] = str(cookie['value'])
if cookie['name'].startswith('__Secure-'):
cookie['secure'] = True
if cookie['name'].startswith('__Host-'):
cookie['path'] = '/'
cookie['secure'] = True
cookie['url'] = page.url
if not page.url.startswith('http'):
cookie['name'] = cookie['name'].replace('__Host-', '__Secure-', 1)
else:
cookie['url'] = page.url
page.run_cdp_loaded('Network.setCookie', **cookie)
continue # 不用设置域名,可退出
if cookie['name'].startswith('__Secure-'):
cookie['secure'] = True
if cookie.get('domain', None):
try:
page.run_cdp_loaded('Network.setCookie', **cookie)
@ -294,7 +300,10 @@ def set_browser_cookies(page, cookies):
except Exception:
pass
ex_url = extract(page._browser_url)
url = page._browser_url
if not url.startswith('http'):
raise RuntimeError(f'未设置域名请设置cookie的domain参数或先访问一个网站。{cookie}')
ex_url = extract(url)
d_list = ex_url.subdomain.split('.')
d_list.append(f'{ex_url.domain}.{ex_url.suffix}' if ex_url.suffix else ex_url.domain)
@ -342,10 +351,10 @@ def get_blob(page, url, as_bytes=True):
js = """
function fetchData(url) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
let xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.onload = function() {
var reader = new FileReader();
let reader = new FileReader();
reader.onloadend = function(){resolve(reader.result);}
reader.readAsDataURL(xhr.response);
};
@ -370,6 +379,7 @@ def tree(ele_or_page):
:param ele_or_page: 页面或元素对象
:return: None
"""
def _tree(obj, last_one=True, body=''):
list_ele = obj.children()
length = len(list_ele)
@ -394,3 +404,30 @@ def tree(ele_or_page):
attrs = ' '.join([f"{k}='{v}'" for k, v in ele.attrs.items()])
print(f'<{ele.tag} {attrs}>'.replace('\n', ' '))
_tree(ele)
def format_headers(txt):
"""从浏览器复制的文本生成dict格式headers文本用换行分隔
:param txt: 从浏览器复制的原始文本格式headers
:return: dict格式headers
"""
if not isinstance(txt, str):
return txt
headers = {}
for header in txt.split('\n'):
if header:
name, value = header.split(': ', maxsplit=1)
headers[name] = value
return headers
def _dict_cookies_to_tuple(cookies: dict):
"""把dict形式的cookies转换为tuple形式
:param cookies: 单个或多个cookies单个时包含'name''value'
:return: 多个dict格式cookies组成的列表
"""
if 'name' in cookies and 'value' in cookies: # 单个cookie
return (cookies,)
keys = ('domain', 'path', 'expires', 'max-age', 'HttpOnly', 'secure', 'expiry')
template = {k: v for k, v in cookies.items() if k in keys}
return tuple(dict(**{'name': k, 'value': v}, **template) for k, v in cookies.items() if k not in keys)

View File

@ -37,7 +37,7 @@ def is_js_func(func: str) -> bool: ...
def cookie_to_dict(cookie: Union[Cookie, str, dict]) -> dict: ...
def cookies_to_tuple(cookies: Union[RequestsCookieJar, list, tuple, str, dict]) -> tuple: ...
def cookies_to_tuple(cookies: Union[RequestsCookieJar, list, tuple, str, dict, Cookie]) -> tuple: ...
def set_session_cookies(session: Session, cookies: Union[RequestsCookieJar, list, tuple, str, dict]) -> None: ...
@ -52,4 +52,7 @@ def is_cookie_in_driver(page: ChromiumBase, cookie: dict) -> bool: ...
def get_blob(page: ChromiumBase, url: str, as_bytes: bool = True) -> bytes: ...
def tree(ele_or_page:BaseParser) -> None: ...
def tree(ele_or_page: BaseParser) -> None: ...
def format_headers(txt: str) -> dict: ...

View File

@ -30,7 +30,7 @@ from .._units.scroller import PageScroller
from .._units.setter import ChromiumBaseSetter
from .._units.states import PageStates
from .._units.waiter import BaseWaiter
from ..errors import ContextLostError, CDPError, PageDisconnectedError, ElementNotFoundError
from ..errors import ContextLostError, CDPError, PageDisconnectedError, ElementNotFoundError, ElementLostError
__ERROR__ = 'error'
@ -156,21 +156,25 @@ class ChromiumBase(BasePage):
if self._is_reading:
return
self._is_reading = True
timeout = timeout if timeout >= .5 else .5
timeout = max(timeout, 2)
end_time = perf_counter() + timeout
while perf_counter() < end_time:
try:
b_id = self.run_cdp('DOM.getDocument', _timeout=timeout)['root']['backendNodeId']
timeout = end_time - perf_counter()
timeout = .5 if timeout <= 0 else timeout
timeout = 1 if timeout <= 1 else timeout
self._root_id = self.run_cdp('DOM.resolveNode', backendNodeId=b_id,
_timeout=timeout)['object']['objectId']
result = True
break
except PageDisconnectedError:
result = False
break
except:
timeout = end_time - perf_counter()
timeout = .5 if timeout <= 0 else timeout
sleep(.1)
else:
result = False
@ -474,7 +478,7 @@ class ChromiumBase(BasePage):
def cookies(self, as_dict=False, all_domains=False, all_info=False):
"""返回cookies信息
:param as_dict: 为True时返回由{name: value}键值对组成的dict为True时返回list且all_info无效
:param as_dict: 为True时以dict格式返回为False时返回list且all_info无效
:param all_domains: 是否返回所有域的cookies
:param all_info: 是否返回所有信息为False时只返回namevaluedomain
:return: cookies信息
@ -667,32 +671,62 @@ class ChromiumBase(BasePage):
return
ele = self._ele(loc_or_ele, raise_err=False)
if ele:
self.run_cdp('DOM.removeNode', nodeId=ele._node_id)
self.run_cdp('DOM.removeNode', nodeId=ele._node_id, _ignore=ElementLostError)
def add_ele(self, outerHTML, insert_to=None, before=None):
def add_ele(self, html_or_info, insert_to=None, before=None):
"""新建一个元素
:param outerHTML: 新元素的html文本
:param insert_to: 插入到哪个元素中可接收元素对象和定位符为None添加到body
:param html_or_info: 新元素的html文本或信息信息格式为(tag, {attr1: value, ...})
:param insert_to: 插入到哪个元素中可接收元素对象和定位符为None且为html添加到body不为html不插入
:param before: 在哪个子节点前面插入可接收对象和定位符为None插入到父元素末尾
:return: 元素对象
"""
insert_to = self.ele(insert_to) if insert_to else self.ele('t:body')
args = [outerHTML, insert_to]
if before:
args.append(self.ele(before))
js = '''
ele = document.createElement(null);
arguments[1].insertBefore(ele, arguments[2]);
ele.outerHTML = arguments[0];
return arguments[2].previousElementSibling;
'''
if isinstance(html_or_info, str):
insert_to = self.ele(insert_to) if insert_to else self.ele('t:body')
args = [html_or_info, insert_to]
if before:
args.append(self.ele(before))
js = '''
ele = document.createElement(null);
arguments[1].insertBefore(ele, arguments[2]);
ele.outerHTML = arguments[0];
return arguments[2].previousElementSibling;
'''
else:
js = '''
ele = document.createElement(null);
arguments[1].appendChild(ele);
ele.outerHTML = arguments[0];
return arguments[1].lastElementChild;
'''
elif isinstance(html_or_info, tuple):
args = [html_or_info[0], html_or_info[1]]
txt = ''
if insert_to:
args.append(self.ele(insert_to))
if before:
args.append(self.ele(before))
txt = '''
arguments[2].insertBefore(ele, arguments[3]);
'''
else:
txt = '''
arguments[2].appendChild(ele);
'''
js = f'''
ele = document.createElement(arguments[0]);
for(let k in arguments[1]){{
if(k=="innerHTML"){{ele.innerHTML=arguments[1][k]}}
else if(k=="innerText"){{ele.innerText=arguments[1][k]}}
else{{ele.setAttribute(k, arguments[1][k]);}}
}}
{txt}
return ele;
'''
else:
js = '''
ele = document.createElement(null);
arguments[1].appendChild(ele);
ele.outerHTML = arguments[0];
return arguments[1].lastElementChild;
'''
raise TypeError('html_or_info参数必须是html文本或tupletuple格式为(tag, {name: value})。')
ele = self.run_js(js, *args)
return ele
@ -753,42 +787,16 @@ class ChromiumBase(BasePage):
:param item: 要获取的项不设置则返回全部
:return: sessionStorage一个或所有项内容
"""
if item:
js = f'sessionStorage.getItem("{item}");'
return self.run_js_loaded(js, as_expr=True)
else:
js = '''
var dp_ls_len = sessionStorage.length;
var dp_ls_arr = new Array();
for(var i = 0; i < dp_ls_len; i++) {
var getKey = sessionStorage.key(i);
var getVal = sessionStorage.getItem(getKey);
dp_ls_arr[i] = {'key': getKey, 'val': getVal}
}
return dp_ls_arr;
'''
return {i['key']: i['val'] for i in self.run_js_loaded(js)}
js = f'sessionStorage.getItem("{item}")' if item else 'sessionStorage'
return self.run_js_loaded(js, as_expr=True)
def local_storage(self, item=None):
"""返回localStorage信息不设置item则获取全部
:param item: 要获取的项目不设置则返回全部
:return: localStorage一个或所有项内容
"""
if item:
js = f'localStorage.getItem("{item}");'
return self.run_js_loaded(js, as_expr=True)
else:
js = '''
var dp_ls_len = localStorage.length;
var dp_ls_arr = new Array();
for(var i = 0; i < dp_ls_len; i++) {
var getKey = localStorage.key(i);
var getVal = localStorage.getItem(getKey);
dp_ls_arr[i] = {'key': getKey, 'val': getVal}
}
return dp_ls_arr;
'''
return {i['key']: i['val'] for i in self.run_js_loaded(js)}
js = f'localStorage.getItem("{item}")' if item else 'localStorage'
return self.run_js_loaded(js, as_expr=True)
def get_screenshot(self, path=None, name=None, as_bytes=None, as_base64=None,
full_page=False, left_top=None, right_bottom=None):
@ -867,6 +875,8 @@ class ChromiumBase(BasePage):
sleep(wait)
self.browser.reconnect()
self._driver = self.browser._get_driver(t_id, self)
self._driver_init(t_id)
self._get_document()
def handle_alert(self, accept=True, send=None, timeout=None, next_one=False):
"""处理提示框,可以自动等待提示框出现
@ -917,7 +927,7 @@ class ChromiumBase(BasePage):
"""alert出现时触发的方法"""
self._alert.activated = True
self._alert.text = kwargs['message']
self._alert.type = kwargs['message']
self._alert.type = kwargs['type']
self._alert.defaultPrompt = kwargs.get('defaultPrompt', None)
self._alert.response_accept = None
self._alert.response_text = None
@ -925,6 +935,8 @@ class ChromiumBase(BasePage):
if self._alert.auto is not None:
self._handle_alert(self._alert.auto)
elif Settings.auto_handle_alert is not None:
self._handle_alert(Settings.auto_handle_alert)
elif self._alert.handle_next is not None:
self._handle_alert(self._alert.handle_next, self._alert.next_text)
self._alert.handle_next = None
@ -1206,6 +1218,7 @@ def close_privacy_dialog(page, tid):
break
except KeyError:
pass
sleep(.05)
driver.run('DOM.discardSearchResults', searchId=sid)
r = driver.run('DOM.resolveNode', backendNodeId=r)['object']['objectId']
r = driver.run('Runtime.callFunctionOn', objectId=r,
@ -1231,7 +1244,7 @@ def get_mhtml(page, path=None, name=None):
Path(path).mkdir(parents=True, exist_ok=True)
name = make_valid_name(name or page.title)
with open(f'{path}{sep}{name}.mhtml', 'w', encoding='utf-8') as f:
f.write(r)
f.write(r.replace('\r\n', '\n'))
return r

View File

@ -8,8 +8,7 @@
from pathlib import Path
from typing import Union, Tuple, List, Any, Optional, Literal
from .chromium_tab import ChromiumTab, WebPageTab
from .web_page import WebPage
from .chromium_tab import ChromiumTab
from .._base.base import BasePage
from .._base.browser import Browser
from .._base.driver import Driver
@ -217,7 +216,7 @@ class ChromiumBase(BasePage):
def remove_ele(self, loc_or_ele: Union[ChromiumElement, ChromiumFrame, str, Tuple[str, str]]) -> None: ...
def add_ele(self,
outerHTML: str,
html_or_info: Union[str, Tuple[str, dict]],
insert_to: Union[ChromiumElement, str, Tuple[str, str], None] = None,
before: Union[ChromiumElement, str, Tuple[str, str], None] = None) -> ChromiumElement: ...

View File

@ -59,7 +59,7 @@ class ChromiumFrame(ChromiumBase):
self._rect = None
self._type = 'ChromiumFrame'
end_time = perf_counter() + 2
while perf_counter() < end_time:
while perf_counter() < end_time: # todo: 优化
if self.url not in (None, 'about:blank'):
break
sleep(.1)
@ -119,6 +119,7 @@ class ChromiumFrame(ChromiumBase):
node = self._target_page.run_cdp('DOM.describeNode', backendNodeId=self._frame_ele._backend_id)['node']
if 'frameId' in node:
break
sleep(.05)
else:
return
@ -164,7 +165,7 @@ class ChromiumFrame(ChromiumBase):
self.doc_ele = ChromiumElement(self._target_page, backend_id=node['contentDocument']['backendNodeId'])
else:
timeout = timeout if timeout >= .5 else .5
timeout = max(timeout, 2)
b_id = self.run_cdp('DOM.getDocument', _timeout=timeout)['root']['backendNodeId']
self.doc_ele = ChromiumElement(self, backend_id=b_id)
@ -176,7 +177,6 @@ class ChromiumFrame(ChromiumBase):
return True
except:
raise
return False
finally:

View File

@ -14,6 +14,7 @@ from requests import get
from .._base.browser import Browser
from .._configs.chromium_options import ChromiumOptions
from .._functions.browser import connect_browser
from .._functions.settings import Settings
from .._functions.tools import PortFinder
from .._pages.chromium_base import ChromiumBase, get_mhtml, get_pdf, Timeout
from .._pages.chromium_tab import ChromiumTab
@ -70,8 +71,9 @@ class ChromiumPage(ChromiumBase):
def _run_browser(self):
"""连接浏览器"""
self._browser = Browser(self._chromium_options.address, self._browser_id, self)
if (self._is_exist and self._chromium_options._headless is False and
'headless' in self._browser.run_cdp('Browser.getVersion')['userAgent'].lower()):
r = self._browser.run_cdp('Browser.getVersion')
self._browser_version = r['product']
if self._is_exist and self._chromium_options._headless is False and 'headless' in r['userAgent'].lower():
self._browser.quit(3)
connect_browser(self._chromium_options)
ws = get(f'http://{self._chromium_options.address}/json/version', headers={'Connection': 'close'})
@ -124,20 +126,26 @@ class ChromiumPage(ChromiumBase):
return self.browser.tabs_count
@property
def tabs(self):
def tab_ids(self):
"""返回所有标签页id组成的列表"""
return self.browser.tabs
return self.browser.tab_ids
@property
def latest_tab(self):
"""返回最新的标签页id最新标签页指最后创建或最后被激活的"""
return self.tabs[0]
"""返回最新的标签页,最新标签页指最后创建或最后被激活的
当Settings.singleton_tab_obj==True时返回Tab对象否则返回tab id"""
return self.get_tab(self.tab_ids[0], as_id=not Settings.singleton_tab_obj)
@property
def process_id(self):
"""返回浏览器进程id"""
return self.browser.process_id
@property
def browser_version(self):
"""返回所控制的浏览器版本号"""
return self._browser_version
def save(self, path=None, name=None, as_pdf=False, **kwargs):
"""把当前页面保存为文件如果path和name参数都为None只返回文本
:param path: 保存路径为None且name不为None时保存在当前路径
@ -148,32 +156,56 @@ class ChromiumPage(ChromiumBase):
"""
return get_pdf(self, path, name, kwargs) if as_pdf else get_mhtml(self, path, name)
def get_tab(self, id_or_num=None):
"""获取一个标签页对象
:param id_or_num: 要获取的标签页id或序号为None时获取当前tab序号从1开始可传入负数获取倒数第几个不是视觉排列顺序而是激活顺序
:return: 标签页对象
def get_tab(self, id_or_num=None, title=None, url=None, tab_type='page', as_id=False):
"""获取一个标签页对象id_or_num不为None时后面几个参数无效
:param id_or_num: 要获取的标签页id或序号序号从1开始可传入负数获取倒数第几个不是视觉排列顺序而是激活顺序
:param title: 要匹配title的文本模糊匹配为None则匹配所有
:param url: 要匹配url的文本模糊匹配为None则匹配所有
:param tab_type: tab类型可用列表输入多个 'page', 'iframe' 为None则匹配所有
:param as_id: 是否返回标签页id而不是标签页对象
:return: ChromiumTab对象
"""
with self._lock:
if id_or_num is not None:
if isinstance(id_or_num, str):
return ChromiumTab(self, id_or_num)
id_or_num = id_or_num
elif isinstance(id_or_num, int):
return ChromiumTab(self, self.tabs[id_or_num - 1 if id_or_num > 0 else id_or_num])
elif id_or_num is None:
return ChromiumTab(self, self.tab_id)
id_or_num = self.tab_ids[id_or_num - 1 if id_or_num > 0 else id_or_num]
elif isinstance(id_or_num, ChromiumTab):
return id_or_num
else:
raise TypeError(f'id_or_num需传入tab id或序号{id_or_num}')
if as_id:
return id_or_num.tab_id
elif Settings.singleton_tab_obj:
return id_or_num
else:
return self.get_tab(id_or_num.tab_id)
def find_tabs(self, title=None, url=None, tab_type=None, single=True):
"""查找符合条件的tab返回它们的id组成的列表
:param title: 要匹配title的文本
:param url: 要匹配url的文本
:param tab_type: tab类型可用列表输入多个
:param single: 是否返回首个结果的id为False返回所有信息
:return: tab id或tab列表
elif title == url == tab_type is None:
id_or_num = self.tab_id
else:
id_or_num = self._browser.find_tabs(title, url, tab_type)
if id_or_num:
id_or_num = id_or_num[0]['id']
else:
return None
if as_id:
return id_or_num
with self._lock:
return ChromiumTab(self, id_or_num)
def get_tabs(self, title=None, url=None, tab_type='page', as_id=False):
"""查找符合条件的tab返回它们组成的列表
:param title: 要匹配title的文本模糊匹配为None则匹配所有
:param url: 要匹配url的文本模糊匹配为None则匹配所有
:param tab_type: tab类型可用列表输入多个 'page', 'iframe' 为None则匹配所有
:param as_id: 是否返回标签页id而不是标签页对象
:return: ChromiumTab对象组成的列表
"""
return self._browser.find_tabs(title, url, tab_type, single)
if as_id:
return [tab['id'] for tab in self._browser.find_tabs(title, url, tab_type)]
with self._lock:
return [ChromiumTab(self, tab['id']) for tab in self._browser.find_tabs(title, url, tab_type)]
def new_tab(self, url=None, new_window=False, background=False, new_context=False):
"""新建一个标签页
@ -183,32 +215,11 @@ class ChromiumPage(ChromiumBase):
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
tab = ChromiumTab(self, tab_id=self._new_tab(new_window, background, new_context))
tab = ChromiumTab(self, tab_id=self.browser.new_tab(new_window, background, new_context))
if url:
tab.get(url)
return tab
def _new_tab(self, new_window=False, background=False, new_context=False):
"""新建一个标签页
:param new_window: 是否在新窗口打开标签页
:param background: 是否不激活新标签页如new_window为True则无效
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
bid = None
if new_context:
bid = self.browser.run_cdp('Target.createBrowserContext')['browserContextId']
kwargs = {'url': ''}
if new_window:
kwargs['newWindow'] = True
if background:
kwargs['background'] = True
if bid:
kwargs['browserContextId'] = bid
return self.browser.run_cdp('Target.createTarget', **kwargs)['targetId']
def close(self):
"""关闭Page管理的标签页"""
self.close_tabs(self.tab_id)
@ -219,7 +230,7 @@ class ChromiumPage(ChromiumBase):
:param others: 是否关闭指定标签页之外的
:return: None
"""
all_tabs = set(self.tabs)
all_tabs = set(self.tab_ids)
if isinstance(tabs_or_ids, str):
tabs = {tabs_or_ids}
elif isinstance(tabs_or_ids, ChromiumTab):
@ -269,6 +280,22 @@ class ChromiumPage(ChromiumBase):
"""
self.close_tabs(tabs_or_ids, True)
@property
def tabs(self):
"""返回所有标签页id组成的列表"""
return self.browser.tab_ids
def find_tabs(self, title=None, url=None, tab_type=None, single=True):
"""查找符合条件的tab返回它们组成的列表
:param title: 要匹配title的文本
:param url: 要匹配url的文本
:param tab_type: tab类型可用列表输入多个
:param single: 是否返回首个结果的id为False返回所有信息
:return: tab id或tab列表
"""
r = self._browser.find_tabs(title, url, tab_type)
return r[0]['id'] if r and single else r
def handle_options(addr_or_opts):
"""设置浏览器启动属性

View File

@ -37,6 +37,7 @@ class ChromiumPage(ChromiumBase):
self._rect: Optional[TabRect] = ...
self._is_exist: bool = ...
self._lock: Lock = ...
self._browser_version: str = ...
def _handle_options(self, addr_or_opts: Union[str, ChromiumOptions]) -> str: ...
@ -51,17 +52,20 @@ class ChromiumPage(ChromiumBase):
def tabs_count(self) -> int: ...
@property
def tabs(self) -> List[str]: ...
def tab_ids(self) -> List[str]: ...
@property
def wait(self) -> PageWaiter: ...
@property
def latest_tab(self) -> str: ...
def latest_tab(self) -> Union[ChromiumTab, ChromiumPage, str]: ...
@property
def process_id(self) -> Optional[int]: ...
@property
def browser_version(self) -> str: ...
@property
def set(self) -> ChromiumPageSetter: ...
@ -86,16 +90,22 @@ class ChromiumPage(ChromiumBase):
generateTaggedPDF: bool = ...,
generateDocumentOutline: bool = ...) -> Union[bytes, str]: ...
def get_tab(self, tab_id: Union[str, ChromiumTab, int] = None) -> ChromiumTab: ...
def get_tab(self,
id_or_num: Union[str, ChromiumTab, int] = None,
title: str = None,
url: str = None,
tab_type: Union[str, list, tuple] = 'page',
as_id: bool = False) -> Union[ChromiumTab, str, None]: ...
def find_tabs(self, title: str = None, url: str = None,
tab_type: Union[str, list, tuple] = None, single: bool = True) -> Union[str, List[str]]: ...
def get_tabs(self,
title: str = None,
url: str = None,
tab_type: Union[str, list, tuple] = 'page',
as_id: bool = False) -> Union[List[ChromiumTab], List[str]]: ...
def new_tab(self, url: str = None, new_window: bool = False, background: bool = False,
new_context: bool = False) -> ChromiumTab: ...
def _new_tab(self, new_window: bool = False, background: bool = False, new_context: bool = False) -> str: ...
def close(self) -> None: ...
def close_tabs(self, tabs_or_ids: Union[str, ChromiumTab, List[Union[str, ChromiumTab]],

View File

@ -106,6 +106,9 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
:param page: WebPage对象
:param tab_id: 要控制的标签页id
"""
if Settings.singleton_tab_obj and hasattr(self, '_created'):
return
self._mode = 'd'
self._has_driver = True
self._has_session = True
@ -363,7 +366,7 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
def cookies(self, as_dict=False, all_domains=False, all_info=False):
"""返回cookies
:param as_dict: 是否以字典方式返回
:param as_dict: 为True时以dict格式返回为False时返回list且all_info无效
:param all_domains: 是否返回所有域的cookies
:param all_info: 是否返回所有信息False则只返回namevaluedomain
:return: cookies信息

View File

@ -5,6 +5,7 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from copy import copy
from pathlib import Path
from re import search, DOTALL
from time import sleep
@ -17,7 +18,7 @@ from tldextract import extract
from .._base.base import BasePage
from .._configs.session_options import SessionOptions
from .._elements.session_element import SessionElement, make_session_ele
from .._functions.web import cookie_to_dict
from .._functions.web import cookie_to_dict, format_headers
from .._units.setter import SessionPageSetter
@ -56,9 +57,9 @@ class SessionPage(BasePage):
elif isinstance(session_or_options, Session):
self._session_options = SessionOptions()
self._headers = session_or_options.headers
session_or_options.headers = None
self._session = session_or_options
self._session = copy(session_or_options)
self._headers = self._session.headers
self._session.headers = None
def _s_set_runtime_settings(self):
"""设置运行时用到的属性"""
@ -200,7 +201,7 @@ class SessionPage(BasePage):
:param index: 获取第几个从1开始可传入负数获取倒数第几个
:return: SessionElement对象或属性文本
"""
return make_session_ele(self.html) if locator is None else self._ele(locator, index=index, method='s_ele()')
return make_session_ele(self) if locator is None else self._ele(locator, index=index, method='s_ele()')
def s_eles(self, locator):
"""返回页面中符合条件的所有元素、属性或节点文本
@ -221,7 +222,7 @@ class SessionPage(BasePage):
def cookies(self, as_dict=False, all_domains=False, all_info=False):
"""返回cookies
:param as_dict: 是否以字典方式返回False则以list返回
:param as_dict: 为True时以dict格式返回为False时返回list且all_info无效
:param all_domains: 是否返回所有域的cookies
:param all_info: 是否返回所有信息False则只返回namevaluedomain
:return: cookies信息
@ -293,7 +294,7 @@ class SessionPage(BasePage):
if 'headers' not in kwargs:
kwargs['headers'] = {}
else:
kwargs['headers'] = CaseInsensitiveDict(kwargs['headers'])
kwargs['headers'] = CaseInsensitiveDict(format_headers(kwargs['headers']))
# 设置referer和host值
parsed_url = urlparse(url)

View File

@ -79,7 +79,7 @@ class SessionPage(BasePage):
params: dict | None = ...,
data: Union[dict, str, None] = ...,
json: Union[dict, str, None] = ...,
headers: dict | None = ...,
headers: Union[dict, str, None] = ...,
cookies: Any | None = ...,
files: Any | None = ...,
auth: Any | None = ...,
@ -140,7 +140,7 @@ class SessionPage(BasePage):
timeout: float | None = ...,
params: dict | None = ...,
json: Union[dict, str, None] = ...,
headers: dict | None = ...,
headers: Union[dict, str, None] = ...,
cookies: Any | None = ...,
files: Any | None = ...,
auth: Any | None = ...,

View File

@ -31,7 +31,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
"""初始化函数
:param mode: 'd' 's'即driver模式和session模式
:param timeout: 超时时间d模式时为寻找元素时间s模式时为连接时间默认10秒
:param chromium_options: Driver对象只使用s模式时应传入False
:param chromium_options: ChromiumOptions对象只使用s模式时应传入False
:param session_or_options: Session对象或SessionOptions对象只使用d模式时应传入False
"""
if hasattr(self, '_created'):
@ -298,7 +298,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
def cookies(self, as_dict=False, all_domains=False, all_info=False):
"""返回cookies
:param as_dict: 是否以字典方式返回False以list形式返回
:param as_dict: 为True时以dict格式返回为False时返回list且all_info无效
:param all_domains: 是否返回所有域的cookies
:param all_info: 是否返回所有信息False则只返回namevaluedomain
:return: cookies信息
@ -308,21 +308,51 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
elif self._mode == 'd':
return super(SessionPage, self).cookies(as_dict, all_domains, all_info)
def get_tab(self, id_or_num=None):
"""获取一个标签页对象
:param id_or_num: 要获取的标签页id或序号为None时获取当前tab序号不是视觉排列顺序而是激活顺序
:return: 标签页对象
def get_tab(self, id_or_num=None, title=None, url=None, tab_type='page', as_id=False):
"""获取一个标签页对象id_or_num不为None时后面几个参数无效
:param id_or_num: 要获取的标签页id或序号序号从1开始可传入负数获取倒数第几个不是视觉排列顺序而是激活顺序
:param title: 要匹配title的文本模糊匹配为None则匹配所有
:param url: 要匹配url的文本模糊匹配为None则匹配所有
:param tab_type: tab类型可用列表输入多个 'page', 'iframe' 为None则匹配所有
:param as_id: 是否返回标签页id而不是标签页对象
:return: WebPageTab对象
"""
if isinstance(id_or_num, str):
return WebPageTab(self, id_or_num)
elif isinstance(id_or_num, int):
return WebPageTab(self, self.tabs[id_or_num])
elif id_or_num is None:
return WebPageTab(self, self.tab_id)
elif isinstance(id_or_num, WebPageTab):
return id_or_num
if id_or_num is not None:
if isinstance(id_or_num, str):
id_or_num = id_or_num
elif isinstance(id_or_num, int):
id_or_num = self.tab_ids[id_or_num - 1 if id_or_num > 0 else id_or_num]
elif isinstance(id_or_num, WebPageTab):
return id_or_num.tab_id if as_id else id_or_num
elif title == url == tab_type is None:
id_or_num = self.tab_id
else:
raise TypeError(f'id_or_num需传入tab id或序号{id_or_num}')
id_or_num = self._browser.find_tabs(title, url, tab_type)
if id_or_num:
id_or_num = id_or_num[0]['id']
else:
return None
if as_id:
return id_or_num
with self._lock:
return WebPageTab(self, id_or_num)
def get_tabs(self, title=None, url=None, tab_type='page', as_id=False):
"""查找符合条件的tab返回它们组成的列表
:param title: 要匹配title的文本模糊匹配为None则匹配所有
:param url: 要匹配url的文本模糊匹配为None则匹配所有
:param tab_type: tab类型可用列表输入多个 'page', 'iframe' 为None则匹配所有
:param as_id: 是否返回标签页id而不是标签页对象
:return: ChromiumTab对象组成的列表
"""
if as_id:
return [tab['id'] for tab in self._browser.find_tabs(title, url, tab_type)]
with self._lock:
return [WebPageTab(self, tab['id']) for tab in self._browser.find_tabs(title, url, tab_type)]
def new_tab(self, url=None, new_window=False, background=False, new_context=False):
"""新建一个标签页
@ -332,7 +362,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
tab = WebPageTab(self, tab_id=self._new_tab(new_window, background, new_context))
tab = WebPageTab(self, tab_id=self.browser.new_tab(new_window, background, new_context))
if url:
tab.get(url)
return tab

View File

@ -90,7 +90,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
params: dict | None = ...,
data: Union[dict, str, None] = ...,
json: Union[dict, str, None] = ...,
headers: dict | None = ...,
headers: Union[dict, str, None] = ...,
cookies: Any | None = ...,
files: Any | None = ...,
auth: Any | None = ...,
@ -127,7 +127,18 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
all_domains: bool = False,
all_info: bool = False) -> Union[dict, list]: ...
def get_tab(self, id_or_num: Union[str, WebPageTab, int] = None) -> WebPageTab: ...
def get_tab(self,
id_or_num: Union[str, WebPageTab, int] = None,
title: str = None,
url: str = None,
tab_type: Union[str, list, tuple] = 'page',
as_id: bool = False) -> Union[WebPageTab, str, None]: ...
def get_tabs(self,
title: str = None,
url: str = None,
tab_type: Union[str, list, tuple] = 'page',
as_id: bool = False) -> Union[List[WebPageTab], List[str]]: ...
def new_tab(self,
url: str = None,
@ -151,7 +162,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
timeout: float | None = ...,
params: dict | None = ...,
json: Union[dict, str, None] = ...,
headers: dict | None = ...,
headers: Union[dict, str, None] = ...,
cookies: Any | None = ...,
files: Any | None = ...,
auth: Any | None = ...,
@ -162,6 +173,9 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
verify: Any | None = ...,
cert: Any | None = ...) -> Union[bool, Response]: ...
@property
def latest_tab(self) -> Union[WebPageTab, WebPage]: ...
@property
def set(self) -> WebPageSetter: ...

View File

@ -8,7 +8,7 @@
from time import sleep, perf_counter
from ..errors import AlertExistsError
from .._functions.keys import modifierBit, keyDescriptionForString, input_text_or_keys, Keys
from .._functions.keys import modifierBit, keyDescriptionForString, input_text_or_keys, Keys, keyDefinitions
from .._functions.web import location_in_viewport
@ -274,18 +274,23 @@ class Actions:
return self
def type(self, keys):
"""用模拟键盘按键方式输入文本,可输入字符串,也可输入组合键,只能输入键盘上有的字符
"""用模拟键盘按键方式输入文本,可输入字符串,也可输入组合键
:param keys: 要按下的按键特殊字符和多个文本可用list或tuple传入
:return: self
"""
modifiers = []
for i in keys:
for character in i:
self.key_down(character)
if character in ('\ue009', '\ue008', '\ue00a', '\ue03d'):
modifiers.append(character)
if character in keyDefinitions:
self.key_down(character)
if character in ('\ue009', '\ue008', '\ue00a', '\ue03d'):
modifiers.append(character)
else:
self.key_up(character)
else:
self.key_up(character)
self.owner.run_cdp('Input.dispatchKeyEvent', type='char', text=character)
for m in modifiers:
self.key_up(m)
return self
@ -298,9 +303,13 @@ class Actions:
input_text_or_keys(self.owner, text)
return self
def wait(self, second):
"""等待若干秒"""
sleep(second)
def wait(self, second, scope=None):
"""等待若干秒,如传入两个参数,等待时间为这两个数间的一个随机数
:param second: 秒数
:param scope: 随机数范围
:return: None
"""
self.owner.wait(second=second, scope=scope)
return self
def _get_key_data(self, key, action):

View File

@ -100,7 +100,7 @@ class Actions:
def input(self, text: Any) -> Actions: ...
def wait(self, second: float) -> Actions: ...
def wait(self, second: float, scope: float = None) -> Actions: ...
def _get_key_data(self, key: str, action: str) -> dict: ...

View File

@ -37,10 +37,12 @@ class Clicker(object):
:return: 是否点击成功
"""
if self._ele.tag == 'option':
if self._ele.states.is_selected:
self._ele.parent('t:select').select.cancel_by_option(self._ele)
else:
if not self._ele.states.is_selected:
self._ele.parent('t:select').select.by_option(self._ele)
else:
select = self._ele.parent('t:select')
if select.select.is_multi:
self._ele.parent('t:select').select.cancel_by_option(self._ele)
return
if not by_js: # 模拟点击
@ -184,17 +186,6 @@ class Clicker(object):
raise RuntimeError('没有出现新标签页。')
return self._ele.page.get_tab(tid)
def for_new_tab(self, by_js=False):
"""点击后等待新tab出现并返回其对象
:param by_js: 是否使用js点击逻辑与click()一致
:return: 新标签页对象如果没有等到新标签页出现则抛出异常
"""
self.left(by_js=by_js)
tid = self._ele.page._page.wait.new_tab()
if not tid:
raise RuntimeError('没有出现新标签页。')
return self._ele.page._page.get_tab(tid)
def _click(self, client_x, client_y, button='left', count=1):
"""实施点击
:param client_x: 视口中的x坐标

View File

@ -5,23 +5,22 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from http.cookiejar import Cookie
from .._functions.web import set_browser_cookies, set_session_cookies
class CookiesSetter(object):
def __init__(self, page):
self._page = page
def __init__(self, owner):
"""
:param owner: ChromiumBase对象
"""
self._owner = owner
def __call__(self, cookies):
"""设置一个或多个cookie
:param cookies: cookies信息
:return: None
"""
if (isinstance(cookies, dict) and 'name' in cookies and 'value' in cookies) or isinstance(cookies, Cookie):
cookies = [cookies]
set_browser_cookies(self._page, cookies)
set_browser_cookies(self._owner, cookies)
def remove(self, name, url=None, domain=None, path=None):
"""删除一个cookie
@ -36,38 +35,38 @@ class CookiesSetter(object):
d['url'] = url
if domain is not None:
d['domain'] = domain
if not url and not domain:
d['url'] = self._owner.url
if path is not None:
d['path'] = path
self._page.run_cdp('Network.deleteCookies', **d)
self._owner.run_cdp('Network.deleteCookies', **d)
def clear(self):
"""清除cookies"""
self._page.run_cdp('Network.clearBrowserCookies')
self._owner.run_cdp('Network.clearBrowserCookies')
class SessionCookiesSetter(object):
def __init__(self, page):
self._page = page
def __init__(self, owner):
self._owner = owner
def __call__(self, cookies):
"""设置多个cookie注意不要传入单个
:param cookies: cookies信息
:return: None
"""
if (isinstance(cookies, dict) and 'name' in cookies and 'value' in cookies) or isinstance(cookies, Cookie):
cookies = [cookies]
set_session_cookies(self._page.session, cookies)
set_session_cookies(self._owner.session, cookies)
def remove(self, name):
"""删除一个cookie
:param name: cookie的name字段
:return: None
"""
self._page.session.cookies.set(name, None)
self._owner.session.cookies.set(name, None)
def clear(self):
"""清除cookies"""
self._page.session.cookies.clear()
self._owner.session.cookies.clear()
class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter):
@ -77,9 +76,9 @@ class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter):
:param cookies: cookies信息
:return: None
"""
if self._page.mode == 'd' and self._page._has_driver:
if self._owner.mode == 'd' and self._owner._has_driver:
super().__call__(cookies)
elif self._page.mode == 's' and self._page._has_session:
elif self._owner.mode == 's' and self._owner._has_session:
super(CookiesSetter, self).__call__(cookies)
def remove(self, name, url=None, domain=None, path=None):
@ -90,16 +89,16 @@ class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter):
:param path: cookie的path字段可选d模式时才有效
:return: None
"""
if self._page.mode == 'd' and self._page._has_driver:
if self._owner.mode == 'd' and self._owner._has_driver:
super().remove(name, url, domain, path)
elif self._page.mode == 's' and self._page._has_session:
elif self._owner.mode == 's' and self._owner._has_session:
if url or domain or path:
raise AttributeError('url、domain、path参数只有d模式下有效。')
super(CookiesSetter, self).remove(name)
def clear(self):
"""清除cookies"""
if self._page.mode == 'd' and self._page._has_driver:
if self._owner.mode == 'd' and self._owner._has_driver:
super().clear()
elif self._page.mode == 's' and self._page._has_session:
elif self._owner.mode == 's' and self._owner._has_session:
super(CookiesSetter, self).clear()

View File

@ -5,11 +5,9 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from http.cookiejar import Cookie
from http.cookiejar import Cookie, CookieJar
from typing import Union
from requests.cookies import RequestsCookieJar
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_tab import WebPageTab
from .._pages.session_page import SessionPage
@ -17,11 +15,11 @@ from .._pages.web_page import WebPage
class CookiesSetter(object):
_page: ChromiumBase
_owner: ChromiumBase
def __init__(self, page: ChromiumBase): ...
def __call__(self, cookies: Union[RequestsCookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def __call__(self, cookies: Union[CookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def remove(self, name: str, url: str = None, domain: str = None, path: str = None) -> None: ...
@ -29,11 +27,11 @@ class CookiesSetter(object):
class SessionCookiesSetter(object):
_page: SessionPage
_owner: SessionPage
def __init__(self, page: SessionPage): ...
def __call__(self, cookies: Union[RequestsCookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def __call__(self, cookies: Union[CookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def remove(self, name: str) -> None: ...
@ -41,11 +39,11 @@ class SessionCookiesSetter(object):
class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter):
_page: Union[WebPage, WebPageTab]
_owner: Union[WebPage, WebPageTab]
def __init__(self, page: SessionPage): ...
def __call__(self, cookies: Union[RequestsCookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def __call__(self, cookies: Union[CookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def remove(self, name: str, url: str = None, domain: str = None, path: str = None) -> None: ...

View File

@ -30,7 +30,7 @@ class DownloadManager(object):
def set_rename(self, tab_id: str, rename: str = None, suffix: str = None) -> None: ...
def set_file_exists(self, tab_id: str, mode: Literal['rename', 'skip', 'overwrite']) -> None: ...
def set_file_exists(self, tab_id: str, mode: Literal['skip', 'rename', 'overwrite', 's', 'r', 'o']) -> None: ...
def set_flag(self, tab_id: str, flag: Union[bool, DownloadMission, None]) -> None: ...

View File

@ -21,13 +21,13 @@ from ..errors import WaitTimeoutError
class Listener(object):
"""监听器基类"""
def __init__(self, page):
def __init__(self, owner):
"""
:param page: ChromiumBase对象
:param owner: ChromiumBase对象
"""
self._page = page
self._address = page.address
self._target_id = page._target_id
self._owner = owner
self._address = owner.address
self._target_id = owner._target_id
self._driver = None
self._running_requests = 0
self._running_targets = 0
@ -167,7 +167,7 @@ class Listener(object):
caught = 0
end = perf_counter() + timeout if timeout else None
while True:
if timeout and perf_counter() > end:
if (timeout and perf_counter() > end) or self._driver._stopped.is_set():
return
if self._caught.qsize() >= gap:
yield self._caught.get_nowait() if gap == 1 else [self._caught.get_nowait() for _ in range(gap)]
@ -216,37 +216,40 @@ class Listener(object):
self._running_requests = 0
self._running_targets = 0
def wait_silent(self, timeout=None, targets_only=False):
def wait_silent(self, timeout=None, targets_only=False, limit=0):
"""等待所有请求结束
:param timeout: 超时为None时无限等待
:param targets_only: 是否只等待targets指定的请求结束
:param limit: 剩下多少个连接时视为结束
:return: 返回是否等待成功
"""
if not self.listening:
raise RuntimeError('监听未启动或已暂停。')
if timeout is None:
while (not targets_only and self._running_requests > 0) or (targets_only and self._running_targets > 0):
while ((not targets_only and self._running_requests > limit)
or (targets_only and self._running_targets > limit)):
sleep(.1)
return True
end_time = perf_counter() + timeout
while perf_counter() < end_time:
if (not targets_only and self._running_requests <= 0) or (targets_only and self._running_targets <= 0):
if ((not targets_only and self._running_requests <= limit)
or (targets_only and self._running_targets <= limit)):
return True
sleep(.1)
else:
return False
def _to_target(self, target_id, address, page):
def _to_target(self, target_id, address, owner):
"""切换监听的页面对象
:param target_id: 新页面对象_target_id
:param address: 新页面对象address
:param page: 新页面对象
:param owner: 新页面对象
:return: None
"""
self._target_id = target_id
self._address = address
self._page = page
self._owner = owner
debug = False
if self._driver:
debug = self._driver._debug
@ -275,7 +278,7 @@ class Listener(object):
and (self._res_type is True or kwargs.get('type', '').upper() in self._res_type)):
self._running_targets += 1
rid = kwargs['requestId']
p = self._request_ids.setdefault(rid, DataPacket(self._page.tab_id, True))
p = self._request_ids.setdefault(rid, DataPacket(self._owner.tab_id, True))
p._raw_request = kwargs
if kwargs['request'].get('hasPostData', None) and not kwargs['request'].get('postData', None):
p._raw_post_data = self._driver.run('Network.getRequestPostData',
@ -289,7 +292,7 @@ class Listener(object):
and (self._method is True or kwargs['request']['method'] in self._method)
and (self._res_type is True or kwargs.get('type', '').upper() in self._res_type)):
self._running_targets += 1
p = self._request_ids.setdefault(rid, DataPacket(self._page.tab_id, target))
p = self._request_ids.setdefault(rid, DataPacket(self._owner.tab_id, target))
p._raw_request = kwargs
break
@ -390,13 +393,13 @@ class Listener(object):
class FrameListener(Listener):
def _requestWillBeSent(self, **kwargs):
"""接收到请求时的回调函数"""
if not self._page._is_diff_domain and kwargs.get('frameId', None) != self._page._frame_id:
if not self._owner._is_diff_domain and kwargs.get('frameId', None) != self._owner._frame_id:
return
super()._requestWillBeSent(**kwargs)
def _response_received(self, **kwargs):
"""接收到返回信息时处理方法"""
if not self._page._is_diff_domain and kwargs.get('frameId', None) != self._page._frame_id:
if not self._owner._is_diff_domain and kwargs.get('frameId', None) != self._owner._frame_id:
return
super()._response_received(**kwargs)
@ -528,8 +531,14 @@ class Request(object):
self._postData = postData
return self._postData
@property
def cookies(self):
"""以list形式返回发送的cookies"""
return [c['cookie'] for c in self.extra_info.associatedCookies if not c['blockedReasons']]
@property
def extra_info(self):
"""返回额外数据"""
return RequestExtraInfo(self._data_packet._request_extra_info or {})

View File

@ -19,8 +19,8 @@ __RES_TYPE__ = Literal['Document', 'Stylesheet', 'Image', 'Media', 'Font', 'Scri
class Listener(object):
def __init__(self, page: ChromiumBase):
self._page: ChromiumBase = ...
def __init__(self, owner: ChromiumBase):
self._owner: ChromiumBase = ...
self._address: str = ...
self._target_id: str = ...
self._targets: Union[str, dict, None] = ...
@ -62,14 +62,22 @@ class Listener(object):
fit_count: bool = True,
raise_err: bool = None) -> Union[List[DataPacket], DataPacket, None]: ...
def steps(self,
count: int = None,
timeout: float = None,
gap=1) -> Iterable[Union[DataPacket, List[DataPacket]]]: ...
@property
def results(self) -> Union[DataPacket, Dict[str, List[DataPacket]], False]: ...
def clear(self) -> None: ...
def wait_silent(self, timeout: float = None, targets_only: bool = False) -> bool: ...
def wait_silent(self,
timeout: float = None,
targets_only: bool = False,
limit: int = 0) -> bool: ...
def _to_target(self, target_id: str, address: str, page: ChromiumBase) -> None: ...
def _to_target(self, target_id: str, address: str, owner: ChromiumBase) -> None: ...
def _requestWillBeSent(self, **kwargs) -> None: ...
@ -83,17 +91,12 @@ class Listener(object):
def _loading_failed(self, **kwargs) -> None: ...
def steps(self,
count: int = None,
timeout: float = None,
gap=1) -> Iterable[Union[DataPacket, List[DataPacket]]]: ...
def _set_callback(self) -> None: ...
class FrameListener(Listener):
def __init__(self, page: ChromiumFrame):
self._page: ChromiumFrame = ...
def __init__(self, owner: ChromiumFrame):
self._owner: ChromiumFrame = ...
self._is_diff: bool = ...
@ -174,6 +177,9 @@ class Request(object):
@property
def postData(self) -> Any: ...
@property
def cookies(self) -> List[dict]: ...
@property
def extra_info(self) -> Optional[RequestExtraInfo]: ...

View File

@ -102,7 +102,8 @@ class ElementRect(object):
:return: 四个角坐标
"""
return self._ele.owner.run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id,
nodeId=self._ele._node_id, objectId=self._ele._obj_id)['model'][quad]
# nodeId=self._ele._node_id, objectId=self._ele._obj_id
)['model'][quad]
def _get_page_coord(self, x, y):
"""根据视口坐标获取绝对坐标"""
@ -113,12 +114,15 @@ class ElementRect(object):
class TabRect(object):
def __init__(self, page):
self._page = page
def __init__(self, owner):
"""
:param owner: Page对象和Tab对象
"""
self._owner = owner
@property
def window_state(self):
"""返回窗口状态normal、fullscreen、maximized、 minimized"""
"""返回窗口状态normal、fullscreen、maximized、minimized"""
return self._get_window_rect()['windowState']
@property
@ -170,23 +174,26 @@ class TabRect(object):
@property
def viewport_size_with_scrollbar(self):
"""返回视口宽高,包括滚动条,格式:(宽, 高)"""
r = self._page.run_js('return window.innerWidth.toString() + " " + window.innerHeight.toString();')
r = self._owner.run_js('return window.innerWidth.toString() + " " + window.innerHeight.toString();')
w, h = r.split(' ')
return int(w), int(h)
def _get_page_rect(self):
"""获取页面范围信息"""
return self._page.run_cdp_loaded('Page.getLayoutMetrics')
return self._owner.run_cdp_loaded('Page.getLayoutMetrics')
def _get_window_rect(self):
"""获取窗口范围信息"""
return self._page.browser.get_window_bounds(self._page.tab_id)
return self._owner.browser.get_window_bounds(self._owner.tab_id)
class FrameRect(object):
"""异域iframe使用"""
def __init__(self, frame):
"""
:param frame: ChromiumFrame对象
"""
self._frame = frame
@property

View File

@ -62,8 +62,8 @@ class ElementRect(object):
class TabRect(object):
def __init__(self, page: ChromiumBase):
self._page: Union[ChromiumPage, ChromiumTab, WebPage, WebPageTab] = ...
def __init__(self, owner: ChromiumBase):
self._owner: Union[ChromiumPage, ChromiumTab, WebPage, WebPageTab] = ...
@property
def window_state(self) -> str: ...

View File

@ -16,8 +16,8 @@ from time import sleep, time
class Screencast(object):
def __init__(self, page):
self._page = page
def __init__(self, owner):
self._owner = owner
self._path = None
self._tmp_path = None
self._running = False
@ -39,16 +39,16 @@ class Screencast(object):
raise ValueError('save_path必须设置。')
if self._mode in ('frugal_video', 'video'):
if self._page.browser.page._chromium_options.tmp_path:
if self._owner.browser.page._chromium_options.tmp_path:
self._tmp_path = Path(
self._page.browser.page._chromium_options.tmp_path) / f'screencast_tmp_{time()}_{randint(0, 100)}'
self._owner.browser.page._chromium_options.tmp_path) / f'screencast_tmp_{time()}_{randint(0, 100)}'
else:
self._tmp_path = Path(gettempdir()) / 'DrissionPage' / f'screencast_tmp_{time()}_{randint(0, 100)}'
self._tmp_path.mkdir(parents=True, exist_ok=True)
if self._mode.startswith('frugal'):
self._page.driver.set_callback('Page.screencastFrame', self._onScreencastFrame)
self._page.run_cdp('Page.startScreencast', everyNthFrame=1, quality=100)
self._owner.driver.set_callback('Page.screencastFrame', self._onScreencastFrame)
self._owner.run_cdp('Page.startScreencast', everyNthFrame=1, quality=100)
elif not self._mode.startswith('js'):
self._running = True
@ -79,8 +79,8 @@ class Screencast(object):
}
'''
print('请手动选择要录制的目标。')
self._page.run_js('var DrissionPage_Screencast_blob;var DrissionPage_Screencast_blob_ok=false;')
self._page.run_js(js)
self._owner.run_js('var DrissionPage_Screencast_blob;var DrissionPage_Screencast_blob_ok=false;')
self._owner.run_js(js)
def stop(self, video_name=None):
"""停止录屏
@ -93,19 +93,19 @@ class Screencast(object):
path = f'{self._path}{sep}{name}'
if self._mode.startswith('js'):
self._page.run_js('mediaRecorder.stop();', as_expr=True)
while not self._page.run_js('return DrissionPage_Screencast_blob_ok;'):
self._owner.run_js('mediaRecorder.stop();', as_expr=True)
while not self._owner.run_js('return DrissionPage_Screencast_blob_ok;'):
sleep(.1)
blob = self._page.run_js('return DrissionPage_Screencast_blob;')
uuid = self._page.run_cdp('IO.resolveBlob', objectId=blob['result']['objectId'])['uuid']
data = self._page.run_cdp('IO.read', handle=f'blob:{uuid}')['data']
blob = self._owner.run_js('return DrissionPage_Screencast_blob;')
uuid = self._owner.run_cdp('IO.resolveBlob', objectId=blob['result']['objectId'])['uuid']
data = self._owner.run_cdp('IO.read', handle=f'blob:{uuid}')['data']
with open(path, 'wb') as f:
f.write(b64decode(data))
return path
if self._mode.startswith('frugal'):
self._page.driver.set_callback('Page.screencastFrame', None)
self._page.run_cdp('Page.stopScreencast')
self._owner.driver.set_callback('Page.screencastFrame', None)
self._owner.run_cdp('Page.stopScreencast')
else:
self._enable = False
while self._running:
@ -155,7 +155,7 @@ class Screencast(object):
self._running = True
path = self._tmp_path or self._path
while self._enable:
self._page.get_screenshot(path=path, name=f'{time()}.jpg')
self._owner.get_screenshot(path=path, name=f'{time()}.jpg')
sleep(.04)
self._running = False
@ -164,7 +164,7 @@ class Screencast(object):
path = self._tmp_path or self._path
with open(f'{path}{sep}{kwargs["metadata"]["timestamp"]}.jpg', 'wb') as f:
f.write(b64decode(kwargs['data']))
self._page.run_cdp('Page.screencastFrameAck', sessionId=kwargs['sessionId'])
self._owner.run_cdp('Page.screencastFrameAck', sessionId=kwargs['sessionId'])
class ScreencastMode(object):

View File

@ -12,8 +12,8 @@ from .._pages.chromium_base import ChromiumBase
class Screencast(object):
def __init__(self, page: ChromiumBase):
self._page: ChromiumBase = ...
def __init__(self, owner: ChromiumBase):
self._owner: ChromiumBase = ...
self._path: Path = ...
self._tmp_path: Path = ...
self._running: bool = ...

View File

@ -87,15 +87,15 @@ class Scroller(object):
if not self._wait_complete:
return
page = self._driver.owner if self._driver._type == 'ChromiumElement' else self._driver
r = page.run_cdp('Page.getLayoutMetrics')
owner = self._driver.owner if self._driver._type == 'ChromiumElement' else self._driver
r = owner.run_cdp('Page.getLayoutMetrics')
x = r['layoutViewport']['pageX']
y = r['layoutViewport']['pageY']
end_time = perf_counter() + page.timeout
end_time = perf_counter() + owner.timeout
while perf_counter() < end_time:
sleep(.1)
r = page.run_cdp('Page.getLayoutMetrics')
r = owner.run_cdp('Page.getLayoutMetrics')
x1 = r['layoutViewport']['pageX']
y1 = r['layoutViewport']['pageY']
@ -120,11 +120,11 @@ class ElementScroller(Scroller):
class PageScroller(Scroller):
def __init__(self, page):
def __init__(self, owner):
"""
:param page: 页面对象
:param owner: 页面对象
"""
super().__init__(page)
super().__init__(owner)
self.t1 = 'window'
self.t2 = 'document.documentElement'
@ -146,7 +146,7 @@ class PageScroller(Scroller):
txt = 'true' if center else 'false'
ele.run_js(f'this.scrollIntoViewIfNeeded({txt});')
if center or (center is not False and ele.states.is_covered):
ele.run_js('''function getWindowScrollTop() {var scroll_top = 0;
ele.run_js('''function getWindowScrollTop() {let scroll_top = 0;
if (document.documentElement && document.documentElement.scrollTop) {
scroll_top = document.documentElement.scrollTop;
} else if (document.body) {scroll_top = document.body.scrollTop;}

View File

@ -51,7 +51,7 @@ class ElementScroller(Scroller):
class PageScroller(Scroller):
def __init__(self, page: ChromiumBase): ...
def __init__(self, owner: ChromiumBase): ...
def to_see(self, loc_or_ele: Union[str, tuple, ChromiumElement], center: Union[bool, None] = None) -> None: ...

View File

@ -5,7 +5,7 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from time import perf_counter
from time import perf_counter, sleep
class SelectElement(object):
@ -215,6 +215,7 @@ class SelectElement(object):
if len(eles) >= text_len:
ok = True
break
sleep(.01)
if ok:
self._select_options(eles, mode)
@ -237,6 +238,7 @@ class SelectElement(object):
if len(self.options) >= text_len:
ok = True
break
sleep(.01)
if ok:
eles = self.options

View File

@ -11,12 +11,18 @@ from time import sleep
from requests.structures import CaseInsensitiveDict
from .cookies_setter import SessionCookiesSetter, CookiesSetter, WebPageCookiesSetter
from .._functions.settings import Settings
from .._functions.tools import show_or_hide_browser
from .._functions.web import format_headers
from ..errors import ElementLostError
class BasePageSetter(object):
def __init__(self, page):
self._page = page
def __init__(self, owner):
"""
:param owner: BasePage对象
"""
self._owner = owner
def NoneElement_value(self, value=None, on_off=True):
"""设置空元素是否返回设定值
@ -24,39 +30,42 @@ class BasePageSetter(object):
:param on_off: 是否启用
:return: None
"""
self._page._none_ele_return_value = on_off
self._page._none_ele_value = value
self._owner._none_ele_return_value = on_off
self._owner._none_ele_value = value
class ChromiumBaseSetter(BasePageSetter):
def __init__(self, page):
super().__init__(page)
def __init__(self, owner):
"""
:param owner: ChromiumBase对象
"""
super().__init__(owner)
self._cookies_setter = None
@property
def load_mode(self):
"""返回用于设置页面加载策略的对象"""
return LoadMode(self._page)
return LoadMode(self._owner)
@property
def scroll(self):
"""返回用于设置页面滚动设置的对象"""
return PageScrollSetter(self._page.scroll)
return PageScrollSetter(self._owner.scroll)
@property
def cookies(self):
"""返回用于设置cookies的对象"""
if self._cookies_setter is None:
self._cookies_setter = CookiesSetter(self._page)
self._cookies_setter = CookiesSetter(self._owner)
return self._cookies_setter
def retry_times(self, times):
"""设置连接失败重连次数"""
self._page.retry_times = times
self._owner.retry_times = times
def retry_interval(self, interval):
"""设置连接失败重连间隔"""
self._page.retry_interval = interval
self._owner.retry_interval = interval
def timeouts(self, base=None, page_load=None, script=None, implicit=None):
"""设置超时时间,单位为秒
@ -67,14 +76,14 @@ class ChromiumBaseSetter(BasePageSetter):
"""
base = base if base is not None else implicit
if base is not None:
self._page.timeouts.base = base
self._page._timeout = base
self._owner.timeouts.base = base
self._owner._timeout = base
if page_load is not None:
self._page.timeouts.page_load = page_load
self._owner.timeouts.page_load = page_load
if script is not None:
self._page.timeouts.script = script
self._owner.timeouts.script = script
def user_agent(self, ua, platform=None):
"""为当前tab设置user agent只在当前tab有效
@ -85,7 +94,7 @@ class ChromiumBaseSetter(BasePageSetter):
keys = {'userAgent': ua}
if platform:
keys['platform'] = platform
self._page.run_cdp('Emulation.setUserAgentOverride', **keys)
self._owner.run_cdp('Emulation.setUserAgentOverride', **keys)
def session_storage(self, item, value):
"""设置或删除某项sessionStorage信息
@ -93,15 +102,15 @@ class ChromiumBaseSetter(BasePageSetter):
:param value: 项的值设置为False时删除该项
:return: None
"""
self._page.run_cdp_loaded('DOMStorage.enable')
i = self._page.run_cdp('Storage.getStorageKeyForFrame', frameId=self._page._frame_id)['storageKey']
self._owner.run_cdp_loaded('DOMStorage.enable')
i = self._owner.run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey']
if value is False:
self._page.run_cdp('DOMStorage.removeDOMStorageItem',
storageId={'storageKey': i, 'isLocalStorage': False}, key=item)
self._owner.run_cdp('DOMStorage.removeDOMStorageItem',
storageId={'storageKey': i, 'isLocalStorage': False}, key=item)
else:
self._page.run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': False},
key=item, value=value)
self._page.run_cdp_loaded('DOMStorage.disable')
self._owner.run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': False},
key=item, value=value)
self._owner.run_cdp_loaded('DOMStorage.disable')
def local_storage(self, item, value):
"""设置或删除某项localStorage信息
@ -109,38 +118,38 @@ class ChromiumBaseSetter(BasePageSetter):
:param value: 项的值设置为False时删除该项
:return: None
"""
self._page.run_cdp_loaded('DOMStorage.enable')
i = self._page.run_cdp('Storage.getStorageKeyForFrame', frameId=self._page._frame_id)['storageKey']
self._owner.run_cdp_loaded('DOMStorage.enable')
i = self._owner.run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey']
if value is False:
self._page.run_cdp('DOMStorage.removeDOMStorageItem',
storageId={'storageKey': i, 'isLocalStorage': True}, key=item)
self._owner.run_cdp('DOMStorage.removeDOMStorageItem',
storageId={'storageKey': i, 'isLocalStorage': True}, key=item)
else:
self._page.run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': True},
key=item, value=value)
self._page.run_cdp_loaded('DOMStorage.disable')
self._owner.run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': True},
key=item, value=value)
self._owner.run_cdp_loaded('DOMStorage.disable')
def upload_files(self, files):
"""等待上传的文件路径
:param files: 文件路径列表或字符串字符串时多个文件用回车分隔
:return: None
"""
if not self._page._upload_list:
self._page.driver.set_callback('Page.fileChooserOpened', self._page._onFileChooserOpened)
self._page.run_cdp('Page.setInterceptFileChooserDialog', enabled=True)
if not self._owner._upload_list:
self._owner.driver.set_callback('Page.fileChooserOpened', self._owner._onFileChooserOpened)
self._owner.run_cdp('Page.setInterceptFileChooserDialog', enabled=True)
if isinstance(files, str):
files = files.split('\n')
elif isinstance(files, Path):
files = (files, )
self._page._upload_list = [str(Path(i).absolute()) for i in files]
files = (files,)
self._owner._upload_list = [str(Path(i).absolute()) for i in files]
def headers(self, headers: dict) -> None:
def headers(self, headers) -> None:
"""设置固定发送的headers
:param headers: dict格式的headers数据
:return: None
"""
self._page.run_cdp('Network.enable')
self._page.run_cdp('Network.setExtraHTTPHeaders', headers=headers)
self._owner.run_cdp('Network.enable')
self._owner.run_cdp('Network.setExtraHTTPHeaders', headers=format_headers(headers))
def auto_handle_alert(self, on_off=True, accept=True):
"""设置是否启用自动处理弹窗
@ -148,7 +157,7 @@ class ChromiumBaseSetter(BasePageSetter):
:param accept: bool表示确定还是取消
:return: None
"""
self._page._alert.auto = accept if on_off else None
self._owner._alert.auto = accept if on_off else None
def blocked_urls(self, urls):
"""设置要忽略的url
@ -161,25 +170,28 @@ class ChromiumBaseSetter(BasePageSetter):
urls = (urls,)
if not isinstance(urls, (list, tuple)):
raise TypeError('urls需传入str、list或tuple类型。')
self._page.run_cdp('Network.enable')
self._page.run_cdp('Network.setBlockedURLs', urls=urls)
self._owner.run_cdp('Network.enable')
self._owner.run_cdp('Network.setBlockedURLs', urls=urls)
# --------------即将废弃---------------
@property
def load_strategy(self):
"""返回用于设置页面加载策略的对象"""
return LoadMode(self._page)
return LoadMode(self._owner)
class TabSetter(ChromiumBaseSetter):
def __init__(self, page):
super().__init__(page)
def __init__(self, owner):
"""
:param owner: 标签页对象
"""
super().__init__(owner)
@property
def window(self):
"""返回用于设置浏览器窗口的对象"""
return WindowSetter(self._page)
return WindowSetter(self._owner)
def download_path(self, path):
"""设置下载路径
@ -187,10 +199,10 @@ class TabSetter(ChromiumBaseSetter):
:return: None
"""
path = str(Path(path).absolute())
self._page._download_path = path
self._page.browser._dl_mgr.set_path(self._page, path)
if self._page._DownloadKit:
self._page._DownloadKit.set.goal_path(path)
self._owner._download_path = path
self._owner.browser._dl_mgr.set_path(self._owner, path)
if self._owner._DownloadKit:
self._owner._DownloadKit.set.goal_path(path)
def download_file_name(self, name=None, suffix=None):
"""设置下一个被下载文件的名称
@ -198,7 +210,7 @@ class TabSetter(ChromiumBaseSetter):
:param suffix: 后缀名显式设置后缀名不使用远程文件后缀
:return: None
"""
self._page.browser._dl_mgr.set_rename(self._page.tab_id, name, suffix)
self._owner.browser._dl_mgr.set_rename(self._owner.tab_id, name, suffix)
def when_download_file_exists(self, mode):
"""设置当存在同名文件时的处理方式
@ -211,11 +223,11 @@ class TabSetter(ChromiumBaseSetter):
if mode not in types:
raise ValueError(f'''mode参数只能是 '{"', '".join(types.keys())}' 之一,现在是:{mode}''')
self._page.browser._dl_mgr.set_file_exists(self._page.tab_id, mode)
self._owner.browser._dl_mgr.set_file_exists(self._owner.tab_id, mode)
def activate(self):
"""使标签页处于最前面"""
self._page.browser.activate_tab(self._page.tab_id)
self._owner.browser.activate_tab(self._owner.tab_id)
class ChromiumPageSetter(TabSetter):
@ -226,39 +238,46 @@ class ChromiumPageSetter(TabSetter):
:return: None
"""
if not tab_or_id:
tab_or_id = self._page.tab_id
tab_or_id = self._owner.tab_id
elif not isinstance(tab_or_id, str): # 传入Tab对象
tab_or_id = tab_or_id.tab_id
self._page.browser.activate_tab(tab_or_id)
self._owner.browser.activate_tab(tab_or_id)
@property
def window(self):
"""返回用于设置浏览器窗口的对象"""
return PageWindowSetter(self._page)
def auto_handle_alert(self, on_off=True, accept=True, all_tabs=False):
"""设置是否启用自动处理弹窗
:param on_off: bool表示开或关
:param accept: bool表示确定还是取消
:param all_tabs: 是否为全局设置
:return: None
"""
if all_tabs:
Settings.auto_handle_alert = on_off
else:
self._owner._alert.auto = accept if on_off else None
class SessionPageSetter(BasePageSetter):
def __init__(self, page):
def __init__(self, owner):
"""
:param page: SessionPage对象
:param owner: SessionPage对象
"""
super().__init__(page)
super().__init__(owner)
self._cookies_setter = None
@property
def cookies(self):
"""返回用于设置cookies的对象"""
if self._cookies_setter is None:
self._cookies_setter = SessionCookiesSetter(self._page)
self._cookies_setter = SessionCookiesSetter(self._owner)
return self._cookies_setter
def retry_times(self, times):
"""设置连接失败时重连次数"""
self._page.retry_times = times
self._owner.retry_times = times
def retry_interval(self, interval):
"""设置连接失败时重连间隔"""
self._page.retry_interval = interval
self._owner.retry_interval = interval
def download_path(self, path):
"""设置下载路径
@ -266,16 +285,16 @@ class SessionPageSetter(BasePageSetter):
:return: None
"""
path = str(Path(path).absolute())
self._page._download_path = path
if self._page._DownloadKit:
self._page._DownloadKit.set.goal_path(path)
self._owner._download_path = path
if self._owner._DownloadKit:
self._owner._DownloadKit.set.goal_path(path)
def timeout(self, second):
"""设置连接超时时间
:param second: 秒数
:return: None
"""
self._page.timeout = second
self._owner.timeout = second
def encoding(self, encoding, set_all=True):
"""设置编码
@ -284,16 +303,16 @@ class SessionPageSetter(BasePageSetter):
:return: None
"""
if set_all:
self._page._encoding = encoding if encoding else None
if self._page.response:
self._page.response.encoding = encoding
self._owner._encoding = encoding if encoding else None
if self._owner.response:
self._owner.response.encoding = encoding
def headers(self, headers):
"""设置通用的headers
:param headers: dict形式的headers
:return: None
"""
self._page._headers = CaseInsensitiveDict(headers)
self._owner._headers = CaseInsensitiveDict(format_headers(headers))
def header(self, name, value):
"""设置headers中一个项
@ -301,14 +320,14 @@ class SessionPageSetter(BasePageSetter):
:param value: 设置值
:return: None
"""
self._page._headers[name] = value
self._owner._headers[name] = value
def user_agent(self, ua):
"""设置user agent
:param ua: user agent
:return: None
"""
self._page._headers['user-agent'] = ua
self._owner._headers['user-agent'] = ua
def proxies(self, http=None, https=None):
"""设置proxies参数
@ -316,63 +335,63 @@ class SessionPageSetter(BasePageSetter):
:param https: https代理地址
:return: None
"""
self._page.session.proxies = {'http': http, 'https': https}
self._owner.session.proxies = {'http': http, 'https': https}
def auth(self, auth):
"""设置认证元组或对象
:param auth: 认证元组或对象
:return: None
"""
self._page.session.auth = auth
self._owner.session.auth = auth
def hooks(self, hooks):
"""设置回调方法
:param hooks: 回调方法
:return: None
"""
self._page.session.hooks = hooks
self._owner.session.hooks = hooks
def params(self, params):
"""设置查询参数字典
:param params: 查询参数字典
:return: None
"""
self._page.session.params = params
self._owner.session.params = params
def verify(self, on_off):
"""设置是否验证SSL证书
:param on_off: 是否验证 SSL 证书
:return: None
"""
self._page.session.verify = on_off
self._owner.session.verify = on_off
def cert(self, cert):
"""SSL客户端证书文件的路径(.pem格式),或(cert, key)元组
:param cert: 证书路径或元组
:return: None
"""
self._page.session.cert = cert
self._owner.session.cert = cert
def stream(self, on_off):
"""设置是否使用流式响应内容
:param on_off: 是否使用流式响应内容
:return: None
"""
self._page.session.stream = on_off
self._owner.session.stream = on_off
def trust_env(self, on_off):
"""设置是否信任环境
:param on_off: 是否信任环境
:return: None
"""
self._page.session.trust_env = on_off
self._owner.session.trust_env = on_off
def max_redirects(self, times):
"""设置最大重定向次数
:param times: 最大重定向次数
:return: None
"""
self._page.session.max_redirects = times
self._owner.session.max_redirects = times
def add_adapter(self, url, adapter):
"""添加适配器
@ -380,20 +399,20 @@ class SessionPageSetter(BasePageSetter):
:param adapter: 适配器对象
:return: None
"""
self._page.session.mount(url, adapter)
self._owner.session.mount(url, adapter)
class WebPageSetter(ChromiumPageSetter):
def __init__(self, page):
super().__init__(page)
self._session_setter = SessionPageSetter(self._page)
self._chromium_setter = ChromiumPageSetter(self._page)
def __init__(self, owner):
super().__init__(owner)
self._session_setter = SessionPageSetter(self._owner)
self._chromium_setter = ChromiumPageSetter(self._owner)
@property
def cookies(self):
"""返回用于设置cookies的对象"""
if self._cookies_setter is None:
self._cookies_setter = WebPageCookiesSetter(self._page)
self._cookies_setter = WebPageCookiesSetter(self._owner)
return self._cookies_setter
def headers(self, headers) -> None:
@ -401,30 +420,30 @@ class WebPageSetter(ChromiumPageSetter):
:param headers: dict格式的headers数据
:return: None
"""
if self._page.mode == 's':
if self._owner.mode == 's':
self._session_setter.headers(headers)
else:
self._chromium_setter.headers(headers)
def user_agent(self, ua, platform=None):
"""设置user agentd模式下只有当前tab有效"""
if self._page.mode == 's':
if self._owner.mode == 's':
self._session_setter.user_agent(ua)
else:
self._chromium_setter.user_agent(ua, platform)
class WebPageTabSetter(TabSetter):
def __init__(self, page):
super().__init__(page)
self._session_setter = SessionPageSetter(self._page)
self._chromium_setter = ChromiumBaseSetter(self._page)
def __init__(self, owner):
super().__init__(owner)
self._session_setter = SessionPageSetter(self._owner)
self._chromium_setter = ChromiumBaseSetter(self._owner)
@property
def cookies(self):
"""返回用于设置cookies的对象"""
if self._cookies_setter is None:
self._cookies_setter = WebPageCookiesSetter(self._page)
self._cookies_setter = WebPageCookiesSetter(self._owner)
return self._cookies_setter
def headers(self, headers) -> None:
@ -432,16 +451,16 @@ class WebPageTabSetter(TabSetter):
:param headers: dict格式的headers数据
:return: None
"""
if self._page._has_session:
if self._owner._has_session:
self._session_setter.headers(headers)
if self._page._has_driver:
if self._owner._has_driver:
self._chromium_setter.headers(headers)
def user_agent(self, ua, platform=None):
"""设置user agentd模式下只有当前tab有效"""
if self._page._has_session:
if self._owner._has_session:
self._session_setter.user_agent(ua)
if self._page._has_driver:
if self._owner._has_driver:
self._chromium_setter.user_agent(ua, platform)
@ -458,7 +477,13 @@ class ChromiumElementSetter(object):
:param value: 属性值
:return: None
"""
self._ele.owner.run_cdp('DOM.setAttributeValue', nodeId=self._ele._node_id, name=name, value=str(value))
try:
self._ele.owner.run_cdp('DOM.setAttributeValue',
nodeId=self._ele._node_id, name=name, value=str(value))
except ElementLostError:
self._ele._refresh_id()
self._ele.owner.run_cdp('DOM.setAttributeValue',
nodeId=self._ele._node_id, name=name, value=str(value))
def property(self, name, value):
"""设置元素property属性
@ -491,17 +516,17 @@ class ChromiumFrameSetter(ChromiumBaseSetter):
:param value: 属性值
:return: None
"""
self._page.frame_ele.set.attr(name, value)
self._owner.frame_ele.set.attr(name, value)
class LoadMode(object):
"""用于设置页面加载策略的类"""
def __init__(self, page):
def __init__(self, owner):
"""
:param page: ChromiumBase对象
:param owner: ChromiumBase对象
"""
self._page = page
self._owner = owner
def __call__(self, value):
"""设置加载策略
@ -510,23 +535,26 @@ class LoadMode(object):
"""
if value.lower() not in ('normal', 'eager', 'none'):
raise ValueError("只能选择 'normal', 'eager', 'none'")
self._page._load_mode = value
self._owner._load_mode = value
def normal(self):
"""设置页面加载策略为normal"""
self._page._load_mode = 'normal'
self._owner._load_mode = 'normal'
def eager(self):
"""设置页面加载策略为eager"""
self._page._load_mode = 'eager'
self._owner._load_mode = 'eager'
def none(self):
"""设置页面加载策略为none"""
self._page._load_mode = 'none'
self._owner._load_mode = 'none'
class PageScrollSetter(object):
def __init__(self, scroll):
"""
:param scroll: PageScroller对象
"""
self._scroll = scroll
def wait_complete(self, on_off=True):
@ -553,11 +581,11 @@ class PageScrollSetter(object):
class WindowSetter(object):
"""用于设置窗口大小的类"""
def __init__(self, page):
def __init__(self, owner):
"""
:param page: 页面对象
:param owner: 页面对象
"""
self._page = page
self._owner = owner
self._window_id = self._get_info()['windowId']
def max(self):
@ -620,7 +648,7 @@ class WindowSetter(object):
"""获取窗口位置及大小信息"""
for _ in range(50):
try:
return self._page.run_cdp('Browser.getWindowForTarget')
return self._owner.run_cdp('Browser.getWindowForTarget')
except:
sleep(.1)
@ -630,7 +658,7 @@ class WindowSetter(object):
:return: None
"""
try:
self._page.run_cdp('Browser.setWindowBounds', windowId=self._window_id, bounds=bounds)
self._owner.run_cdp('Browser.setWindowBounds', windowId=self._window_id, bounds=bounds)
except:
raise RuntimeError('浏览器全屏或最小化状态时请先调用set.window.normal()恢复正常状态。')
@ -648,12 +676,10 @@ class WindowSetter(object):
"""设置窗口为全屏"""
self.full()
class PageWindowSetter(WindowSetter):
def hide(self):
"""隐藏浏览器窗口只在Windows系统可用"""
show_or_hide_browser(self._page, hide=True)
show_or_hide_browser(self._owner, hide=True)
def show(self):
"""显示浏览器窗口只在Windows系统可用"""
show_or_hide_browser(self._page, hide=False)
show_or_hide_browser(self._owner, hide=False)

View File

@ -6,7 +6,7 @@
@License : BSD 3-Clause.
"""
from pathlib import Path
from typing import Union, Tuple, Literal, Any
from typing import Union, Tuple, Literal, Any, Optional
from requests.adapters import HTTPAdapter
from requests.auth import HTTPBasicAuth
@ -18,7 +18,7 @@ from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage
from .._pages.chromium_tab import ChromiumTab
from .._pages.chromium_tab import ChromiumTab, WebPageTab
from .._pages.session_page import SessionPage
from .._pages.web_page import WebPage
@ -26,15 +26,15 @@ FILE_EXISTS = Literal['skip', 'rename', 'overwrite', 's', 'r', 'o']
class BasePageSetter(object):
def __init__(self, page: BasePage):
self._page: BasePage = ...
def __init__(self, owner: BasePage):
self._owner: BasePage = ...
def NoneElement_value(self, value: Any = None, on_off: bool = True) -> None: ...
class ChromiumBaseSetter(BasePageSetter):
def __init__(self, page):
self._page: ChromiumBase = ...
def __init__(self, owner):
self._owner: ChromiumBase = ...
self._cookies_setter: CookiesSetter = ...
@property
@ -58,7 +58,7 @@ class ChromiumBaseSetter(BasePageSetter):
def local_storage(self, item: str, value: Union[str, bool]) -> None: ...
def headers(self, headers: dict) -> None: ...
def headers(self, headers: Union[dict, str]) -> None: ...
def auto_handle_alert(self, on_off: bool = True, accept: bool = True) -> None: ...
@ -68,7 +68,9 @@ class ChromiumBaseSetter(BasePageSetter):
class TabSetter(ChromiumBaseSetter):
def __init__(self, page): ...
_owner: ChromiumTab = ...
def __init__(self, owner: Union[ChromiumTab, WebPageTab, WebPage, ChromiumPage]): ...
@property
def window(self) -> WindowSetter: ...
@ -83,20 +85,18 @@ class TabSetter(ChromiumBaseSetter):
class ChromiumPageSetter(TabSetter):
_page: ChromiumPage = ...
@property
def window(self) -> PageWindowSetter: ...
def main_tab(self, tab_id: str = None) -> None: ...
_owner: ChromiumPage = ...
def tab_to_front(self, tab_or_id: Union[str, ChromiumTab] = None) -> None: ...
def auto_handle_alert(self, on_off: bool = True, accept: bool = True, all_tabs: bool = False) -> None: ...
class SessionPageSetter(BasePageSetter):
def __init__(self, page: SessionPage):
self._page: SessionPage = ...
self._cookies_setter: SessionCookiesSetter = ...
_owner: SessionPage = ...
_cookies_setter: Optional[SessionCookiesSetter] = ...
def __init__(self, owner: SessionPage): ...
@property
def cookies(self) -> SessionCookiesSetter: ...
@ -111,7 +111,7 @@ class SessionPageSetter(BasePageSetter):
def encoding(self, encoding: Union[str, None], set_all: bool = True) -> None: ...
def headers(self, headers: dict) -> None: ...
def headers(self, headers: Union[str, dict]) -> None: ...
def header(self, name: str, value: str) -> None: ...
@ -139,26 +139,26 @@ class SessionPageSetter(BasePageSetter):
class WebPageSetter(ChromiumPageSetter):
_page: WebPage = ...
_owner: WebPage = ...
_session_setter: SessionPageSetter = ...
_chromium_setter: ChromiumPageSetter = ...
def user_agent(self, ua: str, platform: str = None) -> None: ...
def headers(self, headers: dict) -> None: ...
def headers(self, headers: Union[str, dict]) -> None: ...
@property
def cookies(self) -> WebPageCookiesSetter: ...
class WebPageTabSetter(TabSetter):
_page: WebPage = ...
_owner: WebPageTab = ...
_session_setter: SessionPageSetter = ...
_chromium_setter: ChromiumBaseSetter = ...
def user_agent(self, ua: str, platform: str = None) -> None: ...
def headers(self, headers: dict) -> None: ...
def headers(self, headers: Union[str, dict]) -> None: ...
@property
def cookies(self) -> WebPageCookiesSetter: ...
@ -178,14 +178,14 @@ class ChromiumElementSetter(object):
class ChromiumFrameSetter(ChromiumBaseSetter):
_page: ChromiumFrame = ...
_owner: ChromiumFrame = ...
def attr(self, name: str, value: str) -> None: ...
class LoadMode(object):
def __init__(self, page: ChromiumBase):
self._page: ChromiumBase = ...
def __init__(self, owner: ChromiumBase):
self._owner: ChromiumBase = ...
def __call__(self, value: str) -> None: ...
@ -206,8 +206,8 @@ class PageScrollSetter(object):
class WindowSetter(object):
def __init__(self, page: ChromiumBase):
self._page: ChromiumBase = ...
def __init__(self, owner: ChromiumBase):
self._owner: ChromiumBase = ...
self._window_id: str = ...
def max(self) -> None: ...
@ -226,10 +226,6 @@ class WindowSetter(object):
def _perform(self, bounds: dict) -> None: ...
class PageWindowSetter(WindowSetter):
_page: ChromiumPage = ...
def hide(self) -> None: ...
def show(self) -> None: ...

View File

@ -105,22 +105,22 @@ class ShadowRootStates(object):
class PageStates(object):
"""Page对象、Tab对象使用"""
def __init__(self, page):
def __init__(self, owner):
"""
:param page: ChromiumBase对象
:param owner: ChromiumBase对象
"""
self._page = page
self._owner = owner
@property
def is_loading(self):
"""返回页面是否在加载状态"""
return self._page._is_loading
return self._owner._is_loading
@property
def is_alive(self):
"""返回页面对象是否仍然可用"""
try:
self._page.run_cdp('Page.getLayoutMetrics')
self._owner.run_cdp('Page.getLayoutMetrics')
return True
except PageDisconnectedError:
return False
@ -128,12 +128,12 @@ class PageStates(object):
@property
def ready_state(self):
"""返回当前页面加载状态,'connecting' 'loading' 'interactive' 'complete'"""
return self._page._ready_state
return self._owner._ready_state
@property
def has_alert(self):
"""返回当前页面是否存在弹窗"""
return self._page._has_alert
return self._owner._has_alert
class FrameStates(object):

View File

@ -59,8 +59,8 @@ class ShadowRootStates(object):
class PageStates(object):
def __init__(self, page: ChromiumBase):
self._page: ChromiumBase = ...
def __init__(self, owner: ChromiumBase):
self._owner: ChromiumBase = ...
@property
def is_loading(self) -> bool: ...

View File

@ -7,17 +7,12 @@
"""
from time import sleep, perf_counter
from .._functions.locator import get_loc
from .._functions.settings import Settings
from ..errors import WaitTimeoutError, NoRectError
class BaseWaiter(object):
def __init__(self, page_or_ele):
"""
:param page_or_ele: 页面对象或元素对象
"""
self._driver = page_or_ele
class OriginWaiter(object):
def __call__(self, second, scope=None):
"""等待若干秒,如传入两个参数,等待时间为这两个数间的一个随机数
:param second: 秒数
@ -30,6 +25,14 @@ class BaseWaiter(object):
from random import uniform
sleep(uniform(second, scope))
class BaseWaiter(OriginWaiter):
def __init__(self, page_or_ele):
"""
:param page_or_ele: 页面对象或元素对象
"""
self._driver = page_or_ele
def ele_deleted(self, loc_or_ele, timeout=None, raise_err=None):
"""等待元素从DOM中删除
:param loc_or_ele: 要等待的元素可以是已有元素定位符
@ -78,18 +81,53 @@ class BaseWaiter(object):
return False
return ele.wait.hidden(timeout, raise_err=raise_err)
def ele_loaded(self, locator, timeout=None, raise_err=None):
"""等待元素加载到DOM
:param locator: 要等待的元素输入定位符
def eles_loaded(self, locators, timeout=None, any_one=False, raise_err=None):
"""等待元素加载到DOM,可等待全部或任意一个
:param locators: 要等待的元素输入定位符用list输入多个
:param timeout: 超时时间默认读取页面超时时间
:param any_one: 是否等待到一个就返回
:param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 成功返回元素对象失败返回False
:return: 成功返回True失败返回False
"""
ele = self._driver._ele(locator, raise_err=False, timeout=timeout)
if ele:
return ele
def _find(loc, driver):
r = driver.run('DOM.performSearch', query=loc, includeUserAgentShadowDOM=True)
if not r or 'error' in r:
return False
elif r['resultCount'] == 0:
driver.run('DOM.discardSearchResults', searchId=r['searchId'])
return False
searchId = r['searchId']
ids = driver.run('DOM.getSearchResults', searchId=searchId, fromIndex=0,
toIndex=r['resultCount'])
if 'error' in ids:
return False
ids = ids['nodeIds']
res = False
for i in ids:
r = driver.run('DOM.describeNode', nodeId=i)
if 'error' in r or r['node']['nodeName'] in ('#text', '#comment'):
continue
else:
res = True
break
driver.run('DOM.discardSearchResults', searchId=searchId)
return res
by = ('id', 'xpath', 'link text', 'partial link text', 'name', 'tag name', 'class name', 'css selector')
locators = ((get_loc(locators)[1],) if (isinstance(locators, str) or isinstance(locators, tuple)
and locators[0] in by and len(locators) == 2)
else [get_loc(l)[1] for l in locators])
timeout = self._driver.timeout if timeout is None else timeout
end_time = perf_counter() + timeout
method = any if any_one else all
while perf_counter() < end_time:
if method([_find(l, self._driver.driver) for l in locators]):
return True
sleep(.01)
if raise_err is True or Settings.raise_when_wait_failed is True:
raise WaitTimeoutError(f'等待元素加载失败(等待{timeout}秒)。')
raise WaitTimeoutError(f'等待元素{locators}加载失败(等待{timeout}秒)。')
else:
return False
@ -125,7 +163,7 @@ class BaseWaiter(object):
:return: 成功返回任务对象失败返回False
"""
if not self._driver.browser._dl_mgr._running:
raise RuntimeError('使用下载管理功能前需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
self._driver.browser._dl_mgr.set_flag(self._driver.tab_id, False if cancel_it else True)
if timeout is None:
timeout = self._driver.timeout
@ -137,6 +175,7 @@ class BaseWaiter(object):
if not isinstance(v, bool):
r = v
break
sleep(.005)
self._driver.browser._dl_mgr.set_flag(self._driver.tab_id, None)
return r
@ -230,6 +269,21 @@ class BaseWaiter(object):
"""
return self._loading(timeout=timeout, start=False, raise_err=raise_err)
def ele_loaded(self, locator, timeout=None, raise_err=None):
"""等待元素加载到DOM
:param locator: 要等待的元素输入定位符
:param timeout: 超时时间默认读取页面超时时间
:param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 成功返回元素对象失败返回False
"""
ele = self._driver._ele(locator, raise_err=False, timeout=timeout)
if ele:
return ele
if raise_err is True or Settings.raise_when_wait_failed is True:
raise WaitTimeoutError(f'等待元素加载失败(等待{timeout}秒)。')
else:
return False
class TabWaiter(BaseWaiter):
@ -240,7 +294,7 @@ class TabWaiter(BaseWaiter):
:return: 是否等待成功
"""
if not self._driver.browser._dl_mgr._running:
raise RuntimeError('使用下载管理功能前需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
if not timeout:
while self._driver.browser._dl_mgr.get_tab_missions(self._driver.tab_id):
sleep(.5)
@ -272,7 +326,6 @@ class TabWaiter(BaseWaiter):
class PageWaiter(TabWaiter):
def __init__(self, page):
super().__init__(page)
# self._listener = None
def new_tab(self, timeout=None, raise_err=None):
"""等待新标签页出现
@ -283,9 +336,9 @@ class PageWaiter(TabWaiter):
timeout = timeout if timeout is not None else self._driver.timeout
end_time = perf_counter() + timeout
while perf_counter() < end_time:
latest_tab = self._driver.latest_tab
if self._driver.tab_id != latest_tab:
return latest_tab
latest_tid = self._driver.tab_ids[0]
if self._driver.tab_id != latest_tid:
return latest_tid
sleep(.01)
if raise_err is True or Settings.raise_when_wait_failed is True:
@ -300,7 +353,7 @@ class PageWaiter(TabWaiter):
:return: 是否等待成功
"""
if not self._driver.browser._dl_mgr._running:
raise RuntimeError('使用下载管理功能前需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
if not timeout:
while self._driver.browser._dl_mgr._missions:
sleep(.5)
@ -322,29 +375,17 @@ class PageWaiter(TabWaiter):
return True
class ElementWaiter(object):
class ElementWaiter(OriginWaiter):
"""等待元素在dom中某种状态如删除、显示、隐藏"""
def __init__(self, page, ele):
def __init__(self, owner, ele):
"""等待元素在dom中某种状态如删除、显示、隐藏
:param page: 元素所在页面
:param owner: 元素所在页面
:param ele: 要等待的元素
"""
self._page = page
self._owner = owner
self._ele = ele
def __call__(self, second, scope=None):
"""等待若干秒,如传入两个参数,等待时间为这两个数间的一个随机数
:param second: 秒数
:param scope: 随机数范围
:return: None
"""
if scope is None:
sleep(second)
else:
from random import uniform
sleep(uniform(second, scope))
def deleted(self, timeout=None, raise_err=None):
"""等待元素从dom删除
:param timeout: 超时时间为None使用元素所在页面timeout属性
@ -408,7 +449,7 @@ class ElementWaiter(object):
:return: 是否等待成功
"""
if timeout is None:
timeout = self._page.timeout
timeout = self._owner.timeout
end_time = perf_counter() + timeout
while perf_counter() < end_time:
if not self._ele.states.is_enabled or not self._ele.states.is_alive:
@ -428,7 +469,7 @@ class ElementWaiter(object):
:return: 是否等待成功
"""
if timeout is None:
timeout = self._page.timeout
timeout = self._owner.timeout
end_time = perf_counter() + timeout
while perf_counter() < end_time:
try:
@ -437,6 +478,7 @@ class ElementWaiter(object):
break
except NoRectError:
pass
sleep(.005)
else:
raise NoRectError
@ -458,7 +500,7 @@ class ElementWaiter(object):
:param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功
"""
return self._wait_state('has_rect', True, timeout, raise_err, err_text='等待元素拥有大小及位置属性失败(等{}秒)。')
return self._wait_state('has_rect', True, timeout, raise_err, err_text='等待元素拥有大小及位置失败(等{}秒)。')
def _wait_state(self, attr, mode=False, timeout=None, raise_err=None, err_text=None):
"""等待元素某个元素状态到达指定状态
@ -471,7 +513,7 @@ class ElementWaiter(object):
"""
err_text = err_text or '等待元素状态改变失败(等待{}秒)。'
if timeout is None:
timeout = self._page.timeout
timeout = self._owner.timeout
end_time = perf_counter() + timeout
while perf_counter() < end_time:
a = self._ele.states.__getattribute__(attr)

View File

@ -14,7 +14,11 @@ from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage
class BaseWaiter(object):
class OriginWaiter(object):
def __call__(self, second: float, scope: float = None) -> None: ...
class BaseWaiter(OriginWaiter):
def __init__(self, page: ChromiumBase):
self._driver: ChromiumBase = ...
@ -33,10 +37,11 @@ class BaseWaiter(object):
def ele_hidden(self, loc_or_ele: Union[str, tuple, ChromiumElement], timeout: float = None,
raise_err: bool = None) -> bool: ...
def ele_loaded(self,
locator: Union[Tuple[str, str], str],
timeout: float = None,
raise_err: bool = None) -> Union[bool, ChromiumElement]: ...
def eles_loaded(self,
locators: Union[Tuple[str, str], str, list, tuple],
timeout: float = None,
any_one: bool = False,
raise_err: bool = None) -> bool: ...
def _loading(self, timeout: float = None, start: bool = True, gap: float = .01, raise_err: bool = None) -> bool: ...
@ -73,10 +78,10 @@ class PageWaiter(TabWaiter):
def all_downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ...
class ElementWaiter(object):
def __init__(self, page: ChromiumBase, ele: ChromiumElement):
class ElementWaiter(OriginWaiter):
def __init__(self, owner: ChromiumBase, ele: ChromiumElement):
self._ele: ChromiumElement = ...
self._page: ChromiumBase = ...
self._owner: ChromiumBase = ...
def __call__(self, second: float, scope: float = None) -> None: ...

View File

@ -11,6 +11,41 @@ from ._functions.keys import Keys
from ._functions.settings import Settings
from ._functions.tools import wait_until, configs_to_here
from ._functions.web import get_blob, tree
from ._pages.chromium_page import ChromiumPage
from ._units.actions import Actions
__all__ = ['make_session_ele', 'Actions', 'Keys', 'By', 'Settings', 'wait_until', 'configs_to_here', 'get_blob', 'tree']
__all__ = ['make_session_ele', 'Actions', 'Keys', 'By', 'Settings', 'wait_until', 'configs_to_here', 'get_blob',
'tree', 'from_selenium', 'from_playwright']
def from_selenium(driver):
"""从selenium的WebDriver对象生成ChromiumPage对象"""
address, port = driver.caps.get('goog:chromeOptions', {}).get('debuggerAddress', ':').split(':')
if not address:
raise RuntimeError('获取失败。')
return ChromiumPage(f'{address}:{port}')
def from_playwright(page_or_browser):
"""从playwright的Page或Browser对象生成ChromiumPage对象"""
if hasattr(page_or_browser, 'context'):
page_or_browser = page_or_browser.context.browser
try:
processes = page_or_browser.new_browser_cdp_session().send('SystemInfo.getProcessInfo')['processInfo']
for process in processes:
if process['type'] == 'browser':
pid = process['id']
break
else:
raise RuntimeError('获取失败。')
except:
raise RuntimeError('获取失败。')
from psutil import net_connections
for con_info in net_connections():
if con_info.pid == pid:
port = con_info.laddr.port
break
else:
raise RuntimeError('获取失败。')
return ChromiumPage(f'127.0.0.1:{port}')

103
README.en.md Normal file
View File

@ -0,0 +1,103 @@
# ✨️ Overview
DrissionPage is a python-based web page automation tool.
It can control the browser, send and receive data packets, and combine the two into one.
It can take into account the convenience of browser automation and the high efficiency of requests.
It is powerful and has countless built-in user-friendly designs and convenient functions.
Its syntax is concise and elegant, the amount of code is small, and it is friendly to novices.
---
<a href='https://gitee.com/g1879/DrissionPage/stargazers'><img src='https://gitee.com/g1879/DrissionPage/badge/star.svg?theme=dark' alt=' star'></img></a>
Project address: [gitee](https://gitee.com/g1879/DrissionPage) | [github](https://github.com/g1879/DrissionPage)
Your star is my greatest support💖
---
Supported systems: Windows, Linux, Mac
python version: 3.6 and above
Supported browsers: Chromium core browsers (such as Chrome and Edge), electron applications
---
# 🛠 How to use
**📖 Usage documentation:** [Click to view](https://drissionpage.cn)
**Communication QQ group:** 636361957
---
# 📕 background
When using requests for data collection, when facing a website to log in to, you have to analyze data packets and JS source code, construct complex requests, and often have to deal with anti-crawling methods such as verification codes, JS obfuscation, and signature parameters. The threshold is high and the development efficiency is low. high.
Using a browser can largely bypass these pitfalls, but the browser is not very efficient.
Therefore, the original intention of this library is to combine them into one and achieve "fast writing" and "fast running" at the same time. It can switch the corresponding mode when different needs are needed, and provide a humanized usage method to improve development and operation efficiency.
In addition to merging the two, this library also encapsulates commonly used functions in web page units, providing very simple operations and statements, allowing users to reduce considerations of details and focus on function implementation. Implement powerful functions in a simple way and make your code more elegant.
The previous version was implemented by repackaging selenium. Starting from 3.0, the author started from scratch, redeveloped the bottom layer, got rid of the dependence on selenium, enhanced functions, and improved operating efficiency.
---
# 💡 Concept
Simple yet powerful!
---
# ☀️ Features and Highlights
After long-term practice, the author has stepped through countless pitfalls, and all the experiences he has summarized have been written down in this library.
## 🎇 Powerful self-developed core
This library uses a fully self-developed kernel, has built-in N number of practical functions, and has integrated and optimized common functions. Compared with selenium, it has the following advantages:
- Not base on webdriver
- No need to download different drivers for different browser versions
- Runs faster
- Can find elements across `<iframe>` without switching in and out
- Treat `<iframe>` as a normal element. After obtaining it, you can directly search for elements in it, making the logic clearer.
- You can operate multiple tabs in the browser at the same time, even if the tab is inactive, no need to switch
- Can directly read the browser cache to save images without using the GUI to click save
- You can take screenshots of the entire web page, including parts outside the viewport (supported by browsers 90 and above)
- Can handle shadow-root in non-open state
## 🎇 Highlighted features
In addition to the above advantages, this library also has numerous built-in humanized designs.
- Minimalist grammar rules. Integrate a large number of commonly used functions to make the code more elegant
- Positioning elements is easier and the function is more powerful and stable
- Ubiquitous wait and auto-retry functionality. Make unstable networks easier to control, programs more stable, and writing more worry-free
- Provide powerful download tools. You can also enjoy fast and reliable download functions when operating the browser
- Allows repeated use of already open browsers. No need to start the browser from scratch every time, making debugging very convenient
- Use ini files to save commonly used configurations and call them automatically, providing convenient settings and staying away from complicated configuration items.
- Built-in lxml as a parsing engine, the parsing speed is improved by several orders of magnitude
- Encapsulated using POM mode, which can be directly used for testing and easy to expand.
- Highly integrated convenient functions, reflected in every detail
- There are many details, so I wont list them all here. You are welcome to experience them in actual use:)
---
# 🖐🏻 Disclaimer
Please do not apply DrissionPage to any work that may violate legal regulations and moral constraints. Please use DrissionPage in a friendly manner, comply with the spider agreement, and do not use DrissionPage for any illegal purposes. If you choose to use DrissionPage
This means that you abide by this agreement. The author does not bear any legal risks and losses caused by your violation of this agreement. You will be responsible for all consequences.
---
# ☕ Buy me coffee
If this project is helpful to you, why not buy the author a cup of coffee :)
![](https://gitee.com/g1879/DrissionPageDocs/raw/master/static/img/code.jpg)

View File

@ -12,6 +12,8 @@ DrissionPage 是一个基于 python 的网页自动化工具。
---
官方网站:[https://drissionpage.cn](https://drissionpage.cn)
<a href='https://gitee.com/g1879/DrissionPage/stargazers'><img src='https://gitee.com/g1879/DrissionPage/badge/star.svg?theme=dark' alt='star'></img></a> <a href='https://gitee.com/g1879/DrissionPage/members'><img src='https://gitee.com/g1879/DrissionPage/badge/fork.svg?theme=dark' alt='fork'></img></a>
项目地址:[gitee](https://gitee.com/g1879/DrissionPage) | [github](https://github.com/g1879/DrissionPage)
@ -36,18 +38,6 @@ python 版本3.6 及以上
---
# 📕 背景
用 requests 做数据采集面对要登录的网站时要分析数据包、JS 源码构造复杂的请求往往还要应付验证码、JS 混淆、签名参数等反爬手段,门槛较高,开发效率不高。
使用浏览器,可以很大程度上绕过这些坑,但浏览器运行效率不高。
因此,这个库设计初衷,是将它们合而为一,同时实现“写得快”和“跑得快”。能够在不同需要时切换相应模式,并提供一种人性化的使用方法,提高开发和运行效率。
除了合并两者,本库还以网页为单位封装了常用功能,提供非常简便的操作和语句,使用户可减少考虑细节,专注功能实现。 以简单的方式实现强大的功能,使代码更优雅。
以前的版本是对 selenium 进行重新封装实现的。从 3.0 开始,作者另起炉灶,对底层进行了重新开发,摆脱对 selenium 的依赖,增强了功能,提升了运行效率。
---
# 💡 理念
简洁而强大!
@ -60,24 +50,16 @@ python 版本3.6 及以上
## 🎇 强大的自研内核
本库采用全自研的内核,内置了 N 多实用功能,对常用功能作了整合和优化,对比 selenium有以下优点
- 无 webdriver 特征
本库采用全自研的内核,内置无数实用功能,对常用功能作了整合和优化,对比 selenium有以下优点
- 不基于 webdriver
- 无需为不同版本的浏览器下载不同的驱动
- 运行速度更快
- 可以跨`<iframe>`查找元素,无需切入切出
- 把`<iframe>`看作普通元素,获取后可直接在其中查找元素,逻辑更清晰
- 可以同时操作浏览器中的多个标签页,即使标签页为非激活状态,无需切换
- 可以直接读取浏览器缓存来保存图片,无需用 GUI 点击另存
- 可以对整个网页截图包括视口外的部分90以上版本浏览器支持
- 可处理非`open`状态的 shadow-root
## 🎇 亮点功能
@ -85,37 +67,24 @@ python 版本3.6 及以上
除了以上优点,本库还内置了无数人性化设计。
- 极简的语法规则。集成大量常用功能,代码更优雅
- 定位元素更加容易,功能更强大稳定
- 无处不在的等待和自动重试功能。使不稳定的网络变得易于控制,程序更稳定,编写更省心
- 提供强大的下载工具。操作浏览器时也能享受快捷可靠的下载功能
- 允许反复使用已经打开的浏览器。无须每次运行从头启动浏览器,调试超方便
- 使用 ini 文件保存常用配置,自动调用,提供便捷的设置,远离繁杂的配置项
- 内置 lxml 作为解析引擎,解析速度成几个数量级提升
- 使用 POM 模式封装,可直接用于测试,便于扩展
- 高度集成的便利功能,从每个细节中体现
- 还有很多细节,这里不一一列举,欢迎实际使用中体验:)
---
# 🔖 版本历史
[点击查看版本历史](https://g1879.gitee.io/drissionpagedocs/history/introduction/)
---
# 🖐🏻 免责声明
请勿将 DrissionPage 应用到任何可能会违反法律规定和道德约束的工作中,请友善使用 DrissionPage遵守蜘蛛协议不要将 DrissionPage 用于任何非法用途。如您选择使用 DrissionPage
即代表您遵守此协议,作者不承担任何由于您违反此协议带来任何的法律风险和损失,一切后果由您承担。
禁止将 DrissionPage 应用到任何可能会违反法律规定和道德约束的项目中。
友善使用 DrissionPage遵守蜘蛛协议禁止将 DrissionPage 用于任何可能有损他人的项目中。
如您选择使用 DrissionPage 即代表您遵守此协议,作者不承担任何由于您违反此协议带来任何的法律风险和损失。
同时,作者不对 DrissionPage 可能存在的缺陷导致的损失承担任何责任,一切后果由您承担。
---
@ -123,4 +92,4 @@ python 版本3.6 及以上
如果本项目对您有所帮助,不妨请作者我喝杯咖啡
![](https://g1879.gitee.io/drissionpagedocs/assets/images/code-cf68de50a2f331a2aa0d39c9aebbbe2c.jpg)
![](https://gitee.com/g1879/DrissionPageDocs/raw/master/static/img/code.jpg)