网页处理工具

HEAD/GET/POST

引擎模板中所有的类, 都继承自 HtmlParseHelper , 它封装了不少工具, 可以帮助你处理网页。当然,也提供了基本的请求 HEAD / GET / POST 方法, 这些方法来自 aiohttp 库的 ClientSession

HtmlParseHelper 类提供了下面 3 个成员函数:

  • async def head(self, url: str, params: dict = None, **kwargs) -> Optional[ClientResponse]

  • async def get(self, url: str, params: dict = None, **kwargs) -> Optional[ClientResponse]

  • async def post(self, url: str, data: dict = None, **kwargs) -> Optional[ClientResponse]

参数见 aiohttp 库的文档

为了复用 ClientSession 内部的连接池,每一个类内部创建了一个 Session 对象, 在请求发出前会自动完成一次初始化,并且捕获了处理过程中可能出现的异常。 如果未设置 Headers, 或者 Headers 中缺少 User-Agent, 将自动为每一个请求设置随机的 User-Agent,默认超时被设置为 ClientTimeout(total=30, sock_connect=5)。 如果请求过程中出现异常,这些方法将返回 None,所以在使用 Response 之前,最好检查一下它是否存在。

每一个请求的参数和响应的信息都记录在日志文件中,如果有问题,可以去 api/logs/ 下查看。 (控制台的日志等级为 INFO, 日志文件为 DEBUG)

需要注意的一个地方是,许多网页返回的 JSON 并不规范,在使用 ClientResponse 对象的 .json() 方法时可能出现问题, 最好加上参数 content_type=None

...
resp = await self.get("http://foo.bar")
if not resp or resp.status != 200:
    return ""
data = await resp.json(content_type=None)

指定 DNS 服务器

由于网络环境的复杂性以及一些众所周知的原因, 你所使用的 DNS 服务器可能 无法正确解析某些网站的域名。HtmlParseHelper 允许你在使用 HEAD GET POST 时使用指定的 DNS 服务器解析域名。

引擎模板中所有的类都可以重写该方法, 每个类设置的 DNS 服务器只对本类发出的请求生效。 如 AnimeSearcher 设置了 DNS 服务器, 但 AnimeDetailParser 没有设置, 所以它仍然使用你系统的 DNS 服务器。

def set_dns_server(self) -> List[str]:
    """设置自定义的 DNS 服务器地址"""
    return ["8.8.8.8", "8.8.4.4"]

XPath

XPath 是提取网页数据的利器,当然 HtmlParseHelper 也封装了这个功能,它来自 lxml 库。

关于 XPath 的语法,参见 w3school , lxml的文档参见 lxml.de

HtmlParseHelper 提供了下面的静态成员函数:

  • def xpath(html: str, xpath: str) -> Optional[etree.Element]

同样的,它捕获了处理中可能发生的异常,如果出错,返回 None,错误详情见日志。

html = """
    <div class="container">
        <img src="http://foo.bar.1"/>
        <img src="http://foo.bar.2"/>
    </div>
"""
result = HtmlParseHelper.xpath(html, "//div[@class='container']/img")
if not result:
    print("not result")
print(f"Elements count: {len(result)}")
for item in result:
    url = item.xpath("@src")[0]
    print(url)
Elements count: 2
http://foo.bar.1
http://foo.bar.2

并行处理

很多时候我们希望能并行解析很多网页,当然 HtmlParseHelper 提供了相关的功能。

下面两个静态成员函数用于处理并行任务,使用过多线/进程库的伙计可能对 as_completed 这个名字很熟悉, 没错,还是熟悉的味道,不过任务类型不再是函数,而是协程(Coroutine)对象。它返回一个迭代器, 每当提交的并行任务中有一个完成了,它立刻返回这个任务的结果。

  • async def as_completed(tasks: Iterable[Task]) -> AsyncIterator[T]

  • async def as_iter_completed(tasks: Iterable[IterTask]) -> AsyncIterator[T]

那么,下面的 as_iter_completed 又是什么鬼?

答:他们两个的参数不一样。as_completed 接受一个协程列表(可迭代的对象均可), 协程任务返回的结果类型为 T , 函数返回 T 的异步生成器。 而 as_iter_completed 接受的协程任务返回的结果为 Iterable[T] , 函数返回也是 T 的异步生成器, 自动对结果进行了迭代,并行提取网页数据的时候,我们需要用到它。

这是 as_completed 的例子:

async def worker():
    data = [1, 2, 3]
    return data


async def test():
    tasks = [worker(), worker(), worker()]
    async for item in HtmlParseHelper.as_completed(tasks):
        print(item, end=' ')


asyncio.run(test())
[1, 2, 3] [1, 2, 3] [1, 2, 3]

来看看 as_iter_completed 的效果:

async def worker():
    data = [1, 2, 3]
    return data


async def test():
    tasks = [worker(), worker(), worker()]
    async for item in HtmlParseHelper.as_iter_completed(tasks):
        print(item, end=' ')


asyncio.run(test())
1 2 3 1 2 3 1 2 3

繁简转换

用于繁体中文和简体中文的转换的小工具,由 zhconv 库提供支持。

有时候我们抓取的网站并非大陆网站,这个时候需要将关键词转化为繁体, 将处理结果转换为简体。

from api.utils.tool import *

if __name__ == '__main__':
    print(convert_to_tw("进击的巨人"))
    print(convert_to_zh("從零開始的異世界"))
進擊的巨人
从零开始的异世界

其它工具

对 MD5 和 BASE64 的简单封装,方便使用。

from api.utils.tool import *

if __name__ == '__main__':
    print(md5("进击的巨人"))
    print(b64encode("從零開始的異世界"))
d54146a0ddfdbc16ccfd28d7bdf74806
5b6e6Zu26ZaL5aeL55qE55Ww5LiW55WM