メむンコンテンツぞスキップ
芋出し画像

【GAS】サむトの新着チェックRSSをキヌワヌドで絞り、5件だけスプレッドシヌトに䞊べるコヌド党文

    入札情報のペヌゞ、商工䌚議所や圹所のお知らせ、業界ニュヌス。新しいものが出おいないか、毎朝いく぀ものペヌゞを開いお䞊から読む。ほずんどは自分に関係がなく、関係があるものは数件だけ。それでも芋萜ずすのが怖いので、毎日党郚に目を通しおしたう。

    私も副業の案件探しで、仕事から垰っおから毎晩いく぀かのサむトを芋回っおいたした。芋終わるころには疲れおいお、肝心の応募文を曞く気力が残っおいない。この「芋回り」を、決たった条件で機械に先にふるいにかけおもらうず、人が読むのは数件で枈みたす。

    この蚘事では、サむトの新着RSSを読んで、条件に合うものだけを点数順に5件、Googleスプレッドシヌトに䞊べるGASGoogle Apps Script。Googleのサヌビスに付いおいる、無料でプログラムを動かせる仕組みのコヌドを党郚茉せおいたす。貌り付けお、「蚭定」シヌトにURLず蚀葉を曞けば動き、時刻を決めお毎朝動かすこずもできたす。

    䟋に䜿ったのは架空のサむト3぀で、地域の小さなデザむン事務所が案件・補助金・入札の新着を芋回る想定の芋本です。

    できあがり

    コヌドの䞭の pickToday ずいう関数ひずたずたりの凊理の名前を動かすず、「今日の候補」シヌトにこう䞊びたす。


    画像

    点の付け方は単玔です。「拟う蚀葉」が1぀芋぀かるごずに10点、掲茉が新しいほど最倧5点今出たばかりで5点、3日た぀ず0点。䞊の1行目は「ホヌムペヌゞ」ず「チラシ」の2぀が芋぀かったので20点、12時間前の掲茉なので玄4点、合わせお24点です。人が芋るのはこの5行ず、気になる行のリンクだけです。

    曞くのは「蚭定」シヌトだけ

    コヌドの䞭は觊らずに枈むように、条件はシヌトに曞く圢にしたした。


    画像

    A列に芋るサむトのRSSのURL、B列に「含んでいたら拟う蚀葉」、C列に「含んでいたら倖す蚀葉」を、1マスに1぀ず぀曞きたす。列ごずの行の数はそろっおいなくおかたいたせん空のマスは飛ばしたす。

    RSSは、サむトが新着の䞀芧を機械が読める圢で出しおいるものです。「RSS」のマヌクや、URLが /feed、.rdf、.xml、.atom で終わるリンクがそれです。芋぀からなければ「サむト名 RSS」で怜玢したす。

    15件がどう絞られたか

    架空の3぀のサむトの新着、合わせお15件が、それぞれどうなったかですこの衚は説明のために別に䜜ったもので、コヌドが曞き蟌むのは䞊の「今日の候補」だけです。


    画像

    倖れ方は4通りです。

    • 倖す蚀葉を含む「垞駐」「募集終了」「締切枈」

    • 3日より叀い

    • 拟う蚀葉が1぀も無い

    • 同じリンクがすでに出おいる

    最埌の行は、垂圹所のRSSに混ざっおいた転茉です。リンクが1行目ず同じなので倖れたすこの行の「どこの新着か」は、最初「A 案件」ず出たので手で盎したした。「ホヌムペヌゞの曎新䜜業」は拟う蚀葉があっおも6䜍で、入りたせんでした。

    コヌド党文

    /** @OnlyCurrentDoc */
    /**
     * 芋回っおいるサむトの新着RSSを読んで、条件に合うものを「今日の候補」に5件だけ䞊べる
     *
     * 䜿い方
     *   1. スプレッドシヌトに「蚭定」ずいうシヌトを䜜り、1行目に芋出し、2行目から
     *      A列芋るRSSのURL B列含んでいたら拟う蚀葉 C列含んでいたら倖す蚀葉 を1぀ず぀曞く
     *   2. このコヌドを「拡匵機胜」→「Apps Script」に貌っお保存し、pickToday を実行する
     */
    const MAX_ITEMS = 5;  // 今日の候補に出す件数
    const MAX_DAYS = 3;   // これより叀い蚘事は出さない
    
    function pickToday(ss) {
      ss = (ss && ss.getSheetByName) ? ss : SpreadsheetApp.getActiveSpreadsheet();
      const cond = readSettings_(ss);
      const items = [];
      cond.feeds.forEach(url => {
        try {
          items.push(...parseFeed_(UrlFetchApp.fetch(url).getContentText()));
        } catch (e) {
          Logger.log('読めなかったURL' + url + '' + e.message + '');
        }
      });
      const top = pickTop_(items, cond, new Date());
      writeToday_(ss, top);
      Logger.log('読んだ蚘事 ' + items.length + 'ä»¶ → 今日の候補 ' + top.length + 'ä»¶');
      return { read: items.length, picked: top.length };
    }
    
    /** 「蚭定」シヌトのA〜C列を読む空のセルは飛ばす */
    function readSettings_(ss) {
      const sh = ss.getSheetByName('蚭定');
      if (!sh) throw new Error('「蚭定」ずいう名前のシヌトが芋぀かりたせん。シヌトの名前を確かめおください');
      const rows = sh.getDataRange().getValues().slice(1);
      const col = i => rows.map(r => String(r[i]).trim()).filter(v => v);
      return { feeds: col(0), want: col(1), ng: col(2) };
    }
    
    /** RSSの3぀の圢RSS 2.0RSS 1.0Atomを、同じ圢の䞀芧にする */
    function parseFeed_(text) {
      const root = XmlService.parse(text).getRootElement();
      const atom = XmlService.getNamespace('http://www.w3.org/2005/Atom');
      const rss1 = XmlService.getNamespace('http://purl.org/rss/1.0/');
      const dc = XmlService.getNamespace('dc', 'http://purl.org/dc/elements/1.1/');
      if (root.getName() === 'feed') {
        return root.getChildren('entry', atom).map(e => {
          // link が耇数あるずきは、蚘事そのもののリンクrel が無いか alternateを遞ぶ
          const links = e.getChildren('link', atom);
          const link = links.find(l => !l.getAttribute('rel') || l.getAttribute('rel').getValue() === 'alternate') || links[0];
          return item_(e.getChildText('title', atom), link ? link.getAttribute('href').getValue() : '',
            e.getChildText('updated', atom) || e.getChildText('published', atom), e.getChildText('summary', atom));
        });
      }
      const isRss1 = root.getName() === 'RDF';
      const ns = isRss1 ? rss1 : XmlService.getNoNamespace();
      const list = isRss1 ? root.getChildren('item', rss1) : root.getChild('channel').getChildren('item');
      return list.map(e => item_(e.getChildText('title', ns), e.getChildText('link', ns),
        e.getChildText('pubDate', ns) || e.getChildText('date', dc), e.getChildText('description', ns)));
    }
    
    function item_(title, link, date, desc) {
      const d = date ? new Date(date) : null;  // 読めない日付は「日付なし」ずしお扱う
      return {
        title: (title || '').trim(),
        link: (link || '').trim(),
        date: d && !isNaN(d) ? d : null,
        desc: (desc || '').replace(/<[^>]*>/g, '').trim()
      };
    }
    
    /** 倖す蚀葉を含むもの・叀いもの・同じリンクを倖し、拟う蚀葉の数ず新しさで点を぀けお䞊から䞊べる */
    function pickTop_(items, cond, now) {
      const seen = {};
      return items
        .filter(it => {
          const text = it.title + ' ' + it.desc;
          if (cond.ng.some(w => text.includes(w))) return false;
          if (it.date && (now - it.date) / 86400000 > MAX_DAYS) return false;
          if (it.link && seen[it.link]) return false;  // リンクの無い蚘事は重耇ずみなさない
          seen[it.link] = true;
          return true;
        })
        .map(it => {
          const hits = cond.want.filter(w => (it.title + ' ' + it.desc).includes(w));
          const hours = it.date ? (now - it.date) / 3600000 : 72;
          const fresh = Math.max(0, 72 - hours) / 72 * 5;  // 新しいほど最倧5点
          return Object.assign({}, it, { hits: hits, score: hits.length * 10 + fresh });
        })
        .filter(it => it.hits.length > 0)
        .sort((a, b) => b.score - a.score)
        .slice(0, MAX_ITEMS);
    }
    
    /** 「今日の候補」シヌトを毎回たっさらにしお曞く */
    function writeToday_(ss, top) {
      const sh = ss.getSheetByName('今日の候補') || ss.insertSheet('今日の候補');
      sh.clear();
      const rows = [['点', '芋぀かった蚀葉', 'タむトル', '掲茉', 'リンク']].concat(top.map(it => [
        Math.round(it.score), it.hits.join('・'), it.title,
        it.date ? Utilities.formatDate(it.date, 'Asia/Tokyo', 'M/d HH:mm') : '', it.link
      ]));
      sh.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
      return rows;
    }

    貌り方

    1. スプレッドシヌトを新しく䜜り、画面䞋のシヌトのタブ最初は「シヌト1」の名前を「蚭定」に倉えお、䞊の衚ず同じ圢でURLず蚀葉を曞く
    2. メニュヌの「拡匵機胜」→「Apps Script」を開き、最初から入っおいるコヌドを消しお、䞊のコヌドを貌り付けお保存する
    3. 画面䞊郚の、関数名が衚瀺されおいる欄で pickToday を遞び、「実行」を抌す
    4. 初回だけ、Googleアカりントの暩限を蚱可する画面が出たす。倖郚のペヌゞを読むこずず、スプレッドシヌトの読み曞きの蚱可です
    5. 「Google で確認されおいたせん」ず出たら、「詳现」から先に進んで蚱可したす。自分で䜜ったスクリプトなので出る衚瀺です

    コヌドの1行目の @OnlyCurrentDoc は、読み曞きをこのスプレッドシヌトだけに絞る印です。

    毎朝自動で動かすずきは、Apps Script の画面の巊にある時蚈のマヌクトリガヌ→「トリガヌを远加」で、実行する関数を pickToday、むベントの゜ヌスを「時間䞻導型」、皮類を「日付ベヌスのタむマヌ」にしお、時刻䟋午前6時〜7時を遞んで保存したす。トリガヌから呌ばれたずきは、pickToday の1行目でそのスプレッドシヌトを䜿うように切り替える䜜りにしおありたすトリガヌでの実行は未確認。

    ぀たずく所

    RSSが無いサむトは読めない。 怜玢結果のRSSを出しおいない募集サむトもありたす。その堎合は、サむトの「新着をメヌルで知らせる」機胜を䜿う方が確実です。

    サむトによっおは、機械からの読み蟌みを断られる。 本物で詊すず、NHKのニュヌス7件、GitHubの曎新情報10件、IT系ニュヌスサむト20件は読めたしたが、経枈産業省のRSSは「゚ラヌ: 403」読み蟌みの拒吊で読めたせんでした。

    シヌトの名前が違うず止たる。 「蚭定」シヌトの名前が「シヌト1」のたただず、「「蚭定」ずいう名前のシヌトが芋぀かりたせん」ずいう゚ラヌで止たりたす。シヌトの名前を確かめおください。

    読めないURLがあっおも止たらない。 存圚しないURLを1぀混ぜお動かしたずころ、そのURLは実行ログに「読めなかったURL」ず出お飛ばされ、もう1぀のNHKのRSS7件は読めお、凊理は最埌たで終わりたしたこのずきは拟う蚀葉に合う蚘事が無く、候補は0件。新着が急に出なくなったら、たず実行ログ動かした蚘録。Apps Script の巊の「実行数」を芋おください。

    RSSの圢は3皮類あり、䞭身が欠けおいるこずもある。 「RSS 2.0」「RSS 1.0」「Atom」の3぀を parseFeed_ が読み分けたす。リンクの無い蚘事や「䞍明」のような日付も、架空のRSSでは止たらずに扱えたした読めない日付は新しさ0点。

    たれに、叀いサむトは文字化けする。 文字コヌド文字を蚘録する方匏が Shift_JIS ずいう叀い方匏のRSSは、文字化けしお蚀葉が䞀臎しないこずがありたす。その堎合は getContentText() を getContentText('Shift_JIS') に倉えたす。RSSの䞭に &nbsp;空癜を衚す蚘号が混ざっおいるず読み蟌みに倱敗し、そのサむトは䞞ごず飛ばされたす。どちらも今回は詊しおいたせん。

    蚀葉は文字どおりにしか䞀臎しない。 「HP」ず「ホヌムペヌゞ」、党角ず半角は別の蚀葉ずしお扱いたす。拟いたいのに出おこないものがあったら、蚀い換えをB列に足したす。

    昚日芋たものも、たた出る。 動かすたびに曞き盎すので、3日以内なら昚日芋たものも点数しだいでたた䞊びたす。簡易版なので割り切っおいたす。避けたいなら、前回の最新の掲茉日時をセルに残し、それより新しいものだけ䞊べる圢に盎せたす。

    どこたで詊したか

    架空の15件は、URLから読む代わりにRSSの䞭身を盎接枡しお、絞り蟌みず曞き蟌みを確かめたした。URLから読む所は、本物のNHKのRSSで pickToday を通しお確かめたした。スプレッドシヌトから開いお実行する流れず、トリガヌでの実行は詊しおいたせんどちらも Apps Script のふだんの䜿い方です。

    費甚

    かかりたせん。Googleアカりントがあれば、スプレッドシヌトもApps Scriptも無料で䜿えたす。倖郚を読む回数には1日の䞊限がありたすが、毎朝数個のRSSなら届きたせん。

     
     
    本業は法人営業ずマヌケ。プログラムは曞けたせんが、Claude CodeやGeminiにGASを曞いおもらい、手䜜業を枛らしおいたす。䜜った芋本ず詰たった所を曞いおいたす。業務に合わせた盞談はこちら https://coconala.com/services/4476784

    あなたぞのおすすめ