GASでGmailの売上情報をスクレイピング1-農家のGAS(3)

前回までに、どのようにしてGmailの売上情報をスクレイピングするかの方向性は決まりました。

今回は実際にプログラミングしていきます。

なお、「GASの始め方」みたいな部分は、インターネットにいくらでも情報があるので、省略です。

GmailをGASでスクレイピング。実例

まず、届くメールはこんな感じです。
(本来のメールを、ブログ用に修正してますが)

これから必要な情報をスクレイピングする為に、以下のようにプログラミングしました。

  1. function myFunction() {
  2.   // 検索条件に該当するスレッド一覧を取得
  3.   var threads = GmailApp.search('subject:ストア売上情報 -時 -label:処理済み');
  4.   // 12時・17時・22時の売上情報メールと、処理済みラベル付きは除外
  5.   // ↓スレッドを一つずつ取り出す
  6.   threads.forEach(function(thread) { //※1
  7.     // ↓スレッド内のメール一覧を取得
  8.     var messages = thread.getMessages();
  9.     // ↓メールを一つずつ取り出す
  10.     messages.forEach(function(message) { //※2
  11.       var body = message.getPlainBody(); // メール本文を取得
  12.       var sub = message.getSubject(); // 件名を取得
  13.       var sub = sub.toString().replace("ストア売上情報",""); //件名から日付だけを残す
  14.       // ↓書き込むシートを取得
  15.       var sheet = SpreadsheetApp.getActive().getSheetByName('在庫管理');
  16.       // ↓確認の為、メール本文のみ表示
  17.       console.log(body)
  18.       // ↓売上メール上での記載順は、売れた順番関係なしに"店舗A"・"店舗B"・"店舗C"・"店舗D"の為、配列もその順番
  19.       var tenpomei = ["店舗A", "店舗B", "店舗C", "店舗D"]
  20.       var tenpo = {}; //tenpomei配列の各店舗が、メール本文の何文字目か。存在しない場合は-1
  21.       var zentenposuu = tenpomei.length; //店舗名配列内に、何店舗入っているかの数
  22.       var tenposuu=0; //今回メール内に、何店舗入っているかの数
  23.       var kakuuriage = {}; //各店舗の売上情報
  24.       //↓ ここから各店舗売上情報文書内
  25.       var yasaimei = ["野菜1", "野菜2","野菜3", "その他"];
  26.       var yasai = {}; //yasaimei配列の各野菜が、売上情報の何文字目か。存在しない場合は-1(その他は例外)
  27.       var zenyasaisuu = yasaimei.length; //野菜名配列内に、いくつの野菜が入っているかの数
  28.       var yasaisuu=0; //今回売上情報内に、何個の野菜名が入っているかの数
  29.       var kakuyasai = {}; //各野菜の情報
  30.       var kingakuiti = {}; //kakuyasai配列内で、"金額"が何文字目か。
  31.       var suuryou = {}; //kakuyasai配列内で、実際の数量
  32.       var kingaku = {}; //kakuyasai配列内で、実際の金額
  33.       var syouhin = 0;
  34.       var uriage = {}; //売上情報を最後にまとめた配列
  35.       //店舗の数だけ処理を行う
  36.       for(let i=0; i<zentenposuu; i++){ //※3
  37.         tenpo[i] = body.indexOf(tenpomei[i]); //tenpomei配列の各店舗が、メール本文の何文字目か。存在しない場合は-1
  38.         if(tenpo[i] !== -1){
  39.           tenposuu = tenposuu+1} //今回メール内に、何店舗の売上情報があるかカウントアップ
  40.       } //※3
  41.       for(let i=zentenposuu-1; i>=0; i--){ //*4 //※3forで、全店舗の存在の有無を確認した後に処理。
  42.         if(tenpo[i] !== -1){
  43.           kakuuriage[i] = body.slice(tenpo[i]) //抽出した本文から、店舗ごとの売上情報を抽出する
  44.           body = body.substring(0,tenpo[i]) //本文から抽出した分を文末から順番に削除する。これをやる為に、この※4forはカウントダウンで行っている
  45.         }
  46.       } //*4
  47.       //↑問題点として、売上メール上の記載順が毎回固定ではないorわからないと、正常に作動しない
  48.       //↓配列内の野菜の数だけ処理を行う
  49.       for(let j=zentenposuu-1; j>=0; j--){ //*c jが、野菜名配列内に、いくつの野菜が入っているかの数。総数の最後から、順番に減らしていく
  50.         yasaisuu = 0 //一周処理が終わったので、次のカウントをするために、0にリセット
  51.         for(let i=0; i<zenyasaisuu; i++){ //*a 全野菜数だけ繰り返す
  52.           if(tenpo[j] !== -1){ //*d //店舗◯に売上情報があるなら
  53.             yasai[i] = kakuuriage[j].indexOf(yasaimei[i]); //今回売上情報内に、"野菜◯"が入っているかどうか
  54.             if(yasai[i] !== -1){ //野菜◯が入っているなら
  55.               yasaisuu = yasaisuu+1 //今回売上情報内に入っている野菜数をカウントアップ
  56.             }
  57.           } //*d
  58.         } //*a
  59.         for(let t=yasaisuu-1; t>=0; t--){ //*yc 今回売上情報内に入っている野菜数のカウントが終わったので、総数の最後から減らしていく
  60.           syouhin = kakuuriage[j].lastIndexOf("商品") //"商品"がkakuuriage[j]内の後ろから数えて何文字目を返す
  61.           kakuyasai[t] = kakuuriage[j].substring(syouhin,999) //kakuyasai[t]に、"商品"より後ろの文字を抽出して代入
  62.           kakuuriage[j] = kakuuriage[j].substring(0,syouhin) //kakuuriage[j]から、代入した分の文字を削除("商品"より前の文字を代入)
  63.         } //*yc
  64.         for(let i=0; i<zenyasaisuu; i++){ //*ya 全野菜数分繰り返す
  65.           if(yasai[i] !== -1){ //*yb 各野菜◯が存在するなら
  66.             for(let t=0; t<yasaisuu; t++){ //*ye 存在する野菜◯の数だけ繰り返す
  67.               if(kakuyasai[t].indexOf(yasaimei[i]) !== -1){ //*yd 野菜◯の◯に入る数字がわからないので、配列の最初から順番に確認する。
  68.                 kingakuiti[t] = kakuyasai[t].lastIndexOf("金額") //金額の文字が、後ろから数えて何文字目か抽出
  69.                 kingaku[t] = kakuyasai[t].substring(kingakuiti[t],999) //"金額"より後ろの文字を抽出して代入
  70.                 kakuyasai[t] = kakuyasai[t].substring(0,kingakuiti[t]) //代入した分の文字を削除("金額"より前の文字を代入)
  71.                 kingaku[t] = kingaku[t].replace(/[^0-9]/gi, ''); //kingaku[t]の中から、半角数字のみ抽出する(金額が何桁かわからないので)
  72.                 suuryou[t] = kakuyasai[t].replace(/[^0-9]/gi, ''); //kakuyasai[t]の中から、半角数字のみ抽出する(数量が何桁かわからないので)
  73.                 console.log(tenpomei[j]+":"+yasaimei[i]+"| 数量:"+suuryou[t]+" 金額:"+kingaku[t])
  74.                 var uriage = [sub,tenpomei[j],yasaimei[i],kingaku[t],suuryou[t]] //売上情報を配列に代入
  75.                 var lastRow = sheet.getLastRow() + 1; //スプレッドシートの最終行を取得
  76.                 sheet.appendRow(uriage); //最終行に売上情報を転記
  77.               } //*yd
  78.             } //*ye
  79.           } //*yb
  80.         } //*ya
  81.       } //*c
  82.     }); //※2
  83.     thread.markRead(); //スレッドを既読にする
  84.     // スレッドに処理済みラベルを付ける
  85.     var label = GmailApp.getUserLabelByName('処理済み');
  86.     thread.addLabel(label);
  87.     }); //※1
  88.   }

これを動かすと、スプレッドシートに売上情報が整理されて書き込まれます。

なお、ここまでたどり着くのに、一週間以上かかりました。

途中の失敗作は数知れず(笑)

さて、長くなりますが少しずつ解説していきます。

1.該当するメールを探しだし、本文と件名をそれぞれ抽出する

  1. function myFunction() {
  2.   // 検索条件に該当するスレッド一覧を取得
  3.   var threads = GmailApp.search('subject:ストア売上情報 -時 -label:処理済み');
  4.   // 12時・17時・22時の売上情報メールと、処理済みラベル付きは除外
  5.   // ↓スレッドを一つずつ取り出す
  6.   threads.forEach(function(thread) { //※1
  7.     // ↓スレッド内のメール一覧を取得
  8.     var messages = thread.getMessages();
  9.     // ↓メールを一つずつ取り出す
  10.     messages.forEach(function(message) { //※2
  11.       var body = message.getPlainBody(); // メール本文を取得
  12.       var sub = message.getSubject(); // 件名を取得
  13.       var sub = sub.toString().replace("ストア売上情報",""); //件名から日付だけを残す
  14.       // ↓書き込むシートを取得
  15.       var sheet = SpreadsheetApp.getActive().getSheetByName('在庫管理');
  16.       // ↓確認の為、メール本文のみ表示
  17.       console.log(body)

03.
Gmail全体から、該当するタイトル(subject)が含まれるスレッドを取得する。
「-時」は、12時・17時・22時に中間報告のメールが届くが、それを除外する為。
「-label:処理済み」は、後述するが処理の終わったメールを複数回処理しない為。

06~11.
Gmailの仕様で、届くメールは全てスレッド扱いとなる。
そこで、スレッドから1通ずつ取り出す作業が必要。

12~19.
取り出したメール文章の中から、body:本文 sub:件名 をそれぞれ取得
また、件名から日付だけを取り出している。

18.
作業する際の確認用で本文をconsoleで表示している。最終的には無くても良い。

2.色々な変数を宣言する

  1.       // ↓売上メール上での記載順は、売れた順番関係なしに"店舗A"・"店舗B"・"店舗C"・"店舗D"の為、配列もその順番
  2.       var tenpomei = ["店舗A", "店舗B", "店舗C", "店舗D"]
  3.       var tenpo = {}; //tenpomei配列の各店舗が、メール本文の何文字目か。存在しない場合は-1
  4.       var zentenposuu = tenpomei.length; //店舗名配列内に、何店舗入っているかの数
  5.       var tenposuu=0; //今回メール内に、何店舗入っているかの数
  6.       var kakuuriage = {}; //各店舗の売上情報
  7.       //↓ ここから各店舗売上情報文書内
  8.       var yasaimei = ["野菜1", "野菜2","野菜3", "その他"];
  9.       var yasai = {}; //yasaimei配列の各野菜が、売上情報の何文字目か。存在しない場合は-1(その他は例外)
  10.       var zenyasaisuu = yasaimei.length; //野菜名配列内に、いくつの野菜が入っているかの数
  11.       var yasaisuu=0; //今回売上情報内に、何個の野菜名が入っているかの数
  12.       var kakuyasai = {}; //各野菜の情報
  13.       var kingakuiti = {}; //kakuyasai配列内で、"金額"が何文字目か。
  14.       var suuryou = {}; //kakuyasai配列内で、実際の数量
  15.       var kingaku = {}; //kakuyasai配列内で、実際の金額
  16.       var syouhin = 0;
  17.       var uriage = {}; //売上情報を最後にまとめた配列

21.
出荷した店舗は合計4店舗。
売上情報メールでは、商品が売れた順番は関係無しに、「店舗A」「店舗B」「店舗C」「店舗D」の順番で必ず記載される。それを利用するため、配列tenpomeiではその順番で宣言している。

22~25,28~36.
プログラム内のコメント通り。変数名は、なるべくわかりやすさを重視している。上級者はこんな事しないのかな?

27.
登録している野菜番号。1~4まであるが、普段3までしか使用していないので、解説では3まで。また、この番号を付け忘れて販売すると、「その他」として分類されてメールが届く。

3.売上情報を各店舗ごとに分割する

  1.       //店舗の数だけ処理を行う
  2.       for(let i=0; i<zentenposuu; i++){ //※3
  3.         tenpo[i] = body.indexOf(tenpomei[i]); //tenpomei配列の各店舗が、メール本文の何文字目か。存在しない場合は-1
  4.         if(tenpo[i] !== -1){
  5.           tenposuu = tenposuu+1} //今回メール内に、何店舗の売上情報があるかカウントアップ
  6.       } //※3
  7.       for(let i=zentenposuu-1; i>=0; i--){ //*4 //※3forで、全店舗の存在の有無を確認した後に処理。
  8.         if(tenpo[i] !== -1){
  9.           kakuuriage[i] = body.slice(tenpo[i]) //抽出した本文から、店舗ごとの売上情報を抽出する
  10.           body = body.substring(0,tenpo[i]) //本文から抽出した分を文末から順番に削除する。これをやる為に、この※4forはカウントダウンで行っている
  11.         }
  12.       } //*4
  13.       //↑問題点として、売上メール上の記載順が毎回固定ではないorわからないと、正常に作動しない

売上情報を、店舗ごとに分割する。

39~43.
まず店舗数は全部で4。
39行は※3から※3までを、4回繰り返す指示。
40行で、各店舗の情報が今回のメールに含まれているかをチェック。

45~48.
各店舗の売上情報の有無を確認した後、文章を各店舗ごとに分割する。
body.slice(tenpo[i]) で、本文(body)内の各店舗の◯文字目から文末までを切り取る。
これを文末から繰り返す事で、店舗ごとの情報に分割出来る。

51.
このやり方は、毎回必ず同じ順番で売上情報が届く為に可能。
不確実な場合は別の手段が必要となる。

各店舗売上情報を、各野菜番号ごとに分割する

  1.       //↓配列内の野菜の数だけ処理を行う
  2.       for(let j=zentenposuu-1; j>=0; j--){ //*c jが、野菜名配列内に、いくつの野菜が入っているかの数。総数の最後から、順番に減らしていく
  3.         yasaisuu = 0 //一周処理が終わったので、次のカウントをするために、0にリセット
  4.         for(let i=0; i<zenyasaisuu; i++){ //*a 全野菜数だけ繰り返す
  5.           if(tenpo[j] !== -1){ //*d //店舗◯に売上情報があるなら
  6.             yasai[i] = kakuuriage[j].indexOf(yasaimei[i]); //今回売上情報内に、"野菜◯"が入っているかどうか
  7.             if(yasai[i] !== -1){ //野菜◯が入っているなら
  8.               yasaisuu = yasaisuu+1 //今回売上情報内に入っている野菜数をカウントアップ
  9.             }
  10.           } //*d
  11.         } //*a
  12.         for(let t=yasaisuu-1; t>=0; t--){ //*yc 今回売上情報内に入っている野菜数のカウントが終わったので、総数の最後から減らしていく
  13.           syouhin = kakuuriage[j].lastIndexOf("商品") //"商品"がkakuuriage[j]内の後ろから数えて何文字目を返す
  14.           kakuyasai[t] = kakuuriage[j].substring(syouhin,999) //kakuyasai[t]に、"商品"より後ろの文字を抽出して代入
  15.           kakuuriage[j] = kakuuriage[j].substring(0,syouhin) //kakuuriage[j]から、代入した分の文字を削除("商品"より前の文字を代入)
  16.         } //*yc

54~68.
各店舗ごとに分割した売上情報内を、さらに野菜番号ごとに分割していく。
やっている事は店舗ごとの分割と大差無し。

58~60.
各野菜番号が売上情報としていくつ入っているかカウントする。

64~68.
カウントした分だけ、情報を抜き出していく。
kakuyasai[t]に、野菜番号ごとに分割した売上情報が代入される。

各野菜番号の売上情報を、さらに金額と数量に分割しスプレッドシートに書き込む

  1.         for(let i=0; i<zenyasaisuu; i++){ //*ya 全野菜数分繰り返す
  2.           if(yasai[i] !== -1){ //*yb 各野菜◯が存在するなら
  3.             for(let t=0; t<yasaisuu; t++){ //*ye 存在する野菜◯の数だけ繰り返す
  4.               if(kakuyasai[t].indexOf(yasaimei[i]) !== -1){ //*yd 野菜◯の◯に入る数字がわからないので、配列の最初から順番に確認する。
  5.                 kingakuiti[t] = kakuyasai[t].lastIndexOf("金額") //金額の文字が、後ろから数えて何文字目か抽出
  6.                 kingaku[t] = kakuyasai[t].substring(kingakuiti[t],999) //"金額"より後ろの文字を抽出して代入
  7.                 kakuyasai[t] = kakuyasai[t].substring(0,kingakuiti[t]) //代入した分の文字を削除("金額"より前の文字を代入)
  8.                 kingaku[t] = kingaku[t].replace(/[^0-9]/gi, ''); //kingaku[t]の中から、半角数字のみ抽出する(金額が何桁かわからないので)
  9.                 suuryou[t] = kakuyasai[t].replace(/[^0-9]/gi, ''); //kakuyasai[t]の中から、半角数字のみ抽出する(数量が何桁かわからないので)
  10.                 console.log(tenpomei[j]+":"+yasaimei[i]+"| 数量:"+suuryou[t]+" 金額:"+kingaku[t])
  11.                 var uriage = [sub,tenpomei[j],yasaimei[i],kingaku[t],suuryou[t]] //売上情報を配列に代入
  12.                 var lastRow = sheet.getLastRow() + 1; //スプレッドシートの最終行を取得
  13.                 sheet.appendRow(uriage); //最終行に売上情報を転記
  14.               } //*yd
  15.             } //*ye
  16.           } //*yb
  17.         } //*ya
  18.       } //*c
  19.     }); //※2
  20.     thread.markRead(); //スレッドを既読にする
  21.     // スレッドに処理済みラベルを付ける
  22.     var label = GmailApp.getUserLabelByName('処理済み');
  23.     thread.addLabel(label);
  24.     }); //※1
  25.   }

74~78.
野菜番号ごとに分割した文字(紫枠)から、金額と数量の情報を分割する。

74.75.
“kingaku[t]"に、"金額"より後ろの文字を切り抜いて代入

76.
“kakuyasai"文字列から、"金額"より後ろの文字列を削除

77.
“kingaku[t]"から、半角数字のみ抽出する。金額の桁数が不明なので、このような処置とした。

78.
“kakuyasai"文字列から、半角数字のみ抽出する。数量の桁数が不明なので、このような処置とした。

80.
“uriage"配列に、ここまで抽出してきた情報を格納していく。
“店舗名" "野菜番号" "数量" "金額"

81.82.
スプレッドシートの最終行を読み取り、そこに上記の"uriage"配列を書き込む

89~93.
最後の処理。
スレッドを既読にして、処理の完了したメールには"処理済み"のラベルを付け、処理の重複を避ける
※3行目で、"処理済み"ラベルが付いているスレッドはスクレイピングしないようにしている。

 

毎日決まった時間に処理する

このプログラムを、毎日決まった時間に起動する事で、全自動化の完成です。

それもGASなら非常に簡単で、「トリガー」という機能で対応出来ます。

これは調べればすぐわかるので、ここでの紹介は省略。

だいたい毎日8時過ぎに売上情報メールが届くので、毎日9時をトリガーにして処理しています。

この全自動化スクレイピングによって、すっっっっごい管理が楽になりました。

そのあたりの解説は、また次回に。