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

View File

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

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

View File

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

View File

@ -7,54 +7,100 @@
"""
from pathlib import Path
from shutil import rmtree
from time import perf_counter, sleep
from threading import Lock
from time import sleep, perf_counter
from requests import Session
from websocket import WebSocketBadStatusException
from .driver import BrowserDriver, Driver
from .._configs.chromium_options import ChromiumOptions
from .._configs.session_options import SessionOptions
from .._functions.browser import connect_browser
from .._functions.cookies import CookiesList
from .._functions.settings import Settings
from .._functions.tools import PortFinder
from .._functions.tools import raise_error
from .._pages.chromium_base import Timeout
from .._pages.tabs import ChromiumTab, MixTab
from .._units.downloader import DownloadManager
from .._units.setter import BrowserSetter
from .._units.waiter import BrowserWaiter
from ..errors import BrowserConnectError, CDPError
from ..errors import PageDisconnectedError
__ERROR__ = 'error'
class Browser(object):
BROWSERS = {}
class Chromium(object):
_BROWSERS = {}
_lock = Lock()
def __new__(cls, address, browser_id, page):
def __new__(cls, addr_or_opts=None, session_options=None):
"""
:param address: 浏览器地址
:param browser_id: 浏览器id
:param page: ChromiumPage对象
:param addr_or_opts: 浏览器地址:端口ChromiumOptions对象或端口数字int
:param session_options: 使用双模Tab时使用的默认Session配置为True使用ini文件配置
"""
if browser_id in cls.BROWSERS:
return cls.BROWSERS[browser_id]
return object.__new__(cls)
opt = handle_options(addr_or_opts)
is_headless, browser_id, is_exists = run_browser(opt)
with cls._lock:
if browser_id in cls._BROWSERS:
r = cls._BROWSERS[browser_id]
while not hasattr(r, '_driver'):
sleep(.1)
return r
r = object.__new__(cls)
r._chromium_options = opt
r.is_headless = is_headless
r._is_exists = is_exists
r.id = browser_id
cls._BROWSERS[browser_id] = r
return r
def __init__(self, address, browser_id, page):
def __init__(self, addr_or_opts=None, session_options=None):
"""
:param address: 浏览器地址
:param browser_id: 浏览器id
:param page: ChromiumPage对象
:param addr_or_opts: 浏览器地址:端口ChromiumOptions对象或端口数字int
:param session_options: 使用双模Tab时使用的默认Session配置为True使用ini文件配置
"""
if hasattr(self, '_created'):
return
self._created = True
Browser.BROWSERS[browser_id] = self
self.page = page
self.address = address
self._driver = BrowserDriver(browser_id, 'browser', address, self)
self.id = browser_id
self._type = 'Chromium'
self._frames = {}
self._drivers = {}
self._all_drivers = {}
self._connected = False
self._set = None
self._wait = None
self._timeouts = Timeout(**self._chromium_options.timeouts)
self._load_mode = self._chromium_options.load_mode
self._download_path = str(Path(self._chromium_options.download_path).absolute())
self.retry_times = self._chromium_options.retry_times
self.retry_interval = self._chromium_options.retry_interval
self.address = self._chromium_options.address
self._driver = BrowserDriver(self.id, 'browser', self.address, self)
if self.is_headless != self._chromium_options.is_headless or (
self._is_exists and self._chromium_options._new_env):
self.quit(3, True)
connect_browser(self._chromium_options)
s = Session()
s.trust_env = False
ws = s.get(f'http://{self._chromium_options.address}/json/version', headers={'Connection': 'close'})
self.id = ws.json()['webSocketDebuggerUrl'].split('/')[-1]
self._driver = BrowserDriver(self.id, 'browser', self.address, self)
ws.close()
s.close()
self._frames = {}
self._drivers = {}
self._all_drivers = {}
self.version = self._run_cdp('Browser.getVersion')['product']
self._process_id = None
try:
r = self.run_cdp('SystemInfo.getProcessInfo')
r = self._run_cdp('SystemInfo.getProcessInfo')
for i in r.get('processInfo', []):
if i['type'] == 'browser':
self._process_id = i['id']
@ -62,9 +108,348 @@ class Browser(object):
except:
pass
self.run_cdp('Target.setDiscoverTargets', discover=True)
self._run_cdp('Target.setDiscoverTargets', discover=True)
self._driver.set_callback('Target.targetDestroyed', self._onTargetDestroyed)
self._driver.set_callback('Target.targetCreated', self._onTargetCreated)
self._dl_mgr = DownloadManager(self)
self._session_options = SessionOptions() if session_options is True else session_options
@property
def user_data_path(self):
"""返回用户文件夹路径"""
return self._chromium_options.user_data_path
@property
def process_id(self):
"""返回浏览器进程id"""
return self._process_id
@property
def timeout(self):
"""返回timeouts设置"""
return self._timeouts.base
@property
def timeouts(self):
"""返回timeouts设置"""
return self._timeouts
@property
def load_mode(self):
"""返回加载模式"""
return self._load_mode
@property
def download_path(self):
"""返回默认下载路径"""
return self._download_path
@property
def set(self):
if self._set is None:
self._set = BrowserSetter(self)
return self._set
@property
def wait(self):
"""返回用于等待的对象"""
if self._wait is None:
self._wait = BrowserWaiter(self)
return self._wait
@property
def tabs_count(self):
"""返回标签页数量"""
j = self._run_cdp('Target.getTargets')['targetInfos'] # 不要改用get避免卡死
return len([i for i in j if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')])
@property
def tab_ids(self):
"""返回所有标签页id组成的列表"""
j = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp因为顺序不对
return [i['id'] for i in j if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')]
@property
def latest_tab(self):
"""返回最新的标签页,最新标签页指最后创建或最后被激活的
当Settings.singleton_tab_obj==True时返回Tab对象否则返回tab id"""
return self.get_tab(self.tab_ids[0], as_id=not Settings.singleton_tab_obj)
def cookies(self, all_info=False):
"""以list格式返回所有域名的cookies
:param all_info: 是否返回所有内容False则只返回name, value, domain
:return: cookies组成的列表
"""
cks = self._run_cdp(f'Storage.getCookies')['cookies']
r = cks if all_info else [{'name': c['name'], 'value': c['value'], 'domain': c['domain']} for c in cks]
return CookiesList(r)
def new_tab(self, url=None, new_window=False, background=False, new_context=False):
"""新建一个标签页
:param url: 新标签页跳转到的网址
:param new_window: 是否在新窗口打开标签页
:param background: 是否不激活新标签页如new_window为True则无效
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
return self._new_tab(ChromiumTab, url=url, new_window=new_window,
background=background, new_context=new_context)
def new_mix_tab(self, url=None, new_window=False, background=False, new_context=False):
"""新建一个标签页
:param url: 新标签页跳转到的网址
:param new_window: 是否在新窗口打开标签页
:param background: 是否不激活新标签页如new_window为True则无效
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
return self._new_tab(MixTab, url=url, new_window=new_window,
background=background, new_context=new_context)
def _new_tab(self, obj, url=None, new_window=False, background=False, new_context=False):
"""新建一个标签页
:param obj: 要创建的Tab类型
:param url: 新标签页跳转到的网址
:param new_window: 是否在新窗口打开标签页
:param background: 是否不激活新标签页如new_window为True则无效
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
tab = None
if new_context:
tab = self._run_cdp('Target.createBrowserContext')['browserContextId']
kwargs = {'url': ''}
if new_window:
kwargs['newWindow'] = True
if background:
kwargs['background'] = True
if tab:
kwargs['browserContextId'] = tab
try:
tab = self._run_cdp('Target.createTarget', **kwargs)['targetId']
except CDPError:
data = ('a', {'href': url or 'https://#', 'target': '_new' if new_window else '_blank'})
tab = self.get_mix_tab() if isinstance(obj, MixTab) else self.get_tab()
return tab.add_ele(data).click.for_new_tab(by_js=True)
while tab not in self._drivers:
sleep(.1)
tab = obj(self, tab)
if url:
tab.get(url)
return tab
def get_tab(self, id_or_num=None, title=None, url=None, tab_type='page', as_id=False):
"""获取一个标签页对象id_or_num不为None时后面几个参数无效
:param id_or_num: 要获取的标签页id或序号序号从1开始可传入负数获取倒数第几个不是视觉排列顺序而是激活顺序
:param title: 要匹配title的文本模糊匹配为None则匹配所有
:param url: 要匹配url的文本模糊匹配为None则匹配所有
:param tab_type: tab类型可用列表输入多个 'page', 'iframe' 为None则匹配所有
:param as_id: 是否返回标签页id而不是标签页对象
:return: Tab对象
"""
return self._get_tab(id_or_num=id_or_num, title=title, url=url, tab_type=tab_type, as_id=as_id)
def get_tabs(self, title=None, url=None, tab_type='page', as_id=False):
"""查找符合条件的tab返回它们组成的列表title和url是与关系
:param title: 要匹配title的文本
:param url: 要匹配url的文本
:param tab_type: tab类型可用列表输入多个
:param as_id: 是否返回标签页id而不是标签页对象
:return: Tab对象列表
"""
return self._get_tabs(title=title, url=url, tab_type=tab_type, as_id=as_id)
def get_mix_tab(self, id_or_num=None, title=None, url=None, tab_type='page', as_id=False):
"""获取一个标签页对象id_or_num不为None时后面几个参数无效
:param id_or_num: 要获取的标签页id或序号序号从1开始可传入负数获取倒数第几个不是视觉排列顺序而是激活顺序
:param title: 要匹配title的文本模糊匹配为None则匹配所有
:param url: 要匹配url的文本模糊匹配为None则匹配所有
:param tab_type: tab类型可用列表输入多个 'page', 'iframe' 为None则匹配所有
:param as_id: 是否返回标签页id而不是标签页对象
:return: Tab对象
"""
return self._get_tab(id_or_num=id_or_num, title=title, url=url, tab_type=tab_type, mix=True, as_id=as_id)
def get_mix_tabs(self, title=None, url=None, tab_type='page', as_id=False):
"""查找符合条件的tab返回它们组成的列表title和url是与关系
:param title: 要匹配title的文本
:param url: 要匹配url的文本
:param tab_type: tab类型可用列表输入多个
:param as_id: 是否返回标签页id而不是标签页对象
:return: Tab对象列表
"""
return self._get_tabs(title=title, url=url, tab_type=tab_type, mix=True, as_id=as_id)
def _get_tab(self, id_or_num=None, title=None, url=None, tab_type='page', mix=False, as_id=False):
"""获取一个标签页对象id_or_num不为None时后面几个参数无效
:param id_or_num: 要获取的标签页id或序号序号从1开始可传入负数获取倒数第几个不是视觉排列顺序而是激活顺序
:param title: 要匹配title的文本模糊匹配为None则匹配所有
:param url: 要匹配url的文本模糊匹配为None则匹配所有
:param tab_type: tab类型可用列表输入多个 'page', 'iframe' 为None则匹配所有
:param mix: 是否返回可切换模式的Tab对象
:param as_id: 是否返回标签页id而不是标签页对象mix=False时无效
:return: Tab对象
"""
if id_or_num is not None:
if isinstance(id_or_num, str):
id_or_num = id_or_num
elif isinstance(id_or_num, int):
id_or_num = self.tab_ids[id_or_num - 1 if id_or_num > 0 else id_or_num]
elif isinstance(id_or_num, ChromiumTab):
return id_or_num.tab_id if as_id else ChromiumTab(self, id_or_num.tab_id)
elif title == url is None and tab_type == 'page':
id_or_num = self.tab_ids[0]
else:
tabs = self._get_tabs(title=title, url=url, tab_type=tab_type, as_id=True)
if tabs:
id_or_num = tabs[0]
else:
return None
if as_id:
return id_or_num
with self._lock:
return MixTab(self, id_or_num) if mix else ChromiumTab(self, id_or_num)
def _get_tabs(self, title=None, url=None, tab_type='page', mix=False, as_id=False):
"""查找符合条件的tab返回它们组成的列表title和url是与关系
:param title: 要匹配title的文本
:param url: 要匹配url的文本
:param tab_type: tab类型可用列表输入多个
:param mix: 是否返回可切换模式的Tab对象
:param as_id: 是否返回标签页id而不是标签页对象mix=False时无效
:return: Tab对象列表
"""
tabs = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp
if isinstance(tab_type, str):
tab_type = {tab_type}
elif isinstance(tab_type, (list, tuple, set)):
tab_type = set(tab_type)
elif tab_type is not None:
raise TypeError('tab_type只能是set、list、tuple、str、None。')
tabs = [i for i in tabs if ((title is None or title in i['title']) and (url is None or url in i['url'])
and (tab_type is None or i['type'] in tab_type))]
if as_id:
return [tab['id'] for tab in tabs]
with self._lock:
if mix:
return [MixTab(self, tab['id']) for tab in tabs]
else:
return [ChromiumTab(self, tab['id']) for tab in tabs]
def close_tabs(self, tabs_or_ids=None, others=False):
"""关闭传入的标签页,默认关闭当前页。可传入多个
:param tabs_or_ids: 要关闭的标签页对象或id可传入列表或元组为None时关闭最后操作的
:param others: 是否关闭指定标签页之外的
:return: None
"""
all_tabs = set(self.tab_ids)
if isinstance(tabs_or_ids, str):
tabs = {tabs_or_ids}
elif isinstance(tabs_or_ids, ChromiumTab):
tabs = {tabs_or_ids.tab_id}
elif tabs_or_ids is None:
tabs = {self.tab_ids[0]}
elif isinstance(tabs_or_ids, (list, tuple)):
tabs = set(i.tab_id if isinstance(i, ChromiumTab) else i for i in tabs_or_ids)
else:
raise TypeError('tabs_or_ids参数只能传入标签页对象或id。')
if others:
tabs = all_tabs - tabs
end_len = len(set(all_tabs) - set(tabs))
if end_len <= 0:
self.quit()
return
for tab in tabs:
self._onTargetDestroyed(targetId=tab)
self._driver.run('Target.closeTarget', targetId=tab)
sleep(.2)
end_time = perf_counter() + 3
while self.tabs_count != end_len and perf_counter() < end_time:
sleep(.1)
def activate_tab(self, id_ind_tab):
"""使标签页变为活动状态
:param id_ind_tab: 标签页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):
"""新建并返回指定tab id的Driver
@ -95,8 +480,7 @@ class Browser(object):
def _onTargetDestroyed(self, **kwargs):
"""标签页关闭时执行"""
tab_id = kwargs['targetId']
if hasattr(self, '_dl_mgr'):
self._dl_mgr.clear_tab_info(tab_id)
self._dl_mgr.clear_tab_info(tab_id)
for key in [k for k, i in self._frames.items() if i == tab_id]:
self._frames.pop(key, None)
for d in self._all_drivers.get(tab_id, tuple()):
@ -104,13 +488,7 @@ class Browser(object):
self._drivers.pop(tab_id, None)
self._all_drivers.pop(tab_id, None)
def connect_to_page(self):
"""执行与page相关的逻辑"""
if not self._connected:
self._dl_mgr = DownloadManager(self)
self._connected = True
def run_cdp(self, cmd, **cmd_args):
def _run_cdp(self, cmd, **cmd_args):
"""执行Chrome DevTools Protocol语句
:param cmd: 协议项目
:param cmd_args: 参数
@ -120,166 +498,10 @@ class Browser(object):
r = self._driver.run(cmd, **cmd_args)
return r if __ERROR__ not in r else raise_error(r, ignore)
@property
def driver(self):
return self._driver
@property
def tabs_count(self):
"""返回标签页数量"""
j = self.run_cdp('Target.getTargets')['targetInfos'] # 不要改用get避免卡死
return len([i for i in j if i['type'] in ('page', 'webview') and not i['url'].startswith('devtools://')])
@property
def tab_ids(self):
"""返回所有标签页id组成的列表"""
j = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp因为顺序不对
return [i['id'] for i in j if i['type'] in ('page', 'webview')
and not i['url'].startswith('devtools://')]
@property
def process_id(self):
"""返回浏览器进程id"""
return self._process_id
def find_tabs(self, title=None, url=None, tab_type=None):
"""查找符合条件的tab返回它们组成的列表title和url是与关系
:param title: 要匹配title的文本
:param url: 要匹配url的文本
:param tab_type: tab类型可用列表输入多个
:return: dict格式的tab信息列表列表
"""
tabs = self._driver.get(f'http://{self.address}/json').json() # 不要改用cdp
if isinstance(tab_type, str):
tab_type = {tab_type}
elif isinstance(tab_type, (list, tuple, set)):
tab_type = set(tab_type)
elif tab_type is not None:
raise TypeError('tab_type只能是set、list、tuple、str、None。')
return [i for i in tabs if ((title is None or title in i['title']) and (url is None or url in i['url'])
and (tab_type is None or i['type'] in tab_type))]
def close_tab(self, tab_id):
"""关闭标签页
:param tab_id: 标签页id
:return: None
"""
self._onTargetDestroyed(targetId=tab_id)
self.driver.run('Target.closeTarget', targetId=tab_id)
def stop_driver(self, driver):
"""停止一个Driver
:param driver: Driver对象
:return: None
"""
driver.stop()
self._all_drivers.get(driver.id, set()).discard(driver)
def activate_tab(self, tab_id):
"""使标签页变为活动状态
:param tab_id: 标签页id
:return: None
"""
self.run_cdp('Target.activateTarget', targetId=tab_id)
def get_window_bounds(self, tab_id=None):
"""返回浏览器窗口位置和大小信息
:param tab_id: 标签页id
:return: 窗口大小字典
"""
return self.run_cdp('Browser.getWindowForTarget', targetId=tab_id or self.id)['bounds']
def new_tab(self, new_window=False, background=False, new_context=False):
"""新建一个标签页
:param new_window: 是否在新窗口打开标签页
:param background: 是否不激活新标签页如new_window为True则无效
:param new_context: 是否创建新的上下文
:return: 新标签页id
"""
bid = None
if new_context:
bid = self.run_cdp('Target.createBrowserContext')['browserContextId']
kwargs = {'url': ''}
if new_window:
kwargs['newWindow'] = True
if background:
kwargs['background'] = True
if bid:
kwargs['browserContextId'] = bid
tid = self.run_cdp('Target.createTarget', **kwargs)['targetId']
while tid not in self._drivers:
sleep(.1)
return tid
def reconnect(self):
"""断开重连"""
self._driver.stop()
BrowserDriver.BROWSERS.pop(self.id)
self._driver = BrowserDriver(self.id, 'browser', self.address, self)
self.run_cdp('Target.setDiscoverTargets', discover=True)
self._driver.set_callback('Target.targetDestroyed', self._onTargetDestroyed)
self._driver.set_callback('Target.targetCreated', self._onTargetCreated)
def quit(self, timeout=5, force=False):
"""关闭浏览器
:param timeout: 等待浏览器关闭超时时间
:param force: 是否立刻强制终止进程
:return: None
"""
try:
self.run_cdp('Browser.close')
except PageDisconnectedError:
pass
self.driver.stop()
drivers = list(self._all_drivers.values())
for tab in drivers:
for driver in tab:
driver.stop()
if not force:
return
try:
pids = [pid['id'] for pid in self.run_cdp('SystemInfo.getProcessInfo')['processInfo']]
except:
return
from psutil import Process
for pid in pids:
try:
Process(pid).kill()
except:
pass
from os import popen
from platform import system
end_time = perf_counter() + timeout
while perf_counter() < end_time:
ok = True
for pid in pids:
txt = f'tasklist | findstr {pid}' if system().lower() == 'windows' else f'ps -ef | grep {pid}'
p = popen(txt)
sleep(.05)
try:
if f' {pid} ' in p.read():
ok = False
break
except TypeError:
pass
if ok:
break
def _on_disconnect(self):
self.page._on_disconnect()
Browser.BROWSERS.pop(self.id, None)
if self.page._chromium_options.is_auto_port and self.page._chromium_options.user_data_path:
path = Path(self.page._chromium_options.user_data_path)
Chromium._BROWSERS.pop(self.id, None)
if self._chromium_options.is_auto_port and self._chromium_options.user_data_path:
path = Path(self._chromium_options.user_data_path)
end_time = perf_counter() + 7
while perf_counter() < end_time:
if not path.exists():
@ -290,3 +512,59 @@ class Browser(object):
except (PermissionError, FileNotFoundError, OSError):
pass
sleep(.03)
def handle_options(addr_or_opts):
"""设置浏览器启动属性
:param addr_or_opts: 'ip:port'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.
@License : BSD 3-Clause.
"""
from typing import List, Optional, Union, Set, Dict
from threading import Lock
from typing import List, Optional, Set, Dict, Union, Tuple
from .driver import BrowserDriver, Driver
from .._pages.chromium_page import ChromiumPage
from .._configs.chromium_options import ChromiumOptions
from .._configs.session_options import SessionOptions
from .._functions.cookies import CookiesList
from .._pages.chromium_base import Timeout
from .._pages.tabs import ChromiumTab, MixTab
from .._units.downloader import DownloadManager
from .._units.setter import BrowserSetter
from .._units.waiter import BrowserWaiter
class Browser(object):
BROWSERS: dict = ...
page: ChromiumPage = ...
_driver: BrowserDriver = ...
class Chromium(object):
id: str = ...
address: str = ...
version: str = ...
retry_times: int = ...
retry_interval: float = ...
is_headless: bool = ...
_BROWSERS: dict = ...
_chromium_options: ChromiumOptions = ...
_session_options: SessionOptions = ...
_driver: BrowserDriver = ...
_frames: dict = ...
_drivers: Dict[str, Driver] = ...
_all_drivers: Dict[str, Set[Driver]] = ...
_process_id: Optional[int] = ...
_dl_mgr: DownloadManager = ...
_connected: bool = ...
_lock: Lock = ...
def __new__(cls, address: str, browser_id: str, page: ChromiumPage): ...
_set: Optional[BrowserSetter] = ...
_wait: Optional[BrowserWaiter] = ...
_timeouts: Timeout = ...
_load_mode: str = ...
_download_path: str = ...
_is_exists: bool = ...
def __init__(self, address: str, browser_id: str, page: ChromiumPage): ...
def __new__(cls,
addr_or_opts: Union[str, int, ChromiumOptions] = None,
session_options: Optional[SessionOptions] = None): ...
def __init__(self, addr_or_opts: Union[str, int, ChromiumOptions] = None,
session_options: Optional[SessionOptions] = None): ...
def _get_driver(self, tab_id: str, owner=None) -> Driver: ...
def run_cdp(self, cmd, **cmd_args) -> dict: ...
def _run_cdp(self, cmd, **cmd_args) -> dict: ...
@property
def driver(self) -> BrowserDriver: ...
def user_data_path(self) -> str: ...
@property
def process_id(self) -> Optional[int]: ...
@property
def timeout(self) -> float: ...
@property
def timeouts(self) -> Timeout: ...
@property
def load_mode(self) -> str: ...
@property
def download_path(self) -> str: ...
@property
def set(self) -> BrowserSetter: ...
@property
def wait(self) -> BrowserWaiter: ...
@property
def tabs_count(self) -> int: ...
@ -43,25 +87,77 @@ class Browser(object):
def tab_ids(self) -> List[str]: ...
@property
def process_id(self) -> Optional[int]: ...
def latest_tab(self) -> Union[ChromiumTab, str]: ...
def find_tabs(self, title: str = None, url: str = None,
tab_type: Union[str, list, tuple] = None) -> List[dict]: ...
def cookies(self, all_info: bool = False) -> CookiesList: ...
def close_tab(self, tab_id: str) -> None: ...
def close_tabs(self,
tabs_or_ids: Union[str, ChromiumTab, List[Union[str, ChromiumTab]],
Tuple[Union[str, ChromiumTab]]] = None,
others: bool = False) -> None: ...
def stop_driver(self, driver: Driver) -> None: ...
def get_tab(self,
id_or_num: Union[str, int] = None,
title: str = None,
url: str = None,
tab_type: str = 'page',
as_id: bool = False) -> Union[ChromiumTab, str]: ...
def activate_tab(self, tab_id: str) -> None: ...
def get_tabs(self,
title: str = None,
url: str = None,
tab_type: str = 'page',
as_id: bool = False) -> List[ChromiumTab, str]: ...
def get_window_bounds(self, tab_id: str = None) -> dict: ...
def get_mix_tab(self,
id_or_num: Union[str, int] = None,
title: str = None,
url: str = None,
tab_type: str = 'page') -> Union[MixTab, str]: ...
def new_tab(self, new_window: bool = False, background: bool = False, new_context: bool = False) -> str: ...
def get_mix_tabs(self,
title: str = None,
url: str = None,
tab_type: str = 'page') -> List[MixTab, str]: ...
def _get_tab(self,
id_or_num: Union[str, int] = None,
title: str = None,
url: str = None,
tab_type: str = 'page',
mix: bool = False,
as_id: bool = False) -> Union[ChromiumTab, str]: ...
def _get_tabs(self,
title: str = None,
url: str = None,
tab_type: str = 'page',
mix: bool = False,
as_id: bool = False) -> List[ChromiumTab, str]: ...
def activate_tab(self, id_ind_tab: Union[int, str, ChromiumTab]) -> None: ...
def _new_tab(self,
obj,
url: str = None,
new_window: bool = False,
background: bool = False,
new_context: bool = False) -> Union[ChromiumTab, MixTab]: ...
def new_tab(self,
url: str = None,
new_window: bool = False,
background: bool = False,
new_context: bool = False) -> ChromiumTab: ...
def new_mix_tab(self,
url: str = None,
new_window: bool = False,
background: bool = False,
new_context: bool = False) -> MixTab: ...
def reconnect(self) -> None: ...
def connect_to_page(self) -> None: ...
def _onTargetCreated(self, **kwargs) -> None: ...
def _onTargetDestroyed(self, **kwargs) -> None: ...

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ user = Default
auto_port = False
system_user_path = False
existing_only = False
new_env = False
[session_options]
headers = {'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'connection': 'keep-alive', 'accept-charset': 'GB2312,utf-8;q=0.7,*;q=0.7'}

View File

@ -64,6 +64,7 @@ class OptionsManager(object):
self.set_item('chromium_options', 'auto_port', 'False')
self.set_item('chromium_options', 'system_user_path', 'False')
self.set_item('chromium_options', 'existing_only', 'False')
self.set_item('chromium_options', 'new_env', 'False')
self.set_item('session_options', 'headers', "{'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X "
"10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10."
"1.2 Safari/603.3.8', 'accept': 'text/html,application/xhtml"

View File

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

View File

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

View File

@ -14,8 +14,8 @@ from .._functions.elements import SessionElementsList, ChromiumElementsList
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage
from .._pages.chromium_tab import ChromiumTab
from .._pages.web_page import WebPage
from .._pages.tabs import ChromiumTab
from .._pages.mix_page import MixPage
from .._units.clicker import Clicker
from .._units.rect import ElementRect
from .._units.scroller import ElementScroller
@ -31,9 +31,8 @@ class ChromiumElement(DrissionElement):
def __init__(self, owner: ChromiumBase, node_id: int = None, obj_id: str = None, backend_id: int = None):
self._tag: str = ...
# self.page: Union[ChromiumPage, WebPage] = ...
self.owner: ChromiumBase = ...
self.page: Union[ChromiumPage, WebPage] = ...
self.page: Union[ChromiumPage, MixPage] = ...
self.tab: Union[ChromiumPage, ChromiumTab] = ...
self._node_id: int = ...
self._obj_id: str = ...
@ -169,7 +168,11 @@ class ChromiumElement(DrissionElement):
def east(self, loc_or_pixel: Union[str, int, None] = None, index: int = 1) -> ChromiumElement: ...
def offset(self, offset_x: int, offset_y: int) -> ChromiumElement: ...
def offset(self,
locator: Optional[str] = None,
x: int = None,
y: int = None,
timeout: float = None) -> ChromiumElement: ...
def _get_relative_eles(self,
mode: str = 'north',
@ -183,7 +186,7 @@ class ChromiumElement(DrissionElement):
def select(self) -> SelectElement: ...
@property
def value(self) -> None: ...
def value(self) -> str: ...
def check(self, uncheck: bool = False, by_js: bool = False) -> None: ...
@ -195,6 +198,8 @@ class ChromiumElement(DrissionElement):
def run_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> Any: ...
def _run_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> Any: ...
def run_async_js(self, script: str, *args, as_expr: bool = False) -> None: ...
def ele(self,
@ -208,9 +213,12 @@ class ChromiumElement(DrissionElement):
def s_ele(self,
locator: Union[Tuple[str, str], str] = None,
index: int = 1) -> SessionElement: ...
index: int = 1,
timeout: float = None) -> SessionElement: ...
def s_eles(self, locator: Union[Tuple[str, str], str] = None) -> SessionElementsList: ...
def s_eles(self,
locator: Union[Tuple[str, str], str] = None,
timeout: float = None) -> SessionElementsList: ...
def _find_elements(self,
locator: Union[Tuple[str, str], str],
@ -266,7 +274,6 @@ class ChromiumElement(DrissionElement):
class ShadowRoot(BaseElement):
def __init__(self, parent_ele: ChromiumElement, obj_id: str = None, backend_id: int = None):
# self.page: Union[ChromiumPage, WebPage] = ...
self.owner: ChromiumBase = ...
self.tab: Union[ChromiumPage, ChromiumTab] = ...
self._obj_id: str = ...
@ -298,6 +305,8 @@ class ShadowRoot(BaseElement):
def run_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> Any: ...
def _run_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> Any: ...
def run_async_js(self, script: str, *args, as_expr: bool = False, timeout: float = None) -> None: ...
def parent(self, level_or_loc: Union[str, int] = 1, index: int = 1) -> ChromiumElement: ...
@ -337,9 +346,10 @@ class ShadowRoot(BaseElement):
def s_ele(self,
locator: Union[Tuple[str, str], str] = None,
index: int = 1) -> SessionElement: ...
index: int = 1,
timeout: float = None) -> SessionElement: ...
def s_eles(self, locator: Union[Tuple[str, str], str]) -> SessionElementsList: ...
def s_eles(self, locator: Union[Tuple[str, str], str], timeout: float = None) -> SessionElementsList: ...
def _find_elements(self,
locator: Union[Tuple[str, str], str],
@ -375,7 +385,7 @@ def find_by_css(ele: ChromiumElement,
timeout: float) -> Union[ChromiumElement, List[ChromiumElement],]: ...
def make_chromium_eles(page: Union[ChromiumBase, ChromiumPage, WebPage, ChromiumTab, ChromiumFrame],
def make_chromium_eles(page: Union[ChromiumBase, ChromiumPage, MixPage, ChromiumTab, ChromiumFrame],
_ids: Union[tuple, list, str, int],
index: Optional[int] = 1,
is_obj_id: bool = True,

View File

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

View File

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

View File

@ -22,7 +22,7 @@ def set_prefs(opt: ChromiumOptions) -> None: ...
def set_flags(opt: ChromiumOptions) -> None: ...
def test_connect(ip: str, port: Union[int, str], timeout: float = 30) -> None: ...
def test_connect(ip: str, port: Union[int, str], timeout: float = 30) -> bool: ...
def get_chrome_path(ini_path: str) -> Union[str, None]: ...

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 .locator import is_loc
from .._elements.none_element import NoneElement
@ -402,6 +403,50 @@ def get_eles(locators, owner, any_one=False, first_ele=True, timeout=10):
return res
def get_frame(owner, loc_ind_ele, timeout=None):
"""获取页面中一个frame对象
:param owner: 要在其中查找元素的对象
:param loc_ind_ele: 定位符iframe序号ChromiumFrame对象序号从1开始可传入负数获取倒数第几个
:param timeout: 查找元素超时时间
:return: ChromiumFrame对象
"""
if isinstance(loc_ind_ele, str):
if not is_loc(loc_ind_ele):
xpath = f'xpath://*[(name()="iframe" or name()="frame") and ' \
f'(@name="{loc_ind_ele}" or @id="{loc_ind_ele}")]'
else:
xpath = loc_ind_ele
ele = owner._ele(xpath, timeout=timeout)
if ele and ele._type != 'ChromiumFrame':
raise TypeError('该定位符不是指向frame元素。')
r = ele
elif isinstance(loc_ind_ele, tuple):
ele = owner._ele(loc_ind_ele, timeout=timeout)
if ele and ele._type != 'ChromiumFrame':
raise TypeError('该定位符不是指向frame元素。')
r = ele
elif isinstance(loc_ind_ele, int):
if loc_ind_ele == 0:
loc_ind_ele = 1
elif loc_ind_ele < 0:
loc_ind_ele = f'last()+{loc_ind_ele}+1'
xpath = f'xpath:(//*[name()="frame" or name()="iframe"])[{loc_ind_ele}]'
r = owner._ele(xpath, timeout=timeout)
elif loc_ind_ele._type == 'ChromiumFrame':
r = loc_ind_ele
else:
raise TypeError('必须传入定位符、iframe序号、id、name、ChromiumFrame对象其中之一。')
if isinstance(r, NoneElement):
r.method = 'get_frame()'
r.args = {'loc_ind_ele': loc_ind_ele}
return r
def _get_attr_all(src_list, aim_list, name, value, method, equal=True):
if equal:
for i in src_list:

View File

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

View File

@ -5,6 +5,8 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from platform import system
from ..errors import AlertExistsError
@ -21,18 +23,14 @@ class Keys:
CANCEL = '\ue001' # ^break
HELP = '\ue002'
BACKSPACE = '\ue003'
BACK_SPACE = BACKSPACE
TAB = '\ue004'
CLEAR = '\ue005'
RETURN = '\ue006'
ENTER = '\ue007'
SHIFT = '\ue008'
LEFT_SHIFT = SHIFT
CONTROL = '\ue009'
CTRL = '\ue009'
LEFT_CONTROL = CONTROL
ALT = '\ue00a'
LEFT_ALT = ALT
PAUSE = '\ue00b'
ESCAPE = '\ue00c'
SPACE = '\ue00d'
@ -41,13 +39,9 @@ class Keys:
END = '\ue010'
HOME = '\ue011'
LEFT = '\ue012'
ARROW_LEFT = LEFT
UP = '\ue013'
ARROW_UP = UP
RIGHT = '\ue014'
ARROW_RIGHT = RIGHT
DOWN = '\ue015'
ARROW_DOWN = DOWN
INSERT = '\ue016'
DELETE = '\ue017'
DEL = '\ue017'
@ -219,15 +213,15 @@ keyDefinitions = {
'\ue005': {'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3},
'\ue006': {'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': '\r', 'location': 3},
'\ue00b': {'keyCode': 19, 'code': 'Pause', 'key': 'Pause'},
'CapsLock': {'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock'},
# 'CapsLock': {'keyCode': 20, 'code': 'CapsLock', 'key': 'CapsLock'},
'\ue00c': {'keyCode': 27, 'code': 'Escape', 'key': 'Escape'},
'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'},
'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'},
# 'Convert': {'keyCode': 28, 'code': 'Convert', 'key': 'Convert'},
# 'NonConvert': {'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert'},
'\ue010': {'keyCode': 35, 'code': 'End', 'key': 'End'},
# 'Numpad1': {'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3},
'Select': {'keyCode': 41, 'code': 'Select', 'key': 'Select'},
'Open': {'keyCode': 43, 'code': 'Open', 'key': 'Execute'},
'PrintScreen': {'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen'},
# 'Select': {'keyCode': 41, 'code': 'Select', 'key': 'Select'},
# 'Open': {'keyCode': 43, 'code': 'Open', 'key': 'Execute'},
# 'PrintScreen': {'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen'},
'\ue016': {'keyCode': 45, 'code': 'Insert', 'key': 'Insert'},
# 'Numpad0': {'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3},
'\ue017': {'keyCode': 46, 'code': 'Delete', 'key': 'Delete'},
@ -243,35 +237,6 @@ keyDefinitions = {
'\ue021': {'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7'},
'\ue022': {'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8'},
'\ue023': {'keyCode': 57, 'code': 'Digit9', 'shiftKey': '\(', 'key': '9'},
'KeyA': {'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a'},
'KeyB': {'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b'},
'KeyC': {'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c'},
'KeyD': {'keyCode': 68, 'code': 'KeyD', 'shiftKey': 'D', 'key': 'd'},
'KeyE': {'keyCode': 69, 'code': 'KeyE', 'shiftKey': 'E', 'key': 'e'},
'KeyF': {'keyCode': 70, 'code': 'KeyF', 'shiftKey': 'F', 'key': 'f'},
'KeyG': {'keyCode': 71, 'code': 'KeyG', 'shiftKey': 'G', 'key': 'g'},
'KeyH': {'keyCode': 72, 'code': 'KeyH', 'shiftKey': 'H', 'key': 'h'},
'KeyI': {'keyCode': 73, 'code': 'KeyI', 'shiftKey': 'I', 'key': 'i'},
'KeyJ': {'keyCode': 74, 'code': 'KeyJ', 'shiftKey': 'J', 'key': 'j'},
'KeyK': {'keyCode': 75, 'code': 'KeyK', 'shiftKey': 'K', 'key': 'k'},
'KeyL': {'keyCode': 76, 'code': 'KeyL', 'shiftKey': 'L', 'key': 'l'},
'KeyM': {'keyCode': 77, 'code': 'KeyM', 'shiftKey': 'M', 'key': 'm'},
'KeyN': {'keyCode': 78, 'code': 'KeyN', 'shiftKey': 'N', 'key': 'n'},
'KeyO': {'keyCode': 79, 'code': 'KeyO', 'shiftKey': 'O', 'key': 'o'},
'KeyP': {'keyCode': 80, 'code': 'KeyP', 'shiftKey': 'P', 'key': 'p'},
'KeyQ': {'keyCode': 81, 'code': 'KeyQ', 'shiftKey': 'Q', 'key': 'q'},
'KeyR': {'keyCode': 82, 'code': 'KeyR', 'shiftKey': 'R', 'key': 'r'},
'KeyS': {'keyCode': 83, 'code': 'KeyS', 'shiftKey': 'S', 'key': 's'},
'KeyT': {'keyCode': 84, 'code': 'KeyT', 'shiftKey': 'T', 'key': 't'},
'KeyU': {'keyCode': 85, 'code': 'KeyU', 'shiftKey': 'U', 'key': 'u'},
'KeyV': {'keyCode': 86, 'code': 'KeyV', 'shiftKey': 'V', 'key': 'v'},
'KeyW': {'keyCode': 87, 'code': 'KeyW', 'shiftKey': 'W', 'key': 'w'},
'KeyX': {'keyCode': 88, 'code': 'KeyX', 'shiftKey': 'X', 'key': 'x'},
'KeyY': {'keyCode': 89, 'code': 'KeyY', 'shiftKey': 'Y', 'key': 'y'},
'KeyZ': {'keyCode': 90, 'code': 'KeyZ', 'shiftKey': 'Z', 'key': 'z'},
'MetaLeft': {'keyCode': 91, 'code': 'MetaLeft', 'key': 'Meta'},
'MetaRight': {'keyCode': 92, 'code': 'MetaRight', 'key': 'Meta'},
'ContextMenu': {'keyCode': 93, 'code': 'ContextMenu', 'key': 'ContextMenu'},
'\ue024': {'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3},
'\ue025': {'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3},
'\ue027': {'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3},
@ -288,64 +253,91 @@ keyDefinitions = {
'\ue03a': {'keyCode': 121, 'code': 'F10', 'key': 'F10'},
'\ue03b': {'keyCode': 122, 'code': 'F11', 'key': 'F11'},
'\ue03c': {'keyCode': 123, 'code': 'F12', 'key': 'F12'},
'F13': {'keyCode': 124, 'code': 'F13', 'key': 'F13'},
'F14': {'keyCode': 125, 'code': 'F14', 'key': 'F14'},
'F15': {'keyCode': 126, 'code': 'F15', 'key': 'F15'},
'F16': {'keyCode': 127, 'code': 'F16', 'key': 'F16'},
'F17': {'keyCode': 128, 'code': 'F17', 'key': 'F17'},
'F18': {'keyCode': 129, 'code': 'F18', 'key': 'F18'},
'F19': {'keyCode': 130, 'code': 'F19', 'key': 'F19'},
'F20': {'keyCode': 131, 'code': 'F20', 'key': 'F20'},
'F21': {'keyCode': 132, 'code': 'F21', 'key': 'F21'},
'F22': {'keyCode': 133, 'code': 'F22', 'key': 'F22'},
'F23': {'keyCode': 134, 'code': 'F23', 'key': 'F23'},
'F24': {'keyCode': 135, 'code': 'F24', 'key': 'F24'},
'NumLock': {'keyCode': 144, 'code': 'NumLock', 'key': 'NumLock'},
'ScrollLock': {'keyCode': 145, 'code': 'ScrollLock', 'key': 'ScrollLock'},
'AudioVolumeMute': {'keyCode': 173, 'code': 'AudioVolumeMute', 'key': 'AudioVolumeMute'},
'AudioVolumeDown': {'keyCode': 174, 'code': 'AudioVolumeDown', 'key': 'AudioVolumeDown'},
'AudioVolumeUp': {'keyCode': 175, 'code': 'AudioVolumeUp', 'key': 'AudioVolumeUp'},
'MediaTrackNext': {'keyCode': 176, 'code': 'MediaTrackNext', 'key': 'MediaTrackNext'},
'MediaTrackPrevious': {'keyCode': 177, 'code': 'MediaTrackPrevious', 'key': 'MediaTrackPrevious'},
'MediaStop': {'keyCode': 178, 'code': 'MediaStop', 'key': 'MediaStop'},
'MediaPlayPause': {'keyCode': 179, 'code': 'MediaPlayPause', 'key': 'MediaPlayPause'},
'\ue018': {'keyCode': 186, 'code': 'Semicolon', 'shiftKey': ':', 'key': ';'},
'Equal': {'keyCode': 187, 'code': 'Equal', 'shiftKey': '+', 'key': '='},
'\ue019': {'keyCode': 187, 'code': 'NumpadEqual', 'key': '=', 'location': 3},
'Comma': {'keyCode': 188, 'code': 'Comma', 'shiftKey': '<', 'key': ','},
'Minus': {'keyCode': 189, 'code': 'Minus', 'shiftKey': '_', 'key': '-'},
'Period': {'keyCode': 190, 'code': 'Period', 'shiftKey': '>', 'key': '.'},
'Slash': {'keyCode': 191, 'code': 'Slash', 'shiftKey': '?', 'key': '/'},
'Backquote': {'keyCode': 192, 'code': 'Backquote', 'shiftKey': '~', 'key': '`'},
'BracketLeft': {'keyCode': 219, 'code': 'BracketLeft', 'shiftKey': '{', 'key': '['},
'Backslash': {'keyCode': 220, 'code': 'Backslash', 'shiftKey': '|', 'key': '\\'},
'BracketRight': {'keyCode': 221, 'code': 'BracketRight', 'shiftKey': '}', 'key': ']'},
'Quote': {'keyCode': 222, 'code': 'Quote', 'shiftKey': '"', 'key': '\''},
'AltGraph': {'keyCode': 225, 'code': 'AltGraph', 'key': 'AltGraph'},
'Props': {'keyCode': 247, 'code': 'Props', 'key': 'CrSel'},
'Cancel': {'keyCode': 3, 'key': 'Cancel', 'code': 'Abort'},
'Clear': {'keyCode': 12, 'key': 'Clear', 'code': 'Numpad5', 'location': 3},
'Shift': {'keyCode': 16, 'key': 'Shift', 'code': 'ShiftLeft'},
'Control': {'keyCode': 17, 'key': 'Control', 'code': 'ControlLeft'},
'Alt': {'keyCode': 18, 'key': 'Alt', 'code': 'AltLeft'},
'Accept': {'keyCode': 30, 'key': 'Accept'},
'ModeChange': {'keyCode': 31, 'key': 'ModeChange'},
'Print': {'keyCode': 42, 'key': 'Print'},
'Execute': {'keyCode': 43, 'key': 'Execute', 'code': 'Open'},
'\u0000': {'keyCode': 46, 'key': '\u0000', 'code': 'NumpadDecimal', 'location': 3},
'Attn': {'keyCode': 246, 'key': 'Attn'},
'CrSel': {'keyCode': 247, 'key': 'CrSel', 'code': 'Props'},
'ExSel': {'keyCode': 248, 'key': 'ExSel'},
'EraseEof': {'keyCode': 249, 'key': 'EraseEof'},
'Play': {'keyCode': 250, 'key': 'Play'},
'ZoomOut': {'keyCode': 251, 'key': 'ZoomOut'},
'Power': {'key': 'Power', 'code': 'Power'},
'Eject': {'key': 'Eject', 'code': 'Eject'},
# 'KeyA': {'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a'},
# 'KeyB': {'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b'},
# 'KeyC': {'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c'},
# 'KeyD': {'keyCode': 68, 'code': 'KeyD', 'shiftKey': 'D', 'key': 'd'},
# 'KeyE': {'keyCode': 69, 'code': 'KeyE', 'shiftKey': 'E', 'key': 'e'},
# 'KeyF': {'keyCode': 70, 'code': 'KeyF', 'shiftKey': 'F', 'key': 'f'},
# 'KeyG': {'keyCode': 71, 'code': 'KeyG', 'shiftKey': 'G', 'key': 'g'},
# 'KeyH': {'keyCode': 72, 'code': 'KeyH', 'shiftKey': 'H', 'key': 'h'},
# 'KeyI': {'keyCode': 73, 'code': 'KeyI', 'shiftKey': 'I', 'key': 'i'},
# 'KeyJ': {'keyCode': 74, 'code': 'KeyJ', 'shiftKey': 'J', 'key': 'j'},
# 'KeyK': {'keyCode': 75, 'code': 'KeyK', 'shiftKey': 'K', 'key': 'k'},
# 'KeyL': {'keyCode': 76, 'code': 'KeyL', 'shiftKey': 'L', 'key': 'l'},
# 'KeyM': {'keyCode': 77, 'code': 'KeyM', 'shiftKey': 'M', 'key': 'm'},
# 'KeyN': {'keyCode': 78, 'code': 'KeyN', 'shiftKey': 'N', 'key': 'n'},
# 'KeyO': {'keyCode': 79, 'code': 'KeyO', 'shiftKey': 'O', 'key': 'o'},
# 'KeyP': {'keyCode': 80, 'code': 'KeyP', 'shiftKey': 'P', 'key': 'p'},
# 'KeyQ': {'keyCode': 81, 'code': 'KeyQ', 'shiftKey': 'Q', 'key': 'q'},
# 'KeyR': {'keyCode': 82, 'code': 'KeyR', 'shiftKey': 'R', 'key': 'r'},
# 'KeyS': {'keyCode': 83, 'code': 'KeyS', 'shiftKey': 'S', 'key': 's'},
# 'KeyT': {'keyCode': 84, 'code': 'KeyT', 'shiftKey': 'T', 'key': 't'},
# 'KeyU': {'keyCode': 85, 'code': 'KeyU', 'shiftKey': 'U', 'key': 'u'},
# 'KeyV': {'keyCode': 86, 'code': 'KeyV', 'shiftKey': 'V', 'key': 'v'},
# 'KeyW': {'keyCode': 87, 'code': 'KeyW', 'shiftKey': 'W', 'key': 'w'},
# 'KeyX': {'keyCode': 88, 'code': 'KeyX', 'shiftKey': 'X', 'key': 'x'},
# 'KeyY': {'keyCode': 89, 'code': 'KeyY', 'shiftKey': 'Y', 'key': 'y'},
# 'KeyZ': {'keyCode': 90, 'code': 'KeyZ', 'shiftKey': 'Z', 'key': 'z'},
# 'MetaLeft': {'keyCode': 91, 'code': 'MetaLeft', 'key': 'Meta'},
# 'MetaRight': {'keyCode': 92, 'code': 'MetaRight', 'key': 'Meta'},
# 'ContextMenu': {'keyCode': 93, 'code': 'ContextMenu', 'key': 'ContextMenu'},
# 'F13': {'keyCode': 124, 'code': 'F13', 'key': 'F13'},
# 'F14': {'keyCode': 125, 'code': 'F14', 'key': 'F14'},
# 'F15': {'keyCode': 126, 'code': 'F15', 'key': 'F15'},
# 'F16': {'keyCode': 127, 'code': 'F16', 'key': 'F16'},
# 'F17': {'keyCode': 128, 'code': 'F17', 'key': 'F17'},
# 'F18': {'keyCode': 129, 'code': 'F18', 'key': 'F18'},
# 'F19': {'keyCode': 130, 'code': 'F19', 'key': 'F19'},
# 'F20': {'keyCode': 131, 'code': 'F20', 'key': 'F20'},
# 'F21': {'keyCode': 132, 'code': 'F21', 'key': 'F21'},
# 'F22': {'keyCode': 133, 'code': 'F22', 'key': 'F22'},
# 'F23': {'keyCode': 134, 'code': 'F23', 'key': 'F23'},
# 'F24': {'keyCode': 135, 'code': 'F24', 'key': 'F24'},
# 'NumLock': {'keyCode': 144, 'code': 'NumLock', 'key': 'NumLock'},
# 'ScrollLock': {'keyCode': 145, 'code': 'ScrollLock', 'key': 'ScrollLock'},
# 'AudioVolumeMute': {'keyCode': 173, 'code': 'AudioVolumeMute', 'key': 'AudioVolumeMute'},
# 'AudioVolumeDown': {'keyCode': 174, 'code': 'AudioVolumeDown', 'key': 'AudioVolumeDown'},
# 'AudioVolumeUp': {'keyCode': 175, 'code': 'AudioVolumeUp', 'key': 'AudioVolumeUp'},
# 'MediaTrackNext': {'keyCode': 176, 'code': 'MediaTrackNext', 'key': 'MediaTrackNext'},
# 'MediaTrackPrevious': {'keyCode': 177, 'code': 'MediaTrackPrevious', 'key': 'MediaTrackPrevious'},
# 'MediaStop': {'keyCode': 178, 'code': 'MediaStop', 'key': 'MediaStop'},
# 'MediaPlayPause': {'keyCode': 179, 'code': 'MediaPlayPause', 'key': 'MediaPlayPause'},
# 'Equal': {'keyCode': 187, 'code': 'Equal', 'shiftKey': '+', 'key': '='},
# 'Comma': {'keyCode': 188, 'code': 'Comma', 'shiftKey': '<', 'key': ','},
# 'Minus': {'keyCode': 189, 'code': 'Minus', 'shiftKey': '_', 'key': '-'},
# 'Period': {'keyCode': 190, 'code': 'Period', 'shiftKey': '>', 'key': '.'},
# 'Slash': {'keyCode': 191, 'code': 'Slash', 'shiftKey': '?', 'key': '/'},
# 'Backquote': {'keyCode': 192, 'code': 'Backquote', 'shiftKey': '~', 'key': '`'},
# 'BracketLeft': {'keyCode': 219, 'code': 'BracketLeft', 'shiftKey': '{', 'key': '['},
# 'Backslash': {'keyCode': 220, 'code': 'Backslash', 'shiftKey': '|', 'key': '\\'},
# 'BracketRight': {'keyCode': 221, 'code': 'BracketRight', 'shiftKey': '}', 'key': ']'},
# 'Quote': {'keyCode': 222, 'code': 'Quote', 'shiftKey': '"', 'key': '\''},
# 'AltGraph': {'keyCode': 225, 'code': 'AltGraph', 'key': 'AltGraph'},
# 'Props': {'keyCode': 247, 'code': 'Props', 'key': 'CrSel'},
# 'Cancel': {'keyCode': 3, 'key': 'Cancel', 'code': 'Abort'},
# 'Clear': {'keyCode': 12, 'key': 'Clear', 'code': 'Numpad5', 'location': 3},
# 'Shift': {'keyCode': 16, 'key': 'Shift', 'code': 'ShiftLeft'},
# 'Control': {'keyCode': 17, 'key': 'Control', 'code': 'ControlLeft'},
# 'Alt': {'keyCode': 18, 'key': 'Alt', 'code': 'AltLeft'},
# 'Accept': {'keyCode': 30, 'key': 'Accept'},
# 'ModeChange': {'keyCode': 31, 'key': 'ModeChange'},
# 'Print': {'keyCode': 42, 'key': 'Print'},
# 'Execute': {'keyCode': 43, 'key': 'Execute', 'code': 'Open'},
# 'Attn': {'keyCode': 246, 'key': 'Attn'},
# 'CrSel': {'keyCode': 247, 'key': 'CrSel', 'code': 'Props'},
# 'ExSel': {'keyCode': 248, 'key': 'ExSel'},
# 'EraseEof': {'keyCode': 249, 'key': 'EraseEof'},
# 'Play': {'keyCode': 250, 'key': 'Play'},
# 'ZoomOut': {'keyCode': 251, 'key': 'ZoomOut'},
# 'Power': {'key': 'Power', 'code': 'Power'},
# 'Eject': {'key': 'Eject', 'code': 'Eject'},
}
modifierBit = {'\ue00a': 1,
'\ue009': 2,
'\ue03d': 4,
'\ue008': 8}
modifierBit = {'\ue00a': 1, '\ue009': 2, '\ue03d': 4, '\ue008': 8}
sys = system().lower()
def keys_to_typing(value):
@ -368,71 +360,69 @@ def keys_to_typing(value):
return modifier, ''.join(typing)
def keyDescriptionForString(_modifiers, keyString): # noqa: C901
shift = _modifiers & 8
description = {'key': '',
'keyCode': 0,
'code': '',
'text': '',
'location': 0}
def make_input_data(modifiers, key, key_up=False):
"""
:param modifiers: 功能键设置
:param key: 按键字符
:param key_up: 是否提起
:return: None
"""
data = keyDefinitions.get(key)
if not data:
return None
definition = keyDefinitions.get(keyString) # type: ignore
if not definition:
raise ValueError(f'未知按键:{keyString}')
result = {'modifiers': modifiers, 'autoRepeat': False, '_ignore': AlertExistsError}
shift = modifiers & 8
if 'key' in definition:
description['key'] = definition['key']
if shift and definition.get('shiftKey'):
description['key'] = definition['shiftKey']
if shift and data.get('shiftKey'):
result['key'] = data['shiftKey']
result['text'] = data['shiftKey']
elif 'key' in data:
result['key'] = data['key']
if 'keyCode' in definition:
description['keyCode'] = definition['keyCode']
if shift and definition.get('shiftKeyCode'):
description['keyCode'] = definition['shiftKeyCode']
if len(result.get('key', '')) == 1: # type: ignore
result['text'] = data['key']
if 'code' in definition:
description['code'] = definition['code']
sys_text = 'windowsVirtualKeyCode' if sys == 'windows' else 'nativeVirtualKeyCode'
if shift and data.get('shiftKeyCode'):
result[sys_text] = data['shiftKeyCode']
elif 'keyCode' in data:
result[sys_text] = data['keyCode']
if 'location' in definition:
description['location'] = definition['location']
if 'code' in data:
result['code'] = data['code']
if len(description['key']) == 1: # type: ignore
description['text'] = description['key']
if 'location' in data:
result['location'] = data['location']
result['isKeypad'] = data['location'] == 3
else:
result['location'] = 0
result['isKeypad'] = False
if 'text' in definition:
description['text'] = definition['text']
if shift and definition.get('shiftText'):
description['text'] = definition['shiftText']
if shift and data.get('shiftText'):
result['text'] = data['shiftText']
result['unmodifiedText'] = data['shiftText']
elif 'text' in data:
result['text'] = data['text']
result['unmodifiedText'] = data['text']
if _modifiers & ~8:
description['text'] = ''
if modifiers & ~8:
result['text'] = ''
return description
result['type'] = 'keyUp' if key_up else ('keyDown' if result.get('text') else 'rawKeyDown')
return result
def send_key(page, modifier, key):
"""发送一个字,在键盘中的字符触发按键,其它直接发送文本"""
if key in keyDefinitions:
description = keyDescriptionForString(modifier, key)
text = description['text']
data = {'type': 'keyDown' if text else 'rawKeyDown',
'modifiers': modifier,
'windowsVirtualKeyCode': description['keyCode'],
'code': description['code'],
'key': description['key'],
'text': text,
'autoRepeat': False,
'unmodifiedText': text,
'location': description['location'],
'isKeypad': description['location'] == 3,
'_ignore': AlertExistsError}
page.run_cdp('Input.dispatchKeyEvent', **data)
data = make_input_data(modifier, key)
if data:
page._run_cdp('Input.dispatchKeyEvent', **data)
data['type'] = 'keyUp'
page.run_cdp('Input.dispatchKeyEvent', **data)
page._run_cdp('Input.dispatchKeyEvent', **data)
else:
page.run_cdp('Input.insertText', text=key, _ignore=AlertExistsError)
page._run_cdp('Input.insertText', text=key, _ignore=AlertExistsError)
def input_text_or_keys(page, text_or_keys):
@ -451,7 +441,7 @@ def input_text_or_keys(page, text_or_keys):
return
if text_or_keys.endswith(('\n', '\ue007')):
page.run_cdp('Input.insertText', text=text_or_keys[:-1], _ignore=AlertExistsError)
page._run_cdp('Input.insertText', text=text_or_keys[:-1], _ignore=AlertExistsError)
send_key(page, modifier, '\n')
else:
page.run_cdp('Input.insertText', text=text_or_keys, _ignore=AlertExistsError)
page._run_cdp('Input.insertText', text=text_or_keys, _ignore=AlertExistsError)

View File

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

View File

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

View File

@ -14,9 +14,11 @@ from .._pages.chromium_base import ChromiumBase
class PortFinder(object):
used_port: dict = ...
used_port: set = ...
prev_time: float = ...
lock: Lock = ...
tmp_dir: Path = ...
checked_paths: set = ...
def __init__(self, path: Union[str, Path] = None): ...
@ -45,4 +47,4 @@ def wait_until(function: callable, kwargs: dict = None, timeout: float = 10): ..
def configs_to_here(file_name: Union[Path, str] = None) -> None: ...
def raise_error(result: dict, ignore=None) -> None: ...
def raise_error(result: dict, ignore=None, user: bool = False) -> None: ...

View File

@ -5,16 +5,14 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from datetime import datetime
from html import unescape
from http.cookiejar import Cookie, CookieJar
from os.path import sep
from pathlib import Path
from re import sub, match
from re import sub
from urllib.parse import urlparse, urljoin, urlunparse
from DataRecorder.tools import make_valid_name
from tldextract import extract
from requests.structures import CaseInsensitiveDict
def get_ele_txt(e):
@ -59,7 +57,7 @@ def get_ele_txt(e):
if sub('[ \n\t\r]', '', el) != '': # 字符除了回车和空格还有其它内容
txt = el
if not pre:
txt = txt.replace('\r\n', ' ').replace('\n', ' ').strip(' ')
txt = txt.replace('\r\n', ' ').replace('\n', ' ')
txt = sub(r' {2,}', ' ', txt)
str_list.append(txt)
@ -80,8 +78,31 @@ def get_ele_txt(e):
re_str = get_node_txt(e)
if re_str and re_str[-1] == '\n':
re_str.pop()
re_str = ''.join([i if i is not True else '\n' for i in re_str])
return format_html(re_str)
l = len(re_str)
if l > 1:
r = []
for i in range(l - 1):
i1 = re_str[i]
i2 = re_str[i + 1]
if i1 is True:
r.append('\n')
continue
if i2 is True:
r.append(i1)
continue
if i1.endswith(' ') and i2.startswith(' '):
i1 = i1[:-1]
r.append(i1)
r.append('\n' if re_str[-1] is True else re_str[-1])
re_str = ''.join(r)
elif not l:
re_str = ''
else:
re_str = re_str[0] if re_str[0] is not True else '\n'
return format_html(re_str.strip())
def format_html(text):
@ -106,30 +127,30 @@ def location_in_viewport(page, loc_x, loc_y):
const vHeight = document.documentElement.clientHeight;
if (x< scrollLeft || y < scrollTop || x > vWidth + scrollLeft || y > vHeight + scrollTop){{return false;}}
return true;}}'''
return page.run_js(js)
return page._run_js(js)
def offset_scroll(ele, offset_x, offset_y):
"""接收元素及偏移坐标,把坐标滚动到页面中间,返回该点在视口中的坐标
"""接收元素及偏移坐标,把坐标滚动到页面中间,返回该点坐标
有偏移量时以元素左上角坐标为基准没有时以click_point为基准
:param ele: 元素对象
:param offset_x: 偏移量x
:param offset_y: 偏移量y
:return: 视口中的坐标
:return: 绝对坐标和相对坐标
"""
loc_x, loc_y = ele.rect.location
cp_x, cp_y = ele.rect.click_point
lx = loc_x + offset_x if offset_x else cp_x
ly = loc_y + offset_y if offset_y else cp_y
if not location_in_viewport(ele.owner, lx, ly):
clientWidth = ele.owner.run_js('return document.body.clientWidth;')
clientHeight = ele.owner.run_js('return document.body.clientHeight;')
clientWidth = ele.owner._run_js('return document.body.clientWidth;')
clientHeight = ele.owner._run_js('return document.body.clientHeight;')
ele.owner.scroll.to_location(lx - clientWidth // 2, ly - clientHeight // 2)
cl_x, cl_y = ele.rect.viewport_location
ccp_x, ccp_y = ele.rect.viewport_click_point
cx = cl_x + offset_x if offset_x else ccp_x
cy = cl_y + offset_y if offset_y else ccp_y
return cx, cy
return lx, ly, cx, cy
def make_absolute_link(link, baseURI=None):
@ -144,8 +165,11 @@ def make_absolute_link(link, baseURI=None):
link = link.strip().replace('\\', '/')
parsed = urlparse(link)._asdict()
if baseURI:
p = urlparse(baseURI)._asdict()
baseURI = f'{p["scheme"]}://{p["netloc"]}'
if link.startswith('./'):
baseURI = baseURI[:baseURI.rfind('/') + 1]
else:
p = urlparse(baseURI)._asdict()
baseURI = f'{p["scheme"]}://{p["netloc"]}'
# 是相对路径与页面url拼接并返回
if not parsed['netloc']:
@ -171,188 +195,6 @@ def is_js_func(func):
return False
def cookie_to_dict(cookie):
"""把Cookie对象转为dict格式
:param cookie: Cookie对象字符串或字典
:return: cookie字典
"""
if isinstance(cookie, Cookie):
cookie_dict = cookie.__dict__.copy()
cookie_dict.pop('rfc2109', None)
cookie_dict.pop('_rest', None)
return cookie_dict
elif isinstance(cookie, dict):
cookie_dict = cookie
elif isinstance(cookie, str):
cookie_dict = {}
for attr in cookie.strip().rstrip(';,').split(',' if ',' in cookie else ';'):
attr_val = attr.strip().split('=', 1)
if attr_val[0] in ('domain', 'path', 'expires', 'max-age', 'HttpOnly', 'secure', 'expiry', 'name', 'value'):
cookie_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else ''
else:
cookie_dict['name'] = attr_val[0]
cookie_dict['value'] = attr_val[1] if len(attr_val) == 2 else ''
return cookie_dict
else:
raise TypeError('cookie参数必须为Cookie、str或dict类型。')
return cookie_dict
def cookies_to_tuple(cookies):
"""把cookies转为tuple格式
:param cookies: cookies信息可为CookieJar, list, tuple, str, dict
:return: 返回tuple形式的cookies
"""
if isinstance(cookies, (list, tuple, CookieJar)):
cookies = tuple(cookie_to_dict(cookie) for cookie in cookies)
elif isinstance(cookies, str):
c_dict = {}
cookies = cookies.rstrip('; ')
cookies = cookies.split(';')
# r = match(r'.*?=([^=]+)=', cookies)
# if not r: # 只有一个
# cookies = [cookies.rstrip(',;')]
# else:
# s = match(r'.*([,;]).*', r.group(1)).group(1)
# cookies = cookies.rstrip(s).split(s)
for attr in cookies:
attr_val = attr.strip().split('=', 1)
c_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else True
cookies = _dict_cookies_to_tuple(c_dict)
elif isinstance(cookies, dict):
cookies = _dict_cookies_to_tuple(cookies)
elif isinstance(cookies, Cookie):
cookies = (cookie_to_dict(cookies),)
else:
raise TypeError('cookies参数必须为Cookie、CookieJar、list、tuple、str或dict类型。')
return cookies
def set_session_cookies(session, cookies):
"""设置Session对象的cookies
:param session: Session对象
:param cookies: cookies信息
:return: None
"""
for cookie in cookies_to_tuple(cookies):
if cookie['value'] is None:
cookie['value'] = ''
kwargs = {x: cookie[x] for x in cookie
if x.lower() in ('version', 'port', 'domain', 'path', 'secure',
'expires', 'discard', 'comment', 'comment_url', 'rest')}
if 'expiry' in cookie:
kwargs['expires'] = cookie['expiry']
session.cookies.set(cookie['name'], cookie['value'], **kwargs)
def set_browser_cookies(page, cookies):
"""设置cookies值
:param page: 页面对象
:param cookies: cookies信息
:return: None
"""
for cookie in cookies_to_tuple(cookies):
if 'expiry' in cookie:
cookie['expires'] = int(cookie['expiry'])
cookie.pop('expiry')
if 'expires' in cookie:
if not cookie['expires']:
cookie.pop('expires')
elif isinstance(cookie['expires'], str):
if cookie['expires'].isdigit():
cookie['expires'] = int(cookie['expires'])
elif cookie['expires'].replace('.', '').isdigit():
cookie['expires'] = float(cookie['expires'])
else:
try:
cookie['expires'] = datetime.strptime(cookie['expires'],
'%a, %d %b %Y %H:%M:%S GMT').timestamp()
except ValueError:
cookie['expires'] = datetime.strptime(cookie['expires'],
'%a, %d %b %y %H:%M:%S GMT').timestamp()
if cookie['value'] is None:
cookie['value'] = ''
elif not isinstance(cookie['value'], str):
cookie['value'] = str(cookie['value'])
if cookie['name'].startswith('__Host-'):
cookie['path'] = '/'
cookie['secure'] = True
if not page.url.startswith('http'):
cookie['name'] = cookie['name'].replace('__Host-', '__Secure-', 1)
else:
cookie['url'] = page.url
page.run_cdp_loaded('Network.setCookie', **cookie)
continue # 不用设置域名,可退出
if cookie['name'].startswith('__Secure-'):
cookie['secure'] = True
if cookie.get('domain', None):
try:
page.run_cdp_loaded('Network.setCookie', **cookie)
if is_cookie_in_driver(page, cookie):
continue
except Exception:
pass
url = page._browser_url
if not url.startswith('http'):
raise RuntimeError(f'未设置域名请设置cookie的domain参数或先访问一个网站。{cookie}')
ex_url = extract(url)
d_list = ex_url.subdomain.split('.')
d_list.append(f'{ex_url.domain}.{ex_url.suffix}' if ex_url.suffix else ex_url.domain)
tmp = [d_list[0]]
if len(d_list) > 1:
for i in d_list[1:]:
tmp.append('.')
tmp.append(i)
for i in range(len(tmp)):
cookie['domain'] = ''.join(tmp[i:])
page.run_cdp_loaded('Network.setCookie', **cookie)
if is_cookie_in_driver(page, cookie):
break
def is_cookie_in_driver(page, cookie):
"""查询cookie是否在浏览器内
:param page: BasePage对象
:param cookie: dict格式cookie
:return: bool
"""
if 'domain' in cookie:
for c in page.cookies(all_domains=True):
if cookie['name'] == c['name'] and cookie['value'] == c['value'] and cookie['domain'] == c.get('domain',
None):
return True
else:
for c in page.cookies(all_domains=True):
if cookie['name'] == c['name'] and cookie['value'] == c['value']:
return True
return False
def get_blob(page, url, as_bytes=True):
"""获取知道blob资源
:param page: 资源所在页面对象
@ -378,7 +220,7 @@ def get_blob(page, url, as_bytes=True):
}
"""
try:
result = page.run_js(js, url)
result = page._run_js(js, url)
except:
raise RuntimeError('无法获取该资源。')
if as_bytes:
@ -426,7 +268,7 @@ def get_mhtml(page, path=None, name=None):
:param name: 文件名为None且path不为None时用title属性值
:return: mhtml文本
"""
r = page.run_cdp('Page.captureSnapshot')['data']
r = page._run_cdp('Page.captureSnapshot')['data']
if path is None and name is None:
return r
@ -452,7 +294,7 @@ def get_pdf(page, path=None, name=None, kwargs=None):
if 'printBackground' not in kwargs:
kwargs['printBackground'] = True
try:
r = page.run_cdp('Page.printToPDF', **kwargs)['data']
r = page._run_cdp('Page.printToPDF', **kwargs)['data']
except:
raise RuntimeError('保存失败,可能浏览器版本不支持。')
from base64 import b64decode
@ -528,7 +370,9 @@ def format_headers(txt):
:param txt: 从浏览器复制的原始文本格式headers
:return: dict格式headers
"""
if not isinstance(txt, str):
if isinstance(txt, (dict, CaseInsensitiveDict)):
for k, v in txt.items():
txt[k] = str(v)
return txt
headers = {}
for header in txt.split('\n'):
@ -536,15 +380,3 @@ def format_headers(txt):
name, value = header.split(': ', maxsplit=1)
headers[name] = value
return headers
def _dict_cookies_to_tuple(cookies: dict):
"""把dict形式的cookies转换为tuple形式
:param cookies: 单个或多个cookies单个时包含'name''value'
:return: 多个dict格式cookies组成的列表
"""
if 'name' in cookies and 'value' in cookies: # 单个cookie
return (cookies,)
keys = ('domain', 'path', 'expires', 'max-age', 'HttpOnly', 'secure', 'expiry')
template = {k: v for k, v in cookies.items() if k in keys}
return tuple(dict(**{'name': k, 'value': v}, **template) for k, v in cookies.items() if k not in keys)

View File

@ -5,18 +5,14 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from http.cookiejar import Cookie
from pathlib import Path
from typing import Union, Optional
from requests import Session
from requests.cookies import RequestsCookieJar
from typing import Union, Optional, Tuple
from .._base.base import DrissionElement, BaseParser
from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_page import ChromiumPage
from .._pages.chromium_tab import ChromiumTab
from .._pages.tabs import ChromiumTab
def get_ele_txt(e: DrissionElement) -> str: ...
@ -28,7 +24,7 @@ def format_html(text: str) -> str: ...
def location_in_viewport(page: ChromiumBase, loc_x: float, loc_y: float) -> bool: ...
def offset_scroll(ele: ChromiumElement, offset_x: float, offset_y: float) -> tuple: ...
def offset_scroll(ele: ChromiumElement, offset_x: float, offset_y: float) -> Tuple[int, int, int, int]: ...
def make_absolute_link(link: str, baseURI: str = None) -> str: ...
@ -37,21 +33,6 @@ def make_absolute_link(link: str, baseURI: str = None) -> str: ...
def is_js_func(func: str) -> bool: ...
def cookie_to_dict(cookie: Union[Cookie, str, dict]) -> dict: ...
def cookies_to_tuple(cookies: Union[RequestsCookieJar, list, tuple, str, dict, Cookie]) -> tuple: ...
def set_session_cookies(session: Session, cookies: Union[RequestsCookieJar, list, tuple, str, dict]) -> None: ...
def set_browser_cookies(page: ChromiumBase, cookies: Union[RequestsCookieJar, list, tuple, str, dict]) -> None: ...
def is_cookie_in_driver(page: ChromiumBase, cookie: dict) -> bool: ...
def get_blob(page: ChromiumBase, url: str, as_bytes: bool = True) -> bytes: ...

View File

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

View File

@ -8,12 +8,13 @@
from pathlib import Path
from typing import Union, Tuple, List, Any, Optional, Literal
from .chromium_tab import ChromiumTab
from .tabs import ChromiumTab, MixTab
from .._base.base import BasePage
from .._base.browser import Browser
from .._base.browser import Chromium
from .._base.driver import Driver
from .._elements.chromium_element import ChromiumElement
from .._elements.session_element import SessionElement
from .._functions.cookies import CookiesList
from .._functions.elements import SessionElementsList, ChromiumElementsList
from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage
@ -31,13 +32,10 @@ PIC_TYPE = Literal['jpg', 'jpeg', 'png', 'webp', True]
class ChromiumBase(BasePage):
def __init__(self,
address: Union[str, int],
tab_id: str = None,
timeout: float = None):
self._browser: Browser = ...
self._page: ChromiumPage = ...
self.tab: Union[ChromiumPage, ChromiumTab] = ...
self.address: str = ...
browser: Chromium,
tab_id: str = None):
self._tab: Union[ChromiumTab, MixTab, ChromiumFrame] = ...
self._browser: Chromium = ...
self._driver: Driver = ...
self._frame_id: str = ...
self._is_reading: bool = ...
@ -65,9 +63,9 @@ class ChromiumBase(BasePage):
self._rect: TabRect = ...
self._type: str = ...
def _connect_browser(self, tab_id: str = None) -> None: ...
def _connect_browser(self, target_id: str = None) -> None: ...
def _driver_init(self, tab_id: str) -> None: ...
def _driver_init(self, target_id: str) -> None: ...
def _get_document(self, timeout: float = 10) -> bool: ...
@ -91,7 +89,7 @@ class ChromiumBase(BasePage):
def _wait_to_stop(self): ...
def _d_set_start_options(self, address) -> None: ...
# def _d_set_start_options(self, address) -> None: ...
def _d_set_runtime_settings(self) -> None: ...
@ -104,7 +102,7 @@ class ChromiumBase(BasePage):
def _js_ready_state(self) -> str: ...
@property
def browser(self) -> Browser: ...
def browser(self) -> Chromium: ...
@property
def title(self) -> str: ...
@ -145,6 +143,9 @@ class ChromiumBase(BasePage):
@property
def rect(self) -> TabRect: ...
@property
def timeout(self) -> float: ...
@property
def timeouts(self) -> Timeout: ...
@ -173,13 +174,16 @@ class ChromiumBase(BasePage):
def run_js_loaded(self, script: Union[str, Path], *args, as_expr: bool = False, timeout: float = None) -> Any: ...
def _run_js(self, script: Union[str, Path], *args, as_expr: bool = False, timeout: float = None) -> Any: ...
def _run_js_loaded(self, script: Union[str, Path], *args, as_expr: bool = False, timeout: float = None) -> Any: ...
def run_async_js(self, script: Union[str, Path], *args, as_expr: bool = False) -> None: ...
def get(self, url: str, show_errmsg: bool = False, retry: int = None,
interval: float = None, timeout: float = None) -> Union[None, bool]: ...
def cookies(self, as_dict: bool = False, all_domains: bool = False, all_info: bool = False) -> Union[
list, dict]: ...
def cookies(self, all_domains: bool = False, all_info: bool = False) -> CookiesList: ...
def ele(self,
locator: Union[Tuple[str, str], str, ChromiumElement, ChromiumFrame],
@ -192,9 +196,12 @@ class ChromiumBase(BasePage):
def s_ele(self,
locator: Union[Tuple[str, str], str] = None,
index: int = 1) -> SessionElement: ...
index: int = 1,
timeout: float = None) -> SessionElement: ...
def s_eles(self, locator: Union[Tuple[str, str], str]) -> SessionElementsList: ...
def s_eles(self,
locator: Union[Tuple[str, str], str],
timeout: float = None) -> SessionElementsList: ...
def _find_elements(self,
locator: Union[Tuple[str, str], str, ChromiumElement, ChromiumFrame],
@ -220,7 +227,9 @@ class ChromiumBase(BasePage):
insert_to: Union[ChromiumElement, str, Tuple[str, str], None] = None,
before: Union[ChromiumElement, str, Tuple[str, str], None] = None) -> ChromiumElement: ...
def get_frame(self, loc_ind_ele: Union[str, int, tuple, ChromiumFrame], timeout: float = None) -> ChromiumFrame: ...
def get_frame(self,
loc_ind_ele: Union[str, int, tuple, ChromiumFrame, ChromiumElement],
timeout: float = None) -> ChromiumFrame: ...
def get_frames(self, locator: Union[str, tuple] = None, timeout: float = None) -> List[ChromiumFrame]: ...
@ -228,6 +237,10 @@ class ChromiumBase(BasePage):
def run_cdp_loaded(self, cmd: str, **cmd_args) -> dict: ...
def _run_cdp(self, cmd: str, **cmd_args) -> dict: ...
def _run_cdp_loaded(self, cmd: str, **cmd_args) -> dict: ...
def session_storage(self, item: str = None) -> Union[str, dict, None]: ...
def local_storage(self, item: str = None) -> Union[str, dict, None]: ...
@ -267,12 +280,14 @@ class ChromiumBase(BasePage):
class Timeout(object):
def __init__(self, page: ChromiumBase, base=None, page_load=None, script=None):
self._page: ChromiumBase = ...
def __init__(self, base=None, page_load=None, script=None):
self.base: float = ...
self.page_load: float = ...
self.script: float = ...
@property
def as_dict(self) -> dict: ...
class Alert(object):

View File

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

View File

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

View File

@ -5,23 +5,13 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from pathlib import Path
from threading import Lock
from time import sleep, perf_counter
from time import sleep
from requests import Session
from .._base.browser import Browser
from .._configs.chromium_options import ChromiumOptions
from .._functions.browser import connect_browser
from .._functions.settings import Settings
from .._functions.tools import PortFinder
from .._base.browser import Chromium
from .._functions.web import save_page
from .._pages.chromium_base import ChromiumBase, Timeout
from .._pages.chromium_tab import ChromiumTab
from .._pages.chromium_base import ChromiumBase
from .._units.setter import ChromiumPageSetter
from .._units.waiter import PageWaiter
from ..errors import BrowserConnectError
class ChromiumPage(ChromiumBase):
@ -34,19 +24,16 @@ class ChromiumPage(ChromiumBase):
:param tab_id: 要控制的标签页id不指定默认为激活的
:param timeout: 超时时间
"""
opt = handle_options(addr_or_opts)
is_exist, browser_id = run_browser(opt)
if browser_id in cls._PAGES:
r = cls._PAGES[browser_id]
browser = Chromium(addr_or_opts=addr_or_opts)
if browser.id in cls._PAGES:
r = cls._PAGES[browser.id]
while not hasattr(r, '_frame_id'):
sleep(.1)
return r
r = object.__new__(cls)
r._chromium_options = opt
r._is_exist = is_exist
r._browser_id = browser_id
r.address = opt.address
cls._PAGES[browser_id] = r
r._browser = browser
cls._PAGES[browser.id] = r
return r
def __init__(self, addr_or_opts=None, tab_id=None, timeout=None):
@ -59,49 +46,19 @@ class ChromiumPage(ChromiumBase):
return
self._created = True
self._page = self
self.tab = self
self._run_browser()
super().__init__(self.address, tab_id)
super().__init__(self.browser, tab_id)
self._type = 'ChromiumPage'
self._lock = Lock()
self.set.timeouts(base=timeout)
self._page_init()
def _run_browser(self):
"""连接浏览器"""
self._browser = Browser(self._chromium_options.address, self._browser_id, self)
r = self._browser.run_cdp('Browser.getVersion')
self._browser_version = r['product']
if self._is_exist and self._chromium_options._headless is False and 'headless' in r['userAgent'].lower():
self._browser.quit(3)
connect_browser(self._chromium_options)
s = Session()
s.trust_env = False
ws = s.get(f'http://{self._chromium_options.address}/json/version', headers={'Connection': 'close'})
bid = ws.json()['webSocketDebuggerUrl'].split('/')[-1]
self._browser = Browser(self._chromium_options.address, bid, self)
ws.close()
s.close()
self._tab = self
def _d_set_runtime_settings(self):
"""设置运行时用到的属性"""
self._timeouts = Timeout(self, page_load=self._chromium_options.timeouts['page_load'],
script=self._chromium_options.timeouts['script'],
base=self._chromium_options.timeouts['base'])
if self._chromium_options.timeouts['base'] is not None:
self._timeout = self._chromium_options.timeouts['base']
self._load_mode = self._chromium_options.load_mode
self._download_path = None if self._chromium_options.download_path is None \
else str(Path(self._chromium_options.download_path).absolute())
self.retry_times = self._chromium_options.retry_times
self.retry_interval = self._chromium_options.retry_interval
def _page_init(self):
"""浏览器相关设置"""
self._browser.connect_to_page()
# ----------挂件----------
self._timeouts = self.browser.timeouts
self._load_mode = self.browser._load_mode
self._download_path = self.browser.download_path
self.retry_times = self.browser.retry_times
self.retry_interval = self.browser.retry_interval
@property
def set(self):
@ -138,7 +95,7 @@ class ChromiumPage(ChromiumBase):
def latest_tab(self):
"""返回最新的标签页,最新标签页指最后创建或最后被激活的
当Settings.singleton_tab_obj==True时返回Tab对象否则返回tab id"""
return self.get_tab(self.tab_ids[0], as_id=not Settings.singleton_tab_obj)
return self.browser.latest_tab
@property
def process_id(self):
@ -148,7 +105,12 @@ class ChromiumPage(ChromiumBase):
@property
def browser_version(self):
"""返回所控制的浏览器版本号"""
return self._browser_version
return self._browser.version
@property
def address(self):
"""返回浏览器地址ip:port"""
return self.browser.address
def save(self, path=None, name=None, as_pdf=False, **kwargs):
"""把当前页面保存为文件如果path和name参数都为None只返回文本
@ -169,34 +131,7 @@ class ChromiumPage(ChromiumBase):
:param as_id: 是否返回标签页id而不是标签页对象
:return: ChromiumTab对象
"""
if id_or_num is not None:
if isinstance(id_or_num, str):
id_or_num = id_or_num
elif isinstance(id_or_num, int):
id_or_num = self.tab_ids[id_or_num - 1 if id_or_num > 0 else id_or_num]
elif isinstance(id_or_num, ChromiumTab):
if as_id:
return id_or_num.tab_id
elif Settings.singleton_tab_obj:
return id_or_num
else:
return self.get_tab(id_or_num.tab_id)
elif title == url == tab_type is None:
id_or_num = self.tab_id
else:
id_or_num = self._browser.find_tabs(title, url, tab_type)
if id_or_num:
id_or_num = id_or_num[0]['id']
else:
return None
if as_id:
return id_or_num
with self._lock:
return ChromiumTab(self, id_or_num)
return self.browser.get_tab(id_or_num=id_or_num, title=title, url=url, tab_type=tab_type, as_id=as_id)
def get_tabs(self, title=None, url=None, tab_type='page', as_id=False):
"""查找符合条件的tab返回它们组成的列表
@ -206,10 +141,7 @@ class ChromiumPage(ChromiumBase):
:param as_id: 是否返回标签页id而不是标签页对象
:return: ChromiumTab对象组成的列表
"""
if as_id:
return [tab['id'] for tab in self._browser.find_tabs(title, url, tab_type)]
with self._lock:
return [ChromiumTab(self, tab['id']) for tab in self._browser.find_tabs(title, url, tab_type)]
return self.browser.get_tabs(title=title, url=url, tab_type=tab_type, as_id=as_id)
def new_tab(self, url=None, new_window=False, background=False, new_context=False):
"""新建一个标签页
@ -219,10 +151,14 @@ class ChromiumPage(ChromiumBase):
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
tab = ChromiumTab(self, tab_id=self.browser.new_tab(new_window, background, new_context))
if url:
tab.get(url)
return tab
return self.browser.new_tab(url=url, new_window=new_window, background=background, new_context=new_context)
def activate_tab(self, id_ind_tab):
"""使标签页变为活动状态
:param id_ind_tab: 标签页idstrTab对象或标签页序号int序号从1开始
:return: None
"""
self.browser.activate_tab(id_ind_tab)
def close(self):
"""关闭Page管理的标签页"""
@ -234,32 +170,7 @@ class ChromiumPage(ChromiumBase):
:param others: 是否关闭指定标签页之外的
:return: None
"""
all_tabs = set(self.tab_ids)
if isinstance(tabs_or_ids, str):
tabs = {tabs_or_ids}
elif isinstance(tabs_or_ids, ChromiumTab):
tabs = {tabs_or_ids.tab_id}
elif tabs_or_ids is None:
tabs = {self.tab_id}
elif isinstance(tabs_or_ids, (list, tuple)):
tabs = set(i.tab_id if isinstance(i, ChromiumTab) else i for i in tabs_or_ids)
else:
raise TypeError('tabs_or_ids参数只能传入标签页对象或id。')
if others:
tabs = all_tabs - tabs
end_len = len(set(all_tabs) - set(tabs))
if end_len <= 0:
self.quit()
return
for tab in tabs:
self.browser.close_tab(tab)
sleep(.2)
end_time = perf_counter() + 3
while self.tabs_count != end_len and perf_counter() < end_time:
sleep(.1)
self.browser.close_tabs(tabs_or_ids=tabs_or_ids, others=others)
def quit(self, timeout=5, force=True):
"""关闭浏览器
@ -271,69 +182,7 @@ class ChromiumPage(ChromiumBase):
def _on_disconnect(self):
"""浏览器退出时执行"""
ChromiumPage._PAGES.pop(self._browser_id, None)
ChromiumPage._PAGES.pop(self._browser.id, None)
def __repr__(self):
return f'<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.
"""
from pathlib import Path
from threading import Lock
from typing import Union, Tuple, List, Optional
from .._base.browser import Browser
from .._base.browser import Chromium
from .._configs.chromium_options import ChromiumOptions
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_tab import ChromiumTab
from .._pages.tabs import ChromiumTab
from .._units.rect import TabRect
from .._units.setter import ChromiumPageSetter
from .._units.waiter import PageWaiter
@ -20,6 +19,10 @@ from .._units.waiter import PageWaiter
class ChromiumPage(ChromiumBase):
_PAGES: dict = ...
tab: ChromiumPage = ...
_browser: Chromium = ...
_rect: Optional[TabRect] = ...
_is_exist: bool = ...
def __new__(cls,
addr_or_opts: Union[str, int, ChromiumOptions] = None,
@ -29,15 +32,7 @@ class ChromiumPage(ChromiumBase):
def __init__(self,
addr_or_opts: Union[str, int, ChromiumOptions] = None,
tab_id: str = None,
timeout: float = None):
self.tab: ChromiumPage = ...
self._chromium_options: ChromiumOptions = ...
self._browser: Browser = ...
self._browser_id: str = ...
self._rect: Optional[TabRect] = ...
self._is_exist: bool = ...
self._lock: Lock = ...
self._browser_version: str = ...
timeout: float = None):...
def _handle_options(self, addr_or_opts: Union[str, ChromiumOptions]) -> str: ...
@ -46,7 +41,7 @@ class ChromiumPage(ChromiumBase):
def _page_init(self) -> None: ...
@property
def browser(self) -> Browser: ...
def browser(self) -> Chromium: ...
@property
def tabs_count(self) -> int: ...
@ -66,6 +61,9 @@ class ChromiumPage(ChromiumBase):
@property
def browser_version(self) -> str: ...
@property
def address(self) -> str: ...
@property
def set(self) -> ChromiumPageSetter: ...
@ -106,6 +104,8 @@ class ChromiumPage(ChromiumBase):
def new_tab(self, url: str = None, new_window: bool = False, background: bool = False,
new_context: bool = False) -> ChromiumTab: ...
def activate_tab(self, id_ind_tab: Union[int, str, ChromiumTab]) -> None: ...
def close(self) -> None: ...
def close_tabs(self, tabs_or_ids: Union[str, ChromiumTab, List[Union[str, ChromiumTab]],
@ -114,12 +114,3 @@ class ChromiumPage(ChromiumBase):
def quit(self, timeout: float = 5, force: bool = True) -> None: ...
def _on_disconnect(self) -> None: ...
def handle_options(addr_or_opts): ...
def run_browser(chromium_options): ...
def get_rename(original: str, rename: str) -> str: ...

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,28 +10,30 @@ from typing import Union
from .downloader import DownloadMission
from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_tab import WebPageTab, ChromiumTab
from .._pages.tabs import MixTab, ChromiumTab
class Clicker(object):
def __init__(self, ele: ChromiumElement):
self._ele: ChromiumElement = ...
def __call__(self, by_js: Union[bool, str, None] = False, timeout: float = 1.5, wait_stop: bool = True) -> bool: ...
def __call__(self, by_js: Union[bool, str, None] = False,
timeout: float = 1.5, wait_stop: bool = True) -> Union[ChromiumElement, False]: ...
def left(self, by_js: Union[bool, str, None] = False, timeout: float = 1.5, wait_stop: bool = True) -> bool: ...
def left(self, by_js: Union[bool, str, None] = False,
timeout: float = 1.5, wait_stop: bool = True) -> Union[ChromiumElement, False]: ...
def right(self) -> None: ...
def right(self) -> ChromiumElement: ...
def middle(self, get_tab: bool = True) -> Union[ChromiumTab, WebPageTab, None]: ...
def middle(self, get_tab: bool = True) -> Union[ChromiumTab, MixTab, None]: ...
def at(self,
offset_x: float = None,
offset_y: float = None,
button: str = 'left',
count: int = 1) -> None: ...
count: int = 1) -> ChromiumElement: ...
def multi(self, times: int = 2) -> None: ...
def multi(self, times: int = 2) -> ChromiumElement: ...
def to_download(self,
save_path: Union[str, Path] = None,
@ -43,6 +45,17 @@ class Clicker(object):
def to_upload(self, file_paths: Union[str, Path, list, tuple], by_js: bool = False) -> None: ...
def for_new_tab(self, by_js: bool = False) -> Union[ChromiumTab, WebPageTab]: ...
def for_new_tab(self, by_js: bool = False, timeout: float = 3) -> Union[ChromiumTab, MixTab]: ...
def _click(self, client_x: float, client_y: float, button: str = 'left', count: int = 1) -> None: ...
def for_url_change(self, text: str = None, exclude: bool = False,
by_js: bool = False, timeout: float = None) -> bool: ...
def for_title_change(self, text: str = None, exclude: bool = False,
by_js: bool = False, timeout: float = None) -> bool: ...
def _click(self, loc_x: float,
loc_y: float,
view_x: float,
view_y: float,
button: str = 'left',
count: int = 1) -> ChromiumElement: ...

View File

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

View File

@ -8,26 +8,35 @@
from http.cookiejar import Cookie, CookieJar
from typing import Union
from .._base.browser import Chromium
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_tab import WebPageTab
from .._pages.tabs import MixTab
from .._pages.session_page import SessionPage
from .._pages.web_page import WebPage
from .._pages.mix_page import MixPage
class CookiesSetter(object):
_owner: ChromiumBase
class BrowserCookiesSetter(object):
_owner: Chromium = ...
def __init__(self, page: ChromiumBase): ...
def __init__(self, page: Chromium): ...
def __call__(self, cookies: Union[CookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def clear(self) -> None: ...
class CookiesSetter(BrowserCookiesSetter):
_owner: ChromiumBase = ...
def __init__(self, page: ChromiumBase): ...
def remove(self, name: str, url: str = None, domain: str = None, path: str = None) -> None: ...
def clear(self) -> None: ...
class SessionCookiesSetter(object):
_owner: SessionPage
_owner: SessionPage = ...
def __init__(self, page: SessionPage): ...
@ -38,8 +47,8 @@ class SessionCookiesSetter(object):
def clear(self) -> None: ...
class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter):
_owner: Union[WebPage, WebPageTab]
class MixPageCookiesSetter(CookiesSetter, SessionCookiesSetter):
_owner: Union[MixPage, MixTab] = ...
def __init__(self, page: SessionPage): ...

View File

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

View File

@ -7,30 +7,30 @@
"""
from typing import Dict, Optional, Union, Literal
from .._base.browser import Browser
from .._base.browser import Chromium
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_page import ChromiumPage
# from .._pages.chromium_page import ChromiumPage
FILE_EXISTS = Literal['skip', 'rename', 'overwrite', 's', 'r', 'o']
class DownloadManager(object):
_browser: Browser = ...
_page: ChromiumPage = ...
_browser: Chromium = ...
# _page: ChromiumPage = ...
_missions: Dict[str, DownloadMission] = ...
_tab_missions: dict = ...
_flags: dict = ...
_running: bool = ...
_save_path: Optional[str] = ...
# _save_path: Optional[str] = ...
def __init__(self, browser: Browser): ...
def __init__(self, browser: Chromium): ...
@property
def missions(self) -> Dict[str, DownloadMission]: ...
def set_path(self, tab: ChromiumBase, path: str) -> None: ...
def set_path(self, tab: Union[str,ChromiumBase], path: str) -> None: ...
def set_rename(self, tab_id: str, rename: str = None, suffix: str = None) -> None: ...
def set_file_exists(self, tab_id: str, mode: Literal['skip', 'rename', 'overwrite', 's', 'r', 'o']) -> None: ...
def set_file_exists(self, tab_id: str, mode: FILE_EXISTS) -> None: ...
def set_flag(self, tab_id: str, flag: Union[bool, DownloadMission, None]) -> None: ...

View File

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

View File

@ -18,7 +18,7 @@ class ElementRect(object):
def corners(self):
"""返回元素四个角坐标顺序左上、右上、右下、左下没有大小的元素抛出NoRectError"""
vr = self._get_viewport_rect('border')
r = self._ele.owner.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport']
r = self._ele.owner._run_cdp_loaded('Page.getLayoutMetrics')['visualViewport']
sx = r['pageX']
sy = r['pageY']
return [(vr[0] + sx, vr[1] + sy), (vr[2] + sx, vr[3] + sy), (vr[4] + sx, vr[5] + sy), (vr[6] + sx, vr[7] + sy)]
@ -32,27 +32,24 @@ class ElementRect(object):
@property
def size(self):
"""返回元素大小,格式(宽, 高)"""
border = self._ele.owner.run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id,
nodeId=self._ele._node_id, objectId=self._ele._obj_id)['model']['border']
border = self._ele.owner._run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id,
nodeId=self._ele._node_id, objectId=self._ele._obj_id)['model']['border']
return border[2] - border[0], border[5] - border[1]
@property
def location(self):
"""返回元素左上角的绝对坐标"""
cl = self.viewport_location
return self._get_page_coord(cl[0], cl[1])
return self._get_page_coord(*self.viewport_location)
@property
def midpoint(self):
"""返回元素中间点的绝对坐标"""
cl = self.viewport_midpoint
return self._get_page_coord(cl[0], cl[1])
return self._get_page_coord(*self.viewport_midpoint)
@property
def click_point(self):
"""返回元素接受点击的点的绝对坐标"""
cl = self.viewport_click_point
return self._get_page_coord(cl[0], cl[1])
return self._get_page_coord(*self.viewport_click_point)
@property
def viewport_location(self):
@ -77,7 +74,7 @@ class ElementRect(object):
"""返回元素左上角在屏幕上坐标,左上角为(0, 0)"""
vx, vy = self._ele.owner.rect.viewport_location
ex, ey = self.viewport_location
pr = self._ele.owner.run_js('return window.devicePixelRatio;')
pr = self._ele.owner._run_js('return window.devicePixelRatio;')
return (vx + ex) * pr, (ey + vy) * pr
@property
@ -85,7 +82,7 @@ class ElementRect(object):
"""返回元素中点在屏幕上坐标,左上角为(0, 0)"""
vx, vy = self._ele.owner.rect.viewport_location
ex, ey = self.viewport_midpoint
pr = self._ele.owner.run_js('return window.devicePixelRatio;')
pr = self._ele.owner._run_js('return window.devicePixelRatio;')
return (vx + ex) * pr, (ey + vy) * pr
@property
@ -93,21 +90,26 @@ class ElementRect(object):
"""返回元素中点在屏幕上坐标,左上角为(0, 0)"""
vx, vy = self._ele.owner.rect.viewport_location
ex, ey = self.viewport_click_point
pr = self._ele.owner.run_js('return window.devicePixelRatio;')
pr = self._ele.owner._run_js('return window.devicePixelRatio;')
return (vx + ex) * pr, (ey + vy) * pr
@property
def scroll_position(self):
"""返回滚动条位置,格式:(x, y)"""
r = self._ele._run_js('return this.scrollLeft.toString() + " " + this.scrollTop.toString();')
w, h = r.split(' ')
return int(w), int(h)
def _get_viewport_rect(self, quad):
"""按照类型返回在可视窗口中的范围
:param quad: 方框类型margin border padding
:return: 四个角坐标
"""
return self._ele.owner.run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id,
# nodeId=self._ele._node_id, objectId=self._ele._obj_id
)['model'][quad]
return self._ele.owner._run_cdp('DOM.getBoxModel', backendNodeId=self._ele._backend_id)['model'][quad]
def _get_page_coord(self, x, y):
"""根据视口坐标获取绝对坐标"""
r = self._ele.owner.run_cdp_loaded('Page.getLayoutMetrics')['visualViewport']
r = self._ele.owner._run_cdp_loaded('Page.getLayoutMetrics')['visualViewport']
sx = r['pageX']
sy = r['pageY']
return x + sx, y + sy
@ -174,17 +176,23 @@ class TabRect(object):
@property
def viewport_size_with_scrollbar(self):
"""返回视口宽高,包括滚动条,格式:(宽, 高)"""
r = self._owner.run_js('return window.innerWidth.toString() + " " + window.innerHeight.toString();')
r = self._owner._run_js('return window.innerWidth.toString() + " " + window.innerHeight.toString();')
w, h = r.split(' ')
return int(w), int(h)
@property
def scroll_position(self):
"""返回滚动条位置,格式:(x, y)"""
r = self._get_page_rect()['visualViewport']
return r['pageX'], r['pageY']
def _get_page_rect(self):
"""获取页面范围信息"""
return self._owner.run_cdp_loaded('Page.getLayoutMetrics')
return self._owner._run_cdp_loaded('Page.getLayoutMetrics')
def _get_window_rect(self):
"""获取窗口范围信息"""
return self._owner.browser.get_window_bounds(self._owner.tab_id)
return self._owner.browser._driver.run('Browser.getWindowForTarget', targetId=self._owner.tab_id)['bounds']
class FrameRect(object):
@ -214,8 +222,8 @@ class FrameRect(object):
@property
def size(self):
"""返回frame内页面尺寸格式(宽, 高)"""
w = self._frame.doc_ele.run_js('return this.body.scrollWidth')
h = self._frame.doc_ele.run_js('return this.body.scrollHeight')
w = self._frame.doc_ele._run_js('return this.body.scrollWidth')
h = self._frame.doc_ele._run_js('return this.body.scrollHeight')
return w, h
@property
@ -232,3 +240,11 @@ class FrameRect(object):
def viewport_corners(self):
"""返回元素四个角视口坐标,顺序:左上、右上、右下、左下"""
return self._frame.frame_ele.rect.viewport_corners
@property
def scroll_position(self):
"""返回滚动条位置,格式:(x, y)"""
r = self._frame.doc_ele._run_js('return this.documentElement.scrollLeft.toString() + " " '
'+ this.documentElement.scrollTop.toString();')
w, h = r.split(' ')
return int(w), int(h)

View File

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

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@ class SelectElement(object):
"""返回第一个被选中的option元素
:return: ChromiumElement对象或None
"""
ele = self._ele.run_js('return this.options[this.selectedIndex];')
ele = self._ele._run_js('return this.options[this.selectedIndex];')
return ele
@property
@ -69,7 +69,7 @@ class SelectElement(object):
for i in self.options:
change = True
mode = 'false' if i.states.is_selected else 'true'
i.run_js(f'this.selected={mode};')
i._run_js(f'this.selected={mode};')
if change:
self._dispatch_change()
@ -258,12 +258,12 @@ class SelectElement(object):
if not self.is_multi and len(option) > 1:
option = option[:1]
for o in option:
o.run_js(f'this.selected={mode};')
o._run_js(f'this.selected={mode};')
self._dispatch_change()
else:
option.run_js(f'this.selected={mode};')
option._run_js(f'this.selected={mode};')
self._dispatch_change()
def _dispatch_change(self):
"""触发修改动作"""
self._ele.run_js('this.dispatchEvent(new CustomEvent("change", {bubbles: true}));')
self._ele._run_js('this.dispatchEvent(new CustomEvent("change", {bubbles: true}));')

View File

@ -10,14 +10,14 @@ from time import sleep
from requests.structures import CaseInsensitiveDict
from .cookies_setter import SessionCookiesSetter, CookiesSetter, WebPageCookiesSetter
from .cookies_setter import SessionCookiesSetter, CookiesSetter, MixPageCookiesSetter, BrowserCookiesSetter
from .._functions.settings import Settings
from .._functions.tools import show_or_hide_browser
from .._functions.web import format_headers
from ..errors import ElementLostError, JavaScriptError
class BasePageSetter(object):
class BaseSetter(object):
def __init__(self, owner):
"""
:param owner: BasePage对象
@ -33,32 +33,6 @@ class BasePageSetter(object):
self._owner._none_ele_return_value = on_off
self._owner._none_ele_value = value
class ChromiumBaseSetter(BasePageSetter):
def __init__(self, owner):
"""
:param owner: ChromiumBase对象
"""
super().__init__(owner)
self._cookies_setter = None
@property
def load_mode(self):
"""返回用于设置页面加载策略的对象"""
return LoadMode(self._owner)
@property
def scroll(self):
"""返回用于设置页面滚动设置的对象"""
return PageScrollSetter(self._owner.scroll)
@property
def cookies(self):
"""返回用于设置cookies的对象"""
if self._cookies_setter is None:
self._cookies_setter = CookiesSetter(self._owner)
return self._cookies_setter
def retry_times(self, times):
"""设置连接失败重连次数"""
self._owner.retry_times = times
@ -67,189 +41,17 @@ class ChromiumBaseSetter(BasePageSetter):
"""设置连接失败重连间隔"""
self._owner.retry_interval = interval
def timeouts(self, base=None, page_load=None, script=None, implicit=None):
"""设置超时时间,单位为秒
:param base: 基本等待时间除页面加载和脚本超时其它等待默认使用
:param page_load: 页面加载超时时间
:param script: 脚本运行超时时间
:return: None
"""
base = base if base is not None else implicit
if base is not None:
self._owner.timeouts.base = base
self._owner._timeout = base
if page_load is not None:
self._owner.timeouts.page_load = page_load
if script is not None:
self._owner.timeouts.script = script
def user_agent(self, ua, platform=None):
"""为当前tab设置user agent只在当前tab有效
:param ua: user agent字符串
:param platform: platform字符串
:return: None
"""
keys = {'userAgent': ua}
if platform:
keys['platform'] = platform
self._owner.run_cdp('Emulation.setUserAgentOverride', **keys)
def session_storage(self, item, value):
"""设置或删除某项sessionStorage信息
:param item: 要设置的项
:param value: 项的值设置为False时删除该项
:return: None
"""
self._owner.run_cdp_loaded('DOMStorage.enable')
i = self._owner.run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey']
if value is False:
self._owner.run_cdp('DOMStorage.removeDOMStorageItem',
storageId={'storageKey': i, 'isLocalStorage': False}, key=item)
else:
self._owner.run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': False},
key=item, value=value)
self._owner.run_cdp_loaded('DOMStorage.disable')
def local_storage(self, item, value):
"""设置或删除某项localStorage信息
:param item: 要设置的项
:param value: 项的值设置为False时删除该项
:return: None
"""
self._owner.run_cdp_loaded('DOMStorage.enable')
i = self._owner.run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey']
if value is False:
self._owner.run_cdp('DOMStorage.removeDOMStorageItem',
storageId={'storageKey': i, 'isLocalStorage': True}, key=item)
else:
self._owner.run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': True},
key=item, value=value)
self._owner.run_cdp_loaded('DOMStorage.disable')
def upload_files(self, files):
"""等待上传的文件路径
:param files: 文件路径列表或字符串字符串时多个文件用回车分隔
:return: None
"""
if not self._owner._upload_list:
self._owner.driver.set_callback('Page.fileChooserOpened', self._owner._onFileChooserOpened)
self._owner.run_cdp('Page.setInterceptFileChooserDialog', enabled=True)
if isinstance(files, str):
files = files.split('\n')
elif isinstance(files, Path):
files = (files,)
self._owner._upload_list = [str(Path(i).absolute()) for i in files]
def headers(self, headers) -> None:
"""设置固定发送的headers
:param headers: dict格式的headers数据
:return: None
"""
self._owner.run_cdp('Network.enable')
self._owner.run_cdp('Network.setExtraHTTPHeaders', headers=format_headers(headers))
def auto_handle_alert(self, on_off=True, accept=True):
"""设置是否启用自动处理弹窗
:param on_off: bool表示开或关
:param accept: bool表示确定还是取消
:return: None
"""
self._owner._alert.auto = accept if on_off else None
def blocked_urls(self, urls):
"""设置要忽略的url
:param urls: 要忽略的url可用*通配符可输入多个传入None时清空已设置的内容
:return: None
"""
if not urls:
urls = []
elif isinstance(urls, str):
urls = (urls,)
if not isinstance(urls, (list, tuple)):
raise TypeError('urls需传入str、list或tuple类型。')
self._owner.run_cdp('Network.enable')
self._owner.run_cdp('Network.setBlockedURLs', urls=urls)
class TabSetter(ChromiumBaseSetter):
def __init__(self, owner):
"""
:param owner: 标签页对象
"""
super().__init__(owner)
@property
def window(self):
"""返回用于设置浏览器窗口的对象"""
return WindowSetter(self._owner)
def download_path(self, path):
"""设置下载路径
:param path: 下载路径
:return: None
"""
path = str(Path(path).absolute())
self._owner._download_path = path
self._owner.browser._dl_mgr.set_path(self._owner, path)
if self._owner._DownloadKit:
self._owner._DownloadKit.set.goal_path(path)
def download_file_name(self, name=None, suffix=None):
"""设置下一个被下载文件的名称
:param name: 文件名可不含后缀会自动使用远程文件后缀
:param suffix: 后缀名显式设置后缀名不使用远程文件后缀
:return: None
"""
self._owner.browser._dl_mgr.set_rename(self._owner.tab_id, name, suffix)
def when_download_file_exists(self, mode):
"""设置当存在同名文件时的处理方式
:param mode: 可在 'rename', 'overwrite', 'skip', 'r', 'o', 's'中选择
:return: None
"""
types = {'rename': 'rename', 'overwrite': 'overwrite', 'skip': 'skip', 'r': 'rename', 'o': 'overwrite',
's': 'skip'}
mode = types.get(mode, mode)
if mode not in types:
raise ValueError(f'''mode参数只能是 '{"', '".join(types.keys())}' 之一,现在是:{mode}''')
self._owner.browser._dl_mgr.set_file_exists(self._owner.tab_id, mode)
def activate(self):
"""使标签页处于最前面"""
self._owner.browser.activate_tab(self._owner.tab_id)
if path is None:
path = '.'
self._owner._download_path = str(Path(path).absolute())
class ChromiumPageSetter(TabSetter):
def tab_to_front(self, tab_or_id=None):
"""激活标签页使其处于最前面
:param tab_or_id: 标签页对象或id为None表示当前标签页
:return: None
"""
if not tab_or_id:
tab_or_id = self._owner.tab_id
elif not isinstance(tab_or_id, str): # 传入Tab对象
tab_or_id = tab_or_id.tab_id
self._owner.browser.activate_tab(tab_or_id)
def auto_handle_alert(self, on_off=True, accept=True, all_tabs=False):
"""设置是否启用自动处理弹窗
:param on_off: bool表示开或关
:param accept: bool表示确定还是取消
:param all_tabs: 是否为全局设置
:return: None
"""
if all_tabs:
Settings.auto_handle_alert = on_off
else:
self._owner._alert.auto = accept if on_off else None
class SessionPageSetter(BasePageSetter):
class SessionPageSetter(BaseSetter):
def __init__(self, owner):
"""
:param owner: SessionPage对象
@ -264,30 +66,21 @@ class SessionPageSetter(BasePageSetter):
self._cookies_setter = SessionCookiesSetter(self._owner)
return self._cookies_setter
def retry_times(self, times):
"""设置连接失败时重连次数"""
self._owner.retry_times = times
def retry_interval(self, interval):
"""设置连接失败时重连间隔"""
self._owner.retry_interval = interval
def download_path(self, path):
"""设置下载路径
:param path: 下载路径
:return: None
"""
path = str(Path(path).absolute())
self._owner._download_path = path
super().download_path(path)
if self._owner._DownloadKit:
self._owner._DownloadKit.set.goal_path(path)
self._owner._DownloadKit.set.goal_path(self._owner._download_path)
def timeout(self, second):
"""设置连接超时时间
:param second: 秒数
:return: None
"""
self._owner.timeout = second
self._owner._timeout = second
def encoding(self, encoding, set_all=True):
"""设置编码
@ -395,7 +188,272 @@ class SessionPageSetter(BasePageSetter):
self._owner.session.mount(url, adapter)
class WebPageSetter(ChromiumPageSetter):
class BrowserBaseSetter(BaseSetter):
"""Browser和ChromiumBase设置"""
def __init__(self, owner):
"""
:param owner: ChromiumBase对象
"""
super().__init__(owner)
self._cookies_setter = None
@property
def load_mode(self):
"""返回用于设置页面加载策略的对象"""
return LoadMode(self._owner)
def timeouts(self, base=None, page_load=None, script=None):
"""设置超时时间,单位为秒
:param base: 基本等待时间除页面加载和脚本超时其它等待默认使用
:param page_load: 页面加载超时时间
:param script: 脚本运行超时时间
:return: None
"""
if base is not None:
self._owner.timeouts.base = base
if page_load is not None:
self._owner.timeouts.page_load = page_load
if script is not None:
self._owner.timeouts.script = script
class BrowserSetter(BrowserBaseSetter):
@property
def cookies(self):
"""返回用于设置cookies的对象"""
if self._cookies_setter is None:
self._cookies_setter = BrowserCookiesSetter(self._owner)
return self._cookies_setter
def auto_handle_alert(self, on_off=True, accept=True):
"""设置是否启用自动处理弹窗
:param on_off: bool表示开或关
:param accept: bool表示确定还是取消
:return: None
"""
Settings.auto_handle_alert = accept if on_off else None
def download_path(self, path):
"""设置下载路径
:param path: 下载路径
:return: None
"""
super().download_path(path)
self._owner._dl_mgr.set_path('browser', self._owner._download_path)
def download_file_name(self, name=None, suffix=None):
"""设置下一个被下载文件的名称
:param name: 文件名可不含后缀会自动使用远程文件后缀
:param suffix: 后缀名显式设置后缀名不使用远程文件后缀
:return: None
"""
self._owner._dl_mgr.set_rename('browser', name, suffix)
def when_download_file_exists(self, mode):
"""设置当存在同名文件时的处理方式
:param mode: 可在 'rename', 'overwrite', 'skip', 'r', 'o', 's'中选择
:return: None
"""
types = {'rename': 'rename', 'overwrite': 'overwrite', 'skip': 'skip', 'r': 'rename', 'o': 'overwrite',
's': 'skip'}
mode = types.get(mode, mode)
if mode not in types:
raise ValueError(f'''mode参数只能是 '{"', '".join(types.keys())}' 之一,现在是:{mode}''')
self._owner._dl_mgr.set_file_exists('browser', mode)
# ---------- 即将废弃 ----------
def tab_to_front(self, tab_or_id):
"""激活标签页使其处于最前面
:param tab_or_id: 标签页对象或id
:return: None
"""
if not isinstance(tab_or_id, str): # 传入Tab对象
tab_or_id = tab_or_id.tab_id
self._owner.activate_tab(tab_or_id)
class ChromiumBaseSetter(BrowserBaseSetter):
@property
def scroll(self):
"""返回用于设置页面滚动设置的对象"""
return PageScrollSetter(self._owner.scroll)
@property
def cookies(self):
"""返回用于设置cookies的对象"""
if self._cookies_setter is None:
self._cookies_setter = CookiesSetter(self._owner)
return self._cookies_setter
def user_agent(self, ua, platform=None):
"""为当前tab设置user agent只在当前tab有效
:param ua: user agent字符串
:param platform: platform字符串
:return: None
"""
keys = {'userAgent': ua}
if platform:
keys['platform'] = platform
self._owner._run_cdp('Emulation.setUserAgentOverride', **keys)
def session_storage(self, item, value):
"""设置或删除某项sessionStorage信息
:param item: 要设置的项
:param value: 项的值设置为False时删除该项
:return: None
"""
self._owner._run_cdp_loaded('DOMStorage.enable')
i = self._owner._run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey']
if value is False:
self._owner._run_cdp('DOMStorage.removeDOMStorageItem',
storageId={'storageKey': i, 'isLocalStorage': False}, key=item)
else:
self._owner._run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': False},
key=item, value=value)
self._owner._run_cdp_loaded('DOMStorage.disable')
def local_storage(self, item, value):
"""设置或删除某项localStorage信息
:param item: 要设置的项
:param value: 项的值设置为False时删除该项
:return: None
"""
self._owner._run_cdp_loaded('DOMStorage.enable')
i = self._owner._run_cdp('Storage.getStorageKeyForFrame', frameId=self._owner._frame_id)['storageKey']
if value is False:
self._owner._run_cdp('DOMStorage.removeDOMStorageItem',
storageId={'storageKey': i, 'isLocalStorage': True}, key=item)
else:
self._owner._run_cdp('DOMStorage.setDOMStorageItem', storageId={'storageKey': i, 'isLocalStorage': True},
key=item, value=value)
self._owner._run_cdp_loaded('DOMStorage.disable')
def upload_files(self, files):
"""等待上传的文件路径
:param files: 文件路径列表或字符串字符串时多个文件用回车分隔
:return: None
"""
if not self._owner._upload_list:
self._owner.driver.set_callback('Page.fileChooserOpened', self._owner._onFileChooserOpened)
self._owner._run_cdp('Page.setInterceptFileChooserDialog', enabled=True)
if isinstance(files, str):
files = files.split('\n')
elif isinstance(files, Path):
files = (files,)
self._owner._upload_list = [str(Path(i).absolute()) for i in files]
def headers(self, headers) -> None:
"""设置固定发送的headers
:param headers: dict格式的headers数据或从浏览器复制的headers文本\n分行
:return: None
"""
self._owner._run_cdp('Network.enable')
self._owner._run_cdp('Network.setExtraHTTPHeaders', headers=format_headers(headers))
def auto_handle_alert(self, on_off=True, accept=True):
"""设置是否启用自动处理弹窗
:param on_off: bool表示开或关
:param accept: bool表示确定还是取消
:return: None
"""
self._owner._alert.auto = accept if on_off else None
def blocked_urls(self, urls):
"""设置要忽略的url
:param urls: 要忽略的url可用*通配符可输入多个传入None时清空已设置的内容
:return: None
"""
if not urls:
urls = []
elif isinstance(urls, str):
urls = (urls,)
if not isinstance(urls, (list, tuple)):
raise TypeError('urls需传入str、list或tuple类型。')
self._owner._run_cdp('Network.enable')
self._owner._run_cdp('Network.setBlockedURLs', urls=urls)
class TabSetter(ChromiumBaseSetter):
def __init__(self, owner):
"""
:param owner: 标签页对象
"""
super().__init__(owner)
@property
def window(self):
"""返回用于设置浏览器窗口的对象"""
return WindowSetter(self._owner)
def download_path(self, path):
"""设置下载路径
:param path: 下载路径
:return: None
"""
super().download_path(path)
self._owner.browser._dl_mgr.set_path(self._owner, self._owner._download_path)
if self._owner._DownloadKit:
self._owner._DownloadKit.set.goal_path(self._owner._download_path)
def download_file_name(self, name=None, suffix=None):
"""设置下一个被下载文件的名称
:param name: 文件名可不含后缀会自动使用远程文件后缀
:param suffix: 后缀名显式设置后缀名不使用远程文件后缀
:return: None
"""
self._owner.browser._dl_mgr.set_rename(self._owner.tab_id, name, suffix)
def when_download_file_exists(self, mode):
"""设置当存在同名文件时的处理方式
:param mode: 可在 'rename', 'overwrite', 'skip', 'r', 'o', 's'中选择
:return: None
"""
types = {'rename': 'rename', 'overwrite': 'overwrite', 'skip': 'skip', 'r': 'rename', 'o': 'overwrite',
's': 'skip'}
mode = types.get(mode, mode)
if mode not in types:
raise ValueError(f'''mode参数只能是 '{"', '".join(types.keys())}' 之一,现在是:{mode}''')
self._owner.browser._dl_mgr.set_file_exists(self._owner.tab_id, mode)
def activate(self):
"""使标签页处于最前面"""
self._owner.browser.activate_tab(self._owner.tab_id)
class ChromiumPageSetter(TabSetter):
def auto_handle_alert(self, on_off=True, accept=True, all_tabs=False):
"""设置是否启用自动处理弹窗
:param on_off: bool表示开或关
:param accept: bool表示确定还是取消
:param all_tabs: 是否为全局设置
:return: None
"""
if all_tabs:
Settings.auto_handle_alert = on_off
else:
self._owner._alert.auto = accept if on_off else None
# ---------- 即将废弃 ----------
def tab_to_front(self, tab_or_id=None):
"""激活标签页使其处于最前面
:param tab_or_id: 标签页对象或id为None表示当前标签页
:return: None
"""
if not tab_or_id:
tab_or_id = self._owner.tab_id
elif not isinstance(tab_or_id, str): # 传入Tab对象
tab_or_id = tab_or_id.tab_id
self._owner.browser.activate_tab(tab_or_id)
class MixPageSetter(ChromiumPageSetter):
def __init__(self, owner):
super().__init__(owner)
self._session_setter = SessionPageSetter(self._owner)
@ -405,7 +463,7 @@ class WebPageSetter(ChromiumPageSetter):
def cookies(self):
"""返回用于设置cookies的对象"""
if self._cookies_setter is None:
self._cookies_setter = WebPageCookiesSetter(self._owner)
self._cookies_setter = MixPageCookiesSetter(self._owner)
return self._cookies_setter
def headers(self, headers) -> None:
@ -426,7 +484,7 @@ class WebPageSetter(ChromiumPageSetter):
self._chromium_setter.user_agent(ua, platform)
class WebPageTabSetter(TabSetter):
class MixTabSetter(TabSetter):
def __init__(self, owner):
super().__init__(owner)
self._session_setter = SessionPageSetter(self._owner)
@ -436,7 +494,7 @@ class WebPageTabSetter(TabSetter):
def cookies(self):
"""返回用于设置cookies的对象"""
if self._cookies_setter is None:
self._cookies_setter = WebPageCookiesSetter(self._owner)
self._cookies_setter = MixPageCookiesSetter(self._owner)
return self._cookies_setter
def headers(self, headers) -> None:
@ -456,6 +514,17 @@ class WebPageTabSetter(TabSetter):
if self._owner._has_driver:
self._chromium_setter.user_agent(ua, platform)
def timeouts(self, base=None, page_load=None, script=None):
"""设置超时时间,单位为秒
:param base: 基本等待时间除页面加载和脚本超时其它等待默认使用
:param page_load: 页面加载超时时间
:param script: 脚本运行超时时间
:return: None
"""
super().timeouts(base=base, page_load=page_load, script=script)
if base is not None:
self._owner._timeout = base
class ChromiumElementSetter(object):
def __init__(self, ele):
@ -464,19 +533,19 @@ class ChromiumElementSetter(object):
"""
self._ele = ele
def attr(self, name, value):
def attr(self, name, value=''):
"""设置元素attribute属性
:param name: 属性名
:param value: 属性值
:return: None
"""
try:
self._ele.owner.run_cdp('DOM.setAttributeValue',
nodeId=self._ele._node_id, name=name, value=str(value))
self._ele.owner._run_cdp('DOM.setAttributeValue',
nodeId=self._ele._node_id, name=name, value=str(value))
except ElementLostError:
self._ele._refresh_id()
self._ele.owner.run_cdp('DOM.setAttributeValue',
nodeId=self._ele._node_id, name=name, value=str(value))
self._ele.owner._run_cdp('DOM.setAttributeValue',
nodeId=self._ele._node_id, name=name, value=str(value))
def property(self, name, value):
"""设置元素property属性
@ -485,7 +554,7 @@ class ChromiumElementSetter(object):
:return: None
"""
value = value.replace('"', r'\"')
self._ele.run_js(f'this.{name}="{value}";')
self._ele._run_js(f'this.{name}="{value}";')
def style(self, name, value):
"""设置元素style样式
@ -494,7 +563,7 @@ class ChromiumElementSetter(object):
:return: None
"""
try:
self._ele.run_js(f'this.style.{name}="{value}";')
self._ele._run_js(f'this.style.{name}="{value}";')
except JavaScriptError:
raise ValueError(f'设置失败,请检查属性名{name}')
@ -522,6 +591,22 @@ class ChromiumFrameSetter(ChromiumBaseSetter):
"""
self._owner.frame_ele.set.attr(name, value)
def property(self, name, value):
"""设置元素property属性
:param name: 属性名
:param value: 属性值
:return: None
"""
self._owner.frame_ele.set.property(name=name, value=value)
def style(self, name, value):
"""设置元素style样式
:param name: 样式名称
:param value: 样式值
:return: None
"""
self._owner.frame_ele.set.style(name=name, value=value)
class LoadMode(object):
"""用于设置页面加载策略的类"""
@ -578,7 +663,7 @@ class PageScrollSetter(object):
if not isinstance(on_off, bool):
raise TypeError('on_off必须为bool。')
b = 'smooth' if on_off else 'auto'
self._scroll._driver.run_js(f'document.documentElement.style.setProperty("scroll-behavior","{b}");')
self._scroll._driver._run_js(f'document.documentElement.style.setProperty("scroll-behavior","{b}");')
self._scroll._wait_complete = on_off
@ -652,7 +737,7 @@ class WindowSetter(object):
"""获取窗口位置及大小信息"""
for _ in range(50):
try:
return self._owner.run_cdp('Browser.getWindowForTarget')
return self._owner._run_cdp('Browser.getWindowForTarget')
except:
sleep(.1)
@ -662,7 +747,7 @@ class WindowSetter(object):
:return: None
"""
try:
self._owner.run_cdp('Browser.setWindowBounds', windowId=self._window_id, bounds=bounds)
self._owner._run_cdp('Browser.setWindowBounds', windowId=self._window_id, bounds=bounds)
except:
raise RuntimeError('浏览器全屏或最小化状态时请先调用set.window.normal()恢复正常状态。')

View File

@ -11,88 +11,35 @@ from typing import Union, Tuple, Literal, Any, Optional
from requests.adapters import HTTPAdapter
from requests.auth import HTTPBasicAuth
from .cookies_setter import SessionCookiesSetter, CookiesSetter, WebPageCookiesSetter
from .cookies_setter import SessionCookiesSetter, CookiesSetter, MixPageCookiesSetter, BrowserCookiesSetter
from .scroller import PageScroller
from .._base.base import BasePage
from .._base.browser import Chromium
from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage
from .._pages.chromium_tab import ChromiumTab, WebPageTab
from .._pages.tabs import ChromiumTab, MixTab
from .._pages.session_page import SessionPage
from .._pages.web_page import WebPage
from .._pages.mix_page import MixPage
FILE_EXISTS = Literal['skip', 'rename', 'overwrite', 's', 'r', 'o']
class BasePageSetter(object):
def __init__(self, owner: BasePage):
self._owner: BasePage = ...
class BaseSetter(object):
def __init__(self, owner: Union[Chromium, BasePage]):
self._owner: Union[Chromium, BasePage] = ...
def NoneElement_value(self, value: Any = None, on_off: bool = True) -> None: ...
class ChromiumBaseSetter(BasePageSetter):
def __init__(self, owner):
self._owner: ChromiumBase = ...
self._cookies_setter: CookiesSetter = ...
@property
def load_mode(self) -> LoadMode: ...
@property
def scroll(self) -> PageScrollSetter: ...
@property
def cookies(self) -> CookiesSetter: ...
def retry_times(self, times: int) -> None: ...
def retry_interval(self, interval: float) -> None: ...
def timeouts(self, base: float = None, page_load: float = None, script: float = None) -> None: ...
def user_agent(self, ua: str, platform: str = None) -> None: ...
def session_storage(self, item: str, value: Union[str, bool]) -> None: ...
def local_storage(self, item: str, value: Union[str, bool]) -> None: ...
def headers(self, headers: Union[dict, str]) -> None: ...
def auto_handle_alert(self, on_off: bool = True, accept: bool = True) -> None: ...
def upload_files(self, files: Union[str, Path, list, tuple]) -> None: ...
def blocked_urls(self, urls: Union[list, tuple, str, None]) -> None: ...
def download_path(self, path: Union[str, Path, None]) -> None: ...
class TabSetter(ChromiumBaseSetter):
_owner: ChromiumTab = ...
def __init__(self, owner: Union[ChromiumTab, WebPageTab, WebPage, ChromiumPage]): ...
@property
def window(self) -> WindowSetter: ...
def download_path(self, path: Union[str, Path]) -> None: ...
def download_file_name(self, name: str = None, suffix: str = None) -> None: ...
def when_download_file_exists(self, mode: FILE_EXISTS) -> None: ...
def activate(self) -> None: ...
class ChromiumPageSetter(TabSetter):
_owner: ChromiumPage = ...
def tab_to_front(self, tab_or_id: Union[str, ChromiumTab] = None) -> None: ...
def auto_handle_alert(self, on_off: bool = True, accept: bool = True, all_tabs: bool = False) -> None: ...
class SessionPageSetter(BasePageSetter):
class SessionPageSetter(BaseSetter):
_owner: SessionPage = ...
_cookies_setter: Optional[SessionCookiesSetter] = ...
@ -105,7 +52,7 @@ class SessionPageSetter(BasePageSetter):
def retry_interval(self, interval: float) -> None: ...
def download_path(self, path: Union[str, Path]) -> None: ...
def download_path(self, path: Union[str, Path, None]) -> None: ...
def timeout(self, second: float) -> None: ...
@ -138,8 +85,83 @@ class SessionPageSetter(BasePageSetter):
def add_adapter(self, url: str, adapter: HTTPAdapter) -> None: ...
class WebPageSetter(ChromiumPageSetter):
_owner: WebPage = ...
class BrowserBaseSetter(BaseSetter):
_cookies_setter: Optional[CookiesSetter] = ...
@property
def load_mode(self) -> LoadMode: ...
def timeouts(self, base=None, page_load=None, script=None) -> None: ...
class BrowserSetter(BrowserBaseSetter):
_owner: Chromium = ...
_cookies_setter: BrowserCookiesSetter = ...
@property
def cookies(self) -> BrowserCookiesSetter: ...
def auto_handle_alert(self, on_off: bool = True, accept: bool = True): ...
def download_path(self, path: Union[Path, str, None]): ...
def download_file_name(self, name: str = None, suffix: str = None): ...
def when_download_file_exists(self, mode: FILE_EXISTS): ...
class ChromiumBaseSetter(BrowserBaseSetter):
_owner: ChromiumBase = ...
_cookies_setter: CookiesSetter = ...
def __init__(self, owner): ...
@property
def scroll(self) -> PageScrollSetter: ...
@property
def cookies(self) -> CookiesSetter: ...
def user_agent(self, ua: str, platform: str = None) -> None: ...
def session_storage(self, item: str, value: Union[str, bool]) -> None: ...
def local_storage(self, item: str, value: Union[str, bool]) -> None: ...
def headers(self, headers: Union[dict, str]) -> None: ...
def auto_handle_alert(self, on_off: bool = True, accept: bool = True) -> None: ...
def upload_files(self, files: Union[str, Path, list, tuple]) -> None: ...
def blocked_urls(self, urls: Union[list, tuple, str, None]) -> None: ...
class TabSetter(ChromiumBaseSetter):
_owner: ChromiumTab = ...
def __init__(self, owner: Union[ChromiumTab, MixTab, MixPage, ChromiumPage]): ...
@property
def window(self) -> WindowSetter: ...
def download_path(self, path: Union[str, Path, None]) -> None: ...
def download_file_name(self, name: str = None, suffix: str = None) -> None: ...
def when_download_file_exists(self, mode: FILE_EXISTS) -> None: ...
def activate(self) -> None: ...
class ChromiumPageSetter(TabSetter):
_owner: ChromiumPage = ...
def auto_handle_alert(self, on_off: bool = True, accept: bool = True, all_tabs: bool = False) -> None: ...
class MixPageSetter(ChromiumPageSetter):
_owner: MixPage = ...
_session_setter: SessionPageSetter = ...
_chromium_setter: ChromiumPageSetter = ...
@ -148,11 +170,11 @@ class WebPageSetter(ChromiumPageSetter):
def headers(self, headers: Union[str, dict]) -> None: ...
@property
def cookies(self) -> WebPageCookiesSetter: ...
def cookies(self) -> MixPageCookiesSetter: ...
class WebPageTabSetter(TabSetter):
_owner: WebPageTab = ...
class MixTabSetter(TabSetter):
_owner: MixTab = ...
_session_setter: SessionPageSetter = ...
_chromium_setter: ChromiumBaseSetter = ...
@ -161,14 +183,16 @@ class WebPageTabSetter(TabSetter):
def headers(self, headers: Union[str, dict]) -> None: ...
@property
def cookies(self) -> WebPageCookiesSetter: ...
def cookies(self) -> MixPageCookiesSetter: ...
def timeouts(self, base: float = None, page_load: float = None, script: float = None) -> None: ...
class ChromiumElementSetter(object):
def __init__(self, ele: ChromiumElement):
self._ele: ChromiumElement = ...
def attr(self, name: str, value: str) -> None: ...
def attr(self, name: str, value: str = '') -> None: ...
def property(self, name: str, value: str) -> None: ...
@ -186,8 +210,9 @@ class ChromiumFrameSetter(ChromiumBaseSetter):
class LoadMode(object):
def __init__(self, owner: ChromiumBase):
self._owner: ChromiumBase = ...
_owner: Union[Chromium, ChromiumBase] = ...
def __init__(self, owner: Union[Chromium, ChromiumBase]): ...
def __call__(self, value: str) -> None: ...

View File

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

View File

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

View File

@ -5,24 +5,42 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from typing import Union, Tuple, Literal, List
from typing import Union, Tuple, List
from .downloader import DownloadMission
from .._base.browser import Chromium
from .._elements.chromium_element import ChromiumElement
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage
from .._pages.mix_page import MixPage
from .._pages.tabs import ChromiumTab, MixTab
class OriginWaiter(object):
def __call__(self, second: float, scope: float = None) -> None: ...
_owner = ...
def __init__(self, owner): ...
def __call__(self, second: float, scope: float = None): ...
class BrowserWaiter(OriginWaiter):
_owner: Chromium = ...
def __init__(self, owner: Chromium): ...
def __call__(self, second: float, scope: float = None) -> Chromium: ...
def download_begin(self, timeout: float = None, cancel_it: bool = False) -> DownloadMission: ...
def new_tab(self, timeout: float = None, curr_tab: str = None, raise_err: bool = None) -> Union[str, bool]: ...
def all_downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ...
class BaseWaiter(OriginWaiter):
def __init__(self, page: ChromiumBase):
self._driver: ChromiumBase = ...
def __call__(self, second: float, scope: float = None) -> None: ...
_owner: ChromiumBase = ...
def ele_deleted(self,
loc_or_ele: Union[str, tuple, ChromiumElement],
@ -64,58 +82,94 @@ class BaseWaiter(OriginWaiter):
class TabWaiter(BaseWaiter):
_owner: Union[ChromiumTab, MixTab] = ...
def __init__(self, owner: Union[ChromiumTab, MixTab]): ...
def __call__(self, second: float, scope: float = None) -> Union[ChromiumTab, MixTab]: ...
def downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ...
def alert_closed(self) -> None: ...
def url_change(self, text: str, exclude: bool = False,
timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumTab, MixTab]: ...
def title_change(self, text: str, exclude: bool = False,
timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumTab, MixTab]: ...
class PageWaiter(TabWaiter):
_driver: ChromiumPage = ...
_owner: Union[ChromiumPage, MixPage] = ...
def __init__(self, owner: Union[ChromiumPage, MixPage]): ...
def __call__(self, second: float, scope: float = None) -> Union[ChromiumPage, MixPage]: ...
def new_tab(self, timeout: float = None, raise_err: bool = None) -> Union[str, bool]: ...
def all_downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ...
def url_change(self, text: str, exclude: bool = False,
timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumPage, MixPage]: ...
def title_change(self, text: str, exclude: bool = False,
timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumPage, MixPage]: ...
class ElementWaiter(OriginWaiter):
def __init__(self, owner: ChromiumBase, ele: ChromiumElement):
self._ele: ChromiumElement = ...
self._owner: ChromiumBase = ...
_owner: ChromiumElement = ...
_ele: ChromiumElement = ...
def __call__(self, second: float, scope: float = None) -> None: ...
def __init__(self, owner: ChromiumElement): ...
def deleted(self, timeout: float = None, raise_err: bool = None) -> bool: ...
def __call__(self, second: float, scope: float = None) -> ChromiumElement: ...
def displayed(self, timeout: float = None, raise_err: bool = None) -> bool: ...
@property
def _timeout(self) -> float: ...
def hidden(self, timeout: float = None, raise_err: bool = None) -> bool: ...
def deleted(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ...
def covered(self, timeout: float = None, raise_err: bool = None) -> Union[Literal[False], int]: ...
def displayed(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ...
def not_covered(self, timeout: float = None, raise_err: bool = None) -> bool: ...
def hidden(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ...
def enabled(self, timeout: float = None, raise_err: bool = None) -> bool: ...
def covered(self, timeout: float = None, raise_err: bool = None) -> Union[False, int]: ...
def disabled(self, timeout: float = None, raise_err: bool = None) -> bool: ...
def not_covered(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ...
def clickable(self, wait_moved: bool = True, timeout: float = None, raise_err: bool = None) -> bool: ...
def enabled(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ...
def disabled(self, timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ...
def clickable(self, wait_moved: bool = True,
timeout: float = None, raise_err: bool = None) -> Union[ChromiumElement, False]: ...
def has_rect(self,
timeout: float = None,
raise_err: bool = None) -> Union[Literal[False], List[Tuple[float, float]]]: ...
raise_err: bool = None) -> Union[False, List[Tuple[float, float]]]: ...
def disabled_or_deleted(self, timeout: float = None, raise_err: bool = None) -> bool: ...
def stop_moving(self, timeout: float = None, gap: float = .1, raise_err: bool = None) -> bool: ...
def stop_moving(self, timeout: float = None, gap: float = .1, raise_err: bool = None) -> Union[ChromiumElement, False]: ...
def _wait_state(self,
attr: str,
mode: bool = False,
timeout: float = None,
raise_err: bool = None,
err_text: str = None) -> bool: ...
err_text: str = None) -> Union[ChromiumElement, False]: ...
class FrameWaiter(BaseWaiter, ElementWaiter):
def __init__(self, frame: ChromiumFrame): ...
_owner: ChromiumFrame = ...
def __init__(self, owner: ChromiumFrame): ...
def __call__(self, second: float, scope: float = None) -> ChromiumFrame: ...
def url_change(self, text: str, exclude: bool = False,
timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumFrame]: ...
def title_change(self, text: str, exclude: bool = False,
timeout: float = None, raise_err: bool = None) -> Union[False, ChromiumFrame]: ...

View File

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

View File

@ -9,7 +9,7 @@ from ._elements.chromium_element import ChromiumElement, ShadowRoot
from ._elements.none_element import NoneElement
from ._elements.session_element import SessionElement
from ._pages.chromium_frame import ChromiumFrame
from ._pages.chromium_tab import ChromiumTab, WebPageTab
from ._pages.tabs import ChromiumTab, MixTab
__all__ = ['ChromiumElement', 'ShadowRoot', 'NoneElement', 'SessionElement', 'ChromiumFrame', 'ChromiumTab',
'WebPageTab']
'MixTab']