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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/e96fe396-017c-41ce-aa08-fe3f0e1a534e.png)

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

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

**わたくし[おきなか](https://twitter.com/okinakamasayos1)と、生成AIを相棒に`書かないGAS`の旅に出かけましょう。**

## 2年越しの夢

LINDEでLT登壇したスライド

https://speakerdeck.com/okinakagaikotu/hpjian-shi-botgazuo-ritai-230520okinaka

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/1b7de45c-352d-4b1d-b9e0-28c05f34e4a2.png)

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

この2年間で目覚ましく進化したAI。 <font color="Red">**もしかして今ならやれるのでは？よしやってやる。**</front>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/c54f25dd-4bc8-431f-a72c-acb39b943f24.png)

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

できたもはこちら

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/7c335bac-59ad-4428-9783-c503af2c66d3.png)

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

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

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

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

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


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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/0612a2f9-10be-47b4-8493-847efc80c507.png)

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

## 餅は餅屋 GASならGemini

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

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/bc28f03f-4247-4cef-9f32-e5c4e7c5d844.png)

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

あと、励ましてくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/b91cdc3d-3ec1-4e9c-987b-ef4c7100d532.png)

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/7ee381a6-7e97-459a-a570-66fd881202fa.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/c8286fa0-ea73-4ba2-9e8f-ab6b06f35407.png)

## 総デプロイ回数212回

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/08deb631-8f64-4be3-9fc1-9d5447fc0bee.png)

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

　<details><summary>Code.gs **(クリックで表示)** </font></summary><div><br>

 ```js
/**
 * メインの処理を実行し、メールを送信します。
 */
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 "";
}

```
</div></details>

　<details><summary>AeonNews.gs **(クリックで表示)** </font></summary><div><br>
 ```js
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;
  },
};
```
</div></details>

　<details><summary>AeonKyushuNews.gs **(クリックで表示)** </font></summary><div><br>
 ```js
 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;
  },
};
```
</div></details>


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

と思います🚄多分。

が、<font color="Red">**作った人や動かした人が偉いのである。**</front>

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

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

https://x.com/okinakamasayos1/status/1899851129716089204

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

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

2年前の自分の登壇スライドの1ページ。まさに！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1793443/c3ac6ad4-30b7-4e88-8492-2995b6b15c69.png)

https://speakerdeck.com/okinakagaikotu/hpjian-shi-botgazuo-ritai-230520okinaka

**書かないGASの輪**拡大中😏

[【ショートカットの達人】 仕事効率化のためのショートカット検索アプリ](https://qiita.com/hoshieee/items/3a785ddebf3657c2a7c9)

[【GAS】嬉しい！毎日指定した時間ジャストにGoogleカレンダーのスケジュールが届く！](https://qiita.com/natsupariri/items/bfd803eeb40e6e29d303)