1. HOME
  2. ブログ
  3. chatGPT活用事例「業界の最新情報を取得。レポートを毎週全員に配信」

chatGPT活用事例「業界の最新情報を取得。レポートを毎週全員に配信」

中小企業の経営者や担当者にとって、業界の最新情報を定期的にチェックすることは重要ですよね。

ただ、積極的に情報を取得するスタッフもいればそうでない人もでてきて、情報の蓄積にばらつきがでてきます。

また、忙しくて、最新情報を定期的にチェックができていないというケースもあると思います。

今回は、chatGPTの機能を使って、毎週業界の最新情報を取得。

そのレポートを配信した事例をご紹介します!

ITツールの価格変更をタイムリーに把握したい

弊社は、訳100個のITツールを販売していて、各社値上げが最近は激しいです。

毎年どこかが、値上げをしているのですが、それを代理店にきちんと配信してくれないベンダーさんがいたりします。

これを自動化できないかと考えまして、

chatGPTとGASを組み合わせて実現しましたので、

そのやり方を共有したいと思います!

構成図

仕組みの構成図

今回の仕組みは上記4ステップになります。

実際のGASスクリプトはこちら

const CONF = {
  TZ: ‘Asia/Tokyo’,
  DAYS: 7,
  TO: PropertiesService.getScriptProperties().getProperty(‘TO’) || ‘ここにメールをいれる’,
  SUBJECT_PREFIX: ‘【週次】ITツール最新情報(自動収集)’,
  SHEET_NAME: ‘Tools’, // 1列目にツール名
  MAX_PER_TOOL: 5,     // ツールごとの最大候補
};
// A列=ツール名、B列=メーカー名 または 公式ドメイン(どちらでも可)
function getToolsAndMakers(name) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName(name) || ss.insertSheet(name);
  const last = Math.max(1, sh.getLastRow());
  const values = sh.getRange(1, 1, last, 2).getValues(); // 2列取得
 
}
function isDomainLike(s){ return /\./.test(s) && !/\s/.test(s); }
// 価格系キーワード(必要に応じて追加可)
const PRICE_TERMS = ‘(価格 OR 料金 OR 値上げ OR 値下げ OR 価格改定 OR price OR pricing OR cost)’;
// 更新/プラン系(厳しめ検索で使用)
const UPDATE_TERMS = ‘(release notes OR changelog OR “what\\\’s new” OR 更新情報 OR 変更点 OR プラン OR plan)’;
function buildQueryStrict(tool, maker) {
  const toolQ  = `”${tool}“`;
  const makerQ = maker ? (isDomainLike(maker) ? `site:${maker}` : `”${maker}“`) : ;
  // ツール AND メーカー(またはsite:) AND 価格語 AND 更新語
  return [toolQ, makerQ, PRICE_TERMS, UPDATE_TERMS].filter(Boolean).join(‘ ‘);
}
function buildQueryFallback(tool, maker) {
  const toolQ  = `”${tool}“`;
  const makerQ = maker ? (isDomainLike(maker) ? `site:${maker}` : `”${maker}“`) : ;
  // 更新語を外して少し広げる(価格語は残す)
  return [toolQ, makerQ, PRICE_TERMS].filter(Boolean).join(‘ ‘);
}
function weeklyItUpdates() {
  const tz = CONF.TZ;
  const today = new Date(new Date().toLocaleString(‘en-US’,{ timeZone: tz }));
  const since = new Date(today); since.setDate(since.getDate() – CONF.DAYS);
  const runId = Utilities.formatDate(today, tz, “yyyy-MM-dd’T’HH:mm:ss”);
  logRow(‘INFO’, runId, ‘weeklyItUpdates start’);
  const pairs = getToolsAndMakers(CONF.SHEET_NAME);
  logRow(‘INFO’, runId, ‘tools loaded’, `count=${pairs.length}; first5=${pairs.slice(0,5).map(p=>p.tool).join(‘,’)}`);
  const candidates = [];
  for (const {tool, maker} of pairs) {
    // 1) 厳しめ検索
    let q = buildQueryStrict(tool, maker);
    let { items, apiUrl, raw } = cseSearchWithUrl(q, ‘w1’, CONF.MAX_PER_TOOL);
    writeCandidates(runId, tool, q, apiUrl, items);
    writeRaw(runId, tool, q, raw);
    // 2) ヒットが少ない場合はフォールバック
    if (items.length === 0) {
      const q2 = buildQueryFallback(tool, maker);
      const r2 = cseSearchWithUrl(q2, ‘w1’, CONF.MAX_PER_TOOL);
      writeCandidates(runId, tool, q2, r2.apiUrl, r2.items);
      writeRaw(runId, tool, q2, r2.raw);
      items = r2.items;
    }
    items.forEach(h => candidates.push({ tool, …h }));
  }
  const screened = openaiScreenAndSummarize(candidates, since, tz);
  writeAccepted(runId, screened.accepted);
  const html = buildHtml(screened, tz, since, today) +
    `<p style=”color:#666″>RunID: ${runId} / Pairs: ${pairs.length} / Candidates: ${candidates.length} / Accepted: ${(screened.accepted||[]).length}</p>`;
  const subject = `${CONF.SUBJECT_PREFIX} ${Utilities.formatDate(today, tz, ‘yyyy-MM-dd’)}`;
  GmailApp.sendEmail(CONF.TO, subject, ‘HTML版をご確認ください。’, { htmlBody: html, name: ‘Weekly IT Updates Bot’ });
  logRow(‘INFO’, runId, ‘weeklyItUpdates done’, `pairs=${pairs.length}, candidates=${candidates.length}, accepted=${(screened.accepted||[]).length}`);
}
function cseSearchWithUrl(query, dateRestrict, maxResults) {
  const key = PropertiesService.getScriptProperties().getProperty(‘CSE_API_KEY’);
  const cx  = PropertiesService.getScriptProperties().getProperty(‘CSE_CX’);
  if (!key || !cx) throw new Error(‘CSE_API_KEY/CSE_CX 未設定’);
  const params = {
    key: key,
    cx: cx,
    q: query,
    num: Math.min(maxResults || 10, 10),
    dateRestrict: dateRestrict || ‘w1’,
    safe: ‘off’,
    lr: ‘lang_ja’,
    hl: ‘ja’
  };
  const apiUrl = ‘https://www.googleapis.com/customsearch/v1?’ +
    Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join(‘&’);
  const res = UrlFetchApp.fetch(apiUrl, { muteHttpExceptions: true });
  const code = res.getResponseCode();
  const raw  = res.getContentText(); // ← RAWを保持
  if (code >= 400) {
    logRow(‘ERROR’, ‘n/a’, `CSE ${code}`, raw.slice(0,3000));
    throw new Error(`Custom Search API エラー: ${code} ${raw}`);
  }
  const data = JSON.parse(raw);
  const items = (data.items || []).map(it => ({
    title: it.title,
    link: it.link,
    snippet: it.snippet || ,
    source: it.link.replace(/^https?:\/\//, ).split(‘/’)[0]
  }));
  return { items, apiUrl, raw };
}
// ====== OpenAI Filtering & Summarization ======
function openaiScreenAndSummarize(items, sinceDate, tz) {
  const key = PropertiesService.getScriptProperties().getProperty(‘OPENAI_API_KEY’);
  const sys = `You are a precise analyst. Output JSON only. Keep official vendor updates within last ${CONF.DAYS} days.`;
  const user = JSON.stringify({
    since: Utilities.formatDate(sinceDate, tz, “yyyy-MM-dd’T’HH:mm:ssXXX”),
    items
  });
  const payload = {
    model: ‘gpt-4o-mini’,
    response_format: { type: ‘json_object’ },
    messages: [
      { role:‘system’, content: sys },
      { role:‘user’, content:
`From the candidate web results, return only official vendor update posts within the past ${CONF.DAYS} days.
For each accepted item, extract:
{tool, title, url, source, published_at(ISO), summary_ja(<=120字), is_official(bool)}
Also add “highlights_ja”: top 3 cross-tool bullets (<=70字 each) at the end as {highlights_ja:[…]}.
JSON keys: {accepted:[], highlights_ja:[]}.
Input JSON: ${user}` }
    ],
    temperature: 0.2
  };
  const resp = UrlFetchApp.fetch(‘https://api.openai.com/v1/chat/completions’, {
    method:‘post’,
    headers:{ ‘Authorization’: `Bearer ${key}`, ‘Content-Type’:‘application/json’ },
    payload: JSON.stringify(payload),
    muteHttpExceptions:true
  });
  if (resp.getResponseCode()>=400) throw new Error(resp.getContentText());
  const text = JSON.parse(resp.getContentText()).choices?.[0]?.message?.content || ‘{}’;
  return JSON.parse(text);
}
// ====== Email HTML ======
function buildHtml(data, tz, since, today) {
  const head = `
    <div style=”font-family:system-ui,Segoe UI,Roboto,sans-serif;line-height:1.6″>
      <h2>${CONF.SUBJECT_PREFIX}</h2>
      <p>対象期間:${Utilities.formatDate(since,tz,‘yyyy-MM-dd’)}${Utilities.formatDate(today,tz,‘yyyy-MM-dd’)}</p>
      ${data.highlights_ja?.length ? `<h3>今週のハイライト</h3><ul>${data.highlights_ja.map(x=>`<li>${escapeHtml(x)}</li>`).join()}</ul>`:}
      <h3>更新一覧(公式・週内)</h3>
  `;
  const body = (data.accepted||[]).map(x => {
    const date = x.published_at ? new Date(x.published_at) : null;
    const ds = date ? Utilities.formatDate(date, tz, ‘yyyy-MM-dd HH:mm’) : ;
    return `<li><strong>${escapeHtml(x.tool)}</strong>:<a href=”${escapeHtml(x.url)}” target=”_blank”>${escapeHtml(x.title)}</a>
      <span style=”color:#666″>(${escapeHtml(x.source)} / ${ds})</span><br>${escapeHtml(x.summary_ja||)}</li>`;
  }).join();
  const tail = `
      <ol>${body||‘<p>該当なし</p>’}</ol>
      <hr><p style=”color:#666;font-size:12px”>自動配信:GAS+CSE+OpenAI(公式更新のみ抽出)</p>
    </div>`;
  return head + tail;
}
function escapeHtml(s){return String(s).replace(/&/g,‘&amp;’).replace(/</g,‘&lt;’).replace(/>/g,‘&gt;’).replace(/”/g,‘&quot;’).replace(/’/g,‘&#39;’);}
// 初回用:月曜09:00 JSTで週次トリガーを作成
function setupWeeklyTrigger(){
  ScriptApp.getProjectTriggers().forEach(t => t.getHandlerFunction()===‘weeklyItUpdates’ && ScriptApp.deleteTrigger(t));
  ScriptApp.newTrigger(‘weeklyItUpdates’).timeBased().onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(9).create();
}
/** ===== ログ用設定 ===== */
const LOGS_SHEET     = ‘Logs’;       // 実行ログ(INFO/ERROR)
const CANDIDATES_SHEET = ‘Candidates’;// 取得候補(検索結果)
const ACCEPTED_SHEET   = ‘Accepted’;  // 最終採用(公式&週内)
const RAW_SHEET        = ‘RawJSON’;   // (任意)検索APIの生JSONを保存
/** シート取得(なければ作成)+ヘッダー付与 */
function getOrCreateSheet(name, headers) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName(name) || ss.insertSheet(name);
  if (sh.getLastRow() === 0 && headers && headers.length) {
    sh.appendRow(headers);
  }
  return sh;
}
/** 実行ログ */
function logRow(level, runId, message, extra) {
  const sh = getOrCreateSheet(LOGS_SHEET, [‘timestamp’,‘level’,‘runId’,‘message’,‘extra’]);
  sh.appendRow([new Date(), level, runId, message, extra || ]);
}
/** 候補の記録 */
function writeCandidates(runId, tool, query, apiUrl, items) {
  const sh = getOrCreateSheet(CANDIDATES_SHEET, [
    ‘runId’,‘timestamp’,‘tool’,‘query’,‘apiUrl’,‘title’,‘link’,‘source’,‘snippet’
  ]);
  const now = new Date();
  const rows = items.map(it => [
    runId, now, tool, query, apiUrl, it.title, it.link, it.source, it.snippet ||
  ]);
  if (rows.length) sh.getRange(sh.getLastRow()+1,1,rows.length,rows[0].length).setValues(rows);
}
/** 採用結果の記録 */
function writeAccepted(runId, accepted) {
  const sh = getOrCreateSheet(ACCEPTED_SHEET, [
    ‘runId’,‘timestamp’,‘tool’,‘title’,‘url’,‘source’,‘published_at’,‘summary_ja’,‘is_official’
  ]);
  const now = new Date();
  const rows = (accepted||[]).map(x => [
    runId, now, x.tool, x.title, x.url, x.source, x.published_at || , x.summary_ja || , x.is_official
  ]);
  if (rows.length) sh.getRange(sh.getLastRow()+1,1,rows.length,rows[0].length).setValues(rows);
}
/** 任意:RAW JSON保存(サイズ注意) */
function writeRaw(runId, tool, query, jsonText) {
  const sh = getOrCreateSheet(RAW_SHEET, [‘runId’,‘timestamp’,‘tool’,‘query’,‘jsonText’]);
  sh.appendRow([runId, new Date(), tool, query, jsonText]);
}

調査結果レポート例

 

以下のような多様なgoogleの先週の最新情報で、今回は価格とかプラン変更とか、値上げとかにマッチするものをリストアップ

リストアップしたもの

 

ここからchatGPTに情報を渡して精査してもらい、最終的に該当する者は以下の2つという判断になり、これがメールで届きました。

レポート例

 

今回は、うちが知りたい情報が、ITツールの値上げだったので、そういう指示にしましたが、各業界の最新情報や法律、ルール変更、その他企業様ごとに知りたい情報にカスタマイズして、定期レポートを配信する仕組みを作ることは可能です。

 

なぜ、chatGPTのタスクだけでないのか?

実はchatGPTにはタスク機能があり、毎週指定した処理を実行してもらい、それこそメールを送ってもらうこともできます。

ただ、やってみて以下の欠点がありました。

①自分のアカウントに届く

本当は社員全員がみるメールに送りたいのに、現状では自分あての送信になってしまいました。

②メール本文にレポート内容を書けない

chatGPTがタスクで送るのは、レポートの内容を本文に書くのではなく、chatGPTの該当スレッドのリンクでした。

それだと、いちいちスタッフがクリックして、中を見てくれるかもわからず、本文にレポートを入れたいというのが、強い希望でありました。

上記①②の理由をクリアするため、GAS×chatGPTという構成になりました。

 

 

GAS×chatGPT によるレポート導入の効果:全員に同じタイミングで最新情報が届く

今回導入の目的は、100あるITツールの値上げを漏れなく把握したいということでしたが、

導入してみて、スタッフごとに情報の取得する先が異なり、必要な情報なのに、同じタイミングで情報が入ってきていないことがよくわかりました。

今回の仕組みを導入して、社員全員が見るメールでに、レポートを配信することで、

全員が同じ情報が把握できるようになったのは、大きいと思いました。

 

 

AI研修で、上記のようなマイGPTを作って、業務改善DXを行いませんか?

弊社では、chatGPT(AI活用)の活用研修も行っております。 上記のような、chatGPTを使っての業務改善を行うのであれば、以下の研修をご検討ください。 人材開発助成金で研修費用の75%を削減できます!

生成AI研修➡

 

関連記事

AI活用の人気記事

ICTオフィス相談室 最新記事

おすすめ記事