14
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(コードを)書いた方が早い?うっせぇわ! #書かないGAS

Last updated at Posted at 2025-03-13

image.png

「生成AIに聞くより、コードを書いた方が早い」って言ってくる人いないですか?

強者マウントに負けてはいけません。動けばいいのです。書けなくても。

わたくしおきなかと、生成AIを相棒に書かないGASの旅に出かけましょう。

2年越しの夢

LINDEでLT登壇したスライド

image.png

毎朝数社のwebサイトの更新を確認する業務がめんどくさい。サイトを巡回し更新があればLINE通知する仕組みを作りたかったけど、うまくいかなかった。

この2年間で目覚ましく進化したAI。 もしかして今ならやれるのでは?よしやってやる。

image.png

コードは生成AIに書かせて完成!

できたもはこちら

image.png

1日に1回指定のサイトを巡回し、記事やURLを取得してスプレッドシードに記録。
その差分をLINEに通知する仕組みが完成💪

コードは1行も書かなかったので、その戯れを振り返ります。

GPTとGASは犬猿の仲(個人意見)

まずコード生成のスピードが遅い。

書けない人が、何回デプロイするのか分かっているのでしょうか。
我々書かない勢は、エラーが出ても、その中身なんて理解しません。
出たエラーを生成AIに打ち込んで、もう一回コードを生成してもらっての繰り返し。
1回の生成スピードの遅さは命取りです。

あと、文字化けなども何度指摘しても直してくれません。

image.png

2年前もこういったことが続いて諦めてしまいました。

餅は餅屋 GASならGemini

と思い立って、Geminiを使ってみました。

まずコードの生成スピードが格段に違う。早い。いい。

GASとの相性がすごく良さそうで、エラーが出る回数はかなり減った気がします(体感)

エラーを貼って、修正してもらって…の繰り返しなのですが、
「エラーだけじゃなく画面全体のスクショを提供して」など、賢く提案してくれます。

image.png

『エラーが出てるけど、そもそもお前のコードの貼り方や、スクリプトプロパティが間違ってない?』など、先回りして問題解決に努めてくれます。

あと、励ましてくれます。
image.png

監視ブロックを乗り越えろ

いくつかのサイトでは、『アクセスがブロックされます』といったエラーが発生してしまいます。
最初から監視しに行こうとするとブロックされるのかな?と推察して、『監視はしなくていいので、シンプルにHTMLの構造を分析して、指定した箇所のデータを抜き出してほしい』、と丁寧に、1ステップずつお願いしてみました。

image.png

監視ブロックの壁を乗り越えて、毎回きちんと情報を取得できるようになりました。

image.png

総デプロイ回数212回

を要したコード、ご精査ください。

image.png

一旦2社分のみ作ってます。

 

Code.gs (クリックで表示)

/**
* メインの処理を実行し、メールを送信します。
*/
function mainFunction() {
 var scriptProperties = PropertiesService.getScriptProperties();
 var spreadsheetId = scriptProperties.getProperty("SPREADSHEET_ID");
 var ss = SpreadsheetApp.openById(spreadsheetId);
 var sheet = ss.getActiveSheet();

 // ①スプレッドシートの過去のデータを写す
 transferPastData(sheet);

 // ②イオンの情報を取得する
 var aeonNews = AeonNews.getNews(sheet);

 // ③その情報をスプレッドシートに書き込む(イオン)
 writeNewsToSheet(sheet, aeonNews, 2, "イオン株式会社"); // 2行目から書き込み

 // ④イオン九州のデータを取得する
 var aeonKyushuNews = AeonKyushuNews.getNews(sheet);

 // ⑤イオンのデータの下に九州のデータを書き連ねる
 writeNewsToSheet(sheet, aeonKyushuNews, 2 + aeonNews.length, "イオン九州株式会社"); // イオンのデータの次の行から書き込み

 // ⑥差分を見てメールする
 sendEmail(sheet);
}

/**
* スプレッドシートの過去のデータを転記します。
* @param {Sheet} sheet - スプレッドシートのシートオブジェクト
*/
function transferPastData(sheet) {
 var lastRow = sheet.getLastRow();
 if (lastRow > 1) {
   var pastData = sheet.getRange(2, 2, lastRow - 1, 3).getValues();
   sheet.getRange(2, 5, lastRow - 1, 3).setValues(pastData);
 }
}

/**
* ニュース記事をスプレッドシートに書き込みます。
* @param {Sheet} sheet - スプレッドシートのシートオブジェクト
* @param {Array<Object>} news - ニュース記事の配列
* @param {number} startRow - 書き込み開始行
* @param {string} siteName - サイト名
*/
function writeNewsToSheet(sheet, news, startRow, siteName) {
 var newData = news.map(function (item) {
   return [item.date, item.title, item.url];
 });
 sheet.getRange(startRow, 2, newData.length, 3).setValues(newData);

 // サイト名をA列に書き込み
 var siteNameArray = [];
 for (var i = 0; i < newData.length; i++) {
   siteNameArray.push([siteName]);
 }
 sheet.getRange(startRow, 1, newData.length, 1).setValues(siteNameArray);
}

/**
* 新着記事の有無を確認し、メールを送信します。
* @param {Sheet} sheet - スプレッドシートのシートオブジェクト
*/
function sendEmail(sheet) {
 var allNews = getAllNewsFromSheet(sheet);
 var pastTitles = getPastTitles(sheet);
 var siteNews = groupNewsBySite(allNews);
 var recipient = Session.getActiveUser().getEmail();
 var subject = "ニュース更新情報";
 var body = "";

 for (var site in siteNews) {
   var news = siteNews[site];
   var newArticles = getNewArticles(news, pastTitles);
   body += createSiteMessage(site, news, newArticles);
 }

 MailApp.sendEmail(recipient, subject, body);
}

/**
* スプレッドシートからすべてのニュース記事を取得します。
* @param {Sheet} sheet - スプレッドシートのシートオブジェクト
* @returns {Array<Object>} - すべてのニュース記事
*/
function getAllNewsFromSheet(sheet) {
 var lastRow = sheet.getLastRow();
 var titles = sheet.getRange(2, 3, lastRow - 1, 1).getValues().flat();
 var dates = sheet.getRange(2, 2, lastRow - 1, 1).getValues().flat();
 var urls = sheet.getRange(2, 4, lastRow - 1, 1).getValues().flat();
 var sites = sheet.getRange(2, 1, lastRow - 1, 1).getValues().flat();

 return titles.map(function (title, index) {
   return {
     date: dates[index],
     title: title,
     url: urls[index],
     site: sites[index],
   };
 });
}

/**
* スプレッドシートから過去の記事タイトルを取得します。
* @param {Sheet} sheet - スプレッドシートのシートオブジェクト
* @returns {Array<string>} - 過去の記事タイトル
*/
function getPastTitles(sheet) {
 return sheet
   .getRange(2, 6, 10, 1)
   .getValues()
   .flat()
   .filter(String);
}

/**
* サイトごとにニュース記事をグループ化します。
* @param {Array<Object>} allNews - すべてのニュース記事
* @returns {Object} - サイトごとにグループ化されたニュース記事
*/
function groupNewsBySite(allNews) {
 var siteNews = {};
 allNews.forEach(function (article) {
   if (!siteNews[article.site]) {
     siteNews[article.site] = [];
   }
   siteNews[article.site].push(article);
 });
 return siteNews;
}

/**
* 新しい記事を取得します。
* @param {Array<Object>} news - ニュース記事
* @param {Array<string>} pastTitles - 過去の記事タイトル
* @returns {Array<Object>} - 新しい記事
*/
function getNewArticles(news, pastTitles) {
 return news.filter(function (article) {
   return !pastTitles.includes(article.title);
 });
}

/**
* サイトごとのメッセージを作成します。
* @param {string} site - サイト名
* @param {Array<Object>} news - ニュース記事
* @param {Array<Object>} newArticles - 新しい記事
* @returns {string} - サイトごとのメッセージ
*/
function createSiteMessage(site, news, newArticles) {
 var message = "会社名:" + site + "\n";
 var siteUrl = getSiteUrl(site); // サイトURLを取得
 if (newArticles.length > 0) {
   message += "新着記事がありました。\n\n";
   message += "ニュースサイトURL:" + siteUrl + "\n\n"; // サイトURLを追加
   newArticles.forEach(function (article) {
     message += "日付:" + formatDate(article.date) + "\n";
     message += "タイトル:" + article.title + "\n";
     message += "URL:" + article.url + "\n\n";
   });
 } else {
   message += "前回からの更新はありません。\n\n";
   message += "サイトURL:" + siteUrl + "\n"; // サイトURLを追加
   message += "最新記事\n";
   message += "日付:" + formatDate(news[0].date) + "\n";
   message += "タイトル:" + news[0].title + "\n";
   message += "URL:" + news[0].url + "\n\n";
 }
 return message;
}

/**
* 日付を月日表示にフォーマットします。
* @param {string} dateString - 日付文字列
* @returns {string} - フォーマットされた日付文字列
*/
function formatDate(dateString) {
 var date = new Date(dateString);
 var month = date.getMonth() + 1;
 var day = date.getDate();
 return month + "" + day + "";
}

/**
* サイト名からサイトURLを取得します。
* @param {string} siteName - サイト名
* @returns {string} - サイトURL
*/
function getSiteUrl(siteName) {
 if (siteName === "イオン株式会社") {
   return "https://www.aeon.info/news/";
 } else if (siteName === "イオン九州株式会社") {
   return "https://www.aeon-kyushu.info/news/";
 }
 // 他のサイトが追加された場合はここに追加
 return "";
}

 

AeonNews.gs (クリックで表示)

var AeonNews = {
 /**
  * イオン株式会社のニュースを取得し、記事オブジェクトの配列を返します。
  * @param {Sheet} sheet - スプレッドシートのシートオブジェクト
  * @returns {Array<Object>} - ニュース記事の配列
  */
 getNews: function (sheet) {
   var url = "https://www.aeon.info/news/";
   var regex = /<dt class="date"><span class="date-time">([^<]+)<\/span><i class="ico ico-[a-z]+">[^<]+<\/i><\/dt><dd class="txt"><a href="([^"]+)">([^<]+)<\/a><\/dd>/g;
   return this.fetchNews(url, regex, function (match) {
     return {
       date: match[1],
       url: match[2],
       title: match[3],
     };
   });
 },

 /**
  * ニュース記事を取得し、記事オブジェクトの配列を返します。
  * @param {string} url - ニュースサイトのURL
  * @param {RegExp} regex - ニュース記事を抽出する正規表現
  * @param {function} createNewsItem - マッチした結果から記事オブジェクトを作成する関数
  * @returns {Array<Object>} - ニュース記事の配列
  */
 fetchNews: function (url, regex, createNewsItem) {
   var response = UrlFetchApp.fetch(url);
   var content = response.getContentText("UTF-8");
   var news = [];
   var match;
   var count = 0;

   while ((match = regex.exec(content)) && count < 5) {
     news.push(createNewsItem(match));
     count++;
   }

   return news;
 },
};

 

AeonKyushuNews.gs (クリックで表示)

var AeonKyushuNews = {
 /**
  * イオン九州株式会社のニュースを取得し、記事オブジェクトの配列を返します。
  * @param {Sheet} sheet - スプレッドシートのシートオブジェクト
  * @returns {Array<Object>} - ニュース記事の配列
  */
 getNews: function (sheet) {
   var url = "https://www.aeon-kyushu.info/news/";
   var regex =
     /<a href="(\/files\/optionallink\/[^"]+\.pdf)" target="_blank" rel="noopener noreferrer">.*?<div class="date">([^<]+)<\/div>.*?<div class="title">([^<]+)<\/div>/gs;
   return this.fetchNews(url, regex, function (match) {
     return {
       date: match[2],
       url: "https://www.aeon-kyushu.info" + match[1],
       title: match[3],
     };
   });
 },

 /**
  * ニュース記事を取得し、記事オブジェクトの配列を返します。
  * @param {string} url - ニュースサイトのURL
  * @param {RegExp} regex - ニュース記事を抽出する正規表現
  * @param {function} createNewsItem - マッチした結果から記事オブジェクトを作成する関数
  * @returns {Array<Object>} - ニュース記事の配列
  */
 fetchNews: function (url, regex, createNewsItem) {
   var response = UrlFetchApp.fetch(url);
   var content = response.getContentText("UTF-8");
   var news = [];
   var match;
   var count = 0;

   while ((match = regex.exec(content)) && count < 5) {
     news.push(createNewsItem(match));
     count++;
   }

   return news;
 },
};

(コードを)書いた方が早い?

と思います🚄多分。

が、作った人や動かした人が偉いのである。

うっせぇわ!といって、動くまでやったりましょう💪

コードライティングなんて「手段」の一つなので!

これからも、AE〇Nの主要サイトどんどん登録していきます。

コードは書けなくてもモノづくりするみなさん、応援してます🤗諦めないで!

2年前の自分の登壇スライドの1ページ。まさに!
image.png

書かないGASの輪拡大中😏

【ショートカットの達人】 仕事効率化のためのショートカット検索アプリ

【GAS】嬉しい!毎日指定した時間ジャストにGoogleカレンダーのスケジュールが届く!

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?