
『プログラミング』noteの抽出ツールを作ってみたいのじゃ(3)
以下記事の続き!
コードの詳細からですね。
前回の記事で記載し忘れていましたが、参考にしたサイトはこちらです。参考サイトのコードをサンプルとして、書き換えたものがこの記事のコードとなります。
先に参考サイトを見たほうがイメージが付きやすいと思います。
🌸完成コード
改めて、完成コードは以下です。
function sukiFunction() {
//アクティブなスプレッドシートを取得
ss = SpreadsheetApp.getActiveSpreadsheet();
//シート名「値を指定」を選択
sheet = ss.getSheetByName('値を指定');
//シート名「値を指定」のA2にある値を取得(記事ID)
id = sheet.getRange('A2').getValue();
//シート名「値を指定」のB2にある値を取得(記事title)
title = sheet.getRange('B2').getValue();
//シート名「test」をアクティブにする
sheet_test = ss.getSheetByName("test");
sheet_test.activate();
Logger.log('id:' + id);
Logger.log('title:' + title);
// 検索条件に該当するスレッド一覧を取得
var threads = GmailApp.search('subject:スキのおしらせ -label:処理済み ' + title);
// スレッドを一つずつ取り出す
threads.forEach(function(thread) {
// スレッド内のメール一覧を取得
var messages = thread.getMessages();
// メールを一つずつ取り出す
messages.forEach(function(message) {
// メール本文を取得
var plainBody = message.getPlainBody();
// メール件名を取得
subject = message.getSubject();
var from = message.getFrom();
//送信元のアドレスが「noreply@note.mu」のみを対象とする。
if(!from.match("noreply@note.mu")){
return;
}
//指定した記事IDが含まれているものだけを対象
if(!plainBody.match(id)){
return;
}
//コメントへのスキは除外
if(subject.match("コメントへのスキのおしらせ")){
return;
}
//メール本文を改行ごとに区切る
var arr_n = plainBody.split('\n');
////////スキしたユーザーのURLを取り出し////////
//?nt=が始まる文字位置を検索
var str_index = arr_n[3].indexOf("?nt=");
//userのurlを取り出し。
var user_url = arr_n[3].slice(1,str_index);
////////スキしたユーザーの名前を取り出し////////
//<httpsが始まる文字位置を検索
var str_index = arr_n[5].indexOf("<https");
//userネームを取り出し。
var user_name = arr_n[5].slice(0,str_index-1);
////////スプレッドシートに書き出し////////
// 最終行を取得
var lastRow = sheet_test.getLastRow() + 1;
// セルを取得して値を転記
sheet_test.getRange(lastRow, 1).setValue(user_name);
sheet_test.getRange(lastRow, 2).setValue(user_url);
});
// スレッドに処理済みラベルを付ける
var label = GmailApp.getUserLabelByName('処理済み');
thread.addLabel(label);
});
// 最終行を取得
var lastRow = sheet_test.getLastRow();
//重複削除用に範囲を指定
var range = ss.getRange("A:B");
//重複行を削除する
range.removeDuplicates([2]);
}
コードを上から順番に解説していきます!
-------コード詳細-------
🌸スプレッドシート操作
//アクティブなスプレッドシートを取得
ss = SpreadsheetApp.getActiveSpreadsheet();
//シート名「値を指定」を選択
sheet = ss.getSheetByName('値を指定');
//シート名「値を指定」のA2にある値を取得(記事ID)
id = sheet.getRange('A2').getValue();
//シート名「値を指定」のB2にある値を取得(記事title)
title = sheet.getRange('B2').getValue();
スプレッドシートに記載した「記事ID」と「記事タイトル」を取得する処理です。
■セルの値取得までの流れ
どのスプレッドシートの
(2行目のコード)
⇩
どのシート名の
(4行目のコード)
⇩
どのセルに(getRange)
何をするか(getValue)
(6行目のコード)
(8行目のコード)
これでセルの値を取得できます。この時点ではシート名「値を取得」を開いている状態なので、書き込み用のシート「test」を開いておきます。それが以下コードになります。
//シート名「test」をアクティブにする
sheet_test = ss.getSheetByName("test");
sheet_test.activate();
このコードは必要ないかもだけど、操作するシートは念の為アクティブにしておいたほうがいいかなって思う。
🌸実行画面に表示
Logger.log('id:' + id);
Logger.log('title:' + title);
Logger.log で スクリプトエディタの実行画面に値を表示することができます。
■上記コードは以下のように表示される
id:シート「値を指定」セルA2で指定した値
title:シート「値を指定」セルB2で指定した値
実際の機能に影響はないため、このコードはなくてもいいのですが、開発中に値を確認しながら行いたい場合があるので書いています。
🌸メールの検索条件
■今回のメール取得条件
1 タイトルが「スキのおしらせ」
2 処理済みではない
3 メール本文に
指定したタイトルを含んでいる
コードは以下になります。
// 検索条件に該当するスレッド一覧を取得
var threads = GmailApp.search('subject:スキのおしらせ -label:処理済み ' + title);
Gmailの検索画面に打ち込んでいるイメージ。
■問題発覚!
記事のタイトルが
「タイトル(1)」「タイトル(2)」のように、カッコ内の数字違いで作成したものがあると、検索がうまくいかない。
▼本来してほしい動き
「スキのおしらせ タイトル(1)」で検索
⇩
タイトル(1)の通知メールが表示される
▼現状なっている動き
「スキのおしらせ タイトル(1)」で検索
⇩
タイトル(1)とタイトル(2)の
通知メールが表示される
▼起こる問題
タイトル(1)の処理を行っただけなのに、タイトル(2)も「処理済み」にされてしまう。
まぁこういう問題があるよ~ということがわかっていれば、何かしら対応はできると思います。解説続けます(笑)。
🌸メール本文取得
先ほどのコードで検索条件を指定しています
// 検索条件に該当するスレッド一覧を取得
var threads = GmailApp.search('subject:スキのおしらせ -label:処理済み ' + title);
検索結果は 「threads」 に入っています。
「スレッド」と「メール」については、最初に紹介したこちらの「スレッドからメール本文を取り出す」がわかりやすいです。
// スレッドを一つずつ取り出す
threads.forEach(function(thread) {
// スレッド内のメール一覧を取得
var messages = thread.getMessages();
// メールを一つずつ取り出す
messages.forEach(function(message) {
// メール本文を取得
var plainBody = message.getPlainBody();
このコードが本文を取り出すコードです。上記サイトを見ると、本文を取り出すまでのイメージがわかると思います。
🌸メール本文から抽出
ここがおもしろいところで、
個性が出るところだと思います。
まず、「スキのおしらせ」をGmailで確認すると……
このようになっております。
これをプログラムで取得してみましょう。
メール本文を取得するプログラムだけ整理すると……
▼本文のみ取得プログラム
function sukiFunction() {
// 検索条件に該当するスレッド一覧を取得
var threads = GmailApp.search('subject:スキのおしらせ -label:処理済み ');
// スレッドを一つずつ取り出す
threads.forEach(function(thread) {
// スレッド内のメール一覧を取得
var messages = thread.getMessages();
// メールを一つずつ取り出す
messages.forEach(function(message) {
// メール本文を取得
var plainBody = message.getPlainBody();
// メール本文が取得できているかログに出力して確認
Logger.log(plainBody);
});
});
}
このようになります。
メール本文を取得して、スクリプトエディタに結果を表示するプログラムです。これを実行してどのように本文が取得されるかを確認すると……
こんな感じ。今回欲しい情報は
■今回欲しい情報
・ユーザーページのURL
・ユーザ名
なので、先程取得した本文からどう取り出すかを考えます。まず、「どの場所を取り出すか」を決めます。私は以下画像の赤枠のところからにしました。
1 : ユーザーページのURL
2 : ユーザー名
ここを取り出したいと思います。
✨改行ごとに区切る
メール本文を1行ずつ配列に入れていきます。
//メール本文を改行ごとに区切る
var arr_n = plainBody.split('\n');
これをループ内で行うと以下イメージになります。
抽出したいものはそれぞれ以下にあるとわかります。
■ユーザーページのURL
arr_n[3]
■ユーザー名
arr_n[5]
✨欲しい情報だけに切り取る
現状 arr_n[3] と arr_n[5] の中身は
■arr_n[3]
<https://note.com/co2co2cocchan?nt=like_3833710>
■arr_n[5]
ぽんこっちゃん <https://note.com/co2co2cocchan?nt=like_3833710>さんがスキしました。
になっています。
以下のようにしたいですね。
■arr_n[3]
<>がいらない
?nt=like_3833710がいらない
■arr_n[5]
「ぽんこっちゃん」以外いらない
切り出しは「slice」を使います。
■slice使い方
切り出したい文字.slice(どこから,どこまで);
■例
var text = "りんごとバナナ";
var str = text.slice(0,2);
とすると、「str」に「りんご」が入ります。
var str = text.slice(4,6); だと
「バナナ」が入ります。
■注意
1文字目は0番目から始まります。
0番目 : 1文字目
1番目 : 2文字目
2番め : 3文字目
これを使っていきましょう。
まずユーザーページURLを切り出し。
■arr_n[3]
<https://note.com/co2co2cocchan?nt=like_3833710>
■sliceの指定
どこから : hから(2文字目)
どこまで : ?ntの前まで
■なぜ「?ntの前まで」?
noteのユーザーページは
https://note.com/ID で表されるから。
「?ntの前まで」は以下の .indexOf で求めます。指定した文字が最初にヒットした文字位置を返してくれます。これで切り出しのコードは以下のようになります。
////////スキしたユーザーのURLを取り出し////////
//?nt=が始まる文字位置を検索
var str_index = arr_n[3].indexOf("?nt=");
//userのurlを取り出し。
var user_url = arr_n[3].slice(1,str_index);
ユーザー名も同様に
////////スキしたユーザーの名前を取り出し////////
//<httpsが始まる文字位置を検索
var str_index = arr_n[5].indexOf("<https");
//userネームを取り出し。
var user_name = arr_n[5].slice(0,str_index-1);
str_index-1 となっているのは、「ぽんこっちゃん」の後に空白が入っているため、その分をマイナスしています。
だったら indexOf("<https") じゃなく
indexOfで空白を指定でもいい気がしてきた。
まぁこれで目的の文字は取り出せました。
🌸除外処理
// メール件名を取得
subject = message.getSubject();
var from = message.getFrom();
//送信元のアドレスが「noreply@note.mu」のみを対象とする。
if(!from.match("noreply@note.mu")){
return;
}
//指定した記事IDが含まれているものだけを対象
if(!plainBody.match(id)){
return;
}
//コメントへのスキは除外
if(subject.match("コメントへのスキのおしらせ")){
return;
}
■getSubject
件名を取得しています。
■getFrom
送信者を取得しています。
■なぜ送信元のアドレス判定
以下画像のように、スレッドの中に複数のメールが存在する場合があります。転送先の設定を行っていたり、返信等を行ったりした場合になります。なので、noteからの「noreply@note.mu」のみを対象としています。
//送信元のアドレスが「noreply@note.mu」のみを対象とする。
if(!from.match("noreply@note.mu")){
return;
}
■コード説明
・!from.match
noreply@note.muじゃなかったら
・return
このコード以下は処理しない
//指定した記事IDが含まれているものだけを対象
if(!plainBody.match(id)){
return;
}
■コード説明
・!plainBody.match
記事IDが本文に含まれていなかったら
・return
このコード以下は処理しない
//コメントへのスキは除外
if(subject.match("コメントへのスキのおしらせ")){
return;
}
■コード説明
・subject.match
メールタイトルが「コメントへのスキのおしらせ」”””だったら”””
・return
このコード以下は処理しない
■注意
subject.match の前には 「!」がついていません。「!」は否定を表しています。
OO.match : 一致
!OO.match : 不一致
■なぜこのコードが必要
「スキのおしらせ」で検索すると「コメントへのスキのおしらせ」がひっかかってしまうからです。
次はようやく出力処理です。
🌸出力処理
出力のコードは以下になります。
////////スプレッドシートに書き出し////////
// 最終行を取得
var lastRow = sheet_test.getLastRow() + 1;
// セルを取得して値を転記
sheet_test.getRange(lastRow, 1).setValue(user_name);
sheet_test.getRange(lastRow, 2).setValue(user_url);
■最終行を取得
シート名「test」の最終行を取得し
それにプラス1しています。
最終行の1個下にどんどん書き込みたいから
■セルを取得して値を転記
シート名「test」の
⇩
書き込み行 A列 に
⇩
user_nameを書き込む
ーーーーーーーーーー
シート名「test」の
⇩
書き込み行 B列 に
⇩
user_urlを書き込む
こんな感じになる。
🌸処理済み
// スレッドに処理済みラベルを付ける
var label = GmailApp.getUserLabelByName('処理済み');
thread.addLabel(label);
処理を行ったスレッドにラベルを追加しています。「処理済み」ラベルは事前にGmailで準備しておく必要があります。前回の記事ではそこの説明が抜けていたため、前回の記事に「ラベル準備」を追加いたしました。
あの状態で実行したらエラーにならないんだろうか。誰も記事を真似して実行してなかったということかな(笑)。
■処理済みが必要な理由
第一回で説明したとおり、もともとこのツールは「一定時間毎」に稼働して「追加更新」を行う予定だったからです。
なので、一度処理したものは次回は処理しないようにしなくてはいけない。
🌸重複削除
// 最終行を取得
var lastRow = sheet_test.getLastRow();
//重複削除用に範囲を指定
var range = ss.getRange("A:B");
//重複行を削除する
range.removeDuplicates([2]);
すべての書き出しが終わったあとに実行されます。B列の「user_url」に重複があった場合、その行を削除します。
通知が重なって来てしまった場合に同じユーザーが出力されるからです。
🌸最後に
ちょっと微妙かなというのが正直なところです。力技感があるかな~って。APIを使った方法のほうがもっと美しくできると思います。
なのでAPIについてもうちょっと調べてみます。