2018年12月12日

Wikiart で公開されている作品を自動でダウンロードする (API編)


WikiartのAPIを利用して指定した作家の作品をダウンロードする方法をメモメモ…



以前の投稿(Wikiart で公開されている作品を自動でダウンロードする)を書いたあとで気づいたのですが、どうやらWikiartにはAPIが用意されているようです。以前は作者の作品一覧ページからいちいちDOMを読み込んで画像データへのURLを取得していましたが、APIを使えば一発で取得できます。完全に見逃してました…恥ずかし…。

というわけで、APIを使って全体的に書き直したのが次のスクリプトです。

  1. // ==UserScript==
  2. // @name Wikiart Image Downloader
  3. // @version 2
  4. // @match https://www.wikiart.org/*
  5. // @grant GM_download
  6. // ==/UserScript==
  7.  
  8. 'use strict';
  9.  
  10.  
  11. // 作品のダウンロードが完了してから次の作品のダウンロードを開始するまでの時間
  12. var interval = 2000;
  13. // ダウンロードが失敗したあともう一度ダウンロードをやり直すまでの時間
  14. var retryInterval = 10000;
  15. // ダウンロードが失敗したあと再試行する回数
  16. var maxRetry = 2;
  17.  
  18. var imageData = [];
  19. var failedData = [];
  20. var currentDownloadIndex = 0;
  21. var currentRetry = 0;
  22.  
  23.  
  24. addButton();
  25.  
  26. // 作家・作品情報の上にボタンを追加
  27. function addButton()
  28. {
  29. const artinfo = document.querySelector(".wiki-layout-artist-info,.wiki-layout-artwork-info");
  30. if(!artinfo) return;
  31.  
  32. const wrapper = document.createElement("div");
  33. wrapper.setAttribute("style", "margin-top: 1em; z-index : 100; float: right;");
  34. artinfo.parentElement.insertBefore(wrapper, artinfo);
  35.  
  36. const button = document.createElement("button");
  37. button.setAttribute("id", "download-all-images-button");
  38. button.setAttribute("style", "font-size: 1em; padding: 0.5em;");
  39. button.innerText = "Download All Images";
  40. wrapper.appendChild(button);
  41.  
  42. const info = document.createElement("div");
  43. info.setAttribute("id", "download-all-images-info");
  44. info.setAttribute("style", "font-size: 0.8em; padding: 0.2em; text-align: right;");
  45. wrapper.appendChild(info);
  46.  
  47. // 押したらスタート
  48. button.addEventListener("click", function(){
  49. enableButton(false);
  50. getImageData();
  51. });
  52. }
  53.  
  54. // WikiartのAPIから作品一覧を取得する
  55. function getImageData()
  56. {
  57. displayInfo("Accessing Wikiart API...");
  58.  
  59. const url = window.location.href;
  60. const artist = url.match(/https:\/\/www\.wikiart\.org\/.+?\/(.+?)(?:\/|$)/)[1];
  61. fetch("https://www.wikiart.org/en/App/Painting/PaintingsByArtist?artistUrl=" + artist)
  62. .then(response => { return response.json(); })
  63. .then(json => {
  64. for(let i=0; i<json.length; i++)
  65. {
  66. const artist = json[i].artistName;
  67. const title = json[i].title;
  68. const year = json[i].yearAsString;
  69. const url = json[i].image.replace(/![^!]+$/, ""); // 末尾の!Large.jpgを取り除く
  70. imageData.push({ artist: artist, title: title, year: year, url: url });
  71. }
  72. startDownload();
  73. })
  74. .catch(() => {
  75. console.log("Failed ...");
  76. displayInfo("Failed (Accessing Wikiart API) ...");
  77. enableButton(true);
  78. });
  79. }
  80.  
  81. // 作品のダウンロードを開始する
  82. function startDownload()
  83. {
  84. if(!imageData.length)
  85. {
  86. console.log("Can't find image data!");
  87. displayInfo("Failed (Can't Find Image Data) ...");
  88. enableButton(true);
  89. return;
  90. }
  91.  
  92. // 最後まで完了したらダウンロードに失敗した作品をコンソールに表示して終了
  93. if(currentDownloadIndex < 0 || currentDownloadIndex >= imageData.length)
  94. {
  95. console.log("Done!");
  96. console.log("Fails: " + failedData.length);
  97. displayInfo("Done! Failed: " + failedData.length);
  98. if(failedData.length) console.log(failedData);
  99. enableButton(true);
  100. return;
  101. }
  102.  
  103. const image = imageData[currentDownloadIndex];
  104. const url = image.url;
  105.  
  106. // ファイル名は "発表年 作品名.拡張子"
  107. const extension = url.match(/(\.[^.]+)$/)[1];
  108. let filename = image.title + extension;
  109. if(image.year) filename = image.year + " " + filename;
  110. filename = filename.replace(/[\/\\?%*:|"<>]/g, ""); // 禁止文字は消去
  111.  
  112. console.log("Download Start!: " + image.title + "(" + currentDownloadIndex + ")");
  113. displayInfo("Downloading ... (" + (currentDownloadIndex + 1) + "/" + imageData.length + ")");
  114. download(image.url, filename);
  115. }
  116.  
  117. // GM_download()を使ってローカルフォルダに指定したファイルをダウンロードする
  118. function download(url, filename)
  119. {
  120. const arg = { url: url,
  121. name: filename,
  122. saveAs: false,
  123. onerror: onError,
  124. onload: onLoad,
  125. ontimeout: onTimeout
  126. };
  127. GM_download(arg);
  128. }
  129.  
  130. // 作品のダウンロードに成功したら次の作品に進む
  131. function onLoad()
  132. {
  133. console.log("Download Complete!: " + imageData[currentDownloadIndex].title + "(" + currentDownloadIndex + ")");
  134. console.log("--------------------");
  135.  
  136. currentDownloadIndex++;
  137. currentRetry = 0;
  138. setTimeout(startDownload, interval);
  139. }
  140.  
  141. // ダウンロードに失敗したときに再試行する
  142. function retry()
  143. {
  144. currentRetry++;
  145. // 規定回数ダウンロードを繰り返す
  146. if(currentRetry <= maxRetry)
  147. {
  148. console.log("Retry! " + currentRetry);
  149. setTimeout(startDownload, retryInterval);
  150. }
  151. // それでもダメだった場合はあとで見つけやすいように登録しておく
  152. else
  153. {
  154. const index = currentDownloadIndex;
  155. const title = imageData[currentDownloadIndex].title;
  156. const url = imageData[currentDownloadIndex].url;
  157. failedData.push({ index: index, title: title, url: url });
  158.  
  159. console.log("--------------------");
  160.  
  161. // 続行
  162. currentDownloadIndex++;
  163. currentRetry = 0;
  164. setTimeout(startDownload, interval);
  165. }
  166. }
  167.  
  168. function onError(err)
  169. {
  170. console.log("*** Error! *** " + imageData[currentDownloadIndex].title + "(" + currentDownloadIndex + ") was not downloaded! Reason: " + err.error);
  171. retry();
  172. }
  173.  
  174. function onTimeout()
  175. {
  176. console.log("*** Timeout! ***" + imageData[currentDownloadIndex].title + "(" + currentDownloadIndex + ") was not downloaded!");
  177. retry();
  178. }
  179.  
  180. function enableButton(isEnabled)
  181. {
  182. const button = document.querySelector("#download-all-images-button");
  183. if(button) button.disabled = !isEnabled;
  184. }
  185.  
  186. function displayInfo(string)
  187. {
  188. const info = document.querySelector("#download-all-images-info");
  189. if(info) info.innerText = string;
  190. }

ボタンを表示するための処理を追加しているので、ちょっと長くなっちゃってます…。

使い方

使い方は以前のものとほぼ同じですが、今回は自動スタートではなくボタンを押すと開始します。
  1. Tampermonkeyに上記のスクリプトを登録する
  2. 「名前を付けて保存」の確認ダイアログが表示されないよう設定変更する
    Chromeの場合であればブラウザ設定から「ダウンロード前に各ファイルの保存場所を確認する」をオフに、Tampermonkey設定から「ダウンロードのモード」を「ブラウザーAPI」にしておく(詳しくは以前の投稿を参照のこと)
  3. ダウンロードしたい作者のページか、その作者が描いたいずれかの作品ページを開く
  4. 作家/作品情報の右上に表示されている "Download All Images" ボタンを押す
  5. ボタンはページ右上に表示
  6. ダウンロードが始まるのでブラウザのデベロッパーツールを起動(F12)してコンソールを眺める
  7. "Download Start!"や"Download Complete!"などの表示がずらずらと流れていれば成功
  8. 完了するまでひたすら待つ

必ず前回の投稿の注意事項を読んでから実行してください。ファイル名やボタン配置など、気にくわない部分は自由に変更して使ってください。


Wikiart API

現在WikiartのAPIには旧版と新版(v2)の2種類があります。上記のスクリプトで使っているのは旧版です。新しいほうでは認証システムが導入されて簡単にログインできるようになっていたり、ページネーション(取得するデータ項目が多すぎたときに小分けにして表示する機能)が追加されていたりといろいろ強化されているようですが、一方でまだ実装されていない機能があったり、意図した動作にならなかったりと不安定な部分があるらしく、念のためまだ古いAPIも残している…という状態らしいです。


この文章を書いている時点(2018年12月12日現在)ではまだ新版のドキュメントが未完成状態でTODOだらけなので、おそらくもうしばらくは旧版のままでも大丈夫だと思います。

基本的にAPIは誰でも使えますが、無料で使う場合は「1時間に400リクエストまで」という制限がかかります。たとえばWikiartを利用したアプリを作成したい場合など、リクエスト数の上限を引き上げる必要がある場合には申請して有料版にする感じでしょうか。正直どういう操作で1リクエストになるのかよくわかりませんが、個人で軽く使う分にはそうそう超えないとは思います(たぶん)。料金ごとの上限など詳しい情報はWikiart APIについてのページに記載されています。

指定した作家の作品情報一覧(画像データへのURL含む)を取得する場合は

(旧) https://www.wikiart.org/en/App/Painting/PaintingsByArtist?artistUrl=作家名
(新) https://www.wikiart.org/en/api/2/PaintingsByArtist?id=作家ID

のようにアクセスすればOK。json形式で全作品データが閲覧できます。ただし、これで取得した画像URLには

https://uploads0.wikiart.org/images/katsushika-hokusai/rainstorm-beneath-the-summit.jpg!Large.jpg

のように末尾に画像サイズ指定用の文字列(!Large.jpg)がくっついているため、原寸大でダウンロードしたい場合は取り除く必要があります。その他、データの取得方法やサムネイルのサイズ指定方法などの詳しい情報はドキュメントを参照してください。


0 件のコメント:

コメントを投稿