新しいGoogle SitesにネイティブなRSSリーダーを作ってみる【GAS】
Google Apps Scriptでは、HTML Serviceを利用して様々なウェブアプリを作成でき、新しいGoogle Sitesに貼り付けるだけでなく、XFrameOptionsModeを使う事で、外部のWordpressのサイトにも貼り付けが可能です。当サイトでもRSSリーダーを作成し、貼り付けて情報収集の一助に役立てています。
但し、この貼り付け実は注意が必要です。というのも、同じ組織内で使う分には問題がないのですが、外部貼り付けや外部向け公開の場合、アプリの上部に「このアプリケーションはGoogleではなく、別のユーザによって作成されたものです」というメッセージが出てしまう。しかし、作った当人には表示されないので、気が付かずに貼り付けてしまうケースが多いです。実用上問題はないのですが、いかんせん格好悪い。
そこで今回はこれを解消しつつ、新しいGoogle Sitesにネイティブで使えるRSSリーダーを作ってみました。外部向けサイトではこの手法で作成することをオススメします。
図:GASのみだと上記のようなメッセージが出てしまい具合が悪い
目次
今回使用するスプレッドシート他
- Site用履歴配信 - RSSデータを取得しJSONで配信するスプレッドシート
今回はスプレッドシート側は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.0、Atom(と、各サイト独自仕様のscheme)、Google News、FC2 Blogの5種類に対応しています。
設定の手順は以下の通りです。
- スプレッドシートを開き、メニューよりセットアップ⇒スタートを実行(スプレッドシートのURLがスクリプトプロパティにセットされます)。
- 同じく、管理設定を実行。ここでは、URLの入力、RSSタイプの選択、Atomの場合はAtom2.0設定を選び、設定保存をクリック。
- 同じく、トリガー設置を実行した場合は、毎日夜中の1時〜2時にRSSから自動でデータを取得してスプレッドシートに反映するトリガーが設置されます。もう一回実行すると再設置もしくはトリガーの除去が可能です。
- 手動で取得を実施するとその場でRSSからデータを受信してスプレッドシートに展開してくれます。
図:初期設定は必ず実行が必要です。
ウェブアプリケーションとして公開
このままでは、データを公開する事ができません。必ず以下のウェブアプリケーションとして公開する手順を踏んで、JSONデータが出力できている事を確認しましょう。
このように公開した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などのテキストエディタ上で開発をするようにしましょう。コードを埋め込む手順は以下の通り。
- Google Siteの編集画面の右サイドパネルの「埋め込む」をクリック
- 埋め込みコードをクリックし、開発したコードをコピペ、挿入ボタンをクリック
- 公開ボタンをクリックして公開する。
- 公開URLを開いてみる
- 画面が出てくるようならば成功。
画面が出てこない場合、コードが間違っているか、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を取得して整形するメインルーチンです。
- データは一番最後のものから整形しないと、最新場一番下になってしまうので、逆順から整形しています。
関連リンク
- スプレッドシートのデータをJSONで取得する
- GASのWebアプリケーションでハマったこと
- WEBサーバーからGoogleスプレッドシート更新時のAccess-Control-Allow-Originエラーに関して
- CORSまとめ
- Google Apps Script cross-domain requests stopped working
- 今から10分ではじめる Google Apps Script(GAS) で Web API公開
- Google Apps Script Content Service - Cross Domain Puzzle
- HTML Serviceで作ったWebアプリの上部に表示されるバナーについて
- GoogleAppsScriptではてブのRSSをパースする
- Google Apps ScriptでXMLをよしなに扱う方法