新しいGoogle SitesにネイティブなRSSリーダーを作ってみる【GAS】

Google Apps Scriptでは、HTML Serviceを利用して様々なウェブアプリを作成でき、新しいGoogle Sitesに貼り付けるだけでなく、XFrameOptionsModeを使う事で、外部のWordpressのサイトにも貼り付けが可能です。当サイトでもRSSリーダーを作成し、貼り付けて情報収集の一助に役立てています。

但し、この貼り付け実は注意が必要です。というのも、同じ組織内で使う分には問題がないのですが、外部貼り付けや外部向け公開の場合、アプリの上部に「このアプリケーションはGoogleではなく、別のユーザによって作成されたものです」というメッセージが出てしまう。しかし、作った当人には表示されないので、気が付かずに貼り付けてしまうケースが多いです。実用上問題はないのですが、いかんせん格好悪い。

そこで今回はこれを解消しつつ、新しいGoogle Sitesにネイティブで使えるRSSリーダーを作ってみました。外部向けサイトではこの手法で作成することをオススメします。

図:GASのみだと上記のようなメッセージが出てしまい具合が悪い

今回使用するスプレッドシート他

今回はスプレッドシート側はJSONデータの配信だけで、実際にリーダーを作るのは、Google Site上で直接記述します。今回はサンプルとして、teratailのRSSを取得しJSONで配信してるものを、Site上に記述したアプリで整形しています。

更新履歴

  • 2019/11/29 - FC2 BlogのRSSフィードに対応, トリガー用の関数を用意
  • 2019/11/28 - 最初のバージョンを公開

事前準備

RSSの準備と設定

RSSはいろいろなバージョンがあります。またバージョンは同じであっても、格納されてるデータのelement名が異なっていたりする事がままあったりして、実の所統一されていたりするわけではないケースがあります。そのため、対象のRSSによってはソースコードの修正が必要な事があります。

今回はteratailのRSSフィードを利用しています。このフィードはRSS1.0のフィードであるため、非常に単純なものです。本アプリはRSS2.0Atom(と、各サイト独自仕様のscheme)、Google News、FC2 Blogの5種類に対応しています。

設定の手順は以下の通りです。

  1. スプレッドシートを開き、メニューよりセットアップ⇒スタートを実行(スプレッドシートのURLがスクリプトプロパティにセットされます)。
  2. 同じく、管理設定を実行。ここでは、URLの入力RSSタイプの選択、Atomの場合はAtom2.0設定を選び、設定保存をクリック。
  3. 同じく、トリガー設置を実行した場合は、毎日夜中の1時〜2時にRSSから自動でデータを取得してスプレッドシートに反映するトリガーが設置されます。もう一回実行すると再設置もしくはトリガーの除去が可能です。
  4. 手動で取得を実施するとその場でRSSからデータを受信してスプレッドシートに展開してくれます。

図:初期設定は必ず実行が必要です。

ウェブアプリケーションとして公開

このままでは、データを公開する事ができません。必ず以下のウェブアプリケーションとして公開する手順を踏んで、JSONデータが出力できている事を確認しましょう。

  1. スクリプトエディタのメニューより、「公開」⇒「ウェブアプリケーションとして公開
  2. 次のユーザとしてアプリケーションを実行で誰の権限で動かすかを指定する。自分かアクセスしてるユーザの二択。後者の場合、ユーザはGoogleアカウントを持ってる必要があります。外部向けは自分の権限にしておきましょう(アカウント持っていない人でも実行出来る)
  3. アプリケーションにアクセスできるユーザを指定する。自分のみ、全員、全員(匿名含む)の三択。但し、全員の場合はGoogleアカウントが必要で、匿名含むの場合は、Googleアカウントなしでアクセス可能ですので、外部向けは匿名ユーザにしておきましょう。。
  4. 最後に導入すると、ウェブアプリケーションのURLが取得できます。このURLでアクセスをします。URLの最後がexecが本番用、devがテスト用で、テスト用は最新のコードをテストのリンクを踏むと表示されますが、変更したコードがそのまますぐに反映されてしまうので、テスト用のURLで運用しないように。

このように公開したURLにアクセスすると、スプレッドシートのデータがJSON形式で出力されているはずです。このJSONデータをSite側コードで受け取って整形する事になります。

ソースコード

GAS側コード

今回は、JSON出力するコード、RSSデータを取得してスプレッドシートに展開するコードの2つに絞って掲載しています。

RSSデータをパースして展開する

//トリガー用実行関数
function rssdatatrigger(){
  getrssdata(-1);
}

//指定のRSSデータを取得してスプレッドシートに反映する
function getrssdata(flg){
  var Prop = PropertiesService.getScriptProperties(); 
  
  //オプション値を取得する
  var rssurl = Prop.getProperty("rssurl");
  var rsstype = Number(Prop.getProperty("rsstype"));
  var rssscheme = Prop.getProperty("scheme");

  //RSS Typeで処理を分岐
  switch(rsstype){
      case 1: 
        var contents = getRSS10(rssurl);
        break;
      case 2:
        var contents = getRSS20(rssurl);
        break;
      case 3: //atom
        var contents = parseXml(rssurl,rssscheme);
        break;
      case 4: //google news
        var contents = parseGNews(rssurl);
        break;
      case 5: //fc2 blog
        var contents = fc2feed(rssurl);
        break;
  }

  //contentsのtitle, url, dateをスプレッドシートに貼り付ける
  var ssid = Prop.getProperty("mysheetid");
  var sheet = SpreadsheetApp.openById(ssid).getSheetByName("data");
  
  //シートをクリアする
  sheet.getRange("A2:D").clearContent();
  
  //データを貼り付ける
  var lastColumn = contents[0].length;  //カラムの数を取得する
  var lastRow = contents.length;      //行の数を取得する
  sheet.getRange(2,1,lastRow,lastColumn).setValues(contents); 
  
  //flgにより処理を分岐
  if(flg == -1){
  }else{
    //メッセージを表示
    var ui = SpreadsheetApp.getUi();
    ui.alert("手動でデータを取得しました。");
    return;
  }
}

function getRSS10(feedURL) {
  var rssdata = [];
  var response = UrlFetchApp.fetch(feedURL);
  var xml = Xml.parse(response.getContentText(), false);
  var items = xml.getElement().getElement("channel").getElements("item");
  for(var i = 0; i < items.length; i++) {
    var title = items[i].getElement("title").getText();
    var url = items[i].getElement("link").getText();
    var dateman = items[i].getElement("pubDate").getText();
    var detail = items[i].getElement("description").getText();
    var text = title + ' ' + url;

    //itemlistを生成する
    var temprssdata = [];
    temprssdata.push(title);
    temprssdata.push(detail);
    temprssdata.push(url);
    temprssdata.push(dateman);
    
    //rssdataにpushする
    rssdata.push(temprssdata);
    
  }
  
  //値を返す
  return rssdata;
  
}

//RSS2.0形式の場合
function getRSS20(feedURL){
  var rssdata = [];
  var response = UrlFetchApp.fetch(feedURL);
  var xml = Xml.parse(response.getContentText(), false);
  var items = xml.getElement().getElement("channel").getElements("item");
  for(var i = 0; i < items.length; i++) {
    var title = items[i].getElement("title").getText();
    var url = items[i].getElement("link").getText();
    var dateman = items[i].getElement("pubDate").getText();
    var detail = items[i].getElement("description").getText();
    var text = title + ' ' + url;

    //itemlistを生成する
    var temprssdata = [];
    temprssdata.push(title);
    temprssdata.push(detail);
    temprssdata.push(url);
    temprssdata.push(dateman);
    
    //rssdataにpushする
    rssdata.push(temprssdata);
  }
  
  //値を返す
  return rssdata;
}

//FC2Blogの場合
function fc2feed(feedURL){
  //XMLをパースする
  var xml = UrlFetchApp.fetch(feedURL).getContentText();
  var document = XmlService.parse(xml);
  var root = document.getRootElement();
  
  //namespaceを取得する
  var rss = XmlService.getNamespace('http://purl.org/rss/1.0/');
  var dc = XmlService.getNamespace('dc', 'http://purl.org/dc/elements/1.1/');
  
  //itemを取得する
  var items = root.getChildren('item', rss);
  
  //返却用の配列を用意する
  var rssdata = [];
  
  //ループでitemをさらっていく
  for (var i = 0; i < items.length; i++) {
    //各要素を取得する
    var title = items[i].getChild('title', rss).getText();
    var link = items[i].getChild('link', rss).getText();
    var dateman = items[i].getChild('date', dc).getText();
    var detail = items[i].getChild('description', rss).getText();
    
    //detailの文字数を削る
    detail = detail.slice(0,30);
  
    //itemlistを生成する
    var temprssdata = [];
    temprssdata.push(title);
    temprssdata.push(detail);
    temprssdata.push(link);
    temprssdata.push(dateman);
    
    //rssdataにpushする
    rssdata.push(temprssdata);

  }

  //値を返す
  return rssdata;
  
}

// Atom2.0データ取得とパース
function parseXml(url,num) {
  var myXml = UrlFetchApp.fetch(url).getContentText();
  var myDoc = XmlService.parse(myXml);
  var root = myDoc.getRootElement();
  var atom = XmlService.getNamespace('http://www.w3.org/2005/Atom');
  var rssdata = [];
  
  // エントリーのタイトルとURLの塊を取得
  var entries = root.getChildren('entry', atom);

  //HTMLを生成する
  var dataLength = entries.length;

  for(var i = 0;i<dataLength;i++){
    //コンテンツデータを分解する
    var title = entries[i].getChild('title', atom).getText();
    var id = entries[i].getChild('id', atom).getText();
    var tempid = id.split("=");
    var dateman = "";
    var detail = "";
    
    //titleにタグのようなものがあった場合に、エスケープ文字にreplaceする
    title = title.replace("<","<");
    title = title.replace(">",">");

    //Quiita対応の為に、-1の時はurlを取得する
    switch(Number(num)){
      case 1:
        //Qiita対応
        url = entries[i].getChild('url', atom).getText();
        dateman = entries[i].getChild('published', atom).getText();
        detail = entries[i].getChild('content', atom).getText();
        
        //detailの文字数を削る
        detail = detail.slice(0,100);
        
        break;
      case 2:
        //gsuiteupdateblog対応
        var multi = entries[i].getChildren("link", atom);
        url = multi[2].getAttribute('href').getValue();
        dateman = entries[i].getChild('published', atom).getText();
        detail = entries[i].getChild('content', atom).getText();
        break;
      case 3:
        //StackOverFlow対応
        url = entries[i].getChild('link', atom).getAttribute('href').getValue();
        dateman = entries[i].getChild('published', atom).getText();
        detail = entries[i].getChild('summary', atom).getText();
        break;
      case 4:
        //gsuiteupdateblog対応
        var multi = entries[i].getChildren("link", atom);
        url = multi[4].getAttribute('href').getValue();
        dateman = entries[i].getChild('published', atom).getText();
        detail = entries[i].getChild('content', atom).getText();
        break;     
      default:
        url = tempid[num]; 
        break;
    }
    
    //itemlistを生成する
    var temprssdata = [];
    temprssdata.push(title);
    temprssdata.push(detail);
    temprssdata.push(url);
    temprssdata.push(dateman);
    
    //rssdataにpushする
    rssdata.push(temprssdata);
  }
  
  //値を返す
  return rssdata;
}

// Google Newsをパースする
function parseGNews(url) {
  var myXml = UrlFetchApp.fetch(url).getContentText();
  var myDoc = XmlService.parse(myXml);
  var root = myDoc.getRootElement();
  var atom = XmlService.getNamespace('http://www.w3.org/2005/Atom');
  var rssdata = [];
  
  var obj = [];
  
  // エントリーのタイトルとURLの塊を取得
  var entries = root.getChildren('entry', atom);

  //HTMLを生成する
  var dataLength = entries.length;

  for(var i = 0;i<dataLength;i++){
    //コンテンツデータを分解する
    var title = entries[i].getChild('title', atom).getText();
    var url = entries[i].getChild('id', atom).getText();
    var dateman = entries[i].getChild('updated', atom).getText();
    var detail = entries[i].getChild('content', atom).getText();

    //itemlistを生成する
    var temprssdata = [];
    temprssdata.push(title);
    temprssdata.push(detail);
    temprssdata.push(url);
    temprssdata.push(dateman);

    //rssdataにpushする
    rssdata.push(temprssdata);
  }

  //値を返す
  return rssdata;
}
  • このコードそのものは、RSSリーダーのそれとほとんど変わりません。

JSONデータとして出力する

//JSONデータをWeb APIとして配信する
function doGet(e){
  //シートデータをJSON化
  var json = backjson();
  
  //JSONデータを返す
  return ContentService.createTextOutput(JSON.stringify(json, null, 2))
  .setMimeType(ContentService.MimeType.JSON);
}

//JSONでデータを返す
function backjson() {
  //スプレッドシートデータを取得する
  var Properties = PropertiesService.getScriptProperties(); 
  var id = Properties.getProperty("mysheetid");
  var sheet = SpreadsheetApp.openById(id).getSheetByName("data");
  var ss = sheet.getDataRange().getValues();
  
  //タイトル行を取得する
  var title = ss.splice(0, 1)[0];
  
  //JSONデータを生成する
  return ss.map(function(row) {
    var json = {}
    row.map(function(item, index) {
      json[title[index]] = item;
    });
    return json;
  });
}
  • HTML ServiceではなくContentServiceでテキストデータを出力します。
  • このコードそのものは、スプレッドシートのデータをJSON出力するコードと変わりません。

Site側コード

コードを埋め込む手順

新しいGoogle SiteはGoogle Apps Scriptのガジェットだけでなく、直接JavaScriptで記述が出来るようになっています。外部のファイルのロードも可能で、ここで直接アプリを書くことが可能ですが、エディタ画面がしょぼすぎるので、Atomなどのテキストエディタ上で開発をするようにしましょう。コードを埋め込む手順は以下の通り。

  1. Google Siteの編集画面の右サイドパネルの「埋め込む」をクリック
  2. 埋め込みコードをクリックし、開発したコードをコピペ、挿入ボタンをクリック
  3. 公開ボタンをクリックして公開する。
  4. 公開URLを開いてみる
  5. 画面が出てくるようならば成功。

画面が出てこない場合、コードが間違っているか、Google Apps Scriptの出力側が組織内ユーザに限定してるケースがあります。この場合、匿名ユーザがアクセスできない状態なので、設定変更が必要です。

図:今回はURLじゃないよ

コード

<!DOCTYPE html>
<html>
  <head>
  	<style>
      /*message box chethstudios */
      A:link {
        color: #0404B4;
        text-decoration: none
      }   /* リンク */
      A:visited {                 /* 既に見たリンク */
        color: #0B3B0B;
        text-decoration: none     /* 下線を消す */
      }
      A:active { color: #00ff00 } /* クリック時のリンク */
      A:hover {                   /* カーソルが上にある時のリンク */
        color: #ff0000;
        text-decoration: none     /* 下線を消す */
      }

      /*DIVBOXの書式設定*/
      .kousin {
        overflow-y: auto;
        width:98%; height:400px;
        padding:3px;
        border:2px dotted #ffffff;
        color:#000000;
        background-color:#ffffff;
        line-height:1.5em;
        margin-left: auto;
        margin-right: auto;
      }

      .box {
        float: left;
        width: 40%;
        margin-right:5px;
      }

      .box2 {
        float: left;
        width: 50%;
        text-align: left;
      }

      .box3 {
        float: left;
        width: 10%;
        text-align: right;
      }

      .boxContainer {
        overflow: hidden;
        width:100%;
      }

      /* clearfix */
      .boxContainer:before,
      .boxContainer:after {
        content: "";
        display: table;
      }

      .boxContainer:after {
        clear: both;
      }

      /* For IE 6/7 (trigger hasLayout) */
      .boxContainer {
        zoom: 1;
      }

      /*部署ごとのアイコン表示設定*/
      .recman{
        border: 1px solid
        margin: 10px 0px;
        padding:15px 10px 15px 50px;
        background-repeat: no-repeat;
        background-position: 10px center;
        font-size:10pt;
        border-top-style: dotted;
        border-width: thin;
        color: #2E2D2D;
        background-color: #FFFFFF; -moz-border-radius-topleft: 18px; -moz-border-radius-topright: 18px; -moz-border-radius-bottomright: 18px; -moz-border-radius-bottomleft: 18px;
      }
  	</style>
    <script>
    	//初期化
    	callman();

      //外部の連絡情報のJSONデータをロードする
    	function callman(){
            httpObj = new XMLHttpRequest();
      	httpObj.open("get", "https://script.google.com/macros/s/AKfycbzmSK0CP1GQAFpzBxtuXZJylIALXU-_iVAfKLcv5xygLg6jWVw/exec", true);
      	httpObj.onload = function(){
          //データを受信する
          var myData = JSON.parse(this.responseText);

          //データの件数
          var numRows = myData.length;

          //カウンタを用意
          var i = Number(numRows) - 1;

          //反映先のノード
          var node = document.getElementById("renraku_div");

          //デフォルトのclass
          var classman = "recman";

          //受信データからHTMLを作成し格納する(最新記事を上にする)
          var html = "";
          while (i > -1) {
            //日付を整形して格納
            var date = new Date(myData[i].date);
            var year = date.getFullYear();
            var month = date.getMonth() + 1;
            var date = date.getDate();
            var strDate =  year + "年" + month + "月" + date + "日";

            //タイトルを取得
            var title = myData[i].title;

            //detailを取得
            var detail = myData[i].detail;

            //リンクを取得
            var url = myData[i].link;

            //HTMLを生成して格納する
            html += "<div class='" + classman + "'><div class='boxContainer'>";
            html += "<div class='box'><b><a href='" + url + "' target='_blank'>" + title + "</a></b></div>";
            html += "<div class='box2'><b>" + detail + "</b></div>";
            html += "<div class='box3'><b>掲載日:" + strDate + "</b></div>";
            html += "</div></div>";

            //カウンタをマイナス
            i = i - 1;

          }

          //生成したHTMLを置き換え
          node.innerHTML = html;

        }

        //httpリクエスト送信
     		httpObj.send(null);
    	}
    </script>
  </head>
  <body>
    <div class="kousin">
            <div id="renraku_div"></div>
        </div>
  </body>
</html>
  • デザインは社内で使っていた連絡掲示板のCSSを使っています。
  • callmanという関数がJSONを取得して整形するメインルーチンです。
  • データは一番最後のものから整形しないと、最新場一番下になってしまうので、逆順から整形しています。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)