1
/
5

iCloud共有アルバムをPython+Seleniumで一括自動ダウンロードした話

みなさんこんにちは。3plus 冨原です。

夏です。いいですねー!夏!!!

仕事が忙しいにも関わらず、石垣の海に行ってきました!

ーーーーーー
今回のまとめ:
Selenium使うと、かなり高度なScriptでも回避して自動化できちゃうよ!
その際のアプローチはこんな感じでやると成功することがあるかもよ!
時には妥協も必要だから、この程度ならセーフ!って話もあるよ!
お仕事だと妥協するとにらまれることもあるから、タフな精神を身に付けよう!
ーーーーーー

さて、旅行に行ってきたのはもちろんスキンダイビング目的!
波照間島でビーチエントリーしていっぱいウミガメの動画とかを撮ってきましたよ。
カクレクマノミも、あんま隠れずに出てきてくれました!

ただ、1回だけ石垣島でツアーを利用し、沖まで行ってきました。
いっぱい写真も撮ってもらいました。GoProも大活躍!

「じゃ、リンク送りますね!」と言われ、後日LINEに届いたリンクをクリックすると・・・
iCloud共有アルバムで、なんと511枚も写真送ってくれてます!!

みなさんも、石垣島で海で遊びたいなら、ファウストさん行った方がいいです。
サービスいいし、めっちゃ写真撮ってくれる。

しかも構図もめっちゃいい。映え。写真うまいのはホント大事ですね。
あと潜るポイントもばっちりです。初めて行ったときもマンタ3枚見れたし。

石垣島 ファウスト
https://fausto-ishigaki.com/

そして、送ってもらったリンクは「iCloud 共有アルバム」。
ありがとう!龍さん!おーしダウンロードすっか!と思ったら、一括ダウンロードボタンとか、そういうのが無い!

あれー、サーバ負荷がかかるから禁止なのかなぁ・・・と思いながら、いろいろとネットで検索・・・
でも、「一括でのダウンロードはありません」みたいな記事しかないんですね。

なんでやねん!工夫したらいけるやろ!誰か抜け道くらい見つけとけや!!

えええ・・・511枚もの写真、どうやってダウンロードするん・・・腱鞘炎なるやん・・・

と思いながら、まずは共有アルバムの仕様をブラウザでチェックしてみました。


1)JavaScriptベースの、スクロールしたらパラパラ出るヤツ

2)一覧表示されているのはサムネイルで、ちゃんとした画質のものは個別に表示しないと出てこない

3)個別表示用のURLはHTMLに埋め込まれているのではなく、JSONで別途情報を取得して組み立ててるみたい

4)画像単位でなく、6枚を1単位としたブロックが存在し、そのブロックが表示状態になると個別URL用JSONを取得し、サムネ表示を行う

5)しかも最初にブロックを取得しても、個別表示でポップアップするとサムネブロックは消え、ポップアップを閉じた時に再作成される
※ここ重要で、スクレイピングできない一番の理由です。

6)再作成されたブロックは元のブロックとは別物なので、最初に取得したものをクリックしようとしてもムダ


つまり、「iCloud共有アルバム」というものは、どういう仕様かというと・・・

ダウンロードは手でポチポチやるしかねえ!!!

ってことです。


さて、ここからが本題です!

つまり、iCloud共有アルバムは、手でポチポチやらないとJavaScriptがいい感じに動いてくれない訳です。かなり高度です。HTMLからチョイチョイURL生成して・・・という訳にはいきません。

つまりスクレイピングは厳しい、ということ。

じゃあそれ(手でポチポチ)をトレースすればいいよね、
ということで、今回はSeleniumを利用します。

Seleniumは、以前自分用にGo言語で利用し、あとはPythonで利用しました。
ので、ちょっとは効率的に開発できるはず。

あと、Java+Seleniumなんていうのも講座で教えてたりします。

テスト自動化 ( Java ) 体験で学ぶ品質向上のキホン|研修コースに参加してみた
https://www.seplus.jp/dokushuzemi/blog/2021/05/how2get_psychological_safety4dev.html


まぁ、Pythonが楽かなー、ということで、今回はPython+SeleniumでiCloud共有アルバムダウンローダー作りましょう。

共有アルバムは、先ほどもあったように「ブロック」の中に「写真」が6枚入っています。一番下で端数調整しています。
つまり、今回の511枚であれば、86ブロックあるわけです。

前述のように、この86ブロックは個別表示した時点で消え、再作成されます。

つまり、流れとしては以下。

1)全ブロック数を取得
2)[ループA]1ブロックずつ処理
3)全ブロックを取得し、対象ブロックを特定
4)対象ブロックまでスクロール
5)中のサムネイルを1件以上(6件以下)取得
6)[ループB]1サムネごとに処理
7)対象サムネをクリック
8)個別表示された画像のURLを取得
9)対象URLをダウンロード&保存
10)ESCキーを送り、個別表示を閉じる
11)[ループB 6)に戻る]
12)[ループA 2)に戻る]

こんな感じでしょうか。

細かい仕様はプログラム見てもらうとして、重要な点は以下です。

1)対象ブロックをスクロールして表示することで、中のサムネイルを生成させる
2)個別表示を「次へ」で進めていくのではなく、毎回閉じる

2)は、「次へ」進めるボタンで取得していくと、なんでかうまくいかない・・・と勘違いしたため、この仕様になりました。
実は、iCloud共有アルバム、「同一ファイル名のアップロード」が可能なのです。
しかも、ダウンロードすると同一ファイル名なのです(クエリパラメータで別画像を出している)。

最初、511枚保存できていないのにファイル名がループしたため、全部ループしてくれないのかな?と勘違いし、じゃあ個別に開いて閉じて、最後まで取得しよう!と思ったのでこのような仕様になりました。
まぁ、どんな仕様であっても、データ取得さえできりゃいいんです。

なんやかんやで、40分ほどかかって作ったプログラムがこれです。
※Apple社のサーバに負荷をかけるような設定でアクセスしないようにしてください!!
※スクレイピング・自動アクセス系は、相手サーバに負荷をかけないよう十分な配慮が必要です!!


from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from urllib.parse import urlparse
import time
import requests
import os

siteurl="共有アルバムのURL"
foldername="任意のフォルダ名(保存先)"
speed = 1
os.makedirs(foldername, exist_ok=True)

#webdriver.chrome.options.add_argument("--headless")
driver = webdriver.Chrome('chromedriver')
driver.get(siteurl)
#10秒たったらタイムアウト
driver.implicitly_wait(10)
#全ブロック取得
blocks=driver.find_elements_by_class_name("x-stream-photo-group-block-view")
count=1
blocksCount=len(blocks)
print("blocks count = " + str(blocksCount))
for i in range(blocksCount):
    #ブロック内のサムネイルを取得。取れなければちょっと待つ
    while True:
        thumbnails=blocks[i].find_elements_by_class_name("x-stream-photo-grid-item-view")
        if(len(thumbnails) > 0):
            break
        else:
            time.sleep(speed * 2)
    print("block:" + str(i+1) + " thumb count:" + str(len(thumbnails)))
    for thumbnail in thumbnails:
        #サムネイルをクリックして個別表示
        thumbnail.click()
        time.sleep(speed * 2)
        img=driver.find_element_by_css_selector(".image-view > img")
        imgUrl = urlparse(img.get_attribute("src"))
        imgFileName = os.path.basename(imgUrl.path)
        #iCloud共有アルバム仕様:同一ファイル名でもアップ可能。しかもそのまま同一ファイル名で取得するので注意。
        #なので仕方なく連番を先頭に付与
        imgFileName = str(count).zfill(4) + "_" + imgFileName
        #この名前ルールで既に存在するのは流石にダウンロード済みなのでスキップ。
        fileExists = os.path.isfile(foldername + "\\" + imgFileName)
        if not fileExists:
            #画像をダウンロード&保存
            imgData = requests.get(img.get_attribute("src")).content
            with open(foldername + "\\" + imgFileName ,mode='wb') as f:
                f.write(imgData)
            act = "downloaded:" + imgFileName
        else:
            act = "skip:" + imgFileName
        body=driver.find_element_by_tag_name("body")
        #ESCで個別表示を閉じる
        body.send_keys(Keys.ESCAPE)
        print(act + " complete:" + str(count) + " photo(s)")
        count += 1
        time.sleep(speed * 5)
driver.close()
exit()


これでやると、1枚目がなんでかサムネサイズで取得されてしまうのですが、たぶん初回は裏で別の通信が忙しいからかな・・・と思いながら、さすがに1枚なら手動で取っちゃえばいいか、と思い、それは修正していません。

何が言いたかったかというと

1)便利なモン作る時は、こだわって最後まで作るより「いかに時間を割かないか」を優先し、完璧なんて捨てていい
2)でもやっぱり511枚手動でポチポチするような原始人みたいなマネはしたくない

こういった意識を持って、プログラムに接することで「いい経験できたー」ってのと「また次行ったときはプログラム組まなくて済むなー」って感じることができます。

とはいえ、511枚手動でやれば1枚5秒で保存して42分・・・手早くやれば、手動の方が速かったかもしれません。

でも!次回は完全自動でできるから!こういうのは再利用性が大事だから!

で、次ダウンロードする時に使うと「あれ?iCloudの仕様変わってね?」ってなって結局プログラム書くことになるっていう。やめて。

株式会社3plusでは一緒に働く仲間を募集しています
2 いいね!
2 いいね!
同じタグの記事
今週のランキング