URLリストを指定してファイルを自動ダウンロード(2020.5.10, 2020.5.12, 2021.10.3, 2022.7.15追記)

Summary

URLを記述したtextファイルを読み込んで、そこに書かれているファイルを順にダウンロードするpythonスクリプトを作成

【2022.7.15追記】 指定したURLからローカルのフォルダにダウンロードする際に階層フォルダも引き継ぐように仕様変更
-uオプションで root URLを指定し、list.txtファイル内ではroot URLからの相対ディレクトリでファイルを指定する。ダウンロード後は -o ./outdirなどで指定したoutdirの中にフォルダを作って配置
ただし、-dオプションが指定されていればoutdirの直下にファイルをベタ置きする。

使い方

1行に1ファイルのURLを記載したlist.txtファイルを準備

実行方法(URLをベタ書きしたリストを用いる場合)

listファイルと出力先のフォルダを指定する

$ fetchfile.py -i list.txt -d -o ./outdir

list.txtの例

# this line is comment (to be specified with "-c #") 
https://www.hogehoge1.com/folder1/file1.zip
https://www.hogehoge1.com/folder2/file2.wave
...

ダウンロード後に書き出すファイルのファイル名は、URLに記載されたものがそのまま使われる

-c オプションで指定した文字または文字列で始まる行はコメント行として読み飛ばされる(たとえば、 -c # とすれば#で始まる行はコメント行として読み飛ばす)

-v オプションを指定するとプログレスバーを表示(tqdmを使用)

$ fetchfile.py -v -i list.txt -o ./outdir -c #

実行方法(ファイル名だけのリストを用いる場合)

listファイルにはファイル名のみを指定し、 -u オプションでroot URLを指定

$ fetchfile.py -i list.txt -o ./outdir -u "https://www.hogehoge1.com/"

root URLとファイル名を結合したURLからダウンロード
ただし出力は./outdirにフォルダを作ってlist.txtで指定したファイル名で保存される(2022.5.15版で修正)

folder1/file1.zip
folder2/file2.zip

自動ファイルダウンローダー実装のポイント

requestを用いたhttpアクセス

User-Agentを設定しないとerrorを返すサイトもあるので、headersで指定する。アクセスに失敗した場合は、total_sizeには0が入る

import requests

in_url = "http://weburl.com/"   

headers = {"User-Agent":"Mozilla/5.0"}
response = requests.get(in_url, headers=headers, stream=True)
total_size = int(response.headers.get('content-length', 0))

【2021.10.3追記】
Transfer-Encoding: chunkedが指定されている場合には、content-lengthが提供されないことがあり、上記のコードではファイルをうまくダウンロードできない。
このため、その様な場合にはtotal_size = -1としてエラー終了せずに継続する様に修正。

プログレスバーの実装

tqdmを使ってプログレスバーを表示する。 使っているコマンドラインターミナルによっては表示が乱れることがある

from tqdm import tqdm

...
total_size = int(response.headers.get('content-length', 0))
block_size = 1024
t = tqdm(total=total_size, unit=' iB', unit_scale=True)
with open(out_fname, "wb") as handle:
    for data in response.iter_content(block_size):
        t.update(len(data))
        handle.write(data)
t.close()

# Check
if total_size != 0 and t.n != total_size:
    print("ERROR reading file")
else:
    print(" ok!")

ソースコード(fetchfile.py)

ファイルを順に開く。 -v オプションをつけるとダウンロード中にプログレスバーを表示する

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# =================================================
# download files located listed URLs in input text
#
# fetchfile2.py
# Coded by Noboru Harada (noboru@ieee.org)
#
# Changes:
# 2020/05/10 First version
# 2020/05/11 added -v option for verbose mode
# 2020/05/12 added -c option for commentout
# 2021/10/03 fixed code for cases with no content-length provided
# 2022/07/15 added -d option to align outfolder names
#
# Usage:
# > fetchfile.py [-v] [-d] -i infile.txt -o outdir [-u root_url] [-c prefix]
#
# =================================================

import sys
import os
import argparse
from tqdm import tqdm
import requests
from urllib.parse import urlparse
import re


# parse commandline args
def getargs():
    parser = argparse.ArgumentParser(
        description="fetch foles from URL listed in infile.txt")
    parser.add_argument("-i", "--infile", type=str,
                        help="infile.txt")
    parser.add_argument("-o", "--outdir", type=str,
                        help="outdir")
    parser.add_argument("-u", "--url", type=str,
                        help="specify root URL")
    parser.add_argument("-v", "--verbose", action='store_true', default=None,
                        help="generate more info during the process")
    parser.add_argument("-d", "--direct", action='store_true', default=None,
                        help="store files directly to the outdir")
    parser.add_argument("-c", "--comment", type=str, default=None,
                        help="specify comment-out prefix (e.g. \"-c #\")")
    args = parser.parse_args()

    if args.outdir is None:
        args.outdir = ""
        parser.print_help()
        exit(-1)

    return args

def fetch_file(infile, outroot, root_url, verbose, comment, direct):
    row_no = 0

    infileobj = open(infile, "r", encoding="utf-8")
    while True:
        line = infileobj.readline()
        row_no += 1

        in_url = line.rstrip('\n')

        # skip lines began with comment prefix
        if comment is not None:
            if re.search("^"+comment, in_url):
                if verbose is not None:
                    print("\"" + in_url + "\"")
                    print(" comment line --> skip")
                continue

        # when root URL string is specified with -u option
        if root_url is not None:
            if in_url is not None:
                data_url = root_url + in_url

        if not in_url:
            print(str(row_no) + ": \""+in_url+"\"")
            print(" Blank line --> skip")
            break
        else:
            #fname = os.path.basename(urlparse(in_url).path)
            dir, fname = os.path.split(urlparse(in_url).path)
            if not fname:
                continue

            if direct is not None:
                out_fname = outroot+fname
            else:
                out_dir = outroot + dir
                out_fname = out_dir + "/" + fname
                print(out_fname)
                if not os.path.exists(out_dir):
                    os.makedirs(out_dir)

            if verbose is not None:
                print(str(row_no) + ": \""+in_url+"\"")
                print(" fetching a file from URL: ")
                print(" "+data_url)
                print(" to: \""+out_fname+"\"")

            # do nothing if the file already exists
            if os.path.exists(out_fname):
                print(" already exists  --> skip")
            else:
                headers = {"User-Agent":"Mozilla/5.0"}
                #response = requests.get(data_url, headers=headers, stream=True, verify=False)
                response = requests.get(data_url, headers=headers, stream=True)

                n = 0
                block_size = 1024
                total_size = 0
                if "content-length" in response.headers:
                    total_size = int(response.headers.get('content-length', 0))
                    if(total_size == 0):
                        print(" ERROR reading URL --> skip")
                        continue
                    else:
                        if verbose is not None:
                            print(" Size: "+str(int(total_size/1024))+" KB")

                    if verbose is not None:
                        t = tqdm(total=total_size, unit=' iB', unit_scale=True)
                        with open(out_fname, "wb") as handle:
                            for data in response.iter_content(block_size):
                                t.update(len(data))
                                handle.write(data)
                        t.close()
                        n = t.n
                    else:
                        with open(out_fname, "wb") as handle:
                            for data in response.iter_content(block_size):
                                n += len(data)
                                handle.write(data)
                elif "chunked" in response.headers.get("Transfer-Encoding", ""):
                    if verbose is not None:
                        print(" Size info does not exist.")
                        print(response.headers)
                    with open(out_fname, "wb") as handle:
                        for data in response.iter_content(block_size):
                                n += len(data)
                                handle.write(data)
                else:
                    print(" ERROR reading URL --> skip")
                    print(" no content-length nor transfer-encoding chunked exist.")
                    if verbose is not None:
                        print(response.headers)
                    continue                    
                # Check
                if total_size > 0 and n != total_size:
                    print("ERROR reading file (read %d KB)" % (n))
                else:
                    print(" ok! (read %d KB)" % (n))
                    
    infileobj.close()
    print("processed "+str(row_no)+" lines")


if __name__ == '__main__':

    # read command line options
    args = getargs()

    out_path = args.outdir.rstrip('/¥')+"/"
    infile = args.infile

    root_url = ""
    if args.url is not None:
        root_url = args.url.rstrip('/¥')+"/"

    # check folder path
    if not os.path.exists(out_path):
        print(out_path+" does not exist")
        exit(-1)

    if not os.path.exists(infile):
        print(infile+" does not exist")
        exit(-1)

    fetch_file(infile, out_path, root_url, args.verbose, args.comment, args.direct)

参考

Back to Index