From ed8c53d7384abc501b1a7caa57a30a35556fc7ff Mon Sep 17 00:00:00 2001 From: g1879 Date: Tue, 7 Nov 2023 16:15:01 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=85=83=E7=B4=A0=E5=A2=9E=E5=8A=A0locatio?= =?UTF-8?q?ns.rect=E5=92=8Clocations.viewport=5Frect=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=EF=BC=9B=E4=BC=98=E5=8C=96is=5Fcovered=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=9B=E4=BC=98=E5=8C=96Driver=20=5Fsend()=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=9B=E4=BF=AE=E5=A4=8DListener=20method=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=9B=E4=BC=98=E5=8C=96=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=9BDriver=E7=BB=9F=E4=B8=80=E5=9C=A8bro?= =?UTF-8?q?wser=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DrissionPage/_base/browser.py | 24 +- DrissionPage/_base/browser.pyi | 7 +- DrissionPage/_base/chromium_driver.py | 17 +- DrissionPage/_commons/web.py | 4 +- DrissionPage/_configs/chromium_options.py | 2 +- DrissionPage/_configs/session_options.py | 3 +- DrissionPage/_elements/chromium_element.py | 367 ++------------------ DrissionPage/_elements/chromium_element.pyi | 117 +------ DrissionPage/_pages/chromium_base.py | 8 +- DrissionPage/_units/clicker.py | 54 +-- DrissionPage/_units/download_manager.py | 17 +- DrissionPage/_units/element_states.py | 100 ++++++ DrissionPage/_units/element_states.pyi | 50 +++ DrissionPage/_units/network_listener.py | 47 ++- DrissionPage/_units/select_element.py | 244 +++++++++++++ DrissionPage/_units/select_element.pyi | 63 ++++ 16 files changed, 593 insertions(+), 531 deletions(-) create mode 100644 DrissionPage/_units/element_states.py create mode 100644 DrissionPage/_units/element_states.pyi create mode 100644 DrissionPage/_units/select_element.py create mode 100644 DrissionPage/_units/select_element.pyi diff --git a/DrissionPage/_base/browser.py b/DrissionPage/_base/browser.py index 5593260..46ad7c1 100644 --- a/DrissionPage/_base/browser.py +++ b/DrissionPage/_base/browser.py @@ -5,7 +5,7 @@ """ from time import sleep -from .chromium_driver import BrowserDriver +from .chromium_driver import BrowserDriver, ChromiumDriver from .._units.download_manager import BrowserDownloadManager @@ -38,6 +38,8 @@ class Browser(object): self._driver = BrowserDriver(browser_id, 'browser', address) self.id = browser_id self._frames = {} + self._drivers = {} + # self._drivers = {t: ChromiumDriver(t, 'page', address) for t in self.tabs} self._connected = False self._process_id = None @@ -49,13 +51,27 @@ class Browser(object): self.run_cdp('Target.setDiscoverTargets', discover=True) self._driver.set_listener('Target.targetDestroyed', self._onTargetDestroyed) + self._driver.set_listener('Target.targetCreated', self._onTargetCreated) + + def _get_driver(self, tab_id): + """获取对应tab id的ChromiumDriver + :param tab_id: 标签页id + :return: ChromiumDriver对象 + """ + return self._drivers.pop(tab_id, ChromiumDriver(tab_id, 'page', self.address)) + + def _onTargetCreated(self, **kwargs): + """标签页创建时执行""" + if kwargs['targetInfo']['type'] == 'page' and not kwargs['targetInfo']['url'].startswith('devtools://'): + self._drivers[kwargs['targetInfo']['targetId']] = ChromiumDriver(kwargs['targetInfo']['targetId'], 'page', self.address) def _onTargetDestroyed(self, **kwargs): """标签页关闭时执行""" tab_id = kwargs['targetId'] self._dl_mgr.clear_tab_info(tab_id) - for item in [(k, i) for k, i in self._frames.items() if i == tab_id]: - self._frames.pop(item[0]) + for key in [k for k, i in self._frames.items() if i == tab_id]: + self._frames.pop(key, None) + self._drivers.pop(tab_id, None) def connect_to_page(self): """执行与page相关的逻辑""" @@ -150,4 +166,4 @@ class Browser(object): break sleep(.2) - Browser.BROWSERS.pop(self.id) + Browser.BROWSERS.pop(self.id, None) diff --git a/DrissionPage/_base/browser.pyi b/DrissionPage/_base/browser.pyi index ee5edad..cb1a13a 100644 --- a/DrissionPage/_base/browser.pyi +++ b/DrissionPage/_base/browser.pyi @@ -5,7 +5,7 @@ """ from typing import List, Optional, Union -from .chromium_driver import BrowserDriver +from .chromium_driver import BrowserDriver, ChromiumDriver from .._pages.chromium_page import ChromiumPage from .._units.download_manager import BrowserDownloadManager @@ -17,6 +17,7 @@ class Browser(object): id: str = ... address: str = ... _frames: dict = ... + _drivers: dict = ... _process_id: Optional[int] = ... _dl_mgr: BrowserDownloadManager = ... _connected: bool = ... @@ -25,6 +26,8 @@ class Browser(object): def __init__(self, address: str, browser_id: str, page: ChromiumPage): ... + def _get_driver(self, tab_id: str)->ChromiumDriver: ... + def run_cdp(self, cmd, **cmd_args) -> dict: ... @property @@ -50,6 +53,8 @@ class Browser(object): def connect_to_page(self) -> None: ... + def _onTargetCreated(self, **kwargs) -> None: ... + def _onTargetDestroyed(self, **kwargs) -> None: ... def quit(self) -> None: ... diff --git a/DrissionPage/_base/chromium_driver.py b/DrissionPage/_base/chromium_driver.py index 83309a4..b9f1c9e 100644 --- a/DrissionPage/_base/chromium_driver.py +++ b/DrissionPage/_base/chromium_driver.py @@ -71,26 +71,29 @@ class ChromiumDriver(object): try: self._ws.send(message_json) except OSError: - self.method_results.pop(ws_id) + self.method_results.pop(ws_id, None) return None while not self._stopped.is_set(): try: - return self.method_results[ws_id].get(timeout=.2) + result = self.method_results[ws_id].get(timeout=.2) + self.method_results.pop(ws_id, None) + return result except Empty: if self.alert_flag: self.alert_flag = False - return {'result': {'message': 'alert exists.'}} + result = {'result': {'message': 'alert exists.'}} + self.method_results.pop(ws_id, None) + return result elif timeout is not None and perf_counter() > timeout: - return {'error': {'message': 'timeout'}} + result = {'error': {'message': 'timeout'}} + self.method_results.pop(ws_id, None) + return result continue - finally: - self.method_results.pop(ws_id) - def _recv_loop(self): """接收浏览器信息的守护线程方法""" while not self._stopped.is_set(): diff --git a/DrissionPage/_commons/web.py b/DrissionPage/_commons/web.py index 543d222..9d9c7b5 100644 --- a/DrissionPage/_commons/web.py +++ b/DrissionPage/_commons/web.py @@ -169,8 +169,8 @@ def cookie_to_dict(cookie): """ if isinstance(cookie, Cookie): cookie_dict = cookie.__dict__.copy() - cookie_dict.pop('rfc2109') - cookie_dict.pop('_rest') + cookie_dict.pop('rfc2109', None) + cookie_dict.pop('_rest', None) return cookie_dict elif isinstance(cookie, dict): diff --git a/DrissionPage/_configs/chromium_options.py b/DrissionPage/_configs/chromium_options.py index 1ce072f..214ccd0 100644 --- a/DrissionPage/_configs/chromium_options.py +++ b/DrissionPage/_configs/chromium_options.py @@ -209,7 +209,7 @@ class ChromiumOptions(object): :param arg: 设置项名称 :return: 当前对象 """ - self._prefs.pop(arg) + self._prefs.pop(arg, None) return self def remove_pref_from_file(self, arg): diff --git a/DrissionPage/_configs/session_options.py b/DrissionPage/_configs/session_options.py index 4339b08..d918f63 100644 --- a/DrissionPage/_configs/session_options.py +++ b/DrissionPage/_configs/session_options.py @@ -161,8 +161,7 @@ class SessionOptions(object): return self attr = attr.lower() - if attr in self._headers: - self._headers.pop(attr) + self._headers.pop(attr, None) return self diff --git a/DrissionPage/_elements/chromium_element.py b/DrissionPage/_elements/chromium_element.py index e51a0d6..704c5d6 100644 --- a/DrissionPage/_elements/chromium_element.py +++ b/DrissionPage/_elements/chromium_element.py @@ -13,12 +13,14 @@ from .._commons.constants import FRAME_ELEMENT, NoneElement, Settings from .._commons.keys import keys_to_typing, keyDescriptionForString, keyDefinitions from .._commons.locator import get_loc from .._commons.tools import get_usable_path -from .._commons.web import make_absolute_link, get_ele_txt, format_html, is_js_func, location_in_viewport, offset_scroll +from .._commons.web import make_absolute_link, get_ele_txt, format_html, is_js_func, offset_scroll from .._units.clicker import Clicker +from .._units.element_states import ChromiumElementStates, ShadowRootStates +from .._units.select_element import SelectElement from .._units.setter import ChromiumElementSetter from .._units.waiter import ChromiumElementWaiter -from ..errors import ContextLossError, ElementLossError, JavaScriptError, ElementNotFoundError, \ - CDPError, NoResourceError, NoRectError, AlertExistsError +from ..errors import (ContextLossError, ElementLossError, JavaScriptError, ElementNotFoundError, + CDPError, NoResourceError, AlertExistsError) class ChromiumElement(DrissionElement): @@ -202,7 +204,7 @@ class ChromiumElement(DrissionElement): if self.tag != 'select': self._select = False else: - self._select = ChromiumSelect(self) + self._select = SelectElement(self) return self._select @@ -1436,103 +1438,6 @@ def send_key(ele, modifier, key): ele.page.run_cdp('Input.dispatchKeyEvent', **data) -class ChromiumElementStates(object): - def __init__(self, ele): - """ - :param ele: ChromiumElement - """ - self._ele = ele - - @property - def is_selected(self): - """返回元素是否被选择""" - return self._ele.run_js('return this.selected;') - - @property - def is_checked(self): - """返回元素是否被选择""" - return self._ele.run_js('return this.checked;') - - @property - def is_displayed(self): - """返回元素是否显示""" - return not (self._ele.style('visibility') == 'hidden' - or self._ele.run_js('return this.offsetParent === null;') - or self._ele.style('display') == 'none') - - @property - def is_enabled(self): - """返回元素是否可用""" - return not self._ele.run_js('return this.disabled;') - - @property - def is_alive(self): - """返回元素是否仍在DOM中""" - try: - d = self._ele.attrs - return True - except Exception: - return False - - @property - def is_in_viewport(self): - """返回元素是否出现在视口中,以元素click_point为判断""" - x, y = self._ele.locations.click_point - return location_in_viewport(self._ele.page, x, y) if x else False - - @property - def is_whole_in_viewport(self): - """返回元素是否整个都在视口内""" - x1, y1 = self._ele.location - w, h = self._ele.size - x2, y2 = x1 + w, y1 + h - return location_in_viewport(self._ele.page, x1, y1) and location_in_viewport(self._ele.page, x2, y2) - - @property - def is_covered(self): - """返回元素是否被覆盖,与是否在视口中无关""" - lx, ly = self._ele.locations.click_point - try: - r = self._ele.page.run_cdp('DOM.getNodeForLocation', x=lx, y=ly) - except CDPError: - return False - - if r.get('backendNodeId') != self._ele.ids.backend_id: - return True - - return False - - @property - def has_rect(self): - """返回元素是否拥有位置和大小,没有返回False,有返回大小元组""" - try: - return self._ele.size - except NoRectError: - return False - - -class ShadowRootStates(object): - def __init__(self, ele): - """ - :param ele: ChromiumElement - """ - self._ele = ele - - @property - def is_enabled(self): - """返回元素是否可用""" - return not self._ele.run_js('return this.disabled;') - - @property - def is_alive(self): - """返回元素是否仍在DOM中""" - try: - self._ele.page.run_cdp('DOM.describeNode', backendNodeId=self._ele.ids.backend_id) - return True - except Exception: - return False - - class Locations(object): def __init__(self, ele): """ @@ -1574,7 +1479,7 @@ class Locations(object): def viewport_click_point(self): """返回元素接受点击的点视口坐标""" m = self._get_viewport_rect('padding') - return int(self.viewport_midpoint[0]), int(m[1]) + 1 + return int(self.viewport_midpoint[0]), int(m[1]) + 3 @property def screen_location(self): @@ -1600,18 +1505,30 @@ class Locations(object): pr = self._ele.page.run_js('return window.devicePixelRatio;') return int((vx + ex) * pr), int((ey + vy) * pr) + @property + def rect(self): + """返回元素四个角坐标,顺序:坐上、右上、右下、左下,没有大小的元素抛出NoRectError""" + vr = self._get_viewport_rect('border') + r = self._ele.page.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport'] + sx = r['pageX'] + sy = r['pageY'] + return [(vr[0] + sx, vr[1] + sy), (vr[2] + sx, vr[3] + sy), (vr[4] + sx, vr[5] + sy), (vr[6] + sx, vr[7] + sy)] + + @property + def viewport_rect(self): + """返回元素四个角视口坐标,顺序:坐上、右上、右下、左下,没有大小的元素抛出NoRectError""" + r = self._get_viewport_rect('border') + return [(r[0], r[1]), (r[2], r[3]), (r[4], r[5]), (r[6], r[7])] + def _get_viewport_rect(self, quad): """按照类型返回在可视窗口中的范围 :param quad: 方框类型,margin border padding - :return: 四个角坐标,大小为0时返回None + :return: 四个角坐标 """ return self._ele.page.run_cdp('DOM.getBoxModel', backendNodeId=self._ele.ids.backend_id)['model'][quad] def _get_page_coord(self, x, y): """根据视口坐标获取绝对坐标""" - # js = 'return document.documentElement.scrollLeft+" "+document.documentElement.scrollTop;' - # xy = self._ele.run_js(js) - # sx, sy = xy.split(' ') r = self._ele.page.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport'] sx = r['pageX'] sy = r['pageY'] @@ -1728,244 +1645,6 @@ class ChromiumElementScroll(ChromiumScroll): self._driver.page.scroll.to_see(self._driver, center=True) -class ChromiumSelect(object): - """ChromiumSelect 类专门用于处理 d 模式下 select 标签""" - - def __init__(self, ele): - """ - :param ele: select 元素对象 - """ - if ele.tag != 'select': - raise TypeError("select方法只能在元素使用。") + + self._ele = ele + + def __call__(self, text_or_index, timeout=None): + """选定下拉列表中子元素 + :param text_or_index: 根据文本、值选或序号择选项,若允许多选,传入list或tuple可多选 + :param timeout: 超时时间,不输入默认实用页面超时时间 + :return: None + """ + para_type = 'index' if isinstance(text_or_index, int) else 'text' + timeout = timeout if timeout is not None else self._ele.page.timeout + return self._select(text_or_index, para_type, timeout=timeout) + + @property + def is_multi(self): + """返回是否多选表单""" + return self._ele.attr('multiple') is not None + + @property + def options(self): + """返回所有选项元素组成的列表""" + return self._ele.eles('xpath://option') + + @property + def selected_option(self): + """返回第一个被选中的option元素 + :return: ChromiumElement对象或None + """ + ele = self._ele.run_js('return this.options[this.selectedIndex];') + return ele + + @property + def selected_options(self): + """返回所有被选中的option元素列表 + :return: ChromiumElement对象组成的列表 + """ + return [x for x in self.options if x.states.is_selected] + + def all(self): + """全选""" + if not self.is_multi: + raise TypeError("只能在多选菜单执行此操作。") + return self._by_loc('tag:option', 1, False) + + def invert(self): + """反选""" + if not self.is_multi: + raise TypeError("只能对多项选框执行反选。") + change = False + for i in self.options: + change = True + mode = 'false' if i.states.is_selected else 'true' + i.run_js(f'this.selected={mode};') + if change: + self._dispatch_change() + + def clear(self): + """清除所有已选项""" + if not self.is_multi: + raise TypeError("只能在多选菜单执行此操作。") + return self._by_loc('tag:option', 1, True) + + def by_text(self, text, timeout=None): + """此方法用于根据text值选择项。当元素是多选列表时,可以接收list或tuple + :param text: text属性值,传入list或tuple可选择多项 + :param timeout: 超时时间,为None默认使用页面超时时间 + :return: 是否选择成功 + """ + return self._select(text, 'text', False, timeout) + + def by_value(self, value, timeout=None): + """此方法用于根据value值选择项。当元素是多选列表时,可以接收list或tuple + :param value: value属性值,传入list或tuple可选择多项 + :param timeout: 超时时间,为None默认使用页面超时时间 + :return: 是否选择成功 + """ + return self._select(value, 'value', False, timeout) + + def by_index(self, index, timeout=None): + """此方法用于根据index值选择项。当元素是多选列表时,可以接收list或tuple + :param index: 序号,0开始,传入list或tuple可选择多项 + :param timeout: 超时时间,为None默认使用页面超时时间 + :return: 是否选择成功 + """ + return self._select(index, 'index', False, timeout) + + def by_loc(self, loc, timeout=None): + """用定位符选择指定的项 + :param loc: 定位符 + :param timeout: 超时时间 + :return: 是否选择成功 + """ + return self._by_loc(loc, timeout) + + def cancel_by_text(self, text, timeout=None): + """此方法用于根据text值取消选择项。当元素是多选列表时,可以接收list或tuple + :param text: 文本,传入list或tuple可取消多项 + :param timeout: 超时时间,不输入默认实用页面超时时间 + :return: 是否取消成功 + """ + return self._select(text, 'text', True, timeout) + + def cancel_by_value(self, value, timeout=None): + """此方法用于根据value值取消选择项。当元素是多选列表时,可以接收list或tuple + :param value: value属性值,传入list或tuple可取消多项 + :param timeout: 超时时间,不输入默认实用页面超时时间 + :return: 是否取消成功 + """ + return self._select(value, 'value', True, timeout) + + def cancel_by_index(self, index, timeout=None): + """此方法用于根据index值取消选择项。当元素是多选列表时,可以接收list或tuple + :param index: 序号,0开始,传入list或tuple可取消多项 + :param timeout: 超时时间,不输入默认实用页面超时时间 + :return: 是否取消成功 + """ + return self._select(index, 'index', True, timeout) + + def cancel_by_loc(self, loc, timeout=None): + """用定位符取消选择指定的项 + :param loc: 定位符 + :param timeout: 超时时间 + :return: 是否选择成功 + """ + return self._by_loc(loc, timeout, True) + + def _by_loc(self, loc, timeout=None, cancel=False): + """用定位符取消选择指定的项 + :param loc: 定位符 + :param timeout: 超时时间 + :param cancel: 是否取消选择 + :return: 是否选择成功 + """ + eles = self._ele.eles(loc, timeout) + if not eles: + return False + + mode = 'false' if cancel else 'true' + if self.is_multi: + for ele in eles: + ele.run_js(f'this.selected={mode};') + self._dispatch_change() + return True + + eles[0].run_js(f'this.selected={mode};') + self._dispatch_change() + return True + + def _select(self, condition, para_type='text', cancel=False, timeout=None): + """选定或取消选定下拉列表中子元素 + :param condition: 根据文本、值选或序号择选项,若允许多选,传入list或tuple可多选 + :param para_type: 参数类型,可选 'text'、'value'、'index' + :param cancel: 是否取消选择 + :return: 是否选择成功 + """ + if not self.is_multi and isinstance(condition, (list, tuple)): + raise TypeError('单选列表只能传入str格式。') + + mode = 'false' if cancel else 'true' + timeout = timeout if timeout is not None else self._ele.page.timeout + condition = set(condition) if isinstance(condition, (list, tuple)) else {condition} + + if para_type in ('text', 'value'): + return self._text_value([str(i) for i in condition], para_type, mode, timeout) + elif para_type == 'index': + return self._index(condition, mode, timeout) + + def _text_value(self, condition, para_type, mode, timeout): + """执行text和value搜索 + :param condition: 条件set + :param para_type: 参数类型,可选 'text'、'value' + :param mode: 'true' 或 'false' + :param timeout: 超时时间 + :return: 是否选择成功 + """ + ok = False + text_len = len(condition) + eles = [] + end_time = perf_counter() + timeout + while perf_counter() < end_time: + if para_type == 'text': + eles = [i for i in self.options if i.text in condition] + elif para_type == 'value': + eles = [i for i in self.options if i.attr('value') in condition] + + if len(eles) >= text_len: + ok = True + break + + if ok: + for i in eles: + i.run_js(f'this.selected={mode};') + + self._dispatch_change() + return True + + return False + + def _index(self, condition, mode, timeout): + """执行index搜索 + :param condition: 条件set + :param mode: 'true' 或 'false' + :param timeout: 超时时间 + :return: 是否选择成功 + """ + ok = False + condition = [int(i) for i in condition] + text_len = max(condition) + end_time = perf_counter() + timeout + while perf_counter() < end_time: + if len(self.options) >= text_len: + ok = True + break + + if ok: + eles = self.options + for i in condition: + eles[i - 1].run_js(f'this.selected={mode};') + + self._dispatch_change() + return True + + return False + + def _dispatch_change(self): + """触发修改动作""" + self._ele.run_js('this.dispatchEvent(new UIEvent("change"));') diff --git a/DrissionPage/_units/select_element.pyi b/DrissionPage/_units/select_element.pyi new file mode 100644 index 0000000..55d07b6 --- /dev/null +++ b/DrissionPage/_units/select_element.pyi @@ -0,0 +1,63 @@ +# -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +""" +from typing import Union, Tuple, List + +from .._elements.chromium_element import ChromiumElement + + +class SelectElement(object): + def __init__(self, ele: ChromiumElement): + self._ele: ChromiumElement = ... + + def __call__(self, text_or_index: Union[str, int, list, tuple], timeout: float = None) -> bool: ... + + @property + def is_multi(self) -> bool: ... + + @property + def options(self) -> List[ChromiumElement]: ... + + @property + def selected_option(self) -> Union[ChromiumElement, None]: ... + + @property + def selected_options(self) -> List[ChromiumElement]: ... + + def clear(self) -> None: ... + + def all(self) -> None: ... + + def by_text(self, text: Union[str, list, tuple], timeout: float = None) -> bool: ... + + def by_value(self, value: Union[str, list, tuple], timeout: float = None) -> bool: ... + + def by_index(self, index: Union[int, list, tuple], timeout: float = None) -> bool: ... + + def by_loc(self, loc: Union[str, Tuple[str, str]], timeout: float = None) -> bool: ... + + def cancel_by_text(self, text: Union[str, list, tuple], timeout: float = None) -> bool: ... + + def cancel_by_value(self, value: Union[str, list, tuple], timeout: float = None) -> bool: ... + + def cancel_by_index(self, index: Union[int, list, tuple], timeout: float = None) -> bool: ... + + def cancel_by_loc(self, loc: Union[str, Tuple[str, str]], timeout: float = None) -> bool: ... + + def invert(self) -> None: ... + + def _by_loc(self, loc: Union[str, Tuple[str, str]], timeout: float = None, cancel: bool = False) -> bool: ... + + def _select(self, + condition: Union[str, int, list, tuple] = None, + para_type: str = 'text', + cancel: bool = False, + timeout: float = None) -> bool: ... + + def _text_value(self, condition: Union[list, set], para_type: str, mode: str, timeout: float) -> bool: ... + + def _index(self, condition: set, mode: str, timeout: float) -> bool: ... + + def _dispatch_change(self) -> None: ... \ No newline at end of file From 3dfbfb957f05a7d60acd03ccf71d57ba7e8c0dc7 Mon Sep 17 00:00:00 2001 From: g1879 Date: Tue, 7 Nov 2023 18:04:18 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E6=97=B6=E5=88=A0=E9=99=A4=E8=AE=B0=E5=BD=95=E7=9A=84?= =?UTF-8?q?id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DrissionPage/_base/browser.py | 6 ++++-- DrissionPage/_base/browser.pyi | 2 ++ DrissionPage/_base/chromium_driver.py | 9 +++++++-- DrissionPage/_base/chromium_driver.pyi | 7 +++++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/DrissionPage/_base/browser.py b/DrissionPage/_base/browser.py index 46ad7c1..1a0d932 100644 --- a/DrissionPage/_base/browser.py +++ b/DrissionPage/_base/browser.py @@ -35,7 +35,7 @@ class Browser(object): self.page = page self.address = address - self._driver = BrowserDriver(browser_id, 'browser', address) + self._driver = BrowserDriver(browser_id, 'browser', address, self) self.id = browser_id self._frames = {} self._drivers = {} @@ -63,7 +63,8 @@ class Browser(object): def _onTargetCreated(self, **kwargs): """标签页创建时执行""" if kwargs['targetInfo']['type'] == 'page' and not kwargs['targetInfo']['url'].startswith('devtools://'): - self._drivers[kwargs['targetInfo']['targetId']] = ChromiumDriver(kwargs['targetInfo']['targetId'], 'page', self.address) + self._drivers[kwargs['targetInfo']['targetId']] = ChromiumDriver(kwargs['targetInfo']['targetId'], 'page', + self.address) def _onTargetDestroyed(self, **kwargs): """标签页关闭时执行""" @@ -166,4 +167,5 @@ class Browser(object): break sleep(.2) + def _on_quit(self): Browser.BROWSERS.pop(self.id, None) diff --git a/DrissionPage/_base/browser.pyi b/DrissionPage/_base/browser.pyi index cb1a13a..fed394c 100644 --- a/DrissionPage/_base/browser.pyi +++ b/DrissionPage/_base/browser.pyi @@ -58,3 +58,5 @@ class Browser(object): def _onTargetDestroyed(self, **kwargs) -> None: ... def quit(self) -> None: ... + + def _on_quit(self) -> None: ... diff --git a/DrissionPage/_base/chromium_driver.py b/DrissionPage/_base/chromium_driver.py index b9f1c9e..9d63a0d 100644 --- a/DrissionPage/_base/chromium_driver.py +++ b/DrissionPage/_base/chromium_driver.py @@ -211,17 +211,18 @@ class ChromiumDriver(object): class BrowserDriver(ChromiumDriver): BROWSERS = {} - def __new__(cls, tab_id, tab_type, address): + def __new__(cls, tab_id, tab_type, address, browser): if tab_id in cls.BROWSERS: return cls.BROWSERS[tab_id] return object.__new__(cls) - def __init__(self, tab_id, tab_type, address): + def __init__(self, tab_id, tab_type, address, browser): if hasattr(self, '_created'): return self._created = True BrowserDriver.BROWSERS[tab_id] = self super().__init__(tab_id, tab_type, address) + self.browser = browser def __repr__(self): return f"" @@ -230,3 +231,7 @@ class BrowserDriver(ChromiumDriver): r = get(url, headers={'Connection': 'close'}) r.close() return r + + def stop(self): + super().stop() + self.browser._on_quit() diff --git a/DrissionPage/_base/chromium_driver.pyi b/DrissionPage/_base/chromium_driver.pyi index fac8ab5..8977874 100644 --- a/DrissionPage/_base/chromium_driver.pyi +++ b/DrissionPage/_base/chromium_driver.pyi @@ -10,6 +10,8 @@ from typing import Union, Callable, Dict, Optional from requests import Response from websocket import WebSocket +from .browser import Browser + class GenericAttr(object): def __init__(self, name: str, tab: ChromiumDriver): ... @@ -58,5 +60,10 @@ class ChromiumDriver(object): class BrowserDriver(ChromiumDriver): BROWSERS: Dict[str, ChromiumDriver] = ... + browser: Browser = ... + + def __new__(cls, tab_id: str, tab_type: str, address: str, browser: Browser): ... + + def __init__(self, tab_id: str, tab_type: str, address: str, browser: Browser): ... def get(self, url) -> Response: ... From c6273d9bf25db807d232aa5a1086214e914756f6 Mon Sep 17 00:00:00 2001 From: g1879 Date: Wed, 8 Nov 2023 11:21:11 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DrissionPage/_elements/chromium_element.py | 98 +------------------ DrissionPage/_elements/chromium_element.pyi | 45 +-------- DrissionPage/_pages/chromium_base.py | 9 +- DrissionPage/_units/clicker.py | 42 +++++--- DrissionPage/_units/element_states.py | 8 +- DrissionPage/_units/element_states.pyi | 8 +- DrissionPage/_units/locations.py | 103 ++++++++++++++++++++ DrissionPage/_units/locations.pyi | 50 ++++++++++ 8 files changed, 201 insertions(+), 162 deletions(-) create mode 100644 DrissionPage/_units/locations.py create mode 100644 DrissionPage/_units/locations.pyi diff --git a/DrissionPage/_elements/chromium_element.py b/DrissionPage/_elements/chromium_element.py index 704c5d6..3b5c185 100644 --- a/DrissionPage/_elements/chromium_element.py +++ b/DrissionPage/_elements/chromium_element.py @@ -16,6 +16,7 @@ from .._commons.tools import get_usable_path from .._commons.web import make_absolute_link, get_ele_txt, format_html, is_js_func, offset_scroll from .._units.clicker import Clicker from .._units.element_states import ChromiumElementStates, ShadowRootStates +from .._units.locations import Locations from .._units.select_element import SelectElement from .._units.setter import ChromiumElementSetter from .._units.waiter import ChromiumElementWaiter @@ -1438,103 +1439,6 @@ def send_key(ele, modifier, key): ele.page.run_cdp('Input.dispatchKeyEvent', **data) -class Locations(object): - def __init__(self, ele): - """ - :param ele: ChromiumElement - """ - self._ele = ele - - @property - def location(self): - """返回元素左上角的绝对坐标""" - cl = self.viewport_location - return self._get_page_coord(cl[0], cl[1]) - - @property - def midpoint(self): - """返回元素中间点的绝对坐标""" - cl = self.viewport_midpoint - return self._get_page_coord(cl[0], cl[1]) - - @property - def click_point(self): - """返回元素接受点击的点的绝对坐标""" - cl = self.viewport_click_point - return self._get_page_coord(cl[0], cl[1]) - - @property - def viewport_location(self): - """返回元素左上角在视口中的坐标""" - m = self._get_viewport_rect('border') - return int(m[0]), int(m[1]) - - @property - def viewport_midpoint(self): - """返回元素中间点在视口中的坐标""" - m = self._get_viewport_rect('border') - return int(m[0] + (m[2] - m[0]) // 2), int(m[3] + (m[5] - m[3]) // 2) - - @property - def viewport_click_point(self): - """返回元素接受点击的点视口坐标""" - m = self._get_viewport_rect('padding') - return int(self.viewport_midpoint[0]), int(m[1]) + 3 - - @property - def screen_location(self): - """返回元素左上角在屏幕上坐标,左上角为(0, 0)""" - vx, vy = self._ele.page.rect.viewport_location - ex, ey = self.viewport_location - pr = self._ele.page.run_js('return window.devicePixelRatio;') - return int((vx + ex) * pr), int((ey + vy) * pr) - - @property - def screen_midpoint(self): - """返回元素中点在屏幕上坐标,左上角为(0, 0)""" - vx, vy = self._ele.page.rect.viewport_location - ex, ey = self.viewport_midpoint - pr = self._ele.page.run_js('return window.devicePixelRatio;') - return int((vx + ex) * pr), int((ey + vy) * pr) - - @property - def screen_click_point(self): - """返回元素中点在屏幕上坐标,左上角为(0, 0)""" - vx, vy = self._ele.page.rect.viewport_location - ex, ey = self.viewport_click_point - pr = self._ele.page.run_js('return window.devicePixelRatio;') - return int((vx + ex) * pr), int((ey + vy) * pr) - - @property - def rect(self): - """返回元素四个角坐标,顺序:坐上、右上、右下、左下,没有大小的元素抛出NoRectError""" - vr = self._get_viewport_rect('border') - r = self._ele.page.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport'] - sx = r['pageX'] - sy = r['pageY'] - return [(vr[0] + sx, vr[1] + sy), (vr[2] + sx, vr[3] + sy), (vr[4] + sx, vr[5] + sy), (vr[6] + sx, vr[7] + sy)] - - @property - def viewport_rect(self): - """返回元素四个角视口坐标,顺序:坐上、右上、右下、左下,没有大小的元素抛出NoRectError""" - r = self._get_viewport_rect('border') - return [(r[0], r[1]), (r[2], r[3]), (r[4], r[5]), (r[6], r[7])] - - def _get_viewport_rect(self, quad): - """按照类型返回在可视窗口中的范围 - :param quad: 方框类型,margin border padding - :return: 四个角坐标 - """ - return self._ele.page.run_cdp('DOM.getBoxModel', backendNodeId=self._ele.ids.backend_id)['model'][quad] - - def _get_page_coord(self, x, y): - """根据视口坐标获取绝对坐标""" - r = self._ele.page.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport'] - sx = r['pageX'] - sy = r['pageY'] - return x + sx, y + sy - - class ChromiumScroll(object): """用于滚动的对象""" diff --git a/DrissionPage/_elements/chromium_element.pyi b/DrissionPage/_elements/chromium_element.pyi index f65b149..c41464f 100644 --- a/DrissionPage/_elements/chromium_element.pyi +++ b/DrissionPage/_elements/chromium_element.pyi @@ -4,7 +4,7 @@ @Contact : g1879@qq.com """ from pathlib import Path -from typing import Union, Tuple, List, Any, Optional +from typing import Union, Tuple, List, Any from .._base.base import DrissionElement, BaseElement from .._commons.constants import NoneElement @@ -15,6 +15,7 @@ from .._pages.chromium_page import ChromiumPage from .._pages.web_page import WebPage from .._units.clicker import Clicker from .._units.element_states import ShadowRootStates, ChromiumElementStates +from .._units.locations import Locations from .._units.select_element import SelectElement from .._units.setter import ChromiumElementSetter from .._units.waiter import ChromiumElementWaiter @@ -363,48 +364,6 @@ def send_enter(ele: ChromiumElement) -> None: ... def send_key(ele: ChromiumElement, modifier: int, key: str) -> None: ... -class Locations(object): - def __init__(self, ele: ChromiumElement): - self._ele: ChromiumElement = ... - - @property - def location(self) -> Tuple[int, int]: ... - - @property - def midpoint(self) -> Tuple[int, int]: ... - - @property - def click_point(self) -> Tuple[int, int]: ... - - @property - def viewport_location(self) -> Tuple[int, int]: ... - - @property - def viewport_midpoint(self) -> Tuple[int, int]: ... - - @property - def viewport_click_point(self) -> Tuple[int, int]: ... - - @property - def screen_location(self) -> Tuple[int, int]: ... - - @property - def screen_midpoint(self) -> Tuple[int, int]: ... - - @property - def screen_click_point(self) -> Tuple[int, int]: ... - - @property - def rect(self) -> list: ... - - @property - def viewport_rect(self) -> list: ... - - def _get_viewport_rect(self, quad: str) -> Union[list, None]: ... - - def _get_page_coord(self, x: int, y: int) -> Tuple[int, int]: ... - - class ChromiumScroll(object): def __init__(self, page_or_ele: Union[ChromiumBase, ChromiumElement, ChromiumFrame]): self.t1: str = ... diff --git a/DrissionPage/_pages/chromium_base.py b/DrissionPage/_pages/chromium_base.py index a6726e8..e2c5b37 100644 --- a/DrissionPage/_pages/chromium_base.py +++ b/DrissionPage/_pages/chromium_base.py @@ -103,6 +103,11 @@ class ChromiumBase(BasePage): self._driver.call_method('Page.enable') self._driver.call_method('Emulation.setFocusEmulationEnabled', enabled=True) + r = self.run_cdp('Page.getFrameTree') + for i in findall(r"'id': '(.*?)'", str(r)): + self.browser._frames[i] = self.tab_id + self._frame_id = r['frameTree']['frame']['id'] + self._driver.set_listener('Page.frameStartedLoading', self._onFrameStartedLoading) self._driver.set_listener('Page.frameNavigated', self._onFrameNavigated) self._driver.set_listener('Page.domContentEventFired', self._onDomContentEventFired) @@ -111,10 +116,6 @@ class ChromiumBase(BasePage): self._driver.set_listener('Page.frameAttached', self._onFrameAttached) self._driver.set_listener('Page.frameDetached', self._onFrameDetached) - r = self.run_cdp('Page.getFrameTree') - for i in findall(r"'id': '(.*?)'", str(r)): - self.browser._frames[i] = self.tab_id - self._frame_id = r['frameTree']['frame']['id'] def _get_document(self): if self._is_reading: diff --git a/DrissionPage/_units/clicker.py b/DrissionPage/_units/clicker.py index eefc5ab..b71688d 100644 --- a/DrissionPage/_units/clicker.py +++ b/DrissionPage/_units/clicker.py @@ -7,7 +7,7 @@ from time import perf_counter, sleep from .._commons.constants import Settings from .._commons.web import offset_scroll -from ..errors import CanNotClickError, CDPError +from ..errors import CanNotClickError, CDPError, NoRectError class Clicker(object): @@ -26,7 +26,7 @@ class Clicker(object): """ return self.left(by_js, timeout) - def left(self, by_js=False, timeout=1): + def left(self, by_js=False, timeout=2): """点击元素,可选择是否用js点击 :param by_js: 是否用js点击,为None时先用模拟点击,遇到遮挡改用js,为True时直接用js点击,为False时只用模拟点击 :param timeout: 模拟点击的超时时间,等待元素可见、不被遮挡、进入视口 @@ -35,34 +35,48 @@ class Clicker(object): if not by_js: # 模拟点击 can_click = False timeout = self._ele.page.timeout if timeout is None else timeout - if timeout == 0 and self._ele.states.has_rect: - self._ele.scroll.to_see() - if self._ele.states.is_in_viewport and self._ele.states.is_enabled and self._ele.states.is_displayed: - can_click = True + rect = None + if timeout == 0: + try: + self._ele.scroll.to_see() + if self._ele.states.is_enabled and self._ele.states.is_displayed: + rect = self._ele.locations.viewport_rect + can_click = True + except NoRectError: + if by_js is False: + raise else: + rect = self._ele.states.has_rect end_time = perf_counter() + timeout - while not self._ele.states.has_rect and perf_counter() < end_time: + while not rect and perf_counter() < end_time: + rect = self._ele.states.has_rect sleep(.001) - if self._ele.states.has_rect: + + self._ele.wait.stop_moving(timeout=end_time - perf_counter()) + if rect: self._ele.scroll.to_see() + rect = self._ele.locations.rect while perf_counter() < end_time: - if (self._ele.states.is_in_viewport and self._ele.states.is_enabled - and self._ele.states.is_displayed): + if self._ele.states.is_enabled and self._ele.states.is_displayed: can_click = True break sleep(.001) - if not self._ele.states.has_rect or not self._ele.states.is_in_viewport: + elif by_js is False: + raise NoRectError + + if can_click and not self._ele.states.is_in_viewport: by_js = True elif can_click and (by_js is False or not self._ele.states.is_covered): - vx, vy = self._ele.locations.click_point + x = int(rect[1][0] - (rect[1][0] - rect[0][0]) / 2) + y = rect[0][0] + 3 try: - r = self._ele.page.run_cdp('DOM.getNodeForLocation', x=vx, y=vy, includeUserAgentShadowDOM=True, + r = self._ele.page.run_cdp('DOM.getNodeForLocation', x=x, y=y, includeUserAgentShadowDOM=True, ignorePointerEventsNone=True) if r['backendNodeId'] != self._ele.ids.backend_id: - vx, vy = self._ele.locations.viewport_click_point + vx, vy = self._ele.locations.viewport_midpoint else: vx, vy = self._ele.locations.viewport_click_point diff --git a/DrissionPage/_units/element_states.py b/DrissionPage/_units/element_states.py index efc2b97..73d724d 100644 --- a/DrissionPage/_units/element_states.py +++ b/DrissionPage/_units/element_states.py @@ -1,4 +1,8 @@ # -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +""" from .._commons.web import location_in_viewport from ..errors import CDPError, NoRectError @@ -71,9 +75,9 @@ class ChromiumElementStates(object): @property def has_rect(self): - """返回元素是否拥有位置和大小,没有返回False,有返回大小元组""" + """返回元素是否拥有位置和大小,没有返回False,有返回四个角在页面中坐标组成的列表""" try: - return self._ele.size + return self._ele.locations.rect except NoRectError: return False diff --git a/DrissionPage/_units/element_states.pyi b/DrissionPage/_units/element_states.pyi index d512f7f..81ebad5 100644 --- a/DrissionPage/_units/element_states.pyi +++ b/DrissionPage/_units/element_states.pyi @@ -1,5 +1,9 @@ # -*- coding:utf-8 -*- -from typing import Union, Tuple +""" +@Author : g1879 +@Contact : g1879@qq.com +""" +from typing import Union, Tuple, List from .._elements.chromium_element import ChromiumShadowRoot, ChromiumElement @@ -33,7 +37,7 @@ class ChromiumElementStates(object): def is_covered(self) -> bool: ... @property - def has_rect(self) -> Union[bool, Tuple[int, int]]: ... + def has_rect(self) -> Union[bool, List[Tuple[float, float]]]: ... class ShadowRootStates(object): diff --git a/DrissionPage/_units/locations.py b/DrissionPage/_units/locations.py new file mode 100644 index 0000000..a402681 --- /dev/null +++ b/DrissionPage/_units/locations.py @@ -0,0 +1,103 @@ +# -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +""" + + +class Locations(object): + def __init__(self, ele): + """ + :param ele: ChromiumElement + """ + self._ele = ele + + @property + def location(self): + """返回元素左上角的绝对坐标""" + cl = self.viewport_location + return self._get_page_coord(cl[0], cl[1]) + + @property + def midpoint(self): + """返回元素中间点的绝对坐标""" + cl = self.viewport_midpoint + return self._get_page_coord(cl[0], cl[1]) + + @property + def click_point(self): + """返回元素接受点击的点的绝对坐标""" + cl = self.viewport_click_point + return self._get_page_coord(cl[0], cl[1]) + + @property + def viewport_location(self): + """返回元素左上角在视口中的坐标""" + m = self._get_viewport_rect('border') + return int(m[0]), int(m[1]) + + @property + def viewport_midpoint(self): + """返回元素中间点在视口中的坐标""" + m = self._get_viewport_rect('border') + return int(m[0] + (m[2] - m[0]) // 2), int(m[3] + (m[5] - m[3]) // 2) + + @property + def viewport_click_point(self): + """返回元素接受点击的点视口坐标""" + m = self._get_viewport_rect('padding') + return int(self.viewport_midpoint[0]), int(m[1]) + 3 + + @property + def screen_location(self): + """返回元素左上角在屏幕上坐标,左上角为(0, 0)""" + vx, vy = self._ele.page.rect.viewport_location + ex, ey = self.viewport_location + pr = self._ele.page.run_js('return window.devicePixelRatio;') + return int((vx + ex) * pr), int((ey + vy) * pr) + + @property + def screen_midpoint(self): + """返回元素中点在屏幕上坐标,左上角为(0, 0)""" + vx, vy = self._ele.page.rect.viewport_location + ex, ey = self.viewport_midpoint + pr = self._ele.page.run_js('return window.devicePixelRatio;') + return int((vx + ex) * pr), int((ey + vy) * pr) + + @property + def screen_click_point(self): + """返回元素中点在屏幕上坐标,左上角为(0, 0)""" + vx, vy = self._ele.page.rect.viewport_location + ex, ey = self.viewport_click_point + pr = self._ele.page.run_js('return window.devicePixelRatio;') + return int((vx + ex) * pr), int((ey + vy) * pr) + + @property + def rect(self): + """返回元素四个角坐标,顺序:坐上、右上、右下、左下,没有大小的元素抛出NoRectError""" + vr = self._get_viewport_rect('border') + r = self._ele.page.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport'] + sx = int(r['pageX']) + sy = int(r['pageY']) + return [(vr[0] + sx, vr[1] + sy), (vr[2] + sx, vr[3] + sy), + (vr[4] + sx, vr[5] + sy), (vr[6] + sx, vr[7] + sy)] + + @property + def viewport_rect(self): + """返回元素四个角视口坐标,顺序:坐上、右上、右下、左下,没有大小的元素抛出NoRectError""" + r = self._get_viewport_rect('border') + return [(int(r[0]), int(r[1])), (int(r[2]), int(r[3])), (int(r[4]), int(r[5])), (int(r[6]), int(r[7]))] + + def _get_viewport_rect(self, quad): + """按照类型返回在可视窗口中的范围 + :param quad: 方框类型,margin border padding + :return: 四个角坐标 + """ + return self._ele.page.run_cdp('DOM.getBoxModel', backendNodeId=self._ele.ids.backend_id)['model'][quad] + + def _get_page_coord(self, x, y): + """根据视口坐标获取绝对坐标""" + r = self._ele.page.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport'] + sx = r['pageX'] + sy = r['pageY'] + return x + sx, y + sy diff --git a/DrissionPage/_units/locations.pyi b/DrissionPage/_units/locations.pyi new file mode 100644 index 0000000..5e61165 --- /dev/null +++ b/DrissionPage/_units/locations.pyi @@ -0,0 +1,50 @@ +# -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +""" +from typing import Tuple, Union, List + +from .._elements.chromium_element import ChromiumElement + + +class Locations(object): + def __init__(self, ele: ChromiumElement): + self._ele: ChromiumElement = ... + + @property + def location(self) -> Tuple[int, int]: ... + + @property + def midpoint(self) -> Tuple[int, int]: ... + + @property + def click_point(self) -> Tuple[int, int]: ... + + @property + def viewport_location(self) -> Tuple[int, int]: ... + + @property + def viewport_midpoint(self) -> Tuple[int, int]: ... + + @property + def viewport_click_point(self) -> Tuple[int, int]: ... + + @property + def screen_location(self) -> Tuple[int, int]: ... + + @property + def screen_midpoint(self) -> Tuple[int, int]: ... + + @property + def screen_click_point(self) -> Tuple[int, int]: ... + + @property + def rect(self) -> List[Tuple[int, int], ...]: ... + + @property + def viewport_rect(self) -> List[Tuple[int, int], ...]: ... + + def _get_viewport_rect(self, quad: str) -> Union[list, None]: ... + + def _get_page_coord(self, x: int, y: int) -> Tuple[int, int]: ... From 457eb27566c0061980db9e128d1d1d3aaa8d5d0c Mon Sep 17 00:00:00 2001 From: g1879 Date: Wed, 8 Nov 2023 15:59:50 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=92=8C=E9=83=A8=E5=88=86=E7=B1=BB=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DrissionPage/_base/browser.py | 4 +- DrissionPage/_base/browser.pyi | 4 +- DrissionPage/_commons/browser.py | 3 +- DrissionPage/_elements/chromium_element.py | 151 +---------------- DrissionPage/_elements/chromium_element.pyi | 80 ++------- DrissionPage/_pages/chromium_base.py | 50 +----- DrissionPage/_pages/chromium_base.pyi | 21 +-- DrissionPage/_pages/chromium_frame.py | 54 +------ DrissionPage/_pages/chromium_frame.pyi | 41 ++--- DrissionPage/_pages/chromium_page.py | 8 +- DrissionPage/_pages/chromium_page.pyi | 10 +- DrissionPage/_pages/chromium_tab.py | 8 +- DrissionPage/_pages/chromium_tab.pyi | 4 +- DrissionPage/_units/download_manager.py | 2 +- DrissionPage/_units/download_manager.pyi | 6 +- DrissionPage/_units/element_states.py | 2 +- DrissionPage/_units/element_states.pyi | 2 +- DrissionPage/_units/ids.py | 57 +++++++ DrissionPage/_units/ids.pyi | 45 ++++++ DrissionPage/_units/scroller.py | 171 ++++++++++++++++++++ DrissionPage/_units/scroller.pyi | 73 +++++++++ DrissionPage/_units/tab_rect.py | 2 +- DrissionPage/_units/tab_rect.pyi | 2 +- DrissionPage/_units/waiter.py | 12 +- DrissionPage/_units/waiter.pyi | 10 +- DrissionPage/common.py | 2 + 26 files changed, 436 insertions(+), 388 deletions(-) create mode 100644 DrissionPage/_units/ids.py create mode 100644 DrissionPage/_units/ids.pyi create mode 100644 DrissionPage/_units/scroller.py create mode 100644 DrissionPage/_units/scroller.pyi diff --git a/DrissionPage/_base/browser.py b/DrissionPage/_base/browser.py index 1a0d932..dc4d4dd 100644 --- a/DrissionPage/_base/browser.py +++ b/DrissionPage/_base/browser.py @@ -6,7 +6,7 @@ from time import sleep from .chromium_driver import BrowserDriver, ChromiumDriver -from .._units.download_manager import BrowserDownloadManager +from .._units.download_manager import DownloadManager class Browser(object): @@ -77,7 +77,7 @@ class Browser(object): def connect_to_page(self): """执行与page相关的逻辑""" if not self._connected: - self._dl_mgr = BrowserDownloadManager(self) + self._dl_mgr = DownloadManager(self) self._connected = True def run_cdp(self, cmd, **cmd_args): diff --git a/DrissionPage/_base/browser.pyi b/DrissionPage/_base/browser.pyi index fed394c..a7a69c5 100644 --- a/DrissionPage/_base/browser.pyi +++ b/DrissionPage/_base/browser.pyi @@ -7,7 +7,7 @@ from typing import List, Optional, Union from .chromium_driver import BrowserDriver, ChromiumDriver from .._pages.chromium_page import ChromiumPage -from .._units.download_manager import BrowserDownloadManager +from .._units.download_manager import DownloadManager class Browser(object): @@ -19,7 +19,7 @@ class Browser(object): _frames: dict = ... _drivers: dict = ... _process_id: Optional[int] = ... - _dl_mgr: BrowserDownloadManager = ... + _dl_mgr: DownloadManager = ... _connected: bool = ... def __new__(cls, address: str, browser_id: str, page: ChromiumPage): ... diff --git a/DrissionPage/_commons/browser.py b/DrissionPage/_commons/browser.py index 25909bb..6a52559 100644 --- a/DrissionPage/_commons/browser.py +++ b/DrissionPage/_commons/browser.py @@ -169,7 +169,8 @@ def test_connect(ip, port, timeout=30): raise BrowserConnectError(f'\n{ip}:{port}浏览器无法链接。\n请确认:\n1、该端口为浏览器\n' f'2、已添加--remote-allow-origins=*和--remote-debugging-port={port}启动项\n' f'3、用户文件夹没有和已打开的浏览器冲突\n' - f'4、如为无界面系统,请使用headless模式\n' + f'4、如为无界面系统,请添加--headless=new参数\n' + f'5、如果是Linux系统,可能还要添加--no-sandbox启动参数\n' f'可使用ChromiumOptions设置端口和用户文件夹路径。') diff --git a/DrissionPage/_elements/chromium_element.py b/DrissionPage/_elements/chromium_element.py index 3b5c185..c57eb52 100644 --- a/DrissionPage/_elements/chromium_element.py +++ b/DrissionPage/_elements/chromium_element.py @@ -15,11 +15,13 @@ from .._commons.locator import get_loc from .._commons.tools import get_usable_path from .._commons.web import make_absolute_link, get_ele_txt, format_html, is_js_func, offset_scroll from .._units.clicker import Clicker -from .._units.element_states import ChromiumElementStates, ShadowRootStates +from .._units.element_states import ElementStates, ShadowRootStates +from .._units.ids import Ids, ElementIds from .._units.locations import Locations +from .._units.scroller import ElementScroller from .._units.select_element import SelectElement from .._units.setter import ChromiumElementSetter -from .._units.waiter import ChromiumElementWaiter +from .._units.waiter import ElementWaiter from ..errors import (ContextLossError, ElementLossError, JavaScriptError, ElementNotFoundError, CDPError, NoResourceError, AlertExistsError) @@ -64,7 +66,7 @@ class ChromiumElement(DrissionElement): else: raise ElementLossError - self._ids = ChromiumElementIds(self) + self._ids = ElementIds(self) doc = self.run_js('return this.ownerDocument;') self._doc_id = doc['objectId'] if doc else None @@ -141,7 +143,7 @@ class ChromiumElement(DrissionElement): def states(self): """返回用于获取元素状态的对象""" if self._states is None: - self._states = ChromiumElementStates(self) + self._states = ElementStates(self) return self._states @property @@ -181,7 +183,7 @@ class ChromiumElement(DrissionElement): def scroll(self): """用于滚动滚动条的对象""" if self._scroll is None: - self._scroll = ChromiumElementScroll(self) + self._scroll = ElementScroller(self) return self._scroll @property @@ -195,7 +197,7 @@ class ChromiumElement(DrissionElement): def wait(self): """返回用于等待的对象""" if self._wait is None: - self._wait = ChromiumElementWaiter(self.page, self) + self._wait = ElementWaiter(self.page, self) return self._wait @property @@ -1081,33 +1083,6 @@ class ChromiumShadowRoot(BaseElement): return r['backendNodeId'] -class Ids(object): - def __init__(self, ele): - self._ele = ele - - @property - def node_id(self): - """返回元素cdp中的node id""" - return self._ele._node_id - - @property - def obj_id(self): - """返回元素js中的object id""" - return self._ele._obj_id - - @property - def backend_id(self): - """返回backend id""" - return self._ele._backend_id - - -class ChromiumElementIds(Ids): - @property - def doc_id(self): - """返回所在document的object id""" - return self._ele._doc_id - - def find_in_chromium_ele(ele, loc, single=True, timeout=None, relative=True): """在chromium元素中查找 :param ele: ChromiumElement对象 @@ -1439,116 +1414,6 @@ def send_key(ele, modifier, key): ele.page.run_cdp('Input.dispatchKeyEvent', **data) -class ChromiumScroll(object): - """用于滚动的对象""" - - def __init__(self, ele): - """ - :param ele: 元素对象 - """ - self._driver = ele - self.t1 = self.t2 = 'this' - self._wait_complete = False - - def _run_js(self, js): - js = js.format(self.t1, self.t2, self.t2) - self._driver.run_js(js) - self._wait_scrolled() - - def to_top(self): - """滚动到顶端,水平位置不变""" - self._run_js('{}.scrollTo({}.scrollLeft, 0);') - - def to_bottom(self): - """滚动到底端,水平位置不变""" - self._run_js('{}.scrollTo({}.scrollLeft, {}.scrollHeight);') - - def to_half(self): - """滚动到垂直中间位置,水平位置不变""" - self._run_js('{}.scrollTo({}.scrollLeft, {}.scrollHeight/2);') - - def to_rightmost(self): - """滚动到最右边,垂直位置不变""" - self._run_js('{}.scrollTo({}.scrollWidth, {}.scrollTop);') - - def to_leftmost(self): - """滚动到最左边,垂直位置不变""" - self._run_js('{}.scrollTo(0, {}.scrollTop);') - - def to_location(self, x, y): - """滚动到指定位置 - :param x: 水平距离 - :param y: 垂直距离 - :return: None - """ - self._run_js(f'{{}}.scrollTo({x}, {y});') - - def up(self, pixel=300): - """向上滚动若干像素,水平位置不变 - :param pixel: 滚动的像素 - :return: None - """ - pixel = -pixel - self._run_js(f'{{}}.scrollBy(0, {pixel});') - - def down(self, pixel=300): - """向下滚动若干像素,水平位置不变 - :param pixel: 滚动的像素 - :return: None - """ - self._run_js(f'{{}}.scrollBy(0, {pixel});') - - def left(self, pixel=300): - """向左滚动若干像素,垂直位置不变 - :param pixel: 滚动的像素 - :return: None - """ - pixel = -pixel - self._run_js(f'{{}}.scrollBy({pixel}, 0);') - - def right(self, pixel=300): - """向右滚动若干像素,垂直位置不变 - :param pixel: 滚动的像素 - :return: None - """ - self._run_js(f'{{}}.scrollBy({pixel}, 0);') - - def _wait_scrolled(self): - if not self._wait_complete: - return - - page = self._driver.page if isinstance(self._driver, ChromiumElement) else self._driver - r = page.run_cdp('Page.getLayoutMetrics') - x = r['layoutViewport']['pageX'] - y = r['layoutViewport']['pageY'] - - end_time = perf_counter() + self._driver.page.timeout - while perf_counter() < end_time: - sleep(.1) - r = page.run_cdp('Page.getLayoutMetrics') - x1 = r['layoutViewport']['pageX'] - y1 = r['layoutViewport']['pageY'] - - if x == x1 and y == y1: - break - - x = x1 - y = y1 - - -class ChromiumElementScroll(ChromiumScroll): - def to_see(self, center=None): - """滚动页面直到元素可见 - :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 - :return: None - """ - self._driver.page.scroll.to_see(self._driver, center=center) - - def to_center(self): - """元素尽量滚动到视口中间""" - self._driver.page.scroll.to_see(self._driver, center=True) - - class Pseudo(object): def __init__(self, ele): """ diff --git a/DrissionPage/_elements/chromium_element.pyi b/DrissionPage/_elements/chromium_element.pyi index c41464f..cf64a41 100644 --- a/DrissionPage/_elements/chromium_element.pyi +++ b/DrissionPage/_elements/chromium_element.pyi @@ -14,11 +14,13 @@ from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_page import ChromiumPage from .._pages.web_page import WebPage from .._units.clicker import Clicker -from .._units.element_states import ShadowRootStates, ChromiumElementStates +from .._units.element_states import ShadowRootStates, ElementStates +from .._units.ids import Ids, ElementIds from .._units.locations import Locations +from .._units.scroller import ElementScroller from .._units.select_element import SelectElement from .._units.setter import ChromiumElementSetter -from .._units.waiter import ChromiumElementWaiter +from .._units.waiter import ElementWaiter class ChromiumElement(DrissionElement): @@ -32,14 +34,14 @@ class ChromiumElement(DrissionElement): self._obj_id: str = ... self._backend_id: str = ... self._doc_id: str = ... - self._ids: ChromiumElementIds = ... - self._scroll: ChromiumElementScroll = ... + self._ids: ElementIds = ... + self._scroll: ElementScroller = ... self._clicker: Clicker = ... self._select: SelectElement = ... - self._wait: ChromiumElementWaiter = ... + self._wait: ElementWaiter = ... self._locations: Locations = ... self._set: ChromiumElementSetter = ... - self._states: ChromiumElementStates = ... + self._states: ElementStates = ... self._pseudo: Pseudo = ... def __repr__(self) -> str: ... @@ -68,7 +70,7 @@ class ChromiumElement(DrissionElement): # -----------------d模式独有属性------------------- @property - def ids(self) -> ChromiumElementIds: ... + def ids(self) -> ElementIds: ... @property def size(self) -> Tuple[int, int]: ... @@ -77,7 +79,7 @@ class ChromiumElement(DrissionElement): def set(self) -> ChromiumElementSetter: ... @property - def states(self) -> ChromiumElementStates: ... + def states(self) -> ElementStates: ... @property def location(self) -> Tuple[int, int]: ... @@ -95,7 +97,7 @@ class ChromiumElement(DrissionElement): def sr(self) -> Union[None, ChromiumShadowRoot]: ... @property - def scroll(self) -> ChromiumElementScroll: ... + def scroll(self) -> ElementScroller: ... @property def click(self) -> Clicker: ... @@ -148,7 +150,7 @@ class ChromiumElement(DrissionElement): ele_only: bool = True) -> List[Union[ChromiumElement, str]]: ... @property - def wait(self) -> ChromiumElementWaiter: ... + def wait(self) -> ElementWaiter: ... @property def select(self) -> SelectElement: ... @@ -301,25 +303,6 @@ class ChromiumShadowRoot(BaseElement): def _get_backend_id(self, node_id: str) -> str: ... -class Ids(object): - def __init__(self, ele: Union[ChromiumElement, ChromiumShadowRoot]): - self._ele: Union[ChromiumElement, ChromiumShadowRoot] = ... - - @property - def node_id(self) -> str: ... - - @property - def obj_id(self) -> str: ... - - @property - def backend_id(self) -> str: ... - - -class ChromiumElementIds(Ids): - @property - def doc_id(self) -> str: ... - - def find_in_chromium_ele(ele: ChromiumElement, loc: Union[str, Tuple[str, str]], single: bool = True, @@ -364,45 +347,6 @@ def send_enter(ele: ChromiumElement) -> None: ... def send_key(ele: ChromiumElement, modifier: int, key: str) -> None: ... -class ChromiumScroll(object): - def __init__(self, page_or_ele: Union[ChromiumBase, ChromiumElement, ChromiumFrame]): - self.t1: str = ... - self.t2: str = ... - self._driver: Union[ChromiumPage, ChromiumElement, ChromiumFrame] = ... - self._wait_complete: bool = ... - - def _run_js(self, js: str): ... - - def to_top(self) -> None: ... - - def to_bottom(self) -> None: ... - - def to_half(self) -> None: ... - - def to_rightmost(self) -> None: ... - - def to_leftmost(self) -> None: ... - - def to_location(self, x: int, y: int) -> None: ... - - def up(self, pixel: int = 300) -> None: ... - - def down(self, pixel: int = 300) -> None: ... - - def left(self, pixel: int = 300) -> None: ... - - def right(self, pixel: int = 300) -> None: ... - - def _wait_scrolled(self) -> None: ... - - -class ChromiumElementScroll(ChromiumScroll): - - def to_see(self, center: Union[bool, None] = None) -> None: ... - - def to_center(self) -> None: ... - - class Pseudo(object): def __init__(self, ele: ChromiumElement): self._ele: ChromiumElement = ... diff --git a/DrissionPage/_pages/chromium_base.py b/DrissionPage/_pages/chromium_base.py index e2c5b37..9b5f141 100644 --- a/DrissionPage/_pages/chromium_base.py +++ b/DrissionPage/_pages/chromium_base.py @@ -10,18 +10,19 @@ from re import findall from threading import Thread from time import perf_counter, sleep +from .._units.scroller import PageScroller from .._base.base import BasePage from .._commons.constants import ERROR, NoneElement from .._commons.locator import get_loc from .._commons.tools import get_usable_path from .._commons.web import location_in_viewport -from .._elements.chromium_element import ChromiumScroll, ChromiumElement, run_js, make_chromium_ele +from .._elements.chromium_element import ChromiumElement, run_js, make_chromium_ele from .._elements.session_element import make_session_ele from .._units.action_chains import ActionChains from .._units.network_listener import NetworkListener from .._units.screencast import Screencast from .._units.setter import ChromiumBaseSetter -from .._units.waiter import ChromiumBaseWaiter +from .._units.waiter import BaseWaiter from ..errors import (ContextLossError, ElementLossError, CDPError, TabClosedError, NoRectError, AlertExistsError, GetDocumentError) @@ -116,7 +117,6 @@ class ChromiumBase(BasePage): self._driver.set_listener('Page.frameAttached', self._onFrameAttached) self._driver.set_listener('Page.frameDetached', self._onFrameDetached) - def _get_document(self): if self._is_reading: return @@ -326,7 +326,7 @@ class ChromiumBase(BasePage): """返回用于滚动滚动条的对象""" self.wait.load_complete() if self._scroll is None: - self._scroll = ChromiumPageScroll(self) + self._scroll = PageScroller(self) return self._scroll @property @@ -343,7 +343,7 @@ class ChromiumBase(BasePage): def wait(self): """返回用于等待的对象""" if self._wait is None: - self._wait = ChromiumBaseWaiter(self) + self._wait = BaseWaiter(self) return self._wait @property @@ -971,46 +971,6 @@ class ChromiumBase(BasePage): return str(path.absolute()) -class ChromiumPageScroll(ChromiumScroll): - def __init__(self, page): - """ - :param page: 页面对象 - """ - super().__init__(page) - self.t1 = 'window' - self.t2 = 'document.documentElement' - - def to_see(self, loc_or_ele, center=None): - """滚动页面直到元素可见 - :param loc_or_ele: 元素的定位信息,可以是loc元组,或查询字符串 - :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 - :return: None - """ - ele = self._driver._ele(loc_or_ele) - self._to_see(ele, center) - - def _to_see(self, ele, center): - """执行滚动页面直到元素可见 - :param ele: 元素对象 - :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 - :return: None - """ - 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; - if (document.documentElement && document.documentElement.scrollTop) { - scroll_top = document.documentElement.scrollTop; - } else if (document.body) {scroll_top = document.body.scrollTop;} - return scroll_top;} - const { top, height } = this.getBoundingClientRect(); - const elCenter = top + height / 2; - const center = window.innerHeight / 2; - window.scrollTo({top: getWindowScrollTop() - (center - elCenter), - behavior: 'instant'});''') - self._wait_scrolled() - - class Timeout(object): """用于保存d模式timeout信息的类""" diff --git a/DrissionPage/_pages/chromium_base.pyi b/DrissionPage/_pages/chromium_base.pyi index 9a8aed2..826e95e 100644 --- a/DrissionPage/_pages/chromium_base.pyi +++ b/DrissionPage/_pages/chromium_base.pyi @@ -10,15 +10,16 @@ from .._base.base import BasePage from .._base.browser import Browser from .._base.chromium_driver import ChromiumDriver from .._commons.constants import NoneElement -from .._elements.chromium_element import ChromiumElement, ChromiumScroll +from .._elements.chromium_element import ChromiumElement from .._elements.session_element import SessionElement from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_page import ChromiumPage from .._units.action_chains import ActionChains from .._units.network_listener import NetworkListener from .._units.screencast import Screencast +from .._units.scroller import Scroller, PageScroller from .._units.setter import ChromiumBaseSetter -from .._units.waiter import ChromiumBaseWaiter +from .._units.waiter import BaseWaiter class ChromiumBase(BasePage): @@ -37,12 +38,12 @@ class ChromiumBase(BasePage): self._first_run: bool = ... self._is_loading: bool = ... self._page_load_strategy: str = ... - self._scroll: ChromiumScroll = ... + self._scroll: Scroller = ... self._url: str = ... self._root_id: str = ... self._debug: bool = ... self._upload_list: list = ... - self._wait: ChromiumBaseWaiter = ... + self._wait: BaseWaiter = ... self._set: ChromiumBaseSetter = ... self._screencast: Screencast = ... self._actions: ActionChains = ... @@ -137,7 +138,7 @@ class ChromiumBase(BasePage): def user_agent(self) -> str: ... @property - def scroll(self) -> ChromiumPageScroll: ... + def scroll(self) -> PageScroller: ... @property def timeouts(self) -> Timeout: ... @@ -146,7 +147,7 @@ class ChromiumBase(BasePage): def upload_list(self) -> list: ... @property - def wait(self) -> ChromiumBaseWaiter: ... + def wait(self) -> BaseWaiter: ... @property def set(self) -> ChromiumBaseSetter: ... @@ -247,14 +248,6 @@ class ChromiumBase(BasePage): timeout: float = None) -> Union[bool, None]: ... -class ChromiumPageScroll(ChromiumScroll): - def __init__(self, page: ChromiumBase): ... - - def to_see(self, loc_or_ele: Union[str, tuple, ChromiumElement], center: Union[bool, None] = None) -> None: ... - - def _to_see(self, ele: ChromiumElement, center: Union[bool, None]) -> None: ... - - class Timeout(object): def __init__(self, page: ChromiumBase, implicit=None, page_load=None, script=None): diff --git a/DrissionPage/_pages/chromium_frame.py b/DrissionPage/_pages/chromium_frame.py index 3ca9b25..1d412b8 100644 --- a/DrissionPage/_pages/chromium_frame.py +++ b/DrissionPage/_pages/chromium_frame.py @@ -8,10 +8,10 @@ from re import search, findall from threading import Thread from time import sleep, perf_counter -from requests import get - from .._elements.chromium_element import ChromiumElement -from .._pages.chromium_base import ChromiumBase, ChromiumPageScroll +from .._pages.chromium_base import ChromiumBase +from .._units.ids import FrameIds +from .._units.scroller import FrameScroller from .._units.setter import ChromiumFrameSetter from .._units.waiter import FrameWaiter from ..errors import ContextLossError, ElementLossError, GetDocumentError @@ -40,7 +40,7 @@ class ChromiumFrame(ChromiumBase): self._backend_id = ele.ids.backend_id self._frame_ele = ele self._states = None - self._ids = ChromiumFrameIds(self) + self._ids = FrameIds(self) if self._is_inner_frame(): self._is_diff_domain = False @@ -329,7 +329,7 @@ class ChromiumFrame(ChromiumBase): @property def scroll(self): """返回用于等待的对象""" - return ChromiumFrameScroll(self) + return FrameScroller(self) @property def set(self): @@ -658,47 +658,3 @@ class ChromiumFrame(ChromiumBase): while self.is_alive: sleep(1) self.driver.stop() - - -class ChromiumFrameIds(object): - def __init__(self, frame): - self._frame = frame - - @property - def tab_id(self): - """返回当前标签页id""" - return self._frame._tab_id - - @property - def backend_id(self): - """返回cdp中的node id""" - return self._frame._backend_id - - @property - def obj_id(self): - """返回frame元素的object id""" - return self._frame.frame_ele.ids.obj_id - - @property - def node_id(self): - """返回cdp中的node id""" - return self._frame.frame_ele.ids.node_id - - -class ChromiumFrameScroll(ChromiumPageScroll): - def __init__(self, frame): - """ - :param frame: ChromiumFrame对象 - """ - self._driver = frame.doc_ele - self.t1 = self.t2 = 'this.documentElement' - self._wait_complete = False - - def to_see(self, loc_or_ele, center=None): - """滚动页面直到元素可见 - :param loc_or_ele: 元素的定位信息,可以是loc元组,或查询字符串 - :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 - :return: None - """ - ele = loc_or_ele if isinstance(loc_or_ele, ChromiumElement) else self._driver._ele(loc_or_ele) - self._to_see(ele, center) diff --git a/DrissionPage/_pages/chromium_frame.pyi b/DrissionPage/_pages/chromium_frame.pyi index 4121917..3e63295 100644 --- a/DrissionPage/_pages/chromium_frame.pyi +++ b/DrissionPage/_pages/chromium_frame.pyi @@ -6,11 +6,15 @@ from pathlib import Path from typing import Union, Tuple, List, Any -from .chromium_base import ChromiumBase, ChromiumPageScroll +from .chromium_base import ChromiumBase from .chromium_page import ChromiumPage from .chromium_tab import ChromiumTab from .web_page import WebPage -from .._elements.chromium_element import ChromiumElement, Locations, ChromiumElementStates +from .._elements.chromium_element import ChromiumElement +from .._units.element_states import ElementStates +from .._units.ids import FrameIds +from .._units.locations import Locations +from .._units.scroller import FrameScroller from .._units.setter import ChromiumFrameSetter from .._units.waiter import FrameWaiter @@ -28,8 +32,8 @@ class ChromiumFrame(ChromiumBase): self._doc_ele: ChromiumElement = ... self._is_diff_domain: bool = ... self.doc_ele: ChromiumElement = ... - self._states: ChromiumElementStates = ... - self._ids: ChromiumFrameIds = ... + self._states: ElementStates = ... + self._ids: FrameIds = ... def __call__(self, loc_or_str: Union[Tuple[str, str], str], @@ -57,7 +61,7 @@ class ChromiumFrame(ChromiumBase): def page(self) -> Union[ChromiumPage, WebPage]: ... @property - def ids(self) -> ChromiumFrameIds: ... + def ids(self) -> FrameIds: ... @property def frame_ele(self) -> ChromiumElement: ... @@ -111,13 +115,13 @@ class ChromiumFrame(ChromiumBase): def is_alive(self) -> bool: ... @property - def scroll(self) -> ChromiumFrameScroll: ... + def scroll(self) -> FrameScroller: ... @property def set(self) -> ChromiumFrameSetter: ... @property - def states(self) -> ChromiumElementStates: ... + def states(self) -> ElementStates: ... @property def wait(self) -> FrameWaiter: ... @@ -197,26 +201,3 @@ class ChromiumFrame(ChromiumBase): timeout: float = None) -> Union[bool, None]: ... def _is_inner_frame(self) -> bool: ... - - -class ChromiumFrameIds(object): - def __init__(self, frame: ChromiumFrame): - self._frame: ChromiumFrame = ... - - @property - def tab_id(self) -> str: ... - - @property - def backend_id(self) -> str: ... - - @property - def obj_id(self) -> str: ... - - @property - def node_id(self) -> str: ... - - -class ChromiumFrameScroll(ChromiumPageScroll): - def __init__(self, frame: ChromiumFrame) -> None: ... - - def to_see(self, loc_or_ele: Union[str, tuple, ChromiumElement], center: Union[None, bool] = None) -> None: ... diff --git a/DrissionPage/_pages/chromium_page.py b/DrissionPage/_pages/chromium_page.py index e19fbb6..ae06dd4 100644 --- a/DrissionPage/_pages/chromium_page.py +++ b/DrissionPage/_pages/chromium_page.py @@ -14,8 +14,8 @@ from .._configs.chromium_options import ChromiumOptions from .._pages.chromium_base import ChromiumBase, Timeout from .._pages.chromium_tab import ChromiumTab from .._units.setter import ChromiumPageSetter -from .._units.tab_rect import ChromiumTabRect -from .._units.waiter import ChromiumPageWaiter +from .._units.tab_rect import TabRect +from .._units.waiter import PageWaiter from ..errors import BrowserConnectError @@ -127,14 +127,14 @@ class ChromiumPage(ChromiumBase): @property def rect(self): if self._rect is None: - self._rect = ChromiumTabRect(self) + self._rect = TabRect(self) return self._rect @property def wait(self): """返回用于等待的对象""" if self._wait is None: - self._wait = ChromiumPageWaiter(self) + self._wait = PageWaiter(self) return self._wait def get_tab(self, tab_id=None): diff --git a/DrissionPage/_pages/chromium_page.pyi b/DrissionPage/_pages/chromium_page.pyi index 5295ba0..a5f5ef7 100644 --- a/DrissionPage/_pages/chromium_page.pyi +++ b/DrissionPage/_pages/chromium_page.pyi @@ -10,8 +10,8 @@ from .._configs.chromium_options import ChromiumOptions from .._pages.chromium_base import ChromiumBase from .._pages.chromium_tab import ChromiumTab from .._units.setter import ChromiumPageSetter -from .._units.tab_rect import ChromiumTabRect -from .._units.waiter import ChromiumPageWaiter +from .._units.tab_rect import TabRect +from .._units.waiter import PageWaiter class ChromiumPage(ChromiumBase): @@ -23,7 +23,7 @@ class ChromiumPage(ChromiumBase): self._driver_options: ChromiumOptions = ... self._main_tab: str = ... self._browser: Browser = ... - self._rect: Optional[ChromiumTabRect] = ... + self._rect: Optional[TabRect] = ... def _handle_options(self, addr_or_opts: Union[str, ChromiumOptions]) -> str: ... @@ -41,10 +41,10 @@ class ChromiumPage(ChromiumBase): def tabs(self) -> List[str]: ... @property - def rect(self) -> ChromiumTabRect: ... + def rect(self) -> TabRect: ... @property - def wait(self) -> ChromiumPageWaiter: ... + def wait(self) -> PageWaiter: ... @property def main_tab(self) -> str: ... diff --git a/DrissionPage/_pages/chromium_tab.py b/DrissionPage/_pages/chromium_tab.py index 8944559..b65bb03 100644 --- a/DrissionPage/_pages/chromium_tab.py +++ b/DrissionPage/_pages/chromium_tab.py @@ -10,8 +10,8 @@ from .._commons.web import set_session_cookies, set_browser_cookies from .._pages.chromium_base import ChromiumBase from .._pages.session_page import SessionPage from .._units.setter import TabSetter, WebPageTabSetter -from .._units.tab_rect import ChromiumTabRect -from .._units.waiter import ChromiumTabWaiter +from .._units.tab_rect import TabRect +from .._units.waiter import TabWaiter class ChromiumTab(ChromiumBase): @@ -48,7 +48,7 @@ class ChromiumTab(ChromiumBase): def rect(self): """返回获取窗口坐标和大小的对象""" if self._rect is None: - self._rect = ChromiumTabRect(self) + self._rect = TabRect(self) return self._rect @property @@ -62,7 +62,7 @@ class ChromiumTab(ChromiumBase): def wait(self): """返回用于等待的对象""" if self._wait is None: - self._wait = ChromiumTabWaiter(self) + self._wait = TabWaiter(self) return self._wait diff --git a/DrissionPage/_pages/chromium_tab.pyi b/DrissionPage/_pages/chromium_tab.pyi index 923c8c7..ef034b3 100644 --- a/DrissionPage/_pages/chromium_tab.pyi +++ b/DrissionPage/_pages/chromium_tab.pyi @@ -17,7 +17,7 @@ from .._base.browser import Browser from .._elements.chromium_element import ChromiumElement from .._elements.session_element import SessionElement from .._units.setter import TabSetter, WebPageTabSetter -from .._units.waiter import ChromiumTabWaiter +from .._units.waiter import TabWaiter class ChromiumTab(ChromiumBase): @@ -41,7 +41,7 @@ class ChromiumTab(ChromiumBase): def set(self) -> TabSetter: ... @property - def wait(self) -> ChromiumTabWaiter: ... + def wait(self) -> TabWaiter: ... class WebPageTab(SessionPage, ChromiumTab): diff --git a/DrissionPage/_units/download_manager.py b/DrissionPage/_units/download_manager.py index a36d362..bb04860 100644 --- a/DrissionPage/_units/download_manager.py +++ b/DrissionPage/_units/download_manager.py @@ -11,7 +11,7 @@ from time import sleep, perf_counter from .._commons.tools import get_usable_path -class BrowserDownloadManager(object): +class DownloadManager(object): def __init__(self, browser): """ diff --git a/DrissionPage/_units/download_manager.pyi b/DrissionPage/_units/download_manager.pyi index c77be03..466270b 100644 --- a/DrissionPage/_units/download_manager.pyi +++ b/DrissionPage/_units/download_manager.pyi @@ -5,7 +5,7 @@ from .._base.browser import Browser from .._pages.chromium_page import ChromiumPage -class BrowserDownloadManager(object): +class DownloadManager(object): _browser: Browser = ... _page: ChromiumPage = ... _missions: Dict[str, DownloadMission] = ... @@ -56,7 +56,7 @@ class TabDownloadSettings(object): class DownloadMission(object): tab_id: str = ... - _mgr: BrowserDownloadManager = ... + _mgr: DownloadManager = ... url: str = ... id: str = ... path: str = ... @@ -68,7 +68,7 @@ class DownloadMission(object): save_path: str = ... _is_done: bool = ... - def __init__(self, mgr: BrowserDownloadManager, tab_id: str, _id: str, path: str, name: str, url: str, + def __init__(self, mgr: DownloadManager, tab_id: str, _id: str, path: str, name: str, url: str, save_path: str): ... @property diff --git a/DrissionPage/_units/element_states.py b/DrissionPage/_units/element_states.py index 73d724d..8f3b524 100644 --- a/DrissionPage/_units/element_states.py +++ b/DrissionPage/_units/element_states.py @@ -7,7 +7,7 @@ from .._commons.web import location_in_viewport from ..errors import CDPError, NoRectError -class ChromiumElementStates(object): +class ElementStates(object): def __init__(self, ele): """ :param ele: ChromiumElement diff --git a/DrissionPage/_units/element_states.pyi b/DrissionPage/_units/element_states.pyi index 81ebad5..5c2485f 100644 --- a/DrissionPage/_units/element_states.pyi +++ b/DrissionPage/_units/element_states.pyi @@ -8,7 +8,7 @@ from typing import Union, Tuple, List from .._elements.chromium_element import ChromiumShadowRoot, ChromiumElement -class ChromiumElementStates(object): +class ElementStates(object): def __init__(self, ele: ChromiumElement): self._ele: ChromiumElement = ... diff --git a/DrissionPage/_units/ids.py b/DrissionPage/_units/ids.py new file mode 100644 index 0000000..5e1ee80 --- /dev/null +++ b/DrissionPage/_units/ids.py @@ -0,0 +1,57 @@ +# -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +""" + + +class Ids(object): + def __init__(self, ele): + self._ele = ele + + @property + def node_id(self): + """返回元素cdp中的node id""" + return self._ele._node_id + + @property + def obj_id(self): + """返回元素js中的object id""" + return self._ele._obj_id + + @property + def backend_id(self): + """返回backend id""" + return self._ele._backend_id + + +class ElementIds(Ids): + @property + def doc_id(self): + """返回所在document的object id""" + return self._ele._doc_id + + +class FrameIds(object): + def __init__(self, frame): + self._frame = frame + + @property + def tab_id(self): + """返回当前标签页id""" + return self._frame._tab_id + + @property + def backend_id(self): + """返回cdp中的node id""" + return self._frame._backend_id + + @property + def obj_id(self): + """返回frame元素的object id""" + return self._frame.frame_ele.ids.obj_id + + @property + def node_id(self): + """返回cdp中的node id""" + return self._frame.frame_ele.ids.node_id diff --git a/DrissionPage/_units/ids.pyi b/DrissionPage/_units/ids.pyi new file mode 100644 index 0000000..f224259 --- /dev/null +++ b/DrissionPage/_units/ids.pyi @@ -0,0 +1,45 @@ +# -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +""" +from typing import Union + +from .._pages.chromium_frame import ChromiumFrame +from .._elements.chromium_element import ChromiumElement, ChromiumShadowRoot + + +class Ids(object): + def __init__(self, ele: Union[ChromiumElement, ChromiumShadowRoot]): + self._ele: Union[ChromiumElement, ChromiumShadowRoot] = ... + + @property + def node_id(self) -> str: ... + + @property + def obj_id(self) -> str: ... + + @property + def backend_id(self) -> str: ... + + +class ElementIds(Ids): + @property + def doc_id(self) -> str: ... + + +class FrameIds(object): + def __init__(self, frame: ChromiumFrame): + self._frame: ChromiumFrame = ... + + @property + def tab_id(self) -> str: ... + + @property + def backend_id(self) -> str: ... + + @property + def obj_id(self) -> str: ... + + @property + def node_id(self) -> str: ... diff --git a/DrissionPage/_units/scroller.py b/DrissionPage/_units/scroller.py new file mode 100644 index 0000000..1771468 --- /dev/null +++ b/DrissionPage/_units/scroller.py @@ -0,0 +1,171 @@ +# -*- coding:utf-8 -*- +from time import sleep, perf_counter + + +class Scroller(object): + """用于滚动的对象""" + + def __init__(self, ele): + """ + :param ele: 元素对象 + """ + self._driver = ele + self.t1 = self.t2 = 'this' + self._wait_complete = False + + def _run_js(self, js): + js = js.format(self.t1, self.t2, self.t2) + self._driver.run_js(js) + self._wait_scrolled() + + def to_top(self): + """滚动到顶端,水平位置不变""" + self._run_js('{}.scrollTo({}.scrollLeft, 0);') + + def to_bottom(self): + """滚动到底端,水平位置不变""" + self._run_js('{}.scrollTo({}.scrollLeft, {}.scrollHeight);') + + def to_half(self): + """滚动到垂直中间位置,水平位置不变""" + self._run_js('{}.scrollTo({}.scrollLeft, {}.scrollHeight/2);') + + def to_rightmost(self): + """滚动到最右边,垂直位置不变""" + self._run_js('{}.scrollTo({}.scrollWidth, {}.scrollTop);') + + def to_leftmost(self): + """滚动到最左边,垂直位置不变""" + self._run_js('{}.scrollTo(0, {}.scrollTop);') + + def to_location(self, x, y): + """滚动到指定位置 + :param x: 水平距离 + :param y: 垂直距离 + :return: None + """ + self._run_js(f'{{}}.scrollTo({x}, {y});') + + def up(self, pixel=300): + """向上滚动若干像素,水平位置不变 + :param pixel: 滚动的像素 + :return: None + """ + pixel = -pixel + self._run_js(f'{{}}.scrollBy(0, {pixel});') + + def down(self, pixel=300): + """向下滚动若干像素,水平位置不变 + :param pixel: 滚动的像素 + :return: None + """ + self._run_js(f'{{}}.scrollBy(0, {pixel});') + + def left(self, pixel=300): + """向左滚动若干像素,垂直位置不变 + :param pixel: 滚动的像素 + :return: None + """ + pixel = -pixel + self._run_js(f'{{}}.scrollBy({pixel}, 0);') + + def right(self, pixel=300): + """向右滚动若干像素,垂直位置不变 + :param pixel: 滚动的像素 + :return: None + """ + self._run_js(f'{{}}.scrollBy({pixel}, 0);') + + def _wait_scrolled(self): + if not self._wait_complete: + return + + page = self._driver.page if 'ChromiumElement' in str(type(self._driver)) else self._driver + r = page.run_cdp('Page.getLayoutMetrics') + x = r['layoutViewport']['pageX'] + y = r['layoutViewport']['pageY'] + + end_time = perf_counter() + self._driver.page.timeout + while perf_counter() < end_time: + sleep(.1) + r = page.run_cdp('Page.getLayoutMetrics') + x1 = r['layoutViewport']['pageX'] + y1 = r['layoutViewport']['pageY'] + + if x == x1 and y == y1: + break + + x = x1 + y = y1 + + +class ElementScroller(Scroller): + def to_see(self, center=None): + """滚动页面直到元素可见 + :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 + :return: None + """ + self._driver.page.scroll.to_see(self._driver, center=center) + + def to_center(self): + """元素尽量滚动到视口中间""" + self._driver.page.scroll.to_see(self._driver, center=True) + + +class PageScroller(Scroller): + def __init__(self, page): + """ + :param page: 页面对象 + """ + super().__init__(page) + self.t1 = 'window' + self.t2 = 'document.documentElement' + + def to_see(self, loc_or_ele, center=None): + """滚动页面直到元素可见 + :param loc_or_ele: 元素的定位信息,可以是loc元组,或查询字符串 + :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 + :return: None + """ + ele = self._driver._ele(loc_or_ele) + self._to_see(ele, center) + + def _to_see(self, ele, center): + """执行滚动页面直到元素可见 + :param ele: 元素对象 + :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 + :return: None + """ + 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; + if (document.documentElement && document.documentElement.scrollTop) { + scroll_top = document.documentElement.scrollTop; + } else if (document.body) {scroll_top = document.body.scrollTop;} + return scroll_top;} + const { top, height } = this.getBoundingClientRect(); + const elCenter = top + height / 2; + const center = window.innerHeight / 2; + window.scrollTo({top: getWindowScrollTop() - (center - elCenter), + behavior: 'instant'});''') + self._wait_scrolled() + + +class FrameScroller(PageScroller): + def __init__(self, frame): + """ + :param frame: ChromiumFrame对象 + """ + self._driver = frame.doc_ele + self.t1 = self.t2 = 'this.documentElement' + self._wait_complete = False + + def to_see(self, loc_or_ele, center=None): + """滚动页面直到元素可见 + :param loc_or_ele: 元素的定位信息,可以是loc元组,或查询字符串 + :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 + :return: None + """ + ele = loc_or_ele if 'ChromiumElement' in str(type(loc_or_ele)) else self._driver._ele(loc_or_ele) + self._to_see(ele, center) diff --git a/DrissionPage/_units/scroller.pyi b/DrissionPage/_units/scroller.pyi new file mode 100644 index 0000000..438da32 --- /dev/null +++ b/DrissionPage/_units/scroller.pyi @@ -0,0 +1,73 @@ +# -*- coding:utf-8 -*- +from typing import Union + +from .._elements.chromium_element import ChromiumElement +from .._pages.chromium_base import ChromiumBase +from .._pages.chromium_frame import ChromiumFrame +from .._pages.chromium_page import ChromiumPage + + +class Scroller(object): + def __init__(self, page_or_ele: Union[ChromiumBase, ChromiumElement, ChromiumFrame]): + self.t1: str = ... + self.t2: str = ... + self._driver: Union[ChromiumPage, ChromiumElement, ChromiumFrame] = ... + self._wait_complete: bool = ... + + def _run_js(self, js: str): ... + + def to_top(self) -> None: ... + + def to_bottom(self) -> None: ... + + def to_half(self) -> None: ... + + def to_rightmost(self) -> None: ... + + def to_leftmost(self) -> None: ... + + def to_location(self, x: int, y: int) -> None: ... + + def up(self, pixel: int = 300) -> None: ... + + def down(self, pixel: int = 300) -> None: ... + + def left(self, pixel: int = 300) -> None: ... + + def right(self, pixel: int = 300) -> None: ... + + def _wait_scrolled(self) -> None: ... + + +class ElementScroller(Scroller): + + def to_see(self, center: Union[bool, None] = None) -> None: ... + + def to_center(self) -> None: ... + + +class PageScroller(Scroller): + def __init__(self, page: ChromiumBase): ... + + def to_see(self, loc_or_ele: Union[str, tuple, ChromiumElement], center: Union[bool, None] = None) -> None: ... + + def _to_see(self, ele: ChromiumElement, center: Union[bool, None]) -> None: ... + + +class FrameScroller(PageScroller): + def __init__(self, frame): + """ + :param frame: ChromiumFrame对象 + """ + self._driver = frame.doc_ele + self.t1 = self.t2 = 'this.documentElement' + self._wait_complete = False + + def to_see(self, loc_or_ele, center=None): + """滚动页面直到元素可见 + :param loc_or_ele: 元素的定位信息,可以是loc元组,或查询字符串 + :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 + :return: None + """ + ele = loc_or_ele if isinstance(loc_or_ele, ChromiumElement) else self._driver._ele(loc_or_ele) + self._to_see(ele, center) diff --git a/DrissionPage/_units/tab_rect.py b/DrissionPage/_units/tab_rect.py index df27036..b72107b 100644 --- a/DrissionPage/_units/tab_rect.py +++ b/DrissionPage/_units/tab_rect.py @@ -5,7 +5,7 @@ """ -class ChromiumTabRect(object): +class TabRect(object): def __init__(self, page): self._page = page diff --git a/DrissionPage/_units/tab_rect.pyi b/DrissionPage/_units/tab_rect.pyi index d21aa1e..6b80684 100644 --- a/DrissionPage/_units/tab_rect.pyi +++ b/DrissionPage/_units/tab_rect.pyi @@ -9,7 +9,7 @@ from .._pages.chromium_page import ChromiumPage from .._pages.chromium_tab import ChromiumTab -class ChromiumTabRect(object): +class TabRect(object): def __init__(self, page: Union[ChromiumPage, ChromiumTab]): self._page: Union[ChromiumPage, ChromiumTab] = ... diff --git a/DrissionPage/_units/waiter.py b/DrissionPage/_units/waiter.py index 8236b30..56de34e 100644 --- a/DrissionPage/_units/waiter.py +++ b/DrissionPage/_units/waiter.py @@ -5,7 +5,7 @@ from .._commons.constants import Settings from ..errors import WaitTimeoutError, NoRectError -class ChromiumBaseWaiter(object): +class BaseWaiter(object): def __init__(self, page_or_ele): """ :param page_or_ele: 页面对象或元素对象 @@ -190,7 +190,7 @@ class ChromiumBaseWaiter(object): return False -class ChromiumTabWaiter(ChromiumBaseWaiter): +class TabWaiter(BaseWaiter): def downloads_done(self, timeout=None, cancel_if_timeout=True): """等待所有浏览器下载任务结束 @@ -219,7 +219,7 @@ class ChromiumTabWaiter(ChromiumBaseWaiter): return True -class ChromiumPageWaiter(ChromiumTabWaiter): +class PageWaiter(TabWaiter): def __init__(self, page): super().__init__(page) # self._listener = None @@ -270,7 +270,7 @@ class ChromiumPageWaiter(ChromiumTabWaiter): return True -class ChromiumElementWaiter(object): +class ElementWaiter(object): """等待元素在dom中某种状态,如删除、显示、隐藏""" def __init__(self, page, ele): @@ -417,10 +417,10 @@ class ChromiumElementWaiter(object): return False -class FrameWaiter(ChromiumBaseWaiter, ChromiumElementWaiter): +class FrameWaiter(BaseWaiter, ElementWaiter): def __init__(self, frame): """ :param frame: ChromiumFrame对象 """ super().__init__(frame) - super(ChromiumBaseWaiter, self).__init__(frame, frame.frame_ele) + super(BaseWaiter, self).__init__(frame, frame.frame_ele) diff --git a/DrissionPage/_units/waiter.pyi b/DrissionPage/_units/waiter.pyi index 1f4da31..d353178 100644 --- a/DrissionPage/_units/waiter.pyi +++ b/DrissionPage/_units/waiter.pyi @@ -13,7 +13,7 @@ from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_page import ChromiumPage -class ChromiumBaseWaiter(object): +class BaseWaiter(object): def __init__(self, page: ChromiumBase): self._driver: ChromiumBase = ... @@ -54,12 +54,12 @@ class ChromiumBaseWaiter(object): raise_err: bool = None) -> bool: ... -class ChromiumTabWaiter(ChromiumBaseWaiter): +class TabWaiter(BaseWaiter): def downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ... -class ChromiumPageWaiter(ChromiumTabWaiter): +class PageWaiter(TabWaiter): _driver: ChromiumPage = ... def new_tab(self, timeout: float = None, raise_err: bool = None) -> Union[str, bool]: ... @@ -67,7 +67,7 @@ class ChromiumPageWaiter(ChromiumTabWaiter): def all_downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ... -class ChromiumElementWaiter(object): +class ElementWaiter(object): def __init__(self, page: ChromiumBase, ele: ChromiumElement): @@ -97,5 +97,5 @@ class ChromiumElementWaiter(object): def _wait_state(self, attr: str, mode: bool = False, timeout: float = None, raise_err: bool = None) -> bool: ... -class FrameWaiter(ChromiumBaseWaiter, ChromiumElementWaiter): +class FrameWaiter(BaseWaiter, ElementWaiter): def __init__(self, frame: ChromiumFrame): ... diff --git a/DrissionPage/common.py b/DrissionPage/common.py index 1dab5d2..72a0deb 100644 --- a/DrissionPage/common.py +++ b/DrissionPage/common.py @@ -9,3 +9,5 @@ from ._commons.keys import Keys from ._commons.by import By from ._commons.constants import Settings from ._commons.tools import wait_until + +__all__ = ['make_session_ele', 'ActionChains', 'Keys', 'By', 'Settings', 'wait_until']