連番の付いた複数の画像ファイルを非同期でダウンロードするJScript(WSH)スクリプト

URL、フォルダ名を書き換えれば好きに使える。
連番がゼロ埋めの固定桁の場合は createUrl(i) をカスタマイズする。

連番を1から始め、HTTPステータス404 Not Foundが返ってくるまでファイルをダウンロードする。
なお、本スクリプトを実行すると、Internet Explorerに履歴が残る。

AsyncDownload.js

//フォルダ名、URLを書き換えて実行
var folderName = 'MyDownloadFolder';
var baseUrl = 'https://aaaa/bbbb/cccc/6.jpg';

//URLのファイル名の数字部を '#' に変換。
baseUrl = baseUrl.replace(/([^\/]*)\d+([^\/]*)\.(jpg|png)$/i, '$1#$2.$3');
//例)http://aaa/xxx/123.jpg --> http://aaa/xxx/#.jpg

//連番からURLを作成する関数 (カスタマイズ用)
function createUrl(i) {
/*
  簡単な処理の例
    return 'http://xxxx/xxxx/' + i + '.jpg'
    return baseUrl + i + '.jpg'

  ゼロ埋めする例 (URLが http://xxxx/xxxx/b0041.jpg のような場合)
    var seq = ('0000' + i).slice(-4);  //例: 23 を '0023' に編集
    return 'http://xxxx/xxxx/b' + seq + '.jpg'
*/
  return baseUrl.replace('#', i);
}

//連番の最大値。ただし HTTP Status 404 Not Found が返ってきた場合、
//この値を更新し後続のHTTP要求を停止させている
var endIndex=999;


//詳細ログを出力する場合、@debug_log に true を設定する
/*@cc_on @*/
/*@set @debug_log = false @*/

var maxThreads = 35;
var timeoutMsec = 90 * 1000;
var waitTimeMsec = 100;
var requestedMaxIndex = 0;  //requestDownload 関数を呼び出した最大のインデックスを格納
var arrayLength = endIndex + 1; //配列はインデックス 1 の要素から使用する
var active = fillValues(new Array(arrayLength), false);
var httpStatus = fillValues(new Array(arrayLength), 0);
var xhr = new Array(arrayLength);
var info = new Array(arrayLength);
var fso = new ActiveXObject('Scripting.FileSystemObject');
var logFilePath = WScript.ScriptFullName.replace(/\.[^.]+$/, '.log');
var errLogFilePath = WScript.ScriptFullName.replace(/\.[^.]+$/, '.Error.log');
var prcsStartTime = void 0;

function main() {

  deleteFile(logFilePath);

  deleteFile(errLogFilePath);

  /*@if(@debug_log)
  putLog('START');
  @end @*/

  //ダウロード用のフォルダを作成(カレントフォルダ内)
  var folderPath = createFolder(downloadFolderName);

  prcsStartTime = new Date();

  //HTTP要求の発行
  requestAll(folderPath);

  //HTTPステータス404が返される前、先んじて発行されたHTTP要求を取り消す
  cancelVainRequests();
  function cancelVainRequests() {
    var i;
    for(i = endIndex + 1; i <= requestedMaxIndex; i++) {
      if(active[i]) {
        abortWork(i);
      }
    }
  }

  //すべてのスレッドが完了するまで待つ
  waitForCompletion();

  var i;
  for(i = 0; i < 3; i++) {
    retryDownload();

    //すべてのスレッドが完了するまで待つ
    waitForCompletion();
  }

  reportSummary();
}

function requestAll(folderPath) {
  //URLからファイル名を抜き出すための正規表現オブジェクト
  var reFileName = /[^\/]+$/;

  var i;
  for(i = 1; i <= endIndex; i++) {
    var url = createUrl(i);
    var fileName = url.match(reFileName)[0];
    var filePath = fso.BuildPath(folderPath, fileName);

    endIndex = getNewEndIndex();

    while(countActiveThread() >= maxThreads) {
      waitAnyone();
      endIndex = getNewEndIndex();
      if(i > endIndex) {
        break;
      }
    }

    if(i > endIndex) {
      break;
    }

    info[i] = new ProcessInfo(url, filePath);
    requestDownload(url, filePath, i);
  }
  /*@if(@debug_log)
  putLog('**** All Url requested. ****');
  @end @*/
}

function retryDownload() {
  var i;
  for(i = 1; i <= endIndex; i++) {
    if(info[i] != null) {
      if(!fso.FileExists(info[i].filePath)) {
        requestDownload(info[i].url, info[i].filePath, i);
      }
    }
  }
}

function requestDownload(url, filePath, index) {

  if(index > requestedMaxIndex) {
    requestedMaxIndex = index;
    //この関数を呼び出した最大のインデックスを格納
  }

  //HTTP通信を行うためのオブジェクト
  var http = getHttpObject();
  xhr[index] = http;
  active[index] = true;
  info[index].startTime = new Date();

  //コールバックを登録する
  http.onreadystatechange = function () {
    if (http.readyState == 4) {
      try {
        httpStatus[index] = http.status;
        if (http.status == 200) {
          var adTypeBinary = 1, adSaveCreateOverWrite = 2;

          //バイナリファイルを保存するためのオブジェクト
          var st = new ActiveXObject('Adodb.Stream');
          //保存する
          st.Type = adTypeBinary;
          st.Open();
          st.Write(http.responseBody); //書き込み
          st.Savetofile(filePath, adSaveCreateOverWrite); //保存
          st.Close();
        }
      } finally {
        active[index] = false;
        /*@if(@debug_log)
        endTime[index] = new Date();
        varsInfoAtEnd[index] = varsToString();
        @end @*/
      }
    }
    //サブスレッドで動くのはこのコールバックのみ
    //
    //注意
    //サブスレッドでは競合の恐れがあるグローバル変数やグローバルオブジェクトを変更するような関数、
    //putLogやputLog2などのログ関数などをコールしないこと
  };

  var t = timeoutMsec;
  http.setTimeouts(t, t, t, t);

  try {
    http.open('GET', url, true);
    //http.setRequestHeader('Content-type', 'image/jpeg');
    //http.setRequestHeader('Content-type', 'image/png');

    /*@if(@debug_log)
    putLog3(index, 'send', url);
    @end @*/
    http.send(null); //送信
  } catch(e){
    handleError(e, index)
  }
}

function getHttpObject() {
  var i;
  for(i = 1; i <= requestedMaxIndex; i++) {
    if(!active[i] && xhr[i] != null) {
      var http = xhr[i];
      xhr[i] = null;
      try {
        xhr[i].abort();
      } catch(e){}
      return http;
    }
  }
  return new ActiveXObject('Msxml2.ServerXMLHTTP');
}

function deleteFile(filePath) {
  try {
    if(fso.FileExists(filePath)) {
      fso.DeleteFile(filePath, true);
    }
  } catch(e) {
    WScript.Echo(createErrorMessage(e) + '\n\n' + quote(filePath) + ' を削除できません');
    WScript.Quit(getErrorCode(e));
  }
}

function createFolder(folderName) {

  var basePath = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), folderName);
  var folderPath = basePath;

  var i = 1;
  while(fso.FolderExists(folderPath) || fso.FileExists(folderPath)) {
    i++;
    folderPath = basePath + ' (' + i + ')';
  }

  fso.CreateFolder(folderPath);

  return folderPath;
}

function toHex(n) {
  return (n >>> 0).toString(16).toUpperCase()
}

function fillValues(array, value) {
  var i;
  for(i = 0; i < array.length; i++) {
    array[i] = value;
  }
  return array;
}

function ProcessInfo(url, filePath) {
  this.url = url;
  this.filePath = filePath;
}

function getNewEndIndex() {
  var i;
  for(i = 1; i <= requestedMaxIndex && i < endIndex; i++) {
    if(httpStatus[i] == 404) {
      /*@if(@debug_log)
      putLog('[endIndex] will be updated by ' + i);
      @end @*/
      return i - 1;
    }
  }
  return endIndex;
}

function countActiveThread() {
  var count = 0;
  var i;
  for(i = 1; i <= requestedMaxIndex; i++) {
    if(active[i]) {
      count++;
    }
  }
  return count;
}

function waitForCompletion() {
  do {
    waitAll();
    /*@if(@debug_log)
    putLog('**** waitForCompletion Loop ****');
    @end @*/
  } while(countActiveThread() != 0)
}

function waitAnyone() {
  wait(true);
}

function waitAll() {
  wait(false);
}

function wait(waitOne) {

  var i;
  for(i = 1; i <= requestedMaxIndex; i++) {
    if(active[i] && xhr[i] != null) {
      try {
        /*@if(@debug_log)
        putLog2(i, 'waitForResponse(' + timeoutMsec + 'msec) START');
        @end @*/

        xhr[i].waitForResponse(waitTimeMsec);

        if(waitOne) {
          return;
        }
      } catch(e) {
        handleError(e, i);
      } finally {
        /*@if(@debug_log)
        putLog2(i, 'waitForResponse(' + timeoutMsec + 'msec) END  ');
        @end @*/
      }
    }
  }

}

//ErrorCode
var WININET_E_TIMEOUT             = 0x80072EE2;  //The operation timed out.
var WININET_E_INVALID_URL         = 0x80072EE5;  //The URL is invalid.
var WININET_E_UNRECOGNIZED_SCHEME = 0x80072EE6;  //The URL does not use a recognized protocol.
var WININET_E_NAME_NOT_RESOLVED   = 0x80072EE7;  //The server name or address could not be resolved.
var WININET_E_PROTOCOL_NOT_FOUND  = 0x80072EE8;  //A protocol with the required capabilities was not found.

function handleError(e, i) {

  var errMsg = createErrorMessage(e);

  switch(e.number >>> 0) {
    case WININET_E_INVALID_URL:
    case WININET_E_UNRECOGNIZED_SCHEME:
    case WININET_E_NAME_NOT_RESOLVED:
    case WININET_E_PROTOCOL_NOT_FOUND:
      WScript.Echo(quote(info[i].url) + '\n\n' + errMsg);
      WScript.Quit(getErrorCode(e));
      break;
    case WININET_E_TIMEOUT:
      /*@if(@debug_log)
      if (i > 0) {
        putLog2(i, 'Timeout');
      }
      @end @*/
      break;
  }

  try {
    putLog2(i, quote(info[i].url) + ' ' + errMsg);
  } catch(e) {}

  abortWork(i);

  /*@if(@debug_log)
  putLog3(i, 'FAILED', info[i].url);
  @end @*/
}

function createErrorMessage(e) {
  var description = e.description.replace(/\s+$/g, '');

  switch(e.number >>> 0) {
    case WININET_E_UNRECOGNIZED_SCHEME:
      description = 'The URL does not use a recognized protocol';
  }
  return 'Error: ' + description + ' (0x' + toHex(e.number) + ')';
}

function getErrorCode(e) {
  return e.number & 0xFFFF;
}

function abortWork(i) {
  try {
    if(xhr[i]) {
      try {
        xhr[i].abort();
        /*@if(@debug_log)
        abortTime[i] = new Date();
        @end @*/
      } catch (e) {}
    }
  } finally {
    active[i] = false;
    /*@if(@debug_log)
    putLog3(i, 'Abort', info[i].url);
    @end @*/
  }
}

function reportSummary() {
  var tsErrLog = void 0;

  var errorReporter = function (msg) {
    try {
      var ForWriting = 2, Unicode = -1;
      tsErrLog = tsErrLog || fso.OpenTextFile(errLogFilePath, ForWriting, true, Unicode);
      tsErrLog.WriteLine(msg);
    } catch(e){}
  }

  var summary = checkResult(errorReporter);

  tsErrLog && tsErrLog.Close();

  /*@if(@debug_log)
  emptyLine();
  putLog('File Count = ' + summary.fileCount + ' / ' + summary.total + ', Error Download = ' + summary.errorCount);
  @end @*/

  var prcsEndTime = new Date();
  var elapsedTime = Math.round(((prcsEndTime - prcsStartTime) / 1000)*10)/10;

  /*@if(@debug_log)
  putLog('END (Elapsed Time:' + elapsedTime + 'sec)');
  writeLogFile(logFilePath);
  @end @*/

  WScript.Echo('File Count = ' + summary.fileCount + ' / ' + summary.total
          + (summary.errorCount? ('\n\nError Download = ' + summary.errorCount) : '')
          + '\n\nElapsed Time = ' + elapsedTime + 's' );
}

function checkResult(errLogFunc) {
  var ret = new Object();
  ret.fileCount = 0;
  ret.errorCount = 0;

  var i;
  for(i = 1; i <= requestedMaxIndex; i++) {
    if(info[i] != null) {
      if (i == 1 || i <= endIndex) {
        if(fso.FileExists(info[i].filePath)) {
          ret.fileCount++;
        } else {
          if(typeof(errLogFunc) === 'function') {
            var errMsg = 'Download Error: ' + info[i].url;
            errLogFunc && errLogFunc(errMsg);
          }
          ret.errorCount++;
        }
      }
      /*@if(@debug_log)
      if(endTime[i]) {
        if(httpStatus[i] != 200) {
          storeLog(endTime[i], false, i, logMsg2(i, 'Returned HTTP Status: ' + httpStatus[i]) );
        }
        var label = fso.FileExists(info[i].filePath) ? 'done' : 'FAILED';
        storeLog(endTime[i], false, i, logMsg2(i, logMsg4(label, info[i].url, varsInfoAtEnd[i])));
      }
      @end @*/
    }
  }
  ret.total = ret.fileCount + ret.errorCount;

  return ret;
}

/*@if(@debug_log)
var endTime = new Array(arrayLength);
var abortTime = new Array(arrayLength);
var varsInfoAtEnd = new Array(arrayLength);
var logBuf = new Array();

function LogLine(timestamp, fromMainThread, index, logSeqno, message) {
  this.timestamp = timestamp;
  this.message = message;

  this.sortKey1 = timestamp.getTime();
  this.sortKey2 = fromMainThread? logSeqno : 0;
  this.sortKey3 = index;
  this.sortKey4 = logSeqno;

  //ログ出力順
  //まず、タイムスタンプで並び替える
  //タイムスタンプが同じなら
  //サブスレッドのログを先、メインスレッドのログは後
  //さらにメインスレッドのログは、ログ登録順
  //サブスレッドは、インデックス、ログ登録順
}

function storeLog(timestamp, fromMainThread, index, message) {
  var logSeqno = logBuf.length + 1;
  var logLine = new LogLine(timestamp, fromMainThread, index, logSeqno, message);
  logBuf.push(logLine);
}

function writeLogFile(logFilePath) {

  sortLogLines();

  var ForWriting = 2, Unicode = -1;
  var ts = fso.OpenTextFile(logFilePath, ForWriting, true, Unicode);

  var i;
  for(i = 0; i < logBuf.length; i++) {
    var s = logBuf[i].message;
    if(s == null) {
      ts.WriteLine();
    } else {
      s = dateToString(logBuf[i].timestamp) + '  ' + s;
      ts.WriteLine(s);
    }
  }
  ts.Close();
}

function sortLogLines() {

  function compareFunc(line1, line2) {
    return sign(line1.sortKey1 - line2.sortKey1 ||
          line1.sortKey2 - line2.sortKey2 ||
          line1.sortKey3 - line2.sortKey3 ||
          line1.sortKey4 - line2.sortKey4);
    //参考
    //JScript.NET(WSH JScriptではない)では比較関数が -1, 0, 1 の
    //いずれかの固定値を返さないと 0x80000000以上の値が正しくソートされない
  }

  logBuf.sort(compareFunc);
}

function sign(x) {
  return x > 0 ? 1 : x < 0 ? -1 : 0;
}
@end @*/

function putLog(str) {
  putLog2(0, str);
}

function putLog2(index, str) {
  /*@if(@debug_log)
    var fromMainThread = true;
    storeLog(new Date(), fromMainThread, index, logMsg2(index, str));
  @else
    if (str != null){
      str = dateToString(new Date()) + '  ' + logMsg2(index, str);
    }
    try {
      var ForAppending = 8, Unicode = -1;
      var ts = fso.OpenTextFile(logFilePath, ForAppending, true, Unicode);
      if(str == null) {
        ts.WriteLine();
      } else {
        ts.WriteLine(str);
      }
      ts.Close();
    } catch(e){}
  @end @*/
}

function putLog3(index, label, url) {
  putLog2(index, logMsg3(label, url));
}

function logMsg2(index, str) {
  if (str && index > 0) {
    return '(' + index + ') ' + str;
  } else {
    return str;
  }
}

function logMsg3(label, url) {
  return logMsg4(label, url, varsToString());
}

function logMsg4(label, url, varsInfo) {
  return (label + '        ').slice(0,  8) + quote(url) + ' ' + varsInfo;
}

function varsToString() {
  return '[Thread Count=' + padStart(countActiveThread(), ' ', 3) +
      ', endIndex=' + padStart(endIndex, ' ', 4) +
      ', requestedMaxIndex=' + padStart(requestedMaxIndex, ' ', 4) +
      ']';
}

function emptyLine() {
  putLog(void 0);
}

function quote(s) {
  return (s == null) ? '' : '\'' + s + '\'';
}

function padStart(value, padString, targetLength) {
  var str = String(value);
  return ('xxxxxxxxxx'.replace(/x/g, padString) + str).slice(-targetLength);
}

function padZero(value, targetLength) {
  return padStart(value, '0', targetLength);
}

function dateToString(dt) {
  var s = '';
  s += dt.getFullYear();
  s += '/' + padZero(dt.getMonth() + 1, 2);
  s += '/' + padZero(dt.getDate(), 2);
  s += ' ' + padZero(dt.getHours(), 2);
  s += ':' + padZero(dt.getMinutes(), 2);
  s += ':' + padZero(dt.getSeconds(), 2);
  s += '.' + padZero(dt.getMilliseconds(), 3);
  return s;
}

main();