diff --git a/.gitee/ISSUE_TEMPLATE.zh-CN.md b/.gitee/ISSUE_TEMPLATE.zh-CN.md index fc92e66..c6e48e4 100644 --- a/.gitee/ISSUE_TEMPLATE.zh-CN.md +++ b/.gitee/ISSUE_TEMPLATE.zh-CN.md @@ -1,3 +1,11 @@ -1. 使用上的问题请先查看文档[使用文档](http://g1879.gitee.io/drissionpagedocs) -2. 遇到bug请详细描述如何重现,并附上代码 -3. 提问前先给本库打个星,谢谢 \ No newline at end of file +在提交issue前,请确认已经给本库点了星星,这对我来说很重要。 + +使用方法请查看[使用文档](http://drissionpage.cn),文档里都有。 +也可在QQ群里提问(636361957)。 + +请围绕以下内容陈述您的问题: + +1. 遇到了什么问题?什么场景下出现的?如何重现? +2. 请附上代码和报错信息(如有) +3. DrissionPage、浏览器、python版本号是多少? +4. 有什么意见建议? diff --git a/DrissionPage/__init__.py b/DrissionPage/__init__.py index 40056cf..a071967 100644 --- a/DrissionPage/__init__.py +++ b/DrissionPage/__init__.py @@ -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' diff --git a/DrissionPage/_base/browser.py b/DrissionPage/_base/browser.py index 4ab366d..c877013 100644 --- a/DrissionPage/_base/browser.py +++ b/DrissionPage/_base/browser.py @@ -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) diff --git a/DrissionPage/_base/browser.pyi b/DrissionPage/_base/browser.pyi index 4f44b8c..170f88b 100644 --- a/DrissionPage/_base/browser.pyi +++ b/DrissionPage/_base/browser.pyi @@ -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: ... diff --git a/DrissionPage/_base/driver.py b/DrissionPage/_base/driver.py index 293f3d0..d98bc31 100644 --- a/DrissionPage/_base/driver.py +++ b/DrissionPage/_base/driver.py @@ -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() diff --git a/DrissionPage/_configs/session_options.py b/DrissionPage/_configs/session_options.py index a3318ed..ee9b0f8 100644 --- a/DrissionPage/_configs/session_options.py +++ b/DrissionPage/_configs/session_options.py @@ -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 diff --git a/DrissionPage/_configs/session_options.pyi b/DrissionPage/_configs/session_options.pyi index 6af3375..54a88af 100644 --- a/DrissionPage/_configs/session_options.pyi +++ b/DrissionPage/_configs/session_options.pyi @@ -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]: ... diff --git a/DrissionPage/_elements/chromium_element.py b/DrissionPage/_elements/chromium_element.py index 6ba1317..b56263b 100644 --- a/DrissionPage/_elements/chromium_element.py +++ b/DrissionPage/_elements/chromium_element.py @@ -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 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: ... diff --git a/DrissionPage/_elements/session_element.py b/DrissionPage/_elements/session_element.py index 40a265c..4fe9c77 100644 --- a/DrissionPage/_elements/session_element.py +++ b/DrissionPage/_elements/session_element.py @@ -37,18 +37,26 @@ class SessionElement(DrissionElement): attrs = [f"{k}='{v}'" for k, v in self.attrs.items()] return f'' - 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 diff --git a/DrissionPage/_elements/session_element.pyi b/DrissionPage/_elements/session_element.pyi index f6e4e00..74034a9 100644 --- a/DrissionPage/_elements/session_element.pyi +++ b/DrissionPage/_elements/session_element.pyi @@ -35,6 +35,8 @@ class SessionElement(DrissionElement): def __eq__(self, other: SessionElement) -> bool: ... + def __getattr__(self, item: str) -> str: ... + @property def tag(self) -> str: ... diff --git a/DrissionPage/_functions/browser.py b/DrissionPage/_functions/browser.py index a707c61..d0829b8 100644 --- a/DrissionPage/_functions/browser.py +++ b/DrissionPage/_functions/browser.py @@ -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) diff --git a/DrissionPage/_functions/keys.py b/DrissionPage/_functions/keys.py index e4bebea..d55267c 100644 --- a/DrissionPage/_functions/keys.py +++ b/DrissionPage/_functions/keys.py @@ -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形式输入 diff --git a/DrissionPage/_functions/keys.pyi b/DrissionPage/_functions/keys.pyi index 896bc42..a06c8be 100644 --- a/DrissionPage/_functions/keys.pyi +++ b/DrissionPage/_functions/keys.pyi @@ -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 diff --git a/DrissionPage/_functions/settings.py b/DrissionPage/_functions/settings.py index 8f57669..d454c80 100644 --- a/DrissionPage/_functions/settings.py +++ b/DrissionPage/_functions/settings.py @@ -13,3 +13,4 @@ class Settings(object): raise_when_wait_failed = False singleton_tab_obj = True cdp_timeout = 30 + auto_handle_alert = None diff --git a/DrissionPage/_functions/tools.py b/DrissionPage/_functions/tools.py index 38640cf..2486718 100644 --- a/DrissionPage/_functions/tools.py +++ b/DrissionPage/_functions/tools.py @@ -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: diff --git a/DrissionPage/_functions/tools.pyi b/DrissionPage/_functions/tools.pyi index 86d3527..225ad81 100644 --- a/DrissionPage/_functions/tools.pyi +++ b/DrissionPage/_functions/tools.pyi @@ -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: ... diff --git a/DrissionPage/_functions/web.py b/DrissionPage/_functions/web.py index 3ef7b1e..95d2209 100644 --- a/DrissionPage/_functions/web.py +++ b/DrissionPage/_functions/web.py @@ -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) diff --git a/DrissionPage/_functions/web.pyi b/DrissionPage/_functions/web.pyi index aaebf3c..8ce0c3a 100644 --- a/DrissionPage/_functions/web.pyi +++ b/DrissionPage/_functions/web.pyi @@ -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: ... diff --git a/DrissionPage/_pages/chromium_base.py b/DrissionPage/_pages/chromium_base.py index 9354e22..027241e 100644 --- a/DrissionPage/_pages/chromium_base.py +++ b/DrissionPage/_pages/chromium_base.py @@ -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时只返回name、value、domain :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文本或tuple,tuple格式为(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 diff --git a/DrissionPage/_pages/chromium_base.pyi b/DrissionPage/_pages/chromium_base.pyi index 59be1c2..ddb89b7 100644 --- a/DrissionPage/_pages/chromium_base.pyi +++ b/DrissionPage/_pages/chromium_base.pyi @@ -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: ... diff --git a/DrissionPage/_pages/chromium_frame.py b/DrissionPage/_pages/chromium_frame.py index fef99d0..500859f 100644 --- a/DrissionPage/_pages/chromium_frame.py +++ b/DrissionPage/_pages/chromium_frame.py @@ -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: diff --git a/DrissionPage/_pages/chromium_page.py b/DrissionPage/_pages/chromium_page.py index c6b10fc..f2b0fe1 100644 --- a/DrissionPage/_pages/chromium_page.py +++ b/DrissionPage/_pages/chromium_page.py @@ -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): """设置浏览器启动属性 diff --git a/DrissionPage/_pages/chromium_page.pyi b/DrissionPage/_pages/chromium_page.pyi index 4a031ac..7117b9e 100644 --- a/DrissionPage/_pages/chromium_page.pyi +++ b/DrissionPage/_pages/chromium_page.pyi @@ -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]], diff --git a/DrissionPage/_pages/chromium_tab.py b/DrissionPage/_pages/chromium_tab.py index 23de4be..e496728 100644 --- a/DrissionPage/_pages/chromium_tab.py +++ b/DrissionPage/_pages/chromium_tab.py @@ -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则只返回name、value、domain :return: cookies信息 diff --git a/DrissionPage/_pages/session_page.py b/DrissionPage/_pages/session_page.py index 1bc7c27..a2d8694 100644 --- a/DrissionPage/_pages/session_page.py +++ b/DrissionPage/_pages/session_page.py @@ -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则只返回name、value、domain :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) diff --git a/DrissionPage/_pages/session_page.pyi b/DrissionPage/_pages/session_page.pyi index 7a3ac69..e229a36 100644 --- a/DrissionPage/_pages/session_page.pyi +++ b/DrissionPage/_pages/session_page.pyi @@ -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 = ..., diff --git a/DrissionPage/_pages/web_page.py b/DrissionPage/_pages/web_page.py index 39fa3a0..3a35e18 100644 --- a/DrissionPage/_pages/web_page.py +++ b/DrissionPage/_pages/web_page.py @@ -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则只返回name、value、domain :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 diff --git a/DrissionPage/_pages/web_page.pyi b/DrissionPage/_pages/web_page.pyi index 0f11253..bc0be0b 100644 --- a/DrissionPage/_pages/web_page.pyi +++ b/DrissionPage/_pages/web_page.pyi @@ -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: ... diff --git a/DrissionPage/_units/actions.py b/DrissionPage/_units/actions.py index f99ee70..d119629 100644 --- a/DrissionPage/_units/actions.py +++ b/DrissionPage/_units/actions.py @@ -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): diff --git a/DrissionPage/_units/actions.pyi b/DrissionPage/_units/actions.pyi index c070799..4904a35 100644 --- a/DrissionPage/_units/actions.pyi +++ b/DrissionPage/_units/actions.pyi @@ -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: ... diff --git a/DrissionPage/_units/clicker.py b/DrissionPage/_units/clicker.py index 0c9efc2..589465e 100644 --- a/DrissionPage/_units/clicker.py +++ b/DrissionPage/_units/clicker.py @@ -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坐标 diff --git a/DrissionPage/_units/cookies_setter.py b/DrissionPage/_units/cookies_setter.py index 63bcc2f..c9d3c41 100644 --- a/DrissionPage/_units/cookies_setter.py +++ b/DrissionPage/_units/cookies_setter.py @@ -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() diff --git a/DrissionPage/_units/cookies_setter.pyi b/DrissionPage/_units/cookies_setter.pyi index 3c6f37a..cbe37de 100644 --- a/DrissionPage/_units/cookies_setter.pyi +++ b/DrissionPage/_units/cookies_setter.pyi @@ -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: ... diff --git a/DrissionPage/_units/downloader.pyi b/DrissionPage/_units/downloader.pyi index 45537f1..eadcc44 100644 --- a/DrissionPage/_units/downloader.pyi +++ b/DrissionPage/_units/downloader.pyi @@ -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: ... diff --git a/DrissionPage/_units/listener.py b/DrissionPage/_units/listener.py index dd5de19..c552fbf 100644 --- a/DrissionPage/_units/listener.py +++ b/DrissionPage/_units/listener.py @@ -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 {}) diff --git a/DrissionPage/_units/listener.pyi b/DrissionPage/_units/listener.pyi index a8fc984..afa3e04 100644 --- a/DrissionPage/_units/listener.pyi +++ b/DrissionPage/_units/listener.pyi @@ -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]: ... diff --git a/DrissionPage/_units/rect.py b/DrissionPage/_units/rect.py index 55baa6b..4428a16 100644 --- a/DrissionPage/_units/rect.py +++ b/DrissionPage/_units/rect.py @@ -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 diff --git a/DrissionPage/_units/rect.pyi b/DrissionPage/_units/rect.pyi index 62fadc1..4fa4e73 100644 --- a/DrissionPage/_units/rect.pyi +++ b/DrissionPage/_units/rect.pyi @@ -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: ... diff --git a/DrissionPage/_units/screencast.py b/DrissionPage/_units/screencast.py index 958dbfc..fca2b68 100644 --- a/DrissionPage/_units/screencast.py +++ b/DrissionPage/_units/screencast.py @@ -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): diff --git a/DrissionPage/_units/screencast.pyi b/DrissionPage/_units/screencast.pyi index 92d7d0f..ec69ea4 100644 --- a/DrissionPage/_units/screencast.pyi +++ b/DrissionPage/_units/screencast.pyi @@ -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 = ... diff --git a/DrissionPage/_units/scroller.py b/DrissionPage/_units/scroller.py index cacdfee..224d640 100644 --- a/DrissionPage/_units/scroller.py +++ b/DrissionPage/_units/scroller.py @@ -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;} diff --git a/DrissionPage/_units/scroller.pyi b/DrissionPage/_units/scroller.pyi index 8233948..7e8b7c6 100644 --- a/DrissionPage/_units/scroller.pyi +++ b/DrissionPage/_units/scroller.pyi @@ -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: ... diff --git a/DrissionPage/_units/selector.py b/DrissionPage/_units/selector.py index 6682f64..fe5a23a 100644 --- a/DrissionPage/_units/selector.py +++ b/DrissionPage/_units/selector.py @@ -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 diff --git a/DrissionPage/_units/setter.py b/DrissionPage/_units/setter.py index 245ac24..8e576b2 100644 --- a/DrissionPage/_units/setter.py +++ b/DrissionPage/_units/setter.py @@ -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 agent,d模式下只有当前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 agent,d模式下只有当前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) diff --git a/DrissionPage/_units/setter.pyi b/DrissionPage/_units/setter.pyi index 8695d61..b0e6810 100644 --- a/DrissionPage/_units/setter.pyi +++ b/DrissionPage/_units/setter.pyi @@ -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: ... diff --git a/DrissionPage/_units/states.py b/DrissionPage/_units/states.py index cda3d77..a038624 100644 --- a/DrissionPage/_units/states.py +++ b/DrissionPage/_units/states.py @@ -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): diff --git a/DrissionPage/_units/states.pyi b/DrissionPage/_units/states.pyi index 067c0bd..71a673b 100644 --- a/DrissionPage/_units/states.pyi +++ b/DrissionPage/_units/states.pyi @@ -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: ... diff --git a/DrissionPage/_units/waiter.py b/DrissionPage/_units/waiter.py index 6450dec..4e6e3c4 100644 --- a/DrissionPage/_units/waiter.py +++ b/DrissionPage/_units/waiter.py @@ -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) diff --git a/DrissionPage/_units/waiter.pyi b/DrissionPage/_units/waiter.pyi index 300c176..4b96890 100644 --- a/DrissionPage/_units/waiter.pyi +++ b/DrissionPage/_units/waiter.pyi @@ -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: ... diff --git a/DrissionPage/common.py b/DrissionPage/common.py index 2c09a2e..7c3b474 100644 --- a/DrissionPage/common.py +++ b/DrissionPage/common.py @@ -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}') diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..ee4b0cf --- /dev/null +++ b/README.en.md @@ -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. + +--- + + star + +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 `