Compare commits

...

3 Commits

Author SHA1 Message Date
g1879
cfed80e196 4.0.4.13(+)
动作链wait()增加随机功能;
latest_tab当tab设置为单例时返回Tab对象,否则返回tab id;
修复新标签页重复创建连接问题;
修复等待新tab不正确问题
2024-03-17 01:20:06 +08:00
g1879
4cbbf1dee1 微调 2024-03-15 01:37:18 +08:00
g1879
991baf18d1 4.0.4.12优化set.cookies()逻辑 2024-03-14 17:18:44 +08:00
16 changed files with 114 additions and 98 deletions

View File

@ -14,4 +14,4 @@ from ._configs.chromium_options import ChromiumOptions
from ._configs.session_options import SessionOptions
__all__ = ['ChromiumPage', 'ChromiumOptions', 'SessionOptions', 'SessionPage', 'WebPage', '__version__']
__version__ = '4.0.4.11'
__version__ = '4.0.4.13'

View File

@ -72,7 +72,9 @@ class Browser(object):
:param owner: 使用该驱动的对象
:return: Driver对象
"""
d = self._drivers.pop(tab_id, Driver(tab_id, 'page', self.address))
d = self._drivers.pop(tab_id, None)
if not d:
d = Driver(tab_id, 'page', self.address)
d.owner = owner
self._all_drivers.setdefault(tab_id, set()).add(d)
return d
@ -188,6 +190,30 @@ class Browser(object):
"""
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()

View File

@ -56,6 +56,8 @@ class Browser(object):
def get_window_bounds(self, tab_id: str = None) -> dict: ...
def new_tab(self, new_window: bool = False, background: bool = False, new_context: bool = False) -> str: ...
def reconnect(self) -> None: ...
def connect_to_page(self) -> None: ...

View File

@ -213,8 +213,8 @@ class SessionOptions(object):
return self._cookies
def set_cookies(self, cookies):
"""设置cookies信息
:param cookies: cookies可为CookieJar, list, tuple, str, dict传入None可在ini文件标记删除
"""设置一个或多个cookies信息
:param cookies: cookies可为Cookie, CookieJar, list, tuple, str, dict传入None可在ini文件标记删除
:return: 返回当前对象
"""
cookies = cookies if cookies is None else list(cookies_to_tuple(cookies))

View File

@ -5,13 +5,13 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from http.cookiejar import CookieJar, Cookie
from pathlib import Path
from typing import Any, Union, Tuple, Optional
from requests import Session
from requests.adapters import HTTPAdapter
from requests.auth import HTTPBasicAuth
from requests.cookies import RequestsCookieJar
from requests.structures import CaseInsensitiveDict
@ -60,7 +60,7 @@ class SessionOptions(object):
@property
def cookies(self) -> list: ...
def set_cookies(self, cookies: Union[RequestsCookieJar, list, tuple, str, dict, None]) -> SessionOptions: ...
def set_cookies(self, cookies: Union[Cookie, CookieJar, list, tuple, str, dict, None]) -> SessionOptions: ...
@property
def auth(self) -> Union[Tuple[str, str], HTTPBasicAuth]: ...

View File

@ -7,11 +7,10 @@
"""
from datetime import datetime
from html import unescape
from http.cookiejar import Cookie
from http.cookiejar import Cookie, CookieJar
from re import sub
from urllib.parse import urlparse, urljoin, urlunparse
from requests.cookies import RequestsCookieJar
from tldextract import extract
@ -181,17 +180,14 @@ def cookie_to_dict(cookie):
cookie_dict = cookie
elif isinstance(cookie, str):
cookie = cookie.rstrip(';,').split(',' if ',' in cookie else ';')
cookie_dict = {}
for key, attr in enumerate(cookie):
attr_val = attr.lstrip().split('=', 1)
if key == 0:
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 ''
else:
cookie_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else ''
return cookie_dict
@ -206,17 +202,24 @@ def cookies_to_tuple(cookies):
:param cookies: cookies信息可为CookieJar, list, tuple, str, dict
:return: 返回tuple形式的cookies
"""
if isinstance(cookies, (list, tuple, RequestsCookieJar)):
if isinstance(cookies, (list, tuple, CookieJar)):
cookies = tuple(cookie_to_dict(cookie) for cookie in cookies)
elif isinstance(cookies, str):
cookies = tuple(cookie_to_dict(c.lstrip()) for c in cookies.rstrip(';,').split(',' if ',' in cookies else ';'))
c_dict = {}
for attr in cookies.strip().rstrip(';,').split(',' if ',' in cookies else ';'):
attr_val = attr.strip().split('=', 1)
c_dict[attr_val[0]] = attr_val[1] if len(attr_val) == 2 else ''
cookies = _dict_cookies_to_tuple(c_dict)
elif isinstance(cookies, dict):
cookies = tuple({'name': cookie, 'value': cookies[cookie]} for cookie in cookies)
cookies = _dict_cookies_to_tuple(cookies)
elif isinstance(cookies, Cookie):
cookies = (cookie_to_dict(cookies),)
else:
raise TypeError('cookies参数必须为RequestsCookieJar、list、tuple、str或dict类型。')
raise TypeError('cookies参数必须为Cookie、CookieJar、list、tuple、str或dict类型。')
return cookies
@ -227,8 +230,7 @@ def set_session_cookies(session, cookies):
:param cookies: cookies信息
:return: None
"""
cookies = cookies_to_tuple(cookies)
for cookie in cookies:
for cookie in cookies_to_tuple(cookies):
if cookie['value'] is None:
cookie['value'] = ''
@ -370,6 +372,7 @@ def tree(ele_or_page):
:param ele_or_page: 页面或元素对象
:return: None
"""
def _tree(obj, last_one=True, body=''):
list_ele = obj.children()
length = len(list_ele)
@ -409,3 +412,15 @@ 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

@ -37,7 +37,7 @@ 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]) -> tuple: ...
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: ...

View File

@ -14,6 +14,7 @@ from requests import get
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 .._pages.chromium_base import ChromiumBase, get_mhtml, get_pdf, Timeout
from .._pages.chromium_tab import ChromiumTab
@ -130,8 +131,9 @@ class ChromiumPage(ChromiumBase):
@property
def latest_tab(self):
"""返回最新的标签页对象,最新标签页指最后创建或最后被激活的"""
return self.get_tab(self.tab_ids[0])
"""返回最新的标签页,最新标签页指最后创建或最后被激活的
当Settings.singleton_tab_obj==True时返回Tab对象否则返回tab id"""
return self.get_tab(self.tab_ids[0], as_id=not Settings.singleton_tab_obj)
@property
def process_id(self):
@ -163,7 +165,12 @@ class ChromiumPage(ChromiumBase):
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 id_or_num
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
@ -202,32 +209,11 @@ class ChromiumPage(ChromiumBase):
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
tab = ChromiumTab(self, tab_id=self._new_tab(new_window, background, new_context))
tab = ChromiumTab(self, tab_id=self.browser.new_tab(new_window, background, new_context))
if url:
tab.get(url)
return tab
def _new_tab(self, new_window=False, background=False, new_context=False):
"""新建一个标签页
:param new_window: 是否在新窗口打开标签页
:param background: 是否不激活新标签页如new_window为True则无效
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
bid = None
if new_context:
bid = self.browser.run_cdp('Target.createBrowserContext')['browserContextId']
kwargs = {'url': ''}
if new_window:
kwargs['newWindow'] = True
if background:
kwargs['background'] = True
if bid:
kwargs['browserContextId'] = bid
return self.browser.run_cdp('Target.createTarget', **kwargs)['targetId']
def close(self):
"""关闭Page管理的标签页"""
self.close_tabs(self.tab_id)

View File

@ -57,7 +57,7 @@ class ChromiumPage(ChromiumBase):
def wait(self) -> PageWaiter: ...
@property
def latest_tab(self) -> Union[ChromiumTab, ChromiumPage]: ...
def latest_tab(self) -> Union[ChromiumTab, ChromiumPage, str]: ...
@property
def process_id(self) -> Optional[int]: ...
@ -102,8 +102,6 @@ class ChromiumPage(ChromiumBase):
def new_tab(self, url: str = None, new_window: bool = False, background: bool = False,
new_context: bool = False) -> ChromiumTab: ...
def _new_tab(self, new_window: bool = False, background: bool = False, new_context: bool = False) -> str: ...
def close(self) -> None: ...
def close_tabs(self, tabs_or_ids: Union[str, ChromiumTab, List[Union[str, ChromiumTab]],

View File

@ -362,7 +362,7 @@ class WebPage(SessionPage, ChromiumPage, BasePage):
:param new_context: 是否创建新的上下文
:return: 新标签页对象
"""
tab = WebPageTab(self, tab_id=self._new_tab(new_window, background, new_context))
tab = WebPageTab(self, tab_id=self.browser.new_tab(new_window, background, new_context))
if url:
tab.get(url)
return tab

View File

@ -298,10 +298,13 @@ class Actions:
input_text_or_keys(self.owner, text)
return self
def wait(self, second):
"""等待若干秒"""
sleep(second)
return self
def wait(self, second, scope=None):
"""等待若干秒,如传入两个参数,等待时间为这两个数间的一个随机数
:param second: 秒数
:param scope: 随机数范围
:return: None
"""
self.owner.wait(second=second, scope=scope)
def _get_key_data(self, key, action):
"""获取用于发送的按键信息

View File

@ -100,7 +100,7 @@ class Actions:
def input(self, text: Any) -> Actions: ...
def wait(self, second: float) -> Actions: ...
def wait(self, second: float, scope: float = None) -> Actions: ...
def _get_key_data(self, key: str, action: str) -> dict: ...

View File

@ -5,8 +5,6 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from http.cookiejar import Cookie
from .._functions.web import set_browser_cookies, set_session_cookies
@ -22,8 +20,6 @@ class CookiesSetter(object):
:param cookies: cookies信息
:return: None
"""
if (isinstance(cookies, dict) and 'name' in cookies and 'value' in cookies) or isinstance(cookies, Cookie):
cookies = [cookies]
set_browser_cookies(self._owner, cookies)
def remove(self, name, url=None, domain=None, path=None):
@ -59,8 +55,6 @@ class SessionCookiesSetter(object):
:param cookies: cookies信息
:return: None
"""
if (isinstance(cookies, dict) and 'name' in cookies and 'value' in cookies) or isinstance(cookies, Cookie):
cookies = [cookies]
set_session_cookies(self._owner.session, cookies)
def remove(self, name):

View File

@ -5,11 +5,9 @@
@Copyright: (c) 2024 by g1879, Inc. All Rights Reserved.
@License : BSD 3-Clause.
"""
from http.cookiejar import Cookie
from http.cookiejar import Cookie, CookieJar
from typing import Union
from requests.cookies import RequestsCookieJar
from .._pages.chromium_base import ChromiumBase
from .._pages.chromium_tab import WebPageTab
from .._pages.session_page import SessionPage
@ -21,7 +19,7 @@ class CookiesSetter(object):
def __init__(self, page: ChromiumBase): ...
def __call__(self, cookies: Union[RequestsCookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def __call__(self, cookies: Union[CookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def remove(self, name: str, url: str = None, domain: str = None, path: str = None) -> None: ...
@ -33,7 +31,7 @@ class SessionCookiesSetter(object):
def __init__(self, page: SessionPage): ...
def __call__(self, cookies: Union[RequestsCookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def __call__(self, cookies: Union[CookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def remove(self, name: str) -> None: ...
@ -45,7 +43,7 @@ class WebPageCookiesSetter(CookiesSetter, SessionCookiesSetter):
def __init__(self, page: SessionPage): ...
def __call__(self, cookies: Union[RequestsCookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def __call__(self, cookies: Union[CookieJar, Cookie, list, tuple, str, dict]) -> None: ...
def remove(self, name: str, url: str = None, domain: str = None, path: str = None) -> None: ...

View File

@ -11,13 +11,7 @@ from .._functions.settings import Settings
from ..errors import WaitTimeoutError, NoRectError
class BaseWaiter(object):
def __init__(self, page_or_ele):
"""
:param page_or_ele: 页面对象或元素对象
"""
self._driver = page_or_ele
class OriginWaiter(object):
def __call__(self, second, scope=None):
"""等待若干秒,如传入两个参数,等待时间为这两个数间的一个随机数
:param second: 秒数
@ -30,6 +24,14 @@ class BaseWaiter(object):
from random import uniform
sleep(uniform(second, scope))
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中删除
:param loc_or_ele: 要等待的元素可以是已有元素定位符
@ -125,7 +127,7 @@ class BaseWaiter(object):
:return: 成功返回任务对象失败返回False
"""
if not self._driver.browser._dl_mgr._running:
raise RuntimeError('使用下载管理功能前需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
self._driver.browser._dl_mgr.set_flag(self._driver.tab_id, False if cancel_it else True)
if timeout is None:
timeout = self._driver.timeout
@ -240,7 +242,7 @@ class TabWaiter(BaseWaiter):
:return: 是否等待成功
"""
if not self._driver.browser._dl_mgr._running:
raise RuntimeError('使用下载管理功能前需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
if not timeout:
while self._driver.browser._dl_mgr.get_tab_missions(self._driver.tab_id):
sleep(.5)
@ -282,9 +284,9 @@ class PageWaiter(TabWaiter):
timeout = timeout if timeout is not None else self._driver.timeout
end_time = perf_counter() + timeout
while perf_counter() < end_time:
latest_tab = self._driver.latest_tab
if self._driver.tab_id != latest_tab:
return latest_tab
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:
@ -299,7 +301,7 @@ class PageWaiter(TabWaiter):
:return: 是否等待成功
"""
if not self._driver.browser._dl_mgr._running:
raise RuntimeError('使用下载管理功能前需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
raise RuntimeError('此功能需显式设置下载路径使用set.download_path()方法、配置对象或ini文件均可')
if not timeout:
while self._driver.browser._dl_mgr._missions:
sleep(.5)
@ -321,7 +323,7 @@ class PageWaiter(TabWaiter):
return True
class ElementWaiter(object):
class ElementWaiter(OriginWaiter):
"""等待元素在dom中某种状态如删除、显示、隐藏"""
def __init__(self, owner, ele):
@ -332,18 +334,6 @@ class ElementWaiter(object):
self._owner = owner
self._ele = ele
def __call__(self, second, scope=None):
"""等待若干秒,如传入两个参数,等待时间为这两个数间的一个随机数
:param second: 秒数
:param scope: 随机数范围
:return: None
"""
if scope is None:
sleep(second)
else:
from random import uniform
sleep(uniform(second, scope))
def deleted(self, timeout=None, raise_err=None):
"""等待元素从dom删除
:param timeout: 超时时间为None使用元素所在页面timeout属性
@ -457,7 +447,7 @@ class ElementWaiter(object):
:param raise_err: 等待失败时是否报错为None时根据Settings设置
:return: 是否等待成功
"""
return self._wait_state('has_rect', True, timeout, raise_err, err_text='等待元素拥有大小及位置属性失败(等{}秒)。')
return self._wait_state('has_rect', True, timeout, raise_err, err_text='等待元素拥有大小及位置失败(等{}秒)。')
def _wait_state(self, attr, mode=False, timeout=None, raise_err=None, err_text=None):
"""等待元素某个元素状态到达指定状态

View File

@ -14,7 +14,11 @@ from .._pages.chromium_frame import ChromiumFrame
from .._pages.chromium_page import ChromiumPage
class BaseWaiter(object):
class OriginWaiter(object):
def __call__(self, second: float, scope: float = None) -> None: ...
class BaseWaiter(OriginWaiter):
def __init__(self, page: ChromiumBase):
self._driver: ChromiumBase = ...
@ -73,7 +77,7 @@ class PageWaiter(TabWaiter):
def all_downloads_done(self, timeout: float = None, cancel_if_timeout: bool = True) -> bool: ...
class ElementWaiter(object):
class ElementWaiter(OriginWaiter):
def __init__(self, owner: ChromiumBase, ele: ChromiumElement):
self._ele: ChromiumElement = ...
self._owner: ChromiumBase = ...