Amazonで商品を見るセール会場へ

GASでHTMLの値を抽出してSlackへ送る練習|架空の株価データを使用

HTMLから値を取り出してSlackへ送る処理を、架空の株価データで練習します。

記事内の固定HTMLから値を抽出する練習です。実際の株価サイトへアクセスして情報を取得するコードは含みません。

目次

静的HTMLから数値を取り出す練習

通知連携の練習コードとローカルテストをダウンロード(ZIP)。

各article-番号.jsを別々のApps Scriptプロジェクトへ保存します。

article-5721.jsを別のApps Scriptプロジェクトへ保存し、demoSlackを実行します。

PRACTICE_HTMLにある練習工業と1,234.50から、架空の通知文を作ります。

外部ページの取得もSlackへの投稿も行いません。

// 各記事の例は別々のApps Scriptプロジェクトへ保存します。
function props_() { return PropertiesService.getScriptProperties(); }
function required_(key) {
  const value = props_().getProperty(key);
  if (!value) throw new Error(key + 'をスクリプトプロパティへ設定してください');
  return value;
}
function plain_(text) {
  return String(text).replace(/\[/g, '[').replace(/\]/g, ']');
}
function stateKey_(kind, destination, id) {
  const digest = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256,
    kind + ':' + destination + ':' + id, Utilities.Charset.UTF_8);
  return 'CGM_JOB_' + Utilities.base64EncodeWebSafe(digest);
}
function preview_(plans) {
  // 実メールや予定の本文はログへ出さない。demo関数で架空の本文を確認。
  Logger.log(JSON.stringify(plans.map(p => ({id: p.id, characters: p.text.length}))));
  return plans;
}
function sendControlled_(kind, destination, buildPlans, transport) {
  if (props_().getProperty('SEND_ENABLED') !== 'true') throw new Error('送信は無効です。まずプレビューしてください');
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(1000)) throw new Error('別の実行が処理中です');
  try {
    const plans = buildPlans();
    for (const plan of plans) {
      const key = stateKey_(kind, destination, plan.id);
      const state = props_().getProperty(key);
      if (state === 'done') continue;
      if (state) throw new Error('結果不明の処理があります。投稿先と記録を照合してください');
      // 通信前に記録。通信成功後の記録失敗でも自動で再送しない。
      props_().setProperty(key, 'sending');
      try {
        transport(plan.text);
        props_().setProperty(key, 'done');
      } catch (error) {
        props_().setProperty(key, 'unknown');
        throw new Error('送信結果を確定できません。自動再送せず投稿先を確認してください');
      }
    }
  } finally { lock.releaseLock(); }
}
const PRACTICE_HTML = '<h1 id="company">練習工業</h1><span class="price">1,234.50</span>';
function parseQuote_(html) {
  // この構造の静的HTML専用。実サイトの汎用HTMLパーサーではありません。
  const name = html.match(/<h1 id="company">([^<>]+)<\/h1>/);
  const price = html.match(/<span class="price">([0-9,]+(?:\.[0-9]+)?)<\/span>/);
  if (!name || !price) throw new Error('想定したHTML要素がありません');
  if (!/^(?:[0-9]+|[0-9]{1,3}(?:,[0-9]{3})+)(?:\.[0-9]+)?$/.test(price[1])) throw new Error('価格形式が違います');
  const number = Number(price[1].replace(/,/g,''));
  if (!Number.isFinite(number)) throw new Error('数値を取得できません');
  return {name:name[1].trim(),price:number};
}
function practiceSlackPlans_() {
  const quote = parseQuote_(PRACTICE_HTML);
  return [{id:'practice-quote-20261002',text:'【架空データ・練習】' + quote.name + ': ' + quote.price}];
}
function demoSlack() { const p = practiceSlackPlans_(); Logger.log(JSON.stringify(p)); return p; }
function sendPracticeToSlack() {
  const webhook = required_('SLACK_WEBHOOK_URL');
  if (!/^https:\/\/hooks\.slack\.com\/services\/[^/?#]+\/[^/?#]+\/[^/?#]+$/.test(webhook)) throw new Error('Webhookの形式が違います');
  sendControlled_('slack-practice', webhook, practiceSlackPlans_, text => {
    const response = UrlFetchApp.fetch(webhook,{method:'post',contentType:'application/json',
      payload:JSON.stringify({text:text}),muteHttpExceptions:true});
    if (response.getResponseCode() !== 200 || response.getContentText().trim() !== 'ok') throw new Error('Slackが成功応答を返しませんでした');
  });
}
スポンサーリンク

抽出とWebhook応答の確認結果

ローカル模擬テストの条件確認した結果
記事内の静的HTML練習工業: 1234.5の通知文を作る
要素がない、数値が1,2のような不正形式空の値を送らずエラーで止まる
Webhookへ投稿する要求JSONのContent-Typeを設定し、本文だけを渡す
200かつ本文ok以外の応答結果を成功と扱わず、自動再試行を止める

許可された送信先へ練習データを送る設定

既存のSlackアプリで許可された練習用チャンネルのIncoming Webhookを使い、スクリプトプロパティへSLACK_WEBHOOK_URLを設定します。

Webhook URLは秘密情報として扱い、記事、公開リポジトリ、ログに掲載しません。

送信関数はsendPracticeToSlackです。

送信先、表示名、アイコンはWebhookを発行したアプリ側の設定に従うため、本文のchannel、username、icon_emojiで上書きする旧例を使いません。

現在の仕様とエラーはSlackの公式Webhook説明で確認しています。

HTTP 200だけではなく、応答本文okも確認します。

このコードは固定の練習データを1回送る例で、同じWebhook URLと固定IDに対する送信済み記録があれば再投稿しません。HTMLの値だけを変えても固定IDは変わらないため、再送されません。一方、Webhook URLの変更や記録削除では別送信として扱われることがあります。

定期的に実データを通知するには、取得元、取得日時、データの識別方法を決めてから実装を追加します。

抽出用の正規表現は記事内の静的HTML専用です。

実サイトのタグ構造、JavaScriptで描画される値、HTMLの文字参照などを汎用的に扱うものではありません。

実データを取得する場合は利用条件と許可を確認し、提供APIがあればその仕様に合わせて別途検証します。

抽出の練習とSlack送信を分けて確認する

練習の段階から順に当てはめると、実データを送る前に確認することが決まります。2026年10月2日の記事の確認内容と、Slackの公式Webhookの説明にもとづく目安です。demoSlackは架空データの通知文を作るだけです。sendPracticeToSlackは、設定と送信許可フラグが揃うと実際にSlackへ投稿します。

  1. まず、架空の練習から始める。記事のコードを別のApps Scriptプロジェクトに保存し、demoSlackを実行する。記事内の静的HTMLから、練習工業と1,234.50を取り出し、通知文を作る。
  2. 送信するなら、既存のSlackアプリで許可された練習用チャンネルのIncoming Webhookを使い、スクリプトプロパティにSLACK_WEBHOOK_URLを設定する。
  3. Webhook URLは秘密情報として扱い、記事、公開リポジトリ、ログに載せない。
  4. 送信先、表示名、アイコンは、Webhookを発行したアプリ側の設定に従う。本文のchannel、username、icon_emojiで上書きする旧例は使わない。
  5. 応答は、HTTP 200だけでなく、応答の本文がokかも確認する。200でも本文がok以外なら、成功と扱わず、自動の再試行を止める。
  6. 実データを取得する場合は、取得元の利用条件と許可を確認し、提供APIがあれば、その仕様に合わせて別に検証する。

このコードは、固定の練習データを1回送る例です。定期的に実データを通知するには、取得元、取得日時、データの識別方法を決めてから実装を追加してください。

送信を有効にする前の確認と止め方

送信先を管理する本人の許可を得て、練習用の自分宛てに送る内容と送信先を確認してから、スクリプトプロパティのSEND_ENABLEDをtrueにします。

デモやプレビューの関数に送信トリガーを付けません。

この固定データの練習ではsendPracticeToSlackを手動で1回実行し、投稿先・内容・保存状態を照合します。定期実行のトリガーは不要です。

旧プロジェクトや別のトリガーが同時に送ると、このプロジェクトのロックだけでは二重投稿を防げません。

通信前にsending、成功応答後にdoneを保存します。

通信や記録に失敗した場合はunknownなどの状態を残して止まり、自動で再送しません。

結果が不明な場合はSEND_ENABLEDをfalseにし、トリガーも停止して、投稿先に同じ内容が届いていないか確認します。

送信が確認できた記録はdoneへ整え、未送信を確認できた場合だけ再実行の準備をします。

送信済みの記録を消すと同じ対象を再送する可能性があります。

長期運用では記録容量と整理方法を決め、旧コード・設定を控えてから変更します。

戻す際は新しいトリガーを止め、保存したコードと設定へ戻します。

練習コードの検証範囲

2026年10月2日に、配布ZIP全体について、Node.jsでGoogle Apps Script・HTTP応答・シート・ロック・保存状態を模した55ケースを確認しました。

ZIPを新しいフォルダーへ展開し、node test_integration.cjsでも同じ結果を確認できます。

実際のGoogle認可、APIへの投稿、シート書き込み、時間主導トリガー、受信側の通知音は未検証です。

ローカルでの成功を、実サービスでの配信成功と扱わないでください。

関連記事

スポンサーリンク

よくある質問:GASからSlackへ投稿するときのチェックリスト

記事のコードは、実際にSlackへ投稿しますか?

最初に実行するdemoSlackは、架空の練習データから通知文を作るだけで、外部ページの取得もSlackへの投稿も行いません。送信するのは、sendPracticeToSlackで、許可された練習用チャンネルに限ります。

Webhook URLは、どう扱いますか?

秘密情報として扱い、スクリプトプロパティに設定します。記事、公開リポジトリ、ログには載せません。

本文でchannelやusernameを指定できますか?

旧例では指定していましたが、送信先、表示名、アイコンは、Webhookを発行したアプリ側の設定に従うため、上書きする旧例は使いません。

送信が成功したかどうかは、どう確認しますか?

HTTP 200だけではなく、応答の本文がokかも確認します。200でも本文がok以外の応答は、成功と扱わず、自動の再試行を止めます。

実際の株価のページから取得できますか?

記事の抽出用の正規表現は、記事内の静的HTML専用です。実サイトのタグの構造、JavaScriptで描画される値、HTMLの文字参照などは、汎用的に扱えません。実データを取得する場合は、利用条件と許可を確認し、提供APIがあれば、その仕様に合わせて別に検証してください。

想定した要素がないときや、数値の形式が違うときは?

会社名や価格の要素が一致しない場合、または価格が1,2のような形式の場合はエラーになります。ただし会社名が空白だけか、重複要素がないかなどを網羅的には検査しません。実データ用には検証処理の追加が必要です。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

仕事効率化のおすすめ書籍

仕事用PCの選び方

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次