Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
lyousan 2024-07-23 01:38:44 +08:00
commit 5baa9f217d
63 changed files with 2966 additions and 2203 deletions

View File

@ -9,3 +9,6 @@
2. 请附上代码和报错信息(如有) 2. 请附上代码和报错信息(如有)
3. DrissionPage、浏览器、python版本号是多少 3. DrissionPage、浏览器、python版本号是多少
4. 有什么意见建议? 4. 有什么意见建议?
请在下方写正文,不要把内容插入到上面的问题中。
---

View File

@ -5,13 +5,13 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from ._pages.chromium_page import ChromiumPage from ._base.browser import Chromium
from ._pages.session_page import SessionPage
from ._pages.web_page import WebPage
# 启动配置类
from ._configs.chromium_options import ChromiumOptions from ._configs.chromium_options import ChromiumOptions
from ._configs.session_options import SessionOptions from ._configs.session_options import SessionOptions
from ._pages.session_page import SessionPage
__all__ = ['ChromiumPage', 'ChromiumOptions', 'SessionOptions', 'SessionPage', 'WebPage', '__version__'] from ._pages.chromium_page import ChromiumPage
__version__ = '4.0.5.4' from ._pages.mix_page import MixPage
from ._pages.mix_page import MixPage as WebPage
__version__ = '4.1.0.0b11'

18
DrissionPage/__init__.pyi Normal file
View File

@ -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 = ...

View File

@ -12,10 +12,11 @@ from urllib.parse import quote
from DownloadKit import DownloadKit 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 .._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 from ..errors import ElementNotFoundError
@ -54,7 +55,6 @@ class BaseElement(BaseParser):
def __init__(self, owner=None): def __init__(self, owner=None):
self.owner = owner self.owner = owner
self.page = owner._page if owner else None
self._type = 'BaseElement' self._type = 'BaseElement'
# ----------------以下属性或方法由后代实现---------------- # ----------------以下属性或方法由后代实现----------------
@ -71,6 +71,16 @@ class BaseElement(BaseParser):
def nexts(self): def nexts(self):
pass 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): def _ele(self, locator, timeout=None, index=1, relative=False, raise_err=None, method=None):
"""调用获取元素的方法 """调用获取元素的方法
:param locator: 定位符 :param locator: 定位符
@ -81,6 +91,8 @@ class BaseElement(BaseParser):
:param method: 调用的方法名 :param method: 调用的方法名
:return: 元素对象或它们组成的列表 :return: 元素对象或它们组成的列表
""" """
if hasattr(locator, '_type'):
return locator
r = self._find_elements(locator, timeout=timeout, index=index, relative=relative, raise_err=raise_err) r = self._find_elements(locator, timeout=timeout, index=index, relative=relative, raise_err=raise_err)
if r or isinstance(r, list): if r or isinstance(r, list):
return r return r
@ -120,11 +132,8 @@ class DrissionElement(BaseElement):
:param text_node_only: 是否只返回文本节点 :param text_node_only: 是否只返回文本节点
:return: 文本列表 :return: 文本列表
""" """
if text_node_only: texts = self.eles('xpath:/text()') if text_node_only else [x if isinstance(x, str) else x.text
texts = self.eles('xpath:/text()') for x in self.eles('xpath:./text() | *')]
else:
texts = [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) != ''] 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): def parent(self, level_or_loc=1, index=1):
@ -138,10 +147,8 @@ class DrissionElement(BaseElement):
elif isinstance(level_or_loc, (tuple, str)): elif isinstance(level_or_loc, (tuple, str)):
loc = get_loc(level_or_loc, True) loc = get_loc(level_or_loc, True)
if loc[0] == 'css selector': if loc[0] == 'css selector':
raise ValueError('此css selector语法不受支持请换成xpath。') raise ValueError('此css selector语法不受支持请换成xpath。')
loc = f'xpath:./ancestor::{loc[1].lstrip(". / ")}[{index}]' loc = f'xpath:./ancestor::{loc[1].lstrip(". / ")}[{index}]'
else: else:
@ -345,7 +352,6 @@ class BasePage(BaseParser):
def __init__(self): def __init__(self):
"""初始化函数""" """初始化函数"""
self._url = None self._url = None
self._timeout = 10
self._url_available = None self._url_available = None
self.retry_times = 3 self.retry_times = 3
self.retry_interval = 2 self.retry_interval = 2
@ -361,16 +367,6 @@ class BasePage(BaseParser):
ele = self._ele('xpath://title', raise_err=False, method='title') ele = self._ele('xpath://title', raise_err=False, method='title')
return ele.text if ele else None return ele.text if ele else None
@property
def timeout(self):
"""返回查找元素时等待的秒数"""
return self._timeout
@timeout.setter
def timeout(self, second):
"""设置查找元素时等待的秒数"""
self._timeout = second
@property @property
def url_available(self): def url_available(self):
"""返回当前访问的url有效性""" """返回当前访问的url有效性"""
@ -420,10 +416,6 @@ class BasePage(BaseParser):
def user_agent(self): def user_agent(self):
return return
@abstractmethod
def cookies(self, as_dict=False, all_info=False):
return {}
@abstractmethod @abstractmethod
def get(self, url, show_errmsg=False, retry=None, interval=None): def get(self, url, show_errmsg=False, retry=None, interval=None):
pass pass

View File

@ -15,7 +15,7 @@ from .._elements.session_element import SessionElement
from .._functions.elements import SessionElementsList from .._functions.elements import SessionElementsList
from .._pages.chromium_page import ChromiumPage from .._pages.chromium_page import ChromiumPage
from .._pages.session_page import SessionPage from .._pages.session_page import SessionPage
from .._pages.web_page import WebPage from .._pages.mix_page import MixPage
class BaseParser(object): class BaseParser(object):
@ -59,7 +59,6 @@ class BaseElement(BaseParser):
def __init__(self, owner: BasePage = None): def __init__(self, owner: BasePage = None):
self.owner: BasePage = ... self.owner: BasePage = ...
self.page: Union[ChromiumPage, SessionPage, WebPage] = ...
# ----------------以下属性或方法由后代实现---------------- # ----------------以下属性或方法由后代实现----------------
@property @property
@ -200,22 +199,15 @@ class BasePage(BaseParser):
self._url_available: bool = ... self._url_available: bool = ...
self.retry_times: int = ... self.retry_times: int = ...
self.retry_interval: float = ... self.retry_interval: float = ...
self._timeout: float = ...
self._download_path: str = ... self._download_path: str = ...
self._DownloadKit: DownloadKit = ... self._DownloadKit: DownloadKit = ...
self._none_ele_return_value: bool = ... self._none_ele_return_value: bool = ...
self._none_ele_value: Any = ... self._none_ele_value: Any = ...
self._page: Union[ChromiumPage, SessionPage, WebPage]=... self._page: Union[ChromiumPage, SessionPage, MixPage] = ...
@property @property
def title(self) -> Union[str, None]: ... def title(self) -> Union[str, None]: ...
@property
def timeout(self) -> float: ...
@timeout.setter
def timeout(self, second: float) -> None: ...
@property @property
def url_available(self) -> bool: ... def url_available(self) -> bool: ...
@ -237,9 +229,6 @@ class BasePage(BaseParser):
@property @property
def user_agent(self) -> str: ... def user_agent(self) -> str: ...
@abstractmethod
def cookies(self, as_dict: bool = False, all_info: bool = False) -> Union[list, dict]: ...
@abstractmethod @abstractmethod
def get(self, url: str, show_errmsg: bool = False, retry: int = None, interval: float = None): ... def get(self, url: str, show_errmsg: bool = False, retry: int = None, interval: float = None): ...

View File

@ -7,54 +7,100 @@
""" """
from pathlib import Path from pathlib import Path
from shutil import rmtree 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 websocket import WebSocketBadStatusException
from .driver import BrowserDriver, Driver 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 .._functions.tools import raise_error
from .._pages.chromium_base import Timeout
from .._pages.tabs import ChromiumTab, MixTab
from .._units.downloader import DownloadManager from .._units.downloader import DownloadManager
from .._units.setter import BrowserSetter
from .._units.waiter import BrowserWaiter
from ..errors import BrowserConnectError, CDPError
from ..errors import PageDisconnectedError from ..errors import PageDisconnectedError
__ERROR__ = 'error' __ERROR__ = 'error'
class Browser(object): class Chromium(object):
BROWSERS = {} _BROWSERS = {}
_lock = Lock()
def __new__(cls, address, browser_id, page): def __new__(cls, addr_or_opts=None, session_options=None):
""" """
:param address: 浏览器地址 :param addr_or_opts: 浏览器地址:端口ChromiumOptions对象或端口数字int
:param browser_id: 浏览器id :param session_options: 使用双模Tab时使用的默认Session配置为True使用ini文件配置
:param page: ChromiumPage对象
""" """
if browser_id in cls.BROWSERS: opt = handle_options(addr_or_opts)
return cls.BROWSERS[browser_id] is_headless, browser_id, is_exists = run_browser(opt)
return object.__new__(cls) 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 addr_or_opts: 浏览器地址:端口ChromiumOptions对象或端口数字int
:param browser_id: 浏览器id :param session_options: 使用双模Tab时使用的默认Session配置为True使用ini文件配置
:param page: ChromiumPage对象
""" """
if hasattr(self, '_created'): if hasattr(self, '_created'):
return return
self._created = True self._created = True
Browser.BROWSERS[browser_id] = self
self.page = page self._type = 'Chromium'
self.address = address
self._driver = BrowserDriver(browser_id, 'browser', address, self)
self.id = browser_id
self._frames = {} self._frames = {}
self._drivers = {} self._drivers = {}
self._all_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 self._process_id = None
try: try:
r = self.run_cdp('SystemInfo.getProcessInfo') r = self._run_cdp('SystemInfo.getProcessInfo')
for i in r.get('processInfo', []): for i in r.get('processInfo', []):
if i['type'] == 'browser': if i['type'] == 'browser':
self._process_id = i['id'] self._process_id = i['id']
@ -62,9 +108,348 @@ class Browser(object):
except: except:
pass 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.targetDestroyed', self._onTargetDestroyed)
self._driver.set_callback('Target.targetCreated', self._onTargetCreated) 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: 标签页idstrTab对象或标签页序号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): def _get_driver(self, tab_id, owner=None):
"""新建并返回指定tab id的Driver """新建并返回指定tab id的Driver
@ -95,8 +480,7 @@ class Browser(object):
def _onTargetDestroyed(self, **kwargs): def _onTargetDestroyed(self, **kwargs):
"""标签页关闭时执行""" """标签页关闭时执行"""
tab_id = kwargs['targetId'] 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]: for key in [k for k, i in self._frames.items() if i == tab_id]:
self._frames.pop(key, None) self._frames.pop(key, None)
for d in self._all_drivers.get(tab_id, tuple()): for d in self._all_drivers.get(tab_id, tuple()):
@ -104,13 +488,7 @@ class Browser(object):
self._drivers.pop(tab_id, None) self._drivers.pop(tab_id, None)
self._all_drivers.pop(tab_id, None) self._all_drivers.pop(tab_id, None)
def connect_to_page(self): def _run_cdp(self, cmd, **cmd_args):
"""执行与page相关的逻辑"""
if not self._connected:
self._dl_mgr = DownloadManager(self)
self._connected = True
def run_cdp(self, cmd, **cmd_args):
"""执行Chrome DevTools Protocol语句 """执行Chrome DevTools Protocol语句
:param cmd: 协议项目 :param cmd: 协议项目
:param cmd_args: 参数 :param cmd_args: 参数
@ -120,166 +498,10 @@ class Browser(object):
r = self._driver.run(cmd, **cmd_args) 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, 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): def _on_disconnect(self):
self.page._on_disconnect() Chromium._BROWSERS.pop(self.id, None)
Browser.BROWSERS.pop(self.id, None) if self._chromium_options.is_auto_port and self._chromium_options.user_data_path:
if self.page._chromium_options.is_auto_port and self.page._chromium_options.user_data_path: path = Path(self._chromium_options.user_data_path)
path = Path(self.page._chromium_options.user_data_path)
end_time = perf_counter() + 7 end_time = perf_counter() + 7
while perf_counter() < end_time: while perf_counter() < end_time:
if not path.exists(): if not path.exists():
@ -290,3 +512,59 @@ class Browser(object):
except (PermissionError, FileNotFoundError, OSError): except (PermissionError, FileNotFoundError, OSError):
pass pass
sleep(.03) sleep(.03)
def handle_options(addr_or_opts):
"""设置浏览器启动属性
:param addr_or_opts: 'ip:port'ChromiumOptionsDriver
: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

View File

@ -5,36 +5,80 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @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 .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.downloader import DownloadManager
from .._units.setter import BrowserSetter
from .._units.waiter import BrowserWaiter
class Browser(object): class Chromium(object):
BROWSERS: dict = ...
page: ChromiumPage = ...
_driver: BrowserDriver = ...
id: str = ... id: str = ...
address: 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 = ... _frames: dict = ...
_drivers: Dict[str, Driver] = ... _drivers: Dict[str, Driver] = ...
_all_drivers: Dict[str, Set[Driver]] = ... _all_drivers: Dict[str, Set[Driver]] = ...
_process_id: Optional[int] = ... _process_id: Optional[int] = ...
_dl_mgr: DownloadManager = ... _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 _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 @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 @property
def tabs_count(self) -> int: ... def tabs_count(self) -> int: ...
@ -43,25 +87,77 @@ class Browser(object):
def tab_ids(self) -> List[str]: ... def tab_ids(self) -> List[str]: ...
@property @property
def process_id(self) -> Optional[int]: ... def latest_tab(self) -> Union[ChromiumTab, str]: ...
def find_tabs(self, title: str = None, url: str = None, def cookies(self, all_info: bool = False) -> CookiesList: ...
tab_type: Union[str, list, tuple] = None) -> List[dict]: ...
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 reconnect(self) -> None: ...
def connect_to_page(self) -> None: ...
def _onTargetCreated(self, **kwargs) -> None: ... def _onTargetCreated(self, **kwargs) -> None: ...
def _onTargetDestroyed(self, **kwargs) -> None: ... def _onTargetDestroyed(self, **kwargs) -> None: ...

View File

@ -7,7 +7,7 @@
""" """
from json import dumps, loads, JSONDecodeError from json import dumps, loads, JSONDecodeError
from queue import Queue, Empty from queue import Queue, Empty
from threading import Thread, Event from threading import Thread
from time import perf_counter, sleep from time import perf_counter, sleep
from requests import Session from requests import Session
@ -15,7 +15,7 @@ from websocket import (WebSocketTimeoutException, WebSocketConnectionClosedExcep
WebSocketException, WebSocketBadStatusException) WebSocketException, WebSocketBadStatusException)
from .._functions.settings import Settings from .._functions.settings import Settings
from ..errors import PageDisconnectedError from ..errors import PageDisconnectedError, BrowserConnectError
class Driver(object): class Driver(object):
@ -30,6 +30,7 @@ class Driver(object):
self.address = address self.address = address
self.type = tab_type self.type = tab_type
self.owner = owner self.owner = owner
# self._debug = True
# self._debug = False # self._debug = False
self.alert_flag = False # 标记alert出现跳过一条请求后复原 self.alert_flag = False # 标记alert出现跳过一条请求后复原
@ -43,7 +44,7 @@ class Driver(object):
self._handle_event_th.daemon = True self._handle_event_th.daemon = True
self._handle_immediate_event_th = None self._handle_immediate_event_th = None
self._stopped = Event() self.is_running = False
self.event_handlers = {} self.event_handlers = {}
self.immediate_event_handlers = {} self.immediate_event_handlers = {}
@ -86,7 +87,7 @@ class Driver(object):
self.method_results.pop(ws_id, None) self.method_results.pop(ws_id, None)
return {'error': {'message': 'connection disconnected'}, 'type': 'connection_error'} return {'error': {'message': 'connection disconnected'}, 'type': 'connection_error'}
while not self._stopped.is_set(): while self.is_running:
try: try:
result = self.method_results[ws_id].get(timeout=.2) result = self.method_results[ws_id].get(timeout=.2)
self.method_results.pop(ws_id, None) self.method_results.pop(ws_id, None)
@ -107,7 +108,7 @@ class Driver(object):
def _recv_loop(self): def _recv_loop(self):
"""接收浏览器信息的守护线程方法""" """接收浏览器信息的守护线程方法"""
while not self._stopped.is_set(): while self.is_running:
try: try:
# self._ws.settimeout(1) # self._ws.settimeout(1)
msg_json = self._ws.recv() msg_json = self._ws.recv()
@ -145,7 +146,7 @@ class Driver(object):
def _handle_event_loop(self): def _handle_event_loop(self):
"""当接收到浏览器信息,执行已绑定的方法""" """当接收到浏览器信息,执行已绑定的方法"""
while not self._stopped.is_set(): while self.is_running:
try: try:
event = self.event_queue.get(timeout=1) event = self.event_queue.get(timeout=1)
except Empty: except Empty:
@ -158,7 +159,7 @@ class Driver(object):
self.event_queue.task_done() self.event_queue.task_done()
def _handle_immediate_event_loop(self): 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) function, kwargs = self.immediate_event_queue.get(timeout=1)
try: try:
function(**kwargs) function(**kwargs)
@ -183,7 +184,7 @@ class Driver(object):
:param kwargs: cdp参数 :param kwargs: cdp参数
:return: 执行结果 :return: 执行结果
""" """
if self._stopped.is_set(): if not self.is_running:
return {'error': 'connection disconnected', 'type': 'connection_error'} return {'error': 'connection disconnected', 'type': 'connection_error'}
timeout = kwargs.pop('_timeout', Settings.cdp_timeout) timeout = kwargs.pop('_timeout', Settings.cdp_timeout)
@ -191,13 +192,13 @@ class Driver(object):
if 'result' not in result and 'error' in result: if 'result' not in result and 'error' in result:
kwargs['_timeout'] = timeout kwargs['_timeout'] = timeout
return {'error': result['error']['message'], 'type': result.get('type', 'call_method_error'), 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: else:
return result['result'] return result['result']
def start(self): def start(self):
"""启动连接""" """启动连接"""
self._stopped.clear() self.is_running = True
try: try:
self._ws = create_connection(self._websocket_url, enable_multithread=True, suppress_origin=True) self._ws = create_connection(self._websocket_url, enable_multithread=True, suppress_origin=True)
except WebSocketBadStatusException as e: except WebSocketBadStatusException as e:
@ -205,6 +206,8 @@ class Driver(object):
raise RuntimeError('请升级websocket-client库。') raise RuntimeError('请升级websocket-client库。')
else: else:
return return
except ConnectionRefusedError:
raise BrowserConnectError('浏览器未开启或已关闭。')
self._recv_th.start() self._recv_th.start()
self._handle_event_th.start() self._handle_event_th.start()
return True return True
@ -218,15 +221,24 @@ class Driver(object):
def _stop(self): def _stop(self):
"""中断连接""" """中断连接"""
if self._stopped.is_set(): if not self.is_running:
return False return False
self._stopped.set() self.is_running = False
if self._ws: if self._ws:
self._ws.close() self._ws.close()
self._ws = None self._ws = None
# try: # 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(): # while not self.event_queue.empty():
# event = self.event_queue.get_nowait() # event = self.event_queue.get_nowait()
# function = self.event_handlers.get(event['method']) # function = self.event_handlers.get(event['method'])

View File

@ -6,13 +6,13 @@
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from queue import Queue from queue import Queue
from threading import Thread, Event from threading import Thread
from typing import Union, Callable, Dict, Optional from typing import Union, Callable, Dict, Optional
from requests import Response, Session from requests import Response, Session
from websocket import WebSocket from websocket import WebSocket
from .browser import Browser from .browser import Chromium
class GenericAttr(object): class GenericAttr(object):
@ -35,7 +35,8 @@ class Driver(object):
_recv_th: Thread _recv_th: Thread
_handle_event_th: Thread _handle_event_th: Thread
_handle_immediate_event_th: Optional[Thread] _handle_immediate_event_th: Optional[Thread]
_stopped: Event # _stopped: Event
is_running: bool
event_handlers: dict event_handlers: dict
immediate_event_handlers: dict immediate_event_handlers: dict
method_results: dict method_results: dict
@ -67,11 +68,11 @@ class Driver(object):
class BrowserDriver(Driver): class BrowserDriver(Driver):
BROWSERS: Dict[str, Driver] = ... BROWSERS: Dict[str, Driver] = ...
owner: Browser = ... owner: Chromium = ...
_control_session: Session = ... _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: ... def get(self, url) -> Response: ...

View File

@ -21,7 +21,7 @@ class ChromiumOptions(object):
self._user = 'Default' self._user = 'Default'
self._prefs_to_del = [] self._prefs_to_del = []
self.clear_file_flags = False self.clear_file_flags = False
self._headless = None self._is_headless = False
if read_file is False: if read_file is False:
ini_path = False ini_path = False
@ -33,10 +33,10 @@ class ChromiumOptions(object):
self.ini_path = str(ini_path) self.ini_path = str(ini_path)
else: else:
self.ini_path = str(Path(__file__).parent / 'configs.ini') self.ini_path = str(Path(__file__).parent / 'configs.ini')
om = OptionsManager(ini_path)
om = OptionsManager(ini_path)
options = om.chromium_options 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._tmp_path = om.paths.get('tmp_path', None) or None
self._arguments = options.get('arguments', []) self._arguments = options.get('arguments', [])
self._browser_path = options.get('browser_path', '') self._browser_path = options.get('browser_path', '')
@ -47,6 +47,11 @@ class ChromiumOptions(object):
self._load_mode = options.get('load_mode', 'normal') self._load_mode = options.get('load_mode', 'normal')
self._system_user_path = options.get('system_user_path', False) self._system_user_path = options.get('system_user_path', False)
self._existing_only = options.get('existing_only', 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) self._proxy = om.proxies.get('http', None) or om.proxies.get('https', None)
@ -164,6 +169,11 @@ class ChromiumOptions(object):
"""返回连接失败时的重试间隔(秒)""" """返回连接失败时的重试间隔(秒)"""
return self._retry_interval return self._retry_interval
@property
def is_headless(self):
"""返回是否无头模式"""
return self._is_headless
def set_retry(self, times=None, interval=None): def set_retry(self, times=None, interval=None):
"""设置连接失败时的重试操作 """设置连接失败时的重试操作
:param times: 重试次数 :param times: 重试次数
@ -184,11 +194,19 @@ class ChromiumOptions(object):
""" """
self.remove_argument(arg) self.remove_argument(arg)
if value is not False: if value is not False:
if arg == '--headless' and value is None: if arg == '--headless':
self._arguments.append('--headless=new') 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: else:
arg_str = arg if value is None else f'{arg}={value}' arg_str = arg if value is None else f'{arg}={value}'
self._arguments.append(arg_str) self._arguments.append(arg_str)
elif arg == '--headless':
self._is_headless = False
return self return self
def remove_argument(self, value): def remove_argument(self, value):
@ -196,14 +214,14 @@ class ChromiumOptions(object):
:param value: 设置项名有值的设置项传入设置名称即可 :param value: 设置项名有值的设置项传入设置名称即可
:return: 当前对象 :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 len(elements_to_delete) == 1:
if argument == value or argument.startswith(f'{value}='): self._arguments.remove(elements_to_delete[0])
del_list.append(argument) else:
self._arguments = [arg for arg in self._arguments if arg not in elements_to_delete]
for del_arg in del_list:
self._arguments.remove(del_arg)
return self return self
@ -282,14 +300,13 @@ class ChromiumOptions(object):
self._prefs = {} self._prefs = {}
return self 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 base: 默认超时时间
:param page_load: 页面加载超时时间 :param page_load: 页面加载超时时间
:param script: 脚本运行超时时间 :param script: 脚本运行超时时间
:return: 当前对象 :return: 当前对象
""" """
base = base if base is not None else implicit
if base is not None: if base is not None:
self._timeouts['base'] = base self._timeouts['base'] = base
if page_load is not None: if page_load is not None:
@ -313,7 +330,7 @@ class ChromiumOptions(object):
:param on_off: 开或关 :param on_off: 开或关
:return: 当前对象 :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) return self.set_argument('--headless', on_off)
def no_imgs(self, on_off=True): def no_imgs(self, on_off=True):
@ -348,6 +365,14 @@ class ChromiumOptions(object):
on_off = None if on_off else False on_off = None if on_off else False
return self.set_argument('--incognito', on_off) 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): def ignore_certificate_errors(self, on_off=True):
"""设置是否忽略证书错误 """设置是否忽略证书错误
:param on_off: 开或关 :param on_off: 开或关
@ -450,7 +475,7 @@ class ChromiumOptions(object):
:param path: 下载路径 :param path: 下载路径
:return: 当前对象 :return: 当前对象
""" """
self._download_path = str(path) self._download_path = '.' if path is None else str(path)
return self return self
def set_tmp_path(self, path): def set_tmp_path(self, path):
@ -488,17 +513,14 @@ class ChromiumOptions(object):
self._system_user_path = on_off self._system_user_path = on_off
return self 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 on_off: 是否开启自动获取端口号
:param tmp_path: 临时文件保存路径为None时保存到系统临时文件夹on_off为False时此参数无效 :param scope: 指定端口范围不含最后的数字为None则使用[9600-59600)
:param scope: 指定端口范围不含最后的数字为None则使用[9600-19600)
:return: 当前对象 :return: 当前对象
""" """
if on_off: if on_off:
self._auto_port = scope if scope else True self._auto_port = scope if scope else (9600, 59600)
if tmp_path:
self._tmp_path = str(tmp_path)
else: else:
self._auto_port = False self._auto_port = False
return self return self
@ -537,7 +559,7 @@ class ChromiumOptions(object):
# 设置chromium_options # 设置chromium_options
attrs = ('address', 'browser_path', 'arguments', 'extensions', 'user', 'load_mode', 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: for i in attrs:
om.set_item('chromium_options', i, self.__getattribute__(f'_{i}')) om.set_item('chromium_options', i, self.__getattribute__(f'_{i}'))
# 设置代理 # 设置代理

View File

@ -10,30 +10,32 @@ from typing import Union, Any, Literal, Optional, Tuple
class ChromiumOptions(object): class ChromiumOptions(object):
def __init__(self, read_file: [bool, None] = True, ini_path: Union[str, Path] = None): ini_path: Optional[str] = ...
self.ini_path: str = ... _driver_path: str = ...
self._driver_path: str = ... _user_data_path: Optional[str] = ...
self._user_data_path: str = ... _download_path: str = ...
self._download_path: str = ... _tmp_path: str = ...
self._tmp_path: str = ... _arguments: list = ...
self._arguments: list = ... _browser_path: str = ...
self._browser_path: str = ... _user: str = ...
self._user: str = ... _load_mode: str = ...
self._load_mode: str = ... _timeouts: dict = ...
self._timeouts: dict = ... _proxy: str = ...
self._proxy: str = ... _address: str = ...
self._address: str = ... _extensions: list = ...
self._extensions: list = ... _prefs: dict = ...
self._prefs: dict = ... _flags: dict = ...
self._flags: dict = ... _prefs_to_del: list = ...
self._prefs_to_del: list = ... _new_env: bool = ...
self.clear_file_flags: bool = ... clear_file_flags: bool = ...
self._auto_port: bool = ... _auto_port: Union[Tuple[int, int], False] = ...
self._system_user_path: bool = ... _system_user_path: bool = ...
self._existing_only: bool = ... _existing_only: bool = ...
self._headless: bool = ... _retry_times: int = ...
self._retry_times: int = ... _retry_interval: float = ...
self._retry_interval: float = ... _is_headless: bool = ...
def __init__(self, read_file: [bool, None] = True, ini_path: Union[str, Path] = None): ...
@property @property
def download_path(self) -> str: ... def download_path(self) -> str: ...
@ -89,6 +91,9 @@ class ChromiumOptions(object):
@property @property
def retry_interval(self) -> float: ... def retry_interval(self) -> float: ...
@property
def is_headless(self) -> bool: ...
def set_retry(self, times: int = None, interval: float = None) -> ChromiumOptions: ... def set_retry(self, times: int = None, interval: float = None) -> ChromiumOptions: ...
def set_argument(self, arg: str, value: Union[str, None, bool] = 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 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_user_agent(self, user_agent: str) -> ChromiumOptions: ...
def set_proxy(self, proxy: str) -> ChromiumOptions: ... def set_proxy(self, proxy: str) -> ChromiumOptions: ...
@ -162,7 +169,6 @@ class ChromiumOptions(object):
def auto_port(self, def auto_port(self,
on_off: bool = True, on_off: bool = True,
tmp_path: Union[str, Path] = None,
scope: Tuple[int, int] = None) -> ChromiumOptions: ... scope: Tuple[int, int] = None) -> ChromiumOptions: ...
def existing_only(self, on_off: bool = True) -> ChromiumOptions: ... def existing_only(self, on_off: bool = True) -> ChromiumOptions: ...

View File

@ -14,6 +14,7 @@ user = Default
auto_port = False auto_port = False
system_user_path = False system_user_path = False
existing_only = False existing_only = False
new_env = False
[session_options] [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'} 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'}

View File

@ -64,6 +64,7 @@ class OptionsManager(object):
self.set_item('chromium_options', 'auto_port', 'False') self.set_item('chromium_options', 'auto_port', 'False')
self.set_item('chromium_options', 'system_user_path', 'False') self.set_item('chromium_options', 'system_user_path', 'False')
self.set_item('chromium_options', 'existing_only', '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 " 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." "10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10."
"1.2 Safari/603.3.8', 'accept': 'text/html,application/xhtml" "1.2 Safari/603.3.8', 'accept': 'text/html,application/xhtml"

View File

@ -12,7 +12,8 @@ from requests import Session
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from .options_manage import OptionsManager 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): class SessionOptions(object):
@ -24,7 +25,7 @@ class SessionOptions(object):
:param ini_path: ini文件路径 :param ini_path: ini文件路径
""" """
self.ini_path = None self.ini_path = None
self._download_path = None self._download_path = '.'
self._timeout = 10 self._timeout = 10
self._del_set = set() # 记录要从ini文件删除的参数 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.set_proxies(om.proxies.get('http', None), om.proxies.get('https', None))
self._timeout = om.timeouts.get('base', 10) 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 others = om.others
self._retry_times = others.get('retry_times', 3) self._retry_times = others.get('retry_times', 3)
@ -100,7 +101,7 @@ class SessionOptions(object):
:param path: 下载路径 :param path: 下载路径
:return: 返回当前对象 :return: 返回当前对象
""" """
self._download_path = str(path) self._download_path = '.' if path is None else str(path)
return self return self
@property @property
@ -419,7 +420,7 @@ class SessionOptions(object):
return session_options_to_dict(self) return session_options_to_dict(self)
def make_session(self): def make_session(self):
"""根据内在的配置生成Session对象ua从对象中分离""" """根据内在的配置生成Session对象headers从对象中分离"""
s = Session() s = Session()
h = CaseInsensitiveDict(self.headers) if self.headers else CaseInsensitiveDict() h = CaseInsensitiveDict(self.headers) if self.headers else CaseInsensitiveDict()

View File

@ -16,10 +16,10 @@ from DataRecorder.tools import get_usable_path, make_valid_name
from .none_element import NoneElement from .none_element import NoneElement
from .session_element import make_session_ele from .session_element import make_session_ele
from .._base.base import DrissionElement, BaseElement from .._base.base import DrissionElement, BaseElement
from .._functions.elements import ChromiumElementsList, SessionElementsList
from .._functions.keys import input_text_or_keys from .._functions.keys import input_text_or_keys
from .._functions.locator import get_loc, locator_to_tuple 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, get_blob
from .._functions.web import make_absolute_link, get_ele_txt, format_html, is_js_func, offset_scroll, get_blob
from .._units.clicker import Clicker from .._units.clicker import Clicker
from .._units.rect import ElementRect from .._units.rect import ElementRect
from .._units.scroller import ElementScroller from .._units.scroller import ElementScroller
@ -44,7 +44,7 @@ class ChromiumElement(DrissionElement):
:param backend_id: backend id :param backend_id: backend id
""" """
super().__init__(owner) super().__init__(owner)
self.tab = self.owner.tab self.tab = self.owner._tab
self._select = None self._select = None
self._scroll = None self._scroll = None
self._rect = None self._rect = None
@ -95,29 +95,29 @@ class ChromiumElement(DrissionElement):
def tag(self): def tag(self):
"""返回元素tag""" """返回元素tag"""
if self._tag is None: if self._tag is None:
self._tag = self.owner.run_cdp('DOM.describeNode', self._tag = self.owner._run_cdp('DOM.describeNode',
backendNodeId=self._backend_id)['node']['localName'].lower() backendNodeId=self._backend_id)['node']['localName'].lower()
return self._tag return self._tag
@property @property
def html(self): def html(self):
"""返回元素outerHTML文本""" """返回元素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 @property
def inner_html(self): def inner_html(self):
"""返回元素innerHTML文本""" """返回元素innerHTML文本"""
return self.run_js('return this.innerHTML;') return self._run_js('return this.innerHTML;')
@property @property
def attrs(self): def attrs(self):
"""返回元素所有attribute属性""" """返回元素所有attribute属性"""
try: 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)} return {attrs[i]: attrs[i + 1] for i in range(0, len(attrs), 2)}
except ElementLostError: except ElementLostError:
self._refresh_id() 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)} return {attrs[i]: attrs[i + 1] for i in range(0, len(attrs), 2)}
except CDPError: # 文档根元素不能调用此方法 except CDPError: # 文档根元素不能调用此方法
return {} return {}
@ -162,18 +162,18 @@ class ChromiumElement(DrissionElement):
return self._rect return self._rect
@property @property
def shadow_root(self): def sr(self):
"""返回当前元素的shadow_root元素对象""" """返回当前元素的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): if not info.get('shadowRoots', None):
return None return None
return ShadowRoot(self, backend_id=info['shadowRoots'][0]['backendNodeId']) return ShadowRoot(self, backend_id=info['shadowRoots'][0]['backendNodeId'])
@property @property
def sr(self): def shadow_root(self):
"""返回当前元素的shadow_root元素对象""" """返回当前元素的shadow_root元素对象"""
return self.shadow_root return self.sr
@property @property
def scroll(self): def scroll(self):
@ -193,7 +193,7 @@ class ChromiumElement(DrissionElement):
def wait(self): def wait(self):
"""返回用于等待的对象""" """返回用于等待的对象"""
if self._wait is None: if self._wait is None:
self._wait = ElementWaiter(self.owner, self) self._wait = ElementWaiter(self)
return self._wait return self._wait
@property @property
@ -225,8 +225,8 @@ class ChromiumElement(DrissionElement):
elif not is_checked and not uncheck: elif not is_checked and not uncheck:
js = 'this.checked=true' js = 'this.checked=true'
if js: if js:
self.run_js(js) self._run_js(js)
self.run_js('this.dispatchEvent(new Event("change", {bubbles: true}));') self._run_js('this.dispatchEvent(new Event("change", {bubbles: true}));')
else: else:
if (is_checked and uncheck) or (not is_checked and not uncheck): if (is_checked and uncheck) or (not is_checked and not uncheck):
@ -351,20 +351,56 @@ class ChromiumElement(DrissionElement):
else: else:
return NoneElement(page=self.owner, method='on()', args={'timeout': timeout}) return NoneElement(page=self.owner, method='on()', args={'timeout': timeout})
def offset(self, offset_x, offset_y): def offset(self, locator=None, x=None, y=None, timeout=None):
"""获取相对本元素左上角左边指定偏移量位置的元素 """获取相对本元素左上角左边指定偏移量位置的元素如果offset_x和offset_y都是None定位到元素中间点
:param offset_x: 横坐标偏移量向右为正 :param locator: 定位符只支持str且不支持xpath和css方式
:param offset_y: 纵坐标偏移量向下为正 :param x: 横坐标偏移量向右为正
:param y: 纵坐标偏移量向下为正
:param timeout: 超时时间为None使用所在页面设置
:return: 元素对象 :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: try:
return ChromiumElement(owner=self.owner, ele = ChromiumElement(owner=self.owner,
backend_id=self.owner.run_cdp('DOM.getNodeForLocation', x=x + offset_x, backend_id=self.owner._run_cdp('DOM.getNodeForLocation', x=x, y=y,
y=y + offset_y, includeUserAgentShadowDOM=True, includeUserAgentShadowDOM=True,
ignorePointerEventsNone=False)['backendNodeId']) ignorePointerEventsNone=False)['backendNodeId'])
except CDPError: 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): def east(self, loc_or_pixel=None, index=1):
"""获取元素右边某个指定元素 """获取元素右边某个指定元素
@ -439,8 +475,8 @@ class ChromiumElement(DrissionElement):
cdp_data[variable] += locator cdp_data[variable] += locator
try: try:
return ChromiumElement(owner=self.owner, return ChromiumElement(owner=self.owner,
backend_id=self.owner.run_cdp('DOM.getNodeForLocation', backend_id=self.owner._run_cdp('DOM.getNodeForLocation',
**cdp_data)['backendNodeId']) **cdp_data)['backendNodeId'])
except CDPError: except CDPError:
return NoneElement(page=self.owner, method=f'{mode}()', args={'locator': locator}) 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: while 0 < cdp_data[variable] < max_len:
cdp_data[variable] += value cdp_data[variable] += value
try: 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: if bid == curr_ele:
continue continue
else: else:
@ -505,7 +541,7 @@ class ChromiumElement(DrissionElement):
:param name: 属性名 :param name: 属性名
:return: None :return: None
""" """
self.run_js(f'this.removeAttribute("{name}");') self._run_js(f'this.removeAttribute("{name}");')
def property(self, name): def property(self, name):
"""获取一个property属性值 """获取一个property属性值
@ -513,12 +549,22 @@ class ChromiumElement(DrissionElement):
:return: 属性值文本 :return: 属性值文本
""" """
try: 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 return format_html(value) if isinstance(value, str) else value
except: except:
return None return None
def run_js(self, script, *args, as_expr=False, timeout=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代码 """对本元素执行javascript代码
:param script: js文本文本中用this表示本元素 :param script: js文本文本中用this表示本元素
:param args: 参数按顺序在js文本中对应arguments[0]arguments[1]... :param args: 参数按顺序在js文本中对应arguments[0]arguments[1]...
@ -554,27 +600,32 @@ class ChromiumElement(DrissionElement):
""" """
return self._ele(locator, timeout=timeout, index=None) 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形式返回 """查找一个符合条件的元素以SessionElement形式返回
:param locator: 元素的定位信息可以是loc元组或查询字符串 :param locator: 元素的定位信息可以是loc元组或查询字符串
:param index: 获取第几个从1开始可传入负数获取倒数第几个 :param index: 获取第几个从1开始可传入负数获取倒数第几个
:param timeout: 查找元素超时时间默认与元素所在页面等待时间一致
:return: SessionElement对象或属性文本 :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列表形式返回 """查找所有符合条件的元素以SessionElement列表形式返回
:param locator: 定位符 :param locator: 定位符
:param timeout: 查找元素超时时间默认与元素所在页面等待时间一致
:return: SessionElement或属性文本组成的列表 :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): def _find_elements(self, locator, timeout=None, index=1, relative=False, raise_err=None):
"""返回当前元素下级符合条件的子元素、属性或节点文本,默认返回第一个 """返回当前元素下级符合条件的子元素、属性或节点文本,默认返回第一个
:param locator: 元素的定位信息可以是loc元组或查询字符串 :param locator: 元素的定位信息可以是loc元组或查询字符串
:param timeout: 查找元素超时时间 :param timeout: 查找元素超时时间
:param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有 :param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有
:param relative: WebPage用的表示是否相对定位的参数 :param relative: MixTab用的表示是否相对定位的参数
:param raise_err: 找不到元素是是否抛出异常为None时根据全局设置 :param raise_err: 找不到元素是是否抛出异常为None时根据全局设置
:return: ChromiumElement对象或文本属性或其组成的列表 :return: ChromiumElement对象或文本属性或其组成的列表
""" """
@ -588,7 +639,7 @@ class ChromiumElement(DrissionElement):
""" """
if pseudo_ele: if pseudo_ele:
pseudo_ele = f', "{pseudo_ele}"' if pseudo_ele.startswith(':') else f', "::{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): def src(self, timeout=None, base64_to_bytes=True):
"""返回元素src资源base64的可转为bytes返回其它返回str """返回元素src资源base64的可转为bytes返回其它返回str
@ -602,7 +653,7 @@ class ChromiumElement(DrissionElement):
'&& this.naturalWidth > 0 && typeof this.naturalHeight != "undefined" ' '&& this.naturalWidth > 0 && typeof this.naturalHeight != "undefined" '
'&& this.naturalHeight > 0') '&& this.naturalHeight > 0')
end_time = perf_counter() + timeout 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) sleep(.1)
src = self.attr('src') src = self.attr('src')
@ -631,11 +682,11 @@ class ChromiumElement(DrissionElement):
if not src: if not src:
continue 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 frame = node.get('frameId', None) or self.owner._frame_id
try: try:
result = self.owner.run_cdp('Page.getResourceContent', frameId=frame, url=src) result = self.owner._run_cdp('Page.getResourceContent', frameId=frame, url=src)
break break
except CDPError: except CDPError:
pass pass
@ -698,7 +749,7 @@ class ChromiumElement(DrissionElement):
js = ('return this.complete && typeof this.naturalWidth != "undefined" && this.naturalWidth > 0 ' js = ('return this.complete && typeof this.naturalWidth != "undefined" && this.naturalWidth > 0 '
'&& typeof this.naturalHeight != "undefined" && this.naturalHeight > 0') '&& typeof this.naturalHeight != "undefined" && this.naturalHeight > 0')
end_time = perf_counter() + self.owner.timeout 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) sleep(.1)
if scroll_to_center: if scroll_to_center:
self.scroll.to_see(center=True) self.scroll.to_see(center=True)
@ -729,7 +780,7 @@ class ChromiumElement(DrissionElement):
if isinstance(vals, (list, tuple)): if isinstance(vals, (list, tuple)):
vals = ''.join([str(i) for i in vals]) vals = ''.join([str(i) for i in vals])
self.set.property('value', str(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 return
self.wait.clickable(wait_moved=False, timeout=.5) self.wait.clickable(wait_moved=False, timeout=.5)
@ -738,7 +789,11 @@ class ChromiumElement(DrissionElement):
else: else:
self._input_focus() 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): def clear(self, by_js=False):
"""清空元素文本 """清空元素文本
@ -746,8 +801,8 @@ class ChromiumElement(DrissionElement):
:return: None :return: None
""" """
if by_js: if by_js:
self.run_js("this.value='';") self._run_js("this.value='';")
self.run_js('this.dispatchEvent(new Event("change", {bubbles: true}));') self._run_js('this.dispatchEvent(new Event("change", {bubbles: true}));')
return return
self._input_focus() self._input_focus()
@ -756,26 +811,24 @@ class ChromiumElement(DrissionElement):
def _input_focus(self): def _input_focus(self):
"""输入前使元素获取焦点""" """输入前使元素获取焦点"""
try: try:
self.owner.run_cdp('DOM.focus', backendNodeId=self._backend_id) self.owner._run_cdp('DOM.focus', backendNodeId=self._backend_id)
except Exception: except Exception:
self.click(by_js=None) self.click(by_js=None)
def focus(self): def focus(self):
"""使元素获取焦点""" """使元素获取焦点"""
try: try:
self.owner.run_cdp('DOM.focus', backendNodeId=self._backend_id) self.owner._run_cdp('DOM.focus', backendNodeId=self._backend_id)
except Exception: except Exception:
self.run_js('this.focus();') self._run_js('this.focus();')
def hover(self, offset_x=None, offset_y=None): def hover(self, offset_x=None, offset_y=None):
"""鼠标悬停,可接受偏移量,偏移量相对于元素左上角坐标。不传入x或y值时悬停在元素中点 """鼠标悬停,可接受偏移量,偏移量相对于元素左上角坐标。不传入offset_x和offset_y值时悬停在元素中点
:param offset_x: 相对元素左上角坐标的x轴偏移量 :param offset_x: 相对元素左上角坐标的x轴偏移量
:param offset_y: 相对元素左上角坐标的y轴偏移量 :param offset_y: 相对元素左上角坐标的y轴偏移量
:return: None :return: None
""" """
self.owner.scroll.to_see(self) self.owner.actions.move_to(self, offset_x=offset_x, offset_y=offset_y, duration=.1)
x, y = offset_scroll(self, offset_x, offset_y)
self.owner.run_cdp('Input.dispatchMouseEvent', type='mouseMoved', x=x, y=y, _ignore=AlertExistsError)
def drag(self, offset_x=0, offset_y=0, duration=.5): def drag(self, offset_x=0, offset_y=0, duration=.5):
"""拖拽当前元素到相对位置 """拖拽当前元素到相对位置
@ -808,9 +861,9 @@ class ChromiumElement(DrissionElement):
:return: js中的object id :return: js中的object id
""" """
if node_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: 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): def _get_node_id(self, obj_id=None, backend_id=None):
"""根据传入object id或backend id获取cdp中的node id """根据传入object id或backend id获取cdp中的node id
@ -819,9 +872,9 @@ class ChromiumElement(DrissionElement):
:return: cdp中的node id :return: cdp中的node id
""" """
if obj_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: 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'] self._tag = n['localName']
return n['nodeId'] return n['nodeId']
@ -830,7 +883,7 @@ class ChromiumElement(DrissionElement):
:param node_id: :param node_id:
:return: backend 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'] self._tag = n['localName']
return n['backendNodeId'] return n['backendNodeId']
@ -850,7 +903,11 @@ class ChromiumElement(DrissionElement):
txt5 = '''return path;''' txt5 = '''return path;'''
elif mode == 'css': elif mode == 'css':
txt1 = '' txt1 = '''
let i = el.getAttribute("id");
if (i){path = '>' + el.tagName.toLowerCase() + "#" + i + path;
break;}
'''
txt3 = '' txt3 = ''
txt4 = '''path = '>' + el.tagName.toLowerCase() + ":nth-child(" + nth + ")" + path;''' txt4 = '''path = '>' + el.tagName.toLowerCase() + ":nth-child(" + nth + ")" + path;'''
txt5 = '''return path.substr(1);''' txt5 = '''return path.substr(1);'''
@ -860,6 +917,7 @@ class ChromiumElement(DrissionElement):
js = '''function(){ js = '''function(){
function e(el) { function e(el) {
//return el;
if (!(el instanceof Element)) return; if (!(el instanceof Element)) return;
let path = ''; let path = '';
while (el.nodeType === Node.ELEMENT_NODE) { while (el.nodeType === Node.ELEMENT_NODE) {
@ -876,7 +934,7 @@ class ChromiumElement(DrissionElement):
} }
return e(this);} return e(this);}
''' '''
t = self.run_js(js) t = self._run_js(js)
return f'{t}' if mode == 'css' else t return f'{t}' if mode == 'css' else t
def _set_file_input(self, files): def _set_file_input(self, files):
@ -887,7 +945,7 @@ class ChromiumElement(DrissionElement):
if isinstance(files, str): if isinstance(files, str):
files = files.split('\n') files = files.split('\n')
files = [str(Path(i).absolute()) for i in files] 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): class ShadowRoot(BaseElement):
@ -900,7 +958,7 @@ class ShadowRoot(BaseElement):
:param backend_id: cdp中的backend id :param backend_id: cdp中的backend id
""" """
super().__init__(parent_ele.owner) super().__init__(parent_ele.owner)
self.tab = self.owner.tab self.tab = self.owner._tab
self.parent_ele = parent_ele self.parent_ele = parent_ele
if backend_id: if backend_id:
self._backend_id = backend_id self._backend_id = backend_id
@ -942,7 +1000,7 @@ class ShadowRoot(BaseElement):
@property @property
def inner_html(self): def inner_html(self):
"""返回内部的html文本""" """返回内部的html文本"""
return self.run_js('return this.innerHTML;') return self._run_js('return this.innerHTML;')
@property @property
def states(self): def states(self):
@ -952,6 +1010,16 @@ class ShadowRoot(BaseElement):
return self._states return self._states
def run_js(self, script, *args, as_expr=False, timeout=None): 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代码 """运行javascript代码
:param script: js文本 :param script: js文本
:param args: 参数按顺序在js文本中对应arguments[0]arguments[1]... :param args: 参数按顺序在js文本中对应arguments[0]arguments[1]...
@ -1128,31 +1196,32 @@ class ShadowRoot(BaseElement):
""" """
return self._ele(locator, timeout=timeout, index=None) 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形式返回处理复杂页面时效率很高 """查找一个符合条件的元素以SessionElement形式返回处理复杂页面时效率很高
:param locator: 元素的定位信息可以是loc元组或查询字符串 :param locator: 元素的定位信息可以是loc元组或查询字符串
:param index: 获取第几个从1开始可传入负数获取倒数第几个 :param index: 获取第几个从1开始可传入负数获取倒数第几个
:param timeout: 查找元素超时时间默认与元素所在页面等待时间一致
:return: SessionElement对象或属性文本 :return: SessionElement对象或属性文本
""" """
r = make_session_ele(self, locator, index=index) return (make_session_ele(self, locator, index=index, method='s_ele()')
if isinstance(r, NoneElement): if self.ele(locator, index=index, timeout=timeout)
r.method = 's_ele()' else NoneElement(self, method='s_ele()', args={'locator': locator, 'index': index}))
r.args = {'locator': locator}
return r
def s_eles(self, locator): def s_eles(self, locator, timeout=None):
"""查找所有符合条件的元素以SessionElement列表形式返回处理复杂页面时效率很高 """查找所有符合条件的元素以SessionElement列表形式返回处理复杂页面时效率很高
:param locator: 元素的定位信息可以是loc元组或查询字符串 :param locator: 元素的定位信息可以是loc元组或查询字符串
:param timeout: 查找元素超时时间默认与元素所在页面等待时间一致
:return: SessionElement对象 :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): def _find_elements(self, locator, timeout=None, index=1, relative=False, raise_err=None):
"""返回当前元素下级符合条件的子元素、属性或节点文本,默认返回第一个 """返回当前元素下级符合条件的子元素、属性或节点文本,默认返回第一个
:param locator: 元素的定位信息可以是loc元组或查询字符串 :param locator: 元素的定位信息可以是loc元组或查询字符串
:param timeout: 查找元素超时时间 :param timeout: 查找元素超时时间
:param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有 :param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有
:param relative: WebPage用的表示是否相对定位的参数 :param relative: MixTab用的表示是否相对定位的参数
:param raise_err: 找不到元素是是否抛出异常为None时根据全局设置 :param raise_err: 找不到元素是是否抛出异常为None时根据全局设置
:return: ChromiumElement对象或其组成的列表 :return: ChromiumElement对象或其组成的列表
""" """
@ -1163,14 +1232,14 @@ class ShadowRoot(BaseElement):
def do_find(): def do_find():
if loc[0] == 'css selector': if loc[0] == 'css selector':
if index == 1: 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: if nod_id:
r = make_chromium_eles(self.owner, _ids=nod_id, is_obj_id=False) r = make_chromium_eles(self.owner, _ids=nod_id, is_obj_id=False)
return None if r is False else r return None if r is False else r
else: else:
nod_ids = self.owner.run_cdp('DOM.querySelectorAll', nod_ids = self.owner._run_cdp('DOM.querySelectorAll',
nodeId=self._node_id, selector=loc[1])['nodeId'] nodeId=self._node_id, selector=loc[1])['nodeId']
r = make_chromium_eles(self.owner, _ids=nod_ids, index=index, is_obj_id=False) r = make_chromium_eles(self.owner, _ids=nod_ids, index=index, is_obj_id=False)
return None if r is False else r return None if r is False else r
@ -1179,17 +1248,22 @@ class ShadowRoot(BaseElement):
if not eles: if not eles:
return None 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: if index is not None:
try: try:
node_id = self.owner.run_cdp('DOM.querySelector', nodeId=self._node_id, node_id = self.owner._run_cdp('DOM.querySelector', nodeId=self._node_id,
selector=css[index - 1])['nodeId'] selector=css[index - 1])['nodeId']
except IndexError: except IndexError:
return None return None
r = make_chromium_eles(self.owner, _ids=node_id, is_obj_id=False) r = make_chromium_eles(self.owner, _ids=node_id, is_obj_id=False)
return None if r is False else r return None if r is False else r
else: 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] for i in css]
if 0 in node_ids: if 0 in node_ids:
return None return None
@ -1209,15 +1283,15 @@ class ShadowRoot(BaseElement):
def _get_node_id(self, obj_id): def _get_node_id(self, obj_id):
"""返回元素node 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): def _get_obj_id(self, back_id):
"""返回元素object 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): def _get_backend_id(self, node_id):
"""返回元素object 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() self._tag = r['localName'].lower()
return r['backendNodeId'] return r['backendNodeId']
@ -1228,7 +1302,7 @@ def find_in_chromium_ele(ele, locator, index=1, timeout=None, relative=True):
:param locator: 元素定位元组 :param locator: 元素定位元组
:param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有 :param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有
:param timeout: 查找元素超时时间 :param timeout: 查找元素超时时间
:param relative: WebPage用于标记是否相对定位使用 :param relative: MixTab用于标记是否相对定位使用
:return: 返回ChromiumElement元素或它们组成的列表 :return: 返回ChromiumElement元素或它们组成的列表
""" """
# ---------------处理定位符--------------- # ---------------处理定位符---------------
@ -1269,15 +1343,15 @@ def find_by_xpath(ele, xpath, index, timeout, relative=True):
ele.owner.wait.doc_loaded() ele.owner.wait.doc_loaded()
def do_find(): def do_find():
res = ele.owner.run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=ele._obj_id, res = ele.owner._run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=ele._obj_id,
returnByValue=False, awaitPromise=True, userGesture=True) returnByValue=False, awaitPromise=True, userGesture=True)
if res['result']['type'] == 'string': if res['result']['type'] == 'string':
return res['result']['value'] return res['result']['value']
if 'exceptionDetails' in res: if 'exceptionDetails' in res:
if 'The result is not a node set' in res['result']['description']: if 'The result is not a node set' in res['result']['description']:
js1 = make_js_for_find_ele_by_xpath(xpath, '1', node_txt) 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, res = ele.owner._run_cdp('Runtime.callFunctionOn', functionDeclaration=js1, objectId=ele._obj_id,
returnByValue=False, awaitPromise=True, userGesture=True) returnByValue=False, awaitPromise=True, userGesture=True)
return res['result']['value'] return res['result']['value']
else: else:
raise SyntaxError(f'查询语句错误:\n{res}') 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 return None if r is False else r
else: else:
res = ele.owner.run_cdp('Runtime.getProperties', objectId=res['result']['objectId'], res = ele.owner._run_cdp('Runtime.getProperties', objectId=res['result']['objectId'],
ownProperties=True)['result'][:-1] ownProperties=True)['result'][:-1]
if index is None: if index is None:
r = ChromiumElementsList(page=ele.owner) r = ChromiumElementsList(page=ele.owner)
for i in res: for i in res:
@ -1341,8 +1415,8 @@ def find_by_css(ele, selector, index, timeout):
ele.owner.wait.doc_loaded() ele.owner.wait.doc_loaded()
def do_find(): def do_find():
res = ele.owner.run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=ele._obj_id, res = ele.owner._run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=ele._obj_id,
returnByValue=False, awaitPromise=True, userGesture=True) returnByValue=False, awaitPromise=True, userGesture=True)
if 'exceptionDetails' in res: if 'exceptionDetails' in res:
raise SyntaxError(f'查询语句错误:\n{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 return None if r is False else r
else: else:
obj_ids = [i['value']['objectId'] for i in ele.owner.run_cdp('Runtime.getProperties', obj_ids = [i['value']['objectId'] for i in ele.owner._run_cdp('Runtime.getProperties',
objectId=res['result']['objectId'], objectId=res['result']['objectId'],
ownProperties=True)['result']] ownProperties=True)['result']]
r = make_chromium_eles(ele.owner, _ids=obj_ids, index=index, is_obj_id=True) r = make_chromium_eles(ele.owner, _ids=obj_ids, index=index, is_obj_id=True)
return None if r is False else r 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 end_time = perf_counter() + timeout
try: try:
if as_expr: if as_expr:
res = page.run_cdp('Runtime.evaluate', expression=script, returnByValue=False, res = page._run_cdp('Runtime.evaluate', expression=script, returnByValue=False,
awaitPromise=True, userGesture=True, _timeout=timeout, _ignore=AlertExistsError) awaitPromise=True, userGesture=True, _timeout=timeout, _ignore=AlertExistsError)
else: else:
args = args or () args = args or ()
if not is_js_func(script): if not is_js_func(script):
script = f'function(){{{script}}}' script = f'function(){{{script}}}'
res = page.run_cdp('Runtime.callFunctionOn', functionDeclaration=script, objectId=obj_id, res = page._run_cdp('Runtime.callFunctionOn', functionDeclaration=script, objectId=obj_id,
arguments=[convert_argument(arg) for arg in args], returnByValue=False, arguments=[convert_argument(arg) for arg in args], returnByValue=False,
awaitPromise=True, userGesture=True, _timeout=timeout, _ignore=AlertExistsError) awaitPromise=True, userGesture=True, _timeout=timeout, _ignore=AlertExistsError)
except TimeoutError: except TimeoutError:
raise TimeoutError(f'执行js超时等待{timeout}秒)。') raise TimeoutError(f'执行js超时等待{timeout}秒)。')
except ContextLostError: except ContextLostError:
@ -1591,7 +1665,7 @@ def parse_js_result(page, ele, result, end_time):
return r return r
elif sub_type == 'array': 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()] 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: elif 'objectId' in result:
@ -1599,9 +1673,9 @@ def parse_js_result(page, ele, result, end_time):
if timeout < 0: if timeout < 0:
return return
js = 'function(){return JSON.stringify(this);}' js = 'function(){return JSON.stringify(this);}'
r = page.run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=result['objectId'], r = page._run_cdp('Runtime.callFunctionOn', functionDeclaration=js, objectId=result['objectId'],
returnByValue=False, awaitPromise=True, userGesture=True, _ignore=AlertExistsError, returnByValue=False, awaitPromise=True, userGesture=True, _ignore=AlertExistsError,
_timeout=timeout) _timeout=timeout)
return loads(parse_js_result(page, ele, r['result'], end_time)) return loads(parse_js_result(page, ele, r['result'], end_time))
else: else:
@ -1610,6 +1684,9 @@ def parse_js_result(page, ele, result, end_time):
elif the_type == 'undefined': elif the_type == 'undefined':
return None return None
elif the_type == 'function':
return result['description']
else: else:
return result['value'] return result['value']

View File

@ -14,8 +14,8 @@ from .._functions.elements import SessionElementsList, ChromiumElementsList
from .._pages.chromium_base import ChromiumBase from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage from .._pages.chromium_page import ChromiumPage
from .._pages.chromium_tab import ChromiumTab from .._pages.tabs import ChromiumTab
from .._pages.web_page import WebPage from .._pages.mix_page import MixPage
from .._units.clicker import Clicker from .._units.clicker import Clicker
from .._units.rect import ElementRect from .._units.rect import ElementRect
from .._units.scroller import ElementScroller 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): def __init__(self, owner: ChromiumBase, node_id: int = None, obj_id: str = None, backend_id: int = None):
self._tag: str = ... self._tag: str = ...
# self.page: Union[ChromiumPage, WebPage] = ...
self.owner: ChromiumBase = ... self.owner: ChromiumBase = ...
self.page: Union[ChromiumPage, WebPage] = ... self.page: Union[ChromiumPage, MixPage] = ...
self.tab: Union[ChromiumPage, ChromiumTab] = ... self.tab: Union[ChromiumPage, ChromiumTab] = ...
self._node_id: int = ... self._node_id: int = ...
self._obj_id: str = ... 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 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, def _get_relative_eles(self,
mode: str = 'north', mode: str = 'north',
@ -183,7 +186,7 @@ class ChromiumElement(DrissionElement):
def select(self) -> SelectElement: ... def select(self) -> SelectElement: ...
@property @property
def value(self) -> None: ... def value(self) -> str: ...
def check(self, uncheck: bool = False, by_js: bool = False) -> None: ... 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_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 run_async_js(self, script: str, *args, as_expr: bool = False) -> None: ...
def ele(self, def ele(self,
@ -208,9 +213,12 @@ class ChromiumElement(DrissionElement):
def s_ele(self, def s_ele(self,
locator: Union[Tuple[str, str], str] = None, 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, def _find_elements(self,
locator: Union[Tuple[str, str], str], locator: Union[Tuple[str, str], str],
@ -266,7 +274,6 @@ class ChromiumElement(DrissionElement):
class ShadowRoot(BaseElement): class ShadowRoot(BaseElement):
def __init__(self, parent_ele: ChromiumElement, obj_id: str = None, backend_id: int = None): def __init__(self, parent_ele: ChromiumElement, obj_id: str = None, backend_id: int = None):
# self.page: Union[ChromiumPage, WebPage] = ...
self.owner: ChromiumBase = ... self.owner: ChromiumBase = ...
self.tab: Union[ChromiumPage, ChromiumTab] = ... self.tab: Union[ChromiumPage, ChromiumTab] = ...
self._obj_id: str = ... 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_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 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: ... 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, def s_ele(self,
locator: Union[Tuple[str, str], str] = None, 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, def _find_elements(self,
locator: Union[Tuple[str, str], str], locator: Union[Tuple[str, str], str],
@ -375,7 +385,7 @@ def find_by_css(ele: ChromiumElement,
timeout: float) -> Union[ChromiumElement, List[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], _ids: Union[tuple, list, str, int],
index: Optional[int] = 1, index: Optional[int] = 1,
is_obj_id: bool = True, is_obj_id: bool = True,

View File

@ -260,7 +260,7 @@ class SessionElement(DrissionElement):
:param locator: 元素的定位信息可以是loc元组或查询字符串 :param locator: 元素的定位信息可以是loc元组或查询字符串
:param timeout: 不起实际作用用于和父类对应 :param timeout: 不起实际作用用于和父类对应
:param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有 :param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有
:param relative: WebPage用的表示是否相对定位的参数 :param relative: MixTab用的表示是否相对定位的参数
:param raise_err: 找不到元素是是否抛出异常为None时根据全局设置 :param raise_err: 找不到元素是是否抛出异常为None时根据全局设置
:return: SessionElement对象 :return: SessionElement对象
""" """
@ -276,6 +276,10 @@ class SessionElement(DrissionElement):
while ele: while ele:
if mode == 'css': 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::*')) brothers = len(ele.eles(f'xpath:./preceding-sibling::*'))
path_str = f'>{ele.tag}:nth-child({brothers + 1}){path_str}' path_str = f'>{ele.tag}:nth-child({brothers + 1}){path_str}'
else: else:
@ -349,11 +353,11 @@ def make_session_ele(html_or_ele, loc=None, index=1, method=None):
xpath = html_or_ele.xpath xpath = html_or_ele.xpath
# ChromiumElement兼容传入的元素在iframe内的情况 # ChromiumElement兼容传入的元素在iframe内的情况
if html_or_ele._doc_id is None: 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 html_or_ele._doc_id = doc['objectId'] if doc else False
if html_or_ele._doc_id: 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: else:
html = html_or_ele.owner.html html = html_or_ele.owner.html
html_or_ele = fromstring(html) html_or_ele = fromstring(html)

View File

@ -8,6 +8,7 @@
from json import load, dump, JSONDecodeError from json import load, dump, JSONDecodeError
from os import environ from os import environ
from pathlib import Path from pathlib import Path
from shutil import rmtree
from subprocess import Popen, DEVNULL from subprocess import Popen, DEVNULL
from tempfile import gettempdir from tempfile import gettempdir
from time import perf_counter, sleep from time import perf_counter, sleep
@ -28,17 +29,22 @@ def connect_browser(option):
browser_path = option.browser_path browser_path = option.browser_path
ip, port = address.split(':') ip, port = address.split(':')
if ip != '127.0.0.1' or port_is_using(ip, port) or option.is_existing_only: using = port_is_using(ip, port)
test_connect(ip, port) if ip != '127.0.0.1' or using or option.is_existing_only:
option._headless = False if test_connect(ip, port):
for i in option.arguments: return True
if i.startswith('--headless') and not i.endswith('=false'): elif ip != '127.0.0.1':
option._headless = True raise BrowserConnectError(f'\n{address}浏览器连接失败。')
break elif using:
return True 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_prefs(option)
set_flags(option) set_flags(option)
try: try:
@ -47,13 +53,16 @@ def connect_browser(option):
# 传入的路径找不到主动在ini文件、注册表、系统变量中找 # 传入的路径找不到主动在ini文件、注册表、系统变量中找
except FileNotFoundError: except FileNotFoundError:
browser_path = get_chrome_path(option.ini_path) browser_path = get_chrome_path(option.ini_path)
if not browser_path: if not browser_path:
raise FileNotFoundError('无法找到浏览器可执行文件路径,请手动配置。') raise FileNotFoundError('无法找到浏览器可执行文件路径,请手动配置。')
_run_browser(port, browser_path, args) _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 return False
@ -64,44 +73,26 @@ def get_launch_args(opt):
""" """
# ----------处理arguments----------- # ----------处理arguments-----------
result = set() result = set()
has_user_path = False user_path = False
headless = None
for i in opt.arguments: for i in opt.arguments:
if i.startswith(('--load-extension=', '--remote-debugging-port=')): if i.startswith(('--load-extension=', '--remote-debugging-port=')):
continue continue
elif i.startswith('--user-data-dir') and not opt.system_user_path: elif i.startswith('--user-data-dir') and not opt.system_user_path:
result.add(f'--user-data-dir={Path(i[16:]).absolute()}') user_path = f'--user-data-dir={Path(i[16:]).absolute()}'
has_user_path = True result.add(user_path)
continue 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) 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' port = opt.address.split(':')[-1] if opt.address else '0'
p = Path(opt.tmp_path) if opt.tmp_path else Path(gettempdir()) / 'DrissionPage' 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) path.mkdir(parents=True, exist_ok=True)
opt.set_user_data_path(path) user_path = path.absolute()
result.add(f'--user-data-dir={path}') opt.set_user_data_path(user_path)
result.add(f'--user-data-dir={user_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')
result = list(result) result = list(result)
opt._headless = headless
# ----------处理插件extensions------------- # ----------处理插件extensions-------------
ext = [str(Path(e).absolute()) for e in opt.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}' ext = f'--load-extension={ext}'
result.append(ext) result.append(ext)
return result return result, user_path
def set_prefs(opt): def set_prefs(opt):
@ -208,18 +199,13 @@ def test_connect(ip, port, timeout=30):
if tab['type'] in ('page', 'webview'): if tab['type'] in ('page', 'webview'):
r.close() r.close()
s.close() s.close()
return return True
r.close() r.close()
except Exception: except Exception:
sleep(.2) sleep(.2)
s.close() s.close()
raise BrowserConnectError(f'\n{ip}:{port}浏览器无法链接。\n请确认:\n1、该端口为浏览器\n' return False
f'2、已添加\'--remote-debugging-port={port}\'启动项\n'
f'3、用户文件夹没有和已打开的浏览器冲突\n'
f'4、如为无界面系统请添加\'--headless=new\'参数\n'
f'5、如果是Linux系统可能还要添加\'--no-sandbox\'启动参数\n'
f'可使用ChromiumOptions设置端口和用户文件夹路径。')
def _run_browser(port, path: str, args) -> Popen: def _run_browser(port, path: str, args) -> Popen:

View File

@ -22,7 +22,7 @@ def set_prefs(opt: ChromiumOptions) -> None: ...
def set_flags(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]: ... def get_chrome_path(ini_path: str) -> Union[str, None]: ...

View File

@ -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)

View File

@ -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: ...

View File

@ -7,6 +7,7 @@
""" """
from time import perf_counter from time import perf_counter
from .locator import is_loc
from .._elements.none_element import NoneElement 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 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): def _get_attr_all(src_list, aim_list, name, value, method, equal=True):
if equal: if equal:
for i in src_list: for i in src_list:

View File

@ -10,6 +10,7 @@ from typing import Union, List, Optional, Iterable
from .._base.base import BaseParser from .._base.base import BaseParser
from .._elements.chromium_element import ChromiumElement from .._elements.chromium_element import ChromiumElement
from .._elements.session_element import SessionElement from .._elements.session_element import SessionElement
from .._pages.chromium_frame import ChromiumFrame
def get_eles(locators: Union[List[str], tuple], def get_eles(locators: Union[List[str], tuple],
@ -19,6 +20,11 @@ def get_eles(locators: Union[List[str], tuple],
timeout: float = 10) -> dict: ... 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): class SessionElementsList(list):
_page = ... _page = ...

View File

@ -5,6 +5,8 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from platform import system
from ..errors import AlertExistsError from ..errors import AlertExistsError
@ -21,18 +23,14 @@ class Keys:
CANCEL = '\ue001' # ^break CANCEL = '\ue001' # ^break
HELP = '\ue002' HELP = '\ue002'
BACKSPACE = '\ue003' BACKSPACE = '\ue003'
BACK_SPACE = BACKSPACE
TAB = '\ue004' TAB = '\ue004'
CLEAR = '\ue005' CLEAR = '\ue005'
RETURN = '\ue006' RETURN = '\ue006'
ENTER = '\ue007' ENTER = '\ue007'
SHIFT = '\ue008' SHIFT = '\ue008'
LEFT_SHIFT = SHIFT
CONTROL = '\ue009' CONTROL = '\ue009'
CTRL = '\ue009' CTRL = '\ue009'
LEFT_CONTROL = CONTROL
ALT = '\ue00a' ALT = '\ue00a'
LEFT_ALT = ALT
PAUSE = '\ue00b' PAUSE = '\ue00b'
ESCAPE = '\ue00c' ESCAPE = '\ue00c'
SPACE = '\ue00d' SPACE = '\ue00d'
@ -41,13 +39,9 @@ class Keys:
END = '\ue010' END = '\ue010'
HOME = '\ue011' HOME = '\ue011'
LEFT = '\ue012' LEFT = '\ue012'
ARROW_LEFT = LEFT
UP = '\ue013' UP = '\ue013'
ARROW_UP = UP
RIGHT = '\ue014' RIGHT = '\ue014'
ARROW_RIGHT = RIGHT
DOWN = '\ue015' DOWN = '\ue015'
ARROW_DOWN = DOWN
INSERT = '\ue016' INSERT = '\ue016'
DELETE = '\ue017' DELETE = '\ue017'
DEL = '\ue017' DEL = '\ue017'
@ -219,15 +213,15 @@ keyDefinitions = {
'\ue005': {'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3}, '\ue005': {'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3},
'\ue006': {'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3}, '\ue006': {'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3},
'\ue00b': {'keyCode': 19, 'code': 'Pause', 'key': 'Pause'}, '\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'}, '\ue00c': {'keyCode': 27, 'code': 'Escape', 'key': 'Escape'},
'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'}, # 'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'},
'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'}, # 'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'},
'\ue010': {'keyCode': 35, 'code': 'End', 'key': 'End'}, '\ue010': {'keyCode': 35, 'code': 'End', 'key': 'End'},
# 'Numpad1': {'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3}, # 'Numpad1': {'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3},
'Select': {'keyCode': 41, 'code': 'Select', 'key': 'Select'}, # 'Select': {'keyCode': 41, 'code': 'Select', 'key': 'Select'},
'Open': {'keyCode': 43, 'code': 'Open', 'key': 'Execute'}, # 'Open': {'keyCode': 43, 'code': 'Open', 'key': 'Execute'},
'PrintScreen': {'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen'}, # 'PrintScreen': {'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen'},
'\ue016': {'keyCode': 45, 'code': 'Insert', 'key': 'Insert'}, '\ue016': {'keyCode': 45, 'code': 'Insert', 'key': 'Insert'},
# 'Numpad0': {'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3}, # 'Numpad0': {'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3},
'\ue017': {'keyCode': 46, 'code': 'Delete', 'key': 'Delete'}, '\ue017': {'keyCode': 46, 'code': 'Delete', 'key': 'Delete'},
@ -243,35 +237,6 @@ keyDefinitions = {
'\ue021': {'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7'}, '\ue021': {'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7'},
'\ue022': {'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8'}, '\ue022': {'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8'},
'\ue023': {'keyCode': 57, 'code': 'Digit9', 'shiftKey': '\(', 'key': '9'}, '\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}, '\ue024': {'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3},
'\ue025': {'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3}, '\ue025': {'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3},
'\ue027': {'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3}, '\ue027': {'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3},
@ -288,64 +253,91 @@ keyDefinitions = {
'\ue03a': {'keyCode': 121, 'code': 'F10', 'key': 'F10'}, '\ue03a': {'keyCode': 121, 'code': 'F10', 'key': 'F10'},
'\ue03b': {'keyCode': 122, 'code': 'F11', 'key': 'F11'}, '\ue03b': {'keyCode': 122, 'code': 'F11', 'key': 'F11'},
'\ue03c': {'keyCode': 123, 'code': 'F12', 'key': 'F12'}, '\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': ';'}, '\ue018': {'keyCode': 186, 'code': 'Semicolon', 'shiftKey': ':', 'key': ';'},
'Equal': {'keyCode': 187, 'code': 'Equal', 'shiftKey': '+', 'key': '='},
'\ue019': {'keyCode': 187, 'code': 'NumpadEqual', 'key': '=', 'location': 3}, '\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}, '\u0000': {'keyCode': 46, 'key': '\u0000', 'code': 'NumpadDecimal', 'location': 3},
'Attn': {'keyCode': 246, 'key': 'Attn'}, # 'KeyA': {'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a'},
'CrSel': {'keyCode': 247, 'key': 'CrSel', 'code': 'Props'}, # 'KeyB': {'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b'},
'ExSel': {'keyCode': 248, 'key': 'ExSel'}, # 'KeyC': {'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c'},
'EraseEof': {'keyCode': 249, 'key': 'EraseEof'}, # 'KeyD': {'keyCode': 68, 'code': 'KeyD', 'shiftKey': 'D', 'key': 'd'},
'Play': {'keyCode': 250, 'key': 'Play'}, # 'KeyE': {'keyCode': 69, 'code': 'KeyE', 'shiftKey': 'E', 'key': 'e'},
'ZoomOut': {'keyCode': 251, 'key': 'ZoomOut'}, # 'KeyF': {'keyCode': 70, 'code': 'KeyF', 'shiftKey': 'F', 'key': 'f'},
'Power': {'key': 'Power', 'code': 'Power'}, # 'KeyG': {'keyCode': 71, 'code': 'KeyG', 'shiftKey': 'G', 'key': 'g'},
'Eject': {'key': 'Eject', 'code': 'Eject'}, # '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, modifierBit = {'\ue00a': 1, '\ue009': 2, '\ue03d': 4, '\ue008': 8}
'\ue009': 2, sys = system().lower()
'\ue03d': 4,
'\ue008': 8}
def keys_to_typing(value): def keys_to_typing(value):
@ -368,71 +360,69 @@ def keys_to_typing(value):
return modifier, ''.join(typing) return modifier, ''.join(typing)
def keyDescriptionForString(_modifiers, keyString): # noqa: C901 def make_input_data(modifiers, key, key_up=False):
shift = _modifiers & 8 """
description = {'key': '', :param modifiers: 功能键设置
'keyCode': 0, :param key: 按键字符
'code': '', :param key_up: 是否提起
'text': '', :return: None
'location': 0} """
data = keyDefinitions.get(key)
if not data:
return None
definition = keyDefinitions.get(keyString) # type: ignore result = {'modifiers': modifiers, 'autoRepeat': False, '_ignore': AlertExistsError}
if not definition: shift = modifiers & 8
raise ValueError(f'未知按键:{keyString}')
if 'key' in definition: if shift and data.get('shiftKey'):
description['key'] = definition['key'] result['key'] = data['shiftKey']
if shift and definition.get('shiftKey'): result['text'] = data['shiftKey']
description['key'] = definition['shiftKey'] elif 'key' in data:
result['key'] = data['key']
if 'keyCode' in definition: if len(result.get('key', '')) == 1: # type: ignore
description['keyCode'] = definition['keyCode'] result['text'] = data['key']
if shift and definition.get('shiftKeyCode'):
description['keyCode'] = definition['shiftKeyCode']
if 'code' in definition: sys_text = 'windowsVirtualKeyCode' if sys == 'windows' else 'nativeVirtualKeyCode'
description['code'] = definition['code'] if shift and data.get('shiftKeyCode'):
result[sys_text] = data['shiftKeyCode']
elif 'keyCode' in data:
result[sys_text] = data['keyCode']
if 'location' in definition: if 'code' in data:
description['location'] = definition['location'] result['code'] = data['code']
if len(description['key']) == 1: # type: ignore if 'location' in data:
description['text'] = description['key'] result['location'] = data['location']
result['isKeypad'] = data['location'] == 3
else:
result['location'] = 0
result['isKeypad'] = False
if 'text' in definition: if shift and data.get('shiftText'):
description['text'] = definition['text'] result['text'] = data['shiftText']
if shift and definition.get('shiftText'): result['unmodifiedText'] = data['shiftText']
description['text'] = definition['shiftText'] elif 'text' in data:
result['text'] = data['text']
result['unmodifiedText'] = data['text']
if _modifiers & ~8: if modifiers & ~8:
description['text'] = '' 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): def send_key(page, modifier, key):
"""发送一个字,在键盘中的字符触发按键,其它直接发送文本""" """发送一个字,在键盘中的字符触发按键,其它直接发送文本"""
if key in keyDefinitions: data = make_input_data(modifier, key)
description = keyDescriptionForString(modifier, key) if data:
text = description['text'] page._run_cdp('Input.dispatchKeyEvent', **data)
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['type'] = 'keyUp' data['type'] = 'keyUp'
page.run_cdp('Input.dispatchKeyEvent', **data) page._run_cdp('Input.dispatchKeyEvent', **data)
else: 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): def input_text_or_keys(page, text_or_keys):
@ -451,7 +441,7 @@ def input_text_or_keys(page, text_or_keys):
return return
if text_or_keys.endswith(('\n', '\ue007')): 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') send_key(page, modifier, '\n')
else: else:
page.run_cdp('Input.insertText', text=text_or_keys, _ignore=AlertExistsError) page._run_cdp('Input.insertText', text=text_or_keys, _ignore=AlertExistsError)

View File

@ -5,7 +5,7 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from typing import Tuple, Dict, Union, Any from typing import Tuple, Union, Any
from .._pages.chromium_base import ChromiumBase from .._pages.chromium_base import ChromiumBase
@ -23,18 +23,14 @@ class Keys:
CANCEL: str CANCEL: str
HELP: str HELP: str
BACKSPACE: str BACKSPACE: str
BACK_SPACE: str
TAB: str TAB: str
CLEAR: str CLEAR: str
RETURN: str RETURN: str
ENTER: str ENTER: str
SHIFT: str SHIFT: str
LEFT_SHIFT: str
CONTROL: str CONTROL: str
CTRL: str CTRL: str
LEFT_CONTROL: str
ALT: str ALT: str
LEFT_ALT: str
PAUSE: str PAUSE: str
ESCAPE: str ESCAPE: str
SPACE: str SPACE: str
@ -43,13 +39,9 @@ class Keys:
END: str END: str
HOME: str HOME: str
LEFT: str LEFT: str
ARROW_LEFT: str
UP: str UP: str
ARROW_UP: str
RIGHT: str RIGHT: str
ARROW_RIGHT: str
DOWN: str DOWN: str
ARROW_DOWN: str
INSERT: str INSERT: str
DELETE: str DELETE: str
DEL: str DEL: str
@ -96,7 +88,7 @@ modifierBit: dict = ...
def keys_to_typing(value: Union[str, int, list, tuple]) -> Tuple[int, str]: ... 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: ... def send_key(page: ChromiumBase, modifier: int, key: str) -> None: ...

View File

@ -8,7 +8,7 @@
from pathlib import Path from pathlib import Path
from platform import system from platform import system
from shutil import rmtree from shutil import rmtree
from tempfile import gettempdir, TemporaryDirectory from tempfile import gettempdir
from threading import Lock from threading import Lock
from time import perf_counter, sleep from time import perf_counter, sleep
@ -18,44 +18,52 @@ from ..errors import (ContextLostError, ElementLostError, CDPError, PageDisconne
class PortFinder(object): class PortFinder(object):
used_port = {} used_port = set()
prev_time = 0
lock = Lock() lock = Lock()
checked_paths = set()
def __init__(self, path=None): def __init__(self, path=None):
""" """
:param path: 临时文件保存路径为None时使用系统临时文件夹 :param path: 临时文件保存路径为None时使用系统临时文件夹
""" """
tmp = Path(path) if path else Path(gettempdir()) / 'DrissionPage' 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) self.tmp_dir.mkdir(parents=True, exist_ok=True)
if not PortFinder.used_port: if str(self.tmp_dir.absolute()) not in PortFinder.checked_paths:
clean_folder(self.tmp_dir) 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): def get_port(self, scope=None):
"""查找一个可用端口 """查找一个可用端口
:param scope: 指定端口范围不含最后的数字为None则使用[9600-19600) :param scope: 指定端口范围不含最后的数字为None则使用[9600-59600)
:return: 可以使用的端口和用户文件夹路径组成的元组 :return: 可以使用的端口和用户文件夹路径组成的元组
""" """
from random import randint
with PortFinder.lock: with PortFinder.lock:
if PortFinder.prev_time and perf_counter() - PortFinder.prev_time > 60:
PortFinder.used_port.clear()
if scope in (True, None): if scope in (True, None):
scope = (9600, 19600) scope = (9600, 59600)
for i in range(scope[0], scope[1]): max_times = scope[1] - scope[0]
if i in PortFinder.used_port: 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 continue
elif port_is_using('127.0.0.1', i): path = self.tmp_dir / str(port)
PortFinder.used_port[i] = None if path.exists():
continue try:
path = TemporaryDirectory(dir=self.tmp_dir).name rmtree(path)
PortFinder.used_port[i] = path except:
return i, path continue
PortFinder.used_port.add(port)
for i in range(scope[0], scope[1]): PortFinder.prev_time = perf_counter()
if port_is_using('127.0.0.1', i): return port, str(path)
continue raise OSError('未找到可用端口。')
rmtree(PortFinder.used_port[i], ignore_errors=True)
return i, TemporaryDirectory(dir=self.tmp_dir).name
raise OSError('未找到可用端口。')
def port_is_using(ip, port): def port_is_using(ip, port):
@ -95,7 +103,7 @@ def show_or_hide_browser(page, hide=True):
:param hide: 是否隐藏 :param hide: 是否隐藏
:return: None :return: None
""" """
if not page.address.startswith(('127.0.0.1', 'localhost')): if not page.browser.address.startswith(('127.0.0.1', 'localhost')):
return return
if system().lower() != 'windows': if system().lower() != 'windows':
@ -191,10 +199,11 @@ def configs_to_here(save_name=None):
om.save(save_name) om.save(save_name)
def raise_error(result, ignore=None): def raise_error(result, ignore=None, user=False):
"""抛出error对应报错 """抛出error对应报错
:param result: 包含error的dict :param result: 包含error的dict
:param ignore: 要忽略的错误 :param ignore: 要忽略的错误
:param user: 是否用户调用的
:return: None :return: None
""" """
error = result['error'] error = result['error']
@ -220,13 +229,18 @@ def raise_error(result, ignore=None):
elif error == 'Given expression does not evaluate to a function': elif error == 'Given expression does not evaluate to a function':
r = JavaScriptError(f'传入的js无法解析成函数\n{result["args"]["functionDeclaration"]}') r = JavaScriptError(f'传入的js无法解析成函数\n{result["args"]["functionDeclaration"]}')
elif error.endswith("' wasn't found"): elif error.endswith("' wasn't found"):
r = RuntimeError(f'你的浏览器可能太旧。\n方法:{result["method"]}\n参数:{result["args"]}') r = RuntimeError(f'没有找到对应功能,方法错误或你的浏览器太旧。\n方法:{result["method"]}\n参数:{result["args"]}')
elif result['type'] in ('call_method_error', 'timeout'): 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__ from DrissionPage import __version__
txt = f'\n错误:{result["error"]}\n方法:{result["method"]}\n参数:{result["args"]}\n' \ txt = f'\n错误:{result["error"]}\n方法:{result["method"]}\n参数:{result["args"]}\n' \
f'版本:{__version__}\n出现这个错误可能意味着程序有bug请把错误信息和重现方法' \ f'版本:{__version__}\n出现这个错误可能意味着程序有bug请把错误信息和重现方法' \
'告知作者,谢谢。\n报告网站https://gitee.com/g1879/DrissionPage/issues' '告知作者,谢谢。\n报告网站https://gitee.com/g1879/DrissionPage/issues'
r = TimeoutError(txt) if result['type'] == 'timeout' else CDPError(txt) r = CDPError(txt)
else: else:
r = RuntimeError(result) r = RuntimeError(result)

View File

@ -14,9 +14,11 @@ from .._pages.chromium_base import ChromiumBase
class PortFinder(object): class PortFinder(object):
used_port: dict = ... used_port: set = ...
prev_time: float = ...
lock: Lock = ... lock: Lock = ...
tmp_dir: Path = ... tmp_dir: Path = ...
checked_paths: set = ...
def __init__(self, path: Union[str, Path] = None): ... 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 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: ...

View File

@ -5,16 +5,14 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from datetime import datetime
from html import unescape from html import unescape
from http.cookiejar import Cookie, CookieJar
from os.path import sep from os.path import sep
from pathlib import Path from pathlib import Path
from re import sub, match from re import sub
from urllib.parse import urlparse, urljoin, urlunparse from urllib.parse import urlparse, urljoin, urlunparse
from DataRecorder.tools import make_valid_name from DataRecorder.tools import make_valid_name
from tldextract import extract from requests.structures import CaseInsensitiveDict
def get_ele_txt(e): def get_ele_txt(e):
@ -59,7 +57,7 @@ def get_ele_txt(e):
if sub('[ \n\t\r]', '', el) != '': # 字符除了回车和空格还有其它内容 if sub('[ \n\t\r]', '', el) != '': # 字符除了回车和空格还有其它内容
txt = el txt = el
if not pre: if not pre:
txt = txt.replace('\r\n', ' ').replace('\n', ' ').strip(' ') txt = txt.replace('\r\n', ' ').replace('\n', ' ')
txt = sub(r' {2,}', ' ', txt) txt = sub(r' {2,}', ' ', txt)
str_list.append(txt) str_list.append(txt)
@ -80,8 +78,31 @@ def get_ele_txt(e):
re_str = get_node_txt(e) re_str = get_node_txt(e)
if re_str and re_str[-1] == '\n': if re_str and re_str[-1] == '\n':
re_str.pop() 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): def format_html(text):
@ -106,30 +127,30 @@ def location_in_viewport(page, loc_x, loc_y):
const vHeight = document.documentElement.clientHeight; const vHeight = document.documentElement.clientHeight;
if (x< scrollLeft || y < scrollTop || x > vWidth + scrollLeft || y > vHeight + scrollTop){{return false;}} if (x< scrollLeft || y < scrollTop || x > vWidth + scrollLeft || y > vHeight + scrollTop){{return false;}}
return true;}}''' return true;}}'''
return page.run_js(js) return page._run_js(js)
def offset_scroll(ele, offset_x, offset_y): def offset_scroll(ele, offset_x, offset_y):
"""接收元素及偏移坐标,把坐标滚动到页面中间,返回该点在视口中的坐标 """接收元素及偏移坐标,把坐标滚动到页面中间,返回该点坐标
有偏移量时以元素左上角坐标为基准没有时以click_point为基准 有偏移量时以元素左上角坐标为基准没有时以click_point为基准
:param ele: 元素对象 :param ele: 元素对象
:param offset_x: 偏移量x :param offset_x: 偏移量x
:param offset_y: 偏移量y :param offset_y: 偏移量y
:return: 视口中的坐标 :return: 绝对坐标和相对坐标
""" """
loc_x, loc_y = ele.rect.location loc_x, loc_y = ele.rect.location
cp_x, cp_y = ele.rect.click_point cp_x, cp_y = ele.rect.click_point
lx = loc_x + offset_x if offset_x else cp_x lx = loc_x + offset_x if offset_x else cp_x
ly = loc_y + offset_y if offset_y else cp_y ly = loc_y + offset_y if offset_y else cp_y
if not location_in_viewport(ele.owner, lx, ly): if not location_in_viewport(ele.owner, lx, ly):
clientWidth = ele.owner.run_js('return document.body.clientWidth;') clientWidth = ele.owner._run_js('return document.body.clientWidth;')
clientHeight = ele.owner.run_js('return document.body.clientHeight;') clientHeight = ele.owner._run_js('return document.body.clientHeight;')
ele.owner.scroll.to_location(lx - clientWidth // 2, ly - clientHeight // 2) ele.owner.scroll.to_location(lx - clientWidth // 2, ly - clientHeight // 2)
cl_x, cl_y = ele.rect.viewport_location cl_x, cl_y = ele.rect.viewport_location
ccp_x, ccp_y = ele.rect.viewport_click_point ccp_x, ccp_y = ele.rect.viewport_click_point
cx = cl_x + offset_x if offset_x else ccp_x cx = cl_x + offset_x if offset_x else ccp_x
cy = cl_y + offset_y if offset_y else ccp_y 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): def make_absolute_link(link, baseURI=None):
@ -144,8 +165,11 @@ def make_absolute_link(link, baseURI=None):
link = link.strip().replace('\\', '/') link = link.strip().replace('\\', '/')
parsed = urlparse(link)._asdict() parsed = urlparse(link)._asdict()
if baseURI: if baseURI:
p = urlparse(baseURI)._asdict() if link.startswith('./'):
baseURI = f'{p["scheme"]}://{p["netloc"]}' baseURI = baseURI[:baseURI.rfind('/') + 1]
else:
p = urlparse(baseURI)._asdict()
baseURI = f'{p["scheme"]}://{p["netloc"]}'
# 是相对路径与页面url拼接并返回 # 是相对路径与页面url拼接并返回
if not parsed['netloc']: if not parsed['netloc']:
@ -171,188 +195,6 @@ def is_js_func(func):
return False 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): def get_blob(page, url, as_bytes=True):
"""获取知道blob资源 """获取知道blob资源
:param page: 资源所在页面对象 :param page: 资源所在页面对象
@ -378,7 +220,7 @@ def get_blob(page, url, as_bytes=True):
} }
""" """
try: try:
result = page.run_js(js, url) result = page._run_js(js, url)
except: except:
raise RuntimeError('无法获取该资源。') raise RuntimeError('无法获取该资源。')
if as_bytes: if as_bytes:
@ -426,7 +268,7 @@ def get_mhtml(page, path=None, name=None):
:param name: 文件名为None且path不为None时用title属性值 :param name: 文件名为None且path不为None时用title属性值
:return: mhtml文本 :return: mhtml文本
""" """
r = page.run_cdp('Page.captureSnapshot')['data'] r = page._run_cdp('Page.captureSnapshot')['data']
if path is None and name is None: if path is None and name is None:
return r return r
@ -452,7 +294,7 @@ def get_pdf(page, path=None, name=None, kwargs=None):
if 'printBackground' not in kwargs: if 'printBackground' not in kwargs:
kwargs['printBackground'] = True kwargs['printBackground'] = True
try: try:
r = page.run_cdp('Page.printToPDF', **kwargs)['data'] r = page._run_cdp('Page.printToPDF', **kwargs)['data']
except: except:
raise RuntimeError('保存失败,可能浏览器版本不支持。') raise RuntimeError('保存失败,可能浏览器版本不支持。')
from base64 import b64decode from base64 import b64decode
@ -528,7 +370,9 @@ def format_headers(txt):
:param txt: 从浏览器复制的原始文本格式headers :param txt: 从浏览器复制的原始文本格式headers
:return: dict格式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 return txt
headers = {} headers = {}
for header in txt.split('\n'): for header in txt.split('\n'):
@ -536,15 +380,3 @@ def format_headers(txt):
name, value = header.split(': ', maxsplit=1) name, value = header.split(': ', maxsplit=1)
headers[name] = value headers[name] = value
return headers 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)

View File

@ -5,18 +5,14 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from http.cookiejar import Cookie
from pathlib import Path from pathlib import Path
from typing import Union, Optional from typing import Union, Optional, Tuple
from requests import Session
from requests.cookies import RequestsCookieJar
from .._base.base import DrissionElement, BaseParser from .._base.base import DrissionElement, BaseParser
from .._elements.chromium_element import ChromiumElement from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_base import ChromiumBase from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_page import ChromiumPage from .._pages.chromium_page import ChromiumPage
from .._pages.chromium_tab import ChromiumTab from .._pages.tabs import ChromiumTab
def get_ele_txt(e: DrissionElement) -> str: ... 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 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: ... 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 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: ... def get_blob(page: ChromiumBase, url: str, as_bytes: bool = True) -> bytes: ...

View File

@ -18,7 +18,9 @@ from .._base.base import BasePage
from .._elements.chromium_element import run_js, make_chromium_eles from .._elements.chromium_element import run_js, make_chromium_eles
from .._elements.none_element import NoneElement from .._elements.none_element import NoneElement
from .._elements.session_element import make_session_ele 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.settings import Settings
from .._functions.tools import raise_error from .._functions.tools import raise_error
from .._functions.web import location_in_viewport from .._functions.web import location_in_viewport
@ -36,15 +38,15 @@ __ERROR__ = 'error'
class ChromiumBase(BasePage): 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 browser: Chromium
:param tab_id: 要控制的标签页id不指定默认为激活的 :param target_id: 要控制的target id不指定默认为激活的标签页
:param timeout: 超时时间
""" """
super().__init__() super().__init__()
self._browser = browser
self._is_loading = None self._is_loading = None
self._root_id = None # object id self._root_id = None # object id
self._set = None self._set = None
@ -65,35 +67,21 @@ class ChromiumBase(BasePage):
if not hasattr(self, '_listener'): if not hasattr(self, '_listener'):
self._listener = None 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._d_set_runtime_settings()
self._connect_browser(tab_id) self._connect_browser(target_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://')
def _d_set_runtime_settings(self): def _d_set_runtime_settings(self):
self._timeouts = Timeout(self) pass
self._load_mode = 'normal'
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 :return: None
""" """
self._is_reading = False self._is_reading = False
if not tab_id: if not target_id:
tabs = self.browser.driver.get(f'http://{self.address}/json').json() tabs = self.browser._driver.get(f'http://{self.browser.address}/json').json()
tabs = [(i['id'], i['url']) for i in tabs tabs = [(i['id'], i['url']) for i in tabs
if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')] if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')]
dialog = None dialog = None
@ -101,30 +89,30 @@ class ChromiumBase(BasePage):
for k, t in enumerate(tabs): for k, t in enumerate(tabs):
if t[1] == 'chrome://privacy-sandbox-dialog/notice': if t[1] == 'chrome://privacy-sandbox-dialog/notice':
dialog = k dialog = k
elif not tab_id: elif not target_id:
tab_id = t[0] target_id = t[0]
if tab_id and dialog is not None: if target_id and dialog is not None:
break break
if dialog is not None: if dialog is not None:
close_privacy_dialog(self, tabs[dialog][0]) close_privacy_dialog(self, tabs[dialog][0])
else: 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: if self._js_ready_state == 'complete' and self._ready_state is None:
self._get_document() self._get_document()
self._ready_state = 'complete' self._ready_state = 'complete'
def _driver_init(self, tab_id): def _driver_init(self, target_id):
"""新建页面、页面刷新、切换标签页后要进行的cdp参数初始化 """新建页面、页面刷新、切换标签页后要进行的cdp参数初始化
:param tab_id: 要跳转到的标签页id :param target_id: 要跳转到的target id
:return: None :return: None
""" """
self._is_loading = True 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._alert = Alert()
self._driver.set_callback('Page.javascriptDialogOpening', self._on_alert_open, immediate=True) 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('Page.enable')
self._driver.run('Emulation.setFocusEmulationEnabled', enabled=True) 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)): for i in findall(r"'id': '(.*?)'", str(r)):
self.browser._frames[i] = self.tab_id self.browser._frames[i] = self.tab_id
if not hasattr(self, '_frame_id'): if not hasattr(self, '_frame_id'):
@ -160,11 +148,11 @@ class ChromiumBase(BasePage):
end_time = perf_counter() + timeout end_time = perf_counter() + timeout
while perf_counter() < end_time: while perf_counter() < end_time:
try: 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 = end_time - perf_counter()
timeout = 1 if timeout <= 1 else timeout timeout = 1 if timeout <= 1 else timeout
self._root_id = self.run_cdp('DOM.resolveNode', backendNodeId=b_id, self._root_id = self._run_cdp('DOM.resolveNode', backendNodeId=b_id,
_timeout=timeout)['object']['objectId'] _timeout=timeout)['object']['objectId']
result = True result = True
break break
@ -180,7 +168,7 @@ class ChromiumBase(BasePage):
result = False result = False
if result: if result:
r = self.run_cdp('Page.getFrameTree') r = self._run_cdp('Page.getFrameTree')
for i in findall(r"'id': '(.*?)'", str(r)): for i in findall(r"'id': '(.*?)'", str(r)):
self.browser._frames[i] = self.tab_id self.browser._frames[i] = self.tab_id
@ -217,7 +205,7 @@ class ChromiumBase(BasePage):
def _onDomContentEventFired(self, **kwargs): def _onDomContentEventFired(self, **kwargs):
"""在页面刷新、变化后重新读取页面内容""" """在页面刷新、变化后重新读取页面内容"""
if self._load_mode == 'eager': 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): if self._get_document(self._load_end_time - perf_counter() - .1):
self._doc_got = True self._doc_got = True
self._ready_state = 'interactive' self._ready_state = 'interactive'
@ -242,10 +230,10 @@ class ChromiumBase(BasePage):
if 'backendNodeId' not in kwargs: if 'backendNodeId' not in kwargs:
raise TypeError('该输入框无法接管,请改用对<input>元素输入路径的方法设置。') raise TypeError('该输入框无法接管,请改用对<input>元素输入路径的方法设置。')
files = self._upload_list if kwargs['mode'] == 'selectMultiple' else self._upload_list[:1] 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.driver.set_callback('Page.fileChooserOpened', None)
self.run_cdp('Page.setInterceptFileChooserDialog', enabled=False) self._run_cdp('Page.setInterceptFileChooserDialog', enabled=False)
self._upload_list = None self._upload_list = None
def __call__(self, locator, index=1, timeout=None): def __call__(self, locator, index=1, timeout=None):
@ -326,6 +314,11 @@ class ChromiumBase(BasePage):
self._rect = TabRect(self) self._rect = TabRect(self)
return self._rect return self._rect
@property
def timeout(self):
"""返回timeout设置"""
return self._timeouts.base
@property @property
def timeouts(self): def timeouts(self):
"""返回timeouts设置""" """返回timeouts设置"""
@ -347,23 +340,23 @@ class ChromiumBase(BasePage):
@property @property
def title(self): def title(self):
"""返回当前页面title""" """返回当前页面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 @property
def url(self): def url(self):
"""返回当前页面url""" """返回当前页面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 @property
def _browser_url(self): def _browser_url(self):
"""用于被WebPage覆盖""" """用于被MixTab覆盖"""
return self.url return self.url
@property @property
def html(self): def html(self):
"""返回当前页面html文本""" """返回当前页面html文本"""
self.wait.doc_loaded() 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 @property
def json(self): def json(self):
@ -381,12 +374,12 @@ class ChromiumBase(BasePage):
@property @property
def _target_id(self): def _target_id(self):
"""返回当前标签页id""" """返回当前标签页id"""
return self.driver.id if not self.driver._stopped.is_set() else '' return self.driver.id if self.driver.is_running else ''
@property @property
def active_ele(self): def active_ele(self):
"""返回当前焦点所在元素""" """返回当前焦点所在元素"""
return self.run_js_loaded('return document.activeElement;') return self._run_js_loaded('return document.activeElement;')
@property @property
def load_mode(self): def load_mode(self):
@ -396,7 +389,7 @@ class ChromiumBase(BasePage):
@property @property
def user_agent(self): def user_agent(self):
"""返回user agent""" """返回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 @property
def upload_list(self): def upload_list(self):
@ -407,7 +400,7 @@ class ChromiumBase(BasePage):
def _js_ready_state(self): def _js_ready_state(self):
"""返回js获取的ready state信息""" """返回js获取的ready state信息"""
try: 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: except ContextLostError:
return None return None
except TimeoutError: except TimeoutError:
@ -419,9 +412,8 @@ class ChromiumBase(BasePage):
:param cmd_args: 参数 :param cmd_args: 参数
:return: 执行的结果 :return: 执行的结果
""" """
ignore = cmd_args.pop('_ignore', None)
r = self.driver.run(cmd, **cmd_args) 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): def run_cdp_loaded(self, cmd, **cmd_args):
"""执行Chrome DevTools Protocol语句执行前等待页面加载完毕 """执行Chrome DevTools Protocol语句执行前等待页面加载完毕
@ -430,7 +422,27 @@ class ChromiumBase(BasePage):
:return: 执行的结果 :return: 执行的结果
""" """
self.wait.doc_loaded() 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): def run_js(self, script, *args, as_expr=False, timeout=None):
"""运行javascript代码 """运行javascript代码
@ -440,9 +452,30 @@ class ChromiumBase(BasePage):
:param timeout: js超时时间为None则使用页面timeouts.script设置 :param timeout: js超时时间为None则使用页面timeouts.script设置
:return: 运行的结果 :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): 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代码执行前等待页面加载完毕 """运行javascript代码执行前等待页面加载完毕
:param script: js文本或js文件路径 :param script: js文本或js文件路径
:param args: 参数按顺序在js文本中对应arguments[0]arguments[1]... :param args: 参数按顺序在js文本中对应arguments[0]arguments[1]...
@ -476,29 +509,27 @@ class ChromiumBase(BasePage):
show_errmsg=show_errmsg, timeout=timeout) show_errmsg=show_errmsg, timeout=timeout)
return self._url_available 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信息 """返回cookies信息
:param as_dict: 为True时以dict格式返回且all_info无效为False时返回list
:param all_domains: 是否返回所有域的cookies :param all_domains: 是否返回所有域的cookies
:param all_info: 是否返回所有信息为False时只返回namevaluedomain :param all_info: 是否返回所有信息为False时只返回namevaluedomain
:return: cookies信息 :return: cookies信息
""" """
txt = 'Storage' if all_domains else 'Network' 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: if all_info:
return {cookie['name']: cookie['value'] for cookie in cookies} r = cookies
elif all_info:
return cookies
else: else:
return [{'name': cookie['name'], 'value': cookie['value'], 'domain': cookie['domain']} r = [{'name': cookie['name'], 'value': cookie['value'], 'domain': cookie['domain']} for cookie in cookies]
for cookie in cookies]
return CookiesList(r)
def ele(self, locator, index=1, timeout=None): def ele(self, locator, index=1, timeout=None):
"""获取一个符合条件的元素对象 """获取一个符合条件的元素对象
:param locator: 定位符或元素对象 :param locator: 定位符或元素对象
:param index: 获取第几个元素从1开始可传入负数获取倒数第几个 :param index: 获取第几个元素从1开始可传入负数获取倒数第几个
:param timeout: 查找超时时间 :param timeout: 查找超时时间默认与页面等待时间一致
:return: ChromiumElement对象 :return: ChromiumElement对象
""" """
return self._ele(locator, timeout=timeout, index=index, method='ele()') return self._ele(locator, timeout=timeout, index=index, method='ele()')
@ -506,32 +537,37 @@ class ChromiumBase(BasePage):
def eles(self, locator, timeout=None): def eles(self, locator, timeout=None):
"""获取所有符合条件的元素对象 """获取所有符合条件的元素对象
:param locator: 定位符或元素对象 :param locator: 定位符或元素对象
:param timeout: 查找超时时间 :param timeout: 查找超时时间默认与页面等待时间一致
:return: ChromiumElement对象组成的列表 :return: ChromiumElement对象组成的列表
""" """
return self._ele(locator, timeout=timeout, index=None) 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形式返回处理复杂页面时效率很高 """查找一个符合条件的元素以SessionElement形式返回处理复杂页面时效率很高
:param locator: 元素的定位信息可以是loc元组或查询字符串 :param locator: 元素的定位信息可以是loc元组或查询字符串
:param index: 获取第几个从1开始可传入负数获取倒数第几个 :param index: 获取第几个从1开始可传入负数获取倒数第几个
:param timeout: 查找元素超时时间默认与页面等待时间一致
:return: SessionElement对象或属性文本 :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列表形式返回 """查找所有符合条件的元素以SessionElement列表形式返回
:param locator: 元素的定位信息可以是loc元组或查询字符串 :param locator: 元素的定位信息可以是loc元组或查询字符串
:param timeout: 查找元素超时时间默认与页面等待时间一致
:return: SessionElement对象组成的列表 :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): def _find_elements(self, locator, timeout=None, index=1, relative=False, raise_err=None):
"""执行元素查找 """执行元素查找
:param locator: 定位符或元素对象 :param locator: 定位符或元素对象
:param timeout: 查找超时时间 :param timeout: 查找超时时间
:param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有 :param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有
:param relative: WebPage用的表示是否相对定位的参数 :param relative: MixTab用的表示是否相对定位的参数
:param raise_err: 找不到元素是是否抛出异常为None时根据全局设置 :param raise_err: 找不到元素是是否抛出异常为None时根据全局设置
:return: ChromiumElement对象或元素对象组成的列表 :return: ChromiumElement对象或元素对象组成的列表
""" """
@ -605,7 +641,7 @@ class ChromiumBase(BasePage):
:return: None :return: None
""" """
self._is_loading = True self._is_loading = True
self.run_cdp('Page.reload', ignoreCache=ignore_cache) self._run_cdp('Page.reload', ignoreCache=ignore_cache)
self.wait.load_start() self.wait.load_start()
def forward(self, steps=1): def forward(self, steps=1):
@ -630,7 +666,7 @@ class ChromiumBase(BasePage):
if steps == 0: if steps == 0:
return return
history = self.run_cdp('Page.getNavigationHistory') history = self._run_cdp('Page.getNavigationHistory')
index = history['currentIndex'] index = history['currentIndex']
history = history['entries'] history = history['entries']
direction = 1 if steps > 0 else -1 direction = 1 if steps > 0 else -1
@ -646,12 +682,12 @@ class ChromiumBase(BasePage):
if nid: if nid:
self._is_loading = True self._is_loading = True
self.run_cdp('Page.navigateToHistoryEntry', entryId=nid) self._run_cdp('Page.navigateToHistoryEntry', entryId=nid)
def stop_loading(self): def stop_loading(self):
"""页面停止加载""" """页面停止加载"""
try: try:
self.run_cdp('Page.stopLoading') self._run_cdp('Page.stopLoading')
end_time = perf_counter() + 5 end_time = perf_counter() + 5
while self._ready_state != 'complete' and perf_counter() < end_time: while self._ready_state != 'complete' and perf_counter() < end_time:
sleep(.1) sleep(.1)
@ -669,7 +705,7 @@ class ChromiumBase(BasePage):
return return
ele = self._ele(loc_or_ele, raise_err=False) ele = self._ele(loc_or_ele, raise_err=False)
if ele: 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): def add_ele(self, html_or_info, insert_to=None, before=None):
"""新建一个元素 """新建一个元素
@ -725,7 +761,7 @@ class ChromiumBase(BasePage):
else: else:
raise TypeError('html_or_info参数必须是html文本或tupletuple格式为(tag, {name: value})。') raise TypeError('html_or_info参数必须是html文本或tupletuple格式为(tag, {name: value})。')
ele = self.run_js(js, *args) ele = self._run_js(js, *args)
return ele return ele
def get_frame(self, loc_ind_ele, timeout=None): def get_frame(self, loc_ind_ele, timeout=None):
@ -734,41 +770,7 @@ class ChromiumBase(BasePage):
:param timeout: 查找元素超时时间 :param timeout: 查找元素超时时间
:return: ChromiumFrame对象 :return: ChromiumFrame对象
""" """
if isinstance(loc_ind_ele, str): return get_frame(self, loc_ind_ele=loc_ind_ele, timeout=timeout)
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
def get_frames(self, locator=None, timeout=None): def get_frames(self, locator=None, timeout=None):
"""获取所有符合条件的frame对象 """获取所有符合条件的frame对象
@ -786,7 +788,7 @@ class ChromiumBase(BasePage):
:return: sessionStorage一个或所有项内容 :return: sessionStorage一个或所有项内容
""" """
js = f'sessionStorage.getItem("{item}")' if item else '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): def local_storage(self, item=None):
"""返回localStorage信息不设置item则获取全部 """返回localStorage信息不设置item则获取全部
@ -794,7 +796,7 @@ class ChromiumBase(BasePage):
:return: localStorage一个或所有项内容 :return: localStorage一个或所有项内容
""" """
js = f'localStorage.getItem("{item}")' if item else '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, def get_screenshot(self, path=None, name=None, as_bytes=None, as_base64=None,
full_page=False, left_top=None, right_bottom=None): full_page=False, left_top=None, right_bottom=None):
@ -816,8 +818,8 @@ class ChromiumBase(BasePage):
:param script: js文本 :param script: js文本
:return: 添加的脚本的id :return: 添加的脚本的id
""" """
js_id = self.run_cdp('Page.addScriptToEvaluateOnNewDocument', source=script, js_id = self._run_cdp('Page.addScriptToEvaluateOnNewDocument', source=script,
includeCommandLineAPI=True)['identifier'] includeCommandLineAPI=True)['identifier']
self._init_jss.append(js_id) self._init_jss.append(js_id)
return js_id return js_id
@ -828,11 +830,11 @@ class ChromiumBase(BasePage):
""" """
if script_id is None: if script_id is None:
for js_id in self._init_jss: 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() self._init_jss.clear()
elif script_id in self._init_jss: 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) self._init_jss.remove(script_id)
def clear_cache(self, session_storage=True, local_storage=True, cache=True, cookies=True): def clear_cache(self, session_storage=True, local_storage=True, cache=True, cookies=True):
@ -844,24 +846,25 @@ class ChromiumBase(BasePage):
:return: None :return: None
""" """
if session_storage or local_storage: if session_storage or local_storage:
self.run_cdp_loaded('DOMStorage.enable') self._run_cdp_loaded('DOMStorage.enable')
i = self.run_cdp('Storage.getStorageKeyForFrame', frameId=self._frame_id)['storageKey'] i = self._run_cdp('Storage.getStorageKeyForFrame', frameId=self._frame_id)['storageKey']
if session_storage: 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: if local_storage:
self.run_cdp('DOMStorage.clear', storageId={'storageKey': i, 'isLocalStorage': True}) self._run_cdp('DOMStorage.clear', storageId={'storageKey': i, 'isLocalStorage': True})
self.run_cdp_loaded('DOMStorage.disable') self._run_cdp_loaded('DOMStorage.disable')
if cache: if cache:
self.run_cdp_loaded('Network.clearBrowserCache') self._run_cdp_loaded('Network.clearBrowserCache')
if cookies: if cookies:
self.run_cdp_loaded('Network.clearBrowserCookies') self._run_cdp_loaded('Network.clearBrowserCookies')
def disconnect(self): def disconnect(self):
"""断开与页面的连接,不关闭页面""" """断开与页面的连接,不关闭页面"""
if self._driver: 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): def reconnect(self, wait=0):
"""断开与页面原来的页面,重新建立连接 """断开与页面原来的页面,重新建立连接
@ -987,7 +990,7 @@ class ChromiumBase(BasePage):
err = None err = None
end_time = perf_counter() + timeout end_time = perf_counter() + timeout
try: 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: if 'errorText' in result:
err = ConnectionError(result['errorText']) err = ConnectionError(result['errorText'])
except TimeoutError: except TimeoutError:
@ -1087,8 +1090,8 @@ class ChromiumBase(BasePage):
v = not (location_in_viewport(self, x, y) and v = not (location_in_viewport(self, x, y) and
location_in_viewport(self, right_bottom[0], right_bottom[1])) location_in_viewport(self, right_bottom[0], right_bottom[1]))
if v and (self.run_js('return document.body.scrollHeight > window.innerHeight;') and if v and (self._run_js('return document.body.scrollHeight > window.innerHeight;') and
not self.run_js('return document.body.scrollWidth > window.innerWidth;')): not self._run_js('return document.body.scrollWidth > window.innerWidth;')):
x += 10 x += 10
vp = {'x': x, 'y': y, 'width': w, 'height': h, 'scale': 1} vp = {'x': x, 'y': y, 'width': w, 'height': h, 'scale': 1}
@ -1099,7 +1102,7 @@ class ChromiumBase(BasePage):
if pic_type == 'jpeg': if pic_type == 'jpeg':
args['quality'] = 100 args['quality'] = 100
png = self.run_cdp_loaded('Page.captureScreenshot', **args)['data'] png = self._run_cdp_loaded('Page.captureScreenshot', **args)['data']
if as_base64: if as_base64:
return png return png
@ -1119,15 +1122,12 @@ class ChromiumBase(BasePage):
class Timeout(object): class Timeout(object):
"""用于保存d模式timeout信息的类""" """用于保存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 base: 默认超时时间
:param page_load: 页面加载超时时间 :param page_load: 页面加载超时时间
:param script: js超时时间 :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.base = 10 if base is None else base
self.page_load = 30 if page_load is None else page_load self.page_load = 30 if page_load is None else page_load
self.script = 30 if script is None else script self.script = 30 if script is None else script
@ -1135,6 +1135,10 @@ class Timeout(object):
def __repr__(self): def __repr__(self):
return str({'base': self.base, 'page_load': self.page_load, 'script': self.script}) 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): class Alert(object):
"""用于保存alert信息的类""" """用于保存alert信息的类"""
@ -1158,6 +1162,7 @@ def close_privacy_dialog(page, tid):
:return: None :return: None
""" """
try: try:
print('ooo')
driver = page.browser._get_driver(tid) driver = page.browser._get_driver(tid)
driver.run('Runtime.enable') driver.run('Runtime.enable')
driver.run('DOM.enable') driver.run('DOM.enable')

View File

@ -8,12 +8,13 @@
from pathlib import Path from pathlib import Path
from typing import Union, Tuple, List, Any, Optional, Literal 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.base import BasePage
from .._base.browser import Browser from .._base.browser import Chromium
from .._base.driver import Driver from .._base.driver import Driver
from .._elements.chromium_element import ChromiumElement from .._elements.chromium_element import ChromiumElement
from .._elements.session_element import SessionElement from .._elements.session_element import SessionElement
from .._functions.cookies import CookiesList
from .._functions.elements import SessionElementsList, ChromiumElementsList from .._functions.elements import SessionElementsList, ChromiumElementsList
from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage from .._pages.chromium_page import ChromiumPage
@ -31,13 +32,10 @@ PIC_TYPE = Literal['jpg', 'jpeg', 'png', 'webp', True]
class ChromiumBase(BasePage): class ChromiumBase(BasePage):
def __init__(self, def __init__(self,
address: Union[str, int], browser: Chromium,
tab_id: str = None, tab_id: str = None):
timeout: float = None): self._tab: Union[ChromiumTab, MixTab, ChromiumFrame] = ...
self._browser: Browser = ... self._browser: Chromium = ...
self._page: ChromiumPage = ...
self.tab: Union[ChromiumPage, ChromiumTab] = ...
self.address: str = ...
self._driver: Driver = ... self._driver: Driver = ...
self._frame_id: str = ... self._frame_id: str = ...
self._is_reading: bool = ... self._is_reading: bool = ...
@ -65,9 +63,9 @@ class ChromiumBase(BasePage):
self._rect: TabRect = ... self._rect: TabRect = ...
self._type: str = ... 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: ... def _get_document(self, timeout: float = 10) -> bool: ...
@ -91,7 +89,7 @@ class ChromiumBase(BasePage):
def _wait_to_stop(self): ... 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: ... def _d_set_runtime_settings(self) -> None: ...
@ -104,7 +102,7 @@ class ChromiumBase(BasePage):
def _js_ready_state(self) -> str: ... def _js_ready_state(self) -> str: ...
@property @property
def browser(self) -> Browser: ... def browser(self) -> Chromium: ...
@property @property
def title(self) -> str: ... def title(self) -> str: ...
@ -145,6 +143,9 @@ class ChromiumBase(BasePage):
@property @property
def rect(self) -> TabRect: ... def rect(self) -> TabRect: ...
@property
def timeout(self) -> float: ...
@property @property
def timeouts(self) -> Timeout: ... 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_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 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, def get(self, url: str, show_errmsg: bool = False, retry: int = None,
interval: float = None, timeout: float = None) -> Union[None, bool]: ... 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[ def cookies(self, all_domains: bool = False, all_info: bool = False) -> CookiesList: ...
list, dict]: ...
def ele(self, def ele(self,
locator: Union[Tuple[str, str], str, ChromiumElement, ChromiumFrame], locator: Union[Tuple[str, str], str, ChromiumElement, ChromiumFrame],
@ -192,9 +196,12 @@ class ChromiumBase(BasePage):
def s_ele(self, def s_ele(self,
locator: Union[Tuple[str, str], str] = None, 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, def _find_elements(self,
locator: Union[Tuple[str, str], str, ChromiumElement, ChromiumFrame], 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, insert_to: Union[ChromiumElement, str, Tuple[str, str], None] = None,
before: Union[ChromiumElement, str, Tuple[str, str], None] = None) -> ChromiumElement: ... 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]: ... 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_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 session_storage(self, item: str = None) -> Union[str, dict, None]: ...
def local_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): class Timeout(object):
def __init__(self, page: ChromiumBase, base=None, page_load=None, script=None): def __init__(self, base=None, page_load=None, script=None):
self._page: ChromiumBase = ...
self.base: float = ... self.base: float = ...
self.page_load: float = ... self.page_load: float = ...
self.script: float = ... self.script: float = ...
@property
def as_dict(self) -> dict: ...
class Alert(object): class Alert(object):

View File

@ -27,42 +27,26 @@ class ChromiumFrame(ChromiumBase):
:param ele: frame所在元素 :param ele: frame所在元素
:param info: frame所在元素信息 :param info: frame所在元素信息
""" """
if owner._type in ('ChromiumPage', 'WebPage'): self._tab = owner._tab
self._page = self._target_page = self.tab = owner self._target_page = 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._backend_id = ele._backend_id self._backend_id = ele._backend_id
self._frame_ele = ele self._frame_ele = ele
self._states = None
self._reloading = False 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'] self._frame_id = node['frameId']
if self._is_inner_frame(): if self._is_inner_frame():
self._is_diff_domain = False self._is_diff_domain = False
self.doc_ele = ChromiumElement(self._target_page, backend_id=node['contentDocument']['backendNodeId']) 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: else:
self._is_diff_domain = True self._is_diff_domain = True
delattr(self, '_frame_id') delattr(self, '_frame_id')
super().__init__(owner.address, node['frameId'], owner.timeout) super().__init__(owner.browser, node['frameId'])
obj_id = super().run_js('document;', as_expr=True)['objectId'] obj_id = super()._run_js('document;', as_expr=True)['objectId']
self.doc_ele = ChromiumElement(self, obj_id=obj_id) self.doc_ele = ChromiumElement(self, obj_id=obj_id)
self._rect = None
self._type = 'ChromiumFrame' 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): def __call__(self, locator, index=1, timeout=None):
"""在内部查找元素 """在内部查找元素
@ -90,16 +74,16 @@ class ChromiumFrame(ChromiumBase):
self._download_path = self._target_page.download_path self._download_path = self._target_page.download_path
self._load_mode = self._target_page._load_mode if not self._is_diff_domain else 'normal' 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错误 """避免出现服务器500错误
:param tab_id: 要跳转到的标签页id :param target_id: 要跳转到的target id
:return: None :return: None
""" """
try: try:
super()._driver_init(tab_id) super()._driver_init(target_id)
except: except:
self.browser.driver.get(f'http://{self.address}/json') self.browser._driver.get(f'http://{self._browser.address}/json')
super()._driver_init(tab_id) super()._driver_init(target_id)
self._driver.set_callback('Inspector.detached', self._onInspectorDetached, immediate=True) self._driver.set_callback('Inspector.detached', self._onInspectorDetached, immediate=True)
self._driver.set_callback('Page.frameDetached', None) self._driver.set_callback('Page.frameDetached', None)
self._driver.set_callback('Page.frameDetached', self._onFrameDetached, immediate=True) 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) self._frame_ele = ChromiumElement(self._target_page, backend_id=self._backend_id)
end_time = perf_counter() + 2 end_time = perf_counter() + 2
while perf_counter() < end_time: 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: if 'frameId' in node:
break break
sleep(.05) sleep(.05)
@ -132,16 +116,16 @@ class ChromiumFrame(ChromiumBase):
self.doc_ele = ChromiumElement(self._target_page, backend_id=node['contentDocument']['backendNodeId']) self.doc_ele = ChromiumElement(self._target_page, backend_id=node['contentDocument']['backendNodeId'])
self._frame_id = node['frameId'] self._frame_id = node['frameId']
if self._listener: if self._listener:
self._listener._to_target(self._target_page.tab_id, self.address, self) self._listener._to_target(self._target_page.tab_id, self._browser.address, self)
super().__init__(self.address, self._target_page.tab_id, self._target_page.timeout) super().__init__(self._browser, self._target_page.tab_id)
# self.driver._debug = d_debug # self.driver._debug = d_debug
else: else:
self._is_diff_domain = True self._is_diff_domain = True
if self._listener: 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 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() timeout = end_time - perf_counter()
if timeout <= 0: if timeout <= 0:
timeout = .5 timeout = .5
@ -161,17 +145,17 @@ class ChromiumFrame(ChromiumBase):
self._is_reading = True self._is_reading = True
try: try:
if self._is_diff_domain is False: 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']) self.doc_ele = ChromiumElement(self._target_page, backend_id=node['contentDocument']['backendNodeId'])
else: else:
timeout = max(timeout, 2) 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.doc_ele = ChromiumElement(self, backend_id=b_id)
self._root_id = self.doc_ele._obj_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)): for i in findall(r"'id': '(.*?)'", str(r)):
self.browser._frames[i] = self.tab_id self.browser._frames[i] = self.tab_id
return True return True
@ -250,11 +234,6 @@ class ChromiumFrame(ChromiumBase):
"""返回cdp中的node id""" """返回cdp中的node id"""
return self.frame_ele._node_id return self.frame_ele._node_id
@property
def page(self):
"""返回所属Page对象"""
return self._page
@property @property
def owner(self): def owner(self):
"""返回所属页面对象""" """返回所属页面对象"""
@ -274,7 +253,7 @@ class ChromiumFrame(ChromiumBase):
def url(self): def url(self):
"""返回frame当前访问的url""" """返回frame当前访问的url"""
try: try:
return self.doc_ele.run_js('return this.location.href;') return self.doc_ele._run_js('return this.location.href;')
except JavaScriptError: except JavaScriptError:
return None return None
@ -282,14 +261,14 @@ class ChromiumFrame(ChromiumBase):
def html(self): def html(self):
"""返回元素outerHTML文本""" """返回元素outerHTML文本"""
tag = self.tag 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) sign = search(rf'<{tag}.*?>', out_html, DOTALL).group(0)
return f'{sign}{self.inner_html}</{tag}>' return f'{sign}{self.inner_html}</{tag}>'
@property @property
def inner_html(self): def inner_html(self):
"""返回元素innerHTML文本""" """返回元素innerHTML文本"""
return self.doc_ele.run_js('return this.documentElement.outerHTML;') return self.doc_ele._run_js('return this.documentElement.outerHTML;')
@property @property
def title(self): def title(self):
@ -305,7 +284,7 @@ class ChromiumFrame(ChromiumBase):
@property @property
def active_ele(self): def active_ele(self):
"""返回当前焦点所在元素""" """返回当前焦点所在元素"""
return self.doc_ele.run_js('return this.activeElement;') return self.doc_ele._run_js('return this.activeElement;')
@property @property
def xpath(self): def xpath(self):
@ -317,15 +296,30 @@ class ChromiumFrame(ChromiumBase):
"""返回frame的css selector绝对路径""" """返回frame的css selector绝对路径"""
return self.frame_ele.css_path return self.frame_ele.css_path
@property
def tab(self):
"""返回frame所在tab的id"""
return self._tab
@property @property
def tab_id(self): def tab_id(self):
"""返回frame所在tab的id""" """返回frame所在tab的id"""
return self._tab_id return self.tab.tab_id
@property @property
def download_path(self): def download_path(self):
return self._download_path 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 @property
def _js_ready_state(self): def _js_ready_state(self):
"""返回当前页面加载状态,'loading' 'interactive' 'complete'""" """返回当前页面加载状态,'loading' 'interactive' 'complete'"""
@ -334,18 +328,18 @@ class ChromiumFrame(ChromiumBase):
else: else:
try: try:
return self.doc_ele.run_js('return this.readyState;') return self.doc_ele._run_js('return this.readyState;')
except ContextLostError: except ContextLostError:
try: 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']) 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: except:
return None return None
def refresh(self): def refresh(self):
"""刷新frame页面""" """刷新frame页面"""
self.doc_ele.run_js('this.location.reload();') self.doc_ele._run_js('this.location.reload();')
def property(self, name): def property(self, name):
"""返回frame元素一个property属性值 """返回frame元素一个property属性值
@ -369,6 +363,16 @@ class ChromiumFrame(ChromiumBase):
self.frame_ele.remove_attr(name) self.frame_ele.remove_attr(name)
def run_js(self, script, *args, as_expr=False, timeout=None): 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代码 """运行javascript代码
:param script: js文本 :param script: js文本
:param args: 参数按顺序在js文本中对应arguments[0]arguments[1]... :param args: 参数按顺序在js文本中对应arguments[0]arguments[1]...
@ -377,9 +381,9 @@ class ChromiumFrame(ChromiumBase):
:return: 运行的结果 :return: 运行的结果
""" """
if script.startswith('this.scrollIntoView'): 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: 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): def parent(self, level_or_loc=1, index=1):
"""返回上面某一级父元素,可指定层数或用查询语法定位 """返回上面某一级父元素,可指定层数或用查询语法定位
@ -542,12 +546,12 @@ class ChromiumFrame(ChromiumBase):
img.style.setProperty("position","fixed"); img.style.setProperty("position","fixed");
arguments[0].insertBefore(img, this); arguments[0].insertBefore(img, this);
return img;''' 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) new_ele.scroll.to_see(center=True)
top = int(self.frame_ele.style('border-top').split('px')[0]) top = int(self.frame_ele.style('border-top').split('px')[0])
left = int(self.frame_ele.style('border-left').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'] sx = r['pageX']
sy = r['pageY'] sy = r['pageY']
r = self.tab.get_screenshot(path=path, name=name, as_bytes=as_bytes, as_base64=as_base64, 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 locator: 定位符或元素对象
:param timeout: 查找超时时间 :param timeout: 查找超时时间
:param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有 :param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有
:param relative: WebPage用的表示是否相对定位的参数 :param relative: MixTab用的表示是否相对定位的参数
:param raise_err: 找不到元素是是否抛出异常为None时根据全局设置 :param raise_err: 找不到元素是是否抛出异常为None时根据全局设置
:return: ChromiumElement对象 :return: ChromiumElement对象
""" """
@ -573,4 +577,4 @@ class ChromiumFrame(ChromiumBase):
def _is_inner_frame(self): def _is_inner_frame(self):
"""返回当前frame是否同域""" """返回当前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'])

View File

@ -9,10 +9,8 @@ from pathlib import Path
from typing import Union, Tuple, List, Any, Optional from typing import Union, Tuple, List, Any, Optional
from .chromium_base import ChromiumBase from .chromium_base import ChromiumBase
from .chromium_page import ChromiumPage from .tabs import ChromiumTab, MixTab
from .chromium_tab import ChromiumTab from .._elements.chromium_element import ChromiumElement, ShadowRoot
from .web_page import WebPage
from .._elements.chromium_element import ChromiumElement
from .._functions.elements import ChromiumElementsList from .._functions.elements import ChromiumElementsList
from .._units.listener import FrameListener from .._units.listener import FrameListener
from .._units.rect import FrameRect from .._units.rect import FrameRect
@ -25,13 +23,11 @@ from .._units.waiter import FrameWaiter
class ChromiumFrame(ChromiumBase): class ChromiumFrame(ChromiumBase):
def __init__(self, def __init__(self,
owner: Union[ChromiumPage, WebPage, ChromiumTab, ChromiumFrame], owner: Union[ChromiumTab, ChromiumFrame],
ele: ChromiumElement, ele: ChromiumElement,
info: dict = None): info: dict = None):
self._target_page: ChromiumBase = ... self._target_page: Union[ChromiumTab, ChromiumFrame] = ...
self._page: ChromiumPage = ... self._tab: Union[MixTab, ChromiumTab] = ...
self.tab: Union[ChromiumPage, ChromiumTab] = ...
self._tab_id: str = ...
self._set: ChromiumFrameSetter = ... self._set: ChromiumFrameSetter = ...
self._frame_ele: ChromiumElement = ... self._frame_ele: ChromiumElement = ...
self._backend_id: int = ... self._backend_id: int = ...
@ -40,7 +36,7 @@ class ChromiumFrame(ChromiumBase):
self.doc_ele: ChromiumElement = ... self.doc_ele: ChromiumElement = ...
self._states: FrameStates = ... self._states: FrameStates = ...
self._reloading: bool = ... self._reloading: bool = ...
self._rect: FrameRect = ... self._rect: Optional[FrameRect] = ...
self._listener: FrameListener = ... self._listener: FrameListener = ...
def __call__(self, def __call__(self,
@ -56,7 +52,7 @@ class ChromiumFrame(ChromiumBase):
def _d_set_runtime_settings(self) -> None: ... 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: ... def _reload(self) -> None: ...
@ -66,9 +62,6 @@ class ChromiumFrame(ChromiumBase):
def _onInspectorDetached(self, **kwargs): ... def _onInspectorDetached(self, **kwargs): ...
@property
def page(self) -> Union[ChromiumPage, WebPage]: ...
@property @property
def owner(self) -> ChromiumBase: ... def owner(self) -> ChromiumBase: ...
@ -126,12 +119,21 @@ class ChromiumFrame(ChromiumBase):
@property @property
def wait(self) -> FrameWaiter: ... def wait(self) -> FrameWaiter: ...
@property
def tab(self) -> Union[ChromiumTab, MixTab]: ...
@property @property
def tab_id(self) -> str: ... def tab_id(self) -> str: ...
@property @property
def download_path(self) -> str: ... 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 refresh(self) -> None: ...
def property(self, name: str) -> Union[str, None]: ... def property(self, name: str) -> Union[str, None]: ...
@ -146,6 +148,12 @@ class ChromiumFrame(ChromiumBase):
as_expr: bool = False, as_expr: bool = False,
timeout: float = None) -> Any: ... timeout: float = None) -> Any: ...
def _run_js(self,
script: str,
*args,
as_expr: bool = False,
timeout: float = None) -> Any: ...
def parent(self, def parent(self,
level_or_loc: Union[Tuple[str, str], str, int] = 1, level_or_loc: Union[Tuple[str, str], str, int] = 1,
index: int = 1) -> ChromiumElement: ... index: int = 1) -> ChromiumElement: ...

View File

@ -5,23 +5,13 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from pathlib import Path from time import sleep
from threading import Lock
from time import sleep, perf_counter
from requests import Session from .._base.browser import Chromium
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 .._functions.web import save_page from .._functions.web import save_page
from .._pages.chromium_base import ChromiumBase, Timeout from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_tab import ChromiumTab
from .._units.setter import ChromiumPageSetter from .._units.setter import ChromiumPageSetter
from .._units.waiter import PageWaiter from .._units.waiter import PageWaiter
from ..errors import BrowserConnectError
class ChromiumPage(ChromiumBase): class ChromiumPage(ChromiumBase):
@ -34,19 +24,16 @@ class ChromiumPage(ChromiumBase):
:param tab_id: 要控制的标签页id不指定默认为激活的 :param tab_id: 要控制的标签页id不指定默认为激活的
:param timeout: 超时时间 :param timeout: 超时时间
""" """
opt = handle_options(addr_or_opts) browser = Chromium(addr_or_opts=addr_or_opts)
is_exist, browser_id = run_browser(opt) if browser.id in cls._PAGES:
if browser_id in cls._PAGES: r = cls._PAGES[browser.id]
r = cls._PAGES[browser_id]
while not hasattr(r, '_frame_id'): while not hasattr(r, '_frame_id'):
sleep(.1) sleep(.1)
return r return r
r = object.__new__(cls) r = object.__new__(cls)
r._chromium_options = opt r._browser = browser
r._is_exist = is_exist cls._PAGES[browser.id] = r
r._browser_id = browser_id
r.address = opt.address
cls._PAGES[browser_id] = r
return r return r
def __init__(self, addr_or_opts=None, tab_id=None, timeout=None): def __init__(self, addr_or_opts=None, tab_id=None, timeout=None):
@ -59,49 +46,19 @@ class ChromiumPage(ChromiumBase):
return return
self._created = True self._created = True
self._page = self
self.tab = self self.tab = self
self._run_browser() super().__init__(self.browser, tab_id)
super().__init__(self.address, tab_id)
self._type = 'ChromiumPage' self._type = 'ChromiumPage'
self._lock = Lock()
self.set.timeouts(base=timeout) self.set.timeouts(base=timeout)
self._page_init() self._tab = self
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()
def _d_set_runtime_settings(self): def _d_set_runtime_settings(self):
"""设置运行时用到的属性""" """设置运行时用到的属性"""
self._timeouts = Timeout(self, page_load=self._chromium_options.timeouts['page_load'], self._timeouts = self.browser.timeouts
script=self._chromium_options.timeouts['script'], self._load_mode = self.browser._load_mode
base=self._chromium_options.timeouts['base']) self._download_path = self.browser.download_path
if self._chromium_options.timeouts['base'] is not None: self.retry_times = self.browser.retry_times
self._timeout = self._chromium_options.timeouts['base'] self.retry_interval = self.browser.retry_interval
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()
# ----------挂件----------
@property @property
def set(self): def set(self):
@ -138,7 +95,7 @@ class ChromiumPage(ChromiumBase):
def latest_tab(self): def latest_tab(self):
"""返回最新的标签页,最新标签页指最后创建或最后被激活的 """返回最新的标签页,最新标签页指最后创建或最后被激活的
当Settings.singleton_tab_obj==True时返回Tab对象否则返回tab id""" 当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 @property
def process_id(self): def process_id(self):
@ -148,7 +105,12 @@ class ChromiumPage(ChromiumBase):
@property @property
def browser_version(self): 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): def save(self, path=None, name=None, as_pdf=False, **kwargs):
"""把当前页面保存为文件如果path和name参数都为None只返回文本 """把当前页面保存为文件如果path和name参数都为None只返回文本
@ -169,34 +131,7 @@ class ChromiumPage(ChromiumBase):
:param as_id: 是否返回标签页id而不是标签页对象 :param as_id: 是否返回标签页id而不是标签页对象
:return: ChromiumTab对象 :return: ChromiumTab对象
""" """
if id_or_num is not None: return self.browser.get_tab(id_or_num=id_or_num, title=title, url=url, tab_type=tab_type, as_id=as_id)
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)
def get_tabs(self, title=None, url=None, tab_type='page', as_id=False): def get_tabs(self, title=None, url=None, tab_type='page', as_id=False):
"""查找符合条件的tab返回它们组成的列表 """查找符合条件的tab返回它们组成的列表
@ -206,10 +141,7 @@ class ChromiumPage(ChromiumBase):
:param as_id: 是否返回标签页id而不是标签页对象 :param as_id: 是否返回标签页id而不是标签页对象
:return: ChromiumTab对象组成的列表 :return: ChromiumTab对象组成的列表
""" """
if as_id: return self.browser.get_tabs(title=title, url=url, tab_type=tab_type, as_id=as_id)
return [tab['id'] for tab in self._browser.find_tabs(title, url, tab_type)]
with self._lock:
return [ChromiumTab(self, tab['id']) for tab in self._browser.find_tabs(title, url, tab_type)]
def new_tab(self, url=None, new_window=False, background=False, new_context=False): def new_tab(self, url=None, new_window=False, background=False, new_context=False):
"""新建一个标签页 """新建一个标签页
@ -219,10 +151,14 @@ class ChromiumPage(ChromiumBase):
:param new_context: 是否创建新的上下文 :param new_context: 是否创建新的上下文
:return: 新标签页对象 :return: 新标签页对象
""" """
tab = ChromiumTab(self, tab_id=self.browser.new_tab(new_window, background, new_context)) return self.browser.new_tab(url=url, new_window=new_window, background=background, new_context=new_context)
if url:
tab.get(url) def activate_tab(self, id_ind_tab):
return tab """使标签页变为活动状态
:param id_ind_tab: 标签页idstrTab对象或标签页序号int序号从1开始
:return: None
"""
self.browser.activate_tab(id_ind_tab)
def close(self): def close(self):
"""关闭Page管理的标签页""" """关闭Page管理的标签页"""
@ -234,32 +170,7 @@ class ChromiumPage(ChromiumBase):
:param others: 是否关闭指定标签页之外的 :param others: 是否关闭指定标签页之外的
:return: None :return: None
""" """
all_tabs = set(self.tab_ids) self.browser.close_tabs(tabs_or_ids=tabs_or_ids, others=others)
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)
def quit(self, timeout=5, force=True): def quit(self, timeout=5, force=True):
"""关闭浏览器 """关闭浏览器
@ -271,69 +182,7 @@ class ChromiumPage(ChromiumBase):
def _on_disconnect(self): def _on_disconnect(self):
"""浏览器退出时执行""" """浏览器退出时执行"""
ChromiumPage._PAGES.pop(self._browser_id, None) ChromiumPage._PAGES.pop(self._browser.id, None)
def __repr__(self): def __repr__(self):
return f'<ChromiumPage browser_id={self.browser.id} tab_id={self.tab_id}>' return f'<ChromiumPage browser_id={self.browser.id} tab_id={self.tab_id}>'
def handle_options(addr_or_opts):
"""设置浏览器启动属性
:param addr_or_opts: 'ip:port'ChromiumOptionsDriver
: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}'

View File

@ -6,13 +6,12 @@
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from pathlib import Path from pathlib import Path
from threading import Lock
from typing import Union, Tuple, List, Optional from typing import Union, Tuple, List, Optional
from .._base.browser import Browser from .._base.browser import Chromium
from .._configs.chromium_options import ChromiumOptions from .._configs.chromium_options import ChromiumOptions
from .._pages.chromium_base import ChromiumBase from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_tab import ChromiumTab from .._pages.tabs import ChromiumTab
from .._units.rect import TabRect from .._units.rect import TabRect
from .._units.setter import ChromiumPageSetter from .._units.setter import ChromiumPageSetter
from .._units.waiter import PageWaiter from .._units.waiter import PageWaiter
@ -20,6 +19,10 @@ from .._units.waiter import PageWaiter
class ChromiumPage(ChromiumBase): class ChromiumPage(ChromiumBase):
_PAGES: dict = ... _PAGES: dict = ...
tab: ChromiumPage = ...
_browser: Chromium = ...
_rect: Optional[TabRect] = ...
_is_exist: bool = ...
def __new__(cls, def __new__(cls,
addr_or_opts: Union[str, int, ChromiumOptions] = None, addr_or_opts: Union[str, int, ChromiumOptions] = None,
@ -29,15 +32,7 @@ class ChromiumPage(ChromiumBase):
def __init__(self, def __init__(self,
addr_or_opts: Union[str, int, ChromiumOptions] = None, addr_or_opts: Union[str, int, ChromiumOptions] = None,
tab_id: str = None, tab_id: str = None,
timeout: float = 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 = ...
def _handle_options(self, addr_or_opts: Union[str, ChromiumOptions]) -> str: ... def _handle_options(self, addr_or_opts: Union[str, ChromiumOptions]) -> str: ...
@ -46,7 +41,7 @@ class ChromiumPage(ChromiumBase):
def _page_init(self) -> None: ... def _page_init(self) -> None: ...
@property @property
def browser(self) -> Browser: ... def browser(self) -> Chromium: ...
@property @property
def tabs_count(self) -> int: ... def tabs_count(self) -> int: ...
@ -66,6 +61,9 @@ class ChromiumPage(ChromiumBase):
@property @property
def browser_version(self) -> str: ... def browser_version(self) -> str: ...
@property
def address(self) -> str: ...
@property @property
def set(self) -> ChromiumPageSetter: ... 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, def new_tab(self, url: str = None, new_window: bool = False, background: bool = False,
new_context: bool = False) -> ChromiumTab: ... new_context: bool = False) -> ChromiumTab: ...
def activate_tab(self, id_ind_tab: Union[int, str, ChromiumTab]) -> None: ...
def close(self) -> None: ... def close(self) -> None: ...
def close_tabs(self, tabs_or_ids: Union[str, ChromiumTab, List[Union[str, ChromiumTab]], 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 quit(self, timeout: float = 5, force: bool = True) -> None: ...
def _on_disconnect(self) -> 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: ...

View File

@ -6,15 +6,14 @@
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from .chromium_page import ChromiumPage from .chromium_page import ChromiumPage
from .chromium_tab import WebPageTab
from .session_page import SessionPage from .session_page import SessionPage
from .._base.base import BasePage from .._base.base import BasePage
from .._configs.chromium_options import ChromiumOptions from .._configs.chromium_options import ChromiumOptions
from .._functions.web import set_session_cookies, set_browser_cookies from .._functions.cookies import set_session_cookies, set_tab_cookies
from .._units.setter import WebPageSetter from .._units.setter import MixPageSetter
class WebPage(SessionPage, ChromiumPage, BasePage): class MixPage(SessionPage, ChromiumPage, BasePage):
"""整合浏览器和request的页面类""" """整合浏览器和request的页面类"""
def __new__(cls, mode='d', timeout=None, chromium_options=None, session_or_options=None): 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) 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 mode: 'd' 's'即driver模式和session模式
:param timeout: 超时时间d模式时为寻找元素时间s模式时为连接时间默认10秒 :param timeout: 超时时间d模式时为寻找元素时间s模式时为连接时间默认10秒
@ -47,7 +46,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
chromium_options = ChromiumOptions(read_file=chromium_options) chromium_options = ChromiumOptions(read_file=chromium_options)
chromium_options.set_timeouts(base=self._timeout).set_paths(download_path=self.download_path) 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) 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) self.change_mode(self._mode, go=False, copy_cookies=False)
def __call__(self, locator, index=1, timeout=None): def __call__(self, locator, index=1, timeout=None):
@ -67,7 +66,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
def set(self): def set(self):
"""返回用于设置的对象""" """返回用于设置的对象"""
if self._set is None: if self._set is None:
self._set = WebPageSetter(self) self._set = MixPageSetter(self)
return self._set return self._set
@property @property
@ -148,15 +147,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
@property @property
def timeout(self): def timeout(self):
"""返回通用timeout设置""" """返回通用timeout设置"""
return self.timeouts.base return self._timeout if self._mode == 's' else self.timeouts.base
@timeout.setter
def timeout(self, second):
"""设置通用超时时间
:param second: 秒数
:return: None
"""
self.set.timeouts(base=second)
def get(self, url, show_errmsg=False, retry=None, interval=None, timeout=None, **kwargs): def get(self, url, show_errmsg=False, retry=None, interval=None, timeout=None, **kwargs):
"""跳转到一个url """跳转到一个url
@ -284,7 +275,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
return return
if copy_user_agent: 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}) self._headers.update({"User-Agent": user_agent})
set_session_cookies(self.session, super(SessionPage, self).cookies()) set_session_cookies(self.session, super(SessionPage, self).cookies())
@ -293,19 +284,18 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
"""把session对象的cookies复制到浏览器""" """把session对象的cookies复制到浏览器"""
if not self._has_driver: if not self._has_driver:
return 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 """返回cookies
:param as_dict: 为True时以dict格式返回为False时返回list且all_info无效
:param all_domains: 是否返回所有域的cookies :param all_domains: 是否返回所有域的cookies
:param all_info: 是否返回所有信息False则只返回namevaluedomain :param all_info: 是否返回所有信息False则只返回namevaluedomain
:return: cookies信息 :return: cookies信息
""" """
if self._mode == 's': if self._mode == 's':
return super().cookies(as_dict, all_domains, all_info) return super().cookies(all_domains, all_info)
elif self._mode == 'd': 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): def get_tab(self, id_or_num=None, title=None, url=None, tab_type='page', as_id=False):
"""获取一个标签页对象id_or_num不为None时后面几个参数无效 """获取一个标签页对象id_or_num不为None时后面几个参数无效
@ -314,31 +304,10 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
:param url: 要匹配url的文本模糊匹配为None则匹配所有 :param url: 要匹配url的文本模糊匹配为None则匹配所有
:param tab_type: tab类型可用列表输入多个 'page', 'iframe' 为None则匹配所有 :param tab_type: tab类型可用列表输入多个 'page', 'iframe' 为None则匹配所有
:param as_id: 是否返回标签页id而不是标签页对象 :param as_id: 是否返回标签页id而不是标签页对象
:return: WebPageTab对象 :return: MixTab对象
""" """
if id_or_num is not None: return self.browser._get_tab(id_or_num=id_or_num, title=title, url=url,
if isinstance(id_or_num, str): tab_type=tab_type, mix=True, as_id=as_id)
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)
def get_tabs(self, title=None, url=None, tab_type='page', as_id=False): def get_tabs(self, title=None, url=None, tab_type='page', as_id=False):
"""查找符合条件的tab返回它们组成的列表 """查找符合条件的tab返回它们组成的列表
@ -348,10 +317,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
:param as_id: 是否返回标签页id而不是标签页对象 :param as_id: 是否返回标签页id而不是标签页对象
:return: ChromiumTab对象组成的列表 :return: ChromiumTab对象组成的列表
""" """
if as_id: return self.browser._get_tabs(title=title, url=url, tab_type=tab_type, mix=True, as_id=as_id)
return [tab['id'] for tab in self._browser.find_tabs(title, url, tab_type)]
with self._lock:
return [WebPageTab(self, tab['id']) for tab in self._browser.find_tabs(title, url, tab_type)]
def new_tab(self, url=None, new_window=False, background=False, new_context=False): 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: 是否创建新的上下文 :param new_context: 是否创建新的上下文
:return: 新标签页对象 :return: 新标签页对象
""" """
tab = WebPageTab(self, tab_id=self.browser.new_tab(new_window, background, new_context)) return self.browser.new_mix_tab(url=url, new_window=new_window, background=background, new_context=new_context)
if url:
tab.get(url)
return tab
def close_driver(self): def close_driver(self):
"""关闭driver及浏览器""" """关闭driver及浏览器"""
@ -403,7 +366,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
:param locator: 元素的定位信息可以是元素对象loc元组或查询字符串 :param locator: 元素的定位信息可以是元素对象loc元组或查询字符串
:param timeout: 查找元素超时时间d模式专用 :param timeout: 查找元素超时时间d模式专用
:param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有 :param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有
:param relative: WebPage用的表示是否相对定位的参数 :param relative: MixTab用的表示是否相对定位的参数
:param raise_err: 找不到元素是是否抛出异常为None时根据全局设置 :param raise_err: 找不到元素是是否抛出异常为None时根据全局设置
:return: 元素对象或属性文本节点文本 :return: 元素对象或属性文本节点文本
""" """
@ -429,4 +392,4 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
self._has_driver = None self._has_driver = None
def __repr__(self): def __repr__(self):
return f'<WebPage browser_id={self.browser.id} tab_id={self.tab_id}>' return f'<MixPage browser_id={self.browser.id} tab_id={self.tab_id}>'

View File

@ -11,7 +11,7 @@ from requests import Session, Response
from .chromium_frame import ChromiumFrame from .chromium_frame import ChromiumFrame
from .chromium_page import ChromiumPage from .chromium_page import ChromiumPage
from .chromium_tab import WebPageTab from .tabs import MixTab
from .session_page import SessionPage from .session_page import SessionPage
from .._base.base import BasePage from .._base.base import BasePage
from .._base.driver import Driver from .._base.driver import Driver
@ -20,10 +20,10 @@ from .._configs.session_options import SessionOptions
from .._elements.chromium_element import ChromiumElement from .._elements.chromium_element import ChromiumElement
from .._elements.session_element import SessionElement from .._elements.session_element import SessionElement
from .._functions.elements import SessionElementsList, ChromiumElementsList 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, def __init__(self,
mode: str = 'd', mode: str = 'd',
@ -31,7 +31,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
chromium_options: Union[ChromiumOptions, bool] = None, chromium_options: Union[ChromiumOptions, bool] = None,
session_or_options: Union[Session, SessionOptions, bool] = None) -> None: session_or_options: Union[Session, SessionOptions, bool] = None) -> None:
self._mode: str = ... self._mode: str = ...
self._set: WebPageSetter = ... self._set: MixPageSetter = ...
self._has_driver: bool = ... self._has_driver: bool = ...
self._has_session: bool = ... self._has_session: bool = ...
self._session_options: Union[SessionOptions, None] = ... self._session_options: Union[SessionOptions, None] = ...
@ -79,9 +79,6 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
@property @property
def timeout(self) -> float: ... def timeout(self) -> float: ...
@timeout.setter
def timeout(self, second: float) -> None: ...
def get(self, def get(self,
url: str, url: str,
show_errmsg: bool = False, show_errmsg: bool = False,
@ -124,28 +121,27 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
def cookies_to_browser(self) -> None: ... def cookies_to_browser(self) -> None: ...
def cookies(self, def cookies(self,
as_dict: bool = False,
all_domains: bool = False, all_domains: bool = False,
all_info: bool = False) -> Union[dict, list]: ... all_info: bool = False) -> Union[dict, list]: ...
def get_tab(self, def get_tab(self,
id_or_num: Union[str, WebPageTab, int] = None, id_or_num: Union[str, MixTab, int] = None,
title: str = None, title: str = None,
url: str = None, url: str = None,
tab_type: Union[str, list, tuple] = 'page', 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, def get_tabs(self,
title: str = None, title: str = None,
url: str = None, url: str = None,
tab_type: Union[str, list, tuple] = 'page', 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, def new_tab(self,
url: str = None, url: str = None,
new_window: bool = False, new_window: bool = False,
background: bool = False, background: bool = False,
new_context: bool = False) -> WebPageTab: ... new_context: bool = False) -> MixTab: ...
def close_driver(self) -> None: ... def close_driver(self) -> None: ...
@ -175,10 +171,10 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
cert: Any | None = ...) -> Union[bool, Response]: ... cert: Any | None = ...) -> Union[bool, Response]: ...
@property @property
def latest_tab(self) -> Union[WebPageTab, WebPage]: ... def latest_tab(self) -> Union[MixTab, MixPage]: ...
@property @property
def set(self) -> WebPageSetter: ... def set(self) -> MixPageSetter: ...
def _find_elements(self, def _find_elements(self,
locator: Union[Tuple[str, str], str, ChromiumElement, SessionElement, ChromiumFrame], locator: Union[Tuple[str, str], str, ChromiumElement, SessionElement, ChromiumFrame],

View File

@ -18,17 +18,17 @@ from tldextract import extract
from .._base.base import BasePage from .._base.base import BasePage
from .._configs.session_options import SessionOptions from .._configs.session_options import SessionOptions
from .._elements.session_element import SessionElement, make_session_ele 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 from .._units.setter import SessionPageSetter
class SessionPage(BasePage): class SessionPage(BasePage):
"""SessionPage封装了页面操作的常用功能使用requests来获取、解析网页""" """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 session_or_options: Session对象或SessionOptions对象
:param timeout: 连接超时时间为None时从ini文件读取或默认10
""" """
super(SessionPage, SessionPage).__init__(self) super(SessionPage, SessionPage).__init__(self)
self._headers = None self._headers = None
@ -38,11 +38,10 @@ class SessionPage(BasePage):
self._encoding = None self._encoding = None
self._type = 'SessionPage' self._type = 'SessionPage'
self._page = self self._page = self
self._timeout = 10
self._s_set_start_options(session_or_options) self._s_set_start_options(session_or_options)
self._s_set_runtime_settings() self._s_set_runtime_settings()
self._create_session() self._create_session()
if timeout is not None:
self.timeout = timeout
def _s_set_start_options(self, session_or_options): def _s_set_start_options(self, session_or_options):
"""启动配置 """启动配置
@ -64,8 +63,7 @@ class SessionPage(BasePage):
def _s_set_runtime_settings(self): def _s_set_runtime_settings(self):
"""设置运行时用到的属性""" """设置运行时用到的属性"""
self._timeout = self._session_options.timeout self._timeout = self._session_options.timeout
self._download_path = None if self._session_options.download_path is None \ self._download_path = str(Path(self._session_options.download_path or '.').absolute())
else str(Path(self._session_options.download_path).absolute())
self.retry_times = self._session_options.retry_times self.retry_times = self._session_options.retry_times
self.retry_interval = self._session_options.retry_interval self.retry_interval = self._session_options.retry_interval
@ -146,6 +144,11 @@ class SessionPage(BasePage):
self._set = SessionPageSetter(self) self._set = SessionPageSetter(self)
return self._set return self._set
@property
def timeout(self):
"""返回超时设置"""
return self._timeout
def get(self, url, show_errmsg=False, retry=None, interval=None, timeout=None, **kwargs): def get(self, url, show_errmsg=False, retry=None, interval=None, timeout=None, **kwargs):
"""用get方式跳转到url可输入文件路径 """用get方式跳转到url可输入文件路径
:param url: 目标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) 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 """返回cookies
:param as_dict: 为True时以dict格式返回为False时返回list且all_info无效
:param all_domains: 是否返回所有域的cookies :param all_domains: 是否返回所有域的cookies
:param all_info: 是否返回所有信息False则只返回namevaluedomain :param all_info: 是否返回所有信息False则只返回namevaluedomain
:return: cookies信息 :return: cookies信息
@ -233,21 +235,20 @@ class SessionPage(BasePage):
if self.url: if self.url:
ex_url = extract(self._session_url) ex_url = extract(self._session_url)
domain = f'{ex_url.domain}.{ex_url.suffix}' if ex_url.suffix else ex_url.domain domain = f'{ex_url.domain}.{ex_url.suffix}' if ex_url.suffix else ex_url.domain
cookies = tuple(c for c in self.session.cookies if domain in c.domain or c.domain == '')
cookies = tuple(x for x in self.session.cookies if domain in x.domain or x.domain == '')
else: else:
cookies = tuple(x for x in self.session.cookies) cookies = tuple(c for c in self.session.cookies)
if as_dict: if all_info:
return {x.name: x.value for x in cookies} r = CookiesList()
elif all_info: for c in cookies:
return [cookie_to_dict(cookie) for cookie in cookies] r.append(cookie_to_dict(c))
else: else:
r = [] r = CookiesList()
for c in cookies: for c in cookies:
c = cookie_to_dict(c) c = cookie_to_dict(c)
r.append({'name': c['name'], 'value': c['value'], 'domain': c['domain']}) r.append({'name': c['name'], 'value': c['value'], 'domain': c['domain']})
return r return r
def close(self): def close(self):
"""关闭Session对象""" """关闭Session对象"""

View File

@ -14,21 +14,21 @@ from requests.structures import CaseInsensitiveDict
from .._base.base import BasePage from .._base.base import BasePage
from .._configs.session_options import SessionOptions from .._configs.session_options import SessionOptions
from .._elements.session_element import SessionElement from .._elements.session_element import SessionElement
from .._functions.cookies import CookiesList
from .._functions.elements import SessionElementsList from .._functions.elements import SessionElementsList
from .._units.setter import SessionPageSetter from .._units.setter import SessionPageSetter
class SessionPage(BasePage): class SessionPage(BasePage):
def __init__(self, def __init__(self,
session_or_options: Union[Session, SessionOptions] = None, session_or_options: Union[Session, SessionOptions] = None):
timeout: float = None):
self._headers: Optional[CaseInsensitiveDict] = ... self._headers: Optional[CaseInsensitiveDict] = ...
self._session: Session = ... self._session: Session = ...
self._session_options: SessionOptions = ... self._session_options: SessionOptions = ...
self._url: str = ... self._url: str = ...
self._response: Response = ... self._response: Response = ...
self._url_available: bool = ... self._url_available: bool = ...
self.timeout: float = ... self._timeout: float = ...
self.retry_times: int = ... self.retry_times: int = ...
self.retry_interval: float = ... self.retry_interval: float = ...
self._set: SessionPageSetter = ... self._set: SessionPageSetter = ...
@ -114,9 +114,8 @@ class SessionPage(BasePage):
raise_err: bool = None) -> Union[SessionElement, SessionElementsList]: ... raise_err: bool = None) -> Union[SessionElement, SessionElementsList]: ...
def cookies(self, def cookies(self,
as_dict: bool = False,
all_domains: bool = False, all_domains: bool = False,
all_info: bool = False) -> Union[dict, list]: ... all_info: bool = False) -> CookiesList: ...
# ----------------session独有属性和方法----------------------- # ----------------session独有属性和方法-----------------------
@property @property
@ -131,6 +130,9 @@ class SessionPage(BasePage):
@property @property
def set(self) -> SessionPageSetter: ... def set(self) -> SessionPageSetter: ...
@property
def timeout(self) -> float: ...
def post(self, def post(self,
url: str, url: str,
show_errmsg: bool = False, show_errmsg: bool = False,

View File

@ -10,11 +10,12 @@ from time import sleep
from .._base.base import BasePage from .._base.base import BasePage
from .._configs.session_options import SessionOptions from .._configs.session_options import SessionOptions
from .._functions.cookies import set_session_cookies, set_tab_cookies
from .._functions.settings import Settings 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.chromium_base import ChromiumBase
from .._pages.session_page import SessionPage from .._pages.session_page import SessionPage
from .._units.setter import TabSetter, WebPageTabSetter from .._units.setter import TabSetter, MixTabSetter
from .._units.waiter import TabWaiter from .._units.waiter import TabWaiter
@ -22,10 +23,10 @@ class ChromiumTab(ChromiumBase):
"""实现浏览器标签页的类""" """实现浏览器标签页的类"""
_TABS = {} _TABS = {}
def __new__(cls, page, tab_id): def __new__(cls, browser, tab_id):
""" """
:param page: ChromiumPage对象 :param browser: Browser对象
:param tab_id: 要控制的标签页id :param tab_id: 标签页id
""" """
if Settings.singleton_tab_obj and tab_id in cls._TABS: if Settings.singleton_tab_obj and tab_id in cls._TABS:
r = cls._TABS[tab_id] r = cls._TABS[tab_id]
@ -36,38 +37,30 @@ class ChromiumTab(ChromiumBase):
cls._TABS[tab_id] = r cls._TABS[tab_id] = r
return r return r
def __init__(self, page, tab_id): def __init__(self, browser, tab_id):
""" """
:param page: ChromiumPage对象 :param browser: Browser对象
:param tab_id: 要控制的标签页id :param tab_id: 标签页id
""" """
if Settings.singleton_tab_obj and hasattr(self, '_created'): if Settings.singleton_tab_obj and hasattr(self, '_created'):
return return
self._created = True self._created = True
self._page = page super().__init__(browser, tab_id)
self.tab = self self._tab = self
self._browser = page.browser
super().__init__(page.address, tab_id, page.timeout)
self._rect = None
self._type = 'ChromiumTab' self._type = 'ChromiumTab'
def _d_set_runtime_settings(self): def _d_set_runtime_settings(self):
"""重写设置浏览器运行参数方法""" """重写设置浏览器运行参数方法"""
self._timeouts = copy(self.page.timeouts) self._timeouts = copy(self.browser.timeouts)
self.retry_times = self.page.retry_times self.retry_times = self.browser.retry_times
self.retry_interval = self.page.retry_interval self.retry_interval = self.browser.retry_interval
self._load_mode = self.page._load_mode self._load_mode = self.browser._load_mode
self._download_path = self.page.download_path self._download_path = self.browser.download_path
def close(self): def close(self):
"""关闭当前标签页""" """关闭当前标签页"""
self.page.close_tabs(self.tab_id) self.browser.close_tabs(self.tab_id)
@property
def page(self):
"""返回总体page对象"""
return self._page
@property @property
def set(self): def set(self):
@ -100,11 +93,11 @@ class ChromiumTab(ChromiumBase):
ChromiumTab._TABS.pop(self.tab_id, None) ChromiumTab._TABS.pop(self.tab_id, None)
class WebPageTab(SessionPage, ChromiumTab, BasePage): class MixTab(SessionPage, ChromiumTab, BasePage):
def __init__(self, page, tab_id): def __init__(self, browser, tab_id):
""" """
:param page: WebPage对象 :param browser: Chromium对象
:param tab_id: 要控制的标签页id :param tab_id: 标签页id
""" """
if Settings.singleton_tab_obj and hasattr(self, '_created'): if Settings.singleton_tab_obj and hasattr(self, '_created'):
return return
@ -112,10 +105,9 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
self._mode = 'd' self._mode = 'd'
self._has_driver = True self._has_driver = True
self._has_session = True self._has_session = True
super().__init__(session_or_options=SessionOptions(read_file=False).from_session(copy(page.session), super().__init__(session_or_options=browser._session_options if browser._session_options else SessionOptions())
page._headers)) super(SessionPage, self).__init__(browser=browser, tab_id=tab_id)
super(SessionPage, self).__init__(page=page, tab_id=tab_id) self._type = 'MixTab'
self._type = 'WebPageTab'
def __call__(self, locator, index=1, timeout=None): def __call__(self, locator, index=1, timeout=None):
"""在内部查找元素 """在内部查找元素
@ -134,7 +126,7 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
def set(self): def set(self):
"""返回用于设置的对象""" """返回用于设置的对象"""
if self._set is None: if self._set is None:
self._set = WebPageTabSetter(self) self._set = MixTabSetter(self)
return self._set return self._set
@property @property
@ -215,15 +207,7 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
@property @property
def timeout(self): def timeout(self):
"""返回通用timeout设置""" """返回通用timeout设置"""
return self.timeouts.base return self._timeout if self._mode == 's' else self.timeouts.base
@timeout.setter
def timeout(self, second):
"""设置通用超时时间
:param second: 秒数
:return: None
"""
self.set.timeouts(base=second)
def get(self, url, show_errmsg=False, retry=None, interval=None, timeout=None, **kwargs): def get(self, url, show_errmsg=False, retry=None, interval=None, timeout=None, **kwargs):
"""跳转到一个url """跳转到一个url
@ -318,7 +302,9 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
# s模式转d模式 # s模式转d模式
if self._mode == 'd': if self._mode == 'd':
if self._driver is None: 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._url = None if not self._has_driver else super(SessionPage, self).url
self._has_driver = True self._has_driver = True
@ -353,7 +339,7 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
return return
if copy_user_agent: 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}) self._headers.update({"User-Agent": user_agent})
set_session_cookies(self.session, super(SessionPage, self).cookies()) set_session_cookies(self.session, super(SessionPage, self).cookies())
@ -362,23 +348,22 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
"""把session对象的cookies复制到浏览器""" """把session对象的cookies复制到浏览器"""
if not self._has_driver: if not self._has_driver:
return 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 """返回cookies
:param as_dict: 为True时以dict格式返回为False时返回list且all_info无效
:param all_domains: 是否返回所有域的cookies :param all_domains: 是否返回所有域的cookies
:param all_info: 是否返回所有信息False则只返回namevaluedomain :param all_info: 是否返回所有信息False则只返回namevaluedomain
:return: cookies信息 :return: cookies信息
""" """
if self._mode == 's': if self._mode == 's':
return super().cookies(as_dict, all_domains, all_info) return super().cookies(all_domains, all_info)
elif self._mode == 'd': 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): def close(self):
"""关闭当前标签页""" """关闭当前标签页"""
self.page.close_tabs(self.tab_id) self.browser.close_tabs(self.tab_id)
self._session.close() self._session.close()
if self._response is not None: if self._response is not None:
self._response.close() self._response.close()
@ -388,7 +373,7 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
:param locator: 元素的定位信息可以是元素对象loc元组或查询字符串 :param locator: 元素的定位信息可以是元素对象loc元组或查询字符串
:param timeout: 查找元素超时时间d模式专用 :param timeout: 查找元素超时时间d模式专用
:param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有 :param index: 第几个结果从1开始可传入负数获取倒数第几个为None返回所有
:param relative: WebPage用的表示是否相对定位的参数 :param relative: MixTab用的表示是否相对定位的参数
:param raise_err: 找不到元素是是否抛出异常为None时根据全局设置 :param raise_err: 找不到元素是是否抛出异常为None时根据全局设置
:return: 元素对象或属性文本节点文本 :return: 元素对象或属性文本节点文本
""" """
@ -398,4 +383,4 @@ class WebPageTab(SessionPage, ChromiumTab, BasePage):
return super(SessionPage, self)._find_elements(locator, timeout=timeout, index=index, relative=relative) return super(SessionPage, self)._find_elements(locator, timeout=timeout, index=index, relative=relative)
def __repr__(self): def __repr__(self):
return f'<WebPageTab browser_id={self.browser.id} tab_id={self.tab_id}>' return f'<MixTab browser_id={self.browser.id} tab_id={self.tab_id}>'

View File

@ -12,35 +12,30 @@ from requests import Session, Response
from .chromium_base import ChromiumBase from .chromium_base import ChromiumBase
from .chromium_frame import ChromiumFrame from .chromium_frame import ChromiumFrame
from .chromium_page import ChromiumPage
from .session_page import SessionPage from .session_page import SessionPage
from .web_page import WebPage from .._base.browser import Chromium
from .._base.browser import Browser
from .._elements.chromium_element import ChromiumElement from .._elements.chromium_element import ChromiumElement
from .._elements.session_element import SessionElement from .._elements.session_element import SessionElement
from .._functions.cookies import CookiesList
from .._functions.elements import SessionElementsList, ChromiumElementsList from .._functions.elements import SessionElementsList, ChromiumElementsList
from .._units.rect import TabRect from .._units.rect import TabRect
from .._units.setter import TabSetter, WebPageTabSetter from .._units.setter import TabSetter, MixTabSetter
from .._units.waiter import TabWaiter from .._units.waiter import TabWaiter
class ChromiumTab(ChromiumBase): class ChromiumTab(ChromiumBase):
_TABS: dict = ... _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): def __init__(self, browser: Chromium, tab_id: str):
self._page: ChromiumPage = ... self._tab: ChromiumTab = ...
self._browser: Browser = ...
self._rect: Optional[TabRect] = ... self._rect: Optional[TabRect] = ...
def _d_set_runtime_settings(self) -> None: ... def _d_set_runtime_settings(self) -> None: ...
def close(self) -> None: ... def close(self) -> None: ...
@property
def page(self) -> ChromiumPage: ...
@property @property
def set(self) -> TabSetter: ... def set(self) -> TabSetter: ...
@ -69,22 +64,19 @@ class ChromiumTab(ChromiumBase):
generateDocumentOutline: bool = ...) -> Union[bytes, str]: ... generateDocumentOutline: bool = ...) -> Union[bytes, str]: ...
class WebPageTab(SessionPage, ChromiumTab): class MixTab(SessionPage, ChromiumTab):
def __init__(self, page: WebPage, tab_id: str): _tab: MixTab = ...
self._page: WebPage = ... _mode: str = ...
self._browser: Browser = ... _has_driver: bool = ...
self._mode: str = ... _has_session: bool = ...
self._has_driver = ...
self._has_session = ... def __init__(self, browser: Chromium, tab_id: str): ...
def __call__(self, def __call__(self,
locator: Union[Tuple[str, str], str, ChromiumElement, SessionElement], locator: Union[Tuple[str, str], str, ChromiumElement, SessionElement],
index: int = 1, index: int = 1,
timeout: float = None) -> Union[ChromiumElement, SessionElement]: ... timeout: float = None) -> Union[ChromiumElement, SessionElement]: ...
@property
def page(self) -> WebPage: ...
@property @property
def url(self) -> Union[str, None]: ... def url(self) -> Union[str, None]: ...
@ -121,9 +113,6 @@ class WebPageTab(SessionPage, ChromiumTab):
@property @property
def timeout(self) -> float: ... def timeout(self) -> float: ...
@timeout.setter
def timeout(self, second: float) -> None: ...
def get(self, def get(self,
url: str, url: str,
show_errmsg: bool = False, show_errmsg: bool = False,
@ -165,8 +154,7 @@ class WebPageTab(SessionPage, ChromiumTab):
def cookies_to_browser(self) -> None: ... def cookies_to_browser(self) -> None: ...
def cookies(self, as_dict: bool = False, all_domains: bool = False, def cookies(self, all_domains: bool = False, all_info: bool = False) -> CookiesList: ...
all_info: bool = False) -> Union[dict, list]: ...
def close(self) -> None: ... def close(self) -> None: ...
@ -192,7 +180,7 @@ class WebPageTab(SessionPage, ChromiumTab):
cert: Any | None = ...) -> Union[bool, Response]: ... cert: Any | None = ...) -> Union[bool, Response]: ...
@property @property
def set(self) -> WebPageTabSetter: ... def set(self) -> MixTabSetter: ...
def _find_elements(self, def _find_elements(self,
locator: Union[Tuple[str, str], str, ChromiumElement, SessionElement, ChromiumFrame], locator: Union[Tuple[str, str], str, ChromiumElement, SessionElement, ChromiumFrame],

View File

@ -7,8 +7,7 @@
""" """
from time import sleep, perf_counter from time import sleep, perf_counter
from ..errors import AlertExistsError from .._functions.keys import modifierBit, make_input_data, input_text_or_keys, Keys
from .._functions.keys import modifierBit, keyDescriptionForString, input_text_or_keys, Keys, keyDefinitions
from .._functions.web import location_in_viewport from .._functions.web import location_in_viewport
@ -26,7 +25,7 @@ class Actions:
self.curr_y = 0 self.curr_y = 0
self._holding = 'left' 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)形式 :param ele_or_loc: 元素对象绝对坐标或文本定位符坐标为tuple(int, int)形式
@ -36,6 +35,11 @@ class Actions:
:return: self :return: self
""" """
is_loc = False 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)): if isinstance(ele_or_loc, (tuple, list)):
is_loc = True is_loc = True
lx = ele_or_loc[0] + offset_x 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': elif isinstance(ele_or_loc, str) or ele_or_loc._type == 'ChromiumElement':
ele_or_loc = self.owner(ele_or_loc) ele_or_loc = self.owner(ele_or_loc)
self.owner.scroll.to_see(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 lx = x + offset_x
ly = y + offset_y ly = y + offset_y
else: else:
@ -51,16 +55,15 @@ class Actions:
if not location_in_viewport(self.owner, lx, ly): if not location_in_viewport(self.owner, lx, ly):
# 把坐标滚动到页面中间 # 把坐标滚动到页面中间
clientWidth = self.owner.run_js('return document.body.clientWidth;') clientWidth = self.owner._run_js('return document.body.clientWidth;')
clientHeight = self.owner.run_js('return document.body.clientHeight;') clientHeight = self.owner._run_js('return document.body.clientHeight;')
self.owner.scroll.to_location(lx - clientWidth // 2, ly - clientHeight // 2) self.owner.scroll.to_location(lx - clientWidth // 2, ly - clientHeight // 2)
# 这样设计为了应付那些不随滚动条滚动的元素 # 这样设计为了应付那些不随滚动条滚动的元素
if is_loc: if is_loc:
cx, cy = location_to_client(self.owner, lx, ly) cx, cy = location_to_client(self.owner, lx, ly)
else: else:
x, y = ele_or_loc.rect.viewport_location if offset_x or offset_y \ x, y = ele_or_loc.rect.viewport_midpoint if mid_point else ele_or_loc.rect.viewport_location
else ele_or_loc.rect.viewport_midpoint
cx = x + offset_x cx = x + offset_x
cy = y + offset_y cy = y + offset_y
@ -95,36 +98,31 @@ class Actions:
return self return self
def click(self, on_ele=None): def click(self, on_ele=None, times=1):
"""点击鼠标左键,可先移动到元素上 """点击鼠标左键,可先移动到元素上
:param on_ele: ChromiumElement元素或文本定位符 :param on_ele: ChromiumElement元素或文本定位符
:param times: 点击次数
:return: self :return: self
""" """
self._hold(on_ele, 'left').wait(.05)._release('left') self._hold(on_ele, 'left', times).wait(.05)._release('left')
return self return self
def r_click(self, on_ele=None): def r_click(self, on_ele=None, times=1):
"""点击鼠标右键,可先移动到元素上 """点击鼠标右键,可先移动到元素上
:param on_ele: ChromiumElement元素或文本定位符 :param on_ele: ChromiumElement元素或文本定位符
:param times: 点击次数
:return: self :return: self
""" """
self._hold(on_ele, 'right').wait(.05)._release('right') self._hold(on_ele, 'right', times).wait(.05)._release('right')
return self return self
def m_click(self, on_ele=None): def m_click(self, on_ele=None, times=1):
"""点击鼠标中键,可先移动到元素上 """点击鼠标中键,可先移动到元素上
:param on_ele: ChromiumElement元素或文本定位符 :param on_ele: ChromiumElement元素或文本定位符
:param times: 点击次数
:return: self :return: self
""" """
self._hold(on_ele, 'middle').wait(.05)._release('middle') self._hold(on_ele, 'middle', times).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')
return self return self
def hold(self, on_ele=None): def hold(self, on_ele=None):
@ -256,9 +254,10 @@ class Actions:
self.modifier |= modifierBit.get(key, 0) self.modifier |= modifierBit.get(key, 0)
return self return self
data = self._get_key_data(key, 'keyDown') data = make_input_data(self.modifier, key, False)
data['_ignore'] = AlertExistsError if not data:
self.owner.run_cdp('Input.dispatchKeyEvent', **data) raise ValueError(f'没有这个按键:{key}')
self.owner._run_cdp('Input.dispatchKeyEvent', **data)
return self return self
def key_up(self, key): def key_up(self, key):
@ -271,9 +270,10 @@ class Actions:
self.modifier ^= modifierBit.get(key, 0) self.modifier ^= modifierBit.get(key, 0)
return self return self
data = self._get_key_data(key, 'keyUp') data = make_input_data(self.modifier, key, True)
data['_ignore'] = AlertExistsError if not data:
self.owner.run_cdp('Input.dispatchKeyEvent', **data) raise ValueError(f'没有这个按键:{key}')
self.owner._run_cdp('Input.dispatchKeyEvent', **data)
return self return self
def type(self, keys): def type(self, keys):
@ -282,17 +282,22 @@ class Actions:
:return: self :return: self
""" """
modifiers = [] modifiers = []
if not isinstance(keys, (str, tuple, list)):
keys = str(keys)
for i in keys: for i in keys:
for character in i: for character in i:
if character in keyDefinitions: if character in ('\ue009', '\ue008', '\ue00a', '\ue03d'):
self.key_down(character) self.modifier |= modifierBit.get(character, 0)
if character in ('\ue009', '\ue008', '\ue00a', '\ue03d'): modifiers.append(character)
modifiers.append(character) data = make_input_data(self.modifier, character, False)
else: if data:
self.key_up(character) 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: 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: for m in modifiers:
self.key_up(m) self.key_up(m)
@ -315,30 +320,9 @@ class Actions:
self.owner.wait(second=second, scope=scope) self.owner.wait(second=second, scope=scope)
return self 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): def location_to_client(page, lx, ly):
"""绝对坐标转换为视口坐标""" """绝对坐标转换为视口坐标"""
scroll_x = page.run_js('return document.documentElement.scrollLeft;') scroll_x = page._run_js('return document.documentElement.scrollLeft;')
scroll_y = page.run_js('return document.documentElement.scrollTop;') scroll_y = page._run_js('return document.documentElement.scrollTop;')
return lx - scroll_x, ly - scroll_y return lx - scroll_x, ly - scroll_y

View File

@ -11,19 +11,19 @@ from .._base.driver import Driver
from .._elements.chromium_element import ChromiumElement from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_base import ChromiumBase from .._pages.chromium_base import ChromiumBase
KEYS = Literal['NULL', 'CANCEL', 'HELP', 'BACKSPACE', 'BACK_SPACE', 'meta', KEYS = Literal['NULL', 'CANCEL', 'HELP', 'BACKSPACE', 'meta',
'TAB', 'CLEAR', 'RETURN', 'ENTER', 'SHIFT', 'LEFT_SHIFT', 'CONTROL', 'command ', 'TAB', 'CLEAR', 'RETURN', 'ENTER', 'SHIFT', 'CONTROL', 'command ',
'CTRL', 'LEFT_CONTROL', 'ALT', 'LEFT_ALT', 'PAUSE', 'ESCAPE', 'SPACE', 'CTRL', 'ALT', 'PAUSE', 'ESCAPE', 'SPACE',
'PAGE_UP', 'PAGE_DOWN', 'END', 'HOME', 'LEFT', 'ARROW_LEFT', 'UP', 'PAGE_UP', 'PAGE_DOWN', 'END', 'HOME', 'LEFT', 'UP',
'ARROW_UP', 'RIGHT', 'ARROW_RIGHT', 'DOWN', 'ARROW_DOWN', 'INSERT', 'RIGHT', 'DOWN', 'INSERT',
'DELETE', 'DEL', 'SEMICOLON', 'EQUALS', 'NUMPAD0', 'NUMPAD1', 'NUMPAD2', 'DELETE', 'DEL', 'SEMICOLON', 'EQUALS', 'NUMPAD0', 'NUMPAD1', 'NUMPAD2',
'NUMPAD3', 'NUMPAD4', 'NUMPAD5', 'NUMPAD6', 'NUMPAD7', 'NUMPAD8', 'NUMPAD9', 'NUMPAD3', 'NUMPAD4', 'NUMPAD5', 'NUMPAD6', 'NUMPAD7', 'NUMPAD8', 'NUMPAD9',
'MULTIPLY', 'ADD', 'SUBTRACT', 'DECIMAL', 'DIVIDE', 'F1', 'F2', 'MULTIPLY', 'ADD', 'SUBTRACT', 'DECIMAL', 'DIVIDE', 'F1', 'F2',
'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'META', 'COMMAND ', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'META', 'COMMAND ',
'null', 'cancel', 'help', 'backspace', 'back_space', 'tab', 'clear', 'return', 'enter', 'null', 'cancel', 'help', 'backspace', 'tab', 'clear', 'return', 'enter',
'shift', 'left_shift', 'control', 'ctrl', 'left_control', 'alt', 'left_alt', 'pause', 'shift', 'control', 'ctrl', 'alt', 'pause',
'escape', 'space', 'page_up', 'page_down', 'end', 'home', 'left', 'arrow_left', 'up', 'escape', 'space', 'page_up', 'page_down', 'end', 'home', 'left', 'up',
'arrow_up', 'right', 'arrow_right', 'down', 'arrow_down', 'insert', 'delete', 'del', 'right', 'down', 'insert', 'delete', 'del',
'semicolon', 'equals', 'numpad0', 'numpad1', 'numpad2', 'numpad3', 'numpad4', 'numpad5', 'semicolon', 'equals', 'numpad0', 'numpad1', 'numpad2', 'numpad3', 'numpad4', 'numpad5',
'numpad6', 'numpad7', 'numpad8', 'numpad9', 'multiply', 'add', 'subtract', 'decimal', 'numpad6', 'numpad7', 'numpad8', 'numpad9', 'multiply', 'add', 'subtract', 'decimal',
'divide', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', '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 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 m_click(self, on_ele: Union[ChromiumElement, str] = None, times: int = 1) -> Actions: ...
def db_click(self, on_ele: Union[ChromiumElement, str] = None) -> Actions: ...
def hold(self, on_ele: Union[ChromiumElement, str] = None) -> 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 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: ... def location_to_client(page, lx: int, ly: int) -> tuple: ...

View File

@ -43,7 +43,7 @@ class Clicker(object):
select = self._ele.parent('t:select') select = self._ele.parent('t:select')
if select.select.is_multi: if select.select.is_multi:
self._ele.parent('t:select').select.cancel_by_option(self._ele) self._ele.parent('t:select').select.cancel_by_option(self._ele)
return return self._ele
if not by_js: # 模拟点击 if not by_js: # 模拟点击
can_click = False can_click = False
@ -87,22 +87,25 @@ class Clicker(object):
x = rect[1][0] - (rect[1][0] - rect[0][0]) / 2 x = rect[1][0] - (rect[1][0] - rect[0][0]) / 2
y = rect[0][0] + 3 y = rect[0][0] + 3
try: try:
r = self._ele.owner.run_cdp('DOM.getNodeForLocation', x=int(x), y=int(y), r = self._ele.owner._run_cdp('DOM.getNodeForLocation', x=int(x), y=int(y),
includeUserAgentShadowDOM=True, ignorePointerEventsNone=True) includeUserAgentShadowDOM=True, ignorePointerEventsNone=True)
if r['backendNodeId'] != self._ele._backend_id: if r['backendNodeId'] != self._ele._backend_id:
vx, vy = self._ele.rect.viewport_midpoint vx, vy = self._ele.rect.viewport_midpoint
lx, ly = self._ele.rect._get_page_coord(vx, vy)
else: else:
vx, vy = self._ele.rect.viewport_click_point vx, vy = self._ele.rect.viewport_click_point
lx, ly = self._ele.rect._get_page_coord(vx, vy)
except CDPError: except CDPError:
vx, vy = self._ele.rect.viewport_midpoint vx, vy = self._ele.rect.viewport_midpoint
lx, ly = self._ele.rect._get_page_coord(vx, vy)
self._click(vx, vy) self._click(lx, ly, vx, vy)
return True return self._ele
if by_js is not False: if by_js is not False:
self._ele.run_js('this.click();') self._ele._run_js('this.click();')
return True return self._ele
if Settings.raise_when_click_failed: if Settings.raise_when_click_failed:
raise CanNotClickError raise CanNotClickError
return False return False
@ -110,8 +113,7 @@ class Clicker(object):
def right(self): def right(self):
"""右键单击""" """右键单击"""
self._ele.owner.scroll.to_see(self._ele) self._ele.owner.scroll.to_see(self._ele)
x, y = self._ele.rect.viewport_click_point return self._click(*self._ele.rect.click_point, *self._ele.rect.viewport_click_point, button='right')
self._click(x, y, 'right')
def middle(self, get_tab=True): def middle(self, get_tab=True):
"""中键单击默认返回新出现的tab对象 """中键单击默认返回新出现的tab对象
@ -119,13 +121,14 @@ class Clicker(object):
:return: Tab对象或None :return: Tab对象或None
""" """
self._ele.owner.scroll.to_see(self._ele) self._ele.owner.scroll.to_see(self._ele)
x, y = self._ele.rect.viewport_click_point curr_tid = self._ele.tab.browser.tab_ids[0]
self._click(x, y, 'middle') self._click(*self._ele.rect.click_point, *self._ele.rect.viewport_click_point, button='middle')
if get_tab: 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: if not tid:
raise RuntimeError('没有出现新标签页。') 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): def at(self, offset_x=None, offset_y=None, button='left', count=1):
"""带偏移量点击本元素相对于左上角坐标。不传入x或y值时点击元素中间点 """带偏移量点击本元素相对于左上角坐标。不传入x或y值时点击元素中间点
@ -140,15 +143,14 @@ class Clicker(object):
w, h = self._ele.rect.size w, h = self._ele.rect.size
offset_x = w // 2 offset_x = w // 2
offset_y = h // 2 offset_y = h // 2
x, y = offset_scroll(self._ele, offset_x, offset_y) return self._click(*offset_scroll(self._ele, offset_x, offset_y), button=button, count=count)
self._click(x, y, button, count)
def multi(self, times=2): def multi(self, times=2):
"""多次点击 """多次点击
:param times: 默认双击 :param times: 默认双击
:return: None :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): 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对象 :return: DownloadMission对象
""" """
if save_path: if save_path:
self._ele.owner.tab.set.download_path(save_path) self._ele.tab.set.download_path(save_path)
elif not self._ele.page._browser._dl_mgr._running: elif not self._ele.tab._browser._dl_mgr._running:
self._ele.page.set.download_path('.') self._ele.tab._browser.set.download_path('.')
obj = self._ele.tab._browser if new_tab else self._ele.owner._tab
if rename or suffix: if rename or suffix:
self._ele.owner.tab.set.download_file_name(rename, suffix) obj.set.download_file_name(rename, suffix)
tab = self._ele.page if new_tab else self._ele.owner
self.left(by_js=by_js) 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): def to_upload(self, file_paths, by_js=False):
"""触发上传文件选择框并自动填入指定路径 """触发上传文件选择框并自动填入指定路径
@ -183,26 +184,61 @@ class Clicker(object):
self.left(by_js=by_js) self.left(by_js=by_js)
self._ele.owner.wait.upload_paths_inputted() 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出现并返回其对象 """点击后等待新tab出现并返回其对象
:param by_js: 是否使用js点击逻辑与click()一致 :param by_js: 是否使用js点击逻辑与click()一致
:param timeout: 等待超时时间
:return: 新标签页对象如果没有等到新标签页出现则抛出异常 :return: 新标签页对象如果没有等到新标签页出现则抛出异常
""" """
curr_tid = self._ele.tab.browser.tab_ids[0]
self.left(by_js=by_js) 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: if not tid:
raise RuntimeError('没有出现新标签页。') 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指定文本时返回Truetext为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指定文本时返回Truetext为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 loc_x: 绝对x坐标
:param client_y: 视口中的y坐标 :param loc_y: 绝对y坐标
:param view_x: 视口x坐标
:param view_y: 视口y坐标
:param button: 'left' 'right' 'middle' 'back' 'forward' :param button: 'left' 'right' 'middle' 'back' 'forward'
:param count: 点击次数 :param count: 点击次数
:return: None :return: None
""" """
self._ele.owner.run_cdp('Input.dispatchMouseEvent', type='mousePressed', x=client_x, self._ele.owner.actions.move_to((loc_x, loc_y), duration=.05)
y=client_y, button=button, clickCount=count, _ignore=AlertExistsError) self._ele.owner._run_cdp('Input.dispatchMouseEvent', type='mousePressed', x=view_x,
self._ele.owner.run_cdp('Input.dispatchMouseEvent', type='mouseReleased', x=client_x, y=view_y, button=button, clickCount=count, _ignore=AlertExistsError)
y=client_y, button=button, _ignore=AlertExistsError) self._ele.owner._run_cdp('Input.dispatchMouseEvent', type='mouseReleased', x=view_x,
y=view_y, button=button, _ignore=AlertExistsError)
return self._ele

View File

@ -10,28 +10,30 @@ from typing import Union
from .downloader import DownloadMission from .downloader import DownloadMission
from .._elements.chromium_element import ChromiumElement from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_tab import WebPageTab, ChromiumTab from .._pages.tabs import MixTab, ChromiumTab
class Clicker(object): class Clicker(object):
def __init__(self, ele: ChromiumElement): def __init__(self, ele: ChromiumElement):
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, def at(self,
offset_x: float = None, offset_x: float = None,
offset_y: float = None, offset_y: float = None,
button: str = 'left', 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, def to_download(self,
save_path: Union[str, Path] = None, 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 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: ...

View File

@ -5,13 +5,13 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @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): def __init__(self, owner):
""" """
:param owner: ChromiumBase对象 :param owner: Chromium对象
""" """
self._owner = owner self._owner = owner
@ -22,6 +22,24 @@ class CookiesSetter(object):
""" """
set_browser_cookies(self._owner, cookies) 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): def remove(self, name, url=None, domain=None, path=None):
"""删除一个cookie """删除一个cookie
:param name: cookie的name字段 :param name: cookie的name字段
@ -37,13 +55,11 @@ class CookiesSetter(object):
d['domain'] = domain d['domain'] = domain
if not url and not domain: if not url and not domain:
d['url'] = self._owner.url d['url'] = self._owner.url
if not d['url'].startswith('http'):
raise ValueError('需设置domain或url值。如设置url值需以http开头。')
if path is not None: if path is not None:
d['path'] = path d['path'] = path
self._owner.run_cdp('Network.deleteCookies', **d) self._owner._run_cdp('Network.deleteCookies', **d)
def clear(self):
"""清除cookies"""
self._owner.run_cdp('Network.clearBrowserCookies')
class SessionCookiesSetter(object): class SessionCookiesSetter(object):
@ -69,7 +85,7 @@ class SessionCookiesSetter(object):
self._owner.session.cookies.clear() self._owner.session.cookies.clear()
class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter): class MixPageCookiesSetter(CookiesSetter, SessionCookiesSetter):
def __call__(self, cookies): def __call__(self, cookies):
"""设置多个cookie注意不要传入单个 """设置多个cookie注意不要传入单个

View File

@ -8,26 +8,35 @@
from http.cookiejar import Cookie, CookieJar from http.cookiejar import Cookie, CookieJar
from typing import Union from typing import Union
from .._base.browser import Chromium
from .._pages.chromium_base import ChromiumBase 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.session_page import SessionPage
from .._pages.web_page import WebPage from .._pages.mix_page import MixPage
class CookiesSetter(object): class BrowserCookiesSetter(object):
_owner: ChromiumBase _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 __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 remove(self, name: str, url: str = None, domain: str = None, path: str = None) -> None: ...
def clear(self) -> None: ... def clear(self) -> None: ...
class SessionCookiesSetter(object): class SessionCookiesSetter(object):
_owner: SessionPage _owner: SessionPage = ...
def __init__(self, page: SessionPage): ... def __init__(self, page: SessionPage): ...
@ -38,8 +47,8 @@ class SessionCookiesSetter(object):
def clear(self) -> None: ... def clear(self) -> None: ...
class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter): class MixPageCookiesSetter(CookiesSetter, SessionCookiesSetter):
_owner: Union[WebPage, WebPageTab] _owner: Union[MixPage, MixTab] = ...
def __init__(self, page: SessionPage): ... def __init__(self, page: SessionPage): ...

View File

@ -20,21 +20,24 @@ class DownloadManager(object):
:param browser: Browser对象 :param browser: Browser对象
""" """
self._browser = browser self._browser = browser
self._page = browser.page # self._page = browser.page
self._when_download_file_exists = 'rename' # self._when_download_file_exists = 'rename'
self._save_path = None # 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._missions = {} # {guid: DownloadMission}
self._tab_missions = {} # {tab_id: DownloadMission} self._tab_missions = {} # {tab_id: DownloadMission}
self._flags = {} # {tab_id: [bool, DownloadMission]} self._flags = {} # {tab_id: [bool, DownloadMission]}
if self._page.download_path: # if self._page.download_path:
self.set_path(self._page, self._page.download_path) # self.set_path(self._page, self._page.download_path)
else: self._running = False
self._running = False
@property @property
def missions(self): def missions(self):
@ -47,13 +50,13 @@ class DownloadManager(object):
:param path: 下载路径绝对路径str :param path: 下载路径绝对路径str
:return: None :return: None
""" """
TabDownloadSettings(tab.tab_id).path = path tid = tab if isinstance(tab, str) else tab.tab_id
if tab is self._page or not self._running: TabDownloadSettings(tid).path = path
self._browser.driver.set_callback('Browser.downloadProgress', self._onDownloadProgress) if not self._running or tid == 'browser':
self._browser.driver.set_callback('Browser.downloadWillBegin', self._onDownloadWillBegin) self._browser._driver.set_callback('Browser.downloadProgress', self._onDownloadProgress)
r = self._browser.run_cdp('Browser.setDownloadBehavior', downloadPath=path, self._browser._driver.set_callback('Browser.downloadWillBegin', self._onDownloadWillBegin)
behavior='allowAndName', eventsEnabled=True) r = self._browser._run_cdp('Browser.setDownloadBehavior', downloadPath=self._browser._download_path,
self._save_path = path behavior='allowAndName', eventsEnabled=True)
if 'error' in r: if 'error' in r:
print('浏览器版本太低无法使用下载管理功能。') print('浏览器版本太低无法使用下载管理功能。')
self._running = True self._running = True
@ -121,7 +124,7 @@ class DownloadManager(object):
""" """
mission.state = 'canceled' mission.state = 'canceled'
try: try:
self._browser.run_cdp('Browser.cancelDownload', guid=mission.id) self._browser._run_cdp('Browser.cancelDownload', guid=mission.id)
except: except:
pass pass
if mission.final_path: if mission.final_path:
@ -134,7 +137,7 @@ class DownloadManager(object):
""" """
mission.state = 'skipped' mission.state = 'skipped'
try: try:
self._browser.run_cdp('Browser.cancelDownload', guid=mission.id) self._browser._run_cdp('Browser.cancelDownload', guid=mission.id)
except: except:
pass pass
@ -149,10 +152,11 @@ class DownloadManager(object):
def _onDownloadWillBegin(self, **kwargs): def _onDownloadWillBegin(self, **kwargs):
"""用于获取弹出新标签页触发的下载任务""" """用于获取弹出新标签页触发的下载任务"""
# print(kwargs)
guid = kwargs['guid'] 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.rename:
if settings.suffix is not None: if settings.suffix is not None:
name = f'{settings.rename}.{settings.suffix}' if settings.suffix else settings.rename 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': elif settings.when_file_exists == 'overwrite':
goal_path.unlink() 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 self._missions[guid] = m
if self.get_flag(tab_id) is False: # 取消该任务 if self.get_flag(tab_id) is False: # 取消该任务
@ -214,7 +218,17 @@ class DownloadManager(object):
mission.total_bytes = kwargs['totalBytes'] mission.total_bytes = kwargs['totalBytes']
form_path = f'{mission.save_path}{sep}{mission.id}' form_path = f'{mission.save_path}{sep}{mission.id}'
to_path = str(get_usable_path(f'{mission.path}{sep}{mission.name}')) 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) self.set_done(mission, 'completed', final_path=to_path)
else: # 'canceled' else: # 'canceled'

View File

@ -7,30 +7,30 @@
""" """
from typing import Dict, Optional, Union, Literal 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_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): class DownloadManager(object):
_browser: Browser = ... _browser: Chromium = ...
_page: ChromiumPage = ... # _page: ChromiumPage = ...
_missions: Dict[str, DownloadMission] = ... _missions: Dict[str, DownloadMission] = ...
_tab_missions: dict = ... _tab_missions: dict = ...
_flags: dict = ... _flags: dict = ...
_running: bool = ... _running: bool = ...
_save_path: Optional[str] = ... # _save_path: Optional[str] = ...
def __init__(self, browser: Browser): ... def __init__(self, browser: Chromium): ...
@property @property
def missions(self) -> Dict[str, DownloadMission]: ... 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_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: ... def set_flag(self, tab_id: str, flag: Union[bool, DownloadMission, None]) -> None: ...

View File

@ -26,7 +26,7 @@ class Listener(object):
:param owner: ChromiumBase对象 :param owner: ChromiumBase对象
""" """
self._owner = owner self._owner = owner
self._address = owner.address self._address = owner.browser.address
self._target_id = owner._target_id self._target_id = owner._target_id
self._driver = None self._driver = None
self._running_requests = 0 self._running_requests = 0
@ -127,13 +127,13 @@ class Listener(object):
if not self.listening: if not self.listening:
raise RuntimeError('监听未启动或已暂停。') raise RuntimeError('监听未启动或已暂停。')
if not timeout: if not timeout:
while self._caught.qsize() < count: while self._driver.is_running and self._caught.qsize() < count:
sleep(.03) sleep(.03)
fail = False fail = False
else: else:
end = perf_counter() + timeout end = perf_counter() + timeout
while True: while self._driver.is_running:
if perf_counter() > end: if perf_counter() > end:
fail = True fail = True
break break
@ -167,8 +167,8 @@ class Listener(object):
raise RuntimeError('监听未启动或已暂停。') raise RuntimeError('监听未启动或已暂停。')
caught = 0 caught = 0
end = perf_counter() + timeout if timeout else None end = perf_counter() + timeout if timeout else None
while True: while self._driver.is_running:
if (timeout and perf_counter() > end) or self._driver._stopped.is_set(): if (timeout and perf_counter() > end) or not self._driver.is_running:
return return
if self._caught.qsize() >= gap: if self._caught.qsize() >= gap:
yield self._caught.get_nowait() if gap == 1 else [self._caught.get_nowait() for _ in range(gap)] yield self._caught.get_nowait() if gap == 1 else [self._caught.get_nowait() for _ in range(gap)]

View File

@ -18,7 +18,7 @@ class ElementRect(object):
def corners(self): def corners(self):
"""返回元素四个角坐标顺序左上、右上、右下、左下没有大小的元素抛出NoRectError""" """返回元素四个角坐标顺序左上、右上、右下、左下没有大小的元素抛出NoRectError"""
vr = self._get_viewport_rect('border') 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'] sx = r['pageX']
sy = r['pageY'] 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)] 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 @property
def size(self): def size(self):
"""返回元素大小,格式(宽, 高)""" """返回元素大小,格式(宽, 高)"""
border = self._ele.owner.run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id, 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'] nodeId=self._ele._node_id, objectId=self._ele._obj_id)['model']['border']
return border[2] - border[0], border[5] - border[1] return border[2] - border[0], border[5] - border[1]
@property @property
def location(self): def location(self):
"""返回元素左上角的绝对坐标""" """返回元素左上角的绝对坐标"""
cl = self.viewport_location return self._get_page_coord(*self.viewport_location)
return self._get_page_coord(cl[0], cl[1])
@property @property
def midpoint(self): def midpoint(self):
"""返回元素中间点的绝对坐标""" """返回元素中间点的绝对坐标"""
cl = self.viewport_midpoint return self._get_page_coord(*self.viewport_midpoint)
return self._get_page_coord(cl[0], cl[1])
@property @property
def click_point(self): def click_point(self):
"""返回元素接受点击的点的绝对坐标""" """返回元素接受点击的点的绝对坐标"""
cl = self.viewport_click_point return self._get_page_coord(*self.viewport_click_point)
return self._get_page_coord(cl[0], cl[1])
@property @property
def viewport_location(self): def viewport_location(self):
@ -77,7 +74,7 @@ class ElementRect(object):
"""返回元素左上角在屏幕上坐标,左上角为(0, 0)""" """返回元素左上角在屏幕上坐标,左上角为(0, 0)"""
vx, vy = self._ele.owner.rect.viewport_location vx, vy = self._ele.owner.rect.viewport_location
ex, ey = self.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 return (vx + ex) * pr, (ey + vy) * pr
@property @property
@ -85,7 +82,7 @@ class ElementRect(object):
"""返回元素中点在屏幕上坐标,左上角为(0, 0)""" """返回元素中点在屏幕上坐标,左上角为(0, 0)"""
vx, vy = self._ele.owner.rect.viewport_location vx, vy = self._ele.owner.rect.viewport_location
ex, ey = self.viewport_midpoint 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 return (vx + ex) * pr, (ey + vy) * pr
@property @property
@ -93,21 +90,26 @@ class ElementRect(object):
"""返回元素中点在屏幕上坐标,左上角为(0, 0)""" """返回元素中点在屏幕上坐标,左上角为(0, 0)"""
vx, vy = self._ele.owner.rect.viewport_location vx, vy = self._ele.owner.rect.viewport_location
ex, ey = self.viewport_click_point 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 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): def _get_viewport_rect(self, quad):
"""按照类型返回在可视窗口中的范围 """按照类型返回在可视窗口中的范围
:param quad: 方框类型margin border padding :param quad: 方框类型margin border padding
:return: 四个角坐标 :return: 四个角坐标
""" """
return self._ele.owner.run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id, return self._ele.owner._run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id)['model'][quad]
# nodeId=self._ele._node_id, objectId=self._ele._obj_id
)['model'][quad]
def _get_page_coord(self, x, y): 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'] sx = r['pageX']
sy = r['pageY'] sy = r['pageY']
return x + sx, y + sy return x + sx, y + sy
@ -174,17 +176,23 @@ class TabRect(object):
@property @property
def viewport_size_with_scrollbar(self): 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(' ') w, h = r.split(' ')
return int(w), int(h) 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): 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): 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): class FrameRect(object):
@ -214,8 +222,8 @@ class FrameRect(object):
@property @property
def size(self): def size(self):
"""返回frame内页面尺寸格式(宽, 高)""" """返回frame内页面尺寸格式(宽, 高)"""
w = self._frame.doc_ele.run_js('return this.body.scrollWidth') w = self._frame.doc_ele._run_js('return this.body.scrollWidth')
h = self._frame.doc_ele.run_js('return this.body.scrollHeight') h = self._frame.doc_ele._run_js('return this.body.scrollHeight')
return w, h return w, h
@property @property
@ -232,3 +240,11 @@ class FrameRect(object):
def viewport_corners(self): def viewport_corners(self):
"""返回元素四个角视口坐标,顺序:左上、右上、右下、左下""" """返回元素四个角视口坐标,顺序:左上、右上、右下、左下"""
return self._frame.frame_ele.rect.viewport_corners 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)

View File

@ -12,8 +12,8 @@ from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_base import ChromiumBase from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage from .._pages.chromium_page import ChromiumPage
from .._pages.chromium_tab import ChromiumTab, WebPageTab from .._pages.tabs import ChromiumTab, MixTab
from .._pages.web_page import WebPage from .._pages.mix_page import MixPage
class ElementRect(object): class ElementRect(object):
@ -56,6 +56,9 @@ class ElementRect(object):
@property @property
def viewport_corners(self) -> Tuple[Tuple[float, float], ...]: ... 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_viewport_rect(self, quad: str) -> Union[list, None]: ...
def _get_page_coord(self, x: float, y: float) -> Tuple[float, float]: ... def _get_page_coord(self, x: float, y: float) -> Tuple[float, float]: ...
@ -63,7 +66,7 @@ class ElementRect(object):
class TabRect(object): class TabRect(object):
def __init__(self, owner: ChromiumBase): def __init__(self, owner: ChromiumBase):
self._owner: Union[ChromiumPage, ChromiumTab, WebPage, WebPageTab] = ... self._owner: Union[ChromiumPage, ChromiumTab, MixPage, MixTab] = ...
@property @property
def window_state(self) -> str: ... def window_state(self) -> str: ...
@ -89,6 +92,9 @@ class TabRect(object):
@property @property
def viewport_size_with_scrollbar(self) -> Tuple[int, int]: ... 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_page_rect(self) -> dict: ...
def _get_window_rect(self) -> dict: ... def _get_window_rect(self) -> dict: ...
@ -118,3 +124,6 @@ class FrameRect(object):
@property @property
def viewport_corners(self) -> Tuple[Tuple[float, float], ...]: ... def viewport_corners(self) -> Tuple[Tuple[float, float], ...]: ...
@property
def scroll_position(self) -> Tuple[float, float]: ...

View File

@ -39,16 +39,16 @@ class Screencast(object):
raise ValueError('save_path必须设置。') raise ValueError('save_path必须设置。')
if self._mode in ('frugal_video', 'video'): 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._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: else:
self._tmp_path = Path(gettempdir()) / 'DrissionPage' / f'screencast_tmp_{time()}_{randint(0, 100)}' self._tmp_path = Path(gettempdir()) / 'DrissionPage' / f'screencast_tmp_{time()}_{randint(0, 100)}'
self._tmp_path.mkdir(parents=True, exist_ok=True) self._tmp_path.mkdir(parents=True, exist_ok=True)
if self._mode.startswith('frugal'): if self._mode.startswith('frugal'):
self._owner.driver.set_callback('Page.screencastFrame', self._onScreencastFrame) 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'): elif not self._mode.startswith('js'):
self._running = True self._running = True
@ -79,8 +79,8 @@ class Screencast(object):
} }
''' '''
print('请手动选择要录制的目标。') print('请手动选择要录制的目标。')
self._owner.run_js('var DrissionPage_Screencast_blob;var DrissionPage_Screencast_blob_ok=false;') self._owner._run_js('var DrissionPage_Screencast_blob;var DrissionPage_Screencast_blob_ok=false;')
self._owner.run_js(js) self._owner._run_js(js)
def stop(self, video_name=None): def stop(self, video_name=None):
"""停止录屏 """停止录屏
@ -93,19 +93,19 @@ class Screencast(object):
path = f'{self._path}{sep}{name}' path = f'{self._path}{sep}{name}'
if self._mode.startswith('js'): if self._mode.startswith('js'):
self._owner.run_js('mediaRecorder.stop();', as_expr=True) self._owner._run_js('mediaRecorder.stop();', as_expr=True)
while not self._owner.run_js('return DrissionPage_Screencast_blob_ok;'): while not self._owner._run_js('return DrissionPage_Screencast_blob_ok;'):
sleep(.1) sleep(.1)
blob = self._owner.run_js('return DrissionPage_Screencast_blob;') blob = self._owner._run_js('return DrissionPage_Screencast_blob;')
uuid = self._owner.run_cdp('IO.resolveBlob', objectId=blob['result']['objectId'])['uuid'] uuid = self._owner._run_cdp('IO.resolveBlob', objectId=blob['result']['objectId'])['uuid']
data = self._owner.run_cdp('IO.read', handle=f'blob:{uuid}')['data'] data = self._owner._run_cdp('IO.read', handle=f'blob:{uuid}')['data']
with open(path, 'wb') as f: with open(path, 'wb') as f:
f.write(b64decode(data)) f.write(b64decode(data))
return path return path
if self._mode.startswith('frugal'): if self._mode.startswith('frugal'):
self._owner.driver.set_callback('Page.screencastFrame', None) self._owner.driver.set_callback('Page.screencastFrame', None)
self._owner.run_cdp('Page.stopScreencast') self._owner._run_cdp('Page.stopScreencast')
else: else:
self._enable = False self._enable = False
while self._running: while self._running:
@ -164,7 +164,7 @@ class Screencast(object):
path = self._tmp_path or self._path path = self._tmp_path or self._path
with open(f'{path}{sep}{kwargs["metadata"]["timestamp"]}.jpg', 'wb') as f: with open(f'{path}{sep}{kwargs["metadata"]["timestamp"]}.jpg', 'wb') as f:
f.write(b64decode(kwargs['data'])) 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): class ScreencastMode(object):

View File

@ -16,12 +16,12 @@ class Scroller(object):
:param ele: 元素对象 :param ele: 元素对象
""" """
self._driver = ele self._driver = ele
self.t1 = self.t2 = 'this' self._t1 = self._t2 = 'this'
self._wait_complete = False self._wait_complete = False
def _run_js(self, js): def _run_js(self, js):
js = js.format(self.t1, self.t2, self.t2) js = js.format(self._t1, self._t2, self._t2)
self._driver.run_js(js) self._driver._run_js(js)
self._wait_scrolled() self._wait_scrolled()
def to_top(self): def to_top(self):
@ -88,14 +88,14 @@ class Scroller(object):
return return
owner = self._driver.owner if self._driver._type == 'ChromiumElement' else self._driver 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'] x = r['layoutViewport']['pageX']
y = r['layoutViewport']['pageY'] y = r['layoutViewport']['pageY']
end_time = perf_counter() + owner.timeout end_time = perf_counter() + owner.timeout
while perf_counter() < end_time: while perf_counter() < end_time:
sleep(.1) sleep(.1)
r = owner.run_cdp('Page.getLayoutMetrics') r = owner._run_cdp('Page.getLayoutMetrics')
x1 = r['layoutViewport']['pageX'] x1 = r['layoutViewport']['pageX']
y1 = r['layoutViewport']['pageY'] y1 = r['layoutViewport']['pageY']
@ -125,8 +125,8 @@ class PageScroller(Scroller):
:param owner: 页面对象 :param owner: 页面对象
""" """
super().__init__(owner) super().__init__(owner)
self.t1 = 'window' self._t1 = 'window'
self.t2 = 'document.documentElement' self._t2 = 'document.documentElement'
def to_see(self, loc_or_ele, center=None): def to_see(self, loc_or_ele, center=None):
"""滚动页面直到元素可见 """滚动页面直到元素可见
@ -144,9 +144,9 @@ class PageScroller(Scroller):
:return: None :return: None
""" """
txt = 'true' if center else 'false' 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): 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) { if (document.documentElement && document.documentElement.scrollTop) {
scroll_top = document.documentElement.scrollTop; scroll_top = document.documentElement.scrollTop;
} else if (document.body) {scroll_top = document.body.scrollTop;} } else if (document.body) {scroll_top = document.body.scrollTop;}
@ -165,7 +165,7 @@ class FrameScroller(PageScroller):
:param frame: ChromiumFrame对象 :param frame: ChromiumFrame对象
""" """
super().__init__(frame.doc_ele) 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): def to_see(self, loc_or_ele, center=None):
"""滚动页面直到元素可见 """滚动页面直到元素可见
@ -173,5 +173,5 @@ class FrameScroller(PageScroller):
:param center: 是否尽量滚动到页面正中为None时如果被遮挡则滚动到页面正中 :param center: 是否尽量滚动到页面正中为None时如果被遮挡则滚动到页面正中
:return: 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) self._to_see(ele, center)

View File

@ -13,8 +13,8 @@ from .._pages.chromium_base import ChromiumBase
class Scroller(object): class Scroller(object):
def __init__(self, page_or_ele: Union[ChromiumBase, ChromiumElement]): def __init__(self, page_or_ele: Union[ChromiumBase, ChromiumElement]):
self.t1: str = ... self._t1: str = ...
self.t2: str = ... self._t2: str = ...
self._driver: Union[ChromiumBase, ChromiumElement] = ... self._driver: Union[ChromiumBase, ChromiumElement] = ...
self._wait_complete: bool = ... self._wait_complete: bool = ...
@ -64,7 +64,7 @@ class FrameScroller(PageScroller):
:param frame: ChromiumFrame对象 :param frame: ChromiumFrame对象
""" """
self._driver = frame.doc_ele self._driver = frame.doc_ele
self.t1 = self.t2 = 'this.documentElement' self._t1 = self._t2 = 'this.documentElement'
self._wait_complete = False self._wait_complete = False
def to_see(self, loc_or_ele, center=None): def to_see(self, loc_or_ele, center=None):

View File

@ -45,7 +45,7 @@ class SelectElement(object):
"""返回第一个被选中的option元素 """返回第一个被选中的option元素
:return: ChromiumElement对象或None :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 return ele
@property @property
@ -69,7 +69,7 @@ class SelectElement(object):
for i in self.options: for i in self.options:
change = True change = True
mode = 'false' if i.states.is_selected else '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: if change:
self._dispatch_change() self._dispatch_change()
@ -258,12 +258,12 @@ class SelectElement(object):
if not self.is_multi and len(option) > 1: if not self.is_multi and len(option) > 1:
option = option[:1] option = option[:1]
for o in option: for o in option:
o.run_js(f'this.selected={mode};') o._run_js(f'this.selected={mode};')
self._dispatch_change() self._dispatch_change()
else: else:
option.run_js(f'this.selected={mode};') option._run_js(f'this.selected={mode};')
self._dispatch_change() self._dispatch_change()
def _dispatch_change(self): 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}));')

View File

@ -10,14 +10,14 @@ from time import sleep
from requests.structures import CaseInsensitiveDict 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.settings import Settings
from .._functions.tools import show_or_hide_browser from .._functions.tools import show_or_hide_browser
from .._functions.web import format_headers from .._functions.web import format_headers
from ..errors import ElementLostError, JavaScriptError from ..errors import ElementLostError, JavaScriptError
class BasePageSetter(object): class BaseSetter(object):
def __init__(self, owner): def __init__(self, owner):
""" """
:param owner: BasePage对象 :param owner: BasePage对象
@ -33,32 +33,6 @@ class BasePageSetter(object):
self._owner._none_ele_return_value = on_off self._owner._none_ele_return_value = on_off
self._owner._none_ele_value = value 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): def retry_times(self, times):
"""设置连接失败重连次数""" """设置连接失败重连次数"""
self._owner.retry_times = times self._owner.retry_times = times
@ -67,189 +41,17 @@ class ChromiumBaseSetter(BasePageSetter):
"""设置连接失败重连间隔""" """设置连接失败重连间隔"""
self._owner.retry_interval = interval 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): def download_path(self, path):
"""设置下载路径 """设置下载路径
:param path: 下载路径 :param path: 下载路径
:return: None :return: None
""" """
path = str(Path(path).absolute()) if path is None:
self._owner._download_path = path path = '.'
self._owner.browser._dl_mgr.set_path(self._owner, path) self._owner._download_path = str(Path(path).absolute())
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)
class ChromiumPageSetter(TabSetter): class SessionPageSetter(BaseSetter):
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):
def __init__(self, owner): def __init__(self, owner):
""" """
:param owner: SessionPage对象 :param owner: SessionPage对象
@ -264,30 +66,21 @@ class SessionPageSetter(BasePageSetter):
self._cookies_setter = SessionCookiesSetter(self._owner) self._cookies_setter = SessionCookiesSetter(self._owner)
return self._cookies_setter 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): def download_path(self, path):
"""设置下载路径 """设置下载路径
:param path: 下载路径 :param path: 下载路径
:return: None :return: None
""" """
path = str(Path(path).absolute()) super().download_path(path)
self._owner._download_path = path
if self._owner._DownloadKit: 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): def timeout(self, second):
"""设置连接超时时间 """设置连接超时时间
:param second: 秒数 :param second: 秒数
:return: None :return: None
""" """
self._owner.timeout = second self._owner._timeout = second
def encoding(self, encoding, set_all=True): def encoding(self, encoding, set_all=True):
"""设置编码 """设置编码
@ -395,7 +188,272 @@ class SessionPageSetter(BasePageSetter):
self._owner.session.mount(url, adapter) 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): def __init__(self, owner):
super().__init__(owner) super().__init__(owner)
self._session_setter = SessionPageSetter(self._owner) self._session_setter = SessionPageSetter(self._owner)
@ -405,7 +463,7 @@ class WebPageSetter(ChromiumPageSetter):
def cookies(self): def cookies(self):
"""返回用于设置cookies的对象""" """返回用于设置cookies的对象"""
if self._cookies_setter is None: if self._cookies_setter is None:
self._cookies_setter = WebPageCookiesSetter(self._owner) self._cookies_setter = MixPageCookiesSetter(self._owner)
return self._cookies_setter return self._cookies_setter
def headers(self, headers) -> None: def headers(self, headers) -> None:
@ -426,7 +484,7 @@ class WebPageSetter(ChromiumPageSetter):
self._chromium_setter.user_agent(ua, platform) self._chromium_setter.user_agent(ua, platform)
class WebPageTabSetter(TabSetter): class MixTabSetter(TabSetter):
def __init__(self, owner): def __init__(self, owner):
super().__init__(owner) super().__init__(owner)
self._session_setter = SessionPageSetter(self._owner) self._session_setter = SessionPageSetter(self._owner)
@ -436,7 +494,7 @@ class WebPageTabSetter(TabSetter):
def cookies(self): def cookies(self):
"""返回用于设置cookies的对象""" """返回用于设置cookies的对象"""
if self._cookies_setter is None: if self._cookies_setter is None:
self._cookies_setter = WebPageCookiesSetter(self._owner) self._cookies_setter = MixPageCookiesSetter(self._owner)
return self._cookies_setter return self._cookies_setter
def headers(self, headers) -> None: def headers(self, headers) -> None:
@ -456,6 +514,17 @@ class WebPageTabSetter(TabSetter):
if self._owner._has_driver: if self._owner._has_driver:
self._chromium_setter.user_agent(ua, platform) 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): class ChromiumElementSetter(object):
def __init__(self, ele): def __init__(self, ele):
@ -464,19 +533,19 @@ class ChromiumElementSetter(object):
""" """
self._ele = ele self._ele = ele
def attr(self, name, value): def attr(self, name, value=''):
"""设置元素attribute属性 """设置元素attribute属性
:param name: 属性名 :param name: 属性名
:param value: 属性值 :param value: 属性值
:return: None :return: None
""" """
try: try:
self._ele.owner.run_cdp('DOM.setAttributeValue', self._ele.owner._run_cdp('DOM.setAttributeValue',
nodeId=self._ele._node_id, name=name, value=str(value)) nodeId=self._ele._node_id, name=name, value=str(value))
except ElementLostError: except ElementLostError:
self._ele._refresh_id() self._ele._refresh_id()
self._ele.owner.run_cdp('DOM.setAttributeValue', self._ele.owner._run_cdp('DOM.setAttributeValue',
nodeId=self._ele._node_id, name=name, value=str(value)) nodeId=self._ele._node_id, name=name, value=str(value))
def property(self, name, value): def property(self, name, value):
"""设置元素property属性 """设置元素property属性
@ -485,7 +554,7 @@ class ChromiumElementSetter(object):
:return: None :return: None
""" """
value = value.replace('"', r'\"') 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): def style(self, name, value):
"""设置元素style样式 """设置元素style样式
@ -494,7 +563,7 @@ class ChromiumElementSetter(object):
:return: None :return: None
""" """
try: try:
self._ele.run_js(f'this.style.{name}="{value}";') self._ele._run_js(f'this.style.{name}="{value}";')
except JavaScriptError: except JavaScriptError:
raise ValueError(f'设置失败,请检查属性名{name}') raise ValueError(f'设置失败,请检查属性名{name}')
@ -522,6 +591,22 @@ class ChromiumFrameSetter(ChromiumBaseSetter):
""" """
self._owner.frame_ele.set.attr(name, value) 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): class LoadMode(object):
"""用于设置页面加载策略的类""" """用于设置页面加载策略的类"""
@ -578,7 +663,7 @@ class PageScrollSetter(object):
if not isinstance(on_off, bool): if not isinstance(on_off, bool):
raise TypeError('on_off必须为bool。') raise TypeError('on_off必须为bool。')
b = 'smooth' if on_off else 'auto' 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 self._scroll._wait_complete = on_off
@ -652,7 +737,7 @@ class WindowSetter(object):
"""获取窗口位置及大小信息""" """获取窗口位置及大小信息"""
for _ in range(50): for _ in range(50):
try: try:
return self._owner.run_cdp('Browser.getWindowForTarget') return self._owner._run_cdp('Browser.getWindowForTarget')
except: except:
sleep(.1) sleep(.1)
@ -662,7 +747,7 @@ class WindowSetter(object):
:return: None :return: None
""" """
try: 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: except:
raise RuntimeError('浏览器全屏或最小化状态时请先调用set.window.normal()恢复正常状态。') raise RuntimeError('浏览器全屏或最小化状态时请先调用set.window.normal()恢复正常状态。')

View File

@ -11,88 +11,35 @@ from typing import Union, Tuple, Literal, Any, Optional
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from requests.auth import HTTPBasicAuth 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 .scroller import PageScroller
from .._base.base import BasePage from .._base.base import BasePage
from .._base.browser import Chromium
from .._elements.chromium_element import ChromiumElement from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_base import ChromiumBase from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage 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.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'] FILE_EXISTS = Literal['skip', 'rename', 'overwrite', 's', 'r', 'o']
class BasePageSetter(object): class BaseSetter(object):
def __init__(self, owner: BasePage): def __init__(self, owner: Union[Chromium, BasePage]):
self._owner: BasePage = ... self._owner: Union[Chromium, BasePage] = ...
def NoneElement_value(self, value: Any = None, on_off: bool = True) -> None: ... 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_times(self, times: int) -> None: ...
def retry_interval(self, interval: float) -> None: ... def retry_interval(self, interval: float) -> None: ...
def timeouts(self, base: float = None, page_load: float = None, script: float = None) -> None: ... def download_path(self, path: Union[str, Path, 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: ...
class TabSetter(ChromiumBaseSetter): class SessionPageSetter(BaseSetter):
_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):
_owner: SessionPage = ... _owner: SessionPage = ...
_cookies_setter: Optional[SessionCookiesSetter] = ... _cookies_setter: Optional[SessionCookiesSetter] = ...
@ -105,7 +52,7 @@ class SessionPageSetter(BasePageSetter):
def retry_interval(self, interval: float) -> None: ... 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: ... def timeout(self, second: float) -> None: ...
@ -138,8 +85,83 @@ class SessionPageSetter(BasePageSetter):
def add_adapter(self, url: str, adapter: HTTPAdapter) -> None: ... def add_adapter(self, url: str, adapter: HTTPAdapter) -> None: ...
class WebPageSetter(ChromiumPageSetter): class BrowserBaseSetter(BaseSetter):
_owner: WebPage = ... _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 = ... _session_setter: SessionPageSetter = ...
_chromium_setter: ChromiumPageSetter = ... _chromium_setter: ChromiumPageSetter = ...
@ -148,11 +170,11 @@ class WebPageSetter(ChromiumPageSetter):
def headers(self, headers: Union[str, dict]) -> None: ... def headers(self, headers: Union[str, dict]) -> None: ...
@property @property
def cookies(self) -> WebPageCookiesSetter: ... def cookies(self) -> MixPageCookiesSetter: ...
class WebPageTabSetter(TabSetter): class MixTabSetter(TabSetter):
_owner: WebPageTab = ... _owner: MixTab = ...
_session_setter: SessionPageSetter = ... _session_setter: SessionPageSetter = ...
_chromium_setter: ChromiumBaseSetter = ... _chromium_setter: ChromiumBaseSetter = ...
@ -161,14 +183,16 @@ class WebPageTabSetter(TabSetter):
def headers(self, headers: Union[str, dict]) -> None: ... def headers(self, headers: Union[str, dict]) -> None: ...
@property @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): class ChromiumElementSetter(object):
def __init__(self, ele: ChromiumElement): def __init__(self, ele: ChromiumElement):
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: ... def property(self, name: str, value: str) -> None: ...
@ -186,8 +210,9 @@ class ChromiumFrameSetter(ChromiumBaseSetter):
class LoadMode(object): class LoadMode(object):
def __init__(self, owner: ChromiumBase): _owner: Union[Chromium, ChromiumBase] = ...
self._owner: ChromiumBase = ...
def __init__(self, owner: Union[Chromium, ChromiumBase]): ...
def __call__(self, value: str) -> None: ... def __call__(self, value: str) -> None: ...

View File

@ -19,31 +19,31 @@ class ElementStates(object):
@property @property
def is_selected(self): def is_selected(self):
"""返回列表元素是否被选择""" """返回列表元素是否被选择"""
return self._ele.run_js('return this.selected;') return self._ele._run_js('return this.selected;')
@property @property
def is_checked(self): def is_checked(self):
"""返回元素是否被选择""" """返回元素是否被选择"""
return self._ele.run_js('return this.checked;') return self._ele._run_js('return this.checked;')
@property @property
def is_displayed(self): def is_displayed(self):
"""返回元素是否显示""" """返回元素是否显示"""
return not (self._ele.style('visibility') == 'hidden' or 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')) or self._ele.style('display') == 'none' or self._ele.property('hidden'))
@property @property
def is_enabled(self): def is_enabled(self):
"""返回元素是否可用""" """返回元素是否可用"""
return not self._ele.run_js('return this.disabled;') return not self._ele._run_js('return this.disabled;')
@property @property
def is_alive(self): def is_alive(self):
"""返回元素是否仍在DOM中""" """返回元素是否仍在DOM中"""
try: try:
return self._ele.owner.run_cdp('DOM.describeNode', return self._ele.owner._run_cdp('DOM.describeNode',
backendNodeId=self._ele._backend_id)['node']['nodeId'] != 0 backendNodeId=self._ele._backend_id)['node']['nodeId'] != 0
except ElementLostError: except ElementLostError:
return False return False
@ -66,7 +66,7 @@ class ElementStates(object):
"""返回元素是否被覆盖与是否在视口中无关如被覆盖返回覆盖元素的backend id否则返回False""" """返回元素是否被覆盖与是否在视口中无关如被覆盖返回覆盖元素的backend id否则返回False"""
lx, ly = self._ele.rect.click_point lx, ly = self._ele.rect.click_point
try: 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 return bid if bid != self._ele._backend_id else False
except CDPError: except CDPError:
return False return False
@ -95,14 +95,14 @@ class ShadowRootStates(object):
@property @property
def is_enabled(self): def is_enabled(self):
"""返回元素是否可用""" """返回元素是否可用"""
return not self._ele.run_js('return this.disabled;') return not self._ele._run_js('return this.disabled;')
@property @property
def is_alive(self): def is_alive(self):
"""返回元素是否仍在DOM中""" """返回元素是否仍在DOM中"""
try: try:
return self._ele.owner.run_cdp('DOM.describeNode', return self._ele.owner._run_cdp('DOM.describeNode',
backendNodeId=self._ele._backend_id)['node']['nodeId'] != 0 backendNodeId=self._ele._backend_id)['node']['nodeId'] != 0
except ElementLostError: except ElementLostError:
return False return False
@ -125,7 +125,7 @@ class PageStates(object):
def is_alive(self): def is_alive(self):
"""返回页面对象是否仍然可用""" """返回页面对象是否仍然可用"""
try: try:
self._owner.run_cdp('Page.getLayoutMetrics') self._owner._run_cdp('Page.getLayoutMetrics')
return True return True
except PageDisconnectedError: except PageDisconnectedError:
return False return False
@ -157,8 +157,8 @@ class FrameStates(object):
def is_alive(self): def is_alive(self):
"""返回frame元素是否可用且里面仍挂载有frame""" """返回frame元素是否可用且里面仍挂载有frame"""
try: try:
node = self._frame._target_page.run_cdp('DOM.describeNode', node = self._frame._target_page._run_cdp('DOM.describeNode',
backendNodeId=self._frame._frame_ele._backend_id)['node'] backendNodeId=self._frame._frame_ele._backend_id)['node']
except (ElementLostError, PageDisconnectedError): except (ElementLostError, PageDisconnectedError):
return False return False
return 'frameId' in node return 'frameId' in node
@ -172,7 +172,7 @@ class FrameStates(object):
def is_displayed(self): def is_displayed(self):
"""返回iframe是否显示""" """返回iframe是否显示"""
return not (self._frame.frame_ele.style('visibility') == 'hidden' 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') or self._frame.frame_ele.style('display') == 'none')
@property @property

View File

@ -13,6 +13,9 @@ from ..errors import WaitTimeoutError, NoRectError
class OriginWaiter(object): class OriginWaiter(object):
def __init__(self, owner):
self._owner = owner
def __call__(self, second, scope=None): def __call__(self, second, scope=None):
"""等待若干秒,如传入两个参数,等待时间为这两个数间的一个随机数 """等待若干秒,如传入两个参数,等待时间为这两个数间的一个随机数
:param second: 秒数 :param second: 秒数
@ -24,14 +27,86 @@ class OriginWaiter(object):
else: else:
from random import uniform from random import uniform
sleep(uniform(second, scope)) 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): 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): def ele_deleted(self, loc_or_ele, timeout=None, raise_err=None):
"""等待元素从DOM中删除 """等待元素从DOM中删除
@ -40,7 +115,7 @@ class BaseWaiter(OriginWaiter):
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :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 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): def ele_displayed(self, loc_or_ele, timeout=None, raise_err=None):
@ -51,9 +126,9 @@ class BaseWaiter(OriginWaiter):
:return: 是否等待成功 :return: 是否等待成功
""" """
if timeout is None: if timeout is None:
timeout = self._driver.timeout timeout = self._owner.timeout
end_time = perf_counter() + 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() timeout = end_time - perf_counter()
if timeout <= 0: if timeout <= 0:
if raise_err is True or Settings.raise_when_wait_failed is True: if raise_err is True or Settings.raise_when_wait_failed is True:
@ -70,9 +145,9 @@ class BaseWaiter(OriginWaiter):
:return: 是否等待成功 :return: 是否等待成功
""" """
if timeout is None: if timeout is None:
timeout = self._driver.timeout timeout = self._owner.timeout
end_time = perf_counter() + 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() timeout = end_time - perf_counter()
if timeout <= 0: if timeout <= 0:
if raise_err is True or Settings.raise_when_wait_failed is True: 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]) else [get_loc(l)[1] for l in locators])
method = any if any_one else all 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 end_time = perf_counter() + timeout
while perf_counter() < end_time: 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 return True
sleep(.01) sleep(.01)
if raise_err is True or Settings.raise_when_wait_failed is True: 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): def upload_paths_inputted(self):
"""等待自动填写上传文件路径""" """等待自动填写上传文件路径"""
end_time = perf_counter() + self._driver.timeout end_time = perf_counter() + self._owner.timeout
while perf_counter() < end_time: while perf_counter() < end_time:
if not self._driver._upload_list: if not self._owner._upload_list:
return True return True
sleep(.01) sleep(.01)
return False return False
@ -163,22 +238,22 @@ class BaseWaiter(OriginWaiter):
:param cancel_it: 是否取消该任务 :param cancel_it: 是否取消该任务
:return: 成功返回任务对象失败返回False :return: 成功返回任务对象失败返回False
""" """
if not self._driver.browser._dl_mgr._running: if not self._owner.browser._dl_mgr._running:
raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可') raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
self._driver.browser._dl_mgr.set_flag(self._driver.tab_id, False if cancel_it else True) self._owner.browser._dl_mgr.set_flag(self._owner.tab_id, False if cancel_it else True)
if timeout is None: if timeout is None:
timeout = self._driver.timeout timeout = self._owner.timeout
r = False r = False
end_time = perf_counter() + timeout end_time = perf_counter() + timeout
while perf_counter() < end_time: 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): if not isinstance(v, bool):
r = v r = v
break break
sleep(.005) 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 return r
def url_change(self, text, exclude=False, timeout=None, raise_err=None): 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 exclude: 是否排除为True时当url不包含text指定文本时返回True
:param timeout: 超时时间 :param timeout: 超时时间
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :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): def title_change(self, text, exclude=False, timeout=None, raise_err=None):
"""等待title变成包含或不包含指定文本 """等待title变成包含或不包含指定文本
:param text: 用于识别的文本 :param text: 用于识别的文本
:param exclude: 是否排除为True时当title不包含text指定文本时返回True :param exclude: 是否排除为True时当title不包含text指定文本时返回True
:param timeout: 超时时间 :param timeout: 超时时间为None使用页面设置
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :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): def _change(self, arg, text, exclude=False, timeout=None, raise_err=None):
"""等待指定属性变成包含或不包含指定文本 """等待指定属性变成包含或不包含指定文本
@ -210,18 +285,26 @@ class BaseWaiter(OriginWaiter):
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :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: if timeout is None:
timeout = self._driver.timeout timeout = self._owner.timeout
end_time = perf_counter() + timeout end_time = perf_counter() + timeout
while perf_counter() < end_time: while perf_counter() < end_time:
if arg == 'url': if do():
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):
return True return True
sleep(.05) sleep(.05)
@ -238,22 +321,22 @@ class BaseWaiter(OriginWaiter):
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :return: 是否等待成功
""" """
if timeout != 0: timeout = timeout if timeout is not None else self._owner.timeout
if timeout is None or timeout is True: timeout = .1 if timeout <= 0 else timeout
timeout = self._driver.timeout end_time = perf_counter() + timeout
end_time = perf_counter() + timeout while perf_counter() < end_time:
while perf_counter() < end_time: if self._owner._is_loading == start:
if self._driver._is_loading == start: return True
return True sleep(gap)
sleep(gap)
if raise_err is True or Settings.raise_when_wait_failed is True: if raise_err is True or Settings.raise_when_wait_failed is True:
raise WaitTimeoutError(f'等待页面加载失败(等待{timeout}秒)。') raise WaitTimeoutError(f'等待页面加载失败(等待{timeout}秒)。')
else: else:
return False return False
class TabWaiter(BaseWaiter): class TabWaiter(BaseWaiter):
"""标签页对象等待对象"""
def downloads_done(self, timeout=None, cancel_if_timeout=True): def downloads_done(self, timeout=None, cancel_if_timeout=True):
"""等待所有浏览器下载任务结束 """等待所有浏览器下载任务结束
@ -261,23 +344,23 @@ class TabWaiter(BaseWaiter):
:param cancel_if_timeout: 超时时是否取消剩余任务 :param cancel_if_timeout: 超时时是否取消剩余任务
:return: 是否等待成功 :return: 是否等待成功
""" """
if not self._driver.browser._dl_mgr._running: if not self._owner.browser._dl_mgr._running:
raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可') raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
if not timeout: 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) sleep(.5)
return True return True
else: else:
end_time = perf_counter() + timeout end_time = perf_counter() + timeout
while perf_counter() < end_time: 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 return True
sleep(.5) 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: 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() m.cancel()
return False return False
else: else:
@ -285,15 +368,14 @@ class TabWaiter(BaseWaiter):
def alert_closed(self): def alert_closed(self):
"""等待弹出框关闭""" """等待弹出框关闭"""
while not self._driver.states.has_alert: while not self._owner.states.has_alert:
sleep(.2) sleep(.2)
while self._driver.states.has_alert: while self._owner.states.has_alert:
sleep(.2) sleep(.2)
class PageWaiter(TabWaiter): class PageWaiter(TabWaiter):
def __init__(self, page): """ChromiumPage和MixPage的等待对象"""
super().__init__(page)
def new_tab(self, timeout=None, raise_err=None): def new_tab(self, timeout=None, raise_err=None):
"""等待新标签页出现 """等待新标签页出现
@ -301,18 +383,7 @@ class PageWaiter(TabWaiter):
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 等到新标签页返回其id否则返回False :return: 等到新标签页返回其id否则返回False
""" """
timeout = timeout if timeout is not None else self._driver.timeout return self._owner.browser.wait.new_tab(timeout=timeout, raise_err=raise_err)
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
def all_downloads_done(self, timeout=None, cancel_if_timeout=True): def all_downloads_done(self, timeout=None, cancel_if_timeout=True):
"""等待所有浏览器下载任务结束 """等待所有浏览器下载任务结束
@ -320,45 +391,26 @@ class PageWaiter(TabWaiter):
:param cancel_if_timeout: 超时时是否取消剩余任务 :param cancel_if_timeout: 超时时是否取消剩余任务
:return: 是否等待成功 :return: 是否等待成功
""" """
if not self._driver.browser._dl_mgr._running: return self._owner.browser.wait.all_downloads_done(timeout=timeout, cancel_if_timeout=cancel_if_timeout)
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
class ElementWaiter(OriginWaiter): class ElementWaiter(OriginWaiter):
"""等待元素在dom中某种状态如删除、显示、隐藏""" """等待元素在dom中某种状态如删除、显示、隐藏"""
def __init__(self, owner, ele): def __init__(self, owner):
"""等待元素在dom中某种状态如删除、显示、隐藏 super().__init__(owner)
:param owner: 元素所在页面 self._ele = owner
:param ele: 要等待的元素
""" @property
self._owner = owner def _timeout(self):
self._ele = ele """返回超时设置"""
return self._ele.owner.timeout
def deleted(self, timeout=None, raise_err=None): def deleted(self, timeout=None, raise_err=None):
"""等待元素从dom删除 """等待元素从dom删除
:param timeout: 超时时间为None使用元素所在页面timeout属性 :param timeout: 超时时间为None使用元素所在页面timeout属性
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :return: 成功返回元素对象失败返回False
""" """
return self._wait_state('is_alive', False, timeout, raise_err, err_text='等待元素被删除失败。') return self._wait_state('is_alive', False, timeout, raise_err, err_text='等待元素被删除失败。')
@ -366,7 +418,7 @@ class ElementWaiter(OriginWaiter):
"""等待元素从dom显示 """等待元素从dom显示
:param timeout: 超时时间为None使用元素所在页面timeout属性 :param timeout: 超时时间为None使用元素所在页面timeout属性
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :return: 成功返回元素对象失败返回False
""" """
return self._wait_state('is_displayed', True, timeout, raise_err, err_text='等待元素显示失败。') return self._wait_state('is_displayed', True, timeout, raise_err, err_text='等待元素显示失败。')
@ -374,7 +426,7 @@ class ElementWaiter(OriginWaiter):
"""等待元素从dom隐藏 """等待元素从dom隐藏
:param timeout: 超时时间为None使用元素所在页面timeout属性 :param timeout: 超时时间为None使用元素所在页面timeout属性
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :return: 成功返回元素对象失败返回False
""" """
return self._wait_state('is_displayed', False, timeout, raise_err, err_text='等待元素隐藏失败。') return self._wait_state('is_displayed', False, timeout, raise_err, err_text='等待元素隐藏失败。')
@ -390,7 +442,7 @@ class ElementWaiter(OriginWaiter):
"""等待当前元素不被遮盖 """等待当前元素不被遮盖
:param timeout: 超时时间为None使用元素所在页面timeout属性 :param timeout: 超时时间为None使用元素所在页面timeout属性
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :return: 成功返回元素对象失败返回False
""" """
return self._wait_state('is_covered', False, timeout, raise_err, err_text='等待元素不被覆盖失败。') return self._wait_state('is_covered', False, timeout, raise_err, err_text='等待元素不被覆盖失败。')
@ -398,7 +450,7 @@ class ElementWaiter(OriginWaiter):
"""等待当前元素变成可用 """等待当前元素变成可用
:param timeout: 超时时间为None使用元素所在页面timeout属性 :param timeout: 超时时间为None使用元素所在页面timeout属性
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :return: 成功返回元素对象失败返回False
""" """
return self._wait_state('is_enabled', True, timeout, raise_err, err_text='等待元素变成可用失败。') return self._wait_state('is_enabled', True, timeout, raise_err, err_text='等待元素变成可用失败。')
@ -406,7 +458,7 @@ class ElementWaiter(OriginWaiter):
"""等待当前元素变成不可用 """等待当前元素变成不可用
:param timeout: 超时时间为None使用元素所在页面timeout属性 :param timeout: 超时时间为None使用元素所在页面timeout属性
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :return: 成功返回元素对象失败返回False
""" """
return self._wait_state('is_enabled', False, timeout, raise_err, err_text='等待元素变成不可用失败。') return self._wait_state('is_enabled', False, timeout, raise_err, err_text='等待元素变成不可用失败。')
@ -414,14 +466,17 @@ class ElementWaiter(OriginWaiter):
"""等待当前元素变成不可用或从DOM移除 """等待当前元素变成不可用或从DOM移除
:param timeout: 超时时间为None使用元素所在页面timeout属性 :param timeout: 超时时间为None使用元素所在页面timeout属性
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :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: if timeout is None:
timeout = self._owner.timeout timeout = self._timeout
end_time = perf_counter() + timeout end_time = perf_counter() + timeout
while perf_counter() < end_time: while perf_counter() < end_time:
if not self._ele.states.is_enabled or not self._ele.states.is_alive: if not self._ele.states.is_enabled or not self._ele.states.is_alive:
return True return self._ele
sleep(.05) sleep(.05)
if raise_err is True or Settings.raise_when_wait_failed is True: 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 timeout: 超时时间为None使用元素所在页面timeout属性
:param gap: 检测间隔时间 :param gap: 检测间隔时间
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :return: 成功返回元素对象失败返回False
""" """
if timeout is None: if timeout is None:
timeout = self._owner.timeout timeout = self._timeout
if timeout <= 0:
timeout = .1
end_time = perf_counter() + timeout end_time = perf_counter() + timeout
while perf_counter() < end_time: while perf_counter() < end_time:
try: try:
@ -453,7 +511,7 @@ class ElementWaiter(OriginWaiter):
while perf_counter() < end_time: while perf_counter() < end_time:
sleep(gap) sleep(gap)
if self._ele.rect.size == size and self._ele.rect.location == location: if self._ele.rect.size == size and self._ele.rect.location == location:
return True return self._ele
size = self._ele.rect.size size = self._ele.rect.size
location = self._ele.rect.location location = self._ele.rect.location
@ -467,11 +525,12 @@ class ElementWaiter(OriginWaiter):
:param wait_moved: 是否等待元素运动结束 :param wait_moved: 是否等待元素运动结束
:param timeout: 超时时间为None使用元素所在页面timeout属性 :param timeout: 超时时间为None使用元素所在页面timeout属性
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功 :return: 成功返回元素对象失败返回False
""" """
timeout = timeout if timeout is not None else self._timeout
t1 = perf_counter() t1 = perf_counter()
r = self._wait_state('is_clickable', True, timeout, raise_err, err_text='等待元素可点击失败(等{}秒)。') 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: if raise_err and not r:
raise WaitTimeoutError(f'等待元素可点击失败(等{timeout}秒)。') raise WaitTimeoutError(f'等待元素可点击失败(等{timeout}秒)。')
return r return r
@ -491,19 +550,19 @@ class ElementWaiter(OriginWaiter):
:param timeout: 超时时间为None使用元素所在页面timeout属性 :param timeout: 超时时间为None使用元素所在页面timeout属性
:param raise_err: 等待失败时是否报错为None时根据Settings设置 :param raise_err: 等待失败时是否报错为None时根据Settings设置
:param err_text: 抛出错误时显示的信息 :param err_text: 抛出错误时显示的信息
:return: 是否等待成功 :return: 成功返回元素对象失败返回False
""" """
a = self._ele.states.__getattribute__(attr) a = self._ele.states.__getattribute__(attr)
if (a and mode) or (not a and not mode): 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: if timeout is None:
timeout = self._owner.timeout timeout = self._timeout
end_time = perf_counter() + timeout end_time = perf_counter() + timeout
while perf_counter() < end_time: while perf_counter() < end_time:
a = self._ele.states.__getattribute__(attr) a = self._ele.states.__getattribute__(attr)
if (a and mode) or (not a and not mode): 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) sleep(.05)
err_text = err_text or '等待元素状态改变失败(等待{}秒)。' err_text = err_text or '等待元素状态改变失败(等待{}秒)。'
@ -514,9 +573,11 @@ class ElementWaiter(OriginWaiter):
class FrameWaiter(BaseWaiter, ElementWaiter): class FrameWaiter(BaseWaiter, ElementWaiter):
def __init__(self, frame): def __init__(self, owner):
""" super().__init__(owner)
:param frame: ChromiumFrame对象 self._ele = owner.frame_ele
"""
super().__init__(frame) @property
super(BaseWaiter, self).__init__(frame, frame.frame_ele) def _timeout(self):
"""返回超时设置"""
return self._owner.timeout

View File

@ -5,24 +5,42 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from typing import Union, Tuple, Literal, List from typing import Union, Tuple, List
from .downloader import DownloadMission from .downloader import DownloadMission
from .._base.browser import Chromium
from .._elements.chromium_element import ChromiumElement from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_base import ChromiumBase from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_frame import ChromiumFrame from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage from .._pages.chromium_page import ChromiumPage
from .._pages.mix_page import MixPage
from .._pages.tabs import ChromiumTab, MixTab
class OriginWaiter(object): 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): class BaseWaiter(OriginWaiter):
def __init__(self, page: ChromiumBase): _owner: ChromiumBase = ...
self._driver: ChromiumBase = ...
def __call__(self, second: float, scope: float = None) -> None: ...
def ele_deleted(self, def ele_deleted(self,
loc_or_ele: Union[str, tuple, ChromiumElement], loc_or_ele: Union[str, tuple, ChromiumElement],
@ -64,58 +82,94 @@ class BaseWaiter(OriginWaiter):
class TabWaiter(BaseWaiter): 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 downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ...
def alert_closed(self) -> None: ... 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): 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 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 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): class ElementWaiter(OriginWaiter):
def __init__(self, owner: ChromiumBase, ele: ChromiumElement): _owner: ChromiumElement = ...
self._ele: ChromiumElement = ... _ele: ChromiumElement = ...
self._owner: ChromiumBase = ...
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, def has_rect(self,
timeout: float = None, 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 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, def _wait_state(self,
attr: str, attr: str,
mode: bool = False, mode: bool = False,
timeout: float = None, timeout: float = None,
raise_err: bool = None, raise_err: bool = None,
err_text: str = None) -> bool: ... err_text: str = None) -> Union[ChromiumElement, False]: ...
class FrameWaiter(BaseWaiter, ElementWaiter): 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]: ...

View File

@ -5,6 +5,7 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved. @Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause. @License : BSD 3-Clause.
""" """
from ._base.browser import Chromium
from ._elements.session_element import make_session_ele from ._elements.session_element import make_session_ele
from ._functions.by import By from ._functions.by import By
from ._functions.elements import get_eles from ._functions.elements import get_eles
@ -12,7 +13,7 @@ from ._functions.keys import Keys
from ._functions.settings import Settings from ._functions.settings import Settings
from ._functions.tools import wait_until, configs_to_here from ._functions.tools import wait_until, configs_to_here
from ._functions.web import get_blob, tree from ._functions.web import get_blob, tree
from ._pages.chromium_page import ChromiumPage
from ._units.actions import Actions from ._units.actions import Actions
__all__ = ['make_session_ele', 'Actions', 'Keys', 'By', 'Settings', 'wait_until', 'configs_to_here', 'get_blob', __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): def from_selenium(driver):
"""从selenium的WebDriver对象生成ChromiumPage对象""" """从selenium的WebDriver对象生成Chromium对象"""
address, port = driver.caps.get('goog:chromeOptions', {}).get('debuggerAddress', ':').split(':') address, port = driver.caps.get('goog:chromeOptions', {}).get('debuggerAddress', ':').split(':')
if not address: if not address:
raise RuntimeError('获取失败。') raise RuntimeError('获取失败。')
return ChromiumPage(f'{address}:{port}') return Chromium(f'{address}:{port}')
def from_playwright(page_or_browser): def from_playwright(page_or_browser):
"""从playwright的Page或Browser对象生成ChromiumPage对象""" """从playwright的Page或Browser对象生成Chromium对象"""
if hasattr(page_or_browser, 'context'): if hasattr(page_or_browser, 'context'):
page_or_browser = page_or_browser.context.browser page_or_browser = page_or_browser.context.browser
try: try:
@ -49,4 +50,4 @@ def from_playwright(page_or_browser):
break break
else: else:
raise RuntimeError('获取失败。') raise RuntimeError('获取失败。')
return ChromiumPage(f'127.0.0.1:{port}') return Chromium(f'127.0.0.1:{port}')

View File

@ -9,7 +9,7 @@ from ._elements.chromium_element import ChromiumElement, ShadowRoot
from ._elements.none_element import NoneElement from ._elements.none_element import NoneElement
from ._elements.session_element import SessionElement from ._elements.session_element import SessionElement
from ._pages.chromium_frame import ChromiumFrame 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', __all__ = ['ChromiumElement', 'ShadowRoot', 'NoneElement', 'SessionElement', 'ChromiumFrame', 'ChromiumTab',
'WebPageTab'] 'MixTab']