V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
Chan775
V2EX  ›  Python

请教关于 Python 多线程下载器问题

  •  
  •   Chan775 · 2020-09-22 22:36:08 +08:00 · 1621 次点击
    这是一个创建于 1525 天前的主题,其中的信息可能已经有所发展或是发生改变。

    刚学完 python 不久,想做个小爬虫练练手,于是整了个小爬虫在 vps 持续爬取某个小视频网站的发布的小视频,小视频的大小为 1-200M,小爬虫负责下载视频的部分很简单,直接 r = requests.get(url),然后把 r.content 写入文件。

    后来觉得下载效率好像不行,于是上网搜了一下多线程下载器,便开始重写下载视频部分的代码了,成品如下,在我的电脑上能正常运行,但在 vps 上持续运行的话,有不少问题,不仅内存占用大(占用 200 多 M 的内存),而且时不时出现Max retries exceeded with url的错误。

    from threading import Thread
    
    import requests
    
    
    class Download:
        def __init__(self, url):
            self.url = url
            self.ua = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0'}
            r = requests.head(self.url, headers=self.ua)
            # 循环寻址
            while r.status_code == 301 or r.status_code == 302:
                self.url = r.headers['Location']
                r = requests.head(self.url, headers=self.ua)
            self.name = self.url.split('?')[0].split('/')[-1]
            self.size = int(r.headers['Content-Length'])
            # 创建等大空文件
            f = open(self.name, "wb")
            f.truncate(self.size)
            f.close()
            # 根据文件大小分配线程
            if self.size < 5 * 1024 * 1024:
                self.thread_num = 1
            elif self.size < 10 * 1024 * 1024:
                self.thread_num = 2
            elif self.size < 20 * 1024 * 1024:
                self.thread_num = 4
            elif self.size < 40 * 1024 * 1024:
                self.thread_num = 8
            else:
                self.thread_num = 16
            # 确定文件块大小
            self.part = self.size // self.thread_num
    
        def dl(self, start, end):
            header = {'Range': 'bytes={}-{}'.format(start, end)}
            header.update(self.ua)
            with requests.get(self.url, headers=header, stream=True) as r:
                with open(self.name, 'rb+') as f:
                    f.seek(start)
                    f.write(r.content)
    
        @staticmethod
        def unit_conversion(byte):
            byte = int(byte)
            if byte > 1024:
                res = byte / 1024
                if res < 1024:
                    res = float('%.2f' % res)
                    return str(res) + 'KB'
                elif res < 1024 * 1024:
                    res = res / 1024
                    res = float('%.2f' % res)
                    return str(res) + 'MB'
                else:
                    res = res / (1024 * 1024)
                    res = float('%.2f' % res)
                    return str(res) + 'GB'
            else:
                return str(byte) + 'B'
    
        def run(self):
            thread_list = []
            for i in range(self.thread_num - 1):
                start = i * self.part
                end = (i + 1) * self.part
                t = Thread(target=self.dl, args=(start, end))
                thread_list.append(t)
            # 最后一部分
            start = (self.thread_num - 1) * self.part
            end = self.size
            t = Thread(target=self.dl, args=(start, end))
            thread_list.append(t)
            # 启动所有子线程
            for t in thread_list:
                t.start()
            # 子进程合并到主线程
            for t in thread_list:
                t.join()
            print(f'{Download.unit_conversion(self.size)} {self.name}下载完成!')
            return self.name, self.size
    
    
    if __name__ == '__main__':
        t = Download('http://www.baidu.com').run()
        print(t)
    
    

    请教大家几个问题: 1 、多线程下载的时候,多个线程读取同一个文件,不同的线程在文件的不同位置写入内容,需要加入线程锁吗?我感觉它们写入的部分不同,好像没有冲突啊

    2 、我在网上查到Max retries exceeded with url的错误是由于 http 连接过多引起的,网上给出的方法有用 with 语句打开 requests.get(url)来确保连接会被关闭,还有的是建议直接加 response.close()关闭,还有的用with closing(requests.get(img_url, stream=True)) as r:,哪种方法比较靠谱呢?

    3 、俺还有啥可以优化改进的地方?

    5 条回复    2020-09-23 13:22:15 +08:00
    ysc3839
        1
    ysc3839  
       2020-09-22 22:49:28 +08:00 via Android
    直接调用 aria2 去下载
    AJQA
        2
    AJQA  
       2020-09-22 22:56:47 +08:00 via Android
    多线程不加锁也行 是不是因为 python gil ? 导致多线程也跟 nodejs 异步差不多?
    laminux29
        3
    laminux29  
       2020-09-22 23:36:41 +08:00
    1.成人网站对于固定 IP,每天会有固定观看额度,你一个或 2 个进程进行下载,就会消耗掉一个额度。这是 Max retries exceeded with url 的产生原因。购买付费会员,额度会提高,但最高付费账号的额度,也经不起爬虫这种遍历式的消耗。

    2.成人网站一般服务器都在境外,而且由于人多,单线程下载的速度都不行。可以购买付费会员,会有专门的控制人数的高速通道,这样单线程下载速度就快了。

    3.多线程下载的软件,一般一个线程是 10kb block,最多也就 1024kb block,你这下载速度慢,然后一个线程还跑了 5MB block,而且还是小内存 VPS 在跑。

    建议:

    要练手的话,买个稳定的机场,去 xxxxhub 练手,每个线程改为 32kb block,每条视频跑几个线程,以及同时跑多少条视频,要根据你的 VPS 内存与带宽来定。
    fasionchan
        4
    fasionchan  
       2020-09-23 08:21:38 +08:00
    r.content 将 response body 整体读到内存,应该是造成内存占用大的关键因素。可以分配固定的缓冲区,分段读写。
    Chan775
        5
    Chan775  
    OP
       2020-09-23 13:22:15 +08:00
    感谢回复,不得不说,老哥你好有经验。

    1. 确实是存在观看限制,不过可以绕过它,通过它的分享页面下载或者用 x-forward 参数伪造 ip 或者直接构造下载直链。

    2. 我的 vps 也是在境外,1.5g 内存。视频下载速度倒是不很慢,就是大文件的话,用 r.content 一次读取到内存,有时会中断,所有想整多线程下载,另外也能实践一下多线程的知识。

    3. 最后多线程这个,我确实不太懂该怎么分配线程,第一次写多线程的代码。我看 aria2 最多分配 16 线程,所以整了个最大 16 线程的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1174 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 18:38 · PVG 02:38 · LAX 10:38 · JFK 13:38
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.