Puppeteerを使ってGoogleフォトのストレージ使用量を0GBに削減するツールを作成しました
この記事で紹介すること
- Googleフォトの仕様
- GoogleOneの仕様
- Puppeteerを使ってGoogleフォトの使用量を削減するツールの紹介
- Puppeteerを使ってGoogleフォトの使用量を削減するツールの作り方
動機
支出を抑えるためGoogle Oneの容量を削減したい!
GoogleOneは2TBプランが月1300円、200GBプランが380円、100GBプランが250円、無料プランが15GBと細かく設定されています。
私は2TBプランを長年使っており、いつ使うかわからないデータでもどんどんGoogleドライブやGoogleフォトにバックアップをとってました。ただiPhoneを使うようになってiCloudで充分かなと思うようになったので、とりあえずGoogleOne無料プランの15GB目指してデータを一気に削除しようと試みました。
GoogleOneはすぐやめたくてもすぐ容量削減できない\(^o^)/
GoogleOneは3つのサービスになっておりGoogleドライブ、Gmail、Gooleフォトの3兄弟で構成されていますがGoogleドライブ以外は癖がありすぐ削除できないのです。Gmailを使わなければアカウントを停止すればいいだけですがストレージは消して無料化したい、でもGmailだけ使い続けたいという場合かなり困難な道を突破する必要があります。サブスクリプションの設計としてすぐやめれないようになっています。。。
Googleドライブの削除仕様 (難易度:NORMAL)
- ルートフォルダ単位で全部消せます。簡単です
- GAS(Google Apps Script)も使えます(が使う必要性がないです)
- 「データをゴミ箱へ」 「ゴミ箱を空にする」 という2段アクションが必要になります
- データが多いと「データをゴミ箱へ」 「ゴミ箱を空にする」のアクションがそれぞれ時間が、かなりかかります。ゴミ箱にデータを送信するジョブと、ゴミ箱を空にするジョブが別にある感じです。なのでデータ上は空に見えてもストレージ容量は減ってないなど摩訶不思議な状態になったりします。
- 1TBだと削除に1週間はかかるイメージです(ファイル数にもよる)。
Gmailの削除仕様 (難易度:HARD)
- メール件数が多いとメールを一括に消すことができません。
- GAS(Google Apps Script)も使えます
- GASが使えますが、GAS無料プランだとタスク処理の制限もあり削除にかなり時間がかかります
- 無料プランでGASを使ってますが15GB消すのに1ヶ月かかる感じで今動かしています
- Googleドライブと同じく、「データをゴミ箱へ」 「ゴミ箱を空にする」というアクションが必要になります
Googleフォトの削除仕様(難易度:HELL)
- 写真動画を一括に消すことができません
- そもそもフォルダという概念がなく、アルバム単位での処理になります、アルバムも日付や場所でかってに作られるので役に立ちません
- 総ファイル数も不明
- GAS(Google Apps Script)も使えません
- 普通にやると手で10個選択して消すー>10個選択して消すを永遠と続ける必要があります・・・・
- ゴミ箱にいれた画像はGoogleOneの容量を食わないという仕様(唯一の良心)
- そのためゴミ箱にいれれば削除完了になる(ゴミ箱は60日で削除される)
今回はこの一番難易度が高い怪獣Googleフォト君を倒すべくChromeの自動操作ツールであるPuppeteerでデータを削除するツールを作りました。
PuppeteerでGoogleフォトを自動的に削除するツールの紹介
ソフトはこちらに置いてあります
Puppeteerですのでヘッドレスでもヘッドありでも動きます。動作を見ながらやったほうがいいのでブラウザを表示しながらやるのがおすすめです。わりと高速化してエラーの発生率を抑える限界値での速度にして実装していますが5時間で7GB削除程度です(画像ファイルのみの場合)
削除ファイル数 | かかる時間(分) |
---|---|
1000 | 16 |
5000 | 80 |
削除バイト数 | かかる時間(時間) |
---|---|
1.4GB | 1 |
7GB | 5 |
使い方としてはこちらのコマンドがおすすめです。
node nizika.js -c google.cookie -n -s 50 -l 100 -d
- -nオプションでヘッドレスをキャンセルして画面表示
- -sオプションで画像を選択する数を50枚に指定
- -lオプションで50枚削除を100回やったらブラウザをいったん閉じる
- -dオプションでデーモン化して永続実行
ログとしてこのような出力になります
count:001 84s 2022/11/23 21:36:58 target=2015年1月2日(金) log=50 枚選択しています
count:002 47s 2022/11/23 21:37:46 target=2014年12月19日(金) log=50 枚選択しています
count:003 47s 2022/11/23 21:38:33 target=2014年12月15日(月) log=50 枚選択しています
count:004 47s 2022/11/23 21:39:21 target=2014年12月11日(木) log=50 枚選択しています
count:005 47s 2022/11/23 21:40:09 target=2014年12月11日(木) log=50 枚選択しています
count:006 47s 2022/11/23 21:40:56 target=2014年11月30日(日) log=50 枚選択しています
count:007 47s 2022/11/23 21:41:44 target=2014年11月29日(土) log=50 枚選択しています
count:008 47s 2022/11/23 21:42:32 target=2014年11月26日(水) log=50 枚選択しています
count:009 47s 2022/11/23 21:43:19 target=2014年11月24日(月) log=50 枚選択しています
count:010 47s 2022/11/23 21:44:07 target=2014年11月23日(日) log=50 枚選択しています
count:011 47s 2022/11/23 21:44:55 target=2014年11月23日(日) log=50 枚選択しています
count:012 47s 2022/11/23 21:45:42 target=2014年11月22日(土) log=50 枚選択しています
count:013 47s 2022/11/23 21:46:30 target=2014年11月20日(木) log=50 枚選択しています
初回こそ50枚削除に84秒かかっていますが、それ以降は50枚削除に47秒になっています。コードの処理上画像1枚選択するのにwaitをいれて1秒程度かけているのでこれぐらいが今の安全圏速度になります。
target=が削除対象の日付、log=がGoogleフォトの左上にでてくるメッセージを取得して出力している部分になります。
このツールを使って数日でここまで削減できました (129GB=>12GB)
Puppeteerを使ってGoogleフォトを削減するツールの作り方
実際の使い方はGitHubに詳細はかいてありますのでここからはどうやって作ったのか?、工夫した点はなにか?を紹介していきたいと思います。
Chrome DevTools Recorderを使って操作を記録してコードテンプレを出力
ChromeにはRecorderという画面の操作を記録して、その操作を繰り返し実行したりPuppeteerコードに出力できるツールが内包されていますので、それを使って記録ボタンをおしてGoogleフォトで画像を選択して削除する操作を何枚かやった後にPuppeteerコードとして出力します。
出力されるコード
const puppeteer = require('puppeteer'); // v13.0.0 or later
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const timeout = 5000;
page.setDefaultTimeout(timeout);
{
const targetPage = page;
await targetPage.setViewport({"width":1151,"height":843})
}
{
const targetPage = page;
const promises = [];
promises.push(targetPage.waitForNavigation());
await targetPage.goto("https://photos.google.com/u/1/");
await Promise.all(promises);
}
{
const targetPage = page;
await targetPage.keyboard.down("x");
}
{
const targetPage = page;
await targetPage.keyboard.up("x");
~省略
Googleログイン状態を再現する
Recorderで作られたコードはログイン状態は再現できません。Googleログイン画面にいってログインするコードをPuppeteerで書いてもいいですが10年前ならできたかもしれませんが2段階認証や2要素認証がある今の時代では現実的ではありません。そのためクッキーを読み込ませてログイン状態を再現するコードを実装します。
const cookies = JSON.parse(fs.readFileSync(cookieFilePath, 'utf-8'));
await page.setCookie(...cookies);
cookieに関してはChrome拡張であるEditThisCookieを使ってファイルにコピーします。
なるべくCSSセレクタとの戦いを避ける
今回一番工夫したというか良かったと個人的に思っている点としては画像の選択と削除に関してはCSSセレクタの処理を使わずショートカットキーのキーボード操作で行った点です。
画像削除を簡単にさせてくれないGoogleフォト君ですがこの点は良心的でした。
上記を参考に、「画像を選択 Xキー」「画像を削除 #キー」「画像を削除承認 Enterキー」を押す処理を実装します(実際にはRecorderがキー操作は出力してくれます)
for (let i = 0; i < _deleteSelectFileNum; i++) {
await page.keyboard.down("ArrowRight");
await new Promise((r) => setTimeout(r, 200))
await page.keyboard.up("ArrowRight");
await new Promise((r) => setTimeout(r, 200))
await page.keyboard.down("x");
await new Promise((r) => setTimeout(r, 200))
await page.keyboard.up("x");
await new Promise((r) => setTimeout(r, 200))
}
〜略
try {
await page.keyboard.down("#");
await new Promise((r) => setTimeout(r, 200))
await page.keyboard.up("#");
// 削除しますか?ダイアログを待つ
await waitForSelector('[id^="dwrFZd"]', page);//dwrFZd0 -> dwrFZd1
successCount++;
} catch {
console.log("Fail: delete");
continue;
}
Puppeteerを使って実装するときに一番大変なのはCSSセレクタとの格闘だと思います。
GoogleフォトにいたってはdwrFZdとか意味がわからない文字列ですし、書いたコードが意図通りにならない、対象のセレクタが出てこないときがありイライラして液晶をパンチするので心理的にも良くない、いつ仕様変更されてもおかしくないのでなるべくCSSセレクターとの戦闘が避けられるのであればそれにこしたことはありません。今回一番の収穫はここでした。
画像が表示されない珍現象に対応する
削除を続けていくとこのような画面になり画像も選択できずプログラムがそこで止まってしまいました。
しばらく(とはいっても半日〜1日)たつと治るのですがどうも特定の日付の画像がサムネイル出力できないことがあるようです。(Yahoo知恵袋など同様の報告は多数あり:https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q11229657066)
なにが原因かは不明ですが、こういう事象があることはわかりましたのでその場合はその日付をSKIPして適当な日付から削除する処理を実装しました。
try {
//画像がロードされるまで待つ、画像の日付文字列を待機する
const dateElm = await waitForSelectors(['.xA0gfb'], page);
targetDate = await (await dateElm.getProperty('textContent')).jsonValue();
}
catch (err) {
console.error(err);
try {
// これだとうまくいかない
/*page.evaluate(_ => {
window[1].scrollBy(0, window.innerHeight);
});*/
const timeout = 100
const element = await waitForSelectors([["#yDmH0d"]], page, { timeout, visible: true });
await scrollIntoViewIfNeeded(element, timeout);
await element.click({
offset: {
x: width - 10,
y: height / 2,
},
});
}
なにがあっても絶対削除するマンを実装する
選択画像が多いとたまに意図通りに動かない場合があるのでその場合は数を減らして実行する処理を追加しました
//選択した画像が指定した枚数に大幅に満たない場合、キー操作が巡回していると判断して枚数を減らす
//11以上だったら10に、そうでなく6以上だったら5に
{
const regex = /[^0-9]/g;
const numLogVal = parseInt(selectNumlog.replace(regex, ""), 10);
let old = _deleteSelectFileNum
const diff = _deleteSelectFileNum - numLogVal
if (_deleteSelectFileNum > 10 && diff >= 10) {
_deleteSelectFileNum = _deleteSelectFileNum - 10;
const fixLog = format("Fix _deleteSelectFileNum %d -> %d numLogVal=%d diff=%d", old, _deleteSelectFileNum, numLogVal, diff)
console.log(fixLog)
}
else if (_deleteSelectFileNum <= 10 && _deleteSelectFileNum > 5 && diff >= 5) {
_deleteSelectFileNum = 5;
const fixLog = format("Fix _deleteSelectFileNum %d -> %d numLogVal=%d diff=%d", old, _deleteSelectFileNum, numLogVal, diff)
console.log(fixLog)
}
}
Googleフォトという謎の怪物をPuppeteerを使って倒す(削除する)のは楽しい
毎日Googleフォトの謎現象と格闘しながらPuppeteerのコードを改良していくと、いつのまにかGoogleフォトの容量バーがRPGのボスのHPバーに見えてきました。コードを作りながら結果が見えるゲームということに見方を変えると面白いコーディングゲームだと思いました。しかも削減できるとサブスクの料金も削減できるという実益もかねたコードゲーム。私はそろそろゲームクリア(0GB)が見えてきましたので、もし同じくGoogleフォトのストレージ問題に挑む必要が出てきたエンジニアの方は本記事を参考により効率的に削除する方法を考案してください。RTA?
Discussion