Запросы с несколькими соединениями

Я использую библиотеку запросов Python для загрузки большого файла, например:

r = requests.get("http://bigfile.com/bigfile.bin")
content = r.content

Большой файл загружается со скоростью +30 Кбит/с, что немного медленнее. Каждое подключение к файловому серверу сжимается, поэтому я хотел бы сделать несколько соединений.

Есть ли способ сделать несколько подключений одновременно для загрузки одного файла?

Ответ 1

Вы можете использовать HTTP Range заголовок для извлечения только части файла (уже включен для python здесь).

Просто запустите несколько потоков и выберите другой диапазон с каждым, и вы закончите;)

def download(url,start):
    req = urllib2.Request('http://www.python.org/')
    req.headers['Range'] = 'bytes=%s-%s' % (start, start+chunk_size)
    f = urllib2.urlopen(req)
    parts[start] = f.read()

threads = []
parts = {}

# Initialize threads
for i in range(0,10):
    t = threading.Thread(target=download, i*chunk_size)
    t.start()
    threads.append( t)

# Join threads back (order doesn't matter, you just want them all)
for i in threads:
    i.join()

# Sort parts and you're done
result = ''
for i in range(0,10):
    result += parts[i*chunk_size]

Также обратите внимание, что не каждый сервер поддерживает Range заголовок (и особенно серверы с скриптами php, ответственными за выборку данных, часто не реализует его обработку).

Ответ 2

Здесь Python script, который сохраняет данный url в файле и использует несколько потоков для его загрузки:

#!/usr/bin/env python
import sys
from functools import partial
from itertools import count, izip
from multiprocessing.dummy import Pool # use threads
from urllib2 import HTTPError, Request, urlopen

def download_chunk(url, byterange):
    req = Request(url, headers=dict(Range='bytes=%d-%d' % byterange))
    try:
        return urlopen(req).read()
    except HTTPError as e:
        return b''  if e.code == 416 else None  # treat range error as EOF
    except EnvironmentError:
        return None

def main():
    url, filename = sys.argv[1:]
    pool = Pool(4) # define number of concurrent connections
    chunksize = 1 << 16
    ranges = izip(count(0, chunksize), count(chunksize - 1, chunksize))
    with open(filename, 'wb') as file:
        for s in pool.imap(partial(download_part, url), ranges):
            if not s:
                break # error or EOF
            file.write(s)
            if len(s) != chunksize:
                break  # EOF (servers with no Range support end up here)

if __name__ == "__main__":
    main()

Конец файла обнаруживается, если сервер возвращает пустое тело или 416 http-код, или если размер ответа не соответствует chunksize.

Он поддерживает серверы, которые не понимают заголовок Range (все загружается в одном запросе в этом случае; для поддержки больших файлов измените download_chunk() на сохранение во временный файл и верните имя файла, которое будет считаться в основной поток вместо самого содержимого файла).

Он позволяет независимо изменять количество одновременных подключений (размер пула) и количество байтов, запрошенных в одном запросе HTTP.

Чтобы использовать несколько процессов вместо потоков, измените импорт:

from multiprocessing.pool import Pool # use processes (other code unchanged)

Ответ 3

Для этого решения требуется утилита linux с именем "aria2c", но у нее есть преимущество в легко возобновлении загрузки.

Также предполагается, что все файлы, которые вы хотите загрузить, перечислены в списке http-адресов для местоположения MY_HTTP_LOC. Я тестировал этот script на экземпляре http-сервера lighttpd/1.4.26. Но вы можете легко изменить этот script так, чтобы он работал для других настроек.

#!/usr/bin/python

import os
import urllib
import re
import subprocess

MY_HTTP_LOC = "http://AAA.BBB.CCC.DDD/"

# retrieve webpage source code
f = urllib.urlopen(MY_HTTP_LOC)
page = f.read()
f.close

# extract relevant URL segments from source code
rgxp = '(\<td\ class="n"\>\<a\ href=")([0-9a-zA-Z\(\)\-\_\.]+)(")'
results =  re.findall(rgxp,str(page))
files = []
for match in results:
    files.append(match[1])

# download (using aria2c) files
for afile in files:
    if os.path.exists(afile) and not os.path.exists(afile+'.aria2'):
        print 'Skipping already-retrieved file: ' + afile
    else:
        print 'Downloading file: ' + afile          
        subprocess.Popen(["aria2c", "-x", "16", "-s", "20", MY_HTTP_LOC+str(afile)]).wait()