你也可以手敲一个高速下载器(九)请求信息与文件名

333 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情


你也可以手敲一个高速下载器(九)请求信息与文件名

前言

我们目前是直接通过 URL 下载的,且默认没有传递任何请求头和请求体的,但在实际使用当中很多请求是要校验这些信息的,比如不登录无法下载,比如没有传递要求的请求头无法下载等

传递请求头参数

大家应该是还记得我们写过的那个通用请求类,这个类是支持传递请求头参数的,所以我们支持要请求头,就在各个入口加上参数就好

传递参数

首先在命令行启动函数和HSDownloader类上的初始化函数里面都分别加入请求头的参数:

async def main(
        url: str,
        *,
        save_path: str = None,
        save_name: str = None,
        headers: dict = None,
):
    """
    高速下载普通文件
    :param url: 下载地址
    :param save_path: 保存的文件夹,默认为./downloads文件夹
    :param save_name: 保存至的文件名, 默认从url中获取
    :param headers: 请求头
    :return:
    """

初始化函数:

    def __init__(
            self,
            url: str,
            *,
            save_path: str = None,
            save_name: str = None,
            concurrency: int = 64,
            headers: dict = None
    ):
        """
        :param url: 要下载的地址
        :param save_path: 保存的文件路径
        :param save_path: 保存的文件名字
        :param concurrency: 并发的数量
        :param headers: 请求头
        """

        ......

        self.headers = headers or {}

这里对比上一节,只是增加了一个headers参数而已,但是注意的是有一个“参数”为我们写的是:“*”,这里不是说参数名的“*”,而是说“*”后面的参数都必须使用k=v的形式传递进去

使用参数

有两个地方需要使用到这个请求头的信息,一个是使用 head 请求获取数据的时候,一个是使用 get 获取真实数据的时候,我们依次的修改一下:

    async def get_head_headers(self):
        """
        获取HEAD请求的响应头
        :return:
        """
        req = Request('HEAD', self.url, sem=self._sem, headers=self.headers)
        ......
    async def get_content(self, index, start=None, end=None):
        """
        获取内容
        :param index: 索引
        :param start: 开始范围
        :param end: 结束范围
        :return: 返回内容的数据
        """
        logger.debug(f"开始下载:bytes={start}-{end}")

        ......

        req = Request("GET", self.url, sem=self._sem, headers=headers)
        ......

OK,请求头的参数就算加好了,剩下的就是测试了。

文件名

我们再来说说文件名,在之前我们自动获取文件名的方法是直接取得 url 的名字,很简单粗暴,但有一些弊端:

  • 如果文件名是中文的,不会自动解码
  • 如果不是静态资源,url 的名字会是路由的名字而不是文件是名字

所以我们要针对以上两种问题对一些处理

中文文件名

我们在浏览器上面访问带有中文的 url 时,会对 url 进行编码,当我们使用编码后的名字时是不带中文的,所以就要进行解码才可以,在 python 中可以使用urllib.parse中的unquotequote进行编码解码,下面示例:

......

from urllib.parse import unquote

class HSDownloader(object):

    def __init__(
            self,
            url: str,
            *,
            save_path: str = None,
            save_name: str = None,
            concurrency: int = 64,
            headers: dict = None
    ):

      ......

      # 文件名默认从url中获取
      self.save_name = save_name or unquote(Path(url).name)

这样就可以解析带有中文的文件名了

动态请求的文件名

这种情况是文件会通过一个请求返回来,而不是一个通过静态服务去访问的,这个时候,为了增加体验往往都会在响应头中增加参数 Content-Disposition,格式一般为:Content-Disposition: attachment; filename="xxx",针对这种请求我们直接在响应头中提取就好了

大家看到这种情况的不知道第一时间想到的是怎么去获取这个文件名,可能是字符串切割或是正则表达式,但我想这些都太麻烦,而且这种成为标准的东西肯定有更加简单优雅的方法,然后果然没有失望,一番查询之后:

所以我们的代码可以这样写:

import cgi

    async def get_head_headers(self):
        """
        获取HEAD请求的响应头
        :return:
        """
        req = Request('HEAD', self.url, sem=self._sem, headers=self.headers)
        try:
            resp = await req.request()
        except RequestException:
            self._head_headers = {}
        else:
            if 'Content-Disposition' in resp.headers:
                self.save_name = cgi.parse_header(resp.headers.get('Content-Disposition'))[1]['filename']
            self._head_headers = resp.headers
            await resp.aclose()
        finally:
            await req.close()

        return self._head_headers

测试

其实上面的逻辑都很简单,但复杂的是测试,需要找到合适的URL去测试各种情况,所以为了避免这些情况,大家可以选择自行写一个简单的下载服务,来模拟各种下载的情况,比如这一节当中的请求头校验和动态文件名,简单的代码如下:

from sanic import Sanic
from sanic.response import Request, text, file

app = Sanic("my_server")


@app.get('/test_dl')
@app.head('/test_dl')
async def dl(request: Request):
    if 'Windows' not in request.headers.get('user-agent'):
        return text("你不配下载", status=403)
    return await file("aDrive.exe", filename="阿里云盘.exe")


if __name__ == '__main__':
    app.static('/', '/')
    app.run(host='0.0.0.0', port=6789, auto_reload=True)

代码仓库:

本节的代码以上传至 Github,请自行下载及观看:第九节代码