diff --git a/.gitee/ISSUE_TEMPLATE.zh-CN.md b/.gitee/ISSUE_TEMPLATE.zh-CN.md index c6e48e4..69c29c4 100644 --- a/.gitee/ISSUE_TEMPLATE.zh-CN.md +++ b/.gitee/ISSUE_TEMPLATE.zh-CN.md @@ -9,3 +9,6 @@ 2. 请附上代码和报错信息(如有) 3. DrissionPage、浏览器、python版本号是多少? 4. 有什么意见建议? + +请在下方写正文,不要把内容插入到上面的问题中。 +--- \ No newline at end of file diff --git a/DrissionPage/__init__.py b/DrissionPage/__init__.py index e173e0d..2617ea8 100644 --- a/DrissionPage/__init__.py +++ b/DrissionPage/__init__.py @@ -5,13 +5,13 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ -from ._pages.chromium_page import ChromiumPage -from ._pages.session_page import SessionPage -from ._pages.web_page import WebPage - -# 启动配置类 +from ._base.browser import Chromium from ._configs.chromium_options import ChromiumOptions from ._configs.session_options import SessionOptions +from ._pages.session_page import SessionPage -__all__ = ['ChromiumPage', 'ChromiumOptions', 'SessionOptions', 'SessionPage', 'WebPage', '__version__'] -__version__ = '4.0.5.4' +from ._pages.chromium_page import ChromiumPage +from ._pages.mix_page import MixPage +from ._pages.mix_page import MixPage as WebPage + +__version__ = '4.1.0.0b11' diff --git a/DrissionPage/__init__.pyi b/DrissionPage/__init__.pyi new file mode 100644 index 0000000..5fc228d --- /dev/null +++ b/DrissionPage/__init__.pyi @@ -0,0 +1,18 @@ +# -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. +@License : BSD 3-Clause. +""" +from ._base.browser import Chromium +from ._configs.chromium_options import ChromiumOptions +from ._configs.session_options import SessionOptions +from ._pages.session_page import SessionPage + +from ._pages.chromium_page import ChromiumPage +from ._pages.mix_page import MixPage +from ._pages.mix_page import MixPage as WebPage + +__all__ = ['MixPage', 'WebPage', 'ChromiumPage', 'Chromium', 'ChromiumOptions', 'SessionOptions', 'SessionPage', '__version__'] +__version__: str = ... diff --git a/DrissionPage/_base/base.py b/DrissionPage/_base/base.py index 1fcbd40..8fa9a7c 100644 --- a/DrissionPage/_base/base.py +++ b/DrissionPage/_base/base.py @@ -12,10 +12,11 @@ from urllib.parse import quote from DownloadKit import DownloadKit -from .._functions.settings import Settings -from .._functions.locator import get_loc -from .._functions.web import format_html from .._elements.none_element import NoneElement +from .._functions.elements import get_frame +from .._functions.locator import get_loc +from .._functions.settings import Settings +from .._functions.web import format_html from ..errors import ElementNotFoundError @@ -54,7 +55,6 @@ class BaseElement(BaseParser): def __init__(self, owner=None): self.owner = owner - self.page = owner._page if owner else None self._type = 'BaseElement' # ----------------以下属性或方法由后代实现---------------- @@ -71,6 +71,16 @@ class BaseElement(BaseParser): def nexts(self): pass + def get_frame(self, loc_or_ind, timeout=None): + """获取元素中一个frame对象 + :param loc_or_ind: 定位符、iframe序号,序号从1开始,可传入负数获取倒数第几个 + :param timeout: 查找元素超时时间(秒) + :return: ChromiumFrame对象 + """ + if not isinstance(loc_or_ind, (int, str, tuple)): + raise TypeError('loc_or_ind参数是定位符或序号。') + return get_frame(self, loc_ind_ele=loc_or_ind, timeout=timeout) + def _ele(self, locator, timeout=None, index=1, relative=False, raise_err=None, method=None): """调用获取元素的方法 :param locator: 定位符 @@ -81,6 +91,8 @@ class BaseElement(BaseParser): :param method: 调用的方法名 :return: 元素对象或它们组成的列表 """ + if hasattr(locator, '_type'): + return locator r = self._find_elements(locator, timeout=timeout, index=index, relative=relative, raise_err=raise_err) if r or isinstance(r, list): return r @@ -120,11 +132,8 @@ class DrissionElement(BaseElement): :param text_node_only: 是否只返回文本节点 :return: 文本列表 """ - if text_node_only: - texts = self.eles('xpath:/text()') - else: - texts = [x if isinstance(x, str) else x.text for x in self.eles('xpath:./text() | *')] - + texts = self.eles('xpath:/text()') if text_node_only else [x if isinstance(x, str) else x.text + for x in self.eles('xpath:./text() | *')] return [format_html(x.strip(' ').rstrip('\n')) for x in texts if x and sub('[\r\n\t ]', '', x) != ''] def parent(self, level_or_loc=1, index=1): @@ -138,10 +147,8 @@ class DrissionElement(BaseElement): elif isinstance(level_or_loc, (tuple, str)): loc = get_loc(level_or_loc, True) - if loc[0] == 'css selector': raise ValueError('此css selector语法不受支持,请换成xpath。') - loc = f'xpath:./ancestor::{loc[1].lstrip(". / ")}[{index}]' else: @@ -345,7 +352,6 @@ class BasePage(BaseParser): def __init__(self): """初始化函数""" self._url = None - self._timeout = 10 self._url_available = None self.retry_times = 3 self.retry_interval = 2 @@ -361,16 +367,6 @@ class BasePage(BaseParser): ele = self._ele('xpath://title', raise_err=False, method='title') return ele.text if ele else None - @property - def timeout(self): - """返回查找元素时等待的秒数""" - return self._timeout - - @timeout.setter - def timeout(self, second): - """设置查找元素时等待的秒数""" - self._timeout = second - @property def url_available(self): """返回当前访问的url有效性""" @@ -420,10 +416,6 @@ class BasePage(BaseParser): def user_agent(self): return - @abstractmethod - def cookies(self, as_dict=False, all_info=False): - return {} - @abstractmethod def get(self, url, show_errmsg=False, retry=None, interval=None): pass diff --git a/DrissionPage/_base/base.pyi b/DrissionPage/_base/base.pyi index 8de4f35..39cd123 100644 --- a/DrissionPage/_base/base.pyi +++ b/DrissionPage/_base/base.pyi @@ -15,7 +15,7 @@ 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 +from .._pages.mix_page import MixPage class BaseParser(object): @@ -59,7 +59,6 @@ class BaseElement(BaseParser): def __init__(self, owner: BasePage = None): self.owner: BasePage = ... - self.page: Union[ChromiumPage, SessionPage, WebPage] = ... # ----------------以下属性或方法由后代实现---------------- @property @@ -200,22 +199,15 @@ class BasePage(BaseParser): self._url_available: bool = ... self.retry_times: int = ... self.retry_interval: float = ... - self._timeout: float = ... self._download_path: str = ... self._DownloadKit: DownloadKit = ... self._none_ele_return_value: bool = ... self._none_ele_value: Any = ... - self._page: Union[ChromiumPage, SessionPage, WebPage]=... + self._page: Union[ChromiumPage, SessionPage, MixPage] = ... @property def title(self) -> Union[str, None]: ... - @property - def timeout(self) -> float: ... - - @timeout.setter - def timeout(self, second: float) -> None: ... - @property def url_available(self) -> bool: ... @@ -237,9 +229,6 @@ class BasePage(BaseParser): @property def user_agent(self) -> str: ... - @abstractmethod - def cookies(self, as_dict: bool = False, all_info: bool = False) -> Union[list, dict]: ... - @abstractmethod def get(self, url: str, show_errmsg: bool = False, retry: int = None, interval: float = None): ... diff --git a/DrissionPage/_base/browser.py b/DrissionPage/_base/browser.py index 00649d5..b01aec3 100644 --- a/DrissionPage/_base/browser.py +++ b/DrissionPage/_base/browser.py @@ -7,54 +7,100 @@ """ from pathlib import Path from shutil import rmtree -from time import perf_counter, sleep +from threading import Lock +from time import sleep, perf_counter +from requests import Session from websocket import WebSocketBadStatusException from .driver import BrowserDriver, Driver +from .._configs.chromium_options import ChromiumOptions +from .._configs.session_options import SessionOptions +from .._functions.browser import connect_browser +from .._functions.cookies import CookiesList +from .._functions.settings import Settings +from .._functions.tools import PortFinder from .._functions.tools import raise_error +from .._pages.chromium_base import Timeout +from .._pages.tabs import ChromiumTab, MixTab from .._units.downloader import DownloadManager +from .._units.setter import BrowserSetter +from .._units.waiter import BrowserWaiter +from ..errors import BrowserConnectError, CDPError from ..errors import PageDisconnectedError __ERROR__ = 'error' -class Browser(object): - BROWSERS = {} +class Chromium(object): + _BROWSERS = {} + _lock = Lock() - def __new__(cls, address, browser_id, page): + def __new__(cls, addr_or_opts=None, session_options=None): """ - :param address: 浏览器地址 - :param browser_id: 浏览器id - :param page: ChromiumPage对象 + :param addr_or_opts: 浏览器地址:端口、ChromiumOptions对象或端口数字(int) + :param session_options: 使用双模Tab时使用的默认Session配置,为True使用ini文件配置 """ - if browser_id in cls.BROWSERS: - return cls.BROWSERS[browser_id] - return object.__new__(cls) + opt = handle_options(addr_or_opts) + is_headless, browser_id, is_exists = run_browser(opt) + with cls._lock: + if browser_id in cls._BROWSERS: + r = cls._BROWSERS[browser_id] + while not hasattr(r, '_driver'): + sleep(.1) + return r + r = object.__new__(cls) + r._chromium_options = opt + r.is_headless = is_headless + r._is_exists = is_exists + r.id = browser_id + cls._BROWSERS[browser_id] = r + return r - def __init__(self, address, browser_id, page): + def __init__(self, addr_or_opts=None, session_options=None): """ - :param address: 浏览器地址 - :param browser_id: 浏览器id - :param page: ChromiumPage对象 + :param addr_or_opts: 浏览器地址:端口、ChromiumOptions对象或端口数字(int) + :param session_options: 使用双模Tab时使用的默认Session配置,为True使用ini文件配置 """ if hasattr(self, '_created'): return self._created = True - Browser.BROWSERS[browser_id] = self - self.page = page - self.address = address - self._driver = BrowserDriver(browser_id, 'browser', address, self) - self.id = browser_id + self._type = 'Chromium' self._frames = {} self._drivers = {} self._all_drivers = {} - self._connected = False + + self._set = None + self._wait = None + self._timeouts = Timeout(**self._chromium_options.timeouts) + self._load_mode = self._chromium_options.load_mode + self._download_path = str(Path(self._chromium_options.download_path).absolute()) + self.retry_times = self._chromium_options.retry_times + self.retry_interval = self._chromium_options.retry_interval + self.address = self._chromium_options.address + self._driver = BrowserDriver(self.id, 'browser', self.address, self) + + if self.is_headless != self._chromium_options.is_headless or ( + self._is_exists and self._chromium_options._new_env): + self.quit(3, True) + connect_browser(self._chromium_options) + s = Session() + s.trust_env = False + ws = s.get(f'http://{self._chromium_options.address}/json/version', headers={'Connection': 'close'}) + self.id = ws.json()['webSocketDebuggerUrl'].split('/')[-1] + self._driver = BrowserDriver(self.id, 'browser', self.address, self) + ws.close() + s.close() + self._frames = {} + self._drivers = {} + self._all_drivers = {} + + self.version = self._run_cdp('Browser.getVersion')['product'] self._process_id = None try: - r = self.run_cdp('SystemInfo.getProcessInfo') + r = self._run_cdp('SystemInfo.getProcessInfo') for i in r.get('processInfo', []): if i['type'] == 'browser': self._process_id = i['id'] @@ -62,9 +108,348 @@ class Browser(object): except: pass - self.run_cdp('Target.setDiscoverTargets', discover=True) + self._run_cdp('Target.setDiscoverTargets', discover=True) self._driver.set_callback('Target.targetDestroyed', self._onTargetDestroyed) self._driver.set_callback('Target.targetCreated', self._onTargetCreated) + self._dl_mgr = DownloadManager(self) + + self._session_options = SessionOptions() if session_options is True else session_options + + @property + def user_data_path(self): + """返回用户文件夹路径""" + return self._chromium_options.user_data_path + + @property + def process_id(self): + """返回浏览器进程id""" + return self._process_id + + @property + def timeout(self): + """返回timeouts设置""" + return self._timeouts.base + + @property + def timeouts(self): + """返回timeouts设置""" + return self._timeouts + + @property + def load_mode(self): + """返回加载模式""" + return self._load_mode + + @property + def download_path(self): + """返回默认下载路径""" + return self._download_path + + @property + def set(self): + if self._set is None: + self._set = BrowserSetter(self) + return self._set + + @property + def wait(self): + """返回用于等待的对象""" + if self._wait is None: + self._wait = BrowserWaiter(self) + return self._wait + + @property + def tabs_count(self): + """返回标签页数量""" + j = self._run_cdp('Target.getTargets')['targetInfos'] # 不要改用get,避免卡死 + return len([i for i in j if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')]) + + @property + def tab_ids(self): + """返回所有标签页id组成的列表""" + j = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp,因为顺序不对 + return [i['id'] for i in j if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')] + + @property + def latest_tab(self): + """返回最新的标签页,最新标签页指最后创建或最后被激活的 + 当Settings.singleton_tab_obj==True时返回Tab对象,否则返回tab id""" + return self.get_tab(self.tab_ids[0], as_id=not Settings.singleton_tab_obj) + + def cookies(self, all_info=False): + """以list格式返回所有域名的cookies + :param all_info: 是否返回所有内容,False则只返回name, value, domain + :return: cookies组成的列表 + """ + cks = self._run_cdp(f'Storage.getCookies')['cookies'] + r = cks if all_info else [{'name': c['name'], 'value': c['value'], 'domain': c['domain']} for c in cks] + return CookiesList(r) + + def new_tab(self, url=None, new_window=False, background=False, new_context=False): + """新建一个标签页 + :param url: 新标签页跳转到的网址 + :param new_window: 是否在新窗口打开标签页 + :param background: 是否不激活新标签页,如new_window为True则无效 + :param new_context: 是否创建新的上下文 + :return: 新标签页对象 + """ + return self._new_tab(ChromiumTab, url=url, new_window=new_window, + background=background, new_context=new_context) + + def new_mix_tab(self, url=None, new_window=False, background=False, new_context=False): + """新建一个标签页 + :param url: 新标签页跳转到的网址 + :param new_window: 是否在新窗口打开标签页 + :param background: 是否不激活新标签页,如new_window为True则无效 + :param new_context: 是否创建新的上下文 + :return: 新标签页对象 + """ + return self._new_tab(MixTab, url=url, new_window=new_window, + background=background, new_context=new_context) + + def _new_tab(self, obj, url=None, new_window=False, background=False, new_context=False): + """新建一个标签页 + :param obj: 要创建的Tab类型 + :param url: 新标签页跳转到的网址 + :param new_window: 是否在新窗口打开标签页 + :param background: 是否不激活新标签页,如new_window为True则无效 + :param new_context: 是否创建新的上下文 + :return: 新标签页对象 + """ + tab = None + if new_context: + tab = self._run_cdp('Target.createBrowserContext')['browserContextId'] + + kwargs = {'url': ''} + if new_window: + kwargs['newWindow'] = True + if background: + kwargs['background'] = True + if tab: + kwargs['browserContextId'] = tab + + try: + tab = self._run_cdp('Target.createTarget', **kwargs)['targetId'] + except CDPError: + data = ('a', {'href': url or 'https://#', 'target': '_new' if new_window else '_blank'}) + tab = self.get_mix_tab() if isinstance(obj, MixTab) else self.get_tab() + return tab.add_ele(data).click.for_new_tab(by_js=True) + + while tab not in self._drivers: + sleep(.1) + tab = obj(self, tab) + if url: + tab.get(url) + return tab + + def get_tab(self, id_or_num=None, title=None, url=None, tab_type='page', as_id=False): + """获取一个标签页对象,id_or_num不为None时,后面几个参数无效 + :param id_or_num: 要获取的标签页id或序号,序号从1开始,可传入负数获取倒数第几个,不是视觉排列顺序,而是激活顺序 + :param title: 要匹配title的文本,模糊匹配,为None则匹配所有 + :param url: 要匹配url的文本,模糊匹配,为None则匹配所有 + :param tab_type: tab类型,可用列表输入多个,如 'page', 'iframe' 等,为None则匹配所有 + :param as_id: 是否返回标签页id而不是标签页对象 + :return: Tab对象 + """ + return self._get_tab(id_or_num=id_or_num, title=title, url=url, tab_type=tab_type, as_id=as_id) + + def get_tabs(self, title=None, url=None, tab_type='page', as_id=False): + """查找符合条件的tab,返回它们组成的列表,title和url是与关系 + :param title: 要匹配title的文本 + :param url: 要匹配url的文本 + :param tab_type: tab类型,可用列表输入多个 + :param as_id: 是否返回标签页id而不是标签页对象 + :return: Tab对象列表 + """ + return self._get_tabs(title=title, url=url, tab_type=tab_type, as_id=as_id) + + def get_mix_tab(self, id_or_num=None, title=None, url=None, tab_type='page', as_id=False): + """获取一个标签页对象,id_or_num不为None时,后面几个参数无效 + :param id_or_num: 要获取的标签页id或序号,序号从1开始,可传入负数获取倒数第几个,不是视觉排列顺序,而是激活顺序 + :param title: 要匹配title的文本,模糊匹配,为None则匹配所有 + :param url: 要匹配url的文本,模糊匹配,为None则匹配所有 + :param tab_type: tab类型,可用列表输入多个,如 'page', 'iframe' 等,为None则匹配所有 + :param as_id: 是否返回标签页id而不是标签页对象 + :return: Tab对象 + """ + return self._get_tab(id_or_num=id_or_num, title=title, url=url, tab_type=tab_type, mix=True, as_id=as_id) + + def get_mix_tabs(self, title=None, url=None, tab_type='page', as_id=False): + """查找符合条件的tab,返回它们组成的列表,title和url是与关系 + :param title: 要匹配title的文本 + :param url: 要匹配url的文本 + :param tab_type: tab类型,可用列表输入多个 + :param as_id: 是否返回标签页id而不是标签页对象 + :return: Tab对象列表 + """ + return self._get_tabs(title=title, url=url, tab_type=tab_type, mix=True, as_id=as_id) + + def _get_tab(self, id_or_num=None, title=None, url=None, tab_type='page', mix=False, as_id=False): + """获取一个标签页对象,id_or_num不为None时,后面几个参数无效 + :param id_or_num: 要获取的标签页id或序号,序号从1开始,可传入负数获取倒数第几个,不是视觉排列顺序,而是激活顺序 + :param title: 要匹配title的文本,模糊匹配,为None则匹配所有 + :param url: 要匹配url的文本,模糊匹配,为None则匹配所有 + :param tab_type: tab类型,可用列表输入多个,如 'page', 'iframe' 等,为None则匹配所有 + :param mix: 是否返回可切换模式的Tab对象 + :param as_id: 是否返回标签页id而不是标签页对象,mix=False时无效 + :return: Tab对象 + """ + if id_or_num is not None: + if isinstance(id_or_num, str): + id_or_num = id_or_num + elif isinstance(id_or_num, int): + id_or_num = self.tab_ids[id_or_num - 1 if id_or_num > 0 else id_or_num] + elif isinstance(id_or_num, ChromiumTab): + return id_or_num.tab_id if as_id else ChromiumTab(self, id_or_num.tab_id) + + elif title == url is None and tab_type == 'page': + id_or_num = self.tab_ids[0] + + else: + tabs = self._get_tabs(title=title, url=url, tab_type=tab_type, as_id=True) + if tabs: + id_or_num = tabs[0] + else: + return None + + if as_id: + return id_or_num + with self._lock: + return MixTab(self, id_or_num) if mix else ChromiumTab(self, id_or_num) + + def _get_tabs(self, title=None, url=None, tab_type='page', mix=False, as_id=False): + """查找符合条件的tab,返回它们组成的列表,title和url是与关系 + :param title: 要匹配title的文本 + :param url: 要匹配url的文本 + :param tab_type: tab类型,可用列表输入多个 + :param mix: 是否返回可切换模式的Tab对象 + :param as_id: 是否返回标签页id而不是标签页对象,mix=False时无效 + :return: Tab对象列表 + """ + tabs = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp + + if isinstance(tab_type, str): + tab_type = {tab_type} + elif isinstance(tab_type, (list, tuple, set)): + tab_type = set(tab_type) + elif tab_type is not None: + raise TypeError('tab_type只能是set、list、tuple、str、None。') + + tabs = [i for i in tabs if ((title is None or title in i['title']) and (url is None or url in i['url']) + and (tab_type is None or i['type'] in tab_type))] + if as_id: + return [tab['id'] for tab in tabs] + with self._lock: + if mix: + return [MixTab(self, tab['id']) for tab in tabs] + else: + return [ChromiumTab(self, tab['id']) for tab in tabs] + + def close_tabs(self, tabs_or_ids=None, others=False): + """关闭传入的标签页,默认关闭当前页。可传入多个 + :param tabs_or_ids: 要关闭的标签页对象或id,可传入列表或元组,为None时关闭最后操作的 + :param others: 是否关闭指定标签页之外的 + :return: None + """ + all_tabs = set(self.tab_ids) + if isinstance(tabs_or_ids, str): + tabs = {tabs_or_ids} + elif isinstance(tabs_or_ids, ChromiumTab): + tabs = {tabs_or_ids.tab_id} + elif tabs_or_ids is None: + tabs = {self.tab_ids[0]} + elif isinstance(tabs_or_ids, (list, tuple)): + tabs = set(i.tab_id if isinstance(i, ChromiumTab) else i for i in tabs_or_ids) + else: + raise TypeError('tabs_or_ids参数只能传入标签页对象或id。') + + if others: + tabs = all_tabs - tabs + + end_len = len(set(all_tabs) - set(tabs)) + if end_len <= 0: + self.quit() + return + + for tab in tabs: + self._onTargetDestroyed(targetId=tab) + self._driver.run('Target.closeTarget', targetId=tab) + sleep(.2) + end_time = perf_counter() + 3 + while self.tabs_count != end_len and perf_counter() < end_time: + sleep(.1) + + def activate_tab(self, id_ind_tab): + """使标签页变为活动状态 + :param id_ind_tab: 标签页id(str)、Tab对象或标签页序号(int),序号从1开始 + :return: None + """ + if isinstance(id_ind_tab, int): + id_ind_tab += -1 if id_ind_tab else 1 + id_ind_tab = self.tab_ids[id_ind_tab] + elif isinstance(id_ind_tab, ChromiumTab): + id_ind_tab = id_ind_tab.tab_id + self._run_cdp('Target.activateTarget', targetId=id_ind_tab) + + def reconnect(self): + """断开重连""" + self._driver.stop() + BrowserDriver.BROWSERS.pop(self.id) + self._driver = BrowserDriver(self.id, 'browser', self.address, self) + self._run_cdp('Target.setDiscoverTargets', discover=True) + self._driver.set_callback('Target.targetDestroyed', self._onTargetDestroyed) + self._driver.set_callback('Target.targetCreated', self._onTargetCreated) + + def quit(self, timeout=5, force=False): + """关闭浏览器 + :param timeout: 等待浏览器关闭超时时间(秒) + :param force: 是否立刻强制终止进程 + :return: None + """ + try: + self._run_cdp('Browser.close') + except PageDisconnectedError: + pass + self._driver.stop() + + drivers = list(self._all_drivers.values()) + for tab in drivers: + for driver in tab: + driver.stop() + + if not force: + return + + try: + pids = [pid['id'] for pid in self._run_cdp('SystemInfo.getProcessInfo')['processInfo']] + except: + return + + from psutil import Process + for pid in pids: + try: + Process(pid).kill() + except: + pass + + from os import popen + from platform import system + end_time = perf_counter() + timeout + while perf_counter() < end_time: + ok = True + for pid in pids: + txt = f'tasklist | findstr {pid}' if system().lower() == 'windows' else f'ps -ef | grep {pid}' + p = popen(txt) + sleep(.05) + try: + if f' {pid} ' in p.read(): + ok = False + break + except TypeError: + pass + + if ok: + break def _get_driver(self, tab_id, owner=None): """新建并返回指定tab id的Driver @@ -95,8 +480,7 @@ class Browser(object): def _onTargetDestroyed(self, **kwargs): """标签页关闭时执行""" tab_id = kwargs['targetId'] - if hasattr(self, '_dl_mgr'): - self._dl_mgr.clear_tab_info(tab_id) + self._dl_mgr.clear_tab_info(tab_id) for key in [k for k, i in self._frames.items() if i == tab_id]: self._frames.pop(key, None) for d in self._all_drivers.get(tab_id, tuple()): @@ -104,13 +488,7 @@ class Browser(object): self._drivers.pop(tab_id, None) self._all_drivers.pop(tab_id, None) - def connect_to_page(self): - """执行与page相关的逻辑""" - if not self._connected: - self._dl_mgr = DownloadManager(self) - self._connected = True - - def run_cdp(self, cmd, **cmd_args): + def _run_cdp(self, cmd, **cmd_args): """执行Chrome DevTools Protocol语句 :param cmd: 协议项目 :param cmd_args: 参数 @@ -120,166 +498,10 @@ class Browser(object): r = self._driver.run(cmd, **cmd_args) return r if __ERROR__ not in r else raise_error(r, ignore) - @property - def driver(self): - return self._driver - - @property - def tabs_count(self): - """返回标签页数量""" - j = self.run_cdp('Target.getTargets')['targetInfos'] # 不要改用get,避免卡死 - return len([i for i in j if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')]) - - @property - def tab_ids(self): - """返回所有标签页id组成的列表""" - j = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp,因为顺序不对 - return [i['id'] for i in j if i['type'] in ('page', 'webview') - and not i['url'].startswith('devtools://')] - - @property - def process_id(self): - """返回浏览器进程id""" - return self._process_id - - def find_tabs(self, title=None, url=None, tab_type=None): - """查找符合条件的tab,返回它们组成的列表,title和url是与关系 - :param title: 要匹配title的文本 - :param url: 要匹配url的文本 - :param tab_type: tab类型,可用列表输入多个 - :return: dict格式的tab信息列表列表 - """ - tabs = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp - - if isinstance(tab_type, str): - tab_type = {tab_type} - elif isinstance(tab_type, (list, tuple, set)): - tab_type = set(tab_type) - elif tab_type is not None: - raise TypeError('tab_type只能是set、list、tuple、str、None。') - - return [i for i in tabs if ((title is None or title in i['title']) and (url is None or url in i['url']) - and (tab_type is None or i['type'] in tab_type))] - - def close_tab(self, tab_id): - """关闭标签页 - :param tab_id: 标签页id - :return: None - """ - self._onTargetDestroyed(targetId=tab_id) - self.driver.run('Target.closeTarget', targetId=tab_id) - - def stop_driver(self, driver): - """停止一个Driver - :param driver: Driver对象 - :return: None - """ - driver.stop() - self._all_drivers.get(driver.id, set()).discard(driver) - - def activate_tab(self, tab_id): - """使标签页变为活动状态 - :param tab_id: 标签页id - :return: None - """ - self.run_cdp('Target.activateTarget', targetId=tab_id) - - def get_window_bounds(self, tab_id=None): - """返回浏览器窗口位置和大小信息 - :param tab_id: 标签页id - :return: 窗口大小字典 - """ - return self.run_cdp('Browser.getWindowForTarget', targetId=tab_id or self.id)['bounds'] - - def new_tab(self, new_window=False, background=False, new_context=False): - """新建一个标签页 - :param new_window: 是否在新窗口打开标签页 - :param background: 是否不激活新标签页,如new_window为True则无效 - :param new_context: 是否创建新的上下文 - :return: 新标签页id - """ - bid = None - if new_context: - bid = self.run_cdp('Target.createBrowserContext')['browserContextId'] - - kwargs = {'url': ''} - if new_window: - kwargs['newWindow'] = True - if background: - kwargs['background'] = True - if bid: - kwargs['browserContextId'] = bid - - tid = self.run_cdp('Target.createTarget', **kwargs)['targetId'] - while tid not in self._drivers: - sleep(.1) - return tid - - def reconnect(self): - """断开重连""" - self._driver.stop() - BrowserDriver.BROWSERS.pop(self.id) - self._driver = BrowserDriver(self.id, 'browser', self.address, self) - self.run_cdp('Target.setDiscoverTargets', discover=True) - self._driver.set_callback('Target.targetDestroyed', self._onTargetDestroyed) - self._driver.set_callback('Target.targetCreated', self._onTargetCreated) - - def quit(self, timeout=5, force=False): - """关闭浏览器 - :param timeout: 等待浏览器关闭超时时间(秒) - :param force: 是否立刻强制终止进程 - :return: None - """ - try: - self.run_cdp('Browser.close') - except PageDisconnectedError: - pass - self.driver.stop() - - drivers = list(self._all_drivers.values()) - for tab in drivers: - for driver in tab: - driver.stop() - - if not force: - return - - try: - pids = [pid['id'] for pid in self.run_cdp('SystemInfo.getProcessInfo')['processInfo']] - except: - return - - from psutil import Process - for pid in pids: - try: - Process(pid).kill() - except: - pass - - from os import popen - from platform import system - end_time = perf_counter() + timeout - while perf_counter() < end_time: - ok = True - for pid in pids: - txt = f'tasklist | findstr {pid}' if system().lower() == 'windows' else f'ps -ef | grep {pid}' - p = popen(txt) - sleep(.05) - try: - if f' {pid} ' in p.read(): - ok = False - break - except TypeError: - pass - - if ok: - break - def _on_disconnect(self): - self.page._on_disconnect() - Browser.BROWSERS.pop(self.id, None) - if self.page._chromium_options.is_auto_port and self.page._chromium_options.user_data_path: - path = Path(self.page._chromium_options.user_data_path) + Chromium._BROWSERS.pop(self.id, None) + if self._chromium_options.is_auto_port and self._chromium_options.user_data_path: + path = Path(self._chromium_options.user_data_path) end_time = perf_counter() + 7 while perf_counter() < end_time: if not path.exists(): @@ -290,3 +512,59 @@ class Browser(object): except (PermissionError, FileNotFoundError, OSError): pass sleep(.03) + + +def handle_options(addr_or_opts): + """设置浏览器启动属性 + :param addr_or_opts: 'ip:port'、ChromiumOptions、Driver + :return: 返回ChromiumOptions对象 + """ + if not addr_or_opts: + _chromium_options = ChromiumOptions(addr_or_opts) + if _chromium_options.is_auto_port: + port, path = PortFinder(_chromium_options.tmp_path).get_port(_chromium_options.is_auto_port) + _chromium_options.set_address(f'127.0.0.1:{port}') + _chromium_options.set_user_data_path(path) + _chromium_options.auto_port(scope=_chromium_options.is_auto_port) + + elif isinstance(addr_or_opts, ChromiumOptions): + if addr_or_opts.is_auto_port: + port, path = PortFinder(addr_or_opts.tmp_path).get_port(addr_or_opts.is_auto_port) + addr_or_opts.set_address(f'127.0.0.1:{port}') + addr_or_opts.set_user_data_path(path) + addr_or_opts.auto_port(scope=addr_or_opts.is_auto_port) + _chromium_options = addr_or_opts + + elif isinstance(addr_or_opts, str): + _chromium_options = ChromiumOptions() + _chromium_options.set_address(addr_or_opts) + + elif isinstance(addr_or_opts, int): + _chromium_options = ChromiumOptions() + _chromium_options.set_local_port(addr_or_opts) + + else: + raise TypeError('只能接收ip:port格式或ChromiumOptions类型参数。') + + return _chromium_options + + +def run_browser(chromium_options): + """连接浏览器""" + is_exists = connect_browser(chromium_options) + try: + s = Session() + s.trust_env = False + ws = s.get(f'http://{chromium_options.address}/json/version', headers={'Connection': 'close'}) + if not ws: + raise BrowserConnectError('\n浏览器连接失败,请确认浏览器是否启动。') + json = ws.json() + browser_id = json['webSocketDebuggerUrl'].split('/')[-1] + is_headless = 'headless' in json['User-Agent'].lower() + ws.close() + s.close() + except KeyError: + raise BrowserConnectError('浏览器版本太旧或此浏览器不支持接管。') + except: + raise BrowserConnectError('\n浏览器连接失败,请确认浏览器是否启动。') + return is_headless, browser_id, is_exists diff --git a/DrissionPage/_base/browser.pyi b/DrissionPage/_base/browser.pyi index 170f88b..fa906ed 100644 --- a/DrissionPage/_base/browser.pyi +++ b/DrissionPage/_base/browser.pyi @@ -5,36 +5,80 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ -from typing import List, Optional, Union, Set, Dict +from threading import Lock +from typing import List, Optional, Set, Dict, Union, Tuple from .driver import BrowserDriver, Driver -from .._pages.chromium_page import ChromiumPage +from .._configs.chromium_options import ChromiumOptions +from .._configs.session_options import SessionOptions +from .._functions.cookies import CookiesList +from .._pages.chromium_base import Timeout +from .._pages.tabs import ChromiumTab, MixTab from .._units.downloader import DownloadManager +from .._units.setter import BrowserSetter +from .._units.waiter import BrowserWaiter -class Browser(object): - BROWSERS: dict = ... - page: ChromiumPage = ... - _driver: BrowserDriver = ... +class Chromium(object): id: str = ... address: str = ... + version: str = ... + retry_times: int = ... + retry_interval: float = ... + is_headless: bool = ... + + _BROWSERS: dict = ... + _chromium_options: ChromiumOptions = ... + _session_options: SessionOptions = ... + _driver: BrowserDriver = ... _frames: dict = ... _drivers: Dict[str, Driver] = ... _all_drivers: Dict[str, Set[Driver]] = ... _process_id: Optional[int] = ... _dl_mgr: DownloadManager = ... - _connected: bool = ... + _lock: Lock = ... - def __new__(cls, address: str, browser_id: str, page: ChromiumPage): ... + _set: Optional[BrowserSetter] = ... + _wait: Optional[BrowserWaiter] = ... + _timeouts: Timeout = ... + _load_mode: str = ... + _download_path: str = ... + _is_exists: bool = ... - def __init__(self, address: str, browser_id: str, page: ChromiumPage): ... + def __new__(cls, + addr_or_opts: Union[str, int, ChromiumOptions] = None, + session_options: Optional[SessionOptions] = None): ... + + def __init__(self, addr_or_opts: Union[str, int, ChromiumOptions] = None, + session_options: Optional[SessionOptions] = None): ... def _get_driver(self, tab_id: str, owner=None) -> Driver: ... - def run_cdp(self, cmd, **cmd_args) -> dict: ... + def _run_cdp(self, cmd, **cmd_args) -> dict: ... @property - def driver(self) -> BrowserDriver: ... + def user_data_path(self) -> str: ... + + @property + def process_id(self) -> Optional[int]: ... + + @property + def timeout(self) -> float: ... + + @property + def timeouts(self) -> Timeout: ... + + @property + def load_mode(self) -> str: ... + + @property + def download_path(self) -> str: ... + + @property + def set(self) -> BrowserSetter: ... + + @property + def wait(self) -> BrowserWaiter: ... @property def tabs_count(self) -> int: ... @@ -43,25 +87,77 @@ class Browser(object): def tab_ids(self) -> List[str]: ... @property - def process_id(self) -> Optional[int]: ... + def latest_tab(self) -> Union[ChromiumTab, str]: ... - def find_tabs(self, title: str = None, url: str = None, - tab_type: Union[str, list, tuple] = None) -> List[dict]: ... + def cookies(self, all_info: bool = False) -> CookiesList: ... - def close_tab(self, tab_id: str) -> None: ... + def close_tabs(self, + tabs_or_ids: Union[str, ChromiumTab, List[Union[str, ChromiumTab]], + Tuple[Union[str, ChromiumTab]]] = None, + others: bool = False) -> None: ... - def stop_driver(self, driver: Driver) -> None: ... + def get_tab(self, + id_or_num: Union[str, int] = None, + title: str = None, + url: str = None, + tab_type: str = 'page', + as_id: bool = False) -> Union[ChromiumTab, str]: ... - def activate_tab(self, tab_id: str) -> None: ... + def get_tabs(self, + title: str = None, + url: str = None, + tab_type: str = 'page', + as_id: bool = False) -> List[ChromiumTab, str]: ... - def get_window_bounds(self, tab_id: str = None) -> dict: ... + def get_mix_tab(self, + id_or_num: Union[str, int] = None, + title: str = None, + url: str = None, + tab_type: str = 'page') -> Union[MixTab, str]: ... - def new_tab(self, new_window: bool = False, background: bool = False, new_context: bool = False) -> str: ... + def get_mix_tabs(self, + title: str = None, + url: str = None, + tab_type: str = 'page') -> List[MixTab, str]: ... + + def _get_tab(self, + id_or_num: Union[str, int] = None, + title: str = None, + url: str = None, + tab_type: str = 'page', + mix: bool = False, + as_id: bool = False) -> Union[ChromiumTab, str]: ... + + def _get_tabs(self, + title: str = None, + url: str = None, + tab_type: str = 'page', + mix: bool = False, + as_id: bool = False) -> List[ChromiumTab, str]: ... + + def activate_tab(self, id_ind_tab: Union[int, str, ChromiumTab]) -> None: ... + + def _new_tab(self, + obj, + url: str = None, + new_window: bool = False, + background: bool = False, + new_context: bool = False) -> Union[ChromiumTab, MixTab]: ... + + def new_tab(self, + url: str = None, + new_window: bool = False, + background: bool = False, + new_context: bool = False) -> ChromiumTab: ... + + def new_mix_tab(self, + url: str = None, + new_window: bool = False, + background: bool = False, + new_context: bool = False) -> MixTab: ... def reconnect(self) -> None: ... - def connect_to_page(self) -> None: ... - def _onTargetCreated(self, **kwargs) -> None: ... def _onTargetDestroyed(self, **kwargs) -> None: ... diff --git a/DrissionPage/_base/driver.py b/DrissionPage/_base/driver.py index 3044a87..c95485a 100644 --- a/DrissionPage/_base/driver.py +++ b/DrissionPage/_base/driver.py @@ -7,7 +7,7 @@ """ from json import dumps, loads, JSONDecodeError from queue import Queue, Empty -from threading import Thread, Event +from threading import Thread from time import perf_counter, sleep from requests import Session @@ -15,7 +15,7 @@ from websocket import (WebSocketTimeoutException, WebSocketConnectionClosedExcep WebSocketException, WebSocketBadStatusException) from .._functions.settings import Settings -from ..errors import PageDisconnectedError +from ..errors import PageDisconnectedError, BrowserConnectError class Driver(object): @@ -30,6 +30,7 @@ class Driver(object): self.address = address self.type = tab_type self.owner = owner + # self._debug = True # self._debug = False self.alert_flag = False # 标记alert出现,跳过一条请求后复原 @@ -43,7 +44,7 @@ class Driver(object): self._handle_event_th.daemon = True self._handle_immediate_event_th = None - self._stopped = Event() + self.is_running = False self.event_handlers = {} self.immediate_event_handlers = {} @@ -86,7 +87,7 @@ class Driver(object): self.method_results.pop(ws_id, None) return {'error': {'message': 'connection disconnected'}, 'type': 'connection_error'} - while not self._stopped.is_set(): + while self.is_running: try: result = self.method_results[ws_id].get(timeout=.2) self.method_results.pop(ws_id, None) @@ -107,7 +108,7 @@ class Driver(object): def _recv_loop(self): """接收浏览器信息的守护线程方法""" - while not self._stopped.is_set(): + while self.is_running: try: # self._ws.settimeout(1) msg_json = self._ws.recv() @@ -145,7 +146,7 @@ class Driver(object): def _handle_event_loop(self): """当接收到浏览器信息,执行已绑定的方法""" - while not self._stopped.is_set(): + while self.is_running: try: event = self.event_queue.get(timeout=1) except Empty: @@ -158,7 +159,7 @@ class Driver(object): self.event_queue.task_done() def _handle_immediate_event_loop(self): - while not self._stopped.is_set() and not self.immediate_event_queue.empty(): + while not self.immediate_event_queue.empty(): function, kwargs = self.immediate_event_queue.get(timeout=1) try: function(**kwargs) @@ -183,7 +184,7 @@ class Driver(object): :param kwargs: cdp参数 :return: 执行结果 """ - if self._stopped.is_set(): + if not self.is_running: return {'error': 'connection disconnected', 'type': 'connection_error'} timeout = kwargs.pop('_timeout', Settings.cdp_timeout) @@ -191,13 +192,13 @@ class Driver(object): if 'result' not in result and 'error' in result: kwargs['_timeout'] = timeout return {'error': result['error']['message'], 'type': result.get('type', 'call_method_error'), - 'method': _method, 'args': kwargs} + 'method': _method, 'args': kwargs, 'data': result['error'].get('data')} else: return result['result'] def start(self): """启动连接""" - self._stopped.clear() + self.is_running = True try: self._ws = create_connection(self._websocket_url, enable_multithread=True, suppress_origin=True) except WebSocketBadStatusException as e: @@ -205,6 +206,8 @@ class Driver(object): raise RuntimeError('请升级websocket-client库。') else: return + except ConnectionRefusedError: + raise BrowserConnectError('浏览器未开启或已关闭。') self._recv_th.start() self._handle_event_th.start() return True @@ -218,15 +221,24 @@ class Driver(object): def _stop(self): """中断连接""" - if self._stopped.is_set(): + if not self.is_running: return False - self._stopped.set() + self.is_running = False if self._ws: self._ws.close() self._ws = None # try: + # while not self.immediate_event_queue.empty(): + # function, kwargs = self.immediate_event_queue.get_nowait() + # try: + # function(**kwargs) + # except PageDisconnectedError: + # raise + # pass + # sleep(.1) + # # while not self.event_queue.empty(): # event = self.event_queue.get_nowait() # function = self.event_handlers.get(event['method']) diff --git a/DrissionPage/_base/driver.pyi b/DrissionPage/_base/driver.pyi index b3f44f9..be5f7c1 100644 --- a/DrissionPage/_base/driver.pyi +++ b/DrissionPage/_base/driver.pyi @@ -6,13 +6,13 @@ @License : BSD 3-Clause. """ from queue import Queue -from threading import Thread, Event +from threading import Thread from typing import Union, Callable, Dict, Optional from requests import Response, Session from websocket import WebSocket -from .browser import Browser +from .browser import Chromium class GenericAttr(object): @@ -35,7 +35,8 @@ class Driver(object): _recv_th: Thread _handle_event_th: Thread _handle_immediate_event_th: Optional[Thread] - _stopped: Event + # _stopped: Event + is_running: bool event_handlers: dict immediate_event_handlers: dict method_results: dict @@ -67,11 +68,11 @@ class Driver(object): class BrowserDriver(Driver): BROWSERS: Dict[str, Driver] = ... - owner: Browser = ... + owner: Chromium = ... _control_session: Session = ... - def __new__(cls, tab_id: str, tab_type: str, address: str, owner: Browser): ... + def __new__(cls, tab_id: str, tab_type: str, address: str, owner: Chromium): ... - def __init__(self, tab_id: str, tab_type: str, address: str, owner: Browser): ... + def __init__(self, tab_id: str, tab_type: str, address: str, owner: Chromium): ... def get(self, url) -> Response: ... diff --git a/DrissionPage/_configs/chromium_options.py b/DrissionPage/_configs/chromium_options.py index 86e64d2..bec890e 100644 --- a/DrissionPage/_configs/chromium_options.py +++ b/DrissionPage/_configs/chromium_options.py @@ -21,7 +21,7 @@ class ChromiumOptions(object): self._user = 'Default' self._prefs_to_del = [] self.clear_file_flags = False - self._headless = None + self._is_headless = False if read_file is False: ini_path = False @@ -33,10 +33,10 @@ class ChromiumOptions(object): self.ini_path = str(ini_path) else: self.ini_path = str(Path(__file__).parent / 'configs.ini') - om = OptionsManager(ini_path) + om = OptionsManager(ini_path) options = om.chromium_options - self._download_path = om.paths.get('download_path', None) or None + self._download_path = om.paths.get('download_path', '.') or '.' self._tmp_path = om.paths.get('tmp_path', None) or None self._arguments = options.get('arguments', []) self._browser_path = options.get('browser_path', '') @@ -47,6 +47,11 @@ class ChromiumOptions(object): self._load_mode = options.get('load_mode', 'normal') self._system_user_path = options.get('system_user_path', False) self._existing_only = options.get('existing_only', False) + self._new_env = options.get('new_env', False) + for i in self._arguments: + if i.startswith('--headless'): + self._is_headless = True + break self._proxy = om.proxies.get('http', None) or om.proxies.get('https', None) @@ -164,6 +169,11 @@ class ChromiumOptions(object): """返回连接失败时的重试间隔(秒)""" return self._retry_interval + @property + def is_headless(self): + """返回是否无头模式""" + return self._is_headless + def set_retry(self, times=None, interval=None): """设置连接失败时的重试操作 :param times: 重试次数 @@ -184,11 +194,19 @@ class ChromiumOptions(object): """ self.remove_argument(arg) if value is not False: - if arg == '--headless' and value is None: - self._arguments.append('--headless=new') + if arg == '--headless': + if value == 'false': + self._is_headless = False + else: + if value is None: + value = 'new' + self._arguments.append(f'--headless={value}') + self._is_headless = True else: arg_str = arg if value is None else f'{arg}={value}' self._arguments.append(arg_str) + elif arg == '--headless': + self._is_headless = False return self def remove_argument(self, value): @@ -196,14 +214,14 @@ class ChromiumOptions(object): :param value: 设置项名,有值的设置项传入设置名称即可 :return: 当前对象 """ - del_list = [] + elements_to_delete = [arg for arg in self._arguments if arg == value or arg.startswith(f'{value}=')] + if not elements_to_delete: + return self - for argument in self._arguments: - if argument == value or argument.startswith(f'{value}='): - del_list.append(argument) - - for del_arg in del_list: - self._arguments.remove(del_arg) + if len(elements_to_delete) == 1: + self._arguments.remove(elements_to_delete[0]) + else: + self._arguments = [arg for arg in self._arguments if arg not in elements_to_delete] return self @@ -282,14 +300,13 @@ class ChromiumOptions(object): self._prefs = {} return self - def set_timeouts(self, base=None, page_load=None, script=None, implicit=None): + def set_timeouts(self, base=None, page_load=None, script=None): """设置超时时间,单位为秒 :param base: 默认超时时间 :param page_load: 页面加载超时时间 :param script: 脚本运行超时时间 :return: 当前对象 """ - base = base if base is not None else implicit if base is not None: self._timeouts['base'] = base if page_load is not None: @@ -313,7 +330,7 @@ class ChromiumOptions(object): :param on_off: 开或关 :return: 当前对象 """ - on_off = 'new' if on_off else 'false' + on_off = 'new' if on_off else on_off return self.set_argument('--headless', on_off) def no_imgs(self, on_off=True): @@ -348,6 +365,14 @@ class ChromiumOptions(object): on_off = None if on_off else False return self.set_argument('--incognito', on_off) + def new_env(self, on_off=True): + """设置是否使用全新浏览器环境 + :param on_off: 开或关 + :return: 当前对象 + """ + self._new_env = on_off + return self + def ignore_certificate_errors(self, on_off=True): """设置是否忽略证书错误 :param on_off: 开或关 @@ -450,7 +475,7 @@ class ChromiumOptions(object): :param path: 下载路径 :return: 当前对象 """ - self._download_path = str(path) + self._download_path = '.' if path is None else str(path) return self def set_tmp_path(self, path): @@ -488,17 +513,14 @@ class ChromiumOptions(object): self._system_user_path = on_off return self - def auto_port(self, on_off=True, tmp_path=None, scope=None): + def auto_port(self, on_off=True, scope=None): """自动获取可用端口 :param on_off: 是否开启自动获取端口号 - :param tmp_path: 临时文件保存路径,为None时保存到系统临时文件夹,on_off为False时此参数无效 - :param scope: 指定端口范围,不含最后的数字,为None则使用[9600-19600) + :param scope: 指定端口范围,不含最后的数字,为None则使用[9600-59600) :return: 当前对象 """ if on_off: - self._auto_port = scope if scope else True - if tmp_path: - self._tmp_path = str(tmp_path) + self._auto_port = scope if scope else (9600, 59600) else: self._auto_port = False return self @@ -537,7 +559,7 @@ class ChromiumOptions(object): # 设置chromium_options attrs = ('address', 'browser_path', 'arguments', 'extensions', 'user', 'load_mode', - 'auto_port', 'system_user_path', 'existing_only', 'flags') + 'auto_port', 'system_user_path', 'existing_only', 'flags', 'new_env') for i in attrs: om.set_item('chromium_options', i, self.__getattribute__(f'_{i}')) # 设置代理 diff --git a/DrissionPage/_configs/chromium_options.pyi b/DrissionPage/_configs/chromium_options.pyi index c682a27..10ba9a8 100644 --- a/DrissionPage/_configs/chromium_options.pyi +++ b/DrissionPage/_configs/chromium_options.pyi @@ -10,30 +10,32 @@ from typing import Union, Any, Literal, Optional, Tuple class ChromiumOptions(object): - def __init__(self, read_file: [bool, None] = True, ini_path: Union[str, Path] = None): - self.ini_path: str = ... - self._driver_path: str = ... - self._user_data_path: str = ... - self._download_path: str = ... - self._tmp_path: str = ... - self._arguments: list = ... - self._browser_path: str = ... - self._user: str = ... - self._load_mode: str = ... - self._timeouts: dict = ... - self._proxy: str = ... - self._address: str = ... - self._extensions: list = ... - self._prefs: dict = ... - self._flags: dict = ... - self._prefs_to_del: list = ... - self.clear_file_flags: bool = ... - self._auto_port: bool = ... - self._system_user_path: bool = ... - self._existing_only: bool = ... - self._headless: bool = ... - self._retry_times: int = ... - self._retry_interval: float = ... + ini_path: Optional[str] = ... + _driver_path: str = ... + _user_data_path: Optional[str] = ... + _download_path: str = ... + _tmp_path: str = ... + _arguments: list = ... + _browser_path: str = ... + _user: str = ... + _load_mode: str = ... + _timeouts: dict = ... + _proxy: str = ... + _address: str = ... + _extensions: list = ... + _prefs: dict = ... + _flags: dict = ... + _prefs_to_del: list = ... + _new_env: bool = ... + clear_file_flags: bool = ... + _auto_port: Union[Tuple[int, int], False] = ... + _system_user_path: bool = ... + _existing_only: bool = ... + _retry_times: int = ... + _retry_interval: float = ... + _is_headless: bool = ... + + def __init__(self, read_file: [bool, None] = True, ini_path: Union[str, Path] = None): ... @property def download_path(self) -> str: ... @@ -89,6 +91,9 @@ class ChromiumOptions(object): @property def retry_interval(self) -> float: ... + @property + def is_headless(self) -> bool: ... + def set_retry(self, times: int = None, interval: float = None) -> ChromiumOptions: ... def set_argument(self, arg: str, value: Union[str, None, bool] = None) -> ChromiumOptions: ... @@ -132,6 +137,8 @@ class ChromiumOptions(object): def incognito(self, on_off: bool = True) -> ChromiumOptions: ... + def new_env(self, on_off: bool = True) -> ChromiumOptions: ... + def set_user_agent(self, user_agent: str) -> ChromiumOptions: ... def set_proxy(self, proxy: str) -> ChromiumOptions: ... @@ -162,7 +169,6 @@ class ChromiumOptions(object): def auto_port(self, on_off: bool = True, - tmp_path: Union[str, Path] = None, scope: Tuple[int, int] = None) -> ChromiumOptions: ... def existing_only(self, on_off: bool = True) -> ChromiumOptions: ... diff --git a/DrissionPage/_configs/configs.ini b/DrissionPage/_configs/configs.ini index f2de400..9e516e5 100644 --- a/DrissionPage/_configs/configs.ini +++ b/DrissionPage/_configs/configs.ini @@ -14,6 +14,7 @@ user = Default auto_port = False system_user_path = False existing_only = False +new_env = False [session_options] headers = {'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'connection': 'keep-alive', 'accept-charset': 'GB2312,utf-8;q=0.7,*;q=0.7'} diff --git a/DrissionPage/_configs/options_manage.py b/DrissionPage/_configs/options_manage.py index f9c85f4..6638c50 100644 --- a/DrissionPage/_configs/options_manage.py +++ b/DrissionPage/_configs/options_manage.py @@ -64,6 +64,7 @@ class OptionsManager(object): self.set_item('chromium_options', 'auto_port', 'False') self.set_item('chromium_options', 'system_user_path', 'False') self.set_item('chromium_options', 'existing_only', 'False') + self.set_item('chromium_options', 'new_env', 'False') self.set_item('session_options', 'headers', "{'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X " "10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10." "1.2 Safari/603.3.8', 'accept': 'text/html,application/xhtml" diff --git a/DrissionPage/_configs/session_options.py b/DrissionPage/_configs/session_options.py index a533e4d..9794395 100644 --- a/DrissionPage/_configs/session_options.py +++ b/DrissionPage/_configs/session_options.py @@ -12,7 +12,8 @@ from requests import Session from requests.structures import CaseInsensitiveDict from .options_manage import OptionsManager -from .._functions.web import cookies_to_tuple, set_session_cookies, format_headers +from .._functions.cookies import cookies_to_tuple, set_session_cookies +from .._functions.web import format_headers class SessionOptions(object): @@ -24,7 +25,7 @@ class SessionOptions(object): :param ini_path: ini文件路径 """ self.ini_path = None - self._download_path = None + self._download_path = '.' self._timeout = 10 self._del_set = set() # 记录要从ini文件删除的参数 @@ -83,7 +84,7 @@ class SessionOptions(object): self.set_proxies(om.proxies.get('http', None), om.proxies.get('https', None)) self._timeout = om.timeouts.get('base', 10) - self._download_path = om.paths.get('download_path', None) or None + self._download_path = om.paths.get('download_path', '.') or '.' others = om.others self._retry_times = others.get('retry_times', 3) @@ -100,7 +101,7 @@ class SessionOptions(object): :param path: 下载路径 :return: 返回当前对象 """ - self._download_path = str(path) + self._download_path = '.' if path is None else str(path) return self @property @@ -419,7 +420,7 @@ class SessionOptions(object): return session_options_to_dict(self) def make_session(self): - """根据内在的配置生成Session对象,ua从对象中分离""" + """根据内在的配置生成Session对象,headers从对象中分离""" s = Session() h = CaseInsensitiveDict(self.headers) if self.headers else CaseInsensitiveDict() diff --git a/DrissionPage/_elements/chromium_element.py b/DrissionPage/_elements/chromium_element.py index 3f42b5f..539a028 100644 --- a/DrissionPage/_elements/chromium_element.py +++ b/DrissionPage/_elements/chromium_element.py @@ -16,10 +16,10 @@ from DataRecorder.tools import get_usable_path, make_valid_name from .none_element import NoneElement from .session_element import make_session_ele from .._base.base import DrissionElement, BaseElement +from .._functions.elements import ChromiumElementsList, SessionElementsList from .._functions.keys import input_text_or_keys 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 .._functions.web import make_absolute_link, get_ele_txt, format_html, is_js_func, get_blob from .._units.clicker import Clicker from .._units.rect import ElementRect from .._units.scroller import ElementScroller @@ -44,7 +44,7 @@ class ChromiumElement(DrissionElement): :param backend_id: backend id """ super().__init__(owner) - self.tab = self.owner.tab + self.tab = self.owner._tab self._select = None self._scroll = None self._rect = None @@ -95,29 +95,29 @@ class ChromiumElement(DrissionElement): def tag(self): """返回元素tag""" if self._tag is None: - self._tag = self.owner.run_cdp('DOM.describeNode', - backendNodeId=self._backend_id)['node']['localName'].lower() + self._tag = self.owner._run_cdp('DOM.describeNode', + backendNodeId=self._backend_id)['node']['localName'].lower() return self._tag @property def html(self): """返回元素outerHTML文本""" - return self.owner.run_cdp('DOM.getOuterHTML', backendNodeId=self._backend_id)['outerHTML'] + return self.owner._run_cdp('DOM.getOuterHTML', backendNodeId=self._backend_id)['outerHTML'] @property def inner_html(self): """返回元素innerHTML文本""" - return self.run_js('return this.innerHTML;') + return self._run_js('return this.innerHTML;') @property def attrs(self): """返回元素所有attribute属性""" try: - attrs = self.owner.run_cdp('DOM.getAttributes', nodeId=self._node_id)['attributes'] + attrs = self.owner._run_cdp('DOM.getAttributes', nodeId=self._node_id)['attributes'] return {attrs[i]: attrs[i + 1] for i in range(0, len(attrs), 2)} except ElementLostError: self._refresh_id() - attrs = self.owner.run_cdp('DOM.getAttributes', nodeId=self._node_id)['attributes'] + attrs = self.owner._run_cdp('DOM.getAttributes', nodeId=self._node_id)['attributes'] return {attrs[i]: attrs[i + 1] for i in range(0, len(attrs), 2)} except CDPError: # 文档根元素不能调用此方法 return {} @@ -162,18 +162,18 @@ class ChromiumElement(DrissionElement): return self._rect @property - def shadow_root(self): + def sr(self): """返回当前元素的shadow_root元素对象""" - info = self.owner.run_cdp('DOM.describeNode', backendNodeId=self._backend_id)['node'] + info = self.owner._run_cdp('DOM.describeNode', backendNodeId=self._backend_id)['node'] if not info.get('shadowRoots', None): return None return ShadowRoot(self, backend_id=info['shadowRoots'][0]['backendNodeId']) @property - def sr(self): + def shadow_root(self): """返回当前元素的shadow_root元素对象""" - return self.shadow_root + return self.sr @property def scroll(self): @@ -193,7 +193,7 @@ class ChromiumElement(DrissionElement): def wait(self): """返回用于等待的对象""" if self._wait is None: - self._wait = ElementWaiter(self.owner, self) + self._wait = ElementWaiter(self) return self._wait @property @@ -225,8 +225,8 @@ class ChromiumElement(DrissionElement): elif not is_checked and not uncheck: js = 'this.checked=true' if js: - self.run_js(js) - self.run_js('this.dispatchEvent(new Event("change", {bubbles: true}));') + self._run_js(js) + self._run_js('this.dispatchEvent(new Event("change", {bubbles: true}));') else: if (is_checked and uncheck) or (not is_checked and not uncheck): @@ -351,20 +351,56 @@ class ChromiumElement(DrissionElement): else: return NoneElement(page=self.owner, method='on()', args={'timeout': timeout}) - def offset(self, offset_x, offset_y): - """获取相对本元素左上角左边指定偏移量位置的元素 - :param offset_x: 横坐标偏移量,向右为正 - :param offset_y: 纵坐标偏移量,向下为正 + def offset(self, locator=None, x=None, y=None, timeout=None): + """获取相对本元素左上角左边指定偏移量位置的元素,如果offset_x和offset_y都是None,定位到元素中间点 + :param locator: 定位符,只支持str,且不支持xpath和css方式 + :param x: 横坐标偏移量,向右为正 + :param y: 纵坐标偏移量,向下为正 + :param timeout: 超时时间(秒),为None使用所在页面设置 :return: 元素对象 """ - x, y = self.rect.location + if locator and not (isinstance(locator, str) and not locator.startswith( + ('x:', 'xpath:', 'x=', 'xpath=', 'c:', 'css:', 'c=', 'css='))): + raise ValueError('locator参数只能是str格式且不支持xpath和css形式。') + + if x == y is None: + x, y = self.rect.midpoint + x = int(x) + y = int(y) + else: + nx, ny = self.rect.location + nx += x if x else 0 + ny += y if y else 0 + x = int(nx) + y = int(ny) + loc_data = locator_to_tuple(locator) if locator else None + timeout = timeout if timeout is not None else self.owner.timeout + end_time = perf_counter() + timeout try: - return ChromiumElement(owner=self.owner, - backend_id=self.owner.run_cdp('DOM.getNodeForLocation', x=x + offset_x, - y=y + offset_y, includeUserAgentShadowDOM=True, + ele = ChromiumElement(owner=self.owner, + backend_id=self.owner._run_cdp('DOM.getNodeForLocation', x=x, y=y, + includeUserAgentShadowDOM=True, ignorePointerEventsNone=False)['backendNodeId']) except CDPError: - return NoneElement(page=self.owner, method='offset()', args={'offset_x': offset_x, 'offset_y': offset_y}) + ele = False + if ele and (loc_data is None or _check_ele(ele, loc_data)): + return ele + + while perf_counter() < end_time: + try: + ele = ChromiumElement(owner=self.owner, + backend_id=self.owner._run_cdp('DOM.getNodeForLocation', x=x, y=y, + includeUserAgentShadowDOM=True, + ignorePointerEventsNone=False)['backendNodeId']) + except CDPError: + ele = False + + if ele and (loc_data is None or _check_ele(ele, loc_data)): + return ele + sleep(.1) + + return NoneElement(page=self.owner, method='offset()', + args={'locator': locator, 'offset_x': x, 'offset_y': y, 'timeout': timeout}) def east(self, loc_or_pixel=None, index=1): """获取元素右边某个指定元素 @@ -439,8 +475,8 @@ class ChromiumElement(DrissionElement): cdp_data[variable] += locator try: return ChromiumElement(owner=self.owner, - backend_id=self.owner.run_cdp('DOM.getNodeForLocation', - **cdp_data)['backendNodeId']) + backend_id=self.owner._run_cdp('DOM.getNodeForLocation', + **cdp_data)['backendNodeId']) except CDPError: return NoneElement(page=self.owner, method=f'{mode}()', args={'locator': locator}) @@ -453,7 +489,7 @@ class ChromiumElement(DrissionElement): while 0 < cdp_data[variable] < max_len: cdp_data[variable] += value try: - bid = self.owner.run_cdp('DOM.getNodeForLocation', **cdp_data)['backendNodeId'] + bid = self.owner._run_cdp('DOM.getNodeForLocation', **cdp_data)['backendNodeId'] if bid == curr_ele: continue else: @@ -505,7 +541,7 @@ class ChromiumElement(DrissionElement): :param name: 属性名 :return: None """ - self.run_js(f'this.removeAttribute("{name}");') + self._run_js(f'this.removeAttribute("{name}");') def property(self, name): """获取一个property属性值 @@ -513,12 +549,22 @@ class ChromiumElement(DrissionElement): :return: 属性值文本 """ try: - value = self.run_js(f'return this.{name};') + value = self._run_js(f'return this.{name};') return format_html(value) if isinstance(value, str) else value except: return None def run_js(self, script, *args, as_expr=False, timeout=None): + """对本元素执行javascript代码 + :param script: js文本,文本中用this表示本元素 + :param args: 参数,按顺序在js文本中对应arguments[0]、arguments[1]... + :param as_expr: 是否作为表达式运行,为True时args无效 + :param timeout: js超时时间(秒),为None则使用页面timeouts.script设置 + :return: 运行的结果 + """ + return self._run_js(script, *args, as_expr=as_expr, timeout=timeout) + + def _run_js(self, script, *args, as_expr=False, timeout=None): """对本元素执行javascript代码 :param script: js文本,文本中用this表示本元素 :param args: 参数,按顺序在js文本中对应arguments[0]、arguments[1]... @@ -554,27 +600,32 @@ class ChromiumElement(DrissionElement): """ return self._ele(locator, timeout=timeout, index=None) - def s_ele(self, locator=None, index=1): + def s_ele(self, locator=None, index=1, timeout=None): """查找一个符合条件的元素,以SessionElement形式返回 :param locator: 元素的定位信息,可以是loc元组,或查询字符串 :param index: 获取第几个,从1开始,可传入负数获取倒数第几个 + :param timeout: 查找元素超时时间(秒),默认与元素所在页面等待时间一致 :return: SessionElement对象或属性、文本 """ - return make_session_ele(self, locator, index=index, method='s_ele()') + return (make_session_ele(self, locator, index=index, method='s_ele()') + if self.ele(locator, index=index, timeout=timeout) + else NoneElement(self, method='s_ele()', args={'locator': locator, 'index': index})) - def s_eles(self, locator=None): + def s_eles(self, locator=None, timeout=None): """查找所有符合条件的元素,以SessionElement列表形式返回 :param locator: 定位符 + :param timeout: 查找元素超时时间(秒),默认与元素所在页面等待时间一致 :return: SessionElement或属性、文本组成的列表 """ - return make_session_ele(self, locator, index=None) + return (make_session_ele(self, locator, index=None) + if self.ele(locator, timeout=timeout) else SessionElementsList()) def _find_elements(self, locator, timeout=None, index=1, relative=False, raise_err=None): """返回当前元素下级符合条件的子元素、属性或节点文本,默认返回第一个 :param locator: 元素的定位信息,可以是loc元组,或查询字符串 :param timeout: 查找元素超时时间(秒) :param index: 第几个结果,从1开始,可传入负数获取倒数第几个,为None返回所有 - :param relative: WebPage用的表示是否相对定位的参数 + :param relative: MixTab用的表示是否相对定位的参数 :param raise_err: 找不到元素是是否抛出异常,为None时根据全局设置 :return: ChromiumElement对象或文本、属性或其组成的列表 """ @@ -588,7 +639,7 @@ class ChromiumElement(DrissionElement): """ if pseudo_ele: pseudo_ele = f', "{pseudo_ele}"' if pseudo_ele.startswith(':') else f', "::{pseudo_ele}"' - return self.run_js(f'return window.getComputedStyle(this{pseudo_ele}).getPropertyValue("{style}");') + return self._run_js(f'return window.getComputedStyle(this{pseudo_ele}).getPropertyValue("{style}");') def src(self, timeout=None, base64_to_bytes=True): """返回元素src资源,base64的可转为bytes返回,其它返回str @@ -602,7 +653,7 @@ class ChromiumElement(DrissionElement): '&& this.naturalWidth > 0 && typeof this.naturalHeight != "undefined" ' '&& this.naturalHeight > 0') end_time = perf_counter() + timeout - while not self.run_js(js) and perf_counter() < end_time: + while not self._run_js(js) and perf_counter() < end_time: sleep(.1) src = self.attr('src') @@ -631,11 +682,11 @@ class ChromiumElement(DrissionElement): if not src: continue - node = self.owner.run_cdp('DOM.describeNode', backendNodeId=self._backend_id)['node'] + node = self.owner._run_cdp('DOM.describeNode', backendNodeId=self._backend_id)['node'] frame = node.get('frameId', None) or self.owner._frame_id try: - result = self.owner.run_cdp('Page.getResourceContent', frameId=frame, url=src) + result = self.owner._run_cdp('Page.getResourceContent', frameId=frame, url=src) break except CDPError: pass @@ -698,7 +749,7 @@ class ChromiumElement(DrissionElement): js = ('return this.complete && typeof this.naturalWidth != "undefined" && this.naturalWidth > 0 ' '&& typeof this.naturalHeight != "undefined" && this.naturalHeight > 0') end_time = perf_counter() + self.owner.timeout - while not self.run_js(js) and perf_counter() < end_time: + while not self._run_js(js) and perf_counter() < end_time: sleep(.1) if scroll_to_center: self.scroll.to_see(center=True) @@ -729,7 +780,7 @@ class ChromiumElement(DrissionElement): if isinstance(vals, (list, tuple)): vals = ''.join([str(i) for i in vals]) self.set.property('value', str(vals)) - self.run_js('this.dispatchEvent(new Event("change", {bubbles: true}));') + self._run_js('this.dispatchEvent(new Event("change", {bubbles: true}));') return self.wait.clickable(wait_moved=False, timeout=.5) @@ -738,7 +789,11 @@ class ChromiumElement(DrissionElement): else: self._input_focus() - input_text_or_keys(self.owner, vals) + if isinstance(vals, str) and vals not in ('\ue003', '\ue017', '\ue010', '\ue011', + '\ue012', '\ue013', '\ue014', '\ue015',): + input_text_or_keys(self.owner, vals) + else: + self.owner.actions.type(vals) def clear(self, by_js=False): """清空元素文本 @@ -746,8 +801,8 @@ class ChromiumElement(DrissionElement): :return: None """ if by_js: - self.run_js("this.value='';") - self.run_js('this.dispatchEvent(new Event("change", {bubbles: true}));') + self._run_js("this.value='';") + self._run_js('this.dispatchEvent(new Event("change", {bubbles: true}));') return self._input_focus() @@ -756,26 +811,24 @@ class ChromiumElement(DrissionElement): def _input_focus(self): """输入前使元素获取焦点""" try: - self.owner.run_cdp('DOM.focus', backendNodeId=self._backend_id) + self.owner._run_cdp('DOM.focus', backendNodeId=self._backend_id) except Exception: self.click(by_js=None) def focus(self): """使元素获取焦点""" try: - self.owner.run_cdp('DOM.focus', backendNodeId=self._backend_id) + self.owner._run_cdp('DOM.focus', backendNodeId=self._backend_id) except Exception: - self.run_js('this.focus();') + self._run_js('this.focus();') def hover(self, offset_x=None, offset_y=None): - """鼠标悬停,可接受偏移量,偏移量相对于元素左上角坐标。不传入x或y值时悬停在元素中点 + """鼠标悬停,可接受偏移量,偏移量相对于元素左上角坐标。不传入offset_x和offset_y值时悬停在元素中点 :param offset_x: 相对元素左上角坐标的x轴偏移量 :param offset_y: 相对元素左上角坐标的y轴偏移量 :return: None """ - self.owner.scroll.to_see(self) - x, y = offset_scroll(self, offset_x, offset_y) - self.owner.run_cdp('Input.dispatchMouseEvent', type='mouseMoved', x=x, y=y, _ignore=AlertExistsError) + self.owner.actions.move_to(self, offset_x=offset_x, offset_y=offset_y, duration=.1) def drag(self, offset_x=0, offset_y=0, duration=.5): """拖拽当前元素到相对位置 @@ -808,9 +861,9 @@ class ChromiumElement(DrissionElement): :return: js中的object id """ if node_id: - return self.owner.run_cdp('DOM.resolveNode', nodeId=node_id)['object']['objectId'] + return self.owner._run_cdp('DOM.resolveNode', nodeId=node_id)['object']['objectId'] else: - return self.owner.run_cdp('DOM.resolveNode', backendNodeId=backend_id)['object']['objectId'] + return self.owner._run_cdp('DOM.resolveNode', backendNodeId=backend_id)['object']['objectId'] def _get_node_id(self, obj_id=None, backend_id=None): """根据传入object id或backend id获取cdp中的node id @@ -819,9 +872,9 @@ class ChromiumElement(DrissionElement): :return: cdp中的node id """ if obj_id: - return self.owner.run_cdp('DOM.requestNode', objectId=obj_id)['nodeId'] + return self.owner._run_cdp('DOM.requestNode', objectId=obj_id)['nodeId'] else: - n = self.owner.run_cdp('DOM.describeNode', backendNodeId=backend_id)['node'] + n = self.owner._run_cdp('DOM.describeNode', backendNodeId=backend_id)['node'] self._tag = n['localName'] return n['nodeId'] @@ -830,7 +883,7 @@ class ChromiumElement(DrissionElement): :param node_id: :return: backend id """ - n = self.owner.run_cdp('DOM.describeNode', nodeId=node_id)['node'] + n = self.owner._run_cdp('DOM.describeNode', nodeId=node_id)['node'] self._tag = n['localName'] return n['backendNodeId'] @@ -850,7 +903,11 @@ class ChromiumElement(DrissionElement): txt5 = '''return path;''' elif mode == 'css': - txt1 = '' + txt1 = ''' + let i = el.getAttribute("id"); + if (i){path = '>' + el.tagName.toLowerCase() + "#" + i + path; + break;} + ''' txt3 = '' txt4 = '''path = '>' + el.tagName.toLowerCase() + ":nth-child(" + nth + ")" + path;''' txt5 = '''return path.substr(1);''' @@ -860,6 +917,7 @@ class ChromiumElement(DrissionElement): js = '''function(){ function e(el) { + //return el; if (!(el instanceof Element)) return; let path = ''; while (el.nodeType === Node.ELEMENT_NODE) { @@ -876,7 +934,7 @@ class ChromiumElement(DrissionElement): } return e(this);} ''' - t = self.run_js(js) + t = self._run_js(js) return f'{t}' if mode == 'css' else t def _set_file_input(self, files): @@ -887,7 +945,7 @@ class ChromiumElement(DrissionElement): if isinstance(files, str): files = files.split('\n') files = [str(Path(i).absolute()) for i in files] - self.owner.run_cdp('DOM.setFileInputFiles', files=files, backendNodeId=self._backend_id) + self.owner._run_cdp('DOM.setFileInputFiles', files=files, backendNodeId=self._backend_id) class ShadowRoot(BaseElement): @@ -900,7 +958,7 @@ class ShadowRoot(BaseElement): :param backend_id: cdp中的backend id """ super().__init__(parent_ele.owner) - self.tab = self.owner.tab + self.tab = self.owner._tab self.parent_ele = parent_ele if backend_id: self._backend_id = backend_id @@ -942,7 +1000,7 @@ class ShadowRoot(BaseElement): @property def inner_html(self): """返回内部的html文本""" - return self.run_js('return this.innerHTML;') + return self._run_js('return this.innerHTML;') @property def states(self): @@ -952,6 +1010,16 @@ class ShadowRoot(BaseElement): return self._states def run_js(self, script, *args, as_expr=False, timeout=None): + """运行javascript代码 + :param script: js文本 + :param args: 参数,按顺序在js文本中对应arguments[0]、arguments[1]... + :param as_expr: 是否作为表达式运行,为True时args无效 + :param timeout: js超时时间(秒),为None则使用页面timeouts.script设置 + :return: 运行的结果 + """ + return self._run_js(script, *args, as_expr=as_expr, timeout=timeout) + + def _run_js(self, script, *args, as_expr=False, timeout=None): """运行javascript代码 :param script: js文本 :param args: 参数,按顺序在js文本中对应arguments[0]、arguments[1]... @@ -1128,31 +1196,32 @@ class ShadowRoot(BaseElement): """ return self._ele(locator, timeout=timeout, index=None) - def s_ele(self, locator=None, index=1): + def s_ele(self, locator=None, index=1, timeout=None): """查找一个符合条件的元素以SessionElement形式返回,处理复杂页面时效率很高 :param locator: 元素的定位信息,可以是loc元组,或查询字符串 :param index: 获取第几个,从1开始,可传入负数获取倒数第几个 + :param timeout: 查找元素超时时间(秒),默认与元素所在页面等待时间一致 :return: SessionElement对象或属性、文本 """ - r = make_session_ele(self, locator, index=index) - if isinstance(r, NoneElement): - r.method = 's_ele()' - r.args = {'locator': locator} - return r + return (make_session_ele(self, locator, index=index, method='s_ele()') + if self.ele(locator, index=index, timeout=timeout) + else NoneElement(self, method='s_ele()', args={'locator': locator, 'index': index})) - def s_eles(self, locator): + def s_eles(self, locator, timeout=None): """查找所有符合条件的元素以SessionElement列表形式返回,处理复杂页面时效率很高 :param locator: 元素的定位信息,可以是loc元组,或查询字符串 + :param timeout: 查找元素超时时间(秒),默认与元素所在页面等待时间一致 :return: SessionElement对象 """ - return make_session_ele(self, locator, index=None) + return (make_session_ele(self, locator, index=None) + if self.ele(locator, timeout=timeout) else SessionElementsList()) def _find_elements(self, locator, timeout=None, index=1, relative=False, raise_err=None): """返回当前元素下级符合条件的子元素、属性或节点文本,默认返回第一个 :param locator: 元素的定位信息,可以是loc元组,或查询字符串 :param timeout: 查找元素超时时间(秒) :param index: 第几个结果,从1开始,可传入负数获取倒数第几个,为None返回所有 - :param relative: WebPage用的表示是否相对定位的参数 + :param relative: MixTab用的表示是否相对定位的参数 :param raise_err: 找不到元素是是否抛出异常,为None时根据全局设置 :return: ChromiumElement对象或其组成的列表 """ @@ -1163,14 +1232,14 @@ class ShadowRoot(BaseElement): def do_find(): if loc[0] == 'css selector': if index == 1: - nod_id = self.owner.run_cdp('DOM.querySelector', nodeId=self._node_id, selector=loc[1])['nodeId'] + nod_id = self.owner._run_cdp('DOM.querySelector', nodeId=self._node_id, selector=loc[1])['nodeId'] if nod_id: r = make_chromium_eles(self.owner, _ids=nod_id, is_obj_id=False) return None if r is False else r else: - nod_ids = self.owner.run_cdp('DOM.querySelectorAll', - nodeId=self._node_id, selector=loc[1])['nodeId'] + nod_ids = self.owner._run_cdp('DOM.querySelectorAll', + nodeId=self._node_id, selector=loc[1])['nodeId'] r = make_chromium_eles(self.owner, _ids=nod_ids, index=index, is_obj_id=False) return None if r is False else r @@ -1179,17 +1248,22 @@ class ShadowRoot(BaseElement): if not eles: return None - css = [i.css_path[61:] for i in eles] + css = [] + for i in eles: + c = i.css_path + if c.startswith('html:nth-child(1)>body:nth-child(1)>shadow_root:nth-child(1)'): + c = c[61:] + css.append(c) if index is not None: try: - node_id = self.owner.run_cdp('DOM.querySelector', nodeId=self._node_id, - selector=css[index - 1])['nodeId'] + node_id = self.owner._run_cdp('DOM.querySelector', nodeId=self._node_id, + selector=css[index - 1])['nodeId'] except IndexError: return None r = make_chromium_eles(self.owner, _ids=node_id, is_obj_id=False) return None if r is False else r else: - node_ids = [self.owner.run_cdp('DOM.querySelector', nodeId=self._node_id, selector=i)['nodeId'] + node_ids = [self.owner._run_cdp('DOM.querySelector', nodeId=self._node_id, selector=i)['nodeId'] for i in css] if 0 in node_ids: return None @@ -1209,15 +1283,15 @@ class ShadowRoot(BaseElement): def _get_node_id(self, obj_id): """返回元素node id""" - return self.owner.run_cdp('DOM.requestNode', objectId=obj_id)['nodeId'] + return self.owner._run_cdp('DOM.requestNode', objectId=obj_id)['nodeId'] def _get_obj_id(self, back_id): """返回元素object id""" - return self.owner.run_cdp('DOM.resolveNode', backendNodeId=back_id)['object']['objectId'] + return self.owner._run_cdp('DOM.resolveNode', backendNodeId=back_id)['object']['objectId'] def _get_backend_id(self, node_id): """返回元素object id""" - r = self.owner.run_cdp('DOM.describeNode', nodeId=node_id)['node'] + r = self.owner._run_cdp('DOM.describeNode', nodeId=node_id)['node'] self._tag = r['localName'].lower() return r['backendNodeId'] @@ -1228,7 +1302,7 @@ def find_in_chromium_ele(ele, locator, index=1, timeout=None, relative=True): :param locator: 元素定位元组 :param index: 第几个结果,从1开始,可传入负数获取倒数第几个,为None返回所有 :param timeout: 查找元素超时时间(秒) - :param relative: WebPage用于标记是否相对定位使用 + :param relative: MixTab用于标记是否相对定位使用 :return: 返回ChromiumElement元素或它们组成的列表 """ # ---------------处理定位符--------------- @@ -1269,15 +1343,15 @@ def find_by_xpath(ele, xpath, index, timeout, relative=True): ele.owner.wait.doc_loaded() def do_find(): - res = ele.owner.run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=ele._obj_id, - returnByValue=False, awaitPromise=True, userGesture=True) + res = ele.owner._run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=ele._obj_id, + returnByValue=False, awaitPromise=True, userGesture=True) if res['result']['type'] == 'string': return res['result']['value'] if 'exceptionDetails' in res: if 'The result is not a node set' in res['result']['description']: js1 = make_js_for_find_ele_by_xpath(xpath, '1', node_txt) - res = ele.owner.run_cdp('Runtime.callFunctionOn', functionDeclaration=js1, objectId=ele._obj_id, - returnByValue=False, awaitPromise=True, userGesture=True) + res = ele.owner._run_cdp('Runtime.callFunctionOn', functionDeclaration=js1, objectId=ele._obj_id, + returnByValue=False, awaitPromise=True, userGesture=True) return res['result']['value'] else: raise SyntaxError(f'查询语句错误:\n{res}') @@ -1290,8 +1364,8 @@ def find_by_xpath(ele, xpath, index, timeout, relative=True): return None if r is False else r else: - res = ele.owner.run_cdp('Runtime.getProperties', objectId=res['result']['objectId'], - ownProperties=True)['result'][:-1] + res = ele.owner._run_cdp('Runtime.getProperties', objectId=res['result']['objectId'], + ownProperties=True)['result'][:-1] if index is None: r = ChromiumElementsList(page=ele.owner) for i in res: @@ -1341,8 +1415,8 @@ def find_by_css(ele, selector, index, timeout): ele.owner.wait.doc_loaded() def do_find(): - res = ele.owner.run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=ele._obj_id, - returnByValue=False, awaitPromise=True, userGesture=True) + res = ele.owner._run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=ele._obj_id, + returnByValue=False, awaitPromise=True, userGesture=True) if 'exceptionDetails' in res: raise SyntaxError(f'查询语句错误:\n{res}') @@ -1354,9 +1428,9 @@ def find_by_css(ele, selector, index, timeout): return None if r is False else r else: - obj_ids = [i['value']['objectId'] for i in ele.owner.run_cdp('Runtime.getProperties', - objectId=res['result']['objectId'], - ownProperties=True)['result']] + obj_ids = [i['value']['objectId'] for i in ele.owner._run_cdp('Runtime.getProperties', + objectId=res['result']['objectId'], + ownProperties=True)['result']] r = make_chromium_eles(ele.owner, _ids=obj_ids, index=index, is_obj_id=True) return None if r is False else r @@ -1535,16 +1609,16 @@ def run_js(page_or_ele, script, as_expr, timeout, args=None): end_time = perf_counter() + timeout try: if as_expr: - res = page.run_cdp('Runtime.evaluate', expression=script, returnByValue=False, - awaitPromise=True, userGesture=True, _timeout=timeout, _ignore=AlertExistsError) + res = page._run_cdp('Runtime.evaluate', expression=script, returnByValue=False, + awaitPromise=True, userGesture=True, _timeout=timeout, _ignore=AlertExistsError) else: args = args or () if not is_js_func(script): script = f'function(){{{script}}}' - res = page.run_cdp('Runtime.callFunctionOn', functionDeclaration=script, objectId=obj_id, - arguments=[convert_argument(arg) for arg in args], returnByValue=False, - awaitPromise=True, userGesture=True, _timeout=timeout, _ignore=AlertExistsError) + res = page._run_cdp('Runtime.callFunctionOn', functionDeclaration=script, objectId=obj_id, + arguments=[convert_argument(arg) for arg in args], returnByValue=False, + awaitPromise=True, userGesture=True, _timeout=timeout, _ignore=AlertExistsError) except TimeoutError: raise TimeoutError(f'执行js超时(等待{timeout}秒)。') except ContextLostError: @@ -1591,7 +1665,7 @@ def parse_js_result(page, ele, result, end_time): return r elif sub_type == 'array': - r = page.run_cdp('Runtime.getProperties', objectId=result['objectId'], ownProperties=True)['result'] + r = page._run_cdp('Runtime.getProperties', objectId=result['objectId'], ownProperties=True)['result'] return [parse_js_result(page, ele, result=i['value'], end_time=end_time) for i in r if i['name'].isdigit()] elif 'objectId' in result: @@ -1599,9 +1673,9 @@ def parse_js_result(page, ele, result, end_time): if timeout < 0: return js = 'function(){return JSON.stringify(this);}' - r = page.run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=result['objectId'], - returnByValue=False, awaitPromise=True, userGesture=True, _ignore=AlertExistsError, - _timeout=timeout) + r = page._run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=result['objectId'], + returnByValue=False, awaitPromise=True, userGesture=True, _ignore=AlertExistsError, + _timeout=timeout) return loads(parse_js_result(page, ele, r['result'], end_time)) else: @@ -1610,6 +1684,9 @@ def parse_js_result(page, ele, result, end_time): elif the_type == 'undefined': return None + elif the_type == 'function': + return result['description'] + else: return result['value'] diff --git a/DrissionPage/_elements/chromium_element.pyi b/DrissionPage/_elements/chromium_element.pyi index 1be5553..c04e13d 100644 --- a/DrissionPage/_elements/chromium_element.pyi +++ b/DrissionPage/_elements/chromium_element.pyi @@ -14,8 +14,8 @@ from .._functions.elements import SessionElementsList, ChromiumElementsList from .._pages.chromium_base import ChromiumBase from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_page import ChromiumPage -from .._pages.chromium_tab import ChromiumTab -from .._pages.web_page import WebPage +from .._pages.tabs import ChromiumTab +from .._pages.mix_page import MixPage from .._units.clicker import Clicker from .._units.rect import ElementRect from .._units.scroller import ElementScroller @@ -31,9 +31,8 @@ class ChromiumElement(DrissionElement): def __init__(self, owner: ChromiumBase, node_id: int = None, obj_id: str = None, backend_id: int = None): self._tag: str = ... - # self.page: Union[ChromiumPage, WebPage] = ... self.owner: ChromiumBase = ... - self.page: Union[ChromiumPage, WebPage] = ... + self.page: Union[ChromiumPage, MixPage] = ... self.tab: Union[ChromiumPage, ChromiumTab] = ... self._node_id: int = ... self._obj_id: str = ... @@ -169,7 +168,11 @@ class ChromiumElement(DrissionElement): 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 offset(self, + locator: Optional[str] = None, + x: int = None, + y: int = None, + timeout: float = None) -> ChromiumElement: ... def _get_relative_eles(self, mode: str = 'north', @@ -183,7 +186,7 @@ class ChromiumElement(DrissionElement): def select(self) -> SelectElement: ... @property - def value(self) -> None: ... + def value(self) -> str: ... def check(self, uncheck: bool = False, by_js: bool = False) -> None: ... @@ -195,6 +198,8 @@ class ChromiumElement(DrissionElement): def run_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> Any: ... + def _run_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> Any: ... + def run_async_js(self, script: str, *args, as_expr: bool = False) -> None: ... def ele(self, @@ -208,9 +213,12 @@ class ChromiumElement(DrissionElement): def s_ele(self, locator: Union[Tuple[str, str], str] = None, - index: int = 1) -> SessionElement: ... + index: int = 1, + timeout: float = None) -> SessionElement: ... - def s_eles(self, locator: Union[Tuple[str, str], str] = None) -> SessionElementsList: ... + def s_eles(self, + locator: Union[Tuple[str, str], str] = None, + timeout: float = None) -> SessionElementsList: ... def _find_elements(self, locator: Union[Tuple[str, str], str], @@ -266,7 +274,6 @@ class ChromiumElement(DrissionElement): class ShadowRoot(BaseElement): def __init__(self, parent_ele: ChromiumElement, obj_id: str = None, backend_id: int = None): - # self.page: Union[ChromiumPage, WebPage] = ... self.owner: ChromiumBase = ... self.tab: Union[ChromiumPage, ChromiumTab] = ... self._obj_id: str = ... @@ -298,6 +305,8 @@ class ShadowRoot(BaseElement): def run_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> Any: ... + def _run_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> Any: ... + def run_async_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> None: ... def parent(self, level_or_loc: Union[str, int] = 1, index: int = 1) -> ChromiumElement: ... @@ -337,9 +346,10 @@ class ShadowRoot(BaseElement): def s_ele(self, locator: Union[Tuple[str, str], str] = None, - index: int = 1) -> SessionElement: ... + index: int = 1, + timeout: float = None) -> SessionElement: ... - def s_eles(self, locator: Union[Tuple[str, str], str]) -> SessionElementsList: ... + def s_eles(self, locator: Union[Tuple[str, str], str], timeout: float = None) -> SessionElementsList: ... def _find_elements(self, locator: Union[Tuple[str, str], str], @@ -375,7 +385,7 @@ def find_by_css(ele: ChromiumElement, timeout: float) -> Union[ChromiumElement, List[ChromiumElement],]: ... -def make_chromium_eles(page: Union[ChromiumBase, ChromiumPage, WebPage, ChromiumTab, ChromiumFrame], +def make_chromium_eles(page: Union[ChromiumBase, ChromiumPage, MixPage, ChromiumTab, ChromiumFrame], _ids: Union[tuple, list, str, int], index: Optional[int] = 1, is_obj_id: bool = True, diff --git a/DrissionPage/_elements/session_element.py b/DrissionPage/_elements/session_element.py index 888269a..29257c8 100644 --- a/DrissionPage/_elements/session_element.py +++ b/DrissionPage/_elements/session_element.py @@ -260,7 +260,7 @@ class SessionElement(DrissionElement): :param locator: 元素的定位信息,可以是loc元组,或查询字符串 :param timeout: 不起实际作用,用于和父类对应 :param index: 第几个结果,从1开始,可传入负数获取倒数第几个,为None返回所有 - :param relative: WebPage用的表示是否相对定位的参数 + :param relative: MixTab用的表示是否相对定位的参数 :param raise_err: 找不到元素是是否抛出异常,为None时根据全局设置 :return: SessionElement对象 """ @@ -276,6 +276,10 @@ class SessionElement(DrissionElement): while ele: if mode == 'css': + id_ = ele.attr('id') + if id_: + path_str = f'>{ele.tag}#{id_}{path_str}' + break brothers = len(ele.eles(f'xpath:./preceding-sibling::*')) path_str = f'>{ele.tag}:nth-child({brothers + 1}){path_str}' else: @@ -349,11 +353,11 @@ def make_session_ele(html_or_ele, loc=None, index=1, method=None): xpath = html_or_ele.xpath # ChromiumElement,兼容传入的元素在iframe内的情况 if html_or_ele._doc_id is None: - doc = html_or_ele.run_js('return this.ownerDocument;') + 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'] + 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) diff --git a/DrissionPage/_functions/browser.py b/DrissionPage/_functions/browser.py index a8f79d4..9acae5d 100644 --- a/DrissionPage/_functions/browser.py +++ b/DrissionPage/_functions/browser.py @@ -8,6 +8,7 @@ from json import load, dump, JSONDecodeError from os import environ from pathlib import Path +from shutil import rmtree from subprocess import Popen, DEVNULL from tempfile import gettempdir from time import perf_counter, sleep @@ -28,17 +29,22 @@ def connect_browser(option): browser_path = option.browser_path ip, port = address.split(':') - if ip != '127.0.0.1' or port_is_using(ip, port) or option.is_existing_only: - test_connect(ip, port) - option._headless = False - for i in option.arguments: - if i.startswith('--headless') and not i.endswith('=false'): - option._headless = True - break - return True + using = port_is_using(ip, port) + if ip != '127.0.0.1' or using or option.is_existing_only: + if test_connect(ip, port): + return True + elif ip != '127.0.0.1': + raise BrowserConnectError(f'\n{address}浏览器连接失败。') + elif using: + raise BrowserConnectError(f'\n{address}浏览器连接失败,请检查{port}端口是否浏览器,' + f'且已添加\'--remote-debugging-port={port}\'启动项。') + else: # option.is_existing_only + raise BrowserConnectError(f'\n{address}浏览器连接失败,请确认浏览器已启动。') # ----------创建浏览器进程---------- - args = get_launch_args(option) + args, user_path = get_launch_args(option) + if option._new_env: + rmtree(user_path, ignore_errors=True) set_prefs(option) set_flags(option) try: @@ -47,13 +53,16 @@ def connect_browser(option): # 传入的路径找不到,主动在ini文件、注册表、系统变量中找 except FileNotFoundError: browser_path = get_chrome_path(option.ini_path) - if not browser_path: raise FileNotFoundError('无法找到浏览器可执行文件路径,请手动配置。') - _run_browser(port, browser_path, args) - test_connect(ip, port) + if not test_connect(ip, port): + raise BrowserConnectError(f'\n{address}浏览器连接失败。\n请确认:\n' + f'1、用户文件夹没有和已打开的浏览器冲突\n' + f'2、如为无界面系统,请添加\'--headless=new\'启动参数\n' + f'3、如果是Linux系统,尝试添加\'--no-sandbox\'启动参数\n' + f'可使用ChromiumOptions设置端口和用户文件夹路径。') return False @@ -64,44 +73,26 @@ def get_launch_args(opt): """ # ----------处理arguments----------- result = set() - has_user_path = False - headless = None + user_path = False for i in opt.arguments: if i.startswith(('--load-extension=', '--remote-debugging-port=')): continue elif i.startswith('--user-data-dir') and not opt.system_user_path: - result.add(f'--user-data-dir={Path(i[16:]).absolute()}') - has_user_path = True + user_path = f'--user-data-dir={Path(i[16:]).absolute()}' + result.add(user_path) continue - elif i.startswith('--headless'): - if i == '--headless=false': - headless = False - continue - elif i == '--headless': - i = '--headless=new' - headless = True - else: - headless = True - result.add(i) - if not has_user_path and not opt.system_user_path: + if not user_path and not opt.system_user_path: port = opt.address.split(':')[-1] if opt.address else '0' p = Path(opt.tmp_path) if opt.tmp_path else Path(gettempdir()) / 'DrissionPage' - path = p / f'userData_{port}' + path = p / 'userData' / port path.mkdir(parents=True, exist_ok=True) - opt.set_user_data_path(path) - result.add(f'--user-data-dir={path}') - - # if headless is None and system().lower() == 'linux': # 无界面Linux自动加入无头 - # from os import popen - # r = popen('systemctl list-units | grep graphical.target') - # if 'graphical.target' not in r.read(): - # headless = True - # result.add('--headless=new') + user_path = path.absolute() + opt.set_user_data_path(user_path) + result.add(f'--user-data-dir={user_path}') result = list(result) - opt._headless = headless # ----------处理插件extensions------------- ext = [str(Path(e).absolute()) for e in opt.extensions] @@ -110,7 +101,7 @@ def get_launch_args(opt): ext = f'--load-extension={ext}' result.append(ext) - return result + return result, user_path def set_prefs(opt): @@ -208,18 +199,13 @@ def test_connect(ip, port, timeout=30): if tab['type'] in ('page', 'webview'): r.close() s.close() - return + return True r.close() except Exception: sleep(.2) s.close() - raise BrowserConnectError(f'\n{ip}:{port}浏览器无法链接。\n请确认:\n1、该端口为浏览器\n' - f'2、已添加\'--remote-debugging-port={port}\'启动项\n' - f'3、用户文件夹没有和已打开的浏览器冲突\n' - f'4、如为无界面系统,请添加\'--headless=new\'参数\n' - f'5、如果是Linux系统,可能还要添加\'--no-sandbox\'启动参数\n' - f'可使用ChromiumOptions设置端口和用户文件夹路径。') + return False def _run_browser(port, path: str, args) -> Popen: diff --git a/DrissionPage/_functions/browser.pyi b/DrissionPage/_functions/browser.pyi index 8815ab4..ff7bdc6 100644 --- a/DrissionPage/_functions/browser.pyi +++ b/DrissionPage/_functions/browser.pyi @@ -22,7 +22,7 @@ def set_prefs(opt: ChromiumOptions) -> None: ... def set_flags(opt: ChromiumOptions) -> None: ... -def test_connect(ip: str, port: Union[int, str], timeout: float = 30) -> None: ... +def test_connect(ip: str, port: Union[int, str], timeout: float = 30) -> bool: ... def get_chrome_path(ini_path: str) -> Union[str, None]: ... diff --git a/DrissionPage/_functions/cookies.py b/DrissionPage/_functions/cookies.py new file mode 100644 index 0000000..be9d374 --- /dev/null +++ b/DrissionPage/_functions/cookies.py @@ -0,0 +1,234 @@ +# -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. +@License : BSD 3-Clause. +""" +from datetime import datetime +from http.cookiejar import Cookie, CookieJar + +from tldextract import extract + + +def cookie_to_dict(cookie): + """把Cookie对象转为dict格式 + :param cookie: Cookie对象、字符串或字典 + :return: cookie字典 + """ + if isinstance(cookie, Cookie): + cookie_dict = cookie.__dict__.copy() + cookie_dict.pop('rfc2109', None) + cookie_dict.pop('_rest', None) + return cookie_dict + + elif isinstance(cookie, dict): + cookie_dict = cookie + + elif isinstance(cookie, str): + cookie_dict = {} + for attr in cookie.strip().rstrip(';,').split(',' if ',' in cookie else ';'): + attr_val = attr.strip().split('=', 1) + if attr_val[0] in ('domain', 'path', 'expires', 'max-age', 'HttpOnly', 'secure', 'expiry', 'name', 'value'): + cookie_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else '' + else: + cookie_dict['name'] = attr_val[0] + cookie_dict['value'] = attr_val[1] if len(attr_val) == 2 else '' + + return cookie_dict + + else: + raise TypeError('cookie参数必须为Cookie、str或dict类型。') + + return cookie_dict + + +def cookies_to_tuple(cookies): + """把cookies转为tuple格式 + :param cookies: cookies信息,可为CookieJar, list, tuple, str, dict + :return: 返回tuple形式的cookies + """ + if isinstance(cookies, (list, tuple, CookieJar)): + cookies = tuple(cookie_to_dict(cookie) for cookie in cookies) + + elif isinstance(cookies, str): + c_dict = {} + cookies = cookies.rstrip('; ') + cookies = cookies.split(';') + + for attr in cookies: + attr_val = attr.strip().split('=', 1) + c_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else True + cookies = _dict_cookies_to_tuple(c_dict) + + elif isinstance(cookies, dict): + cookies = _dict_cookies_to_tuple(cookies) + + elif isinstance(cookies, Cookie): + cookies = (cookie_to_dict(cookies),) + + else: + raise TypeError('cookies参数必须为Cookie、CookieJar、list、tuple、str或dict类型。') + + return cookies + + +def set_session_cookies(session, cookies): + """设置Session对象的cookies + :param session: Session对象 + :param cookies: cookies信息 + :return: None + """ + for cookie in cookies_to_tuple(cookies): + if cookie['value'] is None: + cookie['value'] = '' + + kwargs = {x: cookie[x] for x in cookie + if x.lower() in ('version', 'port', 'domain', 'path', 'secure', + 'expires', 'discard', 'comment', 'comment_url', 'rest')} + + if 'expiry' in cookie: + kwargs['expires'] = cookie['expiry'] + + session.cookies.set(cookie['name'], cookie['value'], **kwargs) + + +def set_browser_cookies(browser, cookies): + """设置cookies值 + :param browser: 页面对象 + :param cookies: cookies信息 + :return: None + """ + c = [] + for cookie in cookies_to_tuple(cookies): + if 'domain' not in cookie and 'url' not in cookie: + raise ValueError(f"cookie必须带有'domain'或'url'字段:{cookie}") + c.append(format_cookie(cookie)) + browser._run_cdp('Storage.setCookies', cookies=c) + + +def set_tab_cookies(page, cookies): + """设置cookies值 + :param page: 页面对象 + :param cookies: cookies信息 + :return: None + """ + for cookie in cookies_to_tuple(cookies): + cookie = format_cookie(cookie) + + if cookie['name'].startswith('__Host-'): + if not page.url.startswith('http'): + cookie['name'] = cookie['name'].replace('__Host-', '__Secure-', 1) + else: + cookie['url'] = page.url + page._run_cdp_loaded('Network.setCookie', **cookie) + continue # 不用设置域名,可退出 + + if cookie.get('domain', None): + try: + page._run_cdp_loaded('Network.setCookie', **cookie) + if not is_cookie_in_driver(page, cookie): + page.browser.set.cookies(cookie) + continue + except Exception: + pass + + url = page._browser_url + if not url.startswith('http'): + raise RuntimeError(f'未设置域名,请设置cookie的domain参数或先访问一个网站。{cookie}') + ex_url = extract(url) + d_list = ex_url.subdomain.split('.') + d_list.append(f'{ex_url.domain}.{ex_url.suffix}' if ex_url.suffix else ex_url.domain) + + tmp = [d_list[0]] + if len(d_list) > 1: + for i in d_list[1:]: + tmp.append('.') + tmp.append(i) + + for i in range(len(tmp)): + cookie['domain'] = ''.join(tmp[i:]) + page._run_cdp_loaded('Network.setCookie', **cookie) + if is_cookie_in_driver(page, cookie): + break + + +def is_cookie_in_driver(page, cookie): + """查询cookie是否在浏览器内 + :param page: BasePage对象 + :param cookie: dict格式cookie + :return: bool + """ + if 'domain' in cookie: + for c in page.cookies(all_domains=True): + if cookie['name'] == c['name'] and cookie['value'] == c['value'] and cookie['domain'] == c.get('domain', + None): + return True + else: + for c in page.cookies(all_domains=True): + if cookie['name'] == c['name'] and cookie['value'] == c['value']: + return True + return False + + +def format_cookie(cookie): + """设置cookie为可用格式 + :param cookie: dict格式cookie + :return: 格式化后的cookie字典 + """ + if 'expiry' in cookie: + cookie['expires'] = int(cookie['expiry']) + cookie.pop('expiry') + + if 'expires' in cookie: + if not cookie['expires']: + cookie.pop('expires') + + elif isinstance(cookie['expires'], str): + if cookie['expires'].isdigit(): + cookie['expires'] = int(cookie['expires']) + + elif cookie['expires'].replace('.', '').isdigit(): + cookie['expires'] = float(cookie['expires']) + + else: + try: + cookie['expires'] = datetime.strptime(cookie['expires'], '%a, %d %b %Y %H:%M:%S GMT').timestamp() + except ValueError: + cookie['expires'] = datetime.strptime(cookie['expires'], '%a, %d %b %y %H:%M:%S GMT').timestamp() + + if cookie['value'] is None: + cookie['value'] = '' + elif not isinstance(cookie['value'], str): + cookie['value'] = str(cookie['value']) + + if cookie['name'].startswith('__Host-'): + cookie['path'] = '/' + cookie['secure'] = True + + elif cookie['name'].startswith('__Secure-'): + cookie['secure'] = True + + return cookie + + +class CookiesList(list): + def as_dict(self): + """以dict格式返回,只包含name和value字段""" + return {c['name']: c['value'] for c in self} + + def as_str(self): + """以str格式返回,只包含name和value字段""" + return '; '.join([f'{c["name"]}={c["value"]}' for c in self]) + + +def _dict_cookies_to_tuple(cookies: dict): + """把dict形式的cookies转换为tuple形式 + :param cookies: 单个或多个cookies,单个时包含'name'和'value' + :return: 多个dict格式cookies组成的列表 + """ + if 'name' in cookies and 'value' in cookies: # 单个cookie + return (cookies,) + keys = ('domain', 'path', 'expires', 'max-age', 'HttpOnly', 'secure', 'expiry') + template = {k: v for k, v in cookies.items() if k in keys} + return tuple(dict(**{'name': k, 'value': v}, **template) for k, v in cookies.items() if k not in keys) diff --git a/DrissionPage/_functions/cookies.pyi b/DrissionPage/_functions/cookies.pyi new file mode 100644 index 0000000..cfee879 --- /dev/null +++ b/DrissionPage/_functions/cookies.pyi @@ -0,0 +1,44 @@ +# -*- coding:utf-8 -*- +""" +@Author : g1879 +@Contact : g1879@qq.com +@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. +@License : BSD 3-Clause. +""" +from http.cookiejar import Cookie +from typing import Union + +from requests import Session +from requests.cookies import RequestsCookieJar + +from .._base.browser import Chromium +from .._pages.chromium_base import ChromiumBase + + +def cookie_to_dict(cookie: Union[Cookie, str, dict]) -> dict: ... + + +def cookies_to_tuple(cookies: Union[RequestsCookieJar, list, tuple, str, dict, Cookie]) -> tuple: ... + + +def set_session_cookies(session: Session, cookies: Union[RequestsCookieJar, list, tuple, str, dict]) -> None: ... + + +def set_browser_cookies(browser: Chromium, cookies: Union[RequestsCookieJar, list, tuple, str, dict]) -> None: ... + + +def set_tab_cookies(page: ChromiumBase, cookies: Union[RequestsCookieJar, list, tuple, str, dict]) -> None: ... + + +def is_cookie_in_driver(page: ChromiumBase, cookie: dict) -> bool: ... + + +def format_cookie(cookie: dict) -> dict: ... + + +class CookiesList(list): + def as_dict(self) -> dict: ... + + def as_str(self) -> str: ... + + def __next__(self) -> dict: ... diff --git a/DrissionPage/_functions/elements.py b/DrissionPage/_functions/elements.py index d77d359..cccb8da 100644 --- a/DrissionPage/_functions/elements.py +++ b/DrissionPage/_functions/elements.py @@ -7,6 +7,7 @@ """ from time import perf_counter +from .locator import is_loc from .._elements.none_element import NoneElement @@ -402,6 +403,50 @@ def get_eles(locators, owner, any_one=False, first_ele=True, timeout=10): return res +def get_frame(owner, loc_ind_ele, timeout=None): + """获取页面中一个frame对象 + :param owner: 要在其中查找元素的对象 + :param loc_ind_ele: 定位符、iframe序号、ChromiumFrame对象,序号从1开始,可传入负数获取倒数第几个 + :param timeout: 查找元素超时时间(秒) + :return: ChromiumFrame对象 + """ + if isinstance(loc_ind_ele, str): + if not is_loc(loc_ind_ele): + xpath = f'xpath://*[(name()="iframe" or name()="frame") and ' \ + f'(@name="{loc_ind_ele}" or @id="{loc_ind_ele}")]' + else: + xpath = loc_ind_ele + ele = owner._ele(xpath, timeout=timeout) + if ele and ele._type != 'ChromiumFrame': + raise TypeError('该定位符不是指向frame元素。') + r = ele + + elif isinstance(loc_ind_ele, tuple): + ele = owner._ele(loc_ind_ele, timeout=timeout) + if ele and ele._type != 'ChromiumFrame': + raise TypeError('该定位符不是指向frame元素。') + r = ele + + elif isinstance(loc_ind_ele, int): + if loc_ind_ele == 0: + loc_ind_ele = 1 + elif loc_ind_ele < 0: + loc_ind_ele = f'last()+{loc_ind_ele}+1' + xpath = f'xpath:(//*[name()="frame" or name()="iframe"])[{loc_ind_ele}]' + r = owner._ele(xpath, timeout=timeout) + + elif loc_ind_ele._type == 'ChromiumFrame': + r = loc_ind_ele + + else: + raise TypeError('必须传入定位符、iframe序号、id、name、ChromiumFrame对象其中之一。') + + if isinstance(r, NoneElement): + r.method = 'get_frame()' + r.args = {'loc_ind_ele': loc_ind_ele} + return r + + def _get_attr_all(src_list, aim_list, name, value, method, equal=True): if equal: for i in src_list: diff --git a/DrissionPage/_functions/elements.pyi b/DrissionPage/_functions/elements.pyi index 41562e5..69f08a6 100644 --- a/DrissionPage/_functions/elements.pyi +++ b/DrissionPage/_functions/elements.pyi @@ -10,6 +10,7 @@ from typing import Union, List, Optional, Iterable from .._base.base import BaseParser from .._elements.chromium_element import ChromiumElement from .._elements.session_element import SessionElement +from .._pages.chromium_frame import ChromiumFrame def get_eles(locators: Union[List[str], tuple], @@ -19,6 +20,11 @@ def get_eles(locators: Union[List[str], tuple], timeout: float = 10) -> dict: ... +def get_frame(owner: BaseParser, + loc_ind_ele: Union[str, int, tuple, ChromiumFrame, ChromiumElement], + timeout: float = None) -> ChromiumFrame: ... + + class SessionElementsList(list): _page = ... diff --git a/DrissionPage/_functions/keys.py b/DrissionPage/_functions/keys.py index d55267c..2e9313a 100644 --- a/DrissionPage/_functions/keys.py +++ b/DrissionPage/_functions/keys.py @@ -5,6 +5,8 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ +from platform import system + from ..errors import AlertExistsError @@ -21,18 +23,14 @@ class Keys: CANCEL = '\ue001' # ^break HELP = '\ue002' BACKSPACE = '\ue003' - BACK_SPACE = BACKSPACE TAB = '\ue004' CLEAR = '\ue005' RETURN = '\ue006' ENTER = '\ue007' SHIFT = '\ue008' - LEFT_SHIFT = SHIFT CONTROL = '\ue009' CTRL = '\ue009' - LEFT_CONTROL = CONTROL ALT = '\ue00a' - LEFT_ALT = ALT PAUSE = '\ue00b' ESCAPE = '\ue00c' SPACE = '\ue00d' @@ -41,13 +39,9 @@ class Keys: END = '\ue010' HOME = '\ue011' LEFT = '\ue012' - ARROW_LEFT = LEFT UP = '\ue013' - ARROW_UP = UP RIGHT = '\ue014' - ARROW_RIGHT = RIGHT DOWN = '\ue015' - ARROW_DOWN = DOWN INSERT = '\ue016' DELETE = '\ue017' DEL = '\ue017' @@ -219,15 +213,15 @@ keyDefinitions = { '\ue005': {'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3}, '\ue006': {'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3}, '\ue00b': {'keyCode': 19, 'code': 'Pause', 'key': 'Pause'}, - 'CapsLock': {'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock'}, + # 'CapsLock': {'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock'}, '\ue00c': {'keyCode': 27, 'code': 'Escape', 'key': 'Escape'}, - 'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'}, - 'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'}, + # 'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'}, + # 'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'}, '\ue010': {'keyCode': 35, 'code': 'End', 'key': 'End'}, # 'Numpad1': {'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3}, - 'Select': {'keyCode': 41, 'code': 'Select', 'key': 'Select'}, - 'Open': {'keyCode': 43, 'code': 'Open', 'key': 'Execute'}, - 'PrintScreen': {'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen'}, + # 'Select': {'keyCode': 41, 'code': 'Select', 'key': 'Select'}, + # 'Open': {'keyCode': 43, 'code': 'Open', 'key': 'Execute'}, + # 'PrintScreen': {'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen'}, '\ue016': {'keyCode': 45, 'code': 'Insert', 'key': 'Insert'}, # 'Numpad0': {'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3}, '\ue017': {'keyCode': 46, 'code': 'Delete', 'key': 'Delete'}, @@ -243,35 +237,6 @@ keyDefinitions = { '\ue021': {'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7'}, '\ue022': {'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8'}, '\ue023': {'keyCode': 57, 'code': 'Digit9', 'shiftKey': '\(', 'key': '9'}, - 'KeyA': {'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a'}, - 'KeyB': {'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b'}, - 'KeyC': {'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c'}, - 'KeyD': {'keyCode': 68, 'code': 'KeyD', 'shiftKey': 'D', 'key': 'd'}, - 'KeyE': {'keyCode': 69, 'code': 'KeyE', 'shiftKey': 'E', 'key': 'e'}, - 'KeyF': {'keyCode': 70, 'code': 'KeyF', 'shiftKey': 'F', 'key': 'f'}, - 'KeyG': {'keyCode': 71, 'code': 'KeyG', 'shiftKey': 'G', 'key': 'g'}, - 'KeyH': {'keyCode': 72, 'code': 'KeyH', 'shiftKey': 'H', 'key': 'h'}, - 'KeyI': {'keyCode': 73, 'code': 'KeyI', 'shiftKey': 'I', 'key': 'i'}, - 'KeyJ': {'keyCode': 74, 'code': 'KeyJ', 'shiftKey': 'J', 'key': 'j'}, - 'KeyK': {'keyCode': 75, 'code': 'KeyK', 'shiftKey': 'K', 'key': 'k'}, - 'KeyL': {'keyCode': 76, 'code': 'KeyL', 'shiftKey': 'L', 'key': 'l'}, - 'KeyM': {'keyCode': 77, 'code': 'KeyM', 'shiftKey': 'M', 'key': 'm'}, - 'KeyN': {'keyCode': 78, 'code': 'KeyN', 'shiftKey': 'N', 'key': 'n'}, - 'KeyO': {'keyCode': 79, 'code': 'KeyO', 'shiftKey': 'O', 'key': 'o'}, - 'KeyP': {'keyCode': 80, 'code': 'KeyP', 'shiftKey': 'P', 'key': 'p'}, - 'KeyQ': {'keyCode': 81, 'code': 'KeyQ', 'shiftKey': 'Q', 'key': 'q'}, - 'KeyR': {'keyCode': 82, 'code': 'KeyR', 'shiftKey': 'R', 'key': 'r'}, - 'KeyS': {'keyCode': 83, 'code': 'KeyS', 'shiftKey': 'S', 'key': 's'}, - 'KeyT': {'keyCode': 84, 'code': 'KeyT', 'shiftKey': 'T', 'key': 't'}, - 'KeyU': {'keyCode': 85, 'code': 'KeyU', 'shiftKey': 'U', 'key': 'u'}, - 'KeyV': {'keyCode': 86, 'code': 'KeyV', 'shiftKey': 'V', 'key': 'v'}, - 'KeyW': {'keyCode': 87, 'code': 'KeyW', 'shiftKey': 'W', 'key': 'w'}, - 'KeyX': {'keyCode': 88, 'code': 'KeyX', 'shiftKey': 'X', 'key': 'x'}, - 'KeyY': {'keyCode': 89, 'code': 'KeyY', 'shiftKey': 'Y', 'key': 'y'}, - 'KeyZ': {'keyCode': 90, 'code': 'KeyZ', 'shiftKey': 'Z', 'key': 'z'}, - 'MetaLeft': {'keyCode': 91, 'code': 'MetaLeft', 'key': 'Meta'}, - 'MetaRight': {'keyCode': 92, 'code': 'MetaRight', 'key': 'Meta'}, - 'ContextMenu': {'keyCode': 93, 'code': 'ContextMenu', 'key': 'ContextMenu'}, '\ue024': {'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3}, '\ue025': {'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3}, '\ue027': {'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3}, @@ -288,64 +253,91 @@ keyDefinitions = { '\ue03a': {'keyCode': 121, 'code': 'F10', 'key': 'F10'}, '\ue03b': {'keyCode': 122, 'code': 'F11', 'key': 'F11'}, '\ue03c': {'keyCode': 123, 'code': 'F12', 'key': 'F12'}, - 'F13': {'keyCode': 124, 'code': 'F13', 'key': 'F13'}, - 'F14': {'keyCode': 125, 'code': 'F14', 'key': 'F14'}, - 'F15': {'keyCode': 126, 'code': 'F15', 'key': 'F15'}, - 'F16': {'keyCode': 127, 'code': 'F16', 'key': 'F16'}, - 'F17': {'keyCode': 128, 'code': 'F17', 'key': 'F17'}, - 'F18': {'keyCode': 129, 'code': 'F18', 'key': 'F18'}, - 'F19': {'keyCode': 130, 'code': 'F19', 'key': 'F19'}, - 'F20': {'keyCode': 131, 'code': 'F20', 'key': 'F20'}, - 'F21': {'keyCode': 132, 'code': 'F21', 'key': 'F21'}, - 'F22': {'keyCode': 133, 'code': 'F22', 'key': 'F22'}, - 'F23': {'keyCode': 134, 'code': 'F23', 'key': 'F23'}, - 'F24': {'keyCode': 135, 'code': 'F24', 'key': 'F24'}, - 'NumLock': {'keyCode': 144, 'code': 'NumLock', 'key': 'NumLock'}, - 'ScrollLock': {'keyCode': 145, 'code': 'ScrollLock', 'key': 'ScrollLock'}, - 'AudioVolumeMute': {'keyCode': 173, 'code': 'AudioVolumeMute', 'key': 'AudioVolumeMute'}, - 'AudioVolumeDown': {'keyCode': 174, 'code': 'AudioVolumeDown', 'key': 'AudioVolumeDown'}, - 'AudioVolumeUp': {'keyCode': 175, 'code': 'AudioVolumeUp', 'key': 'AudioVolumeUp'}, - 'MediaTrackNext': {'keyCode': 176, 'code': 'MediaTrackNext', 'key': 'MediaTrackNext'}, - 'MediaTrackPrevious': {'keyCode': 177, 'code': 'MediaTrackPrevious', 'key': 'MediaTrackPrevious'}, - 'MediaStop': {'keyCode': 178, 'code': 'MediaStop', 'key': 'MediaStop'}, - 'MediaPlayPause': {'keyCode': 179, 'code': 'MediaPlayPause', 'key': 'MediaPlayPause'}, '\ue018': {'keyCode': 186, 'code': 'Semicolon', 'shiftKey': ':', 'key': ';'}, - 'Equal': {'keyCode': 187, 'code': 'Equal', 'shiftKey': '+', 'key': '='}, '\ue019': {'keyCode': 187, 'code': 'NumpadEqual', 'key': '=', 'location': 3}, - 'Comma': {'keyCode': 188, 'code': 'Comma', 'shiftKey': '<', 'key': ','}, - 'Minus': {'keyCode': 189, 'code': 'Minus', 'shiftKey': '_', 'key': '-'}, - 'Period': {'keyCode': 190, 'code': 'Period', 'shiftKey': '>', 'key': '.'}, - 'Slash': {'keyCode': 191, 'code': 'Slash', 'shiftKey': '?', 'key': '/'}, - 'Backquote': {'keyCode': 192, 'code': 'Backquote', 'shiftKey': '~', 'key': '`'}, - 'BracketLeft': {'keyCode': 219, 'code': 'BracketLeft', 'shiftKey': '{', 'key': '['}, - 'Backslash': {'keyCode': 220, 'code': 'Backslash', 'shiftKey': '|', 'key': '\\'}, - 'BracketRight': {'keyCode': 221, 'code': 'BracketRight', 'shiftKey': '}', 'key': ']'}, - 'Quote': {'keyCode': 222, 'code': 'Quote', 'shiftKey': '"', 'key': '\''}, - 'AltGraph': {'keyCode': 225, 'code': 'AltGraph', 'key': 'AltGraph'}, - 'Props': {'keyCode': 247, 'code': 'Props', 'key': 'CrSel'}, - 'Cancel': {'keyCode': 3, 'key': 'Cancel', 'code': 'Abort'}, - 'Clear': {'keyCode': 12, 'key': 'Clear', 'code': 'Numpad5', 'location': 3}, - 'Shift': {'keyCode': 16, 'key': 'Shift', 'code': 'ShiftLeft'}, - 'Control': {'keyCode': 17, 'key': 'Control', 'code': 'ControlLeft'}, - 'Alt': {'keyCode': 18, 'key': 'Alt', 'code': 'AltLeft'}, - 'Accept': {'keyCode': 30, 'key': 'Accept'}, - 'ModeChange': {'keyCode': 31, 'key': 'ModeChange'}, - 'Print': {'keyCode': 42, 'key': 'Print'}, - 'Execute': {'keyCode': 43, 'key': 'Execute', 'code': 'Open'}, '\u0000': {'keyCode': 46, 'key': '\u0000', 'code': 'NumpadDecimal', 'location': 3}, - 'Attn': {'keyCode': 246, 'key': 'Attn'}, - 'CrSel': {'keyCode': 247, 'key': 'CrSel', 'code': 'Props'}, - 'ExSel': {'keyCode': 248, 'key': 'ExSel'}, - 'EraseEof': {'keyCode': 249, 'key': 'EraseEof'}, - 'Play': {'keyCode': 250, 'key': 'Play'}, - 'ZoomOut': {'keyCode': 251, 'key': 'ZoomOut'}, - 'Power': {'key': 'Power', 'code': 'Power'}, - 'Eject': {'key': 'Eject', 'code': 'Eject'}, + # 'KeyA': {'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a'}, + # 'KeyB': {'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b'}, + # 'KeyC': {'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c'}, + # 'KeyD': {'keyCode': 68, 'code': 'KeyD', 'shiftKey': 'D', 'key': 'd'}, + # 'KeyE': {'keyCode': 69, 'code': 'KeyE', 'shiftKey': 'E', 'key': 'e'}, + # 'KeyF': {'keyCode': 70, 'code': 'KeyF', 'shiftKey': 'F', 'key': 'f'}, + # 'KeyG': {'keyCode': 71, 'code': 'KeyG', 'shiftKey': 'G', 'key': 'g'}, + # 'KeyH': {'keyCode': 72, 'code': 'KeyH', 'shiftKey': 'H', 'key': 'h'}, + # 'KeyI': {'keyCode': 73, 'code': 'KeyI', 'shiftKey': 'I', 'key': 'i'}, + # 'KeyJ': {'keyCode': 74, 'code': 'KeyJ', 'shiftKey': 'J', 'key': 'j'}, + # 'KeyK': {'keyCode': 75, 'code': 'KeyK', 'shiftKey': 'K', 'key': 'k'}, + # 'KeyL': {'keyCode': 76, 'code': 'KeyL', 'shiftKey': 'L', 'key': 'l'}, + # 'KeyM': {'keyCode': 77, 'code': 'KeyM', 'shiftKey': 'M', 'key': 'm'}, + # 'KeyN': {'keyCode': 78, 'code': 'KeyN', 'shiftKey': 'N', 'key': 'n'}, + # 'KeyO': {'keyCode': 79, 'code': 'KeyO', 'shiftKey': 'O', 'key': 'o'}, + # 'KeyP': {'keyCode': 80, 'code': 'KeyP', 'shiftKey': 'P', 'key': 'p'}, + # 'KeyQ': {'keyCode': 81, 'code': 'KeyQ', 'shiftKey': 'Q', 'key': 'q'}, + # 'KeyR': {'keyCode': 82, 'code': 'KeyR', 'shiftKey': 'R', 'key': 'r'}, + # 'KeyS': {'keyCode': 83, 'code': 'KeyS', 'shiftKey': 'S', 'key': 's'}, + # 'KeyT': {'keyCode': 84, 'code': 'KeyT', 'shiftKey': 'T', 'key': 't'}, + # 'KeyU': {'keyCode': 85, 'code': 'KeyU', 'shiftKey': 'U', 'key': 'u'}, + # 'KeyV': {'keyCode': 86, 'code': 'KeyV', 'shiftKey': 'V', 'key': 'v'}, + # 'KeyW': {'keyCode': 87, 'code': 'KeyW', 'shiftKey': 'W', 'key': 'w'}, + # 'KeyX': {'keyCode': 88, 'code': 'KeyX', 'shiftKey': 'X', 'key': 'x'}, + # 'KeyY': {'keyCode': 89, 'code': 'KeyY', 'shiftKey': 'Y', 'key': 'y'}, + # 'KeyZ': {'keyCode': 90, 'code': 'KeyZ', 'shiftKey': 'Z', 'key': 'z'}, + # 'MetaLeft': {'keyCode': 91, 'code': 'MetaLeft', 'key': 'Meta'}, + # 'MetaRight': {'keyCode': 92, 'code': 'MetaRight', 'key': 'Meta'}, + # 'ContextMenu': {'keyCode': 93, 'code': 'ContextMenu', 'key': 'ContextMenu'}, + # 'F13': {'keyCode': 124, 'code': 'F13', 'key': 'F13'}, + # 'F14': {'keyCode': 125, 'code': 'F14', 'key': 'F14'}, + # 'F15': {'keyCode': 126, 'code': 'F15', 'key': 'F15'}, + # 'F16': {'keyCode': 127, 'code': 'F16', 'key': 'F16'}, + # 'F17': {'keyCode': 128, 'code': 'F17', 'key': 'F17'}, + # 'F18': {'keyCode': 129, 'code': 'F18', 'key': 'F18'}, + # 'F19': {'keyCode': 130, 'code': 'F19', 'key': 'F19'}, + # 'F20': {'keyCode': 131, 'code': 'F20', 'key': 'F20'}, + # 'F21': {'keyCode': 132, 'code': 'F21', 'key': 'F21'}, + # 'F22': {'keyCode': 133, 'code': 'F22', 'key': 'F22'}, + # 'F23': {'keyCode': 134, 'code': 'F23', 'key': 'F23'}, + # 'F24': {'keyCode': 135, 'code': 'F24', 'key': 'F24'}, + # 'NumLock': {'keyCode': 144, 'code': 'NumLock', 'key': 'NumLock'}, + # 'ScrollLock': {'keyCode': 145, 'code': 'ScrollLock', 'key': 'ScrollLock'}, + # 'AudioVolumeMute': {'keyCode': 173, 'code': 'AudioVolumeMute', 'key': 'AudioVolumeMute'}, + # 'AudioVolumeDown': {'keyCode': 174, 'code': 'AudioVolumeDown', 'key': 'AudioVolumeDown'}, + # 'AudioVolumeUp': {'keyCode': 175, 'code': 'AudioVolumeUp', 'key': 'AudioVolumeUp'}, + # 'MediaTrackNext': {'keyCode': 176, 'code': 'MediaTrackNext', 'key': 'MediaTrackNext'}, + # 'MediaTrackPrevious': {'keyCode': 177, 'code': 'MediaTrackPrevious', 'key': 'MediaTrackPrevious'}, + # 'MediaStop': {'keyCode': 178, 'code': 'MediaStop', 'key': 'MediaStop'}, + # 'MediaPlayPause': {'keyCode': 179, 'code': 'MediaPlayPause', 'key': 'MediaPlayPause'}, + # 'Equal': {'keyCode': 187, 'code': 'Equal', 'shiftKey': '+', 'key': '='}, + # 'Comma': {'keyCode': 188, 'code': 'Comma', 'shiftKey': '<', 'key': ','}, + # 'Minus': {'keyCode': 189, 'code': 'Minus', 'shiftKey': '_', 'key': '-'}, + # 'Period': {'keyCode': 190, 'code': 'Period', 'shiftKey': '>', 'key': '.'}, + # 'Slash': {'keyCode': 191, 'code': 'Slash', 'shiftKey': '?', 'key': '/'}, + # 'Backquote': {'keyCode': 192, 'code': 'Backquote', 'shiftKey': '~', 'key': '`'}, + # 'BracketLeft': {'keyCode': 219, 'code': 'BracketLeft', 'shiftKey': '{', 'key': '['}, + # 'Backslash': {'keyCode': 220, 'code': 'Backslash', 'shiftKey': '|', 'key': '\\'}, + # 'BracketRight': {'keyCode': 221, 'code': 'BracketRight', 'shiftKey': '}', 'key': ']'}, + # 'Quote': {'keyCode': 222, 'code': 'Quote', 'shiftKey': '"', 'key': '\''}, + # 'AltGraph': {'keyCode': 225, 'code': 'AltGraph', 'key': 'AltGraph'}, + # 'Props': {'keyCode': 247, 'code': 'Props', 'key': 'CrSel'}, + # 'Cancel': {'keyCode': 3, 'key': 'Cancel', 'code': 'Abort'}, + # 'Clear': {'keyCode': 12, 'key': 'Clear', 'code': 'Numpad5', 'location': 3}, + # 'Shift': {'keyCode': 16, 'key': 'Shift', 'code': 'ShiftLeft'}, + # 'Control': {'keyCode': 17, 'key': 'Control', 'code': 'ControlLeft'}, + # 'Alt': {'keyCode': 18, 'key': 'Alt', 'code': 'AltLeft'}, + # 'Accept': {'keyCode': 30, 'key': 'Accept'}, + # 'ModeChange': {'keyCode': 31, 'key': 'ModeChange'}, + # 'Print': {'keyCode': 42, 'key': 'Print'}, + # 'Execute': {'keyCode': 43, 'key': 'Execute', 'code': 'Open'}, + # 'Attn': {'keyCode': 246, 'key': 'Attn'}, + # 'CrSel': {'keyCode': 247, 'key': 'CrSel', 'code': 'Props'}, + # 'ExSel': {'keyCode': 248, 'key': 'ExSel'}, + # 'EraseEof': {'keyCode': 249, 'key': 'EraseEof'}, + # 'Play': {'keyCode': 250, 'key': 'Play'}, + # 'ZoomOut': {'keyCode': 251, 'key': 'ZoomOut'}, + # 'Power': {'key': 'Power', 'code': 'Power'}, + # 'Eject': {'key': 'Eject', 'code': 'Eject'}, } -modifierBit = {'\ue00a': 1, - '\ue009': 2, - '\ue03d': 4, - '\ue008': 8} +modifierBit = {'\ue00a': 1, '\ue009': 2, '\ue03d': 4, '\ue008': 8} +sys = system().lower() def keys_to_typing(value): @@ -368,71 +360,69 @@ def keys_to_typing(value): return modifier, ''.join(typing) -def keyDescriptionForString(_modifiers, keyString): # noqa: C901 - shift = _modifiers & 8 - description = {'key': '', - 'keyCode': 0, - 'code': '', - 'text': '', - 'location': 0} +def make_input_data(modifiers, key, key_up=False): + """ + :param modifiers: 功能键设置 + :param key: 按键字符 + :param key_up: 是否提起 + :return: None + """ + data = keyDefinitions.get(key) + if not data: + return None - definition = keyDefinitions.get(keyString) # type: ignore - if not definition: - raise ValueError(f'未知按键:{keyString}') + result = {'modifiers': modifiers, 'autoRepeat': False, '_ignore': AlertExistsError} + shift = modifiers & 8 - if 'key' in definition: - description['key'] = definition['key'] - if shift and definition.get('shiftKey'): - description['key'] = definition['shiftKey'] + if shift and data.get('shiftKey'): + result['key'] = data['shiftKey'] + result['text'] = data['shiftKey'] + elif 'key' in data: + result['key'] = data['key'] - if 'keyCode' in definition: - description['keyCode'] = definition['keyCode'] - if shift and definition.get('shiftKeyCode'): - description['keyCode'] = definition['shiftKeyCode'] + if len(result.get('key', '')) == 1: # type: ignore + result['text'] = data['key'] - if 'code' in definition: - description['code'] = definition['code'] + sys_text = 'windowsVirtualKeyCode' if sys == 'windows' else 'nativeVirtualKeyCode' + if shift and data.get('shiftKeyCode'): + result[sys_text] = data['shiftKeyCode'] + elif 'keyCode' in data: + result[sys_text] = data['keyCode'] - if 'location' in definition: - description['location'] = definition['location'] + if 'code' in data: + result['code'] = data['code'] - if len(description['key']) == 1: # type: ignore - description['text'] = description['key'] + if 'location' in data: + result['location'] = data['location'] + result['isKeypad'] = data['location'] == 3 + else: + result['location'] = 0 + result['isKeypad'] = False - if 'text' in definition: - description['text'] = definition['text'] - if shift and definition.get('shiftText'): - description['text'] = definition['shiftText'] + if shift and data.get('shiftText'): + result['text'] = data['shiftText'] + result['unmodifiedText'] = data['shiftText'] + elif 'text' in data: + result['text'] = data['text'] + result['unmodifiedText'] = data['text'] - if _modifiers & ~8: - description['text'] = '' + if modifiers & ~8: + result['text'] = '' - return description + result['type'] = 'keyUp' if key_up else ('keyDown' if result.get('text') else 'rawKeyDown') + return result def send_key(page, modifier, key): """发送一个字,在键盘中的字符触发按键,其它直接发送文本""" - if key in keyDefinitions: - description = keyDescriptionForString(modifier, key) - text = description['text'] - data = {'type': 'keyDown' if text else 'rawKeyDown', - 'modifiers': modifier, - 'windowsVirtualKeyCode': description['keyCode'], - 'code': description['code'], - 'key': description['key'], - 'text': text, - 'autoRepeat': False, - 'unmodifiedText': text, - 'location': description['location'], - 'isKeypad': description['location'] == 3, - '_ignore': AlertExistsError} - - page.run_cdp('Input.dispatchKeyEvent', **data) + data = make_input_data(modifier, key) + if data: + page._run_cdp('Input.dispatchKeyEvent', **data) data['type'] = 'keyUp' - page.run_cdp('Input.dispatchKeyEvent', **data) + page._run_cdp('Input.dispatchKeyEvent', **data) else: - page.run_cdp('Input.insertText', text=key, _ignore=AlertExistsError) + page._run_cdp('Input.insertText', text=key, _ignore=AlertExistsError) def input_text_or_keys(page, text_or_keys): @@ -451,7 +441,7 @@ def input_text_or_keys(page, text_or_keys): return if text_or_keys.endswith(('\n', '\ue007')): - page.run_cdp('Input.insertText', text=text_or_keys[:-1], _ignore=AlertExistsError) + page._run_cdp('Input.insertText', text=text_or_keys[:-1], _ignore=AlertExistsError) send_key(page, modifier, '\n') else: - page.run_cdp('Input.insertText', text=text_or_keys, _ignore=AlertExistsError) + page._run_cdp('Input.insertText', text=text_or_keys, _ignore=AlertExistsError) diff --git a/DrissionPage/_functions/keys.pyi b/DrissionPage/_functions/keys.pyi index a06c8be..c9254d1 100644 --- a/DrissionPage/_functions/keys.pyi +++ b/DrissionPage/_functions/keys.pyi @@ -5,7 +5,7 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ -from typing import Tuple, Dict, Union, Any +from typing import Tuple, Union, Any from .._pages.chromium_base import ChromiumBase @@ -23,18 +23,14 @@ class Keys: CANCEL: str HELP: str BACKSPACE: str - BACK_SPACE: str TAB: str CLEAR: str RETURN: str ENTER: str SHIFT: str - LEFT_SHIFT: str CONTROL: str CTRL: str - LEFT_CONTROL: str ALT: str - LEFT_ALT: str PAUSE: str ESCAPE: str SPACE: str @@ -43,13 +39,9 @@ class Keys: END: str HOME: str LEFT: str - ARROW_LEFT: str UP: str - ARROW_UP: str RIGHT: str - ARROW_RIGHT: str DOWN: str - ARROW_DOWN: str INSERT: str DELETE: str DEL: str @@ -96,7 +88,7 @@ modifierBit: dict = ... def keys_to_typing(value: Union[str, int, list, tuple]) -> Tuple[int, str]: ... -def keyDescriptionForString(_modifiers: int, keyString: str) -> Dict: ... +def make_input_data(modifiers: int, key: str, key_up: bool = False) -> dict: ... def send_key(page: ChromiumBase, modifier: int, key: str) -> None: ... diff --git a/DrissionPage/_functions/tools.py b/DrissionPage/_functions/tools.py index 2486718..29ffb9e 100644 --- a/DrissionPage/_functions/tools.py +++ b/DrissionPage/_functions/tools.py @@ -8,7 +8,7 @@ from pathlib import Path from platform import system from shutil import rmtree -from tempfile import gettempdir, TemporaryDirectory +from tempfile import gettempdir from threading import Lock from time import perf_counter, sleep @@ -18,44 +18,52 @@ from ..errors import (ContextLostError, ElementLostError, CDPError, PageDisconne class PortFinder(object): - used_port = {} + used_port = set() + prev_time = 0 lock = Lock() + checked_paths = set() def __init__(self, path=None): """ :param path: 临时文件保存路径,为None时使用系统临时文件夹 """ tmp = Path(path) if path else Path(gettempdir()) / 'DrissionPage' - self.tmp_dir = tmp / 'UserTempFolder' + self.tmp_dir = tmp / 'autoPortData' self.tmp_dir.mkdir(parents=True, exist_ok=True) - if not PortFinder.used_port: - clean_folder(self.tmp_dir) + if str(self.tmp_dir.absolute()) not in PortFinder.checked_paths: + for i in self.tmp_dir.iterdir(): + if i.is_dir() and not port_is_using('127.0.0.1', i.name): + rmtree(i, ignore_errors=True) + PortFinder.checked_paths.add(str(self.tmp_dir.absolute())) def get_port(self, scope=None): """查找一个可用端口 - :param scope: 指定端口范围,不含最后的数字,为None则使用[9600-19600) + :param scope: 指定端口范围,不含最后的数字,为None则使用[9600-59600) :return: 可以使用的端口和用户文件夹路径组成的元组 """ + from random import randint with PortFinder.lock: + if PortFinder.prev_time and perf_counter() - PortFinder.prev_time > 60: + PortFinder.used_port.clear() if scope in (True, None): - scope = (9600, 19600) - for i in range(scope[0], scope[1]): - if i in PortFinder.used_port: + scope = (9600, 59600) + max_times = scope[1] - scope[0] + times = 0 + while times < max_times: + times += 1 + port = randint(*scope) + if port in PortFinder.used_port or port_is_using('127.0.0.1', port): continue - elif port_is_using('127.0.0.1', i): - PortFinder.used_port[i] = None - continue - path = TemporaryDirectory(dir=self.tmp_dir).name - PortFinder.used_port[i] = path - return i, path - - for i in range(scope[0], scope[1]): - if port_is_using('127.0.0.1', i): - continue - rmtree(PortFinder.used_port[i], ignore_errors=True) - return i, TemporaryDirectory(dir=self.tmp_dir).name - - raise OSError('未找到可用端口。') + path = self.tmp_dir / str(port) + if path.exists(): + try: + rmtree(path) + except: + continue + PortFinder.used_port.add(port) + PortFinder.prev_time = perf_counter() + return port, str(path) + raise OSError('未找到可用端口。') def port_is_using(ip, port): @@ -95,7 +103,7 @@ def show_or_hide_browser(page, hide=True): :param hide: 是否隐藏 :return: None """ - if not page.address.startswith(('127.0.0.1', 'localhost')): + if not page.browser.address.startswith(('127.0.0.1', 'localhost')): return if system().lower() != 'windows': @@ -191,10 +199,11 @@ def configs_to_here(save_name=None): om.save(save_name) -def raise_error(result, ignore=None): +def raise_error(result, ignore=None, user=False): """抛出error对应报错 :param result: 包含error的dict :param ignore: 要忽略的错误 + :param user: 是否用户调用的 :return: None """ error = result['error'] @@ -220,13 +229,18 @@ def raise_error(result, ignore=None): elif error == 'Given expression does not evaluate to a function': r = JavaScriptError(f'传入的js无法解析成函数:\n{result["args"]["functionDeclaration"]}') elif error.endswith("' wasn't found"): - r = RuntimeError(f'你的浏览器可能太旧。\n方法:{result["method"]}\n参数:{result["args"]}') - elif result['type'] in ('call_method_error', 'timeout'): + r = RuntimeError(f'没有找到对应功能,方法错误或你的浏览器太旧。\n方法:{result["method"]}\n参数:{result["args"]}') + elif result['type'] == 'timeout': + from DrissionPage import __version__ + txt = f'\n错误:{result["error"]}\n方法:{result["method"]}\n参数:{result["args"]}\n' \ + f'版本:{__version__}\n超时,可能是浏览器卡了。' + r = TimeoutError(txt) + elif result['type'] == 'call_method_error' and not user: from DrissionPage import __version__ txt = f'\n错误:{result["error"]}\n方法:{result["method"]}\n参数:{result["args"]}\n' \ f'版本:{__version__}\n出现这个错误可能意味着程序有bug,请把错误信息和重现方法' \ '告知作者,谢谢。\n报告网站:https://gitee.com/g1879/DrissionPage/issues' - r = TimeoutError(txt) if result['type'] == 'timeout' else CDPError(txt) + r = CDPError(txt) else: r = RuntimeError(result) diff --git a/DrissionPage/_functions/tools.pyi b/DrissionPage/_functions/tools.pyi index a6fc535..5e344f3 100644 --- a/DrissionPage/_functions/tools.pyi +++ b/DrissionPage/_functions/tools.pyi @@ -14,9 +14,11 @@ from .._pages.chromium_base import ChromiumBase class PortFinder(object): - used_port: dict = ... + used_port: set = ... + prev_time: float = ... lock: Lock = ... tmp_dir: Path = ... + checked_paths: set = ... def __init__(self, path: Union[str, Path] = None): ... @@ -45,4 +47,4 @@ def wait_until(function: callable, kwargs: dict = None, timeout: float = 10): .. def configs_to_here(file_name: Union[Path, str] = None) -> None: ... -def raise_error(result: dict, ignore=None) -> None: ... +def raise_error(result: dict, ignore=None, user: bool = False) -> None: ... diff --git a/DrissionPage/_functions/web.py b/DrissionPage/_functions/web.py index 461f143..f86605e 100644 --- a/DrissionPage/_functions/web.py +++ b/DrissionPage/_functions/web.py @@ -5,16 +5,14 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ -from datetime import datetime from html import unescape -from http.cookiejar import Cookie, CookieJar from os.path import sep from pathlib import Path -from re import sub, match +from re import sub from urllib.parse import urlparse, urljoin, urlunparse from DataRecorder.tools import make_valid_name -from tldextract import extract +from requests.structures import CaseInsensitiveDict def get_ele_txt(e): @@ -59,7 +57,7 @@ def get_ele_txt(e): if sub('[ \n\t\r]', '', el) != '': # 字符除了回车和空格还有其它内容 txt = el if not pre: - txt = txt.replace('\r\n', ' ').replace('\n', ' ').strip(' ') + txt = txt.replace('\r\n', ' ').replace('\n', ' ') txt = sub(r' {2,}', ' ', txt) str_list.append(txt) @@ -80,8 +78,31 @@ def get_ele_txt(e): re_str = get_node_txt(e) if re_str and re_str[-1] == '\n': re_str.pop() - re_str = ''.join([i if i is not True else '\n' for i in re_str]) - return format_html(re_str) + + l = len(re_str) + if l > 1: + r = [] + for i in range(l - 1): + i1 = re_str[i] + i2 = re_str[i + 1] + if i1 is True: + r.append('\n') + continue + if i2 is True: + r.append(i1) + continue + if i1.endswith(' ') and i2.startswith(' '): + i1 = i1[:-1] + r.append(i1) + r.append('\n' if re_str[-1] is True else re_str[-1]) + re_str = ''.join(r) + + elif not l: + re_str = '' + else: + re_str = re_str[0] if re_str[0] is not True else '\n' + + return format_html(re_str.strip()) def format_html(text): @@ -106,30 +127,30 @@ def location_in_viewport(page, loc_x, loc_y): const vHeight = document.documentElement.clientHeight; if (x< scrollLeft || y < scrollTop || x > vWidth + scrollLeft || y > vHeight + scrollTop){{return false;}} return true;}}''' - return page.run_js(js) + return page._run_js(js) def offset_scroll(ele, offset_x, offset_y): - """接收元素及偏移坐标,把坐标滚动到页面中间,返回该点在视口中的坐标 + """接收元素及偏移坐标,把坐标滚动到页面中间,返回该点坐标 有偏移量时以元素左上角坐标为基准,没有时以click_point为基准 :param ele: 元素对象 :param offset_x: 偏移量x :param offset_y: 偏移量y - :return: 视口中的坐标 + :return: 绝对坐标和相对坐标 """ loc_x, loc_y = ele.rect.location cp_x, cp_y = ele.rect.click_point lx = loc_x + offset_x if offset_x else cp_x ly = loc_y + offset_y if offset_y else cp_y if not location_in_viewport(ele.owner, lx, ly): - clientWidth = ele.owner.run_js('return document.body.clientWidth;') - clientHeight = ele.owner.run_js('return document.body.clientHeight;') + clientWidth = ele.owner._run_js('return document.body.clientWidth;') + clientHeight = ele.owner._run_js('return document.body.clientHeight;') ele.owner.scroll.to_location(lx - clientWidth // 2, ly - clientHeight // 2) cl_x, cl_y = ele.rect.viewport_location ccp_x, ccp_y = ele.rect.viewport_click_point cx = cl_x + offset_x if offset_x else ccp_x cy = cl_y + offset_y if offset_y else ccp_y - return cx, cy + return lx, ly, cx, cy def make_absolute_link(link, baseURI=None): @@ -144,8 +165,11 @@ def make_absolute_link(link, baseURI=None): link = link.strip().replace('\\', '/') parsed = urlparse(link)._asdict() if baseURI: - p = urlparse(baseURI)._asdict() - baseURI = f'{p["scheme"]}://{p["netloc"]}' + if link.startswith('./'): + baseURI = baseURI[:baseURI.rfind('/') + 1] + else: + p = urlparse(baseURI)._asdict() + baseURI = f'{p["scheme"]}://{p["netloc"]}' # 是相对路径,与页面url拼接并返回 if not parsed['netloc']: @@ -171,188 +195,6 @@ def is_js_func(func): return False -def cookie_to_dict(cookie): - """把Cookie对象转为dict格式 - :param cookie: Cookie对象、字符串或字典 - :return: cookie字典 - """ - if isinstance(cookie, Cookie): - cookie_dict = cookie.__dict__.copy() - cookie_dict.pop('rfc2109', None) - cookie_dict.pop('_rest', None) - return cookie_dict - - elif isinstance(cookie, dict): - cookie_dict = cookie - - elif isinstance(cookie, str): - cookie_dict = {} - for attr in cookie.strip().rstrip(';,').split(',' if ',' in cookie else ';'): - attr_val = attr.strip().split('=', 1) - if attr_val[0] in ('domain', 'path', 'expires', 'max-age', 'HttpOnly', 'secure', 'expiry', 'name', 'value'): - cookie_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else '' - else: - cookie_dict['name'] = attr_val[0] - cookie_dict['value'] = attr_val[1] if len(attr_val) == 2 else '' - - return cookie_dict - - else: - raise TypeError('cookie参数必须为Cookie、str或dict类型。') - - return cookie_dict - - -def cookies_to_tuple(cookies): - """把cookies转为tuple格式 - :param cookies: cookies信息,可为CookieJar, list, tuple, str, dict - :return: 返回tuple形式的cookies - """ - if isinstance(cookies, (list, tuple, CookieJar)): - cookies = tuple(cookie_to_dict(cookie) for cookie in cookies) - - elif isinstance(cookies, str): - c_dict = {} - cookies = cookies.rstrip('; ') - cookies = cookies.split(';') - # r = match(r'.*?=([^=]+)=', cookies) - # if not r: # 只有一个 - # cookies = [cookies.rstrip(',;')] - # else: - # s = match(r'.*([,;]).*', r.group(1)).group(1) - # cookies = cookies.rstrip(s).split(s) - - for attr in cookies: - attr_val = attr.strip().split('=', 1) - c_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else True - cookies = _dict_cookies_to_tuple(c_dict) - - elif isinstance(cookies, dict): - cookies = _dict_cookies_to_tuple(cookies) - - elif isinstance(cookies, Cookie): - cookies = (cookie_to_dict(cookies),) - - else: - raise TypeError('cookies参数必须为Cookie、CookieJar、list、tuple、str或dict类型。') - - return cookies - - -def set_session_cookies(session, cookies): - """设置Session对象的cookies - :param session: Session对象 - :param cookies: cookies信息 - :return: None - """ - for cookie in cookies_to_tuple(cookies): - if cookie['value'] is None: - cookie['value'] = '' - - kwargs = {x: cookie[x] for x in cookie - if x.lower() in ('version', 'port', 'domain', 'path', 'secure', - 'expires', 'discard', 'comment', 'comment_url', 'rest')} - - if 'expiry' in cookie: - kwargs['expires'] = cookie['expiry'] - - session.cookies.set(cookie['name'], cookie['value'], **kwargs) - - -def set_browser_cookies(page, cookies): - """设置cookies值 - :param page: 页面对象 - :param cookies: cookies信息 - :return: None - """ - for cookie in cookies_to_tuple(cookies): - if 'expiry' in cookie: - cookie['expires'] = int(cookie['expiry']) - cookie.pop('expiry') - - if 'expires' in cookie: - if not cookie['expires']: - cookie.pop('expires') - - elif isinstance(cookie['expires'], str): - if cookie['expires'].isdigit(): - cookie['expires'] = int(cookie['expires']) - - elif cookie['expires'].replace('.', '').isdigit(): - cookie['expires'] = float(cookie['expires']) - - else: - try: - cookie['expires'] = datetime.strptime(cookie['expires'], - '%a, %d %b %Y %H:%M:%S GMT').timestamp() - except ValueError: - cookie['expires'] = datetime.strptime(cookie['expires'], - '%a, %d %b %y %H:%M:%S GMT').timestamp() - - if cookie['value'] is None: - cookie['value'] = '' - elif not isinstance(cookie['value'], str): - cookie['value'] = str(cookie['value']) - - if cookie['name'].startswith('__Host-'): - cookie['path'] = '/' - cookie['secure'] = True - if not page.url.startswith('http'): - cookie['name'] = cookie['name'].replace('__Host-', '__Secure-', 1) - else: - cookie['url'] = page.url - page.run_cdp_loaded('Network.setCookie', **cookie) - continue # 不用设置域名,可退出 - - if cookie['name'].startswith('__Secure-'): - cookie['secure'] = True - - if cookie.get('domain', None): - try: - page.run_cdp_loaded('Network.setCookie', **cookie) - if is_cookie_in_driver(page, cookie): - continue - except Exception: - pass - - url = page._browser_url - if not url.startswith('http'): - raise RuntimeError(f'未设置域名,请设置cookie的domain参数或先访问一个网站。{cookie}') - ex_url = extract(url) - d_list = ex_url.subdomain.split('.') - d_list.append(f'{ex_url.domain}.{ex_url.suffix}' if ex_url.suffix else ex_url.domain) - - tmp = [d_list[0]] - if len(d_list) > 1: - for i in d_list[1:]: - tmp.append('.') - tmp.append(i) - - for i in range(len(tmp)): - cookie['domain'] = ''.join(tmp[i:]) - page.run_cdp_loaded('Network.setCookie', **cookie) - if is_cookie_in_driver(page, cookie): - break - - -def is_cookie_in_driver(page, cookie): - """查询cookie是否在浏览器内 - :param page: BasePage对象 - :param cookie: dict格式cookie - :return: bool - """ - if 'domain' in cookie: - for c in page.cookies(all_domains=True): - if cookie['name'] == c['name'] and cookie['value'] == c['value'] and cookie['domain'] == c.get('domain', - None): - return True - else: - for c in page.cookies(all_domains=True): - if cookie['name'] == c['name'] and cookie['value'] == c['value']: - return True - return False - - def get_blob(page, url, as_bytes=True): """获取知道blob资源 :param page: 资源所在页面对象 @@ -378,7 +220,7 @@ def get_blob(page, url, as_bytes=True): } """ try: - result = page.run_js(js, url) + result = page._run_js(js, url) except: raise RuntimeError('无法获取该资源。') if as_bytes: @@ -426,7 +268,7 @@ def get_mhtml(page, path=None, name=None): :param name: 文件名,为None且path不为None时用title属性值 :return: mhtml文本 """ - r = page.run_cdp('Page.captureSnapshot')['data'] + r = page._run_cdp('Page.captureSnapshot')['data'] if path is None and name is None: return r @@ -452,7 +294,7 @@ def get_pdf(page, path=None, name=None, kwargs=None): if 'printBackground' not in kwargs: kwargs['printBackground'] = True try: - r = page.run_cdp('Page.printToPDF', **kwargs)['data'] + r = page._run_cdp('Page.printToPDF', **kwargs)['data'] except: raise RuntimeError('保存失败,可能浏览器版本不支持。') from base64 import b64decode @@ -528,7 +370,9 @@ def format_headers(txt): :param txt: 从浏览器复制的原始文本格式headers :return: dict格式headers """ - if not isinstance(txt, str): + if isinstance(txt, (dict, CaseInsensitiveDict)): + for k, v in txt.items(): + txt[k] = str(v) return txt headers = {} for header in txt.split('\n'): @@ -536,15 +380,3 @@ def format_headers(txt): name, value = header.split(': ', maxsplit=1) headers[name] = value return headers - - -def _dict_cookies_to_tuple(cookies: dict): - """把dict形式的cookies转换为tuple形式 - :param cookies: 单个或多个cookies,单个时包含'name'和'value' - :return: 多个dict格式cookies组成的列表 - """ - if 'name' in cookies and 'value' in cookies: # 单个cookie - return (cookies,) - keys = ('domain', 'path', 'expires', 'max-age', 'HttpOnly', 'secure', 'expiry') - template = {k: v for k, v in cookies.items() if k in keys} - return tuple(dict(**{'name': k, 'value': v}, **template) for k, v in cookies.items() if k not in keys) diff --git a/DrissionPage/_functions/web.pyi b/DrissionPage/_functions/web.pyi index 3fdca32..c7a40db 100644 --- a/DrissionPage/_functions/web.pyi +++ b/DrissionPage/_functions/web.pyi @@ -5,18 +5,14 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ -from http.cookiejar import Cookie from pathlib import Path -from typing import Union, Optional - -from requests import Session -from requests.cookies import RequestsCookieJar +from typing import Union, Optional, Tuple from .._base.base import DrissionElement, BaseParser from .._elements.chromium_element import ChromiumElement from .._pages.chromium_base import ChromiumBase from .._pages.chromium_page import ChromiumPage -from .._pages.chromium_tab import ChromiumTab +from .._pages.tabs import ChromiumTab def get_ele_txt(e: DrissionElement) -> str: ... @@ -28,7 +24,7 @@ def format_html(text: str) -> str: ... def location_in_viewport(page: ChromiumBase, loc_x: float, loc_y: float) -> bool: ... -def offset_scroll(ele: ChromiumElement, offset_x: float, offset_y: float) -> tuple: ... +def offset_scroll(ele: ChromiumElement, offset_x: float, offset_y: float) -> Tuple[int, int, int, int]: ... def make_absolute_link(link: str, baseURI: str = None) -> str: ... @@ -37,21 +33,6 @@ def make_absolute_link(link: str, baseURI: str = None) -> str: ... def is_js_func(func: str) -> bool: ... -def cookie_to_dict(cookie: Union[Cookie, str, dict]) -> dict: ... - - -def cookies_to_tuple(cookies: Union[RequestsCookieJar, list, tuple, str, dict, Cookie]) -> tuple: ... - - -def set_session_cookies(session: Session, cookies: Union[RequestsCookieJar, list, tuple, str, dict]) -> None: ... - - -def set_browser_cookies(page: ChromiumBase, cookies: Union[RequestsCookieJar, list, tuple, str, dict]) -> None: ... - - -def is_cookie_in_driver(page: ChromiumBase, cookie: dict) -> bool: ... - - def get_blob(page: ChromiumBase, url: str, as_bytes: bool = True) -> bytes: ... diff --git a/DrissionPage/_pages/chromium_base.py b/DrissionPage/_pages/chromium_base.py index 96a1173..9ae334b 100644 --- a/DrissionPage/_pages/chromium_base.py +++ b/DrissionPage/_pages/chromium_base.py @@ -18,7 +18,9 @@ from .._base.base import BasePage from .._elements.chromium_element import run_js, make_chromium_eles from .._elements.none_element import NoneElement from .._elements.session_element import make_session_ele -from .._functions.locator import get_loc, is_loc +from .._functions.cookies import CookiesList +from .._functions.elements import SessionElementsList, get_frame +from .._functions.locator import get_loc from .._functions.settings import Settings from .._functions.tools import raise_error from .._functions.web import location_in_viewport @@ -36,15 +38,15 @@ __ERROR__ = 'error' class ChromiumBase(BasePage): - """标签页、frame、页面基类""" + """标签页、Frame、Page基类""" - def __init__(self, address, tab_id=None, timeout=None): + def __init__(self, browser, target_id=None): """ - :param address: 浏览器 ip:port - :param tab_id: 要控制的标签页id,不指定默认为激活的 - :param timeout: 超时时间(秒) + :param browser: Chromium + :param target_id: 要控制的target id,不指定默认为激活的标签页 """ super().__init__() + self._browser = browser self._is_loading = None self._root_id = None # object id self._set = None @@ -65,35 +67,21 @@ class ChromiumBase(BasePage): if not hasattr(self, '_listener'): self._listener = None - if isinstance(address, int) or (isinstance(address, str) and address.isdigit()): - address = f'127.0.0.1:{address}' - - self._d_set_start_options(address) self._d_set_runtime_settings() - self._connect_browser(tab_id) - if timeout is not None: - self.timeout = timeout - - def _d_set_start_options(self, address): - """设置浏览器启动属性 - :param address: 'ip:port' - :return: None - """ - self.address = address.replace('localhost', '127.0.0.1').lstrip('http://').lstrip('https://') + self._connect_browser(target_id) def _d_set_runtime_settings(self): - self._timeouts = Timeout(self) - self._load_mode = 'normal' + pass - def _connect_browser(self, tab_id=None): + def _connect_browser(self, target_id=None): """连接浏览器,在第一次时运行 - :param tab_id: 要控制的标签页id,不指定默认为激活的 + :param target_id: 要控制的target id,不指定默认为激活的标签页 :return: None """ self._is_reading = False - if not tab_id: - tabs = self.browser.driver.get(f'http://{self.address}/json').json() + if not target_id: + tabs = self.browser._driver.get(f'http://{self.browser.address}/json').json() tabs = [(i['id'], i['url']) for i in tabs if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')] dialog = None @@ -101,30 +89,30 @@ class ChromiumBase(BasePage): for k, t in enumerate(tabs): if t[1] == 'chrome://privacy-sandbox-dialog/notice': dialog = k - elif not tab_id: - tab_id = t[0] + elif not target_id: + target_id = t[0] - if tab_id and dialog is not None: + if target_id and dialog is not None: break if dialog is not None: close_privacy_dialog(self, tabs[dialog][0]) else: - tab_id = tabs[0][0] + target_id = tabs[0][0] - self._driver_init(tab_id) + self._driver_init(target_id) if self._js_ready_state == 'complete' and self._ready_state is None: self._get_document() self._ready_state = 'complete' - def _driver_init(self, tab_id): + def _driver_init(self, target_id): """新建页面、页面刷新、切换标签页后要进行的cdp参数初始化 - :param tab_id: 要跳转到的标签页id + :param target_id: 要跳转到的target id :return: None """ self._is_loading = True - self._driver = self.browser._get_driver(tab_id, self) + self._driver = self.browser._get_driver(target_id, self) self._alert = Alert() self._driver.set_callback('Page.javascriptDialogOpening', self._on_alert_open, immediate=True) @@ -134,7 +122,7 @@ class ChromiumBase(BasePage): self._driver.run('Page.enable') self._driver.run('Emulation.setFocusEmulationEnabled', enabled=True) - r = self.run_cdp('Page.getFrameTree') + r = self._run_cdp('Page.getFrameTree') for i in findall(r"'id': '(.*?)'", str(r)): self.browser._frames[i] = self.tab_id if not hasattr(self, '_frame_id'): @@ -160,11 +148,11 @@ class ChromiumBase(BasePage): end_time = perf_counter() + timeout while perf_counter() < end_time: try: - b_id = self.run_cdp('DOM.getDocument', _timeout=timeout)['root']['backendNodeId'] + b_id = self._run_cdp('DOM.getDocument', _timeout=timeout)['root']['backendNodeId'] timeout = end_time - perf_counter() timeout = 1 if timeout <= 1 else timeout - self._root_id = self.run_cdp('DOM.resolveNode', backendNodeId=b_id, - _timeout=timeout)['object']['objectId'] + self._root_id = self._run_cdp('DOM.resolveNode', backendNodeId=b_id, + _timeout=timeout)['object']['objectId'] result = True break @@ -180,7 +168,7 @@ class ChromiumBase(BasePage): result = False if result: - r = self.run_cdp('Page.getFrameTree') + r = self._run_cdp('Page.getFrameTree') for i in findall(r"'id': '(.*?)'", str(r)): self.browser._frames[i] = self.tab_id @@ -217,7 +205,7 @@ class ChromiumBase(BasePage): def _onDomContentEventFired(self, **kwargs): """在页面刷新、变化后重新读取页面内容""" if self._load_mode == 'eager': - self.run_cdp('Page.stopLoading') + self._run_cdp('Page.stopLoading') if self._get_document(self._load_end_time - perf_counter() - .1): self._doc_got = True self._ready_state = 'interactive' @@ -242,10 +230,10 @@ class ChromiumBase(BasePage): if 'backendNodeId' not in kwargs: raise TypeError('该输入框无法接管,请改用对元素输入路径的方法设置。') files = self._upload_list if kwargs['mode'] == 'selectMultiple' else self._upload_list[:1] - self.run_cdp('DOM.setFileInputFiles', files=files, backendNodeId=kwargs['backendNodeId']) + self._run_cdp('DOM.setFileInputFiles', files=files, backendNodeId=kwargs['backendNodeId']) self.driver.set_callback('Page.fileChooserOpened', None) - self.run_cdp('Page.setInterceptFileChooserDialog', enabled=False) + self._run_cdp('Page.setInterceptFileChooserDialog', enabled=False) self._upload_list = None def __call__(self, locator, index=1, timeout=None): @@ -326,6 +314,11 @@ class ChromiumBase(BasePage): self._rect = TabRect(self) return self._rect + @property + def timeout(self): + """返回timeout设置""" + return self._timeouts.base + @property def timeouts(self): """返回timeouts设置""" @@ -347,23 +340,23 @@ class ChromiumBase(BasePage): @property def title(self): """返回当前页面title""" - return self.run_cdp_loaded('Target.getTargetInfo', targetId=self._target_id)['targetInfo']['title'] + return self._run_cdp_loaded('Target.getTargetInfo', targetId=self._target_id)['targetInfo']['title'] @property def url(self): """返回当前页面url""" - return self.run_cdp_loaded('Target.getTargetInfo', targetId=self._target_id)['targetInfo']['url'] + return self._run_cdp_loaded('Target.getTargetInfo', targetId=self._target_id)['targetInfo']['url'] @property def _browser_url(self): - """用于被WebPage覆盖""" + """用于被MixTab覆盖""" return self.url @property def html(self): """返回当前页面html文本""" self.wait.doc_loaded() - return self.run_cdp('DOM.getOuterHTML', objectId=self._root_id)['outerHTML'] + return self._run_cdp('DOM.getOuterHTML', objectId=self._root_id)['outerHTML'] @property def json(self): @@ -381,12 +374,12 @@ class ChromiumBase(BasePage): @property def _target_id(self): """返回当前标签页id""" - return self.driver.id if not self.driver._stopped.is_set() else '' + return self.driver.id if self.driver.is_running else '' @property def active_ele(self): """返回当前焦点所在元素""" - return self.run_js_loaded('return document.activeElement;') + return self._run_js_loaded('return document.activeElement;') @property def load_mode(self): @@ -396,7 +389,7 @@ class ChromiumBase(BasePage): @property def user_agent(self): """返回user agent""" - return self.run_cdp('Runtime.evaluate', expression='navigator.userAgent;')['result']['value'] + return self._run_cdp('Runtime.evaluate', expression='navigator.userAgent;')['result']['value'] @property def upload_list(self): @@ -407,7 +400,7 @@ class ChromiumBase(BasePage): def _js_ready_state(self): """返回js获取的ready state信息""" try: - return self.run_cdp('Runtime.evaluate', expression='document.readyState;', _timeout=3)['result']['value'] + return self._run_cdp('Runtime.evaluate', expression='document.readyState;', _timeout=3)['result']['value'] except ContextLostError: return None except TimeoutError: @@ -419,9 +412,8 @@ class ChromiumBase(BasePage): :param cmd_args: 参数 :return: 执行的结果 """ - ignore = cmd_args.pop('_ignore', None) r = self.driver.run(cmd, **cmd_args) - return r if __ERROR__ not in r else raise_error(r, ignore) + return r if __ERROR__ not in r else raise_error(r, user=True) def run_cdp_loaded(self, cmd, **cmd_args): """执行Chrome DevTools Protocol语句,执行前等待页面加载完毕 @@ -430,7 +422,27 @@ class ChromiumBase(BasePage): :return: 执行的结果 """ self.wait.doc_loaded() - return self.run_cdp(cmd, **cmd_args) + r = self.driver.run(cmd, **cmd_args) + return r if __ERROR__ not in r else raise_error(r, user=True) + + def _run_cdp(self, cmd, **cmd_args): + """执行Chrome DevTools Protocol语句 + :param cmd: 协议项目 + :param cmd_args: 参数 + :return: 执行的结果 + """ + ignore = cmd_args.pop('_ignore', None) + r = self.driver.run(cmd, **cmd_args) + return r if __ERROR__ not in r else raise_error(r, ignore) + + def _run_cdp_loaded(self, cmd, **cmd_args): + """执行Chrome DevTools Protocol语句,执行前等待页面加载完毕 + :param cmd: 协议项目 + :param cmd_args: 参数 + :return: 执行的结果 + """ + self.wait.doc_loaded() + return self._run_cdp(cmd, **cmd_args) def run_js(self, script, *args, as_expr=False, timeout=None): """运行javascript代码 @@ -440,9 +452,30 @@ class ChromiumBase(BasePage): :param timeout: js超时时间(秒),为None则使用页面timeouts.script设置 :return: 运行的结果 """ - return run_js(self, script, as_expr, self.timeouts.script if timeout is None else timeout, args) + return self._run_js(script, *args, as_expr=as_expr, timeout=timeout) def run_js_loaded(self, script, *args, as_expr=False, timeout=None): + """运行javascript代码,执行前等待页面加载完毕 + :param script: js文本或js文件路径 + :param args: 参数,按顺序在js文本中对应arguments[0]、arguments[1]... + :param as_expr: 是否作为表达式运行,为True时args无效 + :param timeout: js超时时间(秒),为None则使用页面timeouts.script属性值 + :return: 运行的结果 + """ + self.wait.doc_loaded() + return self._run_js(script, *args, as_expr=as_expr, timeout=timeout) + + def _run_js(self, script, *args, as_expr=False, timeout=None): + """运行javascript代码 + :param script: js文本或js文件路径 + :param args: 参数,按顺序在js文本中对应arguments[0]、arguments[1]... + :param as_expr: 是否作为表达式运行,为True时args无效 + :param timeout: js超时时间(秒),为None则使用页面timeouts.script设置 + :return: 运行的结果 + """ + return run_js(self, script, as_expr, self.timeouts.script if timeout is None else timeout, args) + + def _run_js_loaded(self, script, *args, as_expr=False, timeout=None): """运行javascript代码,执行前等待页面加载完毕 :param script: js文本或js文件路径 :param args: 参数,按顺序在js文本中对应arguments[0]、arguments[1]... @@ -476,29 +509,27 @@ class ChromiumBase(BasePage): show_errmsg=show_errmsg, timeout=timeout) return self._url_available - def cookies(self, as_dict=False, all_domains=False, all_info=False): + def cookies(self, all_domains=False, all_info=False): """返回cookies信息 - :param as_dict: 为True时以dict格式返回且all_info无效,为False时返回list :param all_domains: 是否返回所有域的cookies :param all_info: 是否返回所有信息,为False时只返回name、value、domain :return: cookies信息 """ txt = 'Storage' if all_domains else 'Network' - cookies = self.run_cdp_loaded(f'{txt}.getCookies')['cookies'] + cookies = self._run_cdp_loaded(f'{txt}.getCookies')['cookies'] - if as_dict: - return {cookie['name']: cookie['value'] for cookie in cookies} - elif all_info: - return cookies + if all_info: + r = cookies else: - return [{'name': cookie['name'], 'value': cookie['value'], 'domain': cookie['domain']} - for cookie in cookies] + r = [{'name': cookie['name'], 'value': cookie['value'], 'domain': cookie['domain']} for cookie in cookies] + + return CookiesList(r) def ele(self, locator, index=1, timeout=None): """获取一个符合条件的元素对象 :param locator: 定位符或元素对象 :param index: 获取第几个元素,从1开始,可传入负数获取倒数第几个 - :param timeout: 查找超时时间(秒) + :param timeout: 查找超时时间(秒),默认与页面等待时间一致 :return: ChromiumElement对象 """ return self._ele(locator, timeout=timeout, index=index, method='ele()') @@ -506,32 +537,37 @@ class ChromiumBase(BasePage): def eles(self, locator, timeout=None): """获取所有符合条件的元素对象 :param locator: 定位符或元素对象 - :param timeout: 查找超时时间(秒) + :param timeout: 查找超时时间(秒),默认与页面等待时间一致 :return: ChromiumElement对象组成的列表 """ return self._ele(locator, timeout=timeout, index=None) - def s_ele(self, locator=None, index=1): + def s_ele(self, locator=None, index=1, timeout=None): """查找一个符合条件的元素以SessionElement形式返回,处理复杂页面时效率很高 :param locator: 元素的定位信息,可以是loc元组,或查询字符串 :param index: 获取第几个,从1开始,可传入负数获取倒数第几个 + :param timeout: 查找元素超时时间(秒),默认与页面等待时间一致 :return: SessionElement对象或属性、文本 """ - return make_session_ele(self, locator, index=index, method='s_ele()') + return (NoneElement(self, method='s_ele()', args={'locator': locator, 'index': index}) + if locator and not self.wait.eles_loaded(locator, timeout=timeout) + else make_session_ele(self, locator, index=index, method='s_ele()')) - def s_eles(self, locator): + def s_eles(self, locator, timeout=None): """查找所有符合条件的元素以SessionElement列表形式返回 :param locator: 元素的定位信息,可以是loc元组,或查询字符串 + :param timeout: 查找元素超时时间(秒),默认与页面等待时间一致 :return: SessionElement对象组成的列表 """ - return make_session_ele(self, locator, index=None) + return (make_session_ele(self, locator, index=None) + if self.wait.eles_loaded(locator, timeout=timeout) else SessionElementsList()) def _find_elements(self, locator, timeout=None, index=1, relative=False, raise_err=None): """执行元素查找 :param locator: 定位符或元素对象 :param timeout: 查找超时时间(秒) :param index: 第几个结果,从1开始,可传入负数获取倒数第几个,为None返回所有 - :param relative: WebPage用的表示是否相对定位的参数 + :param relative: MixTab用的表示是否相对定位的参数 :param raise_err: 找不到元素是是否抛出异常,为None时根据全局设置 :return: ChromiumElement对象或元素对象组成的列表 """ @@ -605,7 +641,7 @@ class ChromiumBase(BasePage): :return: None """ self._is_loading = True - self.run_cdp('Page.reload', ignoreCache=ignore_cache) + self._run_cdp('Page.reload', ignoreCache=ignore_cache) self.wait.load_start() def forward(self, steps=1): @@ -630,7 +666,7 @@ class ChromiumBase(BasePage): if steps == 0: return - history = self.run_cdp('Page.getNavigationHistory') + history = self._run_cdp('Page.getNavigationHistory') index = history['currentIndex'] history = history['entries'] direction = 1 if steps > 0 else -1 @@ -646,12 +682,12 @@ class ChromiumBase(BasePage): if nid: self._is_loading = True - self.run_cdp('Page.navigateToHistoryEntry', entryId=nid) + self._run_cdp('Page.navigateToHistoryEntry', entryId=nid) def stop_loading(self): """页面停止加载""" try: - self.run_cdp('Page.stopLoading') + self._run_cdp('Page.stopLoading') end_time = perf_counter() + 5 while self._ready_state != 'complete' and perf_counter() < end_time: sleep(.1) @@ -669,7 +705,7 @@ class ChromiumBase(BasePage): return ele = self._ele(loc_or_ele, raise_err=False) if ele: - self.run_cdp('DOM.removeNode', nodeId=ele._node_id, _ignore=ElementLostError) + self._run_cdp('DOM.removeNode', nodeId=ele._node_id, _ignore=ElementLostError) def add_ele(self, html_or_info, insert_to=None, before=None): """新建一个元素 @@ -725,7 +761,7 @@ class ChromiumBase(BasePage): else: raise TypeError('html_or_info参数必须是html文本或tuple,tuple格式为(tag, {name: value})。') - ele = self.run_js(js, *args) + ele = self._run_js(js, *args) return ele def get_frame(self, loc_ind_ele, timeout=None): @@ -734,41 +770,7 @@ class ChromiumBase(BasePage): :param timeout: 查找元素超时时间(秒) :return: ChromiumFrame对象 """ - if isinstance(loc_ind_ele, str): - if not is_loc(loc_ind_ele): - xpath = f'xpath://*[(name()="iframe" or name()="frame") and ' \ - f'(@name="{loc_ind_ele}" or @id="{loc_ind_ele}")]' - else: - xpath = loc_ind_ele - ele = self._ele(xpath, timeout=timeout) - if ele and ele._type != 'ChromiumFrame': - raise TypeError('该定位符不是指向frame元素。') - r = ele - - elif isinstance(loc_ind_ele, tuple): - ele = self._ele(loc_ind_ele, timeout=timeout) - if ele and ele._type != 'ChromiumFrame': - raise TypeError('该定位符不是指向frame元素。') - r = ele - - elif isinstance(loc_ind_ele, int): - if loc_ind_ele == 0: - loc_ind_ele = 1 - elif loc_ind_ele < 0: - loc_ind_ele = f'last()+{loc_ind_ele}+1' - xpath = f'xpath:(//*[name()="frame" or name()="iframe"])[{loc_ind_ele}]' - r = self._ele(xpath, timeout=timeout) - - elif loc_ind_ele._type == 'ChromiumFrame': - r = loc_ind_ele - - else: - raise TypeError('必须传入定位符、iframe序号、id、name、ChromiumFrame对象其中之一。') - - if isinstance(r, NoneElement): - r.method = 'get_frame()' - r.args = {'loc_ind_ele': loc_ind_ele} - return r + return get_frame(self, loc_ind_ele=loc_ind_ele, timeout=timeout) def get_frames(self, locator=None, timeout=None): """获取所有符合条件的frame对象 @@ -786,7 +788,7 @@ class ChromiumBase(BasePage): :return: sessionStorage一个或所有项内容 """ js = f'sessionStorage.getItem("{item}")' if item else 'sessionStorage' - return self.run_js_loaded(js, as_expr=True) + return self._run_js_loaded(js, as_expr=True) def local_storage(self, item=None): """返回localStorage信息,不设置item则获取全部 @@ -794,7 +796,7 @@ class ChromiumBase(BasePage): :return: localStorage一个或所有项内容 """ js = f'localStorage.getItem("{item}")' if item else 'localStorage' - return self.run_js_loaded(js, as_expr=True) + return self._run_js_loaded(js, as_expr=True) def get_screenshot(self, path=None, name=None, as_bytes=None, as_base64=None, full_page=False, left_top=None, right_bottom=None): @@ -816,8 +818,8 @@ class ChromiumBase(BasePage): :param script: js文本 :return: 添加的脚本的id """ - js_id = self.run_cdp('Page.addScriptToEvaluateOnNewDocument', source=script, - includeCommandLineAPI=True)['identifier'] + js_id = self._run_cdp('Page.addScriptToEvaluateOnNewDocument', source=script, + includeCommandLineAPI=True)['identifier'] self._init_jss.append(js_id) return js_id @@ -828,11 +830,11 @@ class ChromiumBase(BasePage): """ if script_id is None: for js_id in self._init_jss: - self.run_cdp('Page.removeScriptToEvaluateOnNewDocument', identifier=js_id) + self._run_cdp('Page.removeScriptToEvaluateOnNewDocument', identifier=js_id) self._init_jss.clear() elif script_id in self._init_jss: - self.run_cdp('Page.removeScriptToEvaluateOnNewDocument', identifier=script_id) + self._run_cdp('Page.removeScriptToEvaluateOnNewDocument', identifier=script_id) self._init_jss.remove(script_id) def clear_cache(self, session_storage=True, local_storage=True, cache=True, cookies=True): @@ -844,24 +846,25 @@ class ChromiumBase(BasePage): :return: None """ if session_storage or local_storage: - self.run_cdp_loaded('DOMStorage.enable') - i = self.run_cdp('Storage.getStorageKeyForFrame', frameId=self._frame_id)['storageKey'] + self._run_cdp_loaded('DOMStorage.enable') + i = self._run_cdp('Storage.getStorageKeyForFrame', frameId=self._frame_id)['storageKey'] if session_storage: - self.run_cdp('DOMStorage.clear', storageId={'storageKey': i, 'isLocalStorage': False}) + self._run_cdp('DOMStorage.clear', storageId={'storageKey': i, 'isLocalStorage': False}) if local_storage: - self.run_cdp('DOMStorage.clear', storageId={'storageKey': i, 'isLocalStorage': True}) - self.run_cdp_loaded('DOMStorage.disable') + self._run_cdp('DOMStorage.clear', storageId={'storageKey': i, 'isLocalStorage': True}) + self._run_cdp_loaded('DOMStorage.disable') if cache: - self.run_cdp_loaded('Network.clearBrowserCache') + self._run_cdp_loaded('Network.clearBrowserCache') if cookies: - self.run_cdp_loaded('Network.clearBrowserCookies') + self._run_cdp_loaded('Network.clearBrowserCookies') def disconnect(self): """断开与页面的连接,不关闭页面""" if self._driver: - self.browser.stop_driver(self._driver) + self._driver.stop() + self.browser._all_drivers.get(self._driver.id, set()).discard(self._driver) def reconnect(self, wait=0): """断开与页面原来的页面,重新建立连接 @@ -987,7 +990,7 @@ class ChromiumBase(BasePage): err = None end_time = perf_counter() + timeout try: - result = self.run_cdp('Page.navigate', frameId=self._frame_id, url=to_url, _timeout=timeout) + result = self._run_cdp('Page.navigate', frameId=self._frame_id, url=to_url, _timeout=timeout) if 'errorText' in result: err = ConnectionError(result['errorText']) except TimeoutError: @@ -1087,8 +1090,8 @@ class ChromiumBase(BasePage): v = not (location_in_viewport(self, x, y) and location_in_viewport(self, right_bottom[0], right_bottom[1])) - if v and (self.run_js('return document.body.scrollHeight > window.innerHeight;') and - not self.run_js('return document.body.scrollWidth > window.innerWidth;')): + if v and (self._run_js('return document.body.scrollHeight > window.innerHeight;') and + not self._run_js('return document.body.scrollWidth > window.innerWidth;')): x += 10 vp = {'x': x, 'y': y, 'width': w, 'height': h, 'scale': 1} @@ -1099,7 +1102,7 @@ class ChromiumBase(BasePage): if pic_type == 'jpeg': args['quality'] = 100 - png = self.run_cdp_loaded('Page.captureScreenshot', **args)['data'] + png = self._run_cdp_loaded('Page.captureScreenshot', **args)['data'] if as_base64: return png @@ -1119,15 +1122,12 @@ class ChromiumBase(BasePage): class Timeout(object): """用于保存d模式timeout信息的类""" - def __init__(self, page, base=None, page_load=None, script=None, implicit=None): + def __init__(self, base=None, page_load=None, script=None): """ - :param page: ChromiumBase页面 :param base: 默认超时时间 :param page_load: 页面加载超时时间 :param script: js超时时间 """ - self._page = page - base = base if base is not None else implicit self.base = 10 if base is None else base self.page_load = 30 if page_load is None else page_load self.script = 30 if script is None else script @@ -1135,6 +1135,10 @@ class Timeout(object): def __repr__(self): return str({'base': self.base, 'page_load': self.page_load, 'script': self.script}) + @property + def as_dict(self): + return {'base': self.base, 'page_load': self.page_load, 'script': self.script} + class Alert(object): """用于保存alert信息的类""" @@ -1158,6 +1162,7 @@ def close_privacy_dialog(page, tid): :return: None """ try: + print('ooo') driver = page.browser._get_driver(tid) driver.run('Runtime.enable') driver.run('DOM.enable') diff --git a/DrissionPage/_pages/chromium_base.pyi b/DrissionPage/_pages/chromium_base.pyi index 17b14bb..2ab5fb2 100644 --- a/DrissionPage/_pages/chromium_base.pyi +++ b/DrissionPage/_pages/chromium_base.pyi @@ -8,12 +8,13 @@ from pathlib import Path from typing import Union, Tuple, List, Any, Optional, Literal -from .chromium_tab import ChromiumTab +from .tabs import ChromiumTab, MixTab from .._base.base import BasePage -from .._base.browser import Browser +from .._base.browser import Chromium from .._base.driver import Driver from .._elements.chromium_element import ChromiumElement from .._elements.session_element import SessionElement +from .._functions.cookies import CookiesList from .._functions.elements import SessionElementsList, ChromiumElementsList from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_page import ChromiumPage @@ -31,13 +32,10 @@ PIC_TYPE = Literal['jpg', 'jpeg', 'png', 'webp', True] class ChromiumBase(BasePage): def __init__(self, - address: Union[str, int], - tab_id: str = None, - timeout: float = None): - self._browser: Browser = ... - self._page: ChromiumPage = ... - self.tab: Union[ChromiumPage, ChromiumTab] = ... - self.address: str = ... + browser: Chromium, + tab_id: str = None): + self._tab: Union[ChromiumTab, MixTab, ChromiumFrame] = ... + self._browser: Chromium = ... self._driver: Driver = ... self._frame_id: str = ... self._is_reading: bool = ... @@ -65,9 +63,9 @@ class ChromiumBase(BasePage): self._rect: TabRect = ... self._type: str = ... - def _connect_browser(self, tab_id: str = None) -> None: ... + def _connect_browser(self, target_id: str = None) -> None: ... - def _driver_init(self, tab_id: str) -> None: ... + def _driver_init(self, target_id: str) -> None: ... def _get_document(self, timeout: float = 10) -> bool: ... @@ -91,7 +89,7 @@ class ChromiumBase(BasePage): def _wait_to_stop(self): ... - def _d_set_start_options(self, address) -> None: ... + # def _d_set_start_options(self, address) -> None: ... def _d_set_runtime_settings(self) -> None: ... @@ -104,7 +102,7 @@ class ChromiumBase(BasePage): def _js_ready_state(self) -> str: ... @property - def browser(self) -> Browser: ... + def browser(self) -> Chromium: ... @property def title(self) -> str: ... @@ -145,6 +143,9 @@ class ChromiumBase(BasePage): @property def rect(self) -> TabRect: ... + @property + def timeout(self) -> float: ... + @property def timeouts(self) -> Timeout: ... @@ -173,13 +174,16 @@ class ChromiumBase(BasePage): def run_js_loaded(self, script: Union[str, Path], *args, as_expr: bool = False, timeout: float = None) -> Any: ... + def _run_js(self, script: Union[str, Path], *args, as_expr: bool = False, timeout: float = None) -> Any: ... + + def _run_js_loaded(self, script: Union[str, Path], *args, as_expr: bool = False, timeout: float = None) -> Any: ... + def run_async_js(self, script: Union[str, Path], *args, as_expr: bool = False) -> None: ... def get(self, url: str, show_errmsg: bool = False, retry: int = None, interval: float = None, timeout: float = None) -> Union[None, bool]: ... - def cookies(self, as_dict: bool = False, all_domains: bool = False, all_info: bool = False) -> Union[ - list, dict]: ... + def cookies(self, all_domains: bool = False, all_info: bool = False) -> CookiesList: ... def ele(self, locator: Union[Tuple[str, str], str, ChromiumElement, ChromiumFrame], @@ -192,9 +196,12 @@ class ChromiumBase(BasePage): def s_ele(self, locator: Union[Tuple[str, str], str] = None, - index: int = 1) -> SessionElement: ... + index: int = 1, + timeout: float = None) -> SessionElement: ... - def s_eles(self, locator: Union[Tuple[str, str], str]) -> SessionElementsList: ... + def s_eles(self, + locator: Union[Tuple[str, str], str], + timeout: float = None) -> SessionElementsList: ... def _find_elements(self, locator: Union[Tuple[str, str], str, ChromiumElement, ChromiumFrame], @@ -220,7 +227,9 @@ class ChromiumBase(BasePage): insert_to: Union[ChromiumElement, str, Tuple[str, str], None] = None, before: Union[ChromiumElement, str, Tuple[str, str], None] = None) -> ChromiumElement: ... - def get_frame(self, loc_ind_ele: Union[str, int, tuple, ChromiumFrame], timeout: float = None) -> ChromiumFrame: ... + def get_frame(self, + loc_ind_ele: Union[str, int, tuple, ChromiumFrame, ChromiumElement], + timeout: float = None) -> ChromiumFrame: ... def get_frames(self, locator: Union[str, tuple] = None, timeout: float = None) -> List[ChromiumFrame]: ... @@ -228,6 +237,10 @@ class ChromiumBase(BasePage): def run_cdp_loaded(self, cmd: str, **cmd_args) -> dict: ... + def _run_cdp(self, cmd: str, **cmd_args) -> dict: ... + + def _run_cdp_loaded(self, cmd: str, **cmd_args) -> dict: ... + def session_storage(self, item: str = None) -> Union[str, dict, None]: ... def local_storage(self, item: str = None) -> Union[str, dict, None]: ... @@ -267,12 +280,14 @@ class ChromiumBase(BasePage): class Timeout(object): - def __init__(self, page: ChromiumBase, base=None, page_load=None, script=None): - self._page: ChromiumBase = ... + def __init__(self, base=None, page_load=None, script=None): self.base: float = ... self.page_load: float = ... self.script: float = ... + @property + def as_dict(self) -> dict: ... + class Alert(object): diff --git a/DrissionPage/_pages/chromium_frame.py b/DrissionPage/_pages/chromium_frame.py index f3e1ab5..2f30aff 100644 --- a/DrissionPage/_pages/chromium_frame.py +++ b/DrissionPage/_pages/chromium_frame.py @@ -27,42 +27,26 @@ class ChromiumFrame(ChromiumBase): :param ele: frame所在元素 :param info: frame所在元素信息 """ - if owner._type in ('ChromiumPage', 'WebPage'): - self._page = self._target_page = self.tab = owner - self._browser = owner.browser - else: # Tab、Frame - self._page = owner.page - self._browser = self._page.browser - self._target_page = owner - self.tab = owner.tab if owner._type == 'ChromiumFrame' else owner - - self.address = owner.address - self._tab_id = owner.tab_id + self._tab = owner._tab + self._target_page = owner self._backend_id = ele._backend_id self._frame_ele = ele - self._states = None self._reloading = False - node = info['node'] if not info else owner.run_cdp('DOM.describeNode', backendNodeId=ele._backend_id)['node'] + node = info['node'] if not info else owner._run_cdp('DOM.describeNode', backendNodeId=ele._backend_id)['node'] self._frame_id = node['frameId'] if self._is_inner_frame(): self._is_diff_domain = False self.doc_ele = ChromiumElement(self._target_page, backend_id=node['contentDocument']['backendNodeId']) - super().__init__(owner.address, owner.tab_id, owner.timeout) + super().__init__(owner.browser, owner.driver.id) else: self._is_diff_domain = True delattr(self, '_frame_id') - super().__init__(owner.address, node['frameId'], owner.timeout) - obj_id = super().run_js('document;', as_expr=True)['objectId'] + super().__init__(owner.browser, node['frameId']) + obj_id = super()._run_js('document;', as_expr=True)['objectId'] self.doc_ele = ChromiumElement(self, obj_id=obj_id) - self._rect = None self._type = 'ChromiumFrame' - # end_time = perf_counter() + 2 - # while perf_counter() < end_time: - # if self.url not in (None, 'about:blank'): - # break - # sleep(.1) def __call__(self, locator, index=1, timeout=None): """在内部查找元素 @@ -90,16 +74,16 @@ class ChromiumFrame(ChromiumBase): self._download_path = self._target_page.download_path self._load_mode = self._target_page._load_mode if not self._is_diff_domain else 'normal' - def _driver_init(self, tab_id, is_init=True): + def _driver_init(self, target_id, is_init=True): """避免出现服务器500错误 - :param tab_id: 要跳转到的标签页id + :param target_id: 要跳转到的target id :return: None """ try: - super()._driver_init(tab_id) + super()._driver_init(target_id) except: - self.browser.driver.get(f'http://{self.address}/json') - super()._driver_init(tab_id) + self.browser._driver.get(f'http://{self._browser.address}/json') + super()._driver_init(target_id) self._driver.set_callback('Inspector.detached', self._onInspectorDetached, immediate=True) self._driver.set_callback('Page.frameDetached', None) self._driver.set_callback('Page.frameDetached', self._onFrameDetached, immediate=True) @@ -116,7 +100,7 @@ class ChromiumFrame(ChromiumBase): self._frame_ele = ChromiumElement(self._target_page, backend_id=self._backend_id) end_time = perf_counter() + 2 while perf_counter() < end_time: - node = self._target_page.run_cdp('DOM.describeNode', backendNodeId=self._frame_ele._backend_id)['node'] + node = self._target_page._run_cdp('DOM.describeNode', backendNodeId=self._frame_ele._backend_id)['node'] if 'frameId' in node: break sleep(.05) @@ -132,16 +116,16 @@ class ChromiumFrame(ChromiumBase): self.doc_ele = ChromiumElement(self._target_page, backend_id=node['contentDocument']['backendNodeId']) self._frame_id = node['frameId'] if self._listener: - self._listener._to_target(self._target_page.tab_id, self.address, self) - super().__init__(self.address, self._target_page.tab_id, self._target_page.timeout) + self._listener._to_target(self._target_page.tab_id, self._browser.address, self) + super().__init__(self._browser, self._target_page.tab_id) # self.driver._debug = d_debug else: self._is_diff_domain = True if self._listener: - self._listener._to_target(node['frameId'], self.address, self) + self._listener._to_target(node['frameId'], self._browser.address, self) end_time = perf_counter() + self.timeouts.page_load - super().__init__(self.address, node['frameId'], self._target_page.timeout) + super().__init__(self._browser, node['frameId']) timeout = end_time - perf_counter() if timeout <= 0: timeout = .5 @@ -161,17 +145,17 @@ class ChromiumFrame(ChromiumBase): self._is_reading = True try: if self._is_diff_domain is False: - node = self._target_page.run_cdp('DOM.describeNode', backendNodeId=self._backend_id)['node'] + node = self._target_page._run_cdp('DOM.describeNode', backendNodeId=self._backend_id)['node'] self.doc_ele = ChromiumElement(self._target_page, backend_id=node['contentDocument']['backendNodeId']) else: timeout = max(timeout, 2) - b_id = self.run_cdp('DOM.getDocument', _timeout=timeout)['root']['backendNodeId'] + b_id = self._run_cdp('DOM.getDocument', _timeout=timeout)['root']['backendNodeId'] self.doc_ele = ChromiumElement(self, backend_id=b_id) self._root_id = self.doc_ele._obj_id - r = self.run_cdp('Page.getFrameTree') + r = self._run_cdp('Page.getFrameTree') for i in findall(r"'id': '(.*?)'", str(r)): self.browser._frames[i] = self.tab_id return True @@ -250,11 +234,6 @@ class ChromiumFrame(ChromiumBase): """返回cdp中的node id""" return self.frame_ele._node_id - @property - def page(self): - """返回所属Page对象""" - return self._page - @property def owner(self): """返回所属页面对象""" @@ -274,7 +253,7 @@ class ChromiumFrame(ChromiumBase): def url(self): """返回frame当前访问的url""" try: - return self.doc_ele.run_js('return this.location.href;') + return self.doc_ele._run_js('return this.location.href;') except JavaScriptError: return None @@ -282,14 +261,14 @@ class ChromiumFrame(ChromiumBase): def html(self): """返回元素outerHTML文本""" tag = self.tag - out_html = self._target_page.run_cdp('DOM.getOuterHTML', backendNodeId=self.frame_ele._backend_id)['outerHTML'] + out_html = self._target_page._run_cdp('DOM.getOuterHTML', backendNodeId=self.frame_ele._backend_id)['outerHTML'] sign = search(rf'<{tag}.*?>', out_html, DOTALL).group(0) return f'{sign}{self.inner_html}' @property def inner_html(self): """返回元素innerHTML文本""" - return self.doc_ele.run_js('return this.documentElement.outerHTML;') + return self.doc_ele._run_js('return this.documentElement.outerHTML;') @property def title(self): @@ -305,7 +284,7 @@ class ChromiumFrame(ChromiumBase): @property def active_ele(self): """返回当前焦点所在元素""" - return self.doc_ele.run_js('return this.activeElement;') + return self.doc_ele._run_js('return this.activeElement;') @property def xpath(self): @@ -317,15 +296,30 @@ class ChromiumFrame(ChromiumBase): """返回frame的css selector绝对路径""" return self.frame_ele.css_path + @property + def tab(self): + """返回frame所在tab的id""" + return self._tab + @property def tab_id(self): """返回frame所在tab的id""" - return self._tab_id + return self.tab.tab_id @property def download_path(self): return self._download_path + @property + def sr(self): + """返回iframe的shadow-root元素对象""" + return self.frame_ele.sr + + @property + def shadow_root(self): + """返回iframe的shadow-root元素对象""" + return self.frame_ele.sr + @property def _js_ready_state(self): """返回当前页面加载状态,'loading' 'interactive' 'complete'""" @@ -334,18 +328,18 @@ class ChromiumFrame(ChromiumBase): else: try: - return self.doc_ele.run_js('return this.readyState;') + return self.doc_ele._run_js('return this.readyState;') except ContextLostError: try: - node = self.run_cdp('DOM.describeNode', backendNodeId=self.frame_ele._backend_id)['node'] + node = self._run_cdp('DOM.describeNode', backendNodeId=self.frame_ele._backend_id)['node'] doc = ChromiumElement(self._target_page, backend_id=node['contentDocument']['backendNodeId']) - return doc.run_js('return this.readyState;') + return doc._run_js('return this.readyState;') except: return None def refresh(self): """刷新frame页面""" - self.doc_ele.run_js('this.location.reload();') + self.doc_ele._run_js('this.location.reload();') def property(self, name): """返回frame元素一个property属性值 @@ -369,6 +363,16 @@ class ChromiumFrame(ChromiumBase): self.frame_ele.remove_attr(name) def run_js(self, script, *args, as_expr=False, timeout=None): + """运行javascript代码 + :param script: js文本 + :param args: 参数,按顺序在js文本中对应arguments[0]、arguments[1]... + :param as_expr: 是否作为表达式运行,为True时args无效 + :param timeout: js超时时间(秒),为None则使用页面timeouts.script设置 + :return: 运行的结果 + """ + return self._run_js(script, *args, as_expr=as_expr, timeout=timeout) + + def _run_js(self, script, *args, as_expr=False, timeout=None): """运行javascript代码 :param script: js文本 :param args: 参数,按顺序在js文本中对应arguments[0]、arguments[1]... @@ -377,9 +381,9 @@ class ChromiumFrame(ChromiumBase): :return: 运行的结果 """ if script.startswith('this.scrollIntoView'): - return self.frame_ele.run_js(script, *args, as_expr=as_expr, timeout=timeout) + return self.frame_ele._run_js(script, *args, as_expr=as_expr, timeout=timeout) else: - return self.doc_ele.run_js(script, *args, as_expr=as_expr, timeout=timeout) + return self.doc_ele._run_js(script, *args, as_expr=as_expr, timeout=timeout) def parent(self, level_or_loc=1, index=1): """返回上面某一级父元素,可指定层数或用查询语法定位 @@ -542,12 +546,12 @@ class ChromiumFrame(ChromiumBase): img.style.setProperty("position","fixed"); arguments[0].insertBefore(img, this); return img;''' - new_ele = first_child.run_js(js, body) + new_ele = first_child._run_js(js, body) new_ele.scroll.to_see(center=True) top = int(self.frame_ele.style('border-top').split('px')[0]) left = int(self.frame_ele.style('border-left').split('px')[0]) - r = self.tab.run_cdp('Page.getLayoutMetrics')['visualViewport'] + r = self.tab._run_cdp('Page.getLayoutMetrics')['visualViewport'] sx = r['pageX'] sy = r['pageY'] r = self.tab.get_screenshot(path=path, name=name, as_bytes=as_bytes, as_base64=as_base64, @@ -561,7 +565,7 @@ class ChromiumFrame(ChromiumBase): :param locator: 定位符或元素对象 :param timeout: 查找超时时间(秒) :param index: 第几个结果,从1开始,可传入负数获取倒数第几个,为None返回所有 - :param relative: WebPage用的表示是否相对定位的参数 + :param relative: MixTab用的表示是否相对定位的参数 :param raise_err: 找不到元素是是否抛出异常,为None时根据全局设置 :return: ChromiumElement对象 """ @@ -573,4 +577,4 @@ class ChromiumFrame(ChromiumBase): def _is_inner_frame(self): """返回当前frame是否同域""" - return self._frame_id in str(self._target_page.run_cdp('Page.getFrameTree')['frameTree']) + return self._frame_id in str(self._target_page._run_cdp('Page.getFrameTree')['frameTree']) diff --git a/DrissionPage/_pages/chromium_frame.pyi b/DrissionPage/_pages/chromium_frame.pyi index b0c1a83..54e3594 100644 --- a/DrissionPage/_pages/chromium_frame.pyi +++ b/DrissionPage/_pages/chromium_frame.pyi @@ -9,10 +9,8 @@ from pathlib import Path from typing import Union, Tuple, List, Any, Optional 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 +from .tabs import ChromiumTab, MixTab +from .._elements.chromium_element import ChromiumElement, ShadowRoot from .._functions.elements import ChromiumElementsList from .._units.listener import FrameListener from .._units.rect import FrameRect @@ -25,13 +23,11 @@ from .._units.waiter import FrameWaiter class ChromiumFrame(ChromiumBase): def __init__(self, - owner: Union[ChromiumPage, WebPage, ChromiumTab, ChromiumFrame], + owner: Union[ChromiumTab, ChromiumFrame], ele: ChromiumElement, info: dict = None): - self._target_page: ChromiumBase = ... - self._page: ChromiumPage = ... - self.tab: Union[ChromiumPage, ChromiumTab] = ... - self._tab_id: str = ... + self._target_page: Union[ChromiumTab, ChromiumFrame] = ... + self._tab: Union[MixTab, ChromiumTab] = ... self._set: ChromiumFrameSetter = ... self._frame_ele: ChromiumElement = ... self._backend_id: int = ... @@ -40,7 +36,7 @@ class ChromiumFrame(ChromiumBase): self.doc_ele: ChromiumElement = ... self._states: FrameStates = ... self._reloading: bool = ... - self._rect: FrameRect = ... + self._rect: Optional[FrameRect] = ... self._listener: FrameListener = ... def __call__(self, @@ -56,7 +52,7 @@ class ChromiumFrame(ChromiumBase): def _d_set_runtime_settings(self) -> None: ... - def _driver_init(self, tab_id: str) -> None: ... + def _driver_init(self, target_id: str, is_init: bool = True) -> None: ... def _reload(self) -> None: ... @@ -66,9 +62,6 @@ class ChromiumFrame(ChromiumBase): def _onInspectorDetached(self, **kwargs): ... - @property - def page(self) -> Union[ChromiumPage, WebPage]: ... - @property def owner(self) -> ChromiumBase: ... @@ -126,12 +119,21 @@ class ChromiumFrame(ChromiumBase): @property def wait(self) -> FrameWaiter: ... + @property + def tab(self) -> Union[ChromiumTab, MixTab]: ... + @property def tab_id(self) -> str: ... @property def download_path(self) -> str: ... + @property + def sr(self) -> Union[None, ShadowRoot]: ... + + @property + def shadow_root(self) -> Union[None, ShadowRoot]: ... + def refresh(self) -> None: ... def property(self, name: str) -> Union[str, None]: ... @@ -146,6 +148,12 @@ class ChromiumFrame(ChromiumBase): as_expr: bool = False, timeout: float = None) -> Any: ... + def _run_js(self, + script: str, + *args, + as_expr: bool = False, + timeout: float = None) -> Any: ... + def parent(self, level_or_loc: Union[Tuple[str, str], str, int] = 1, index: int = 1) -> ChromiumElement: ... diff --git a/DrissionPage/_pages/chromium_page.py b/DrissionPage/_pages/chromium_page.py index 7bd7277..48bcbf9 100644 --- a/DrissionPage/_pages/chromium_page.py +++ b/DrissionPage/_pages/chromium_page.py @@ -5,23 +5,13 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ -from pathlib import Path -from threading import Lock -from time import sleep, perf_counter +from time import sleep -from requests import Session - -from .._base.browser import Browser -from .._configs.chromium_options import ChromiumOptions -from .._functions.browser import connect_browser -from .._functions.settings import Settings -from .._functions.tools import PortFinder +from .._base.browser import Chromium from .._functions.web import save_page -from .._pages.chromium_base import ChromiumBase, Timeout -from .._pages.chromium_tab import ChromiumTab +from .._pages.chromium_base import ChromiumBase from .._units.setter import ChromiumPageSetter from .._units.waiter import PageWaiter -from ..errors import BrowserConnectError class ChromiumPage(ChromiumBase): @@ -34,19 +24,16 @@ class ChromiumPage(ChromiumBase): :param tab_id: 要控制的标签页id,不指定默认为激活的 :param timeout: 超时时间(秒) """ - opt = handle_options(addr_or_opts) - is_exist, browser_id = run_browser(opt) - if browser_id in cls._PAGES: - r = cls._PAGES[browser_id] + browser = Chromium(addr_or_opts=addr_or_opts) + if browser.id in cls._PAGES: + r = cls._PAGES[browser.id] while not hasattr(r, '_frame_id'): sleep(.1) return r + r = object.__new__(cls) - r._chromium_options = opt - r._is_exist = is_exist - r._browser_id = browser_id - r.address = opt.address - cls._PAGES[browser_id] = r + r._browser = browser + cls._PAGES[browser.id] = r return r def __init__(self, addr_or_opts=None, tab_id=None, timeout=None): @@ -59,49 +46,19 @@ class ChromiumPage(ChromiumBase): return self._created = True - self._page = self self.tab = self - self._run_browser() - super().__init__(self.address, tab_id) + super().__init__(self.browser, tab_id) self._type = 'ChromiumPage' - self._lock = Lock() self.set.timeouts(base=timeout) - self._page_init() - - def _run_browser(self): - """连接浏览器""" - self._browser = Browser(self._chromium_options.address, self._browser_id, self) - r = self._browser.run_cdp('Browser.getVersion') - self._browser_version = r['product'] - if self._is_exist and self._chromium_options._headless is False and 'headless' in r['userAgent'].lower(): - self._browser.quit(3) - connect_browser(self._chromium_options) - s = Session() - s.trust_env = False - ws = s.get(f'http://{self._chromium_options.address}/json/version', headers={'Connection': 'close'}) - bid = ws.json()['webSocketDebuggerUrl'].split('/')[-1] - self._browser = Browser(self._chromium_options.address, bid, self) - ws.close() - s.close() + self._tab = self def _d_set_runtime_settings(self): """设置运行时用到的属性""" - self._timeouts = Timeout(self, page_load=self._chromium_options.timeouts['page_load'], - script=self._chromium_options.timeouts['script'], - base=self._chromium_options.timeouts['base']) - if self._chromium_options.timeouts['base'] is not None: - self._timeout = self._chromium_options.timeouts['base'] - self._load_mode = self._chromium_options.load_mode - self._download_path = None if self._chromium_options.download_path is None \ - else str(Path(self._chromium_options.download_path).absolute()) - self.retry_times = self._chromium_options.retry_times - self.retry_interval = self._chromium_options.retry_interval - - def _page_init(self): - """浏览器相关设置""" - self._browser.connect_to_page() - - # ----------挂件---------- + self._timeouts = self.browser.timeouts + self._load_mode = self.browser._load_mode + self._download_path = self.browser.download_path + self.retry_times = self.browser.retry_times + self.retry_interval = self.browser.retry_interval @property def set(self): @@ -138,7 +95,7 @@ class ChromiumPage(ChromiumBase): def latest_tab(self): """返回最新的标签页,最新标签页指最后创建或最后被激活的 当Settings.singleton_tab_obj==True时返回Tab对象,否则返回tab id""" - return self.get_tab(self.tab_ids[0], as_id=not Settings.singleton_tab_obj) + return self.browser.latest_tab @property def process_id(self): @@ -148,7 +105,12 @@ class ChromiumPage(ChromiumBase): @property def browser_version(self): """返回所控制的浏览器版本号""" - return self._browser_version + return self._browser.version + + @property + def address(self): + """返回浏览器地址ip:port""" + return self.browser.address def save(self, path=None, name=None, as_pdf=False, **kwargs): """把当前页面保存为文件,如果path和name参数都为None,只返回文本 @@ -169,34 +131,7 @@ class ChromiumPage(ChromiumBase): :param as_id: 是否返回标签页id而不是标签页对象 :return: ChromiumTab对象 """ - if id_or_num is not None: - if isinstance(id_or_num, str): - id_or_num = id_or_num - elif isinstance(id_or_num, int): - id_or_num = self.tab_ids[id_or_num - 1 if id_or_num > 0 else id_or_num] - elif isinstance(id_or_num, ChromiumTab): - if as_id: - return id_or_num.tab_id - elif Settings.singleton_tab_obj: - return id_or_num - else: - return self.get_tab(id_or_num.tab_id) - - elif title == url == tab_type is None: - id_or_num = self.tab_id - - else: - id_or_num = self._browser.find_tabs(title, url, tab_type) - if id_or_num: - id_or_num = id_or_num[0]['id'] - else: - return None - - if as_id: - return id_or_num - - with self._lock: - return ChromiumTab(self, id_or_num) + return self.browser.get_tab(id_or_num=id_or_num, title=title, url=url, tab_type=tab_type, as_id=as_id) def get_tabs(self, title=None, url=None, tab_type='page', as_id=False): """查找符合条件的tab,返回它们组成的列表 @@ -206,10 +141,7 @@ class ChromiumPage(ChromiumBase): :param as_id: 是否返回标签页id而不是标签页对象 :return: ChromiumTab对象组成的列表 """ - if as_id: - return [tab['id'] for tab in self._browser.find_tabs(title, url, tab_type)] - with self._lock: - return [ChromiumTab(self, tab['id']) for tab in self._browser.find_tabs(title, url, tab_type)] + return self.browser.get_tabs(title=title, url=url, tab_type=tab_type, as_id=as_id) def new_tab(self, url=None, new_window=False, background=False, new_context=False): """新建一个标签页 @@ -219,10 +151,14 @@ class ChromiumPage(ChromiumBase): :param new_context: 是否创建新的上下文 :return: 新标签页对象 """ - tab = ChromiumTab(self, tab_id=self.browser.new_tab(new_window, background, new_context)) - if url: - tab.get(url) - return tab + return self.browser.new_tab(url=url, new_window=new_window, background=background, new_context=new_context) + + def activate_tab(self, id_ind_tab): + """使标签页变为活动状态 + :param id_ind_tab: 标签页id(str)、Tab对象或标签页序号(int),序号从1开始 + :return: None + """ + self.browser.activate_tab(id_ind_tab) def close(self): """关闭Page管理的标签页""" @@ -234,32 +170,7 @@ class ChromiumPage(ChromiumBase): :param others: 是否关闭指定标签页之外的 :return: None """ - all_tabs = set(self.tab_ids) - if isinstance(tabs_or_ids, str): - tabs = {tabs_or_ids} - elif isinstance(tabs_or_ids, ChromiumTab): - tabs = {tabs_or_ids.tab_id} - elif tabs_or_ids is None: - tabs = {self.tab_id} - elif isinstance(tabs_or_ids, (list, tuple)): - tabs = set(i.tab_id if isinstance(i, ChromiumTab) else i for i in tabs_or_ids) - else: - raise TypeError('tabs_or_ids参数只能传入标签页对象或id。') - - if others: - tabs = all_tabs - tabs - - end_len = len(set(all_tabs) - set(tabs)) - if end_len <= 0: - self.quit() - return - - for tab in tabs: - self.browser.close_tab(tab) - sleep(.2) - end_time = perf_counter() + 3 - while self.tabs_count != end_len and perf_counter() < end_time: - sleep(.1) + self.browser.close_tabs(tabs_or_ids=tabs_or_ids, others=others) def quit(self, timeout=5, force=True): """关闭浏览器 @@ -271,69 +182,7 @@ class ChromiumPage(ChromiumBase): def _on_disconnect(self): """浏览器退出时执行""" - ChromiumPage._PAGES.pop(self._browser_id, None) + ChromiumPage._PAGES.pop(self._browser.id, None) def __repr__(self): return f'' - - -def handle_options(addr_or_opts): - """设置浏览器启动属性 - :param addr_or_opts: 'ip:port'、ChromiumOptions、Driver - :return: 返回ChromiumOptions对象 - """ - if not addr_or_opts: - _chromium_options = ChromiumOptions(addr_or_opts) - if _chromium_options.is_auto_port: - port, path = PortFinder(_chromium_options.tmp_path).get_port(_chromium_options.is_auto_port) - _chromium_options.set_address(f'127.0.0.1:{port}') - _chromium_options.set_user_data_path(path) - _chromium_options.auto_port(scope=_chromium_options.is_auto_port) - - elif isinstance(addr_or_opts, ChromiumOptions): - if addr_or_opts.is_auto_port: - port, path = PortFinder(addr_or_opts.tmp_path).get_port(addr_or_opts.is_auto_port) - addr_or_opts.set_address(f'127.0.0.1:{port}') - addr_or_opts.set_user_data_path(path) - addr_or_opts.auto_port(scope=addr_or_opts.is_auto_port) - _chromium_options = addr_or_opts - - elif isinstance(addr_or_opts, str): - _chromium_options = ChromiumOptions() - _chromium_options.set_address(addr_or_opts) - - elif isinstance(addr_or_opts, int): - _chromium_options = ChromiumOptions() - _chromium_options.set_local_port(addr_or_opts) - - else: - raise TypeError('只能接收ip:port格式或ChromiumOptions类型参数。') - - return _chromium_options - - -def run_browser(chromium_options): - """连接浏览器""" - is_exist = connect_browser(chromium_options) - try: - s = Session() - s.trust_env = False - ws = s.get(f'http://{chromium_options.address}/json/version', headers={'Connection': 'close'}) - if not ws: - raise BrowserConnectError('\n浏览器连接失败,如使用全局代理,须设置不代理127.0.0.1地址。') - browser_id = ws.json()['webSocketDebuggerUrl'].split('/')[-1] - ws.close() - s.close() - except KeyError: - raise BrowserConnectError('浏览器版本太旧或此浏览器不支持接管。') - except: - raise BrowserConnectError('\n浏览器连接失败,如使用全局代理,须设置不代理127.0.0.1地址。') - return is_exist, browser_id - - -def get_rename(original, rename): - if '.' in rename: - return rename - else: - suffix = original[original.rfind('.'):] if '.' in original else '' - return f'{rename}{suffix}' diff --git a/DrissionPage/_pages/chromium_page.pyi b/DrissionPage/_pages/chromium_page.pyi index 7117b9e..8178e46 100644 --- a/DrissionPage/_pages/chromium_page.pyi +++ b/DrissionPage/_pages/chromium_page.pyi @@ -6,13 +6,12 @@ @License : BSD 3-Clause. """ from pathlib import Path -from threading import Lock from typing import Union, Tuple, List, Optional -from .._base.browser import Browser +from .._base.browser import Chromium from .._configs.chromium_options import ChromiumOptions from .._pages.chromium_base import ChromiumBase -from .._pages.chromium_tab import ChromiumTab +from .._pages.tabs import ChromiumTab from .._units.rect import TabRect from .._units.setter import ChromiumPageSetter from .._units.waiter import PageWaiter @@ -20,6 +19,10 @@ from .._units.waiter import PageWaiter class ChromiumPage(ChromiumBase): _PAGES: dict = ... + tab: ChromiumPage = ... + _browser: Chromium = ... + _rect: Optional[TabRect] = ... + _is_exist: bool = ... def __new__(cls, addr_or_opts: Union[str, int, ChromiumOptions] = None, @@ -29,15 +32,7 @@ class ChromiumPage(ChromiumBase): def __init__(self, addr_or_opts: Union[str, int, ChromiumOptions] = None, tab_id: str = None, - timeout: float = None): - self.tab: ChromiumPage = ... - self._chromium_options: ChromiumOptions = ... - self._browser: Browser = ... - self._browser_id: str = ... - self._rect: Optional[TabRect] = ... - self._is_exist: bool = ... - self._lock: Lock = ... - self._browser_version: str = ... + timeout: float = None):... def _handle_options(self, addr_or_opts: Union[str, ChromiumOptions]) -> str: ... @@ -46,7 +41,7 @@ class ChromiumPage(ChromiumBase): def _page_init(self) -> None: ... @property - def browser(self) -> Browser: ... + def browser(self) -> Chromium: ... @property def tabs_count(self) -> int: ... @@ -66,6 +61,9 @@ class ChromiumPage(ChromiumBase): @property def browser_version(self) -> str: ... + @property + def address(self) -> str: ... + @property def set(self) -> ChromiumPageSetter: ... @@ -106,6 +104,8 @@ class ChromiumPage(ChromiumBase): def new_tab(self, url: str = None, new_window: bool = False, background: bool = False, new_context: bool = False) -> ChromiumTab: ... + def activate_tab(self, id_ind_tab: Union[int, str, ChromiumTab]) -> None: ... + def close(self) -> None: ... def close_tabs(self, tabs_or_ids: Union[str, ChromiumTab, List[Union[str, ChromiumTab]], @@ -114,12 +114,3 @@ class ChromiumPage(ChromiumBase): def quit(self, timeout: float = 5, force: bool = True) -> None: ... def _on_disconnect(self) -> None: ... - - -def handle_options(addr_or_opts): ... - - -def run_browser(chromium_options): ... - - -def get_rename(original: str, rename: str) -> str: ... diff --git a/DrissionPage/_pages/web_page.py b/DrissionPage/_pages/mix_page.py similarity index 86% rename from DrissionPage/_pages/web_page.py rename to DrissionPage/_pages/mix_page.py index ea9e83d..e4e834c 100644 --- a/DrissionPage/_pages/web_page.py +++ b/DrissionPage/_pages/mix_page.py @@ -6,15 +6,14 @@ @License : BSD 3-Clause. """ from .chromium_page import ChromiumPage -from .chromium_tab import WebPageTab from .session_page import SessionPage from .._base.base import BasePage from .._configs.chromium_options import ChromiumOptions -from .._functions.web import set_session_cookies, set_browser_cookies -from .._units.setter import WebPageSetter +from .._functions.cookies import set_session_cookies, set_tab_cookies +from .._units.setter import MixPageSetter -class WebPage(SessionPage, ChromiumPage, BasePage): +class MixPage(SessionPage, ChromiumPage, BasePage): """整合浏览器和request的页面类""" def __new__(cls, mode='d', timeout=None, chromium_options=None, session_or_options=None): @@ -26,7 +25,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): """ return super().__new__(cls, chromium_options) - def __init__(self, mode='d', timeout=None, chromium_options=None, session_or_options=None, driver_or_options=None): + def __init__(self, mode='d', timeout=None, chromium_options=None, session_or_options=None): """初始化函数 :param mode: 'd' 或 's',即driver模式和session模式 :param timeout: 超时时间(秒),d模式时为寻找元素时间,s模式时为连接时间,默认10秒 @@ -47,7 +46,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): chromium_options = ChromiumOptions(read_file=chromium_options) chromium_options.set_timeouts(base=self._timeout).set_paths(download_path=self.download_path) super(SessionPage, self).__init__(addr_or_opts=chromium_options, timeout=timeout) - self._type = 'WebPage' + self._type = 'MixPage' self.change_mode(self._mode, go=False, copy_cookies=False) def __call__(self, locator, index=1, timeout=None): @@ -67,7 +66,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): def set(self): """返回用于设置的对象""" if self._set is None: - self._set = WebPageSetter(self) + self._set = MixPageSetter(self) return self._set @property @@ -148,15 +147,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): @property def timeout(self): """返回通用timeout设置""" - return self.timeouts.base - - @timeout.setter - def timeout(self, second): - """设置通用超时时间 - :param second: 秒数 - :return: None - """ - self.set.timeouts(base=second) + return self._timeout if self._mode == 's' else self.timeouts.base def get(self, url, show_errmsg=False, retry=None, interval=None, timeout=None, **kwargs): """跳转到一个url @@ -284,7 +275,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): return if copy_user_agent: - user_agent = self.run_cdp('Runtime.evaluate', expression='navigator.userAgent;')['result']['value'] + user_agent = self._run_cdp('Runtime.evaluate', expression='navigator.userAgent;')['result']['value'] self._headers.update({"User-Agent": user_agent}) set_session_cookies(self.session, super(SessionPage, self).cookies()) @@ -293,19 +284,18 @@ class WebPage(SessionPage, ChromiumPage, BasePage): """把session对象的cookies复制到浏览器""" if not self._has_driver: return - set_browser_cookies(self, super().cookies()) + set_tab_cookies(self, super().cookies()) - def cookies(self, as_dict=False, all_domains=False, all_info=False): + def cookies(self, all_domains=False, all_info=False): """返回cookies - :param as_dict: 为True时以dict格式返回,为False时返回list且all_info无效 :param all_domains: 是否返回所有域的cookies :param all_info: 是否返回所有信息,False则只返回name、value、domain :return: cookies信息 """ if self._mode == 's': - return super().cookies(as_dict, all_domains, all_info) + return super().cookies(all_domains, all_info) elif self._mode == 'd': - return super(SessionPage, self).cookies(as_dict, all_domains, all_info) + return super(SessionPage, self).cookies(all_domains, all_info) def get_tab(self, id_or_num=None, title=None, url=None, tab_type='page', as_id=False): """获取一个标签页对象,id_or_num不为None时,后面几个参数无效 @@ -314,31 +304,10 @@ class WebPage(SessionPage, ChromiumPage, BasePage): :param url: 要匹配url的文本,模糊匹配,为None则匹配所有 :param tab_type: tab类型,可用列表输入多个,如 'page', 'iframe' 等,为None则匹配所有 :param as_id: 是否返回标签页id而不是标签页对象 - :return: WebPageTab对象 + :return: MixTab对象 """ - if id_or_num is not None: - if isinstance(id_or_num, str): - id_or_num = id_or_num - elif isinstance(id_or_num, int): - id_or_num = self.tab_ids[id_or_num - 1 if id_or_num > 0 else id_or_num] - elif isinstance(id_or_num, WebPageTab): - return id_or_num.tab_id if as_id else id_or_num - - elif title == url == tab_type is None: - id_or_num = self.tab_id - - else: - id_or_num = self._browser.find_tabs(title, url, tab_type) - if id_or_num: - id_or_num = id_or_num[0]['id'] - else: - return None - - if as_id: - return id_or_num - - with self._lock: - return WebPageTab(self, id_or_num) + return self.browser._get_tab(id_or_num=id_or_num, title=title, url=url, + tab_type=tab_type, mix=True, as_id=as_id) def get_tabs(self, title=None, url=None, tab_type='page', as_id=False): """查找符合条件的tab,返回它们组成的列表 @@ -348,10 +317,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): :param as_id: 是否返回标签页id而不是标签页对象 :return: ChromiumTab对象组成的列表 """ - if as_id: - return [tab['id'] for tab in self._browser.find_tabs(title, url, tab_type)] - with self._lock: - return [WebPageTab(self, tab['id']) for tab in self._browser.find_tabs(title, url, tab_type)] + return self.browser._get_tabs(title=title, url=url, tab_type=tab_type, mix=True, as_id=as_id) def new_tab(self, url=None, new_window=False, background=False, new_context=False): """新建一个标签页 @@ -361,10 +327,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): :param new_context: 是否创建新的上下文 :return: 新标签页对象 """ - tab = WebPageTab(self, tab_id=self.browser.new_tab(new_window, background, new_context)) - if url: - tab.get(url) - return tab + return self.browser.new_mix_tab(url=url, new_window=new_window, background=background, new_context=new_context) def close_driver(self): """关闭driver及浏览器""" @@ -403,7 +366,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): :param locator: 元素的定位信息,可以是元素对象,loc元组,或查询字符串 :param timeout: 查找元素超时时间(秒),d模式专用 :param index: 第几个结果,从1开始,可传入负数获取倒数第几个,为None返回所有 - :param relative: WebPage用的表示是否相对定位的参数 + :param relative: MixTab用的表示是否相对定位的参数 :param raise_err: 找不到元素是是否抛出异常,为None时根据全局设置 :return: 元素对象或属性、文本节点文本 """ @@ -429,4 +392,4 @@ class WebPage(SessionPage, ChromiumPage, BasePage): self._has_driver = None def __repr__(self): - return f'' + return f'' diff --git a/DrissionPage/_pages/web_page.pyi b/DrissionPage/_pages/mix_page.pyi similarity index 90% rename from DrissionPage/_pages/web_page.pyi rename to DrissionPage/_pages/mix_page.pyi index e1046d8..7d335da 100644 --- a/DrissionPage/_pages/web_page.pyi +++ b/DrissionPage/_pages/mix_page.pyi @@ -11,7 +11,7 @@ from requests import Session, Response from .chromium_frame import ChromiumFrame from .chromium_page import ChromiumPage -from .chromium_tab import WebPageTab +from .tabs import MixTab from .session_page import SessionPage from .._base.base import BasePage from .._base.driver import Driver @@ -20,10 +20,10 @@ from .._configs.session_options import SessionOptions from .._elements.chromium_element import ChromiumElement from .._elements.session_element import SessionElement from .._functions.elements import SessionElementsList, ChromiumElementsList -from .._units.setter import WebPageSetter +from .._units.setter import MixPageSetter -class WebPage(SessionPage, ChromiumPage, BasePage): +class MixPage(SessionPage, ChromiumPage, BasePage): def __init__(self, mode: str = 'd', @@ -31,7 +31,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage): chromium_options: Union[ChromiumOptions, bool] = None, session_or_options: Union[Session, SessionOptions, bool] = None) -> None: self._mode: str = ... - self._set: WebPageSetter = ... + self._set: MixPageSetter = ... self._has_driver: bool = ... self._has_session: bool = ... self._session_options: Union[SessionOptions, None] = ... @@ -79,9 +79,6 @@ class WebPage(SessionPage, ChromiumPage, BasePage): @property def timeout(self) -> float: ... - @timeout.setter - def timeout(self, second: float) -> None: ... - def get(self, url: str, show_errmsg: bool = False, @@ -124,28 +121,27 @@ class WebPage(SessionPage, ChromiumPage, BasePage): def cookies_to_browser(self) -> None: ... def cookies(self, - as_dict: bool = False, all_domains: bool = False, all_info: bool = False) -> Union[dict, list]: ... def get_tab(self, - id_or_num: Union[str, WebPageTab, int] = None, + id_or_num: Union[str, MixTab, int] = None, title: str = None, url: str = None, tab_type: Union[str, list, tuple] = 'page', - as_id: bool = False) -> Union[WebPageTab, str, None]: ... + as_id: bool = False) -> Union[MixTab, str, None]: ... def get_tabs(self, title: str = None, url: str = None, tab_type: Union[str, list, tuple] = 'page', - as_id: bool = False) -> Union[List[WebPageTab], List[str]]: ... + as_id: bool = False) -> Union[List[MixTab], List[str]]: ... def new_tab(self, url: str = None, new_window: bool = False, background: bool = False, - new_context: bool = False) -> WebPageTab: ... + new_context: bool = False) -> MixTab: ... def close_driver(self) -> None: ... @@ -175,10 +171,10 @@ class WebPage(SessionPage, ChromiumPage, BasePage): cert: Any | None = ...) -> Union[bool, Response]: ... @property - def latest_tab(self) -> Union[WebPageTab, WebPage]: ... + def latest_tab(self) -> Union[MixTab, MixPage]: ... @property - def set(self) -> WebPageSetter: ... + def set(self) -> MixPageSetter: ... def _find_elements(self, locator: Union[Tuple[str, str], str, ChromiumElement, SessionElement, ChromiumFrame], diff --git a/DrissionPage/_pages/session_page.py b/DrissionPage/_pages/session_page.py index 94a7d07..4e39598 100644 --- a/DrissionPage/_pages/session_page.py +++ b/DrissionPage/_pages/session_page.py @@ -18,17 +18,17 @@ from tldextract import extract from .._base.base import BasePage from .._configs.session_options import SessionOptions from .._elements.session_element import SessionElement, make_session_ele -from .._functions.web import cookie_to_dict, format_headers +from .._functions.cookies import cookie_to_dict, CookiesList +from .._functions.web import format_headers from .._units.setter import SessionPageSetter class SessionPage(BasePage): """SessionPage封装了页面操作的常用功能,使用requests来获取、解析网页""" - def __init__(self, session_or_options=None, timeout=None): + def __init__(self, session_or_options=None): """ :param session_or_options: Session对象或SessionOptions对象 - :param timeout: 连接超时时间(秒),为None时从ini文件读取或默认10 """ super(SessionPage, SessionPage).__init__(self) self._headers = None @@ -38,11 +38,10 @@ class SessionPage(BasePage): self._encoding = None self._type = 'SessionPage' self._page = self + self._timeout = 10 self._s_set_start_options(session_or_options) self._s_set_runtime_settings() self._create_session() - if timeout is not None: - self.timeout = timeout def _s_set_start_options(self, session_or_options): """启动配置 @@ -64,8 +63,7 @@ class SessionPage(BasePage): def _s_set_runtime_settings(self): """设置运行时用到的属性""" self._timeout = self._session_options.timeout - self._download_path = None if self._session_options.download_path is None \ - else str(Path(self._session_options.download_path).absolute()) + self._download_path = str(Path(self._session_options.download_path or '.').absolute()) self.retry_times = self._session_options.retry_times self.retry_interval = self._session_options.retry_interval @@ -146,6 +144,11 @@ class SessionPage(BasePage): self._set = SessionPageSetter(self) return self._set + @property + def timeout(self): + """返回超时设置""" + return self._timeout + def get(self, url, show_errmsg=False, retry=None, interval=None, timeout=None, **kwargs): """用get方式跳转到url,可输入文件路径 :param url: 目标url,可指定本地文件路径 @@ -220,9 +223,8 @@ class SessionPage(BasePage): """ return locator if isinstance(locator, SessionElement) else make_session_ele(self, locator, index=index) - def cookies(self, as_dict=False, all_domains=False, all_info=False): + def cookies(self, all_domains=False, all_info=False): """返回cookies - :param as_dict: 为True时以dict格式返回,为False时返回list且all_info无效 :param all_domains: 是否返回所有域的cookies :param all_info: 是否返回所有信息,False则只返回name、value、domain :return: cookies信息 @@ -233,21 +235,20 @@ class SessionPage(BasePage): if self.url: ex_url = extract(self._session_url) domain = f'{ex_url.domain}.{ex_url.suffix}' if ex_url.suffix else ex_url.domain - - cookies = tuple(x for x in self.session.cookies if domain in x.domain or x.domain == '') + cookies = tuple(c for c in self.session.cookies if domain in c.domain or c.domain == '') else: - cookies = tuple(x for x in self.session.cookies) + cookies = tuple(c for c in self.session.cookies) - if as_dict: - return {x.name: x.value for x in cookies} - elif all_info: - return [cookie_to_dict(cookie) for cookie in cookies] + if all_info: + r = CookiesList() + for c in cookies: + r.append(cookie_to_dict(c)) else: - r = [] + r = CookiesList() for c in cookies: c = cookie_to_dict(c) r.append({'name': c['name'], 'value': c['value'], 'domain': c['domain']}) - return r + return r def close(self): """关闭Session对象""" diff --git a/DrissionPage/_pages/session_page.pyi b/DrissionPage/_pages/session_page.pyi index 16f95be..cec9098 100644 --- a/DrissionPage/_pages/session_page.pyi +++ b/DrissionPage/_pages/session_page.pyi @@ -14,21 +14,21 @@ from requests.structures import CaseInsensitiveDict from .._base.base import BasePage from .._configs.session_options import SessionOptions from .._elements.session_element import SessionElement +from .._functions.cookies import CookiesList from .._functions.elements import SessionElementsList from .._units.setter import SessionPageSetter class SessionPage(BasePage): def __init__(self, - session_or_options: Union[Session, SessionOptions] = None, - timeout: float = None): + session_or_options: Union[Session, SessionOptions] = None): self._headers: Optional[CaseInsensitiveDict] = ... self._session: Session = ... self._session_options: SessionOptions = ... self._url: str = ... self._response: Response = ... self._url_available: bool = ... - self.timeout: float = ... + self._timeout: float = ... self.retry_times: int = ... self.retry_interval: float = ... self._set: SessionPageSetter = ... @@ -114,9 +114,8 @@ class SessionPage(BasePage): raise_err: bool = None) -> Union[SessionElement, SessionElementsList]: ... def cookies(self, - as_dict: bool = False, all_domains: bool = False, - all_info: bool = False) -> Union[dict, list]: ... + all_info: bool = False) -> CookiesList: ... # ----------------session独有属性和方法----------------------- @property @@ -131,6 +130,9 @@ class SessionPage(BasePage): @property def set(self) -> SessionPageSetter: ... + @property + def timeout(self) -> float: ... + def post(self, url: str, show_errmsg: bool = False, diff --git a/DrissionPage/_pages/chromium_tab.py b/DrissionPage/_pages/tabs.py similarity index 84% rename from DrissionPage/_pages/chromium_tab.py rename to DrissionPage/_pages/tabs.py index d965a5d..38188a2 100644 --- a/DrissionPage/_pages/chromium_tab.py +++ b/DrissionPage/_pages/tabs.py @@ -10,11 +10,12 @@ from time import sleep from .._base.base import BasePage from .._configs.session_options import SessionOptions +from .._functions.cookies import set_session_cookies, set_tab_cookies from .._functions.settings import Settings -from .._functions.web import set_session_cookies, set_browser_cookies, save_page +from .._functions.web import save_page from .._pages.chromium_base import ChromiumBase from .._pages.session_page import SessionPage -from .._units.setter import TabSetter, WebPageTabSetter +from .._units.setter import TabSetter, MixTabSetter from .._units.waiter import TabWaiter @@ -22,10 +23,10 @@ class ChromiumTab(ChromiumBase): """实现浏览器标签页的类""" _TABS = {} - def __new__(cls, page, tab_id): + def __new__(cls, browser, tab_id): """ - :param page: ChromiumPage对象 - :param tab_id: 要控制的标签页id + :param browser: Browser对象 + :param tab_id: 标签页id """ if Settings.singleton_tab_obj and tab_id in cls._TABS: r = cls._TABS[tab_id] @@ -36,38 +37,30 @@ class ChromiumTab(ChromiumBase): cls._TABS[tab_id] = r return r - def __init__(self, page, tab_id): + def __init__(self, browser, tab_id): """ - :param page: ChromiumPage对象 - :param tab_id: 要控制的标签页id + :param browser: Browser对象 + :param tab_id: 标签页id """ if Settings.singleton_tab_obj and hasattr(self, '_created'): return self._created = True - self._page = page - self.tab = self - self._browser = page.browser - super().__init__(page.address, tab_id, page.timeout) - self._rect = None + super().__init__(browser, tab_id) + self._tab = self self._type = 'ChromiumTab' def _d_set_runtime_settings(self): """重写设置浏览器运行参数方法""" - self._timeouts = copy(self.page.timeouts) - self.retry_times = self.page.retry_times - self.retry_interval = self.page.retry_interval - self._load_mode = self.page._load_mode - self._download_path = self.page.download_path + self._timeouts = copy(self.browser.timeouts) + self.retry_times = self.browser.retry_times + self.retry_interval = self.browser.retry_interval + self._load_mode = self.browser._load_mode + self._download_path = self.browser.download_path def close(self): """关闭当前标签页""" - self.page.close_tabs(self.tab_id) - - @property - def page(self): - """返回总体page对象""" - return self._page + self.browser.close_tabs(self.tab_id) @property def set(self): @@ -100,11 +93,11 @@ class ChromiumTab(ChromiumBase): ChromiumTab._TABS.pop(self.tab_id, None) -class WebPageTab(SessionPage, ChromiumTab, BasePage): - def __init__(self, page, tab_id): +class MixTab(SessionPage, ChromiumTab, BasePage): + def __init__(self, browser, tab_id): """ - :param page: WebPage对象 - :param tab_id: 要控制的标签页id + :param browser: Chromium对象 + :param tab_id: 标签页id """ if Settings.singleton_tab_obj and hasattr(self, '_created'): return @@ -112,10 +105,9 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage): self._mode = 'd' self._has_driver = True self._has_session = True - super().__init__(session_or_options=SessionOptions(read_file=False).from_session(copy(page.session), - page._headers)) - super(SessionPage, self).__init__(page=page, tab_id=tab_id) - self._type = 'WebPageTab' + super().__init__(session_or_options=browser._session_options if browser._session_options else SessionOptions()) + super(SessionPage, self).__init__(browser=browser, tab_id=tab_id) + self._type = 'MixTab' def __call__(self, locator, index=1, timeout=None): """在内部查找元素 @@ -134,7 +126,7 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage): def set(self): """返回用于设置的对象""" if self._set is None: - self._set = WebPageTabSetter(self) + self._set = MixTabSetter(self) return self._set @property @@ -215,15 +207,7 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage): @property def timeout(self): """返回通用timeout设置""" - return self.timeouts.base - - @timeout.setter - def timeout(self, second): - """设置通用超时时间 - :param second: 秒数 - :return: None - """ - self.set.timeouts(base=second) + return self._timeout if self._mode == 's' else self.timeouts.base def get(self, url, show_errmsg=False, retry=None, interval=None, timeout=None, **kwargs): """跳转到一个url @@ -318,7 +302,9 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage): # s模式转d模式 if self._mode == 'd': if self._driver is None: - self._connect_browser(self.page._chromium_options) + tabs = self.browser.tab_ids + tid = self.tab_id if self.tab_id in tabs else tabs[0] + self._connect_browser(tid) self._url = None if not self._has_driver else super(SessionPage, self).url self._has_driver = True @@ -353,7 +339,7 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage): return if copy_user_agent: - user_agent = self.run_cdp('Runtime.evaluate', expression='navigator.userAgent;')['result']['value'] + user_agent = self._run_cdp('Runtime.evaluate', expression='navigator.userAgent;')['result']['value'] self._headers.update({"User-Agent": user_agent}) set_session_cookies(self.session, super(SessionPage, self).cookies()) @@ -362,23 +348,22 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage): """把session对象的cookies复制到浏览器""" if not self._has_driver: return - set_browser_cookies(self, super().cookies()) + set_tab_cookies(self, super().cookies()) - def cookies(self, as_dict=False, all_domains=False, all_info=False): + def cookies(self, all_domains=False, all_info=False): """返回cookies - :param as_dict: 为True时以dict格式返回,为False时返回list且all_info无效 :param all_domains: 是否返回所有域的cookies :param all_info: 是否返回所有信息,False则只返回name、value、domain :return: cookies信息 """ if self._mode == 's': - return super().cookies(as_dict, all_domains, all_info) + return super().cookies(all_domains, all_info) elif self._mode == 'd': - return super(SessionPage, self).cookies(as_dict, all_domains, all_info) + return super(SessionPage, self).cookies(all_domains, all_info) def close(self): """关闭当前标签页""" - self.page.close_tabs(self.tab_id) + self.browser.close_tabs(self.tab_id) self._session.close() if self._response is not None: self._response.close() @@ -388,7 +373,7 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage): :param locator: 元素的定位信息,可以是元素对象,loc元组,或查询字符串 :param timeout: 查找元素超时时间(秒),d模式专用 :param index: 第几个结果,从1开始,可传入负数获取倒数第几个,为None返回所有 - :param relative: WebPage用的表示是否相对定位的参数 + :param relative: MixTab用的表示是否相对定位的参数 :param raise_err: 找不到元素是是否抛出异常,为None时根据全局设置 :return: 元素对象或属性、文本节点文本 """ @@ -398,4 +383,4 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage): return super(SessionPage, self)._find_elements(locator, timeout=timeout, index=index, relative=relative) def __repr__(self): - return f'' + return f'' diff --git a/DrissionPage/_pages/chromium_tab.pyi b/DrissionPage/_pages/tabs.pyi similarity index 85% rename from DrissionPage/_pages/chromium_tab.pyi rename to DrissionPage/_pages/tabs.pyi index 2161070..3bba12b 100644 --- a/DrissionPage/_pages/chromium_tab.pyi +++ b/DrissionPage/_pages/tabs.pyi @@ -12,35 +12,30 @@ from requests import Session, Response from .chromium_base import ChromiumBase from .chromium_frame import ChromiumFrame -from .chromium_page import ChromiumPage from .session_page import SessionPage -from .web_page import WebPage -from .._base.browser import Browser +from .._base.browser import Chromium from .._elements.chromium_element import ChromiumElement from .._elements.session_element import SessionElement +from .._functions.cookies import CookiesList from .._functions.elements import SessionElementsList, ChromiumElementsList from .._units.rect import TabRect -from .._units.setter import TabSetter, WebPageTabSetter +from .._units.setter import TabSetter, MixTabSetter from .._units.waiter import TabWaiter class ChromiumTab(ChromiumBase): _TABS: dict = ... - def __new__(cls, page: ChromiumPage, tab_id: str): ... + def __new__(cls, browser: Chromium, tab_id: str): ... - def __init__(self, page: ChromiumPage, tab_id: str): - self._page: ChromiumPage = ... - self._browser: Browser = ... + def __init__(self, browser: Chromium, tab_id: str): + self._tab: ChromiumTab = ... self._rect: Optional[TabRect] = ... def _d_set_runtime_settings(self) -> None: ... def close(self) -> None: ... - @property - def page(self) -> ChromiumPage: ... - @property def set(self) -> TabSetter: ... @@ -69,22 +64,19 @@ class ChromiumTab(ChromiumBase): generateDocumentOutline: bool = ...) -> Union[bytes, str]: ... -class WebPageTab(SessionPage, ChromiumTab): - def __init__(self, page: WebPage, tab_id: str): - self._page: WebPage = ... - self._browser: Browser = ... - self._mode: str = ... - self._has_driver = ... - self._has_session = ... +class MixTab(SessionPage, ChromiumTab): + _tab: MixTab = ... + _mode: str = ... + _has_driver: bool = ... + _has_session: bool = ... + + def __init__(self, browser: Chromium, tab_id: str): ... def __call__(self, locator: Union[Tuple[str, str], str, ChromiumElement, SessionElement], index: int = 1, timeout: float = None) -> Union[ChromiumElement, SessionElement]: ... - @property - def page(self) -> WebPage: ... - @property def url(self) -> Union[str, None]: ... @@ -121,9 +113,6 @@ class WebPageTab(SessionPage, ChromiumTab): @property def timeout(self) -> float: ... - @timeout.setter - def timeout(self, second: float) -> None: ... - def get(self, url: str, show_errmsg: bool = False, @@ -165,8 +154,7 @@ class WebPageTab(SessionPage, ChromiumTab): def cookies_to_browser(self) -> None: ... - def cookies(self, as_dict: bool = False, all_domains: bool = False, - all_info: bool = False) -> Union[dict, list]: ... + def cookies(self, all_domains: bool = False, all_info: bool = False) -> CookiesList: ... def close(self) -> None: ... @@ -192,7 +180,7 @@ class WebPageTab(SessionPage, ChromiumTab): cert: Any | None = ...) -> Union[bool, Response]: ... @property - def set(self) -> WebPageTabSetter: ... + def set(self) -> MixTabSetter: ... def _find_elements(self, locator: Union[Tuple[str, str], str, ChromiumElement, SessionElement, ChromiumFrame], diff --git a/DrissionPage/_units/actions.py b/DrissionPage/_units/actions.py index e1c951c..c92aa90 100644 --- a/DrissionPage/_units/actions.py +++ b/DrissionPage/_units/actions.py @@ -7,8 +7,7 @@ """ from time import sleep, perf_counter -from ..errors import AlertExistsError -from .._functions.keys import modifierBit, keyDescriptionForString, input_text_or_keys, Keys, keyDefinitions +from .._functions.keys import modifierBit, make_input_data, input_text_or_keys, Keys from .._functions.web import location_in_viewport @@ -26,7 +25,7 @@ class Actions: self.curr_y = 0 self._holding = 'left' - def move_to(self, ele_or_loc, offset_x=0, offset_y=0, duration=.5): + def move_to(self, ele_or_loc, offset_x=None, offset_y=None, duration=.5): """鼠标移动到元素中点,或页面上的某个绝对坐标。可设置偏移量 当带偏移量时,偏移量相对于元素左上角坐标 :param ele_or_loc: 元素对象、绝对坐标或文本定位符,坐标为tuple(int, int)形式 @@ -36,6 +35,11 @@ class Actions: :return: self """ is_loc = False + mid_point = offset_x == offset_y is None + if offset_x is None: + offset_x = 0 + if offset_y is None: + offset_y = 0 if isinstance(ele_or_loc, (tuple, list)): is_loc = True lx = ele_or_loc[0] + offset_x @@ -43,7 +47,7 @@ class Actions: elif isinstance(ele_or_loc, str) or ele_or_loc._type == 'ChromiumElement': ele_or_loc = self.owner(ele_or_loc) self.owner.scroll.to_see(ele_or_loc) - x, y = ele_or_loc.rect.location if offset_x or offset_y else ele_or_loc.rect.midpoint + x, y = ele_or_loc.rect.midpoint if mid_point else ele_or_loc.rect.location lx = x + offset_x ly = y + offset_y else: @@ -51,16 +55,15 @@ class Actions: if not location_in_viewport(self.owner, lx, ly): # 把坐标滚动到页面中间 - clientWidth = self.owner.run_js('return document.body.clientWidth;') - clientHeight = self.owner.run_js('return document.body.clientHeight;') + clientWidth = self.owner._run_js('return document.body.clientWidth;') + clientHeight = self.owner._run_js('return document.body.clientHeight;') self.owner.scroll.to_location(lx - clientWidth // 2, ly - clientHeight // 2) # 这样设计为了应付那些不随滚动条滚动的元素 if is_loc: cx, cy = location_to_client(self.owner, lx, ly) else: - x, y = ele_or_loc.rect.viewport_location if offset_x or offset_y \ - else ele_or_loc.rect.viewport_midpoint + x, y = ele_or_loc.rect.viewport_midpoint if mid_point else ele_or_loc.rect.viewport_location cx = x + offset_x cy = y + offset_y @@ -95,36 +98,31 @@ class Actions: return self - def click(self, on_ele=None): + def click(self, on_ele=None, times=1): """点击鼠标左键,可先移动到元素上 :param on_ele: ChromiumElement元素或文本定位符 + :param times: 点击次数 :return: self """ - self._hold(on_ele, 'left').wait(.05)._release('left') + self._hold(on_ele, 'left', times).wait(.05)._release('left') return self - def r_click(self, on_ele=None): + def r_click(self, on_ele=None, times=1): """点击鼠标右键,可先移动到元素上 :param on_ele: ChromiumElement元素或文本定位符 + :param times: 点击次数 :return: self """ - self._hold(on_ele, 'right').wait(.05)._release('right') + self._hold(on_ele, 'right', times).wait(.05)._release('right') return self - def m_click(self, on_ele=None): + def m_click(self, on_ele=None, times=1): """点击鼠标中键,可先移动到元素上 :param on_ele: ChromiumElement元素或文本定位符 + :param times: 点击次数 :return: self """ - self._hold(on_ele, 'middle').wait(.05)._release('middle') - return self - - def db_click(self, on_ele=None): - """双击鼠标左键,可先移动到元素上 - :param on_ele: ChromiumElement元素或文本定位符 - :return: self - """ - self._hold(on_ele, 'left', 2).wait(.05)._release('left') + self._hold(on_ele, 'middle', times).wait(.05)._release('middle') return self def hold(self, on_ele=None): @@ -256,9 +254,10 @@ class Actions: self.modifier |= modifierBit.get(key, 0) return self - data = self._get_key_data(key, 'keyDown') - data['_ignore'] = AlertExistsError - self.owner.run_cdp('Input.dispatchKeyEvent', **data) + data = make_input_data(self.modifier, key, False) + if not data: + raise ValueError(f'没有这个按键:{key}') + self.owner._run_cdp('Input.dispatchKeyEvent', **data) return self def key_up(self, key): @@ -271,9 +270,10 @@ class Actions: self.modifier ^= modifierBit.get(key, 0) return self - data = self._get_key_data(key, 'keyUp') - data['_ignore'] = AlertExistsError - self.owner.run_cdp('Input.dispatchKeyEvent', **data) + data = make_input_data(self.modifier, key, True) + if not data: + raise ValueError(f'没有这个按键:{key}') + self.owner._run_cdp('Input.dispatchKeyEvent', **data) return self def type(self, keys): @@ -282,17 +282,22 @@ class Actions: :return: self """ modifiers = [] + if not isinstance(keys, (str, tuple, list)): + keys = str(keys) for i in keys: for character in i: - if character in keyDefinitions: - self.key_down(character) - if character in ('\ue009', '\ue008', '\ue00a', '\ue03d'): - modifiers.append(character) - else: - self.key_up(character) + if character in ('\ue009', '\ue008', '\ue00a', '\ue03d'): + self.modifier |= modifierBit.get(character, 0) + modifiers.append(character) + data = make_input_data(self.modifier, character, False) + if data: + self.owner._run_cdp('Input.dispatchKeyEvent', **data) + if character not in ('\ue009', '\ue008', '\ue00a', '\ue03d'): + data['type'] = 'keyUp' + self.owner._run_cdp('Input.dispatchKeyEvent', **data) else: - self.owner.run_cdp('Input.dispatchKeyEvent', type='char', text=character) + self.owner._run_cdp('Input.dispatchKeyEvent', type='char', text=character) for m in modifiers: self.key_up(m) @@ -315,30 +320,9 @@ class Actions: self.owner.wait(second=second, scope=scope) return self - def _get_key_data(self, key, action): - """获取用于发送的按键信息 - :param key: 按键 - :param action: 'keyDown' 或 'keyUp' - :return: 按键信息 - """ - description = keyDescriptionForString(self.modifier, key) - text = description['text'] - if action != 'keyUp': - action = 'keyDown' if text else 'rawKeyDown' - return {'type': action, - 'modifiers': self.modifier, - 'windowsVirtualKeyCode': description['keyCode'], - 'code': description['code'], - 'key': description['key'], - 'text': text, - 'autoRepeat': False, - 'unmodifiedText': text, - 'location': description['location'], - 'isKeypad': description['location'] == 3} - def location_to_client(page, lx, ly): """绝对坐标转换为视口坐标""" - scroll_x = page.run_js('return document.documentElement.scrollLeft;') - scroll_y = page.run_js('return document.documentElement.scrollTop;') + scroll_x = page._run_js('return document.documentElement.scrollLeft;') + scroll_y = page._run_js('return document.documentElement.scrollTop;') return lx - scroll_x, ly - scroll_y diff --git a/DrissionPage/_units/actions.pyi b/DrissionPage/_units/actions.pyi index bc610eb..ad8f54a 100644 --- a/DrissionPage/_units/actions.pyi +++ b/DrissionPage/_units/actions.pyi @@ -11,19 +11,19 @@ from .._base.driver import Driver from .._elements.chromium_element import ChromiumElement from .._pages.chromium_base import ChromiumBase -KEYS = Literal['NULL', 'CANCEL', 'HELP', 'BACKSPACE', 'BACK_SPACE', 'meta', -'TAB', 'CLEAR', 'RETURN', 'ENTER', 'SHIFT', 'LEFT_SHIFT', 'CONTROL', 'command ', -'CTRL', 'LEFT_CONTROL', 'ALT', 'LEFT_ALT', 'PAUSE', 'ESCAPE', 'SPACE', -'PAGE_UP', 'PAGE_DOWN', 'END', 'HOME', 'LEFT', 'ARROW_LEFT', 'UP', -'ARROW_UP', 'RIGHT', 'ARROW_RIGHT', 'DOWN', 'ARROW_DOWN', 'INSERT', +KEYS = Literal['NULL', 'CANCEL', 'HELP', 'BACKSPACE', 'meta', +'TAB', 'CLEAR', 'RETURN', 'ENTER', 'SHIFT', 'CONTROL', 'command ', +'CTRL', 'ALT', 'PAUSE', 'ESCAPE', 'SPACE', +'PAGE_UP', 'PAGE_DOWN', 'END', 'HOME', 'LEFT', 'UP', +'RIGHT', 'DOWN', 'INSERT', 'DELETE', 'DEL', 'SEMICOLON', 'EQUALS', 'NUMPAD0', 'NUMPAD1', 'NUMPAD2', 'NUMPAD3', 'NUMPAD4', 'NUMPAD5', 'NUMPAD6', 'NUMPAD7', 'NUMPAD8', 'NUMPAD9', 'MULTIPLY', 'ADD', 'SUBTRACT', 'DECIMAL', 'DIVIDE', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'META', 'COMMAND ', -'null', 'cancel', 'help', 'backspace', 'back_space', 'tab', 'clear', 'return', 'enter', -'shift', 'left_shift', 'control', 'ctrl', 'left_control', 'alt', 'left_alt', 'pause', -'escape', 'space', 'page_up', 'page_down', 'end', 'home', 'left', 'arrow_left', 'up', -'arrow_up', 'right', 'arrow_right', 'down', 'arrow_down', 'insert', 'delete', 'del', +'null', 'cancel', 'help', 'backspace', 'tab', 'clear', 'return', 'enter', +'shift', 'control', 'ctrl', 'alt', 'pause', +'escape', 'space', 'page_up', 'page_down', 'end', 'home', 'left', 'up', +'right', 'down', 'insert', 'delete', 'del', 'semicolon', 'equals', 'numpad0', 'numpad1', 'numpad2', 'numpad3', 'numpad4', 'numpad5', 'numpad6', 'numpad7', 'numpad8', 'numpad9', 'multiply', 'add', 'subtract', 'decimal', 'divide', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', @@ -57,13 +57,11 @@ class Actions: def move(self, offset_x: float = 0, offset_y: float = 0, duration: float = .5) -> Actions: ... - def click(self, on_ele: Union[ChromiumElement, str] = None) -> Actions: ... + def click(self, on_ele: Union[ChromiumElement, str] = None, times: int = 1) -> Actions: ... - def r_click(self, on_ele: Union[ChromiumElement, str] = None) -> Actions: ... + def r_click(self, on_ele: Union[ChromiumElement, str] = None, times: int = 1) -> Actions: ... - def m_click(self, on_ele: Union[ChromiumElement, str] = None) -> Actions: ... - - def db_click(self, on_ele: Union[ChromiumElement, str] = None) -> Actions: ... + def m_click(self, on_ele: Union[ChromiumElement, str] = None, times: int = 1) -> Actions: ... def hold(self, on_ele: Union[ChromiumElement, str] = None) -> Actions: ... @@ -103,7 +101,5 @@ class Actions: def wait(self, second: float, scope: float = None) -> Actions: ... - def _get_key_data(self, key: str, action: str) -> dict: ... - def location_to_client(page, lx: int, ly: int) -> tuple: ... diff --git a/DrissionPage/_units/clicker.py b/DrissionPage/_units/clicker.py index 6006b4c..cb889ee 100644 --- a/DrissionPage/_units/clicker.py +++ b/DrissionPage/_units/clicker.py @@ -43,7 +43,7 @@ class Clicker(object): select = self._ele.parent('t:select') if select.select.is_multi: self._ele.parent('t:select').select.cancel_by_option(self._ele) - return + return self._ele if not by_js: # 模拟点击 can_click = False @@ -87,22 +87,25 @@ class Clicker(object): x = rect[1][0] - (rect[1][0] - rect[0][0]) / 2 y = rect[0][0] + 3 try: - r = self._ele.owner.run_cdp('DOM.getNodeForLocation', x=int(x), y=int(y), - includeUserAgentShadowDOM=True, ignorePointerEventsNone=True) + r = self._ele.owner._run_cdp('DOM.getNodeForLocation', x=int(x), y=int(y), + includeUserAgentShadowDOM=True, ignorePointerEventsNone=True) if r['backendNodeId'] != self._ele._backend_id: vx, vy = self._ele.rect.viewport_midpoint + lx, ly = self._ele.rect._get_page_coord(vx, vy) else: vx, vy = self._ele.rect.viewport_click_point + lx, ly = self._ele.rect._get_page_coord(vx, vy) except CDPError: vx, vy = self._ele.rect.viewport_midpoint + lx, ly = self._ele.rect._get_page_coord(vx, vy) - self._click(vx, vy) - return True + self._click(lx, ly, vx, vy) + return self._ele if by_js is not False: - self._ele.run_js('this.click();') - return True + self._ele._run_js('this.click();') + return self._ele if Settings.raise_when_click_failed: raise CanNotClickError return False @@ -110,8 +113,7 @@ class Clicker(object): def right(self): """右键单击""" self._ele.owner.scroll.to_see(self._ele) - x, y = self._ele.rect.viewport_click_point - self._click(x, y, 'right') + return self._click(*self._ele.rect.click_point, *self._ele.rect.viewport_click_point, button='right') def middle(self, get_tab=True): """中键单击,默认返回新出现的tab对象 @@ -119,13 +121,14 @@ class Clicker(object): :return: Tab对象或None """ self._ele.owner.scroll.to_see(self._ele) - x, y = self._ele.rect.viewport_click_point - self._click(x, y, 'middle') + curr_tid = self._ele.tab.browser.tab_ids[0] + self._click(*self._ele.rect.click_point, *self._ele.rect.viewport_click_point, button='middle') if get_tab: - tid = self._ele.page.wait.new_tab() + tid = self._ele.tab.browser.wait.new_tab(curr_tab=curr_tid) if not tid: raise RuntimeError('没有出现新标签页。') - return self._ele.page.get_tab(tid) + return (self._ele.tab.browser.get_mix_tab(tid) if self._ele.tab._type == 'MixTab' + else self._ele.tab.browser.get_tab(tid)) def at(self, offset_x=None, offset_y=None, button='left', count=1): """带偏移量点击本元素,相对于左上角坐标。不传入x或y值时点击元素中间点 @@ -140,15 +143,14 @@ class Clicker(object): w, h = self._ele.rect.size offset_x = w // 2 offset_y = h // 2 - x, y = offset_scroll(self._ele, offset_x, offset_y) - self._click(x, y, button, count) + return self._click(*offset_scroll(self._ele, offset_x, offset_y), button=button, count=count) def multi(self, times=2): """多次点击 :param times: 默认双击 :return: None """ - self.at(count=times) + return self.at(count=times) def to_download(self, save_path=None, rename=None, suffix=None, new_tab=False, by_js=False, timeout=None): """点击触发下载 @@ -161,17 +163,16 @@ class Clicker(object): :return: DownloadMission对象 """ if save_path: - self._ele.owner.tab.set.download_path(save_path) - elif not self._ele.page._browser._dl_mgr._running: - self._ele.page.set.download_path('.') + self._ele.tab.set.download_path(save_path) + elif not self._ele.tab._browser._dl_mgr._running: + self._ele.tab._browser.set.download_path('.') + obj = self._ele.tab._browser if new_tab else self._ele.owner._tab if rename or suffix: - self._ele.owner.tab.set.download_file_name(rename, suffix) - - tab = self._ele.page if new_tab else self._ele.owner + obj.set.download_file_name(rename, suffix) self.left(by_js=by_js) - return tab.wait.download_begin(timeout=timeout) + return obj.wait.download_begin(timeout=timeout) def to_upload(self, file_paths, by_js=False): """触发上传文件选择框并自动填入指定路径 @@ -183,26 +184,61 @@ class Clicker(object): self.left(by_js=by_js) self._ele.owner.wait.upload_paths_inputted() - def for_new_tab(self, by_js=False): + def for_new_tab(self, by_js=False, timeout=3): """点击后等待新tab出现并返回其对象 :param by_js: 是否使用js点击,逻辑与click()一致 + :param timeout: 等待超时时间 :return: 新标签页对象,如果没有等到新标签页出现则抛出异常 """ + curr_tid = self._ele.tab.browser.tab_ids[0] self.left(by_js=by_js) - tid = self._ele.page.wait.new_tab() + tid = self._ele.tab.browser.wait.new_tab(timeout=timeout, curr_tab=curr_tid) if not tid: raise RuntimeError('没有出现新标签页。') - return self._ele.page.get_tab(tid) + return (self._ele.tab.browser.get_mix_tab(tid) if self._ele.tab._type == 'MixTab' + else self._ele.tab.browser.get_tab(tid)) - def _click(self, client_x, client_y, button='left', count=1): + def for_url_change(self, text=None, exclude=False, by_js=False, timeout=None): + """点击并等待tab的url变成包含或不包含指定文本 + :param text: 用于识别的文本,为None等待当前url变化 + :param exclude: 是否排除,为True时当url不包含text指定文本时返回True,text为None时自动设为True + :param by_js: 是否用js点击 + :param timeout: 超时时间(秒),为None使用页面设置 + :return: 是否等待成功 + """ + if text is None: + exclude = True + text = self._ele.tab.url + self.left(by_js=by_js) + return True if self._ele.tab.wait.url_change(text=text, exclude=exclude, timeout=timeout) else False + + def for_title_change(self, text=None, exclude=False, by_js=False, timeout=None): + """点击并等待tab的title变成包含或不包含指定文本 + :param text: 用于识别的文本,为None等待当前title变化 + :param exclude: 是否排除,为True时当title不包含text指定文本时返回True,text为None时自动设为True + :param by_js: 是否用js点击 + :param timeout: 超时时间(秒),为None使用页面设置 + :return: 是否等待成功 + """ + if text is None: + exclude = True + text = self._ele.tab.title + self.left(by_js=by_js) + return True if self._ele.tab.wait.title_change(text=text, exclude=exclude, timeout=timeout) else False + + def _click(self, loc_x, loc_y, view_x, view_y, button='left', count=1): """实施点击 - :param client_x: 视口中的x坐标 - :param client_y: 视口中的y坐标 + :param loc_x: 绝对x坐标 + :param loc_y: 绝对y坐标 + :param view_x: 视口x坐标 + :param view_y: 视口y坐标 :param button: 'left' 'right' 'middle' 'back' 'forward' :param count: 点击次数 :return: None """ - self._ele.owner.run_cdp('Input.dispatchMouseEvent', type='mousePressed', x=client_x, - y=client_y, button=button, clickCount=count, _ignore=AlertExistsError) - self._ele.owner.run_cdp('Input.dispatchMouseEvent', type='mouseReleased', x=client_x, - y=client_y, button=button, _ignore=AlertExistsError) + self._ele.owner.actions.move_to((loc_x, loc_y), duration=.05) + self._ele.owner._run_cdp('Input.dispatchMouseEvent', type='mousePressed', x=view_x, + y=view_y, button=button, clickCount=count, _ignore=AlertExistsError) + self._ele.owner._run_cdp('Input.dispatchMouseEvent', type='mouseReleased', x=view_x, + y=view_y, button=button, _ignore=AlertExistsError) + return self._ele diff --git a/DrissionPage/_units/clicker.pyi b/DrissionPage/_units/clicker.pyi index 15fc212..1511100 100644 --- a/DrissionPage/_units/clicker.pyi +++ b/DrissionPage/_units/clicker.pyi @@ -10,28 +10,30 @@ from typing import Union from .downloader import DownloadMission from .._elements.chromium_element import ChromiumElement -from .._pages.chromium_tab import WebPageTab, ChromiumTab +from .._pages.tabs import MixTab, ChromiumTab class Clicker(object): def __init__(self, ele: ChromiumElement): self._ele: ChromiumElement = ... - def __call__(self, by_js: Union[bool, str, None] = False, timeout: float = 1.5, wait_stop: bool = True) -> bool: ... + def __call__(self, by_js: Union[bool, str, None] = False, + timeout: float = 1.5, wait_stop: bool = True) -> Union[ChromiumElement, False]: ... - def left(self, by_js: Union[bool, str, None] = False, timeout: float = 1.5, wait_stop: bool = True) -> bool: ... + def left(self, by_js: Union[bool, str, None] = False, + timeout: float = 1.5, wait_stop: bool = True) -> Union[ChromiumElement, False]: ... - def right(self) -> None: ... + def right(self) -> ChromiumElement: ... - def middle(self, get_tab: bool = True) -> Union[ChromiumTab, WebPageTab, None]: ... + def middle(self, get_tab: bool = True) -> Union[ChromiumTab, MixTab, None]: ... def at(self, offset_x: float = None, offset_y: float = None, button: str = 'left', - count: int = 1) -> None: ... + count: int = 1) -> ChromiumElement: ... - def multi(self, times: int = 2) -> None: ... + def multi(self, times: int = 2) -> ChromiumElement: ... def to_download(self, save_path: Union[str, Path] = None, @@ -43,6 +45,17 @@ class Clicker(object): def to_upload(self, file_paths: Union[str, Path, list, tuple], by_js: bool = False) -> None: ... - def for_new_tab(self, by_js: bool = False) -> Union[ChromiumTab, WebPageTab]: ... + def for_new_tab(self, by_js: bool = False, timeout: float = 3) -> Union[ChromiumTab, MixTab]: ... - def _click(self, client_x: float, client_y: float, button: str = 'left', count: int = 1) -> None: ... + def for_url_change(self, text: str = None, exclude: bool = False, + by_js: bool = False, timeout: float = None) -> bool: ... + + def for_title_change(self, text: str = None, exclude: bool = False, + by_js: bool = False, timeout: float = None) -> bool: ... + + def _click(self, loc_x: float, + loc_y: float, + view_x: float, + view_y: float, + button: str = 'left', + count: int = 1) -> ChromiumElement: ... diff --git a/DrissionPage/_units/cookies_setter.py b/DrissionPage/_units/cookies_setter.py index c9d3c41..dd9764c 100644 --- a/DrissionPage/_units/cookies_setter.py +++ b/DrissionPage/_units/cookies_setter.py @@ -5,13 +5,13 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ -from .._functions.web import set_browser_cookies, set_session_cookies +from .._functions.cookies import set_tab_cookies, set_session_cookies, set_browser_cookies -class CookiesSetter(object): +class BrowserCookiesSetter(object): def __init__(self, owner): """ - :param owner: ChromiumBase对象 + :param owner: Chromium对象 """ self._owner = owner @@ -22,6 +22,24 @@ class CookiesSetter(object): """ set_browser_cookies(self._owner, cookies) + def clear(self): + """清除cookies""" + self._owner._run_cdp('Storage.clearCookies') + + +class CookiesSetter(BrowserCookiesSetter): + + def __call__(self, cookies): + """设置一个或多个cookie + :param cookies: cookies信息 + :return: None + """ + set_tab_cookies(self._owner, cookies) + + def clear(self): + """清除cookies""" + self._owner._run_cdp('Network.clearBrowserCookies') + def remove(self, name, url=None, domain=None, path=None): """删除一个cookie :param name: cookie的name字段 @@ -37,13 +55,11 @@ class CookiesSetter(object): d['domain'] = domain if not url and not domain: d['url'] = self._owner.url + if not d['url'].startswith('http'): + raise ValueError('需设置domain或url值。如设置url值,需以http开头。') if path is not None: d['path'] = path - self._owner.run_cdp('Network.deleteCookies', **d) - - def clear(self): - """清除cookies""" - self._owner.run_cdp('Network.clearBrowserCookies') + self._owner._run_cdp('Network.deleteCookies', **d) class SessionCookiesSetter(object): @@ -69,7 +85,7 @@ class SessionCookiesSetter(object): self._owner.session.cookies.clear() -class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter): +class MixPageCookiesSetter(CookiesSetter, SessionCookiesSetter): def __call__(self, cookies): """设置多个cookie,注意不要传入单个 diff --git a/DrissionPage/_units/cookies_setter.pyi b/DrissionPage/_units/cookies_setter.pyi index cbe37de..8ce3d58 100644 --- a/DrissionPage/_units/cookies_setter.pyi +++ b/DrissionPage/_units/cookies_setter.pyi @@ -8,26 +8,35 @@ from http.cookiejar import Cookie, CookieJar from typing import Union +from .._base.browser import Chromium from .._pages.chromium_base import ChromiumBase -from .._pages.chromium_tab import WebPageTab +from .._pages.tabs import MixTab from .._pages.session_page import SessionPage -from .._pages.web_page import WebPage +from .._pages.mix_page import MixPage -class CookiesSetter(object): - _owner: ChromiumBase +class BrowserCookiesSetter(object): + _owner: Chromium = ... - def __init__(self, page: ChromiumBase): ... + def __init__(self, page: Chromium): ... def __call__(self, cookies: Union[CookieJar, Cookie, list, tuple, str, dict]) -> None: ... + def clear(self) -> None: ... + + +class CookiesSetter(BrowserCookiesSetter): + _owner: ChromiumBase = ... + + def __init__(self, page: ChromiumBase): ... + def remove(self, name: str, url: str = None, domain: str = None, path: str = None) -> None: ... def clear(self) -> None: ... class SessionCookiesSetter(object): - _owner: SessionPage + _owner: SessionPage = ... def __init__(self, page: SessionPage): ... @@ -38,8 +47,8 @@ class SessionCookiesSetter(object): def clear(self) -> None: ... -class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter): - _owner: Union[WebPage, WebPageTab] +class MixPageCookiesSetter(CookiesSetter, SessionCookiesSetter): + _owner: Union[MixPage, MixTab] = ... def __init__(self, page: SessionPage): ... diff --git a/DrissionPage/_units/downloader.py b/DrissionPage/_units/downloader.py index ec41cc4..48a5095 100644 --- a/DrissionPage/_units/downloader.py +++ b/DrissionPage/_units/downloader.py @@ -20,21 +20,24 @@ class DownloadManager(object): :param browser: Browser对象 """ self._browser = browser - self._page = browser.page - self._when_download_file_exists = 'rename' - self._save_path = None + # self._page = browser.page + # self._when_download_file_exists = 'rename' + # self._save_path = None + + t = TabDownloadSettings('browser') + t.path = self._browser.download_path + t.rename = None + t.suffix = None + t.when_file_exists = 'rename' - t = TabDownloadSettings(self._page.tab_id) - t.path = self._page.download_path self._missions = {} # {guid: DownloadMission} self._tab_missions = {} # {tab_id: DownloadMission} self._flags = {} # {tab_id: [bool, DownloadMission]} - if self._page.download_path: - self.set_path(self._page, self._page.download_path) + # if self._page.download_path: + # self.set_path(self._page, self._page.download_path) - else: - self._running = False + self._running = False @property def missions(self): @@ -47,13 +50,13 @@ class DownloadManager(object): :param path: 下载路径(绝对路径str) :return: None """ - TabDownloadSettings(tab.tab_id).path = path - if tab is self._page or not self._running: - self._browser.driver.set_callback('Browser.downloadProgress', self._onDownloadProgress) - self._browser.driver.set_callback('Browser.downloadWillBegin', self._onDownloadWillBegin) - r = self._browser.run_cdp('Browser.setDownloadBehavior', downloadPath=path, - behavior='allowAndName', eventsEnabled=True) - self._save_path = path + tid = tab if isinstance(tab, str) else tab.tab_id + TabDownloadSettings(tid).path = path + if not self._running or tid == 'browser': + self._browser._driver.set_callback('Browser.downloadProgress', self._onDownloadProgress) + self._browser._driver.set_callback('Browser.downloadWillBegin', self._onDownloadWillBegin) + r = self._browser._run_cdp('Browser.setDownloadBehavior', downloadPath=self._browser._download_path, + behavior='allowAndName', eventsEnabled=True) if 'error' in r: print('浏览器版本太低无法使用下载管理功能。') self._running = True @@ -121,7 +124,7 @@ class DownloadManager(object): """ mission.state = 'canceled' try: - self._browser.run_cdp('Browser.cancelDownload', guid=mission.id) + self._browser._run_cdp('Browser.cancelDownload', guid=mission.id) except: pass if mission.final_path: @@ -134,7 +137,7 @@ class DownloadManager(object): """ mission.state = 'skipped' try: - self._browser.run_cdp('Browser.cancelDownload', guid=mission.id) + self._browser._run_cdp('Browser.cancelDownload', guid=mission.id) except: pass @@ -149,10 +152,11 @@ class DownloadManager(object): def _onDownloadWillBegin(self, **kwargs): """用于获取弹出新标签页触发的下载任务""" + # print(kwargs) guid = kwargs['guid'] - tab_id = self._browser._frames.get(kwargs['frameId'], self._page.tab_id) + tab_id = self._browser._frames.get(kwargs['frameId'], 'browser') - settings = TabDownloadSettings(tab_id if tab_id in TabDownloadSettings.TABS else self._page.tab_id) + settings = TabDownloadSettings(tab_id if tab_id in TabDownloadSettings.TABS else 'browser') if settings.rename: if settings.suffix is not None: name = f'{settings.rename}.{settings.suffix}' if settings.suffix else settings.rename @@ -184,7 +188,7 @@ class DownloadManager(object): elif settings.when_file_exists == 'overwrite': goal_path.unlink() - m = DownloadMission(self, tab_id, guid, settings.path, name, kwargs['url'], self._save_path) + m = DownloadMission(self, tab_id, guid, settings.path, name, kwargs['url'], self._browser.download_path) self._missions[guid] = m if self.get_flag(tab_id) is False: # 取消该任务 @@ -214,7 +218,17 @@ class DownloadManager(object): mission.total_bytes = kwargs['totalBytes'] form_path = f'{mission.save_path}{sep}{mission.id}' to_path = str(get_usable_path(f'{mission.path}{sep}{mission.name}')) - move(form_path, to_path) + not_moved = True + for _ in range(10): + try: + move(form_path, to_path) + not_moved = False + break + except PermissionError: + sleep(.5) + if not_moved: + from shutil import copy + copy(form_path, to_path) self.set_done(mission, 'completed', final_path=to_path) else: # 'canceled' diff --git a/DrissionPage/_units/downloader.pyi b/DrissionPage/_units/downloader.pyi index eadcc44..d9768c4 100644 --- a/DrissionPage/_units/downloader.pyi +++ b/DrissionPage/_units/downloader.pyi @@ -7,30 +7,30 @@ """ from typing import Dict, Optional, Union, Literal -from .._base.browser import Browser +from .._base.browser import Chromium from .._pages.chromium_base import ChromiumBase -from .._pages.chromium_page import ChromiumPage - +# from .._pages.chromium_page import ChromiumPage +FILE_EXISTS = Literal['skip', 'rename', 'overwrite', 's', 'r', 'o'] class DownloadManager(object): - _browser: Browser = ... - _page: ChromiumPage = ... + _browser: Chromium = ... + # _page: ChromiumPage = ... _missions: Dict[str, DownloadMission] = ... _tab_missions: dict = ... _flags: dict = ... _running: bool = ... - _save_path: Optional[str] = ... + # _save_path: Optional[str] = ... - def __init__(self, browser: Browser): ... + def __init__(self, browser: Chromium): ... @property def missions(self) -> Dict[str, DownloadMission]: ... - def set_path(self, tab: ChromiumBase, path: str) -> None: ... + def set_path(self, tab: Union[str,ChromiumBase], path: str) -> None: ... def set_rename(self, tab_id: str, rename: str = None, suffix: str = None) -> None: ... - def set_file_exists(self, tab_id: str, mode: Literal['skip', 'rename', 'overwrite', 's', 'r', 'o']) -> None: ... + def set_file_exists(self, tab_id: str, mode: FILE_EXISTS) -> None: ... def set_flag(self, tab_id: str, flag: Union[bool, DownloadMission, None]) -> None: ... diff --git a/DrissionPage/_units/listener.py b/DrissionPage/_units/listener.py index ef49b35..0166e15 100644 --- a/DrissionPage/_units/listener.py +++ b/DrissionPage/_units/listener.py @@ -26,7 +26,7 @@ class Listener(object): :param owner: ChromiumBase对象 """ self._owner = owner - self._address = owner.address + self._address = owner.browser.address self._target_id = owner._target_id self._driver = None self._running_requests = 0 @@ -127,13 +127,13 @@ class Listener(object): if not self.listening: raise RuntimeError('监听未启动或已暂停。') if not timeout: - while self._caught.qsize() < count: + while self._driver.is_running and self._caught.qsize() < count: sleep(.03) fail = False else: end = perf_counter() + timeout - while True: + while self._driver.is_running: if perf_counter() > end: fail = True break @@ -167,8 +167,8 @@ class Listener(object): raise RuntimeError('监听未启动或已暂停。') caught = 0 end = perf_counter() + timeout if timeout else None - while True: - if (timeout and perf_counter() > end) or self._driver._stopped.is_set(): + while self._driver.is_running: + if (timeout and perf_counter() > end) or not self._driver.is_running: return if self._caught.qsize() >= gap: yield self._caught.get_nowait() if gap == 1 else [self._caught.get_nowait() for _ in range(gap)] diff --git a/DrissionPage/_units/rect.py b/DrissionPage/_units/rect.py index b751d4b..f94a94b 100644 --- a/DrissionPage/_units/rect.py +++ b/DrissionPage/_units/rect.py @@ -18,7 +18,7 @@ class ElementRect(object): def corners(self): """返回元素四个角坐标,顺序:左上、右上、右下、左下,没有大小的元素抛出NoRectError""" vr = self._get_viewport_rect('border') - r = self._ele.owner.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport'] + r = self._ele.owner._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)] @@ -32,27 +32,24 @@ class ElementRect(object): @property def size(self): """返回元素大小,格式(宽, 高)""" - border = self._ele.owner.run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id, - nodeId=self._ele._node_id, objectId=self._ele._obj_id)['model']['border'] + border = self._ele.owner._run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id, + nodeId=self._ele._node_id, objectId=self._ele._obj_id)['model']['border'] return border[2] - border[0], border[5] - border[1] @property def location(self): """返回元素左上角的绝对坐标""" - cl = self.viewport_location - return self._get_page_coord(cl[0], cl[1]) + return self._get_page_coord(*self.viewport_location) @property def midpoint(self): """返回元素中间点的绝对坐标""" - cl = self.viewport_midpoint - return self._get_page_coord(cl[0], cl[1]) + return self._get_page_coord(*self.viewport_midpoint) @property def click_point(self): """返回元素接受点击的点的绝对坐标""" - cl = self.viewport_click_point - return self._get_page_coord(cl[0], cl[1]) + return self._get_page_coord(*self.viewport_click_point) @property def viewport_location(self): @@ -77,7 +74,7 @@ class ElementRect(object): """返回元素左上角在屏幕上坐标,左上角为(0, 0)""" vx, vy = self._ele.owner.rect.viewport_location ex, ey = self.viewport_location - pr = self._ele.owner.run_js('return window.devicePixelRatio;') + pr = self._ele.owner._run_js('return window.devicePixelRatio;') return (vx + ex) * pr, (ey + vy) * pr @property @@ -85,7 +82,7 @@ class ElementRect(object): """返回元素中点在屏幕上坐标,左上角为(0, 0)""" vx, vy = self._ele.owner.rect.viewport_location ex, ey = self.viewport_midpoint - pr = self._ele.owner.run_js('return window.devicePixelRatio;') + pr = self._ele.owner._run_js('return window.devicePixelRatio;') return (vx + ex) * pr, (ey + vy) * pr @property @@ -93,21 +90,26 @@ class ElementRect(object): """返回元素中点在屏幕上坐标,左上角为(0, 0)""" vx, vy = self._ele.owner.rect.viewport_location ex, ey = self.viewport_click_point - pr = self._ele.owner.run_js('return window.devicePixelRatio;') + pr = self._ele.owner._run_js('return window.devicePixelRatio;') return (vx + ex) * pr, (ey + vy) * pr + @property + def scroll_position(self): + """返回滚动条位置,格式:(x, y)""" + r = self._ele._run_js('return this.scrollLeft.toString() + " " + this.scrollTop.toString();') + w, h = r.split(' ') + return int(w), int(h) + def _get_viewport_rect(self, quad): """按照类型返回在可视窗口中的范围 :param quad: 方框类型,margin border padding :return: 四个角坐标 """ - return self._ele.owner.run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id, - # nodeId=self._ele._node_id, objectId=self._ele._obj_id - )['model'][quad] + return self._ele.owner._run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id)['model'][quad] def _get_page_coord(self, x, y): """根据视口坐标获取绝对坐标""" - r = self._ele.owner.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport'] + r = self._ele.owner._run_cdp_loaded('Page.getLayoutMetrics')['visualViewport'] sx = r['pageX'] sy = r['pageY'] return x + sx, y + sy @@ -174,17 +176,23 @@ class TabRect(object): @property def viewport_size_with_scrollbar(self): """返回视口宽高,包括滚动条,格式:(宽, 高)""" - r = self._owner.run_js('return window.innerWidth.toString() + " " + window.innerHeight.toString();') + r = self._owner._run_js('return window.innerWidth.toString() + " " + window.innerHeight.toString();') w, h = r.split(' ') return int(w), int(h) + @property + def scroll_position(self): + """返回滚动条位置,格式:(x, y)""" + r = self._get_page_rect()['visualViewport'] + return r['pageX'], r['pageY'] + def _get_page_rect(self): """获取页面范围信息""" - return self._owner.run_cdp_loaded('Page.getLayoutMetrics') + return self._owner._run_cdp_loaded('Page.getLayoutMetrics') def _get_window_rect(self): """获取窗口范围信息""" - return self._owner.browser.get_window_bounds(self._owner.tab_id) + return self._owner.browser._driver.run('Browser.getWindowForTarget', targetId=self._owner.tab_id)['bounds'] class FrameRect(object): @@ -214,8 +222,8 @@ class FrameRect(object): @property def size(self): """返回frame内页面尺寸,格式:(宽, 高)""" - w = self._frame.doc_ele.run_js('return this.body.scrollWidth') - h = self._frame.doc_ele.run_js('return this.body.scrollHeight') + w = self._frame.doc_ele._run_js('return this.body.scrollWidth') + h = self._frame.doc_ele._run_js('return this.body.scrollHeight') return w, h @property @@ -232,3 +240,11 @@ class FrameRect(object): def viewport_corners(self): """返回元素四个角视口坐标,顺序:左上、右上、右下、左下""" return self._frame.frame_ele.rect.viewport_corners + + @property + def scroll_position(self): + """返回滚动条位置,格式:(x, y)""" + r = self._frame.doc_ele._run_js('return this.documentElement.scrollLeft.toString() + " " ' + '+ this.documentElement.scrollTop.toString();') + w, h = r.split(' ') + return int(w), int(h) diff --git a/DrissionPage/_units/rect.pyi b/DrissionPage/_units/rect.pyi index 4fa4e73..abd6047 100644 --- a/DrissionPage/_units/rect.pyi +++ b/DrissionPage/_units/rect.pyi @@ -12,8 +12,8 @@ from .._elements.chromium_element import ChromiumElement from .._pages.chromium_base import ChromiumBase from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_page import ChromiumPage -from .._pages.chromium_tab import ChromiumTab, WebPageTab -from .._pages.web_page import WebPage +from .._pages.tabs import ChromiumTab, MixTab +from .._pages.mix_page import MixPage class ElementRect(object): @@ -56,6 +56,9 @@ class ElementRect(object): @property def viewport_corners(self) -> Tuple[Tuple[float, float], ...]: ... + @property + def scroll_position(self) -> Tuple[float, float]: ... + def _get_viewport_rect(self, quad: str) -> Union[list, None]: ... def _get_page_coord(self, x: float, y: float) -> Tuple[float, float]: ... @@ -63,7 +66,7 @@ class ElementRect(object): class TabRect(object): def __init__(self, owner: ChromiumBase): - self._owner: Union[ChromiumPage, ChromiumTab, WebPage, WebPageTab] = ... + self._owner: Union[ChromiumPage, ChromiumTab, MixPage, MixTab] = ... @property def window_state(self) -> str: ... @@ -89,6 +92,9 @@ class TabRect(object): @property def viewport_size_with_scrollbar(self) -> Tuple[int, int]: ... + @property + def scroll_position(self) -> Tuple[int, int]: ... + def _get_page_rect(self) -> dict: ... def _get_window_rect(self) -> dict: ... @@ -118,3 +124,6 @@ class FrameRect(object): @property def viewport_corners(self) -> Tuple[Tuple[float, float], ...]: ... + + @property + def scroll_position(self) -> Tuple[float, float]: ... diff --git a/DrissionPage/_units/screencast.py b/DrissionPage/_units/screencast.py index fca2b68..e67df23 100644 --- a/DrissionPage/_units/screencast.py +++ b/DrissionPage/_units/screencast.py @@ -39,16 +39,16 @@ class Screencast(object): raise ValueError('save_path必须设置。') if self._mode in ('frugal_video', 'video'): - if self._owner.browser.page._chromium_options.tmp_path: + if self._owner.browser._chromium_options.tmp_path: self._tmp_path = Path( - self._owner.browser.page._chromium_options.tmp_path) / f'screencast_tmp_{time()}_{randint(0, 100)}' + self._owner.browser._chromium_options.tmp_path) / f'screencast_tmp_{time()}_{randint(0, 100)}' else: self._tmp_path = Path(gettempdir()) / 'DrissionPage' / f'screencast_tmp_{time()}_{randint(0, 100)}' self._tmp_path.mkdir(parents=True, exist_ok=True) if self._mode.startswith('frugal'): self._owner.driver.set_callback('Page.screencastFrame', self._onScreencastFrame) - self._owner.run_cdp('Page.startScreencast', everyNthFrame=1, quality=100) + self._owner._run_cdp('Page.startScreencast', everyNthFrame=1, quality=100) elif not self._mode.startswith('js'): self._running = True @@ -79,8 +79,8 @@ class Screencast(object): } ''' print('请手动选择要录制的目标。') - self._owner.run_js('var DrissionPage_Screencast_blob;var DrissionPage_Screencast_blob_ok=false;') - self._owner.run_js(js) + self._owner._run_js('var DrissionPage_Screencast_blob;var DrissionPage_Screencast_blob_ok=false;') + self._owner._run_js(js) def stop(self, video_name=None): """停止录屏 @@ -93,19 +93,19 @@ class Screencast(object): path = f'{self._path}{sep}{name}' if self._mode.startswith('js'): - self._owner.run_js('mediaRecorder.stop();', as_expr=True) - while not self._owner.run_js('return DrissionPage_Screencast_blob_ok;'): + self._owner._run_js('mediaRecorder.stop();', as_expr=True) + while not self._owner._run_js('return DrissionPage_Screencast_blob_ok;'): sleep(.1) - blob = self._owner.run_js('return DrissionPage_Screencast_blob;') - uuid = self._owner.run_cdp('IO.resolveBlob', objectId=blob['result']['objectId'])['uuid'] - data = self._owner.run_cdp('IO.read', handle=f'blob:{uuid}')['data'] + blob = self._owner._run_js('return DrissionPage_Screencast_blob;') + uuid = self._owner._run_cdp('IO.resolveBlob', objectId=blob['result']['objectId'])['uuid'] + data = self._owner._run_cdp('IO.read', handle=f'blob:{uuid}')['data'] with open(path, 'wb') as f: f.write(b64decode(data)) return path if self._mode.startswith('frugal'): self._owner.driver.set_callback('Page.screencastFrame', None) - self._owner.run_cdp('Page.stopScreencast') + self._owner._run_cdp('Page.stopScreencast') else: self._enable = False while self._running: @@ -164,7 +164,7 @@ class Screencast(object): path = self._tmp_path or self._path with open(f'{path}{sep}{kwargs["metadata"]["timestamp"]}.jpg', 'wb') as f: f.write(b64decode(kwargs['data'])) - self._owner.run_cdp('Page.screencastFrameAck', sessionId=kwargs['sessionId']) + self._owner._run_cdp('Page.screencastFrameAck', sessionId=kwargs['sessionId']) class ScreencastMode(object): diff --git a/DrissionPage/_units/scroller.py b/DrissionPage/_units/scroller.py index 224d640..bc5f334 100644 --- a/DrissionPage/_units/scroller.py +++ b/DrissionPage/_units/scroller.py @@ -16,12 +16,12 @@ class Scroller(object): :param ele: 元素对象 """ self._driver = ele - self.t1 = self.t2 = 'this' + 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) + js = js.format(self._t1, self._t2, self._t2) + self._driver._run_js(js) self._wait_scrolled() def to_top(self): @@ -88,14 +88,14 @@ class Scroller(object): return owner = self._driver.owner if self._driver._type == 'ChromiumElement' else self._driver - r = owner.run_cdp('Page.getLayoutMetrics') + r = owner._run_cdp('Page.getLayoutMetrics') x = r['layoutViewport']['pageX'] y = r['layoutViewport']['pageY'] end_time = perf_counter() + owner.timeout while perf_counter() < end_time: sleep(.1) - r = owner.run_cdp('Page.getLayoutMetrics') + r = owner._run_cdp('Page.getLayoutMetrics') x1 = r['layoutViewport']['pageX'] y1 = r['layoutViewport']['pageY'] @@ -125,8 +125,8 @@ class PageScroller(Scroller): :param owner: 页面对象 """ super().__init__(owner) - self.t1 = 'window' - self.t2 = 'document.documentElement' + self._t1 = 'window' + self._t2 = 'document.documentElement' def to_see(self, loc_or_ele, center=None): """滚动页面直到元素可见 @@ -144,9 +144,9 @@ class PageScroller(Scroller): :return: None """ txt = 'true' if center else 'false' - ele.run_js(f'this.scrollIntoViewIfNeeded({txt});') + ele._run_js(f'this.scrollIntoViewIfNeeded({txt});') if center or (center is not False and ele.states.is_covered): - ele.run_js('''function getWindowScrollTop() {let scroll_top = 0; + ele._run_js('''function getWindowScrollTop() {let scroll_top = 0; if (document.documentElement && document.documentElement.scrollTop) { scroll_top = document.documentElement.scrollTop; } else if (document.body) {scroll_top = document.body.scrollTop;} @@ -165,7 +165,7 @@ class FrameScroller(PageScroller): :param frame: ChromiumFrame对象 """ super().__init__(frame.doc_ele) - self.t1 = self.t2 = 'this.documentElement' + self._t1 = self._t2 = 'this.documentElement' def to_see(self, loc_or_ele, center=None): """滚动页面直到元素可见 @@ -173,5 +173,5 @@ class FrameScroller(PageScroller): :param center: 是否尽量滚动到页面正中,为None时如果被遮挡,则滚动到页面正中 :return: None """ - ele = loc_or_ele if loc_or_ele._type == 'ChromiumElement' else self._driver._ele(loc_or_ele) + ele = self._driver._ele(loc_or_ele) self._to_see(ele, center) diff --git a/DrissionPage/_units/scroller.pyi b/DrissionPage/_units/scroller.pyi index 7e8b7c6..e48c976 100644 --- a/DrissionPage/_units/scroller.pyi +++ b/DrissionPage/_units/scroller.pyi @@ -13,8 +13,8 @@ from .._pages.chromium_base import ChromiumBase class Scroller(object): def __init__(self, page_or_ele: Union[ChromiumBase, ChromiumElement]): - self.t1: str = ... - self.t2: str = ... + self._t1: str = ... + self._t2: str = ... self._driver: Union[ChromiumBase, ChromiumElement] = ... self._wait_complete: bool = ... @@ -64,7 +64,7 @@ class FrameScroller(PageScroller): :param frame: ChromiumFrame对象 """ self._driver = frame.doc_ele - self.t1 = self.t2 = 'this.documentElement' + self._t1 = self._t2 = 'this.documentElement' self._wait_complete = False def to_see(self, loc_or_ele, center=None): diff --git a/DrissionPage/_units/selector.py b/DrissionPage/_units/selector.py index c97aded..27301ad 100644 --- a/DrissionPage/_units/selector.py +++ b/DrissionPage/_units/selector.py @@ -45,7 +45,7 @@ class SelectElement(object): """返回第一个被选中的option元素 :return: ChromiumElement对象或None """ - ele = self._ele.run_js('return this.options[this.selectedIndex];') + ele = self._ele._run_js('return this.options[this.selectedIndex];') return ele @property @@ -69,7 +69,7 @@ class SelectElement(object): for i in self.options: change = True mode = 'false' if i.states.is_selected else 'true' - i.run_js(f'this.selected={mode};') + i._run_js(f'this.selected={mode};') if change: self._dispatch_change() @@ -258,12 +258,12 @@ class SelectElement(object): if not self.is_multi and len(option) > 1: option = option[:1] for o in option: - o.run_js(f'this.selected={mode};') + o._run_js(f'this.selected={mode};') self._dispatch_change() else: - option.run_js(f'this.selected={mode};') + option._run_js(f'this.selected={mode};') self._dispatch_change() def _dispatch_change(self): """触发修改动作""" - self._ele.run_js('this.dispatchEvent(new CustomEvent("change", {bubbles: true}));') + self._ele._run_js('this.dispatchEvent(new CustomEvent("change", {bubbles: true}));') diff --git a/DrissionPage/_units/setter.py b/DrissionPage/_units/setter.py index 9f0e443..972ab75 100644 --- a/DrissionPage/_units/setter.py +++ b/DrissionPage/_units/setter.py @@ -10,14 +10,14 @@ from time import sleep from requests.structures import CaseInsensitiveDict -from .cookies_setter import SessionCookiesSetter, CookiesSetter, WebPageCookiesSetter +from .cookies_setter import SessionCookiesSetter, CookiesSetter, MixPageCookiesSetter, BrowserCookiesSetter from .._functions.settings import Settings from .._functions.tools import show_or_hide_browser from .._functions.web import format_headers from ..errors import ElementLostError, JavaScriptError -class BasePageSetter(object): +class BaseSetter(object): def __init__(self, owner): """ :param owner: BasePage对象 @@ -33,32 +33,6 @@ class BasePageSetter(object): self._owner._none_ele_return_value = on_off self._owner._none_ele_value = value - -class ChromiumBaseSetter(BasePageSetter): - def __init__(self, owner): - """ - :param owner: ChromiumBase对象 - """ - super().__init__(owner) - self._cookies_setter = None - - @property - def load_mode(self): - """返回用于设置页面加载策略的对象""" - return LoadMode(self._owner) - - @property - def scroll(self): - """返回用于设置页面滚动设置的对象""" - return PageScrollSetter(self._owner.scroll) - - @property - def cookies(self): - """返回用于设置cookies的对象""" - if self._cookies_setter is None: - self._cookies_setter = CookiesSetter(self._owner) - return self._cookies_setter - def retry_times(self, times): """设置连接失败重连次数""" self._owner.retry_times = times @@ -67,189 +41,17 @@ class ChromiumBaseSetter(BasePageSetter): """设置连接失败重连间隔""" self._owner.retry_interval = interval - def timeouts(self, base=None, page_load=None, script=None, implicit=None): - """设置超时时间,单位为秒 - :param base: 基本等待时间,除页面加载和脚本超时,其它等待默认使用 - :param page_load: 页面加载超时时间 - :param script: 脚本运行超时时间 - :return: None - """ - base = base if base is not None else implicit - if base is not None: - self._owner.timeouts.base = base - self._owner._timeout = base - - if page_load is not None: - self._owner.timeouts.page_load = page_load - - if script is not None: - self._owner.timeouts.script = script - - def user_agent(self, ua, platform=None): - """为当前tab设置user agent,只在当前tab有效 - :param ua: user agent字符串 - :param platform: platform字符串 - :return: None - """ - keys = {'userAgent': ua} - if platform: - keys['platform'] = platform - self._owner.run_cdp('Emulation.setUserAgentOverride', **keys) - - def session_storage(self, item, value): - """设置或删除某项sessionStorage信息 - :param item: 要设置的项 - :param value: 项的值,设置为False时,删除该项 - :return: None - """ - self._owner.run_cdp_loaded('DOMStorage.enable') - i = self._owner.run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey'] - if value is False: - self._owner.run_cdp('DOMStorage.removeDOMStorageItem', - storageId={'storageKey': i, 'isLocalStorage': False}, key=item) - else: - self._owner.run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': False}, - key=item, value=value) - self._owner.run_cdp_loaded('DOMStorage.disable') - - def local_storage(self, item, value): - """设置或删除某项localStorage信息 - :param item: 要设置的项 - :param value: 项的值,设置为False时,删除该项 - :return: None - """ - self._owner.run_cdp_loaded('DOMStorage.enable') - i = self._owner.run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey'] - if value is False: - self._owner.run_cdp('DOMStorage.removeDOMStorageItem', - storageId={'storageKey': i, 'isLocalStorage': True}, key=item) - else: - self._owner.run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': True}, - key=item, value=value) - self._owner.run_cdp_loaded('DOMStorage.disable') - - def upload_files(self, files): - """等待上传的文件路径 - :param files: 文件路径列表或字符串,字符串时多个文件用回车分隔 - :return: None - """ - if not self._owner._upload_list: - self._owner.driver.set_callback('Page.fileChooserOpened', self._owner._onFileChooserOpened) - self._owner.run_cdp('Page.setInterceptFileChooserDialog', enabled=True) - - if isinstance(files, str): - files = files.split('\n') - elif isinstance(files, Path): - files = (files,) - self._owner._upload_list = [str(Path(i).absolute()) for i in files] - - def headers(self, headers) -> None: - """设置固定发送的headers - :param headers: dict格式的headers数据 - :return: None - """ - self._owner.run_cdp('Network.enable') - self._owner.run_cdp('Network.setExtraHTTPHeaders', headers=format_headers(headers)) - - def auto_handle_alert(self, on_off=True, accept=True): - """设置是否启用自动处理弹窗 - :param on_off: bool表示开或关 - :param accept: bool表示确定还是取消 - :return: None - """ - self._owner._alert.auto = accept if on_off else None - - def blocked_urls(self, urls): - """设置要忽略的url - :param urls: 要忽略的url,可用*通配符,可输入多个,传入None时清空已设置的内容 - :return: None - """ - if not urls: - urls = [] - elif isinstance(urls, str): - urls = (urls,) - if not isinstance(urls, (list, tuple)): - raise TypeError('urls需传入str、list或tuple类型。') - self._owner.run_cdp('Network.enable') - self._owner.run_cdp('Network.setBlockedURLs', urls=urls) - - -class TabSetter(ChromiumBaseSetter): - def __init__(self, owner): - """ - :param owner: 标签页对象 - """ - super().__init__(owner) - - @property - def window(self): - """返回用于设置浏览器窗口的对象""" - return WindowSetter(self._owner) - def download_path(self, path): """设置下载路径 :param path: 下载路径 :return: None """ - path = str(Path(path).absolute()) - self._owner._download_path = path - self._owner.browser._dl_mgr.set_path(self._owner, path) - if self._owner._DownloadKit: - self._owner._DownloadKit.set.goal_path(path) - - def download_file_name(self, name=None, suffix=None): - """设置下一个被下载文件的名称 - :param name: 文件名,可不含后缀,会自动使用远程文件后缀 - :param suffix: 后缀名,显式设置后缀名,不使用远程文件后缀 - :return: None - """ - self._owner.browser._dl_mgr.set_rename(self._owner.tab_id, name, suffix) - - def when_download_file_exists(self, mode): - """设置当存在同名文件时的处理方式 - :param mode: 可在 'rename', 'overwrite', 'skip', 'r', 'o', 's'中选择 - :return: None - """ - types = {'rename': 'rename', 'overwrite': 'overwrite', 'skip': 'skip', 'r': 'rename', 'o': 'overwrite', - 's': 'skip'} - mode = types.get(mode, mode) - if mode not in types: - raise ValueError(f'''mode参数只能是 '{"', '".join(types.keys())}' 之一,现在是:{mode}''') - - self._owner.browser._dl_mgr.set_file_exists(self._owner.tab_id, mode) - - def activate(self): - """使标签页处于最前面""" - self._owner.browser.activate_tab(self._owner.tab_id) + if path is None: + path = '.' + self._owner._download_path = str(Path(path).absolute()) -class ChromiumPageSetter(TabSetter): - - def tab_to_front(self, tab_or_id=None): - """激活标签页使其处于最前面 - :param tab_or_id: 标签页对象或id,为None表示当前标签页 - :return: None - """ - if not tab_or_id: - tab_or_id = self._owner.tab_id - elif not isinstance(tab_or_id, str): # 传入Tab对象 - tab_or_id = tab_or_id.tab_id - self._owner.browser.activate_tab(tab_or_id) - - def auto_handle_alert(self, on_off=True, accept=True, all_tabs=False): - """设置是否启用自动处理弹窗 - :param on_off: bool表示开或关 - :param accept: bool表示确定还是取消 - :param all_tabs: 是否为全局设置 - :return: None - """ - if all_tabs: - Settings.auto_handle_alert = on_off - else: - self._owner._alert.auto = accept if on_off else None - - -class SessionPageSetter(BasePageSetter): +class SessionPageSetter(BaseSetter): def __init__(self, owner): """ :param owner: SessionPage对象 @@ -264,30 +66,21 @@ class SessionPageSetter(BasePageSetter): self._cookies_setter = SessionCookiesSetter(self._owner) return self._cookies_setter - def retry_times(self, times): - """设置连接失败时重连次数""" - self._owner.retry_times = times - - def retry_interval(self, interval): - """设置连接失败时重连间隔""" - self._owner.retry_interval = interval - def download_path(self, path): """设置下载路径 :param path: 下载路径 :return: None """ - path = str(Path(path).absolute()) - self._owner._download_path = path + super().download_path(path) if self._owner._DownloadKit: - self._owner._DownloadKit.set.goal_path(path) + self._owner._DownloadKit.set.goal_path(self._owner._download_path) def timeout(self, second): """设置连接超时时间 :param second: 秒数 :return: None """ - self._owner.timeout = second + self._owner._timeout = second def encoding(self, encoding, set_all=True): """设置编码 @@ -395,7 +188,272 @@ class SessionPageSetter(BasePageSetter): self._owner.session.mount(url, adapter) -class WebPageSetter(ChromiumPageSetter): +class BrowserBaseSetter(BaseSetter): + """Browser和ChromiumBase设置""" + + def __init__(self, owner): + """ + :param owner: ChromiumBase对象 + """ + super().__init__(owner) + self._cookies_setter = None + + @property + def load_mode(self): + """返回用于设置页面加载策略的对象""" + return LoadMode(self._owner) + + def timeouts(self, base=None, page_load=None, script=None): + """设置超时时间,单位为秒 + :param base: 基本等待时间,除页面加载和脚本超时,其它等待默认使用 + :param page_load: 页面加载超时时间 + :param script: 脚本运行超时时间 + :return: None + """ + if base is not None: + self._owner.timeouts.base = base + + if page_load is not None: + self._owner.timeouts.page_load = page_load + + if script is not None: + self._owner.timeouts.script = script + + +class BrowserSetter(BrowserBaseSetter): + + @property + def cookies(self): + """返回用于设置cookies的对象""" + if self._cookies_setter is None: + self._cookies_setter = BrowserCookiesSetter(self._owner) + return self._cookies_setter + + def auto_handle_alert(self, on_off=True, accept=True): + """设置是否启用自动处理弹窗 + :param on_off: bool表示开或关 + :param accept: bool表示确定还是取消 + :return: None + """ + Settings.auto_handle_alert = accept if on_off else None + + def download_path(self, path): + """设置下载路径 + :param path: 下载路径 + :return: None + """ + super().download_path(path) + self._owner._dl_mgr.set_path('browser', self._owner._download_path) + + def download_file_name(self, name=None, suffix=None): + """设置下一个被下载文件的名称 + :param name: 文件名,可不含后缀,会自动使用远程文件后缀 + :param suffix: 后缀名,显式设置后缀名,不使用远程文件后缀 + :return: None + """ + self._owner._dl_mgr.set_rename('browser', name, suffix) + + def when_download_file_exists(self, mode): + """设置当存在同名文件时的处理方式 + :param mode: 可在 'rename', 'overwrite', 'skip', 'r', 'o', 's'中选择 + :return: None + """ + types = {'rename': 'rename', 'overwrite': 'overwrite', 'skip': 'skip', 'r': 'rename', 'o': 'overwrite', + 's': 'skip'} + mode = types.get(mode, mode) + if mode not in types: + raise ValueError(f'''mode参数只能是 '{"', '".join(types.keys())}' 之一,现在是:{mode}''') + self._owner._dl_mgr.set_file_exists('browser', mode) + + # ---------- 即将废弃 ---------- + def tab_to_front(self, tab_or_id): + """激活标签页使其处于最前面 + :param tab_or_id: 标签页对象或id + :return: None + """ + if not isinstance(tab_or_id, str): # 传入Tab对象 + tab_or_id = tab_or_id.tab_id + self._owner.activate_tab(tab_or_id) + + +class ChromiumBaseSetter(BrowserBaseSetter): + + @property + def scroll(self): + """返回用于设置页面滚动设置的对象""" + return PageScrollSetter(self._owner.scroll) + + @property + def cookies(self): + """返回用于设置cookies的对象""" + if self._cookies_setter is None: + self._cookies_setter = CookiesSetter(self._owner) + return self._cookies_setter + + def user_agent(self, ua, platform=None): + """为当前tab设置user agent,只在当前tab有效 + :param ua: user agent字符串 + :param platform: platform字符串 + :return: None + """ + keys = {'userAgent': ua} + if platform: + keys['platform'] = platform + self._owner._run_cdp('Emulation.setUserAgentOverride', **keys) + + def session_storage(self, item, value): + """设置或删除某项sessionStorage信息 + :param item: 要设置的项 + :param value: 项的值,设置为False时,删除该项 + :return: None + """ + self._owner._run_cdp_loaded('DOMStorage.enable') + i = self._owner._run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey'] + if value is False: + self._owner._run_cdp('DOMStorage.removeDOMStorageItem', + storageId={'storageKey': i, 'isLocalStorage': False}, key=item) + else: + self._owner._run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': False}, + key=item, value=value) + self._owner._run_cdp_loaded('DOMStorage.disable') + + def local_storage(self, item, value): + """设置或删除某项localStorage信息 + :param item: 要设置的项 + :param value: 项的值,设置为False时,删除该项 + :return: None + """ + self._owner._run_cdp_loaded('DOMStorage.enable') + i = self._owner._run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey'] + if value is False: + self._owner._run_cdp('DOMStorage.removeDOMStorageItem', + storageId={'storageKey': i, 'isLocalStorage': True}, key=item) + else: + self._owner._run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': True}, + key=item, value=value) + self._owner._run_cdp_loaded('DOMStorage.disable') + + def upload_files(self, files): + """等待上传的文件路径 + :param files: 文件路径列表或字符串,字符串时多个文件用回车分隔 + :return: None + """ + if not self._owner._upload_list: + self._owner.driver.set_callback('Page.fileChooserOpened', self._owner._onFileChooserOpened) + self._owner._run_cdp('Page.setInterceptFileChooserDialog', enabled=True) + + if isinstance(files, str): + files = files.split('\n') + elif isinstance(files, Path): + files = (files,) + self._owner._upload_list = [str(Path(i).absolute()) for i in files] + + def headers(self, headers) -> None: + """设置固定发送的headers + :param headers: dict格式的headers数据,或从浏览器复制的headers文本(\n分行) + :return: None + """ + self._owner._run_cdp('Network.enable') + self._owner._run_cdp('Network.setExtraHTTPHeaders', headers=format_headers(headers)) + + def auto_handle_alert(self, on_off=True, accept=True): + """设置是否启用自动处理弹窗 + :param on_off: bool表示开或关 + :param accept: bool表示确定还是取消 + :return: None + """ + self._owner._alert.auto = accept if on_off else None + + def blocked_urls(self, urls): + """设置要忽略的url + :param urls: 要忽略的url,可用*通配符,可输入多个,传入None时清空已设置的内容 + :return: None + """ + if not urls: + urls = [] + elif isinstance(urls, str): + urls = (urls,) + if not isinstance(urls, (list, tuple)): + raise TypeError('urls需传入str、list或tuple类型。') + self._owner._run_cdp('Network.enable') + self._owner._run_cdp('Network.setBlockedURLs', urls=urls) + + +class TabSetter(ChromiumBaseSetter): + def __init__(self, owner): + """ + :param owner: 标签页对象 + """ + super().__init__(owner) + + @property + def window(self): + """返回用于设置浏览器窗口的对象""" + return WindowSetter(self._owner) + + def download_path(self, path): + """设置下载路径 + :param path: 下载路径 + :return: None + """ + super().download_path(path) + self._owner.browser._dl_mgr.set_path(self._owner, self._owner._download_path) + if self._owner._DownloadKit: + self._owner._DownloadKit.set.goal_path(self._owner._download_path) + + def download_file_name(self, name=None, suffix=None): + """设置下一个被下载文件的名称 + :param name: 文件名,可不含后缀,会自动使用远程文件后缀 + :param suffix: 后缀名,显式设置后缀名,不使用远程文件后缀 + :return: None + """ + self._owner.browser._dl_mgr.set_rename(self._owner.tab_id, name, suffix) + + def when_download_file_exists(self, mode): + """设置当存在同名文件时的处理方式 + :param mode: 可在 'rename', 'overwrite', 'skip', 'r', 'o', 's'中选择 + :return: None + """ + types = {'rename': 'rename', 'overwrite': 'overwrite', 'skip': 'skip', 'r': 'rename', 'o': 'overwrite', + 's': 'skip'} + mode = types.get(mode, mode) + if mode not in types: + raise ValueError(f'''mode参数只能是 '{"', '".join(types.keys())}' 之一,现在是:{mode}''') + self._owner.browser._dl_mgr.set_file_exists(self._owner.tab_id, mode) + + def activate(self): + """使标签页处于最前面""" + self._owner.browser.activate_tab(self._owner.tab_id) + + +class ChromiumPageSetter(TabSetter): + + def auto_handle_alert(self, on_off=True, accept=True, all_tabs=False): + """设置是否启用自动处理弹窗 + :param on_off: bool表示开或关 + :param accept: bool表示确定还是取消 + :param all_tabs: 是否为全局设置 + :return: None + """ + if all_tabs: + Settings.auto_handle_alert = on_off + else: + self._owner._alert.auto = accept if on_off else None + + # ---------- 即将废弃 ---------- + def tab_to_front(self, tab_or_id=None): + """激活标签页使其处于最前面 + :param tab_or_id: 标签页对象或id,为None表示当前标签页 + :return: None + """ + if not tab_or_id: + tab_or_id = self._owner.tab_id + elif not isinstance(tab_or_id, str): # 传入Tab对象 + tab_or_id = tab_or_id.tab_id + self._owner.browser.activate_tab(tab_or_id) + + +class MixPageSetter(ChromiumPageSetter): def __init__(self, owner): super().__init__(owner) self._session_setter = SessionPageSetter(self._owner) @@ -405,7 +463,7 @@ class WebPageSetter(ChromiumPageSetter): def cookies(self): """返回用于设置cookies的对象""" if self._cookies_setter is None: - self._cookies_setter = WebPageCookiesSetter(self._owner) + self._cookies_setter = MixPageCookiesSetter(self._owner) return self._cookies_setter def headers(self, headers) -> None: @@ -426,7 +484,7 @@ class WebPageSetter(ChromiumPageSetter): self._chromium_setter.user_agent(ua, platform) -class WebPageTabSetter(TabSetter): +class MixTabSetter(TabSetter): def __init__(self, owner): super().__init__(owner) self._session_setter = SessionPageSetter(self._owner) @@ -436,7 +494,7 @@ class WebPageTabSetter(TabSetter): def cookies(self): """返回用于设置cookies的对象""" if self._cookies_setter is None: - self._cookies_setter = WebPageCookiesSetter(self._owner) + self._cookies_setter = MixPageCookiesSetter(self._owner) return self._cookies_setter def headers(self, headers) -> None: @@ -456,6 +514,17 @@ class WebPageTabSetter(TabSetter): if self._owner._has_driver: self._chromium_setter.user_agent(ua, platform) + def timeouts(self, base=None, page_load=None, script=None): + """设置超时时间,单位为秒 + :param base: 基本等待时间,除页面加载和脚本超时,其它等待默认使用 + :param page_load: 页面加载超时时间 + :param script: 脚本运行超时时间 + :return: None + """ + super().timeouts(base=base, page_load=page_load, script=script) + if base is not None: + self._owner._timeout = base + class ChromiumElementSetter(object): def __init__(self, ele): @@ -464,19 +533,19 @@ class ChromiumElementSetter(object): """ self._ele = ele - def attr(self, name, value): + def attr(self, name, value=''): """设置元素attribute属性 :param name: 属性名 :param value: 属性值 :return: None """ try: - self._ele.owner.run_cdp('DOM.setAttributeValue', - nodeId=self._ele._node_id, name=name, value=str(value)) + self._ele.owner._run_cdp('DOM.setAttributeValue', + nodeId=self._ele._node_id, name=name, value=str(value)) except ElementLostError: self._ele._refresh_id() - self._ele.owner.run_cdp('DOM.setAttributeValue', - nodeId=self._ele._node_id, name=name, value=str(value)) + self._ele.owner._run_cdp('DOM.setAttributeValue', + nodeId=self._ele._node_id, name=name, value=str(value)) def property(self, name, value): """设置元素property属性 @@ -485,7 +554,7 @@ class ChromiumElementSetter(object): :return: None """ value = value.replace('"', r'\"') - self._ele.run_js(f'this.{name}="{value}";') + self._ele._run_js(f'this.{name}="{value}";') def style(self, name, value): """设置元素style样式 @@ -494,7 +563,7 @@ class ChromiumElementSetter(object): :return: None """ try: - self._ele.run_js(f'this.style.{name}="{value}";') + self._ele._run_js(f'this.style.{name}="{value}";') except JavaScriptError: raise ValueError(f'设置失败,请检查属性名{name}') @@ -522,6 +591,22 @@ class ChromiumFrameSetter(ChromiumBaseSetter): """ self._owner.frame_ele.set.attr(name, value) + def property(self, name, value): + """设置元素property属性 + :param name: 属性名 + :param value: 属性值 + :return: None + """ + self._owner.frame_ele.set.property(name=name, value=value) + + def style(self, name, value): + """设置元素style样式 + :param name: 样式名称 + :param value: 样式值 + :return: None + """ + self._owner.frame_ele.set.style(name=name, value=value) + class LoadMode(object): """用于设置页面加载策略的类""" @@ -578,7 +663,7 @@ class PageScrollSetter(object): if not isinstance(on_off, bool): raise TypeError('on_off必须为bool。') b = 'smooth' if on_off else 'auto' - self._scroll._driver.run_js(f'document.documentElement.style.setProperty("scroll-behavior","{b}");') + self._scroll._driver._run_js(f'document.documentElement.style.setProperty("scroll-behavior","{b}");') self._scroll._wait_complete = on_off @@ -652,7 +737,7 @@ class WindowSetter(object): """获取窗口位置及大小信息""" for _ in range(50): try: - return self._owner.run_cdp('Browser.getWindowForTarget') + return self._owner._run_cdp('Browser.getWindowForTarget') except: sleep(.1) @@ -662,7 +747,7 @@ class WindowSetter(object): :return: None """ try: - self._owner.run_cdp('Browser.setWindowBounds', windowId=self._window_id, bounds=bounds) + self._owner._run_cdp('Browser.setWindowBounds', windowId=self._window_id, bounds=bounds) except: raise RuntimeError('浏览器全屏或最小化状态时请先调用set.window.normal()恢复正常状态。') diff --git a/DrissionPage/_units/setter.pyi b/DrissionPage/_units/setter.pyi index f371bf6..ca43f30 100644 --- a/DrissionPage/_units/setter.pyi +++ b/DrissionPage/_units/setter.pyi @@ -11,88 +11,35 @@ from typing import Union, Tuple, Literal, Any, Optional from requests.adapters import HTTPAdapter from requests.auth import HTTPBasicAuth -from .cookies_setter import SessionCookiesSetter, CookiesSetter, WebPageCookiesSetter +from .cookies_setter import SessionCookiesSetter, CookiesSetter, MixPageCookiesSetter, BrowserCookiesSetter from .scroller import PageScroller from .._base.base import BasePage +from .._base.browser import Chromium from .._elements.chromium_element import ChromiumElement from .._pages.chromium_base import ChromiumBase from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_page import ChromiumPage -from .._pages.chromium_tab import ChromiumTab, WebPageTab +from .._pages.tabs import ChromiumTab, MixTab from .._pages.session_page import SessionPage -from .._pages.web_page import WebPage +from .._pages.mix_page import MixPage FILE_EXISTS = Literal['skip', 'rename', 'overwrite', 's', 'r', 'o'] -class BasePageSetter(object): - def __init__(self, owner: BasePage): - self._owner: BasePage = ... +class BaseSetter(object): + def __init__(self, owner: Union[Chromium, BasePage]): + self._owner: Union[Chromium, BasePage] = ... def NoneElement_value(self, value: Any = None, on_off: bool = True) -> None: ... - -class ChromiumBaseSetter(BasePageSetter): - def __init__(self, owner): - self._owner: ChromiumBase = ... - self._cookies_setter: CookiesSetter = ... - - @property - def load_mode(self) -> LoadMode: ... - - @property - def scroll(self) -> PageScrollSetter: ... - - @property - def cookies(self) -> CookiesSetter: ... - def retry_times(self, times: int) -> None: ... def retry_interval(self, interval: float) -> None: ... - def timeouts(self, base: float = None, page_load: float = None, script: float = None) -> None: ... - - def user_agent(self, ua: str, platform: str = None) -> None: ... - - def session_storage(self, item: str, value: Union[str, bool]) -> None: ... - - def local_storage(self, item: str, value: Union[str, bool]) -> None: ... - - def headers(self, headers: Union[dict, str]) -> None: ... - - def auto_handle_alert(self, on_off: bool = True, accept: bool = True) -> None: ... - - def upload_files(self, files: Union[str, Path, list, tuple]) -> None: ... - - def blocked_urls(self, urls: Union[list, tuple, str, None]) -> None: ... + def download_path(self, path: Union[str, Path, None]) -> None: ... -class TabSetter(ChromiumBaseSetter): - _owner: ChromiumTab = ... - - def __init__(self, owner: Union[ChromiumTab, WebPageTab, WebPage, ChromiumPage]): ... - - @property - def window(self) -> WindowSetter: ... - - def download_path(self, path: Union[str, Path]) -> None: ... - - def download_file_name(self, name: str = None, suffix: str = None) -> None: ... - - def when_download_file_exists(self, mode: FILE_EXISTS) -> None: ... - - def activate(self) -> None: ... - - -class ChromiumPageSetter(TabSetter): - _owner: ChromiumPage = ... - - def tab_to_front(self, tab_or_id: Union[str, ChromiumTab] = None) -> None: ... - - def auto_handle_alert(self, on_off: bool = True, accept: bool = True, all_tabs: bool = False) -> None: ... - - -class SessionPageSetter(BasePageSetter): +class SessionPageSetter(BaseSetter): _owner: SessionPage = ... _cookies_setter: Optional[SessionCookiesSetter] = ... @@ -105,7 +52,7 @@ class SessionPageSetter(BasePageSetter): def retry_interval(self, interval: float) -> None: ... - def download_path(self, path: Union[str, Path]) -> None: ... + def download_path(self, path: Union[str, Path, None]) -> None: ... def timeout(self, second: float) -> None: ... @@ -138,8 +85,83 @@ class SessionPageSetter(BasePageSetter): def add_adapter(self, url: str, adapter: HTTPAdapter) -> None: ... -class WebPageSetter(ChromiumPageSetter): - _owner: WebPage = ... +class BrowserBaseSetter(BaseSetter): + _cookies_setter: Optional[CookiesSetter] = ... + + @property + def load_mode(self) -> LoadMode: ... + + def timeouts(self, base=None, page_load=None, script=None) -> None: ... + + +class BrowserSetter(BrowserBaseSetter): + _owner: Chromium = ... + _cookies_setter: BrowserCookiesSetter = ... + + @property + def cookies(self) -> BrowserCookiesSetter: ... + + def auto_handle_alert(self, on_off: bool = True, accept: bool = True): ... + + def download_path(self, path: Union[Path, str, None]): ... + + def download_file_name(self, name: str = None, suffix: str = None): ... + + def when_download_file_exists(self, mode: FILE_EXISTS): ... + + +class ChromiumBaseSetter(BrowserBaseSetter): + _owner: ChromiumBase = ... + _cookies_setter: CookiesSetter = ... + + def __init__(self, owner): ... + + @property + def scroll(self) -> PageScrollSetter: ... + + @property + def cookies(self) -> CookiesSetter: ... + + def user_agent(self, ua: str, platform: str = None) -> None: ... + + def session_storage(self, item: str, value: Union[str, bool]) -> None: ... + + def local_storage(self, item: str, value: Union[str, bool]) -> None: ... + + def headers(self, headers: Union[dict, str]) -> None: ... + + def auto_handle_alert(self, on_off: bool = True, accept: bool = True) -> None: ... + + def upload_files(self, files: Union[str, Path, list, tuple]) -> None: ... + + def blocked_urls(self, urls: Union[list, tuple, str, None]) -> None: ... + + +class TabSetter(ChromiumBaseSetter): + _owner: ChromiumTab = ... + + def __init__(self, owner: Union[ChromiumTab, MixTab, MixPage, ChromiumPage]): ... + + @property + def window(self) -> WindowSetter: ... + + def download_path(self, path: Union[str, Path, None]) -> None: ... + + def download_file_name(self, name: str = None, suffix: str = None) -> None: ... + + def when_download_file_exists(self, mode: FILE_EXISTS) -> None: ... + + def activate(self) -> None: ... + + +class ChromiumPageSetter(TabSetter): + _owner: ChromiumPage = ... + + def auto_handle_alert(self, on_off: bool = True, accept: bool = True, all_tabs: bool = False) -> None: ... + + +class MixPageSetter(ChromiumPageSetter): + _owner: MixPage = ... _session_setter: SessionPageSetter = ... _chromium_setter: ChromiumPageSetter = ... @@ -148,11 +170,11 @@ class WebPageSetter(ChromiumPageSetter): def headers(self, headers: Union[str, dict]) -> None: ... @property - def cookies(self) -> WebPageCookiesSetter: ... + def cookies(self) -> MixPageCookiesSetter: ... -class WebPageTabSetter(TabSetter): - _owner: WebPageTab = ... +class MixTabSetter(TabSetter): + _owner: MixTab = ... _session_setter: SessionPageSetter = ... _chromium_setter: ChromiumBaseSetter = ... @@ -161,14 +183,16 @@ class WebPageTabSetter(TabSetter): def headers(self, headers: Union[str, dict]) -> None: ... @property - def cookies(self) -> WebPageCookiesSetter: ... + def cookies(self) -> MixPageCookiesSetter: ... + + def timeouts(self, base: float = None, page_load: float = None, script: float = None) -> None: ... class ChromiumElementSetter(object): def __init__(self, ele: ChromiumElement): self._ele: ChromiumElement = ... - def attr(self, name: str, value: str) -> None: ... + def attr(self, name: str, value: str = '') -> None: ... def property(self, name: str, value: str) -> None: ... @@ -186,8 +210,9 @@ class ChromiumFrameSetter(ChromiumBaseSetter): class LoadMode(object): - def __init__(self, owner: ChromiumBase): - self._owner: ChromiumBase = ... + _owner: Union[Chromium, ChromiumBase] = ... + + def __init__(self, owner: Union[Chromium, ChromiumBase]): ... def __call__(self, value: str) -> None: ... diff --git a/DrissionPage/_units/states.py b/DrissionPage/_units/states.py index 763e27f..0a664b1 100644 --- a/DrissionPage/_units/states.py +++ b/DrissionPage/_units/states.py @@ -19,31 +19,31 @@ class ElementStates(object): @property def is_selected(self): """返回列表元素是否被选择""" - return self._ele.run_js('return this.selected;') + return self._ele._run_js('return this.selected;') @property def is_checked(self): """返回元素是否被选择""" - return self._ele.run_js('return this.checked;') + 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;') + self._ele._run_js('return this.offsetParent === null;') or self._ele.style('display') == 'none' or self._ele.property('hidden')) @property def is_enabled(self): """返回元素是否可用""" - return not self._ele.run_js('return this.disabled;') + return not self._ele._run_js('return this.disabled;') @property def is_alive(self): """返回元素是否仍在DOM中""" try: - return self._ele.owner.run_cdp('DOM.describeNode', - backendNodeId=self._ele._backend_id)['node']['nodeId'] != 0 + return self._ele.owner._run_cdp('DOM.describeNode', + backendNodeId=self._ele._backend_id)['node']['nodeId'] != 0 except ElementLostError: return False @@ -66,7 +66,7 @@ class ElementStates(object): """返回元素是否被覆盖,与是否在视口中无关,如被覆盖返回覆盖元素的backend id,否则返回False""" lx, ly = self._ele.rect.click_point try: - bid = self._ele.owner.run_cdp('DOM.getNodeForLocation', x=int(lx), y=int(ly)).get('backendNodeId') + bid = self._ele.owner._run_cdp('DOM.getNodeForLocation', x=int(lx), y=int(ly)).get('backendNodeId') return bid if bid != self._ele._backend_id else False except CDPError: return False @@ -95,14 +95,14 @@ class ShadowRootStates(object): @property def is_enabled(self): """返回元素是否可用""" - return not self._ele.run_js('return this.disabled;') + return not self._ele._run_js('return this.disabled;') @property def is_alive(self): """返回元素是否仍在DOM中""" try: - return self._ele.owner.run_cdp('DOM.describeNode', - backendNodeId=self._ele._backend_id)['node']['nodeId'] != 0 + return self._ele.owner._run_cdp('DOM.describeNode', + backendNodeId=self._ele._backend_id)['node']['nodeId'] != 0 except ElementLostError: return False @@ -125,7 +125,7 @@ class PageStates(object): def is_alive(self): """返回页面对象是否仍然可用""" try: - self._owner.run_cdp('Page.getLayoutMetrics') + self._owner._run_cdp('Page.getLayoutMetrics') return True except PageDisconnectedError: return False @@ -157,8 +157,8 @@ class FrameStates(object): def is_alive(self): """返回frame元素是否可用,且里面仍挂载有frame""" try: - node = self._frame._target_page.run_cdp('DOM.describeNode', - backendNodeId=self._frame._frame_ele._backend_id)['node'] + node = self._frame._target_page._run_cdp('DOM.describeNode', + backendNodeId=self._frame._frame_ele._backend_id)['node'] except (ElementLostError, PageDisconnectedError): return False return 'frameId' in node @@ -172,7 +172,7 @@ class FrameStates(object): def is_displayed(self): """返回iframe是否显示""" return not (self._frame.frame_ele.style('visibility') == 'hidden' - or self._frame.frame_ele.run_js('return this.offsetParent === null;') + or self._frame.frame_ele._run_js('return this.offsetParent === null;') or self._frame.frame_ele.style('display') == 'none') @property diff --git a/DrissionPage/_units/waiter.py b/DrissionPage/_units/waiter.py index 35bf3b4..d988c01 100644 --- a/DrissionPage/_units/waiter.py +++ b/DrissionPage/_units/waiter.py @@ -13,6 +13,9 @@ from ..errors import WaitTimeoutError, NoRectError class OriginWaiter(object): + def __init__(self, owner): + self._owner = owner + def __call__(self, second, scope=None): """等待若干秒,如传入两个参数,等待时间为这两个数间的一个随机数 :param second: 秒数 @@ -24,14 +27,86 @@ class OriginWaiter(object): else: from random import uniform sleep(uniform(second, scope)) + return self._owner + + +class BrowserWaiter(OriginWaiter): + + def new_tab(self, timeout=None, curr_tab=None, raise_err=None): + """等待新标签页出现 + :param timeout: 超时时间(秒),为None则使用页面对象timeout属性 + :param curr_tab: 指定当前最新的tab id,用于判断新tab出现,为None自动获取 + :param raise_err: 等待失败时是否报错,为None时根据Settings设置 + :return: 等到新标签页返回其id,否则返回False + """ + curr_tid = curr_tab if curr_tab else self._owner.tab_ids[0] + timeout = timeout if timeout is not None else self._owner.timeout + end_time = perf_counter() + timeout + while perf_counter() < end_time: + latest_tid = self._owner.tab_ids[0] + if curr_tid != latest_tid: + return latest_tid + sleep(.01) + + if raise_err is True or Settings.raise_when_wait_failed is True: + raise WaitTimeoutError(f'等待新标签页失败(等待{timeout}秒)。') + else: + return False + + def download_begin(self, timeout=None, cancel_it=False): + """等待浏览器下载开始,可将其拦截 + :param timeout: 超时时间(秒),None使用页面对象超时时间 + :param cancel_it: 是否取消该任务 + :return: 成功返回任务对象,失败返回False + """ + if not self._owner._dl_mgr._running: + raise RuntimeError('此功能需显式设置下载路径(使用set.download_path()方法、配置对象或ini文件均可)。') + self._owner._dl_mgr.set_flag('browser', False if cancel_it else True) + if timeout is None: + timeout = self._owner.timeout + + r = False + end_time = perf_counter() + timeout + while perf_counter() < end_time: + v = self._owner._dl_mgr.get_flag('browser') + if not isinstance(v, bool): + r = v + break + sleep(.005) + + self._owner._dl_mgr.set_flag('browser', None) + return r + + def all_downloads_done(self, timeout=None, cancel_if_timeout=True): + """等待所有浏览器下载任务结束 + :param timeout: 超时时间(秒),为None时无限等待 + :param cancel_if_timeout: 超时时是否取消剩余任务 + :return: 是否等待成功 + """ + if not self._owner._dl_mgr._running: + raise RuntimeError('此功能需显式设置下载路径(使用set.download_path()方法、配置对象或ini文件均可)。') + if not timeout: + while self._owner._dl_mgr._missions: + sleep(.5) + return True + + else: + end_time = perf_counter() + timeout + while perf_counter() < end_time: + if not self._owner._dl_mgr._missions: + return True + sleep(.5) + + if self._owner._dl_mgr._missions: + if cancel_if_timeout: + for m in list(self._owner._dl_mgr._missions.values()): + m.cancel() + return False + else: + return True class BaseWaiter(OriginWaiter): - def __init__(self, page_or_ele): - """ - :param page_or_ele: 页面对象或元素对象 - """ - self._driver = page_or_ele def ele_deleted(self, loc_or_ele, timeout=None, raise_err=None): """等待元素从DOM中删除 @@ -40,7 +115,7 @@ class BaseWaiter(OriginWaiter): :param raise_err: 等待失败时是否报错,为None时根据Settings设置 :return: 是否等待成功 """ - ele = self._driver._ele(loc_or_ele, raise_err=False, timeout=0) + ele = self._owner._ele(loc_or_ele, raise_err=False, timeout=0) return ele.wait.deleted(timeout, raise_err=raise_err) if ele else True def ele_displayed(self, loc_or_ele, timeout=None, raise_err=None): @@ -51,9 +126,9 @@ class BaseWaiter(OriginWaiter): :return: 是否等待成功 """ if timeout is None: - timeout = self._driver.timeout + timeout = self._owner.timeout end_time = perf_counter() + timeout - ele = self._driver._ele(loc_or_ele, raise_err=False, timeout=timeout) + ele = self._owner._ele(loc_or_ele, raise_err=False, timeout=timeout) timeout = end_time - perf_counter() if timeout <= 0: if raise_err is True or Settings.raise_when_wait_failed is True: @@ -70,9 +145,9 @@ class BaseWaiter(OriginWaiter): :return: 是否等待成功 """ if timeout is None: - timeout = self._driver.timeout + timeout = self._owner.timeout end_time = perf_counter() + timeout - ele = self._driver._ele(loc_or_ele, raise_err=False, timeout=timeout) + ele = self._owner._ele(loc_or_ele, raise_err=False, timeout=timeout) timeout = end_time - perf_counter() if timeout <= 0: if raise_err is True or Settings.raise_when_wait_failed is True: @@ -121,10 +196,10 @@ class BaseWaiter(OriginWaiter): else [get_loc(l)[1] for l in locators]) method = any if any_one else all - timeout = self._driver.timeout if timeout is None else timeout + timeout = self._owner.timeout if timeout is None else timeout end_time = perf_counter() + timeout while perf_counter() < end_time: - if method([_find(l, self._driver.driver) for l in locators]): + if method([_find(l, self._owner.driver) for l in locators]): return True sleep(.01) if raise_err is True or Settings.raise_when_wait_failed is True: @@ -150,9 +225,9 @@ class BaseWaiter(OriginWaiter): def upload_paths_inputted(self): """等待自动填写上传文件路径""" - end_time = perf_counter() + self._driver.timeout + end_time = perf_counter() + self._owner.timeout while perf_counter() < end_time: - if not self._driver._upload_list: + if not self._owner._upload_list: return True sleep(.01) return False @@ -163,22 +238,22 @@ class BaseWaiter(OriginWaiter): :param cancel_it: 是否取消该任务 :return: 成功返回任务对象,失败返回False """ - if not self._driver.browser._dl_mgr._running: + if not self._owner.browser._dl_mgr._running: raise RuntimeError('此功能需显式设置下载路径(使用set.download_path()方法、配置对象或ini文件均可)。') - self._driver.browser._dl_mgr.set_flag(self._driver.tab_id, False if cancel_it else True) + self._owner.browser._dl_mgr.set_flag(self._owner.tab_id, False if cancel_it else True) if timeout is None: - timeout = self._driver.timeout + timeout = self._owner.timeout r = False end_time = perf_counter() + timeout while perf_counter() < end_time: - v = self._driver.browser._dl_mgr.get_flag(self._driver.tab_id) + v = self._owner.browser._dl_mgr.get_flag(self._owner.tab_id) if not isinstance(v, bool): r = v break sleep(.005) - self._driver.browser._dl_mgr.set_flag(self._driver.tab_id, None) + self._owner.browser._dl_mgr.set_flag(self._owner.tab_id, None) return r def url_change(self, text, exclude=False, timeout=None, raise_err=None): @@ -187,19 +262,19 @@ class BaseWaiter(OriginWaiter): :param exclude: 是否排除,为True时当url不包含text指定文本时返回True :param timeout: 超时时间(秒) :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 等待成功返回页面对象,否则返回False """ - return self._change('url', text, exclude, timeout, raise_err) + return self._owner if self._change('url', text, exclude, timeout, raise_err) else False def title_change(self, text, exclude=False, timeout=None, raise_err=None): """等待title变成包含或不包含指定文本 :param text: 用于识别的文本 :param exclude: 是否排除,为True时当title不包含text指定文本时返回True - :param timeout: 超时时间(秒) + :param timeout: 超时时间(秒),为None使用页面设置 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 等待成功返回页面对象,否则返回False """ - return self._change('title', text, exclude, timeout, raise_err) + return self._owner if self._change('title', text, exclude, timeout, raise_err) else False def _change(self, arg, text, exclude=False, timeout=None, raise_err=None): """等待指定属性变成包含或不包含指定文本 @@ -210,18 +285,26 @@ class BaseWaiter(OriginWaiter): :param raise_err: 等待失败时是否报错,为None时根据Settings设置 :return: 是否等待成功 """ + + def do(): + if arg == 'url': + v = self._owner.url + elif arg == 'title': + v = self._owner.title + else: + raise ValueError + if (not exclude and text in v) or (exclude and text not in v): + return True + + if do(): + return True + if timeout is None: - timeout = self._driver.timeout + timeout = self._owner.timeout end_time = perf_counter() + timeout while perf_counter() < end_time: - if arg == 'url': - val = self._driver.url - elif arg == 'title': - val = self._driver.title - else: - raise ValueError - if (not exclude and text in val) or (exclude and text not in val): + if do(): return True sleep(.05) @@ -238,22 +321,22 @@ class BaseWaiter(OriginWaiter): :param raise_err: 等待失败时是否报错,为None时根据Settings设置 :return: 是否等待成功 """ - if timeout != 0: - if timeout is None or timeout is True: - timeout = self._driver.timeout - end_time = perf_counter() + timeout - while perf_counter() < end_time: - if self._driver._is_loading == start: - return True - sleep(gap) + timeout = timeout if timeout is not None else self._owner.timeout + timeout = .1 if timeout <= 0 else timeout + end_time = perf_counter() + timeout + while perf_counter() < end_time: + if self._owner._is_loading == start: + return True + sleep(gap) - if raise_err is True or Settings.raise_when_wait_failed is True: - raise WaitTimeoutError(f'等待页面加载失败(等待{timeout}秒)。') - else: - return False + if raise_err is True or Settings.raise_when_wait_failed is True: + raise WaitTimeoutError(f'等待页面加载失败(等待{timeout}秒)。') + else: + return False class TabWaiter(BaseWaiter): + """标签页对象等待对象""" def downloads_done(self, timeout=None, cancel_if_timeout=True): """等待所有浏览器下载任务结束 @@ -261,23 +344,23 @@ class TabWaiter(BaseWaiter): :param cancel_if_timeout: 超时时是否取消剩余任务 :return: 是否等待成功 """ - if not self._driver.browser._dl_mgr._running: + if not self._owner.browser._dl_mgr._running: raise RuntimeError('此功能需显式设置下载路径(使用set.download_path()方法、配置对象或ini文件均可)。') if not timeout: - while self._driver.browser._dl_mgr.get_tab_missions(self._driver.tab_id): + while self._owner.browser._dl_mgr.get_tab_missions(self._owner.tab_id): sleep(.5) return True else: end_time = perf_counter() + timeout while perf_counter() < end_time: - if not self._driver.browser._dl_mgr.get_tab_missions(self._driver.tab_id): + if not self._owner.browser._dl_mgr.get_tab_missions(self._owner.tab_id): return True sleep(.5) - if self._driver.browser._dl_mgr.get_tab_missions(self._driver.tab_id): + if self._owner.browser._dl_mgr.get_tab_missions(self._owner.tab_id): if cancel_if_timeout: - for m in self._driver.browser._dl_mgr.get_tab_missions(self._driver.tab_id): + for m in self._owner.browser._dl_mgr.get_tab_missions(self._owner.tab_id): m.cancel() return False else: @@ -285,15 +368,14 @@ class TabWaiter(BaseWaiter): def alert_closed(self): """等待弹出框关闭""" - while not self._driver.states.has_alert: + while not self._owner.states.has_alert: sleep(.2) - while self._driver.states.has_alert: + while self._owner.states.has_alert: sleep(.2) class PageWaiter(TabWaiter): - def __init__(self, page): - super().__init__(page) + """ChromiumPage和MixPage的等待对象""" def new_tab(self, timeout=None, raise_err=None): """等待新标签页出现 @@ -301,18 +383,7 @@ class PageWaiter(TabWaiter): :param raise_err: 等待失败时是否报错,为None时根据Settings设置 :return: 等到新标签页返回其id,否则返回False """ - timeout = timeout if timeout is not None else self._driver.timeout - end_time = perf_counter() + timeout - while perf_counter() < end_time: - latest_tid = self._driver.tab_ids[0] - if self._driver.tab_id != latest_tid: - return latest_tid - sleep(.01) - - if raise_err is True or Settings.raise_when_wait_failed is True: - raise WaitTimeoutError(f'等待新标签页失败(等待{timeout}秒)。') - else: - return False + return self._owner.browser.wait.new_tab(timeout=timeout, raise_err=raise_err) def all_downloads_done(self, timeout=None, cancel_if_timeout=True): """等待所有浏览器下载任务结束 @@ -320,45 +391,26 @@ class PageWaiter(TabWaiter): :param cancel_if_timeout: 超时时是否取消剩余任务 :return: 是否等待成功 """ - if not self._driver.browser._dl_mgr._running: - raise RuntimeError('此功能需显式设置下载路径(使用set.download_path()方法、配置对象或ini文件均可)。') - if not timeout: - while self._driver.browser._dl_mgr._missions: - sleep(.5) - return True - - else: - end_time = perf_counter() + timeout - while perf_counter() < end_time: - if not self._driver.browser._dl_mgr._missions: - return True - sleep(.5) - - if self._driver.browser._dl_mgr._missions: - if cancel_if_timeout: - for m in list(self._driver.browser._dl_mgr._missions.values()): - m.cancel() - return False - else: - return True + return self._owner.browser.wait.all_downloads_done(timeout=timeout, cancel_if_timeout=cancel_if_timeout) class ElementWaiter(OriginWaiter): """等待元素在dom中某种状态,如删除、显示、隐藏""" - def __init__(self, owner, ele): - """等待元素在dom中某种状态,如删除、显示、隐藏 - :param owner: 元素所在页面 - :param ele: 要等待的元素 - """ - self._owner = owner - self._ele = ele + def __init__(self, owner): + super().__init__(owner) + self._ele = owner + + @property + def _timeout(self): + """返回超时设置""" + return self._ele.owner.timeout def deleted(self, timeout=None, raise_err=None): """等待元素从dom删除 :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ return self._wait_state('is_alive', False, timeout, raise_err, err_text='等待元素被删除失败。') @@ -366,7 +418,7 @@ class ElementWaiter(OriginWaiter): """等待元素从dom显示 :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ return self._wait_state('is_displayed', True, timeout, raise_err, err_text='等待元素显示失败。') @@ -374,7 +426,7 @@ class ElementWaiter(OriginWaiter): """等待元素从dom隐藏 :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ return self._wait_state('is_displayed', False, timeout, raise_err, err_text='等待元素隐藏失败。') @@ -390,7 +442,7 @@ class ElementWaiter(OriginWaiter): """等待当前元素不被遮盖 :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ return self._wait_state('is_covered', False, timeout, raise_err, err_text='等待元素不被覆盖失败。') @@ -398,7 +450,7 @@ class ElementWaiter(OriginWaiter): """等待当前元素变成可用 :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ return self._wait_state('is_enabled', True, timeout, raise_err, err_text='等待元素变成可用失败。') @@ -406,7 +458,7 @@ class ElementWaiter(OriginWaiter): """等待当前元素变成不可用 :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ return self._wait_state('is_enabled', False, timeout, raise_err, err_text='等待元素变成不可用失败。') @@ -414,14 +466,17 @@ class ElementWaiter(OriginWaiter): """等待当前元素变成不可用或从DOM移除 :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ + if not self._ele.states.is_enabled or not self._ele.states.is_alive: + return self._ele + if timeout is None: - timeout = self._owner.timeout + timeout = self._timeout end_time = perf_counter() + timeout while perf_counter() < end_time: if not self._ele.states.is_enabled or not self._ele.states.is_alive: - return True + return self._ele sleep(.05) if raise_err is True or Settings.raise_when_wait_failed is True: @@ -434,10 +489,13 @@ class ElementWaiter(OriginWaiter): :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param gap: 检测间隔时间 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ if timeout is None: - timeout = self._owner.timeout + timeout = self._timeout + if timeout <= 0: + timeout = .1 + end_time = perf_counter() + timeout while perf_counter() < end_time: try: @@ -453,7 +511,7 @@ class ElementWaiter(OriginWaiter): while perf_counter() < end_time: sleep(gap) if self._ele.rect.size == size and self._ele.rect.location == location: - return True + return self._ele size = self._ele.rect.size location = self._ele.rect.location @@ -467,11 +525,12 @@ class ElementWaiter(OriginWaiter): :param wait_moved: 是否等待元素运动结束 :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ + timeout = timeout if timeout is not None else self._timeout t1 = perf_counter() r = self._wait_state('is_clickable', True, timeout, raise_err, err_text='等待元素可点击失败(等{}秒)。') - r = self.stop_moving(timeout=perf_counter() - t1) if wait_moved and r else r + r = self.stop_moving(timeout=timeout - perf_counter() + t1) if wait_moved and r else r if raise_err and not r: raise WaitTimeoutError(f'等待元素可点击失败(等{timeout}秒)。') return r @@ -491,19 +550,19 @@ class ElementWaiter(OriginWaiter): :param timeout: 超时时间(秒),为None使用元素所在页面timeout属性 :param raise_err: 等待失败时是否报错,为None时根据Settings设置 :param err_text: 抛出错误时显示的信息 - :return: 是否等待成功 + :return: 成功返回元素对象,失败返回False """ a = self._ele.states.__getattribute__(attr) if (a and mode) or (not a and not mode): - return True if isinstance(a, bool) else a + return self._ele if isinstance(a, bool) else a if timeout is None: - timeout = self._owner.timeout + timeout = self._timeout end_time = perf_counter() + timeout while perf_counter() < end_time: a = self._ele.states.__getattribute__(attr) if (a and mode) or (not a and not mode): - return True if isinstance(a, bool) else a + return self._ele if isinstance(a, bool) else a sleep(.05) err_text = err_text or '等待元素状态改变失败(等待{}秒)。' @@ -514,9 +573,11 @@ class ElementWaiter(OriginWaiter): class FrameWaiter(BaseWaiter, ElementWaiter): - def __init__(self, frame): - """ - :param frame: ChromiumFrame对象 - """ - super().__init__(frame) - super(BaseWaiter, self).__init__(frame, frame.frame_ele) + def __init__(self, owner): + super().__init__(owner) + self._ele = owner.frame_ele + + @property + def _timeout(self): + """返回超时设置""" + return self._owner.timeout diff --git a/DrissionPage/_units/waiter.pyi b/DrissionPage/_units/waiter.pyi index f05ca94..9e3a70f 100644 --- a/DrissionPage/_units/waiter.pyi +++ b/DrissionPage/_units/waiter.pyi @@ -5,24 +5,42 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ -from typing import Union, Tuple, Literal, List +from typing import Union, Tuple, List from .downloader import DownloadMission +from .._base.browser import Chromium from .._elements.chromium_element import ChromiumElement from .._pages.chromium_base import ChromiumBase from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_page import ChromiumPage +from .._pages.mix_page import MixPage +from .._pages.tabs import ChromiumTab, MixTab class OriginWaiter(object): - def __call__(self, second: float, scope: float = None) -> None: ... + _owner = ... + + def __init__(self, owner): ... + + def __call__(self, second: float, scope: float = None): ... + + +class BrowserWaiter(OriginWaiter): + _owner: Chromium = ... + + def __init__(self, owner: Chromium): ... + + def __call__(self, second: float, scope: float = None) -> Chromium: ... + + def download_begin(self, timeout: float = None, cancel_it: bool = False) -> DownloadMission: ... + + def new_tab(self, timeout: float = None, curr_tab: str = None, raise_err: bool = None) -> Union[str, bool]: ... + + def all_downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ... class BaseWaiter(OriginWaiter): - def __init__(self, page: ChromiumBase): - self._driver: ChromiumBase = ... - - def __call__(self, second: float, scope: float = None) -> None: ... + _owner: ChromiumBase = ... def ele_deleted(self, loc_or_ele: Union[str, tuple, ChromiumElement], @@ -64,58 +82,94 @@ class BaseWaiter(OriginWaiter): class TabWaiter(BaseWaiter): + _owner: Union[ChromiumTab, MixTab] = ... + + def __init__(self, owner: Union[ChromiumTab, MixTab]): ... + + def __call__(self, second: float, scope: float = None) -> Union[ChromiumTab, MixTab]: ... def downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ... def alert_closed(self) -> None: ... + def url_change(self, text: str, exclude: bool = False, + timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumTab, MixTab]: ... + + def title_change(self, text: str, exclude: bool = False, + timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumTab, MixTab]: ... + class PageWaiter(TabWaiter): - _driver: ChromiumPage = ... + _owner: Union[ChromiumPage, MixPage] = ... + + def __init__(self, owner: Union[ChromiumPage, MixPage]): ... + + def __call__(self, second: float, scope: float = None) -> Union[ChromiumPage, MixPage]: ... def new_tab(self, timeout: float = None, raise_err: bool = None) -> Union[str, bool]: ... def all_downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ... + def url_change(self, text: str, exclude: bool = False, + timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumPage, MixPage]: ... + + def title_change(self, text: str, exclude: bool = False, + timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumPage, MixPage]: ... + class ElementWaiter(OriginWaiter): - def __init__(self, owner: ChromiumBase, ele: ChromiumElement): - self._ele: ChromiumElement = ... - self._owner: ChromiumBase = ... + _owner: ChromiumElement = ... + _ele: ChromiumElement = ... - def __call__(self, second: float, scope: float = None) -> None: ... + def __init__(self, owner: ChromiumElement): ... - def deleted(self, timeout: float = None, raise_err: bool = None) -> bool: ... + def __call__(self, second: float, scope: float = None) -> ChromiumElement: ... - def displayed(self, timeout: float = None, raise_err: bool = None) -> bool: ... + @property + def _timeout(self) -> float: ... - def hidden(self, timeout: float = None, raise_err: bool = None) -> bool: ... + def deleted(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ... - def covered(self, timeout: float = None, raise_err: bool = None) -> Union[Literal[False], int]: ... + def displayed(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ... - def not_covered(self, timeout: float = None, raise_err: bool = None) -> bool: ... + def hidden(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ... - def enabled(self, timeout: float = None, raise_err: bool = None) -> bool: ... + def covered(self, timeout: float = None, raise_err: bool = None) -> Union[False, int]: ... - def disabled(self, timeout: float = None, raise_err: bool = None) -> bool: ... + def not_covered(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ... - def clickable(self, wait_moved: bool = True, timeout: float = None, raise_err: bool = None) -> bool: ... + def enabled(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ... + + def disabled(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ... + + def clickable(self, wait_moved: bool = True, + timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ... def has_rect(self, timeout: float = None, - raise_err: bool = None) -> Union[Literal[False], List[Tuple[float, float]]]: ... + raise_err: bool = None) -> Union[False, List[Tuple[float, float]]]: ... def disabled_or_deleted(self, timeout: float = None, raise_err: bool = None) -> bool: ... - def stop_moving(self, timeout: float = None, gap: float = .1, raise_err: bool = None) -> bool: ... + def stop_moving(self, timeout: float = None, gap: float = .1, raise_err: bool = None) -> Union[ChromiumElement, False]: ... def _wait_state(self, attr: str, mode: bool = False, timeout: float = None, raise_err: bool = None, - err_text: str = None) -> bool: ... + err_text: str = None) -> Union[ChromiumElement, False]: ... class FrameWaiter(BaseWaiter, ElementWaiter): - def __init__(self, frame: ChromiumFrame): ... + _owner: ChromiumFrame = ... + + def __init__(self, owner: ChromiumFrame): ... + + def __call__(self, second: float, scope: float = None) -> ChromiumFrame: ... + + def url_change(self, text: str, exclude: bool = False, + timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumFrame]: ... + + def title_change(self, text: str, exclude: bool = False, + timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumFrame]: ... diff --git a/DrissionPage/common.py b/DrissionPage/common.py index eb9adac..e7a0717 100644 --- a/DrissionPage/common.py +++ b/DrissionPage/common.py @@ -5,6 +5,7 @@ @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @License : BSD 3-Clause. """ +from ._base.browser import Chromium from ._elements.session_element import make_session_ele from ._functions.by import By from ._functions.elements import get_eles @@ -12,7 +13,7 @@ from ._functions.keys import Keys from ._functions.settings import Settings from ._functions.tools import wait_until, configs_to_here from ._functions.web import get_blob, tree -from ._pages.chromium_page import ChromiumPage + from ._units.actions import Actions __all__ = ['make_session_ele', 'Actions', 'Keys', 'By', 'Settings', 'wait_until', 'configs_to_here', 'get_blob', @@ -20,15 +21,15 @@ __all__ = ['make_session_ele', 'Actions', 'Keys', 'By', 'Settings', 'wait_until' def from_selenium(driver): - """从selenium的WebDriver对象生成ChromiumPage对象""" + """从selenium的WebDriver对象生成Chromium对象""" address, port = driver.caps.get('goog:chromeOptions', {}).get('debuggerAddress', ':').split(':') if not address: raise RuntimeError('获取失败。') - return ChromiumPage(f'{address}:{port}') + return Chromium(f'{address}:{port}') def from_playwright(page_or_browser): - """从playwright的Page或Browser对象生成ChromiumPage对象""" + """从playwright的Page或Browser对象生成Chromium对象""" if hasattr(page_or_browser, 'context'): page_or_browser = page_or_browser.context.browser try: @@ -49,4 +50,4 @@ def from_playwright(page_or_browser): break else: raise RuntimeError('获取失败。') - return ChromiumPage(f'127.0.0.1:{port}') + return Chromium(f'127.0.0.1:{port}') diff --git a/DrissionPage/items.py b/DrissionPage/items.py index 0715e91..6881812 100644 --- a/DrissionPage/items.py +++ b/DrissionPage/items.py @@ -9,7 +9,7 @@ from ._elements.chromium_element import ChromiumElement, ShadowRoot from ._elements.none_element import NoneElement from ._elements.session_element import SessionElement from ._pages.chromium_frame import ChromiumFrame -from ._pages.chromium_tab import ChromiumTab, WebPageTab +from ._pages.tabs import ChromiumTab, MixTab __all__ = ['ChromiumElement', 'ShadowRoot', 'NoneElement', 'SessionElement', 'ChromiumFrame', 'ChromiumTab', - 'WebPageTab'] + 'MixTab']