新しい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を取得して整形するメインルーチンです。
  • データは一番最後のものから整形しないと、最新場一番下になってしまうので、逆順から整形しています。

関連リンク

コメントを残す

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

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