Pythonでファイルのダウンロードを高速化する

多くの場合、一部の管理者はファイルのダウンロード速度に制限を設定します。これによりネットワークの負荷が軽減されますが、同時に、特に大きなファイル(1 GBから)をダウンロードする必要がある場合は、ユーザーにとって非常に煩わしいものになります。速度は約1メガビット/秒(125キロバイト/秒)で変動します。これらのデータに基づいて、ダウンロード速度は少なくとも8192秒(2時間16分32秒)になると結論付けます。私たちの帯域幅では最大16Mbps(2 MB /秒)を転送できますが、512秒(8分32秒)かかります。

これらの値は偶然ではありません。そのようなダウンロードでは、最初は4Gインターネットのみを使用していました。

場合:

私が開発し、以下にレイアウトしたユーティリティは、次の場合にのみ機能します。

  • 帯域幅がダウンロード速度よりも速いことを事前に知っています
  • 大きなサイトページでもすぐに読み込まれます(人為的に低速の最初の兆候)
  • 低速のプロキシまたはVPNを使用していません
  • サイトへの良いping

これらの制限は何ですか?

  • バックエンドの最適化と静的ファイルの返却
  • DDoS保護

この減速はどのように実装されますか?

Nginx

location /static/ {
   ...
   limit rate 50k; -> 50 kilobytes per second for a single connection 
   ...
}

location /videos/ {
   ...
   limit rate 500k; -> 500 kilobytes per second for a single connection
   limit_rate_after 10m; -> after 10 megabytes download speed, will 500 kilobytes per second for a single connection
   ...
}

zipファイル付きの機能

zip拡張子のファイルをダウンロードするときに興味深い機能が発見されました。各部分では、アーカイブ内のファイルを部分的に表示できますが、ほとんどのアーカイバは、ファイルが壊れていて無効であると言いますが、コンテンツとファイル名の一部は次のようになります。表示されます。

コード解析:

このプログラムを作成するには、Python、asyncio、aiohttp、aiofilesが必要です。すべてのコードは非同期になり、パフォーマンスが向上し、メモリと速度の観点からオーバーヘッドが最小限に抑えられます。スレッドやプロセスで実行することも可能ですが、大きなファイルをロードするときに、スレッドやプロセスを作成できないときにエラーが発生する可能性があります。

async def get_content_length(url):
    async with aiohttp.ClientSession() as session:
        async with session.head(url) as request:
            return request.content_length

この関数は、ファイルの長さを返します。また、リクエスト自体はGETではなくHEADを使用します。つまり、本文(指定されたURLのコンテンツ)を含まず、ヘッダーのみを取得します。

def parts_generator(size, start=0, part_size=10 * 1024 ** 2):
    while size - start > part_size:
        yield start, start + part_size
        start += part_size
    yield start, size

このジェネレーターは、ダウンロード用の範囲を返します。重要な点は、メガバイトあたりの比率を維持するために1024の倍数であるpart_sizeを選択することですが、どの数でもかまいません。part_size = 1では正しく機能しないため、デフォルトでパーツあたり10MBに設定しました。

async def download(url, headers, save_path):
    async with aiohttp.ClientSession(headers=headers) as session:
        async with session.get(url) as request:
            file = await aiofiles.open(save_path, 'wb')
            await file.write(await request.content.read())

主な機能の1つは、ファイルのダウンロードです。非同期で動作します。ここでは、入出力操作をブロックしないことでディスク書き込みを高速化するための非同期ファイルが必要です。

async def process(url):
    filename = os.path.basename(urlparse(url).path)
    tmp_dir = TemporaryDirectory(prefix=filename, dir=os.path.abspath('.'))
    size = await get_content_length(url)
    tasks = []
    file_parts = []
    for number, sizes in enumerate(parts_generator(size)):
        part_file_name = os.path.join(tmp_dir.name, f'{filename}.part{number}')
        file_parts.append(part_file_name)
        tasks.append(download(URL, {'Range': f'bytes={sizes[0]}-{sizes[1]}'}, part_file_name))
    await asyncio.gather(*tasks)
    with open(filename, 'wb') as wfd:
        for f in file_parts:
            with open(f, 'rb') as fd:
                shutil.copyfileobj(fd, wfd)

最も基本的な機能は、URLからファイル名を取得し、それを番号付きの.partファイルに変換し、元のファイルの下に一時ディレクトリを作成し、すべての部分をそのファイルにダウンロードします。await asyncio.gather(* tasks)を使用すると、収集されたすべてのコルーチンを同時に実行できるため、ダウンロードが大幅に高速化されます。その後、すでに同期されているshutil.copyfileobjメソッドは、すべてのファイルを1つのファイルに連結します。

async def main():
    if len(sys.argv) <= 1:
        print('Add URLS')
        exit(1)
    urls = sys.argv[1:]
    await asyncio.gather(*[process(url) for url in urls])

main関数は、コマンドラインからURLのリストを受け取り、すでにおなじみのasyncio.gatherを使用して、同時に多くのファイルのダウンロードを開始します。

基準:

私が見つけたリソースの1つで、ある大学(低速サーバー)のサイトからGentooLinuxイメージをダウンロードする際のベンチマークが実行されました。

  • 非同期:164.682秒
  • 同期:453.545秒

DietPiディストリビューション(高速サーバー)をダウンロードします。

  • 非同期:17.106秒のベストタイム、20.056秒のワーストタイム
  • 同期:15.897秒のベストタイム、25.832秒のワーストタイム

ご覧のとおり、結果はほぼ3倍の加速度に達します。一部のファイルでは、結果が20〜30回に達しました。

考えられる改善:

  • より安全なダウンロード。エラーが発生した場合は、ダウンロードを再開してください。
  • メモリの最適化。問題の1つは、ストレージスペースの消費量が2倍になることです。(すべてのパーツがダウンロードされ、新しいファイルにコピーされますが、ディレクトリはまだ削除されていません)。パーツの内容をコピーした直後にファイルを削除することで簡単に修正できます。
  • 一部のサーバーは接続数を追跡し、そのような負荷を台無しにする可能性があります。これには、一時停止するか、パーツのサイズを大幅に増やす必要があります。
  • プログレスバーを追加します。

結論として、非同期ロードは解決策であると言えますが、残念ながら、ファイルのダウンロードに関しては特効薬ではありません。

import asyncio
import os.path
import shutil

import aiofiles
import aiohttp
from tempfile import TemporaryDirectory
import sys
from urllib.parse import urlparse


async def get_content_length(url):
    async with aiohttp.ClientSession() as session:
        async with session.head(url) as request:
            return request.content_length


def parts_generator(size, start=0, part_size=10 * 1024 ** 2):
    while size - start > part_size:
        yield start, start + part_size
        start += part_size
    yield start, size


async def download(url, headers, save_path):
    async with aiohttp.ClientSession(headers=headers) as session:
        async with session.get(url) as request:
            file = await aiofiles.open(save_path, 'wb')
            await file.write(await request.content.read())


async def process(url):
    filename = os.path.basename(urlparse(url).path)
    tmp_dir = TemporaryDirectory(prefix=filename, dir=os.path.abspath('.'))
    size = await get_content_length(url)
    tasks = []
    file_parts = []
    for number, sizes in enumerate(parts_generator(size)):
        part_file_name = os.path.join(tmp_dir.name, f'{filename}.part{number}')
        file_parts.append(part_file_name)
        tasks.append(download(url, {'Range': f'bytes={sizes[0]}-{sizes[1]}'}, part_file_name))
    await asyncio.gather(*tasks)
    with open(filename, 'wb') as wfd:
        for f in file_parts:
            with open(f, 'rb') as fd:
                shutil.copyfileobj(fd, wfd)


async def main():
    if len(sys.argv) <= 1:
        print('Add URLS')
        exit(1)
    urls = sys.argv[1:]
    await asyncio.gather(*[process(url) for url in urls])


if __name__ == '__main__':
    import time

    start_code = time.monotonic()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    print(f'{time.monotonic() - start_code} seconds!') 

ソース:https ://hackernoon.com/how-to-speed-up-file-downloads-with-python

#python 

What is GEEK

Buddha Community

Pythonでファイルのダウンロードを高速化する

Pythonでファイルのダウンロードを高速化する

多くの場合、一部の管理者はファイルのダウンロード速度に制限を設定します。これによりネットワークの負荷が軽減されますが、同時に、特に大きなファイル(1 GBから)をダウンロードする必要がある場合は、ユーザーにとって非常に煩わしいものになります。速度は約1メガビット/秒(125キロバイト/秒)で変動します。これらのデータに基づいて、ダウンロード速度は少なくとも8192秒(2時間16分32秒)になると結論付けます。私たちの帯域幅では最大16Mbps(2 MB /秒)を転送できますが、512秒(8分32秒)かかります。

これらの値は偶然ではありません。そのようなダウンロードでは、最初は4Gインターネットのみを使用していました。

場合:

私が開発し、以下にレイアウトしたユーティリティは、次の場合にのみ機能します。

  • 帯域幅がダウンロード速度よりも速いことを事前に知っています
  • 大きなサイトページでもすぐに読み込まれます(人為的に低速の最初の兆候)
  • 低速のプロキシまたはVPNを使用していません
  • サイトへの良いping

これらの制限は何ですか?

  • バックエンドの最適化と静的ファイルの返却
  • DDoS保護

この減速はどのように実装されますか?

Nginx

location /static/ {
   ...
   limit rate 50k; -> 50 kilobytes per second for a single connection 
   ...
}

location /videos/ {
   ...
   limit rate 500k; -> 500 kilobytes per second for a single connection
   limit_rate_after 10m; -> after 10 megabytes download speed, will 500 kilobytes per second for a single connection
   ...
}

zipファイル付きの機能

zip拡張子のファイルをダウンロードするときに興味深い機能が発見されました。各部分では、アーカイブ内のファイルを部分的に表示できますが、ほとんどのアーカイバは、ファイルが壊れていて無効であると言いますが、コンテンツとファイル名の一部は次のようになります。表示されます。

コード解析:

このプログラムを作成するには、Python、asyncio、aiohttp、aiofilesが必要です。すべてのコードは非同期になり、パフォーマンスが向上し、メモリと速度の観点からオーバーヘッドが最小限に抑えられます。スレッドやプロセスで実行することも可能ですが、大きなファイルをロードするときに、スレッドやプロセスを作成できないときにエラーが発生する可能性があります。

async def get_content_length(url):
    async with aiohttp.ClientSession() as session:
        async with session.head(url) as request:
            return request.content_length

この関数は、ファイルの長さを返します。また、リクエスト自体はGETではなくHEADを使用します。つまり、本文(指定されたURLのコンテンツ)を含まず、ヘッダーのみを取得します。

def parts_generator(size, start=0, part_size=10 * 1024 ** 2):
    while size - start > part_size:
        yield start, start + part_size
        start += part_size
    yield start, size

このジェネレーターは、ダウンロード用の範囲を返します。重要な点は、メガバイトあたりの比率を維持するために1024の倍数であるpart_sizeを選択することですが、どの数でもかまいません。part_size = 1では正しく機能しないため、デフォルトでパーツあたり10MBに設定しました。

async def download(url, headers, save_path):
    async with aiohttp.ClientSession(headers=headers) as session:
        async with session.get(url) as request:
            file = await aiofiles.open(save_path, 'wb')
            await file.write(await request.content.read())

主な機能の1つは、ファイルのダウンロードです。非同期で動作します。ここでは、入出力操作をブロックしないことでディスク書き込みを高速化するための非同期ファイルが必要です。

async def process(url):
    filename = os.path.basename(urlparse(url).path)
    tmp_dir = TemporaryDirectory(prefix=filename, dir=os.path.abspath('.'))
    size = await get_content_length(url)
    tasks = []
    file_parts = []
    for number, sizes in enumerate(parts_generator(size)):
        part_file_name = os.path.join(tmp_dir.name, f'{filename}.part{number}')
        file_parts.append(part_file_name)
        tasks.append(download(URL, {'Range': f'bytes={sizes[0]}-{sizes[1]}'}, part_file_name))
    await asyncio.gather(*tasks)
    with open(filename, 'wb') as wfd:
        for f in file_parts:
            with open(f, 'rb') as fd:
                shutil.copyfileobj(fd, wfd)

最も基本的な機能は、URLからファイル名を取得し、それを番号付きの.partファイルに変換し、元のファイルの下に一時ディレクトリを作成し、すべての部分をそのファイルにダウンロードします。await asyncio.gather(* tasks)を使用すると、収集されたすべてのコルーチンを同時に実行できるため、ダウンロードが大幅に高速化されます。その後、すでに同期されているshutil.copyfileobjメソッドは、すべてのファイルを1つのファイルに連結します。

async def main():
    if len(sys.argv) <= 1:
        print('Add URLS')
        exit(1)
    urls = sys.argv[1:]
    await asyncio.gather(*[process(url) for url in urls])

main関数は、コマンドラインからURLのリストを受け取り、すでにおなじみのasyncio.gatherを使用して、同時に多くのファイルのダウンロードを開始します。

基準:

私が見つけたリソースの1つで、ある大学(低速サーバー)のサイトからGentooLinuxイメージをダウンロードする際のベンチマークが実行されました。

  • 非同期:164.682秒
  • 同期:453.545秒

DietPiディストリビューション(高速サーバー)をダウンロードします。

  • 非同期:17.106秒のベストタイム、20.056秒のワーストタイム
  • 同期:15.897秒のベストタイム、25.832秒のワーストタイム

ご覧のとおり、結果はほぼ3倍の加速度に達します。一部のファイルでは、結果が20〜30回に達しました。

考えられる改善:

  • より安全なダウンロード。エラーが発生した場合は、ダウンロードを再開してください。
  • メモリの最適化。問題の1つは、ストレージスペースの消費量が2倍になることです。(すべてのパーツがダウンロードされ、新しいファイルにコピーされますが、ディレクトリはまだ削除されていません)。パーツの内容をコピーした直後にファイルを削除することで簡単に修正できます。
  • 一部のサーバーは接続数を追跡し、そのような負荷を台無しにする可能性があります。これには、一時停止するか、パーツのサイズを大幅に増やす必要があります。
  • プログレスバーを追加します。

結論として、非同期ロードは解決策であると言えますが、残念ながら、ファイルのダウンロードに関しては特効薬ではありません。

import asyncio
import os.path
import shutil

import aiofiles
import aiohttp
from tempfile import TemporaryDirectory
import sys
from urllib.parse import urlparse


async def get_content_length(url):
    async with aiohttp.ClientSession() as session:
        async with session.head(url) as request:
            return request.content_length


def parts_generator(size, start=0, part_size=10 * 1024 ** 2):
    while size - start > part_size:
        yield start, start + part_size
        start += part_size
    yield start, size


async def download(url, headers, save_path):
    async with aiohttp.ClientSession(headers=headers) as session:
        async with session.get(url) as request:
            file = await aiofiles.open(save_path, 'wb')
            await file.write(await request.content.read())


async def process(url):
    filename = os.path.basename(urlparse(url).path)
    tmp_dir = TemporaryDirectory(prefix=filename, dir=os.path.abspath('.'))
    size = await get_content_length(url)
    tasks = []
    file_parts = []
    for number, sizes in enumerate(parts_generator(size)):
        part_file_name = os.path.join(tmp_dir.name, f'{filename}.part{number}')
        file_parts.append(part_file_name)
        tasks.append(download(url, {'Range': f'bytes={sizes[0]}-{sizes[1]}'}, part_file_name))
    await asyncio.gather(*tasks)
    with open(filename, 'wb') as wfd:
        for f in file_parts:
            with open(f, 'rb') as fd:
                shutil.copyfileobj(fd, wfd)


async def main():
    if len(sys.argv) <= 1:
        print('Add URLS')
        exit(1)
    urls = sys.argv[1:]
    await asyncio.gather(*[process(url) for url in urls])


if __name__ == '__main__':
    import time

    start_code = time.monotonic()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    print(f'{time.monotonic() - start_code} seconds!') 

ソース:https ://hackernoon.com/how-to-speed-up-file-downloads-with-python

#python