Google Apps Scriptでgmailのインライン画像を毎日自動保存する
どうも、久しぶりの投稿になります。乃木オタエンジニアです。今回は初めてGoogle Apps Script(GAS)の記事を書きます。やりたいことはタイトルの通りです。
モチベーション
乃木坂46にはモバイルメールというコンテンツで、メンバーが毎日ファン向けのメールを配信しているサービスがあります。僕は4期生の早川聖来ちゃんのモバメを取っているんですが、めちゃくちゃに可愛い自撮りを送ってくれるんですねこれが。これらの可愛い画像を保存しておきたいと思うのは乃木オタとして自然な摂理というわけです。
しかし、メールから画像を保存しようとすると、PCのダウンロードフォルダに全てunnamed.jpgという名前で保存されます。別の画像を保存しようとするとunnamed(1).jpgとかになってしまう始末。1枚ずつダウンロードするのも大変だし、その上名前を変更しなきゃいつのメールの画像だか分からない。なんなら毎日勝手に保存して欲しい。こういう時はエンジニアとして自分でプログラム組んでしまいましょうよというわけです。
恐らく、乃木オタの方はモバメの画像を保存して管理している、管理したいという方が多くいると思います。そんな方にこの記事が目に留まると嬉しいです。
概要
この記事を見た普通の乃木オタの人も使えるといいなと思うので、GASの使い方から書こうと思います。
やりたいタスクを一般的な形でいうと、「特定のメールアドレスからgmailに送られてくるメール内の、インライン表示の画像を定期的にGoogle drive保存する」ということになります。
- GASの使い方、下準備
- 実際に画像を保存する関数
- 定期実行する方法
- 全体のソースコード
というような構成で書いていきます。
GASの使い方、下準備
まずgmailに目的のメールが送られてくることが必須条件です。gmail以外の場合はgmailに自動転送するなりしてください。
まず、画像の保存先のフォルダをGoogle drive内に作成します。自分の場合は「せーらめーる」というフォルダです。次に使用するgmailのドライブにスプレッドシートを作成します。
スプレッドシート自体には何も記入はしません。GASを使用するためにスクリプトエディタを開きます。作成したスプレッドシートを開き、ツール内の「スクリプトエディタ」を選択します。
すると次のような画面が開くのでここにプログラムを書いていきます。
画像を保存する関数
var query = 'from:seira_hayakawa@nogizaka46.com';
const folderName='せーらめーる'
function pictureGetter() {
const threads = GmailApp.search(query)
const messages = GmailApp.getMessagesForThreads(threads);
for(const thread of messages){
for(const message of thread){
// メールの取得日時をファイル名に使う
const date = message.getDate();
var strDate = Utilities.formatDate(date, "Asia/Tokyo", "yyyyMMdd_HHmmss");
// メール本文をhtml形式で取得し、その中から画像のurlを抽出
const body = message.getBody();
var ary = body.split("src=\"");
for(var i=1;i<ary.length;i++){
Utilities.sleep(2000);
const url = ary[i].split("\"")[0]
var fileName=strDate+"_"+i+".jpg"
// ドライブ内の指定フォルダに既に同じ画像がないか検索
var folders = DriveApp.getFoldersByName(folderName);
while(folders.hasNext()) {
var folder = folders.next();
if(folder.getName() == folderName){
break;
}
}
files = folder.getFiles();
list = []
while(files.hasNext()) {
var buff = files.next();
list.push(buff.getName());
}
// urlを開き、画像を保存、画像ファイルを指定フォルダにコピー
if(list.indexOf(fileName)<0){
var response = UrlFetchApp.fetch(url);
var fileBlob = response.getBlob().setName(fileName);
var file = DriveApp.createFile(fileBlob);
file.makeCopy(file.getName(), folder);
file.setTrashed(true);
}
}
}
}
}
まず、最初のqueryでメールアドレスを指定します(コード内のアドレスはダミーです)。このメールアドレスに自分が画像を保存したいアドレスを入れます。ここではgmailの検索文字列を記入しているので、after:やbefore:を追加で記載すれば日時指定もできます。folderNameで保存するフォルダ名を指定します。
保存のために実行する関数がpictureGetterです。少しずつ何をしているのか説明します。
メールの取得
const threads = GmailApp.search(query)
const messages = GmailApp.getMessagesForThreads(threads);
for(const thread of messages){
for(const message of thread){
...
}
}
GmailApp.searchで検索文字列でヒットするスレッドを取得し、GmailApp.getMessagesForThreadsでそのスレッドに含まれるメールを取得します。これにfor文を2重で使って1つ1つのメールを取り出して処理をしていきます。
画像の名前に使うためメールの受信日時を取得
const date = message.getDate();
var strDate = Utilities.formatDate(date, "Asia/Tokyo", "yyyyMMdd_HHmmss");
メールの本文を取り出し、画像のurlを抽出
const body = message.getBody();
var ary = body.split("src=\"");
for(var i=1;i<ary.length;i++){
const url = ary[i].split("\"")[0]
...
}
本文はhtml形式で返ってきます。本当はスクレイピング好きとしてはちゃんとhtml解析したいですが、今回は画像urlさえ分かればいいので、src="でhtmlを区切り、その区切られたものを"で区切ることでurlだけ抽出しています。
google driveのフォルダと、その中のファイル名を取得
var folders = DriveApp.getFoldersByName(folderName);
while(folders.hasNext()) {
var folder = folders.next();
if(folder.getName() == folderName){
break;
}
}
files = folder.getFiles();
list = []
while(files.hasNext()) {
var buff = files.next();
list.push(buff.getName());
}
ドライブ内の指定した名前のフォルダを探し、そのフォルダ内のファイル名を配列に入れています。これは既にダウンロードした画像を2重でダウンロードすることを防ぐために取得しています。
画像の保存
if(list.indexOf(fileName)<0){
var response = UrlFetchApp.fetch(url);
var fileBlob = response.getBlob().setName(fileName);
var file = DriveApp.createFile(fileBlob);
file.makeCopy(file.getName(), folder);
file.setTrashed(true);
}
画像urlを開いて保存し、フォルダーにコピーしています。
このpictireGetterでは日時指定していないので、指定したアドレスからのメール全てをチェックして画像があれば保存します。ただ、GASの仕様で1つの関数を6分間以上は実行できないので、量が多い場合は日時指定で区切る必要があります。
定期実行する方法
ここからは、pictureGetterを定期実行する方法を説明します。今回の定期実行は、「毎日0時5分に、前日に届いたメールをチェックして、自動で画像を保存する」ということを目指します。「前日に届いた」という部分を実現するため、次のpictureGetterEverydayを作ります。実行時の日付を取得し、その前日以降の日付をメールの検索文字列に追加して、pictureGetterを実行します。
function pictureGetterEveryday(){
const time = new Date();
time.setDate(time.getDate()-1);
query = query + " after:" +Utilities.formatDate(time, "Asia/Tokyo", "yyyy/MM/dd");
Logger.log(query);
pictureGetter();
}
トリガーという機能を使うのですが、毎日実行は正確な時間を指定することができないようです。そのため、setTriggerという関数を使ってプログラムで正確な日時のトリガーを仕掛けます。setTrigger自体は毎日23時~0時の間にトリガーを設定して、翌0時5分にpictureGetterEverydayが実行されるようにトリガーを設定します。
function setTrigger(){
delTrigger()
const time = new Date();
time.setDate(time.getDate() + 1);
time.setHours(0);
time.setMinutes(5);
Logger.log(time);
ScriptApp.newTrigger('pictureGetterEveryday').timeBased().at(time).create();
}
ここで、delTriggerという関数が含まれていますが、これはsetTriggerでセットしたトリガーが貯まってしまわないように削除するものです。
function delTrigger() {
const triggers = ScriptApp.getProjectTriggers();
for(const trigger of triggers){
if(trigger.getHandlerFunction() == "pictureGetterEveryday"){
ScriptApp.deleteTrigger(trigger);
}
}
}
では、SetTriggerのトリガー設定の方法を説明して最後になります。
スクリプトエディタの下記の画像の赤枠のストップウォッチのようなマークをクリックします。
すると、トリガー設定画面が開くので、トリガーを追加をクリックし、以下のように設定して保存します。
これで毎日11時台にsetTriggerがpictureGetterEverydayを0時5分に実行するトリガーを設定して目的を達成できます。
実際に仕掛けておくとこんな感じで画像が貯まっていきます。(モバメのアップはNGだけどめっちゃ小さいサムネなので許してください)
全体ソースコード
var query = 'from:nogizaka46-seira_hayakawa@m.nogizaka46.com';
const folderName='せーらめーる'
function pictureGetter() {
const threads = GmailApp.search(query)
const messages = GmailApp.getMessagesForThreads(threads);
for(const thread of messages){
for(const message of thread){
// メールの取得日時をファイル名に使う
const date = message.getDate();
var strDate = Utilities.formatDate(date, "Asia/Tokyo", "yyyyMMdd_HHmmss");
const body = message.getBody();
// メール本文をhtml形式で取得し、その中から画像のurlを抽出
var ary = body.split("src=\"");
for(var i=1;i<ary.length;i++){
Utilities.sleep(2000);
const url = ary[i].split("\"")[0]
var fileName=strDate+"_"+i+".jpg"
// ドライブ内の指定フォルダに既に同じ画像がないか検索
var folders = DriveApp.getFoldersByName(folderName);
while(folders.hasNext()) {
var folder = folders.next();
if(folder.getName() == folderName){
break;
}
}
files = folder.getFiles();
list = []
while(files.hasNext()) {
var buff = files.next();
list.push(buff.getName());
}
// urlを開き、画像を保存、画像ファイルを指定フォルダにコピー
if(list.indexOf(fileName)<0){
var response = UrlFetchApp.fetch(url);
var fileBlob = response.getBlob().setName(fileName);
var file = DriveApp.createFile(fileBlob);
file.makeCopy(file.getName(), folder);
file.setTrashed(true);
}
}
}
}
}
function pictureGetterEveryday(){
const time = new Date();
time.setDate(time.getDate()-1);
query = query + " after:" +Utilities.formatDate(time, "Asia/Tokyo", "yyyy/MM/dd");
Logger.log(query);
pictureGetter();
}
function setTrigger(){
delTrigger()
const time = new Date();
time.setDate(time.getDate() + 1);
time.setHours(0);
time.setMinutes(5);
Logger.log(time);
ScriptApp.newTrigger('pictureGetterEveryday').timeBased().at(time).create();
}
function delTrigger() {
const triggers = ScriptApp.getProjectTriggers();
for(const trigger of triggers){
if(trigger.getHandlerFunction() == "pictureGetterEveryday"){
ScriptApp.deleteTrigger(trigger);
}
}
}
##最後に
このスクリプト書いて実際に聖来ちゃんの可愛い画像がドライブに貯まるのが幸せでした笑。こういうことやるなら他にも4期生のモバメ取っておけばよかったと若干後悔もしています笑。皆さんも良ければ推しの画像を集めてみてください。乃木オタじゃなくても、gmailの画像を保存する必要性がある方は使ってみてください。twitter等での絡みも歓迎致します。
##参考文献
参考にした記事です。ほとんどコードパクってる部分もあります。非常に参考になりました。
https://qiita.com/akiko-pusu/items/43c89dcfeb1d544cce38
https://a-zumi.net/google-apps-script-image-download-save-google-drive/
https://tonari-it.com/gas-trigger-set/
https://tonari-it.com/gas-gmail-attachment-drive/