From e6d15054b17498f1be4c6366837d09ced013276b Mon Sep 17 00:00:00 2001 From: g1879 Date: Wed, 1 Dec 2021 10:02:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=80=E4=BA=9B=E5=B0=8F=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DrissionPage/base.py | 8 +- DrissionPage/common.py | 204 +++++++++++++--------------- DrissionPage/mix_page.py | 6 +- DrissionPage/session_page.py | 17 +-- DrissionPage/shadow_root_element.py | 20 +-- DrissionPage/tools.py | 3 - 6 files changed, 113 insertions(+), 145 deletions(-) diff --git a/DrissionPage/base.py b/DrissionPage/base.py index b2085b7..5379030 100644 --- a/DrissionPage/base.py +++ b/DrissionPage/base.py @@ -15,6 +15,8 @@ from .common import format_html class BaseParser(object): + """所有页面、元素类的基类""" + def __call__(self, loc_or_str): return self.ele(loc_or_str) @@ -43,7 +45,7 @@ class BaseParser(object): class BaseElement(BaseParser): - """SessionElement 和 DriverElement 的基类""" + """各元素类的基类""" def __init__(self, ele: Union[WebElement, HtmlElement], page=None): self._inner_ele = ele @@ -85,6 +87,8 @@ class BaseElement(BaseParser): class DrissionElement(BaseElement): + """DriverElement 和 SessionElement的基类,但不是ShadowRootElement的基类""" + @property def parent(self): """返回父级元素""" @@ -210,6 +214,8 @@ class DrissionElement(BaseElement): class BasePage(BaseParser): + """页面类的基类""" + def __init__(self, timeout: float = 10): """初始化函数""" self._url = None diff --git a/DrissionPage/common.py b/DrissionPage/common.py index c842529..a9f7b52 100644 --- a/DrissionPage/common.py +++ b/DrissionPage/common.py @@ -12,32 +12,74 @@ from typing import Union from zipfile import ZipFile +def get_ele_txt(e) -> str: + """获取元素内所有文本 + :param e: 元素对象 + :return: 元素内所有文本 + """ + # 前面无须换行的元素 + nowrap_list = ('sub', 'em', 'strong', 'a', 'font', 'b', 'span', 's', 'i', 'del', 'ins', 'img', 'td', 'th', + 'abbr', 'bdi', 'bdo', 'cite', 'code', 'data', 'dfn', 'kbd', 'mark', 'q', 'rp', 'rt', 'ruby', + 'samp', 'small', 'sub', 'time', 'u', 'var', 'wbr', 'button', 'slot', 'content') + # 后面添加换行的元素 + wrap_after_list = ('p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'li', 'blockquote', 'header', + 'footer', 'address' 'article', 'aside', 'main', 'nav', 'section', 'figcaption', 'summary') + # 不获取文本的元素 + noText_list = ('script', 'style', 'video', 'audio', 'iframe', 'embed', 'noscript', 'canvas', 'template') + # 用/t分隔的元素 + tab_list = ('td', 'th') + + if e.tag in noText_list: + return e.raw_text + + def get_node_txt(ele, pre: bool = False): + str_list = [] + tag = ele.tag.lower() + + if tag in noText_list: # 标签内的文本不返回 + return str_list + if tag == 'br': + return '\n' + if tag == 'pre': + pre = True + + nodes = ele.eles('xpath:./text() | *') + prev_ele = '' + for el in nodes: + if isinstance(el, str): # 字符节点 + if pre: + str_list.append(el) + + else: + if sub('[ \n]', '', el) != '': # 字符除了回车和空格还有其它内容 + txt = el + if not pre: + txt = txt.replace('\n', ' ').strip(' ') + txt = sub(r' {2,}', ' ', txt) + str_list.append(txt) + + else: # 元素节点 + if el.tag.lower() not in nowrap_list and str_list and str_list[-1] != '\n': # 元素间换行的情况 + str_list.append('\n') + if el.tag.lower() in tab_list and prev_ele in tab_list: # 表格的行 + str_list.append('\t') + + str_list.extend(get_node_txt(el, pre)) + prev_ele = el.tag.lower() + + if tag in wrap_after_list and str_list and str_list[-1] != '\n': # 有些元素后面要添加回车 + str_list.append('\n') + + return str_list + + re_str = ''.join(get_node_txt(e)) + return format_html(re_str, False).strip(' \n') + + def str_to_loc(loc: str) -> tuple: """处理元素查找语句 \n 查找方式:属性、tag name及属性、文本、xpath、css selector、id、class \n @表示属性,.表示class,#表示id,=表示精确匹配,:表示模糊匹配,无控制字符串时默认搜索该字符串 \n - 示例: \n - .ele_class - class等于ele_class的元素 \n - .:ele_class - class含有ele_class的元素 \n - #ele_id - id等于ele_id的元素 \n - #:ele_id - id含有ele_id的元素 \n - @class:ele_class - class含有ele_class的元素 \n - @class=ele_class - class等于ele_class的元素 \n - @class - 带class属性的元素 \n - tag:div - div元素 \n - tag:div@class:ele_class - class含有ele_class的div元素 \n - tag:div@class=ele_class - class等于ele_class的div元素 \n - tag:div@text():search_text - 文本含有search_text的div元素 \n - tag:div@text()=search_text - 文本等于search_text的div元素 \n - text:search_text - 文本含有search_text的元素 \n - text=search_text - 文本等于search_text的元素 \n - xpath://div[@class="ele_class"] - 用xpath查找 \n - css:div.ele_class - 用css selector查找 \n - xpath://div[@class="ele_class"] - 等同于 x://div[@class="ele_class"] \n - css:div.ele_class - 等同于 c:div.ele_class \n - tag:div - 等同于 t:div \n - text:search_text - 等同于 tx:search_text \n - text=search_text - 等同于 tx=search_text \n """ loc_by = 'xpath' @@ -115,64 +157,45 @@ def str_to_loc(loc: str) -> tuple: return loc_by, loc_str -def get_ele_txt(e) -> str: - # 前面无须换行的元素 - nowrap_list = ('sub', 'em', 'strong', 'a', 'font', 'b', 'span', 's', 'i', 'del', 'ins', 'img', 'td', 'th', - 'abbr', 'bdi', 'bdo', 'cite', 'code', 'data', 'dfn', 'kbd', 'mark', 'q', 'rp', 'rt', 'ruby', - 'samp', 'small', 'sub', 'time', 'u', 'var', 'wbr', 'button', 'slot', 'content') - # 后面添加换行的元素 - wrap_after_list = ('p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'li', 'blockquote', 'header', - 'footer', 'address' 'article', 'aside', 'main', 'nav', 'section', 'figcaption', 'summary') - # 不获取文本的元素 - noText_list = ('script', 'style', 'video', 'audio', 'iframe', 'embed', 'noscript', 'canvas', 'template') - # 用/t分隔的元素 - tab_list = ('td', 'th') +def translate_loc(loc: tuple) -> tuple: + """把By类型的loc元组转换为css selector或xpath类型的 \n + :param loc: By类型的loc元组 + :return: css selector或xpath类型的loc元组 + """ + if len(loc) != 2: + raise ValueError('定位符长度必须为2。') - if e.tag in noText_list: - return e.raw_text + loc_by = 'xpath' - def get_node_txt(ele, pre: bool = False): - str_list = [] - tag = ele.tag.lower() + if loc[0] == 'xpath': + loc_str = loc[1] - if tag in noText_list: # script标签内的文本不返回 - return str_list - if tag == 'br': - return '\n' - if tag == 'pre': - pre = True + elif loc[0] == 'css selector': + loc_by = 'css selector' + loc_str = loc[1] - nodes = ele.eles('xpath:./text() | *') - prev_ele = '' - for el in nodes: - if isinstance(el, str): # 字符节点 - if pre: - str_list.append(el) + elif loc[0] == 'id': + loc_str = f'//*[@id="{loc[1]}"]' - else: - if sub('[ \n]', '', el) != '': # 字符除了回车和空格还有其它内容 - txt = el - if not pre: - txt = txt.replace('\n', ' ').strip(' ') - txt = sub(r' {2,}', ' ', txt) - str_list.append(txt) + elif loc[0] == 'class name': + loc_str = f'//*[@class="{loc[1]}"]' - else: # 元素节点 - if el.tag.lower() not in nowrap_list and str_list and str_list[-1] != '\n': # 元素间换行的情况 - str_list.append('\n') - if el.tag.lower() in tab_list and prev_ele in tab_list: # 表格的行 - str_list.append('\t') + elif loc[0] == 'link text': + loc_str = f'//a[text()="{loc[1]}"]' - str_list.extend(get_node_txt(el, pre)) - prev_ele = el.tag.lower() + elif loc[0] == 'name': + loc_str = f'//*[@name="{loc[1]}"]' - if tag in wrap_after_list and str_list and str_list[-1] != '\n': # 有些元素后面要添加回车 - str_list.append('\n') + elif loc[0] == 'tag name': + loc_str = f'//{loc[1]}' - return str_list + elif loc[0] == 'partial link text': + loc_str = f'//a[contains(text(),"{loc[1]}")]' - re_str = ''.join(get_node_txt(e)) - return format_html(re_str, False).strip(' \n') + else: + raise ValueError('无法识别的定位符。') + + return loc_by, loc_str def _make_xpath_str(tag: str, arg: str, val: str, mode: str = 'fuzzy') -> str: @@ -232,47 +255,6 @@ def format_html(text: str, trans: bool = True) -> str: return text.replace('\xa0', ' ') -def translate_loc(loc: tuple) -> tuple: - """把By类型的loc元组转换为css selector或xpath类型的 \n - :param loc: By类型的loc元组 - :return: css selector或xpath类型的loc元组 - """ - if len(loc) != 2: - raise ValueError('定位符长度必须为2。') - - loc_by = 'xpath' - - if loc[0] == 'xpath': - loc_str = loc[1] - - elif loc[0] == 'css selector': - loc_by = 'css selector' - loc_str = loc[1] - - elif loc[0] == 'id': - loc_str = f'//*[@id="{loc[1]}"]' - - elif loc[0] == 'class name': - loc_str = f'//*[@class="{loc[1]}"]' - - elif loc[0] == 'link text': - loc_str = f'//a[text()="{loc[1]}"]' - - elif loc[0] == 'name': - loc_str = f'//*[@name="{loc[1]}"]' - - elif loc[0] == 'tag name': - loc_str = f'//{loc[1]}' - - elif loc[0] == 'partial link text': - loc_str = f'//a[contains(text(),"{loc[1]}")]' - - else: - raise ValueError('无法识别的定位符。') - - return loc_by, loc_str - - def clean_folder(folder_path: str, ignore: list = None) -> None: """清空一个文件夹,除了ignore里的文件和文件夹 \n :param folder_path: 要清空的文件夹路径 diff --git a/DrissionPage/mix_page.py b/DrissionPage/mix_page.py index 02b54d4..7f1acde 100644 --- a/DrissionPage/mix_page.py +++ b/DrissionPage/mix_page.py @@ -364,7 +364,7 @@ class MixPage(SessionPage, DriverPage, BasePage): goal_path: str = None, rename: str = None, file_exists: str = 'rename', - post_data: dict = None, + post_data: Union[str, dict] = None, show_msg: bool = False, show_errmsg: bool = False, retry: int = None, @@ -376,13 +376,13 @@ class MixPage(SessionPage, DriverPage, BasePage): :param goal_path: 存放路径,默认为ini文件中指定的临时文件夹 :param rename: 重命名文件,可不写扩展名 :param file_exists: 若存在同名文件,可选择 'rename', 'overwrite', 'skip' 方式处理 - :param post_data: post方式的数据 + :param post_data: post方式的数据,这个参数不为None时自动转成post方式 :param show_msg: 是否显示下载信息 :param show_errmsg: 是否显示和抛出异常 :param retry: 重试次数 :param interval: 重试间隔时间 :param kwargs: 连接参数 - :return: 下载是否成功(bool)和状态信息(成功时信息为文件路径)的元组 + :return: 下载是否成功(bool)和状态信息(成功时信息为文件路径)的元组,跳过时第一位返回None """ if self.mode == 'd': self.cookies_to_session() diff --git a/DrissionPage/session_page.py b/DrissionPage/session_page.py index 030ec4f..500dbb5 100644 --- a/DrissionPage/session_page.py +++ b/DrissionPage/session_page.py @@ -267,7 +267,7 @@ class SessionPage(BasePage): :param goal_path: 存放路径 :param rename: 重命名文件,可不写扩展名 :param file_exists: 若存在同名文件,可选择 'rename', 'overwrite', 'skip' 方式处理 - :param post_data: post方式的数据 + :param post_data: post方式的数据,这个参数不为None时自动转成post方式 :param show_msg: 是否显示下载信息 :param show_errmsg: 是否抛出和显示异常 :param retry: 重试次数 @@ -285,7 +285,7 @@ class SessionPage(BasePage): if 'timeout' not in kwargs: kwargs['timeout'] = 20 - mode = 'post' if post_data else 'get' + mode = 'post' if post_data is not None else 'get' # 生成的response不写入self._response,是临时的 r, info = self._make_response(file_url, mode=mode, data=post_data, show_errmsg=show_errmsg, **kwargs) @@ -496,15 +496,15 @@ class SessionPage(BasePage): return r, 'Success' -def _get_download_file_name(url, headers) -> str: +def _get_download_file_name(url, response) -> str: """从headers或url中获取文件名,如果获取不到,生成一个随机文件名 :param url: 文件url - :param headers: 返回的headers + :param response: 返回的response :return: 下载文件的文件名 """ file_name = '' charset = '' - content_disposition = headers.get('content-disposition', '').replace(' ', '') + content_disposition = response.headers.get('content-disposition', '').replace(' ', '') # 使用header里的文件名 if content_disposition: @@ -516,16 +516,13 @@ def _get_download_file_name(url, headers) -> str: else: file_name = txt[0] - else: # 文件名没有编码方式 + else: # 文件名没带编码方式 txt = search(r'filename="?([^";]+)', content_disposition) if txt: file_name = txt.group(1) # 获取编码(如有) - content_type = headers.get('content-type', '').lower() - charset = search(r'charset[=: ]*(.*)?[;]', content_type) - if charset: - charset = charset.group(1) + charset = response.encoding file_name = file_name.strip("'") diff --git a/DrissionPage/shadow_root_element.py b/DrissionPage/shadow_root_element.py index 17b649f..601ad41 100644 --- a/DrissionPage/shadow_root_element.py +++ b/DrissionPage/shadow_root_element.py @@ -16,6 +16,8 @@ from .session_element import make_session_ele class ShadowRootElement(BaseElement): + """ShadowRootElement是用于处理ShadowRoot的类,使用方法和DriverElement基本一致""" + def __init__(self, inner_ele: WebElement, parent_ele: DriverElement): super().__init__(inner_ele, parent_ele.page) self.parent_ele = parent_ele @@ -191,24 +193,8 @@ class ShadowRootElement(BaseElement): def str_to_css_loc(loc: str) -> tuple: """处理元素查找语句 \n - 查找方式:属性、tag name及属性、文本、css selector \n + 查找方式:属性、tag name及属性、文本、css selector \n @表示属性,.表示class,#表示id,=表示精确匹配,:表示模糊匹配,无控制字符串时默认搜索该字符串 \n - 示例: \n - .ele_class - class为ele_class的元素 \n - .:ele_class - class含有ele_class的元素 \n - #ele_id - id为ele_id的元素 \n - #:ele_id - id含有ele_id的元素 \n - @class:ele_class - class含有ele_class的元素 \n - @class=ele_class - class等于ele_class的元素 \n - @class - 带class属性的元素 \n - tag:div - div元素 \n - tag:div@class:ele_class - class含有ele_class的div元素 \n - tag:div@class=ele_class - class等于ele_class的div元素 \n - tag:div@text():search_text - 文本含有search_text的div元素 \n - tag:div@text()=search_text - 文本等于search_text的div元素 \n - text:search_text - 文本含有search_text的元素 \n - text=search_text - 文本等于search_text的元素 \n - css:div.ele_class - 符合css selector的元素 \n """ loc_by = 'css selector' diff --git a/DrissionPage/tools.py b/DrissionPage/tools.py index 3bb71d8..63dcfca 100644 --- a/DrissionPage/tools.py +++ b/DrissionPage/tools.py @@ -4,6 +4,3 @@ """ from .session_element import make_session_ele from .easy_set import get_match_driver - -# TODO: 保存cookies到文件 -# TODO: 从文件读取cookies