From b4f94caad033d6f14e2214bbb7514d8fd9d66c63 Mon Sep 17 00:00:00 2001 From: g1879 Date: Mon, 1 Jul 2024 17:31:04 +0800 Subject: [PATCH] 4.0.5.4 --- DrissionPage/__init__.py | 2 +- DrissionPage/_base/base.py | 18 +- DrissionPage/_base/base.pyi | 3 +- DrissionPage/_configs/chromium_options.py | 50 +- DrissionPage/_configs/session_options.py | 11 - DrissionPage/_elements/chromium_element.py | 288 ++++++++--- DrissionPage/_elements/chromium_element.pyi | 46 +- DrissionPage/_elements/none_element.py | 9 + DrissionPage/_elements/session_element.py | 41 +- DrissionPage/_elements/session_element.pyi | 22 +- DrissionPage/_functions/elements.py | 507 ++++++++++++++++++++ DrissionPage/_functions/elements.pyi | 220 +++++++++ DrissionPage/_functions/locator.py | 274 +++++++---- DrissionPage/_functions/locator.pyi | 6 + DrissionPage/_functions/web.py | 14 +- DrissionPage/_pages/chromium_base.py | 57 +-- DrissionPage/_pages/chromium_base.pyi | 8 +- DrissionPage/_pages/chromium_frame.py | 33 +- DrissionPage/_pages/chromium_frame.pyi | 6 +- DrissionPage/_pages/chromium_page.py | 29 +- DrissionPage/_pages/chromium_tab.py | 4 - DrissionPage/_pages/chromium_tab.pyi | 10 +- DrissionPage/_pages/session_page.py | 16 +- DrissionPage/_pages/session_page.pyi | 10 +- DrissionPage/_pages/web_page.py | 13 +- DrissionPage/_pages/web_page.pyi | 8 +- DrissionPage/_units/actions.py | 10 +- DrissionPage/_units/clicker.py | 14 - DrissionPage/_units/downloader.py | 2 +- DrissionPage/_units/listener.py | 14 +- DrissionPage/_units/selector.py | 4 +- DrissionPage/_units/setter.py | 7 - DrissionPage/_units/states.py | 14 +- DrissionPage/_units/states.pyi | 2 +- DrissionPage/_units/waiter.py | 100 ++-- DrissionPage/_units/waiter.pyi | 8 +- DrissionPage/common.py | 3 +- 37 files changed, 1321 insertions(+), 562 deletions(-) create mode 100644 DrissionPage/_functions/elements.py create mode 100644 DrissionPage/_functions/elements.pyi diff --git a/DrissionPage/__init__.py b/DrissionPage/__init__.py index e0efad4..e173e0d 100644 --- a/DrissionPage/__init__.py +++ b/DrissionPage/__init__.py @@ -14,4 +14,4 @@ from ._configs.session_options import SessionOptions __all__ = ['ChromiumPage', 'ChromiumOptions', 'SessionOptions', 'SessionPage', 'WebPage', '__version__'] -__version__ = '4.0.4.23' +__version__ = '4.0.5.4' diff --git a/DrissionPage/_base/base.py b/DrissionPage/_base/base.py index 274b236..d91337f 100644 --- a/DrissionPage/_base/base.py +++ b/DrissionPage/_base/base.py @@ -169,14 +169,8 @@ def child(self, locator='', index=1, timeout=None, ele_only=True): loc = loc[1].lstrip('./') node = self._ele(f'xpath:./{loc}', timeout=timeout, index=index, relative=True, raise_err=False) - if node: - return node - - if Settings.raise_when_ele_not_found: - raise ElementNotFoundError(None, 'child()', {'locator': locator, 'index': index, - 'ele_only': ele_only}) - else: - return NoneElement(self.owner, 'child()', {'locator': locator, 'index': index, 'ele_only': ele_only}) + return node if node else NoneElement(self.owner, 'child()', + {'locator': locator, 'index': index, 'ele_only': ele_only}) def prev(self, locator='', index=1, timeout=None, ele_only=True): """返回前面的一个兄弟元素,可用查询语法筛选,可指定返回筛选结果的第几个 @@ -289,12 +283,8 @@ def _get_relative(self, func, direction, brother, locator='', index=1, timeout=N index = locator locator = '' node = self._get_relatives(index, locator, direction, brother, timeout, ele_only) - if node: - return node - if Settings.raise_when_ele_not_found: - raise ElementNotFoundError(None, func, {'locator': locator, 'index': index, 'ele_only': ele_only}) - else: - return NoneElement(self.owner, func, {'locator': locator, 'index': index, 'ele_only': ele_only}) + return node if node else NoneElement(self.owner, func, + {'locator': locator, 'index': index, 'ele_only': ele_only}) def _get_relatives(self, index=None, locator='', direction='following', brother=True, timeout=.5, ele_only=True): """按要求返回兄弟元素或节点组成的列表 diff --git a/DrissionPage/_base/base.pyi b/DrissionPage/_base/base.pyi index 8b73101..7be20f2 100644 --- a/DrissionPage/_base/base.pyi +++ b/DrissionPage/_base/base.pyi @@ -12,6 +12,7 @@ from DownloadKit import DownloadKit from .._elements.none_element import NoneElement from .._elements.session_element import SessionElement +from .._functions.elements import SessionElementsList from .._pages.chromium_page import ChromiumPage from .._pages.session_page import SessionPage from .._pages.web_page import WebPage @@ -37,7 +38,7 @@ class BaseParser(object): locator: Union[Tuple[str, str], str, BaseElement, None] = None, index: int = 1) -> SessionElement: ... - def s_eles(self, locator: Union[Tuple[str, str], str]) -> List[SessionElement]: ... + def s_eles(self, locator: Union[Tuple[str, str], str]) -> SessionElementsList: ... def _ele(self, locator: Union[Tuple[str, str], str], diff --git a/DrissionPage/_configs/chromium_options.py b/DrissionPage/_configs/chromium_options.py index fc94d69..ba2401f 100644 --- a/DrissionPage/_configs/chromium_options.py +++ b/DrissionPage/_configs/chromium_options.py @@ -389,7 +389,7 @@ def set_load_mode(self, value): return self def set_paths(self, browser_path=None, local_port=None, address=None, download_path=None, - user_data_path=None, cache_path=None, debugger_address=None): + user_data_path=None, cache_path=None): """快捷的路径设置函数 :param browser_path: 浏览器可执行文件路径 :param local_port: 本地端口号 @@ -399,7 +399,6 @@ def set_paths(self, browser_path=None, local_port=None, address=None, download_p :param cache_path: 缓存路径 :return: 当前对象 """ - address = address or debugger_address if browser_path is not None: self.set_browser_path(browser_path) @@ -568,50 +567,3 @@ def save_to_default(self): def __repr__(self): return f'' - - # ---------------即将废弃-------------- - - @property - def debugger_address(self): - """返回浏览器地址,ip:port""" - return self._address - - @debugger_address.setter - def debugger_address(self, address): - """设置浏览器地址,格式ip:port""" - self.set_address(address) - - def set_page_load_strategy(self, value): - return self.set_load_mode(value) - - def set_headless(self, on_off=True): - """设置是否隐藏浏览器界面 - :param on_off: 开或关 - :return: 当前对象 - """ - on_off = 'new' if on_off else 'false' - return self.set_argument('--headless', on_off) - - def set_no_imgs(self, on_off=True): - """设置是否加载图片 - :param on_off: 开或关 - :return: 当前对象 - """ - on_off = None if on_off else False - return self.set_argument('--blink-settings=imagesEnabled=false', on_off) - - def set_no_js(self, on_off=True): - """设置是否禁用js - :param on_off: 开或关 - :return: 当前对象 - """ - on_off = None if on_off else False - return self.set_argument('--disable-javascript', on_off) - - def set_mute(self, on_off=True): - """设置是否静音 - :param on_off: 开或关 - :return: 当前对象 - """ - on_off = None if on_off else False - return self.set_argument('--mute-audio', on_off) diff --git a/DrissionPage/_configs/session_options.py b/DrissionPage/_configs/session_options.py index 8487593..bb55b50 100644 --- a/DrissionPage/_configs/session_options.py +++ b/DrissionPage/_configs/session_options.py @@ -457,17 +457,6 @@ def from_session(self, session, headers=None): self._adapters = [(k, i) for k, i in session.adapters.items()] return self - # --------------即将废弃--------------- - - def set_paths(self, download_path=None): - """设置默认下载路径 - :param download_path: 下载路径 - :return: 返回当前对象 - """ - if download_path is not None: - self._download_path = str(download_path) - return self - def __repr__(self): return f'' diff --git a/DrissionPage/_elements/chromium_element.py b/DrissionPage/_elements/chromium_element.py index 646af49..9965fc2 100644 --- a/DrissionPage/_elements/chromium_element.py +++ b/DrissionPage/_elements/chromium_element.py @@ -17,8 +17,8 @@ from .session_element import make_session_ele from .._base.base import DrissionElement, BaseElement from .._functions.keys import input_text_or_keys -from .._functions.locator import get_loc -from .._functions.settings import Settings +from .._functions.locator import get_loc, locator_to_tuple +from .._functions.elements import ChromiumElementsList from .._functions.web import make_absolute_link, get_ele_txt, format_html, is_js_func, offset_scroll, get_blob from .._units.clicker import Clicker from .._units.rect import ElementRect @@ -27,8 +27,8 @@ from .._units.setter import ChromiumElementSetter from .._units.states import ElementStates, ShadowRootStates from .._units.waiter import ElementWaiter -from ..errors import (ContextLostError, ElementLostError, JavaScriptError, ElementNotFoundError, - CDPError, NoResourceError, AlertExistsError) +from ..errors import ContextLostError, ElementLostError, JavaScriptError, CDPError, NoResourceError, AlertExistsError, \ + NoRectError __FRAME_ELEMENT__ = ('iframe', 'frame') @@ -55,6 +55,7 @@ def __init__(self, owner, node_id=None, obj_id=None, backend_id=None): self._tag = None self._wait = None self._type = 'ChromiumElement' + self._doc_id = None if node_id and obj_id and backend_id: self._node_id = node_id @@ -75,9 +76,6 @@ def __init__(self, owner, node_id=None, obj_id=None, backend_id=None): else: raise ElementLostError - doc = self.run_js('return this.ownerDocument;') - self._doc_id = doc['objectId'] if doc else None - def __repr__(self): attrs = [f"{k}='{v}'" for k, v in self.attrs.items()] return f'' @@ -93,14 +91,6 @@ def __call__(self, locator, index=1, timeout=None): 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""" @@ -221,25 +211,6 @@ def select(self): def value(self): return self.property('value') - # -----即将废弃开始-------- - @property - def location(self): - """返回元素左上角的绝对坐标""" - return self.rect.location - - @property - def size(self): - """返回元素宽和高组成的元组""" - return self.rect.size - - def prop(self, prop): - return self.property(prop) - - def get_src(self, timeout=None, base64_to_bytes=True): - return self.src(timeout=timeout, base64_to_bytes=base64_to_bytes) - - # -----即将废弃结束-------- - def check(self, uncheck=False, by_js=False): """选中或取消选中当前元素 :param uncheck: 是否取消选中 @@ -328,7 +299,7 @@ def children(self, locator='', timeout=None, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 直接子元素或节点文本组成的列表 """ - return super().children(locator, timeout, ele_only=ele_only) + return ChromiumElementsList(self.owner, super().children(locator, timeout, ele_only=ele_only)) def prevs(self, locator='', timeout=None, ele_only=True): """返回当前元素前面符合条件的同级元素或节点组成的列表,可用查询语法筛选 @@ -337,7 +308,7 @@ def prevs(self, locator='', timeout=None, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 兄弟元素或节点文本组成的列表 """ - return super().prevs(locator, timeout, ele_only=ele_only) + return ChromiumElementsList(self.owner, super().prevs(locator, timeout, ele_only=ele_only)) def nexts(self, locator='', timeout=None, ele_only=True): """返回当前元素后面符合条件的同级元素或节点组成的列表,可用查询语法筛选 @@ -346,7 +317,7 @@ def nexts(self, locator='', timeout=None, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 兄弟元素或节点文本组成的列表 """ - return super().nexts(locator, timeout, ele_only=ele_only) + return ChromiumElementsList(self.owner, super().nexts(locator, timeout, ele_only=ele_only)) def befores(self, locator='', timeout=None, ele_only=True): """返回文档中当前元素前面符合条件的元素或节点组成的列表,可用查询语法筛选 @@ -356,7 +327,7 @@ def befores(self, locator='', timeout=None, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 本元素前面的元素或节点组成的列表 """ - return super().befores(locator, timeout, ele_only=ele_only) + return ChromiumElementsList(self.owner, super().befores(locator, timeout, ele_only=ele_only)) def afters(self, locator='', timeout=None, ele_only=True): """返回文档中当前元素后面符合条件的元素或节点组成的列表,可用查询语法筛选 @@ -366,7 +337,137 @@ def afters(self, locator='', timeout=None, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 本元素后面的元素或节点组成的列表 """ - return super().afters(locator, timeout, ele_only=ele_only) + return ChromiumElementsList(self.owner, super().afters(locator, timeout, ele_only=ele_only)) + + def over(self, timeout=None): + """获取覆盖在本元素上最上层的元素 + :param timeout: 等待元素出现的超时时间(秒) + :return: 元素对象 + """ + timeout = timeout if timeout is None else self.owner.timeout + bid = self.wait.covered(timeout=timeout) + if bid: + return ChromiumElement(owner=self.owner, backend_id=bid) + else: + return NoneElement(page=self.owner, method='on()', args={'timeout': timeout}) + + def offset(self, offset_x, offset_y): + """获取相对本元素左上角左边指定偏移量位置的元素 + :param offset_x: 横坐标偏移量,向右为正 + :param offset_y: 纵坐标偏移量,向下为正 + :return: 元素对象 + """ + x, y = self.rect.location + try: + return ChromiumElement(owner=self.owner, + backend_id=self.owner.run_cdp('DOM.getNodeForLocation', x=x + offset_x, + y=y + offset_y, includeUserAgentShadowDOM=True, + ignorePointerEventsNone=False)['backendNodeId']) + except CDPError: + return NoneElement(page=self.owner, method='offset()', args={'offset_x': offset_x, 'offset_y': offset_y}) + + def east(self, loc_or_pixel=None, index=1): + """获取元素右边某个指定元素 + :param loc_or_pixel: 定位符,只支持str或int,且不支持xpath和css方式,传入int按像素距离获取 + :param index: 第几个,从1开始 + :return: 获取到的元素对象 + """ + return self._get_relative_eles(mode='east', locator=loc_or_pixel, index=index) + + def south(self, loc_or_pixel=None, index=1): + """获取元素下方某个指定元素 + :param loc_or_pixel: 定位符,只支持str或int,且不支持xpath和css方式,传入int按像素距离获取 + :param index: 第几个,从1开始 + :return: 获取到的元素对象 + """ + return self._get_relative_eles(mode='south', locator=loc_or_pixel, index=index) + + def west(self, loc_or_pixel=None, index=1): + """获取元素左边某个指定元素 + :param loc_or_pixel: 定位符,只支持str或int,且不支持xpath和css方式,传入int按像素距离获取 + :param index: 第几个,从1开始 + :return: 获取到的元素对象 + """ + return self._get_relative_eles(mode='west', locator=loc_or_pixel, index=index) + + def north(self, loc_or_pixel=None, index=1): + """获取元素上方某个指定元素 + :param loc_or_pixel: 定位符,只支持str或int,且不支持xpath和css方式,传入int按像素距离获取 + :param index: 第几个,从1开始 + :return: 获取到的元素对象 + """ + return self._get_relative_eles(mode='north', locator=loc_or_pixel, index=index) + + def _get_relative_eles(self, mode='north', locator=None, index=1): + """获取元素下方某个指定元素 + :param locator: 定位符,只支持str或int,且不支持xpath和css方式 + :param index: 第几个,从1开始 + :return: 获取到的元素对象 + """ + if locator and not (isinstance(locator, str) and not locator.startswith( + ('x:', 'xpath:', 'x=', 'xpath=', 'c:', 'css:', 'c=', 'css=')) or isinstance(locator, int)): + raise ValueError('locator参数只能是str格式且不支持xpath和css形式。') + rect = self.states.has_rect + if not rect: + raise NoRectError + + if mode == 'east': + cdp_data = {'x': int(rect[1][0]), 'y': int(self.rect.midpoint[1]), + 'includeUserAgentShadowDOM': True, 'ignorePointerEventsNone': False} + variable = 'x' + minus = False + elif mode == 'south': + cdp_data = {'x': int(self.rect.midpoint[0]), 'y': int(rect[2][1]), + 'includeUserAgentShadowDOM': True, 'ignorePointerEventsNone': False} + variable = 'y' + minus = False + elif mode == 'west': + cdp_data = {'x': int(rect[0][0]), 'y': int(self.rect.midpoint[1]), + 'includeUserAgentShadowDOM': True, 'ignorePointerEventsNone': False} + variable = 'x' + minus = True + else: # north + cdp_data = {'x': int(self.rect.midpoint[0]), 'y': int(rect[0][1]), + 'includeUserAgentShadowDOM': True, 'ignorePointerEventsNone': False} + variable = 'y' + minus = True + + if isinstance(locator, int): + if minus: + cdp_data[variable] -= locator + else: + cdp_data[variable] += locator + try: + return ChromiumElement(owner=self.owner, + backend_id=self.owner.run_cdp('DOM.getNodeForLocation', + **cdp_data)['backendNodeId']) + except CDPError: + return NoneElement(page=self.owner, method=f'{mode}()', args={'locator': locator}) + + num = 0 + value = -8 if minus else 8 + size = self.owner.rect.size + max_len = size[0] if mode == 'east' else size[1] + loc_data = locator_to_tuple(locator) if locator else None + curr_ele = None + while 0 < cdp_data[variable] < max_len: + cdp_data[variable] += value + try: + bid = self.owner.run_cdp('DOM.getNodeForLocation', **cdp_data)['backendNodeId'] + if bid == curr_ele: + continue + else: + curr_ele = bid + ele = ChromiumElement(self.owner, backend_id=bid) + + if loc_data is None or _check_ele(ele, loc_data): + num += 1 + if num == index: + return ele + except: + pass + + return NoneElement(page=self.owner, method=f'{mode}()', args={'locator': locator}) def attr(self, attr): """返回一个attribute属性值 @@ -459,14 +560,7 @@ def s_ele(self, locator=None, index=1): :param index: 获取第几个,从1开始,可传入负数获取倒数第几个 :return: SessionElement对象或属性、文本 """ - 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}) - else: - r.method = 's_ele()' - r.args = {'locator': locator} - return r + return make_session_ele(self, locator, index=index, method='s_ele()') def s_eles(self, locator=None): """查找所有符合条件的元素,以SessionElement列表形式返回 @@ -638,6 +732,7 @@ def input(self, vals, clear=False, by_js=False): self.run_js('this.dispatchEvent(new Event("change", {bubbles: true}));') return + self.wait.clickable(wait_moved=False, timeout=.5) if clear and vals not in ('\n', '\ue007'): self.clear(by_js=False) else: @@ -704,7 +799,6 @@ def drag_to(self, ele_or_loc, duration=.5): ele_or_loc = ele_or_loc.rect.midpoint elif not isinstance(ele_or_loc, (list, tuple)): raise TypeError('需要ChromiumElement对象或坐标。') - self.owner.actions.hold(self).move_to(ele_or_loc, duration=duration).release() def _get_obj_id(self, node_id=None, backend_id=None): @@ -917,13 +1011,8 @@ def child(self, locator='', index=1): loc = f'xpath:./{loc}' ele = self._ele(loc, index=index, relative=True) - if ele: - return ele - if Settings.raise_when_ele_not_found: - raise ElementNotFoundError(None, 'child()', {'locator': locator, 'index': index}) - else: - return NoneElement(self.owner, 'child()', {'locator': locator, 'index': index}) + return ele if ele else NoneElement(self.owner, 'child()', {'locator': locator, 'index': index}) def next(self, locator='', index=1): """返回当前元素后面一个符合条件的同级元素,可用查询语法筛选,可指定返回筛选结果的第几个 @@ -938,13 +1027,8 @@ def next(self, locator='', index=1): loc = loc[1].lstrip('./') xpath = f'xpath:./{loc}' ele = self.parent_ele._ele(xpath, index=index, relative=True) - if ele: - return ele - if Settings.raise_when_ele_not_found: - raise ElementNotFoundError(None, 'next()', {'locator': locator, 'index': index}) - else: - return NoneElement(self.owner, 'next()', {'locator': locator, 'index': index}) + return ele if ele else NoneElement(self.owner, 'next()', {'locator': locator, 'index': index}) def before(self, locator='', index=1): """返回文档中当前元素前面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个 @@ -960,13 +1044,8 @@ def before(self, locator='', index=1): loc = loc[1].lstrip('./') xpath = f'xpath:./preceding::{loc}' ele = self.parent_ele._ele(xpath, index=index, relative=True) - if ele: - return ele - if Settings.raise_when_ele_not_found: - raise ElementNotFoundError(None, 'before()', {'locator': locator, 'index': index}) - else: - return NoneElement(self.owner, 'before()', {'locator': locator, 'index': index}) + return ele if ele else NoneElement(self.owner, 'before()', {'locator': locator, 'index': index}) def after(self, locator='', index=1): """返回文档中此当前元素后面符合条件的一个元素,可用查询语法筛选,可指定返回筛选结果的第几个 @@ -976,12 +1055,7 @@ def after(self, locator='', index=1): :return: 本元素后面的某个元素或节点 """ nodes = self.afters(locator=locator) - if nodes: - return nodes[index - 1] - if Settings.raise_when_ele_not_found: - raise ElementNotFoundError(None, 'after()', {'locator': locator, 'index': index}) - else: - return NoneElement(self.owner, 'after()', {'locator': locator, 'index': index}) + return nodes[index - 1] if nodes else NoneElement(self.owner, 'after()', {'locator': locator, 'index': index}) def children(self, locator=''): """返回当前元素符合条件的直接子元素或节点组成的列表,可用查询语法筛选 @@ -1219,8 +1293,12 @@ def do_find(): res = ele.owner.run_cdp('Runtime.getProperties', objectId=res['result']['objectId'], ownProperties=True)['result'][:-1] if index is None: - r = [make_chromium_eles(ele.owner, _ids=i['value']['objectId'], is_obj_id=True) - if i['value']['type'] == 'object' else i['value']['value'] for i in res] + r = ChromiumElementsList(page=ele.owner) + for i in res: + if i['value']['type'] == 'object': + r.append(make_chromium_eles(ele.owner, _ids=i['value']['objectId'], is_obj_id=True)) + else: + r.append(i['value']['value']) return None if False in r else r else: @@ -1244,7 +1322,7 @@ def do_find(): if result: return result - return NoneElement(ele.owner) if index is not None else [] + return NoneElement(ele.owner) if index is not None else ChromiumElementsList(page=ele.owner) def find_by_css(ele, selector, index, timeout): @@ -1290,7 +1368,7 @@ def do_find(): if result: return result - return NoneElement(ele.owner) if index is not None else [] + return NoneElement(ele.owner) if index is not None else ChromiumElementsList(page=ele.owner) def make_chromium_eles(page, _ids, index=1, is_obj_id=True, ele_only=False): @@ -1322,7 +1400,7 @@ def make_chromium_eles(page, _ids, index=1, is_obj_id=True, ele_only=False): return get_node_func(page, obj_id, ele_only) else: # 获取全部 - nodes = [] + nodes = ChromiumElementsList(page=page) for obj_id in _ids: tmp = get_node_func(page, obj_id, ele_only) if tmp is False: @@ -1569,3 +1647,59 @@ def before(self): def after(self): """返回当前元素的::after伪元素内容""" return self._ele.style('content', 'after') + + +def _check_ele(ele, loc_data): + """检查元素是否符合loc_data指定的要求 + :param ele: 元素对象 + :param loc_data: 格式: {'and': bool, 'args': ['属性名称', '匹配方式', '属性值', 是否否定]} + :return: bool + """ + attrs = ele.attrs + if loc_data['and']: + ok = True + for i in loc_data['args']: + name, symbol, value, deny = i + if name == 'tag()': + arg = ele.tag + symbol = '=' + elif name == 'text()': + arg = ele.raw_text + elif name is None: + arg = None + else: + arg = attrs.get(name, '') + + if ((symbol == '=' and ((deny and arg == value) or (not deny and arg != value))) + or (symbol == ':' and ((deny and value in arg) or (not deny and value not in arg))) + or (symbol == '^' and ((deny and arg.startswith(value)) + or (not deny and not arg.startswith(value)))) + or (symbol == '$' and ((deny and arg.endswith(value)) or (not deny and not arg.endswith(value)))) + or (arg is None and attrs)): + ok = False + break + + else: + ok = False + for i in loc_data['args']: + name, value, symbol, deny = i + if name == 'tag()': + arg = ele.tag + symbol = '=' + elif name == 'text()': + arg = ele.text + elif name is None: + arg = None + else: + arg = attrs.get(name, '') + + if ((symbol == '=' and ((not deny and arg == value) or (deny and arg != value))) + or (symbol == ':' and ((not deny and value in arg) or (deny and value not in arg))) + or (symbol == '^' and ((not deny and arg.startswith(value)) + or (deny and not arg.startswith(value)))) + or (symbol == '$' and ((not deny and arg.endswith(value)) or (deny and not arg.endswith(value)))) + or (arg is None and not attrs)): + ok = True + break + + return ok diff --git a/DrissionPage/_elements/chromium_element.pyi b/DrissionPage/_elements/chromium_element.pyi index 1bf2b91..9b0e5a8 100644 --- a/DrissionPage/_elements/chromium_element.pyi +++ b/DrissionPage/_elements/chromium_element.pyi @@ -10,6 +10,7 @@ from typing import Union, Tuple, List, Any, Literal, Optional from .._base.base import DrissionElement, BaseElement from .._elements.session_element import SessionElement +from .._functions.elements import SessionElementsList, ChromiumElementsList from .._pages.chromium_base import ChromiumBase from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_page import ChromiumPage @@ -56,8 +57,6 @@ class ChromiumElement(DrissionElement): def __eq__(self, other: ChromiumElement) -> bool: ... - def __getattr__(self, item: str) -> str: ... - @property def tag(self) -> str: ... @@ -138,27 +137,44 @@ class ChromiumElement(DrissionElement): def children(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[ChromiumElement, str]]: ... + ele_only: bool = True) -> Union[ChromiumElementsList, List[Union[ChromiumElement, str]]]: ... def prevs(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[ChromiumElement, str]]: ... + ele_only: bool = True) -> Union[ChromiumElementsList, List[Union[ChromiumElement, str]]]: ... def nexts(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[ChromiumElement, str]]: ... + ele_only: bool = True) -> Union[ChromiumElementsList, List[Union[ChromiumElement, str]]]: ... def befores(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[ChromiumElement, str]]: ... + ele_only: bool = True) -> Union[ChromiumElementsList, List[Union[ChromiumElement, str]]]: ... def afters(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[ChromiumElement, str]]: ... + ele_only: bool = True) -> Union[ChromiumElementsList, List[Union[ChromiumElement, str]]]: ... + + def over(self, timeout: float = None) -> ChromiumElement: ... + + def south(self, loc_or_pixel: Union[str, int, None] = None, index: int = 1) -> ChromiumElement: ... + + def north(self, loc_or_pixel: Union[str, int, None] = None, index: int = 1) -> ChromiumElement: ... + + def west(self, loc_or_pixel: Union[str, int, None] = None, index: int = 1) -> ChromiumElement: ... + + def east(self, loc_or_pixel: Union[str, int, None] = None, index: int = 1) -> ChromiumElement: ... + + def offset(self, offset_x: int, offset_y: int) -> ChromiumElement: ... + + def _get_relative_eles(self, + mode: str = 'north', + locator: Union[int, str] = None, + index: int = 1) -> ChromiumElement: ... @property def wait(self) -> ElementWaiter: ... @@ -188,21 +204,20 @@ class ChromiumElement(DrissionElement): def eles(self, locator: Union[Tuple[str, str], str], - timeout: float = None) -> List[ChromiumElement]: ... + timeout: float = None) -> ChromiumElementsList: ... def s_ele(self, locator: Union[Tuple[str, str], str] = None, index: int = 1) -> SessionElement: ... - def s_eles(self, locator: Union[Tuple[str, str], str] = None) -> List[SessionElement]: ... + def s_eles(self, locator: Union[Tuple[str, str], str] = None) -> SessionElementsList: ... def _find_elements(self, locator: Union[Tuple[str, str], str], timeout: float = None, index: Optional[int] = 1, relative: bool = False, - raise_err: bool = False) -> Union[ChromiumElement, ChromiumFrame, - List[Union[ChromiumElement, ChromiumFrame]]]: ... + raise_err: bool = False) -> Union[ChromiumElement, ChromiumFrame, ChromiumElementsList]: ... def style(self, style: str, pseudo_ele: str = '') -> str: ... @@ -318,21 +333,20 @@ class ShadowRoot(BaseElement): def eles(self, locator: Union[Tuple[str, str], str], - timeout: float = None) -> List[ChromiumElement]: ... + timeout: float = None) -> ChromiumElementsList: ... def s_ele(self, locator: Union[Tuple[str, str], str] = None, index: int = 1) -> SessionElement: ... - def s_eles(self, locator: Union[Tuple[str, str], str]) -> List[SessionElement]: ... + def s_eles(self, locator: Union[Tuple[str, str], str]) -> SessionElementsList: ... def _find_elements(self, locator: Union[Tuple[str, str], str], timeout: float = None, index: Optional[int] = 1, relative: bool = False, - raise_err: bool = None) -> Union[ChromiumElement, ChromiumFrame, str, - List[Union[ChromiumElement, ChromiumFrame, str]]]: ... + raise_err: bool = None) -> Union[ChromiumElement, ChromiumFrame, str, ChromiumElementsList]: ... def _get_node_id(self, obj_id: str) -> int: ... @@ -366,7 +380,7 @@ def make_chromium_eles(page: Union[ChromiumBase, ChromiumPage, WebPage, Chromium index: Optional[int] = 1, is_obj_id: bool = True, ele_only: bool = False - ) -> Union[ChromiumElement, ChromiumFrame, List[Union[ChromiumElement, ChromiumFrame]]]: ... + ) -> Union[ChromiumElement, ChromiumFrame, ChromiumElementsList]: ... def make_js_for_find_ele_by_xpath(xpath: str, type_txt: str, node_txt: str) -> str: ... diff --git a/DrissionPage/_elements/none_element.py b/DrissionPage/_elements/none_element.py index 6aa8cef..1294da2 100644 --- a/DrissionPage/_elements/none_element.py +++ b/DrissionPage/_elements/none_element.py @@ -5,11 +5,20 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ +from .._functions.settings import Settings from ..errors import ElementNotFoundError class NoneElement(object): def __init__(self, page=None, method=None, args=None): + """ + :param page: 元素所在页面 + :param method: 查找元素的方法 + :param args: 查找元素的参数 + """ + if method and Settings.raise_when_ele_not_found: # 无传入method时不自动抛出,由调用者处理 + raise ElementNotFoundError(None, method=method, arguments=args) + if page: self._none_ele_value = page._none_ele_value self._none_ele_return_value = page._none_ele_return_value diff --git a/DrissionPage/_elements/session_element.py b/DrissionPage/_elements/session_element.py index 0a9c7cd..1d15844 100644 --- a/DrissionPage/_elements/session_element.py +++ b/DrissionPage/_elements/session_element.py @@ -13,6 +13,7 @@ from .none_element import NoneElement from .._base.base import DrissionElement, BasePage, BaseElement +from .._functions.elements import SessionElementsList from .._functions.locator import get_loc from .._functions.web import get_ele_txt, make_absolute_link @@ -50,13 +51,6 @@ def __call__(self, locator, index=1, timeout=None): 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): """返回元素类型""" @@ -156,7 +150,7 @@ def children(self, locator='', timeout=0, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 直接子元素或节点文本组成的列表 """ - return super().children(locator, timeout, ele_only=ele_only) + return SessionElementsList(self.owner, super().children(locator, timeout, ele_only=ele_only)) def prevs(self, locator='', timeout=None, ele_only=True): """返回当前元素前面符合条件的同级元素或节点组成的列表,可用查询语法筛选 @@ -165,7 +159,7 @@ def prevs(self, locator='', timeout=None, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 同级元素或节点文本组成的列表 """ - return super().prevs(locator, timeout, ele_only=ele_only) + return SessionElementsList(self.owner, super().prevs(locator, timeout, ele_only=ele_only)) def nexts(self, locator='', timeout=None, ele_only=True): """返回当前元素后面符合条件的同级元素或节点组成的列表,可用查询语法筛选 @@ -174,7 +168,7 @@ def nexts(self, locator='', timeout=None, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 同级元素或节点文本组成的列表 """ - return super().nexts(locator, timeout, ele_only=ele_only) + return SessionElementsList(self.owner, super().nexts(locator, timeout, ele_only=ele_only)) def befores(self, locator='', timeout=None, ele_only=True): """返回文档中当前元素前面符合条件的元素或节点组成的列表,可用查询语法筛选 @@ -184,7 +178,7 @@ def befores(self, locator='', timeout=None, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 本元素前面的元素或节点组成的列表 """ - return super().befores(locator, timeout, ele_only=ele_only) + return SessionElementsList(self.owner, super().befores(locator, timeout, ele_only=ele_only)) def afters(self, locator='', timeout=None, ele_only=True): """返回文档中当前元素后面符合条件的元素或节点组成的列表,可用查询语法筛选 @@ -194,7 +188,7 @@ def afters(self, locator='', timeout=None, ele_only=True): :param ele_only: 是否只获取元素,为False时把文本、注释节点也纳入 :return: 本元素后面的元素或节点组成的列表 """ - return super().afters(locator, timeout, ele_only=ele_only) + return SessionElementsList(self.owner, super().afters(locator, timeout, ele_only=ele_only)) def attr(self, name): """返回attribute属性值 @@ -293,12 +287,13 @@ def _get_ele_path(self, mode): return f'{path_str[1:]}' if mode == 'css' else path_str -def make_session_ele(html_or_ele, loc=None, index=1): +def make_session_ele(html_or_ele, loc=None, index=1, method=None): """从接收到的对象或html文本中查找元素,返回SessionElement对象 如要直接从html生成SessionElement而不在下级查找,loc输入None即可 :param html_or_ele: html文本、BaseParser对象 :param loc: 定位元组或字符串,为None时不在下级查找,返回根元素 :param index: 获取第几个元素,从1开始,可传入负数获取倒数第几个,None获取所有 + :param method: 调用此方法的方法 :return: 返回SessionElement元素或列表,或属性文本 """ # ---------------处理定位符--------------- @@ -353,8 +348,14 @@ def make_session_ele(html_or_ele, loc=None, index=1): page = html_or_ele.owner xpath = html_or_ele.xpath # ChromiumElement,兼容传入的元素在iframe内的情况 - html = html_or_ele.owner.run_cdp('DOM.getOuterHTML', objectId=html_or_ele._doc_id)['outerHTML'] \ - if html_or_ele._doc_id else html_or_ele.owner.html + if html_or_ele._doc_id is None: + doc = html_or_ele.run_js('return this.ownerDocument;') + html_or_ele._doc_id = doc['objectId'] if doc else False + + if html_or_ele._doc_id: + html = html_or_ele.owner.run_cdp('DOM.getOuterHTML', objectId=html_or_ele._doc_id)['outerHTML'] + else: + html = html_or_ele.owner.html html_or_ele = fromstring(html) html_or_ele = html_or_ele.xpath(xpath)[0] @@ -394,12 +395,16 @@ def make_session_ele(html_or_ele, loc=None, index=1): # 把lxml元素对象包装成SessionElement对象并按需要返回一个或全部 if index is None: - return [SessionElement(e, page) if isinstance(e, HtmlElement) else e for e in eles if e != '\n'] + r = SessionElementsList(page=page) + for e in eles: + if e != '\n': + r.append(SessionElement(e, page) if isinstance(e, HtmlElement) else e) + return r else: eles_count = len(eles) if eles_count == 0 or abs(index) > eles_count: - return NoneElement(page) + return NoneElement(page, method=method, args={'locator': loc, 'index': index}) if index < 0: index = eles_count + index + 1 @@ -409,7 +414,7 @@ def make_session_ele(html_or_ele, loc=None, index=1): elif isinstance(ele, str): return ele else: - return NoneElement(page) + return NoneElement(page, method=method, args={'locator': loc, 'index': index}) except Exception as e: if 'Invalid expression' in str(e): diff --git a/DrissionPage/_elements/session_element.pyi b/DrissionPage/_elements/session_element.pyi index f82bc70..185cfae 100644 --- a/DrissionPage/_elements/session_element.pyi +++ b/DrissionPage/_elements/session_element.pyi @@ -11,6 +11,7 @@ from lxml.html import HtmlElement from .._base.base import DrissionElement, BaseElement from .._elements.chromium_element import ChromiumElement +from .._functions.elements import SessionElementsList from .._pages.chromium_base import ChromiumBase from .._pages.chromium_frame import ChromiumFrame from .._pages.session_page import SessionPage @@ -35,8 +36,6 @@ class SessionElement(DrissionElement): def __eq__(self, other: SessionElement) -> bool: ... - def __getattr__(self, item: str) -> str: ... - @property def tag(self) -> str: ... @@ -92,27 +91,27 @@ class SessionElement(DrissionElement): def children(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[SessionElement, str]]: ... + ele_only: bool = True) -> Union[SessionElementsList, List[Union[SessionElement, str]]]: ... def prevs(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[SessionElement, str]]: ... + ele_only: bool = True) -> Union[SessionElementsList, List[Union[SessionElement, str]]]: ... def nexts(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[SessionElement, str]]: ... + ele_only: bool = True) -> Union[SessionElementsList, List[Union[SessionElement, str]]]: ... def befores(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[SessionElement, str]]: ... + ele_only: bool = True) -> Union[SessionElementsList, List[Union[SessionElement, str]]]: ... def afters(self, locator: Union[Tuple[str, str], str] = '', timeout: float = None, - ele_only: bool = True) -> List[Union[SessionElement, str]]: ... + ele_only: bool = True) -> Union[SessionElementsList, List[Union[SessionElement, str]]]: ... def attr(self, name: str) -> Optional[str]: ... @@ -123,20 +122,20 @@ class SessionElement(DrissionElement): def eles(self, locator: Union[Tuple[str, str], str], - timeout: float = None) -> List[SessionElement]: ... + timeout: float = None) -> SessionElementsList: ... def s_ele(self, locator: Union[Tuple[str, str], str] = None, index: int = 1) -> SessionElement: ... - def s_eles(self, locator: Union[Tuple[str, str], str]) -> List[SessionElement]: ... + def s_eles(self, locator: Union[Tuple[str, str], str]) -> SessionElementsList: ... def _find_elements(self, locator: Union[Tuple[str, str], str], timeout: float = None, index: Optional[int] = 1, relative: bool = False, - raise_err: bool = None) -> Union[SessionElement, List[SessionElement]]: ... + raise_err: bool = None) -> Union[SessionElement, SessionElementsList]: ... def _get_ele_path(self, mode: str) -> str: ... @@ -144,4 +143,5 @@ class SessionElement(DrissionElement): def make_session_ele(html_or_ele: Union[str, SessionElement, SessionPage, ChromiumElement, BaseElement, ChromiumFrame, ChromiumBase], loc: Union[str, Tuple[str, str]] = None, - index: Optional[int] = 1) -> Union[SessionElement, List[SessionElement]]: ... + index: Optional[int] = 1, + method: Optional[str] = None) -> Union[SessionElement, SessionElementsList]: ... diff --git a/DrissionPage/_functions/elements.py b/DrissionPage/_functions/elements.py new file mode 100644 index 0000000..d77d359 --- /dev/null +++ b/DrissionPage/_functions/elements.py @@ -0,0 +1,507 @@ +# -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. +@License : BSD 3-Clause. +""" +from time import perf_counter + +from .._elements.none_element import NoneElement + + +class SessionElementsList(list): + def __init__(self, page=None, *args): + super().__init__(*args) + self._page = page + + @property + def get(self): + return Getter(self) + + @property + def filter(self): + return SessionFilter(self) + + @property + def filter_one(self): + return SessionFilterOne(self) + + +class ChromiumElementsList(SessionElementsList): + + @property + def filter(self): + return ChromiumFilter(self) + + @property + def filter_one(self): + return ChromiumFilterOne(self) + + def search(self, displayed=None, checked=None, selected=None, enabled=None, clickable=None, + have_rect=None, have_text=None): + """或关系筛选元素 + :param displayed: 是否显示,bool,None为忽略该项 + :param checked: 是否被选中,bool,None为忽略该项 + :param selected: 是否被选择,bool,None为忽略该项 + :param enabled: 是否可用,bool,None为忽略该项 + :param clickable: 是否可点击,bool,None为忽略该项 + :param have_rect: 是否拥有大小和位置,bool,None为忽略该项 + :param have_text: 是否含有文本,bool,None为忽略该项 + :return: 筛选结果 + """ + return _search(self, displayed=displayed, checked=checked, selected=selected, enabled=enabled, + clickable=clickable, have_rect=have_rect, have_text=have_text) + + def search_one(self, index=1, displayed=None, checked=None, selected=None, enabled=None, clickable=None, + have_rect=None, have_text=None): + """或关系筛选元素,获取一个结果 + :param index: 元素序号,从1开始 + :param displayed: 是否显示,bool,None为忽略该项 + :param checked: 是否被选中,bool,None为忽略该项 + :param selected: 是否被选择,bool,None为忽略该项 + :param enabled: 是否可用,bool,None为忽略该项 + :param clickable: 是否可点击,bool,None为忽略该项 + :param have_rect: 是否拥有大小和位置,bool,None为忽略该项 + :param have_text: 是否含有文本,bool,None为忽略该项 + :return: 筛选结果 + """ + return _search_one(self, index=index, displayed=displayed, checked=checked, selected=selected, + enabled=enabled, clickable=clickable, have_rect=have_rect, have_text=have_text) + + +class SessionFilterOne(object): + def __init__(self, _list): + self._list = _list + self._index = 1 + + def __call__(self, index=1): + """返回结果中第几个元素 + :param index: 元素序号,从1开始 + :return: 对象自身 + """ + self._index = index + return self + + def attr(self, name, value, equal=True): + """以是否拥有某个attribute值为条件筛选元素 + :param name: 属性名称 + :param value: 属性值 + :param equal: True表示匹配name值为value值的元素,False表示匹配name值不为value值的 + :return: 筛选结果 + """ + return self._get_attr(name, value, 'attr', equal=equal) + + def text(self, text, fuzzy=True, contain=True): + """以是否含有指定文本为条件筛选元素 + :param text: 用于匹配的文本 + :param fuzzy: 是否模糊匹配 + :param contain: 是否包含该字符串,False表示不包含 + :return: 筛选结果 + """ + num = 0 + if contain: + for i in self._list: + t = i if isinstance(i, str) else i.raw_text + if (fuzzy and text in t) or (not fuzzy and text == t): + num += 1 + if self._index == num: + return i + else: + for i in self._list: + t = i if isinstance(i, str) else i.raw_text + if (fuzzy and text not in t) or (not fuzzy and text != t): + num += 1 + if self._index == num: + return i + return NoneElement(self._list._page, 'text()', + args={'text': text, 'fuzzy': fuzzy, 'contain': contain, 'index': self._index}) + + def _get_attr(self, name, value, method, equal=True): + """返回通过某个方法可获得某个值的元素 + :param name: 属性名称 + :param value: 属性值 + :param method: 方法名称 + :return: 筛选结果 + """ + num = 0 + if equal: + for i in self._list: + if not isinstance(i, str) and getattr(i, method)(name) == value: + num += 1 + if self._index == num: + return i + else: + for i in self._list: + if not isinstance(i, str) and getattr(i, method)(name) != value: + num += 1 + if self._index == num: + return i + return NoneElement(self._list._page, f'{method}()', + args={'name': name, 'value': value, 'equal': equal, 'index': self._index}) + + +class SessionFilter(SessionFilterOne): + + def __iter__(self): + return iter(self._list) + + def __next__(self): + return next(self._list) + + def __len__(self): + return len(self._list) + + def __getitem__(self, item): + return self._list[item] + + @property + def get(self): + """返回用于获取元素属性的对象""" + return self._list.get + + def text(self, text, fuzzy=True, contain=True): + """以是否含有指定文本为条件筛选元素 + :param text: 用于匹配的文本 + :param fuzzy: 是否模糊匹配 + :param contain: 是否包含该字符串,False表示不包含 + :return: 筛选结果 + """ + self._list = _text_all(self._list, SessionElementsList(page=self._list._page), + text=text, fuzzy=fuzzy, contain=contain) + + def _get_attr(self, name, value, method, equal=True): + """返回通过某个方法可获得某个值的元素 + :param name: 属性名称 + :param value: 属性值 + :param method: 方法名称 + :return: 筛选结果 + """ + self._list = _get_attr_all(self._list, SessionElementsList(page=self._list._page), + name=name, value=value, method=method, equal=equal) + return self + + +class ChromiumFilterOne(SessionFilterOne): + + def displayed(self, equal=True): + """以是否显示为条件筛选元素 + :param equal: 是否匹配显示的元素,False匹配不显示的 + :return: 筛选结果 + """ + return self._any_state('is_displayed', equal=equal) + + def checked(self, equal=True): + """以是否被选中为条件筛选元素 + :param equal: 是否匹配被选中的元素,False匹配不被选中的 + :return: 筛选结果 + """ + return self._any_state('is_checked', equal=equal) + + def selected(self, equal=True): + """以是否被选择为条件筛选元素,用于