pythonのpixivpyを使ってpixivから特定のユーザーの作品をすべて一括でダウンロードする（うごイラ含む）(その1)

# 2021/11/25追記　こちらの記事は残しますが、動きません。

apiの変更などによりプログラムを一新したため、イラストのダウンロードの記事をこちら

https://qiita.com/yuki_2020/items/716fa4e4ada65306f688

をメインとして書いていくことにします。
現在こちらの記事に書いているプログラムはバージョン1.0であり、念のため残しておきますが、__動きません。__

なお、新しい記事のバージョン2.0において、特定のイラストレーターさんの作品をダウンロードしたい場合は、

```.py
for user_id in client_info["ids"]:
```

となっているところを、

```.py
for user_id in [11,12848282]:
```

というようにダウンロードしたいユーザーのid書き換えてに書き換えてください。
上の例ですと、ユーザーidが`11`、`12848282`のユーザーの作品をダウンロードするようになります。


よろしくお願いします。



<!--
# 2021/11/7追記　現在この記事のプログラムは動きません。
どうやらpixiv側が`api = PixivAPI()`としていた`Public API`を無効化したようで、このapiを使用しているこの記事のプログラムは動かなくなりました。
`aapi = AppPixivAPI()`としているapiは使用できるので、こちらに移行中です。
プログラムができ次第、更新します。
しばしお待ちを。



# 2021/2/21追記　ログイン方法を変えることで動くようになりました！！

https://qiita.com/yuki_2020/items/759e639a4cecc0770758

記事に従ってプログラムを変更してください。

今のこの記事のプログラムは変更していないので動きません。
自分で直してください。m(__)m
-->


<!--
# 2021/2/11追記　仕様変更によって現在動きません(解決済み)

https://github.com/upbit/pixivpy/issues/158
こちらに詳しいことがありますが、pixivpyはもともとandroidアプリを通してログインをしていましたが、
アップデートでログイン方法が変わったため、現在ログインできません。

```
PixivError: [ERROR] auth() failed! check username and password.
HTTP 400: {"has_error":true,"errors":{"system":{"message":"Invalid grant_type parameter or parameter missing","code":1508}},"error":"invalid_grant"}

```
こうなります。
とりあえず今はpixivpyの改良を待つか、
https://github.com/danbooru/danbooru/blob/39cc3ed5cf913499093d2f641d70d7682a14fa42/app/logical/pixiv_ajax_client.rb
こんなものが紹介されていたので、動くかわかりませんがrubyを勉強して試してみます。

-->


# 概要
pixivからイラストをいちいち1枚ずつダウンロードするのは面倒くさいですよね
pythonを使って特定のユーザーの作品を自動でダウンロードできるプログラムを作りました。
画像だけでなく漫画、うごイラ、にも対応しています。
特にうごイラはそれぞれの画像をダウンロードし、gifも作成します。
タグや閲覧数による絞り込みや、ダウンロードする上限などを決めることができます。



# 参考にしたサイト

自動でダウンロードしてくれるソフトがないか調べてみましたが、なかなか見つかりません。
そこで見つけたのが

https://qiita.com/Hirosaji/items/304de7508df4b1cae904

https://www.mathgram.xyz/entry/scraping/pixiv

https://qiita.com/choshicure/items/8795bf929e34af6622fc

https://qiita.com/Mechanetai/items/1c59ba627ebc1654868d

https://ithikawa.hatenablog.com/entry/2018/11/11/203125

こんなサイトです。

主に

https://qiita.com/Hirosaji/items/304de7508df4b1cae904

を参考にして作ったプログラムです






# 注意
pixivpyはおそらく中国の有志が作ったライブラリであり、公式のものではありません。
そのためログイン時のユーザーid、passwordは流出してもいいように捨て垢を使うなどの方法を使うことをお勧めします。
また乱用厳禁です。
スクレイピングのルールとして1秒当たりリクエリは1回までという暗黙のルールがあります。
これがないとdos攻撃となってしまい、法的にアウトです。
sleepをコメントアウトする際は自己責任でお願いします。
また何か問題が起きても自己責任でお願いします。


# 目次
* [動作環境](#動作環境)
* [ディレクトリ](#ディレクトリ)
* [ライブラリのインポート](#ライブラリのインポート)
* [コード](#コード)
* [使用方法](#使用方法)


## 動作環境
このプログラムはwindows10環境下で動かしています。
Linux、Mac環境下で動作するかはわかりません。
2020/12/30現在でのanacondaの最新のバージョンで動きます

* python3.8.5
* pixivpy3.5.10
* pillow8.0.1


## ディレクトリ
こんな感じに用意してください

```
.
├── pixiv_downloader.py
├── img
│   ├── 
├── client.json

```
画像だとこんな感じ
![ディレクトリ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/b011b43e-3c7a-c9c8-bff0-05d901d9d004.png)

pythonを実行する前に、このディレクトリまで移動してください。
移動の仕方

https://kenchikku.com/archi-prog/py-a02/



## ライブラリのインポート
```
pip install pixivpy
```
インストールできないときは管理者でコマンドプロンプトを開いてやってみてください

使用したライブラリ：pixivpy
中国語（オリジナル）

https://github.com/upbit/pixivpy

日本語

https://github.com/tsubasa/pixiv.py


## client.jsonの作成
ログイン用に使う情報を書いてください
メモ帳などで下の内容をコピペして自分の情報を書き、保存時に拡張子を.txtではなく.jsonにしてclient.jsonという名前で保存して下さい。



```client.json
{
  "pixiv_id": "pixiv_id",
  "password": "password",
}
```
![ログイン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/d45a813f-6a92-5b62-8503-315c98b44563.png)
"pixiv_id"、"password"は画像のところに普段入力している文字列です。
`"pixiv_id": "12345"`のように""で囲って記述してください。



## コード
以下をメモ帳などにコピペし、pixiv_downloader.pyという名前で保存してください。

```pixiv_downloader.py

from pixivpy3 import *
import json
import os
from PIL import Image
import glob
from time import sleep


#ここは各個人にやってもらう設定です

#ダウンロードしたいユーザーのid(webでユーザーのページに行った際のurlの最後にある数字です)
id_search = 11
#一人当たりダウンロードする作品の最大数、すべてダウンロードしたい場合はできるだけ大きくすること（一人の作者の作品数を超えること）
#新しい順にダウンロードされます
works=10
#ブックマーク数によるフィルター、最小値を設定、0ならすべて
score=0
#閲覧数によるフィルター、最小値を設定、0ならすべて
view=0

#タグによるフィルター　書き方 target_tag = ["Fate/GrandOrder","FGO","FateGO","Fate/staynight"]
target_tag = [] #target_tag内に複数書くと、そのうち少なくとも一つあればダウンロード
target_tag2 = []#さらにtarget_tag2に書くとtarget_tagを満たし、かつtarget_tag2を満たすものだけダウンロード
extag = ["R-18"]#extag内のタグが一つでも入っていればダウンロードしない

#画像を保存するディレクトリ
main_saving_direcory_path = "./img/"




#事前処理

#事前に作ったアカウント情報が書かれたファイルを読み込む
with open("client.json", "r") as f:
    client_info = json.load(f)

# pixivpyのログイン処理
api = PixivAPI()
api.login(client_info["pixiv_id"], client_info["password"])
aapi = AppPixivAPI()
aapi.login(client_info["pixiv_id"], client_info["password"])




#ここからダウンロードについて

#入力されたidの作品数を取得
illustrator_id = api.users_works(id_search, per_page=works)


#アカウントだけで絵が投稿されていない人を避けるため
if not illustrator_id.count == 0:
    total_works = illustrator_id.pagination.total
    if works < total_works:
        total_works=works

    #ユーザーの情報を得るために最初の作品のデータをとってくる
    illust = illustrator_id.response[0]

    #user名でフォルダを作るため、windowsのフォルダ名に使えない文字を置換
    username = illust.user.name.translate(str.maketrans({'/': '_' , ':': '：', ',': '_', ';': '；' ,'*': '_', '?': '？', '"': "'", '>': ')', '<': '(', '|': '｜'}))
    username = username.rstrip(".")
    username = username.lstrip(".")
    username = username.rstrip(" ")
    username = username.rstrip("　")

    #ユーザー名（id）の形でフォルダを作成
    
    saving_direcory_path = main_saving_direcory_path + username + ("(") +str(illust.user.id) + (")") + "/"

    #フォルダ名を最新のユーザー名に直す
    saving_direcory_name = saving_direcory_path[:-1]
    present_folder_list = glob.glob(main_saving_direcory_path + "*")

    for present_dir in present_folder_list:
        num = present_dir.rsplit("(", 1)[-1][:-1]
        #print(num)
        name = present_dir.rsplit("\\", 1)[-1]
        name = name.rsplit("(", 1)[0]
        #print(name)
        #print("--------------------------------------")
        if num == str(illust.user.id) and username != name:
            print(present_dir + "を次に変更" + saving_direcory_name)
            print("--------------------------------------")
            os.rename(present_dir, saving_direcory_name)





    #フォルダが存在しないときは作る
    if not os.path.exists(saving_direcory_path):
        os.mkdir(saving_direcory_path)
    separator = "------------------------------------------------------------"

    #Display information of illustrator and the number of illustrations
    print("Illustrator: {}".format(illust.user.name))
    print("Works number: {}".format(illustrator_id.pagination.total))
    print(separator)




    #Download
    for work_no in range(0, total_works):

        illust = illustrator_id.response[work_no]

        #フィルター

        #タグによるフィルター
        if len(list(set(target_tag)&set(illust.tags))) == 0 and target_tag != []:
            continue
        if len(list(set(target_tag2)&set(illust.tags))) == 0 and target_tag2 != []:
            continue
        #exタグが一つでも入っていたらスキップ
        if len(list(set(extag)&set(illust.tags))) > 0 :
            continue
        #score以上の作品だけダウンロード
        if illust.stats.favorited_count.private + illust.stats.favorited_count.public < score :
            continue
        #view以上の作品だけダウンロード
        if illust.stats.views_count < view :
            continue



        #if the illustration has already downloaded, skip downloading it
        #pixivに投稿できるのはpng,jpg,gif,うごイラ
        #ここではあくまで1ページ目がダウンロードされているかで判断しているので、ダウンロード中に通信が途切れ、２ページ目以降がダウンロードされていないかはわからない

        if os.path.exists(saving_direcory_path+str(illust.id)+"_p0.png") or os.path.exists(saving_direcory_path+str(illust.id)+"_p0.jpg") or os.path.exists(saving_direcory_path+str(illust.id)+'_ugoira') or os.path.exists(saving_direcory_path+str(illust.id)+"_p0.gif"):
            print("Title:"+str(illust.title)+" has already downloaded.")
            print(separator)
            continue

        #ダウンロード前のスリープ
        sleep(1)
        print("Now: {0}/{1}".format(work_no + 1, total_works))
        print("Title: {}".format(illust.title))


        #うごイラ
        if illust.type == "ugoira":
        #イラストIDの入力待機
            illust_id = illust.id
            ugoira_url = aapi.illust_detail(illust_id).illust.meta_single_page.original_image_url.rsplit('0', 1)
            ugoira = aapi.ugoira_metadata(illust_id)
            ugoira_frames = len(ugoira.ugoira_metadata.frames)
            ugoira_delay = ugoira.ugoira_metadata.frames[0].delay
            dir_name = saving_direcory_path + str(illust_id)+'_ugoira'


            #うごイラを保存するフォルダの作成
            if not os.path.isdir(dir_name):
                os.mkdir(dir_name)

            #うごイラに使われているすべての画像のダウンロード
            for frame in range(ugoira_frames):
                #ダウンロード中のsleep
                sleep(1)
                frame_url = ugoira_url[0] + str(frame) + ugoira_url[1]
                aapi.download(frame_url, path=dir_name)


            #保存した画像をもとにgifを作成
            #力不足でgifにする際に結構画像が劣化します
            #pythonでgif作成のライブラリを探しましたが、圧縮度合いを設定できるものが見つかりませんでした。
            frames = glob.glob(f'{dir_name}/*')
            frames.sort(key=os.path.getmtime, reverse=False)
            ims = []
            for frame in frames:
                ims.append(Image.open(frame))
            ims[0].save(f'{dir_name}/{illust_id}.gif', save_all=True, append_images=ims[1:], optimize=False, duration=ugoira_delay, loop=0)

        # illustrations with more than one picture
        elif illust.is_manga:
            work_info = api.works(illust.id)
            for page_no in range(0, work_info.response[0].page_count):
                #ダウンロード中のsleep
                sleep(1)
                page_info = work_info.response[0].metadata.pages[page_no]
                aapi.download(page_info.image_urls.large, saving_direcory_path)

        # illustrations with only one picture
        else:
            aapi.download(illust.image_urls.large, saving_direcory_path)

        print(separator)


    print("Download complete!　Thanks to {0}{1}!!".format(illust.user.id, illust.user.name))
```


## 使用方法
```pixiv_downloader.pyの最初のほうにあるところ.py
#ここは各個人にやってもらう設定です
#ダウンロードしたいユーザーのid(webでユーザーのページに行った際のurlの最後にある数字です)
id_search = 11
#一人当たりダウンロードする作品の最大数、すべてダウンロードしたい場合はできるだけ大きくすること（一人の作者の作品数を超えること）
#新しい順にダウンロードされます
works=10
#ブックマーク数によるフィルター、最小値を設定、0ならすべて
score=0
#閲覧数によるフィルター、最小値を設定、0ならすべて
view=0
#タグによるフィルター　書き方 target_tag = ["Fate/GrandOrder","FGO","FateGO","Fate/staynight"]
target_tag = [] #target_tag内に複数書くと、そのうち少なくとも一つあればダウンロード
target_tag2 = []#さらにtarget_tag2に書くとtarget_tagを満たし、かつtarget_tag2を満たすものだけダウンロード
extag = ["R-18"]#extag内のタグが一つでも入っていればダウンロードしない
```

個々のコメントにあるように設定してください
上記の状態では、pixiv公式のアカウントからR-18のタグが付いた作品を除く中から新しい順に10こダウンロードされます。
詳しく解説します。
### id_search

id_searchはダウンロードしたいユーザーのユーザーidです。
ユーザーのページに行き、urlの最後の番号です。
![InkedDesktop Screenshot 2020.12.31 - 21.22.03.74-1_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/3f918582-79d2-c144-35a6-26306ebe2bf8.jpeg)

`https://www.pixiv.net/users/11`

のようにあったら11です
その後実行すればimgフォルダー内にユーザーごとに分かれたフォルダが作成され、中に画像がダウンロードされます
今回はpixiv事務局(11)というフォルダ内にダウンロードされます。

### works
今回`works=10`となっていますが、これは新しい順に10作品（漫画などはまとめて1とカウント）ダウンロードされます。
なおすでにダウンロードされている画像もカウントします。
ダウンロードする数を制限しない場合は`works=10000000000`など、膨大な数にしてください。

### score
scoreはブックマークの数を表しています。
今回の`score=0`ではすべての作品をダウンロードします。
例えば`scre=100`とすれば100未満の作品はダウンロードされません。

### view
viewは閲覧数を指します。
動作はscoreと同じで、その値以上の作品だけダウンロードします。

### tag
tagの挙動は面倒くさいです。
`target_tag = ["Fate/GrandOrder","FGO","FateGO","Fate/staynight"]`の時、
タグの中にFate/GrandOrder __または__FGO __または__FateGO __または__Fate/staynightが含まれていればダウンロードします。
さらに、
`target_tag = ["Fate/GrandOrder"]
target_tag2 = ["FGO"]`
の時はFate/GrandOrder __かつ__FGOが含まれている作品がダウンロードされます。

extagは要素が一つでも含まれていたらダウンロードしません。
`extag = ["R-18","R-15"]`の場合、R-18 __または__R-15というタグが含まれていればダウンロードしません。

### エラーについて
実行中に

```
PixivError: requests GET https://public-api.secure.pixiv.net/v1/works/58652797.json 
error: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
```

と出た場合は短時間にダウンロードしすぎた場合だと思われます。
時間をおいてもう一度実行してください
今のところアカウントがbanされたことはありません。

# まとめ
今回はpythonを使ってユーザーの作品をフィルターをかけてダウンロードする方法を解説しました。
自分がこれを作り始めたきっかけは2、3年前に好きだった絵師さんが作品を全部消してしまったことがあったからです。
あの時全部ダウンロードしとけば、でも手動では時間が足りない、と思って作り出しました。
2年間自分で使ってみて、バグを直したり、gifダウンロードできるようにしたりしていました。
初期のころはユーザーさんが名前を変えると作品を全部ダウンロードしなおすこともあり、大変でした。
最近完成してきたかな、と思い公開しました。
追加してほしい機能、バグなどありましたらコメントしてください。
ありがとうございました。



# 展望
pixivpyについてもっと解説しようかな
~~フォローしているユーザーの作品をすべてダウンロードするプログラムも作ってあるので、時間があったら書きます~~
書きました

https://qiita.com/yuki_2020/items/716fa4e4ada65306f688

破損したファイルの検出、すべてダウンロードできなかった場合の発見方法なども作ってあるので時間があれば

http://shimarisu.webcrow.jp/pixiv_api.html

こんなのを見つけてしまった。
同じくpixivpyを使っている
exif(画像のメタデータ)を書き込む機能があるみたい
gifをmp4にしている　←　その手があったか
ffmpegを使っているぽい
folder.jpgという画像を用意すればフォルダのサムネを指定できるらしい

https://xtech.nikkei.com/it/pc/article/NPC/20060420/235865/

このプログラムでは作者の画像をこうしてダウンロードしている。



# 編集履歴のうち重要なもの
2020/1/4追記
2021/1/4以前のプログラム等、間違っていて動作しませんでした。
すみませんでした。
直したのでもう一度使ってみてください。