Запросы с несколькими соединениями
Я использую библиотеку запросов 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()