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}{tag}>'
@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']