新しい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データをパースして展開する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
//トリガー用実行関数 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データとして出力する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
//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じゃないよ
コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
<!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をよしなに扱う方法