Yoki(@enyyokii)と申します。
今回はRSS記事を取得し、FireStoreに定期的に格納していくコードを書いてみました。(動作確認済み)
環境
使用言語:Typescript
主な使用ライブラリ:rss-parser
やること
- RSSから記事情報を取得し、Firestoreに格納する
- 新しい記事がある場合のみ追加する
- 12時間ごとに定期実行する
Firestoreの構造
FireStoreのデータ構造は下記のようになっています。
- Articles
- 記事情報一覧(全記事一覧の意。RSSから取得した記事のみならず、それ以外から取得した記事も格納される。)
- RSS
- 各RSSに分け、それぞれで取得してきた記事が格納される
├── Articles(コレクション)
│ ├── 111111(自動生成ID)(ドキュメント)
│ ├── title: "タイトルです"
│ ├── imgUrl: "https://image.png"
│ ├── summary: "aaaaaaaa"
│ ├── url: "https://urlurl"
│ ├── date: 2019年1月1日 11:11 UTC+9
│ └── category: ["カテゴリ名"]
│ └── 222222
│ └── (省略)
│
├── RSS(コレクション)
│ ├── CNET(ドキュメント)
│ └── Items(サブコレクション)
│ ├── 333333(自動生成ID)(ドキュメント)
│ ├── title: "タイトルです"
│ ├── imgUrl: "https://image.png"
│ ├── summary: "aaaaaaaa"
│ ├── url: "https://urlurl"
│ ├── date: 2019年1月1日 11:11 UTC+9
│ └── category: ["カテゴリ名"]
│ └── 444444(自動生成ID)(ドキュメント)
│ └── (省略)
│ └── Hatena
│ └── (省略)
記事取得
パーサーとしてrss-parserを利用しています。
処理の流れとしては、
RSSより記事取得
→ 最新の記事を取得
→ FireStoreのRSSに格納されている最新の記事取得
→ FireStoreに最新の記事がなければRSSのItemsに書き込み処理
→ Articlesコレクションにも追加
となっています。
const fetchColumn = async (rssName: string, urlString: string) => {
const items: rssParser.Item[] = [];
const parser = new rssParser();
const feed = await parser.parseURL(urlString);
if (feed && feed.items) {
feed.items.forEach(item => {
items.push(item)
});
}
const firstItem = items[0]
const postData = postToFireStoreData(firstItem);
const itemsRef = admin
.firestore()
.collection("Rss")
.doc(`${rssName}`)
.collection("Items");
const querySnapShot = await itemsRef
.orderBy("date", "desc")
.limit(1)
.get()
.catch((error: Error) => {
console.log("エラー アイテム取得: ", error);
});
const latestItem: any | null = querySnapShot ? querySnapShot.docs[0] : null
const latestUrl = latestItem ? latestItem.data().url : "";
if (String(latestUrl) !== firstItem.link) {
await itemsRef
.add(postData)
.catch(error => {
console.log("エラー Document書き込み:", error);
});
// Articlesにデータを追加
await addArticle(postData)
}
};
スケジュール設定について
スケジュールの設定方法はcronのスケジュール設定方式に従います。
以下のような設定が可能です。
種類 | 例 | |
---|---|---|
繰り返し | 毎日 | every 5 mins(5分毎に)、every 12 hours(12時間毎)、every 1 hours from 08:00 to 16:00(毎日 08:00~16:00 に 1 時間ごとに) |
特定の日時 | 1, 15 of month 09:00(1日と15日の9時に) 日付の他にも曜日、月を指定することも可能 |
定期実行の処理は以下のように簡単にかけます!
exports.fetchData = functions.pubsub
.schedule("every 12 hours")
.onRun(async context => {
// 定期実行の処理
return 0
});
return 0
としていますが、これはint
を返さないと
Function returned undefined, expected Promise or value
という警告が出るのを回避するためです。
参考:Cloud Functions for FirebaseでPubSubをトリガしてみる
ソースコード全体
index.js
import * as functions from "firebase-functions";
import * as rss from "./rss";
import * as admin from "firebase-admin";
import * as rssItem from "./Constant/rssItem";
admin.initializeApp();
exports.fetchData = functions.pubsub
.schedule("every 12 hours")
.onRun(async context => {
await rss.fetchColumn(rssItem.cnet[0], rssItem.cnet[1]);
await rss.fetchColumn(rssItem.cnn[0], rssItem.cnn[1]);
await rss.fetchColumn(rssItem.gigazine[0], rssItem.gigazine[1]);
await rss.fetchColumn(rssItem.hatena[0], rssItem.hatena[1]);
await rss.fetchColumn(rssItem.huffpost[0], rssItem.huffpost[1]);
await rss.fetchColumn(rssItem.lifehacker[0], rssItem.lifehacker[1]);
await rss.fetchColumn(rssItem.netlab[0], rssItem.netlab[1]);
await rss.fetchColumn(rssItem.techCrunch[0], rssItem.techCrunch[1]);
await rss.fetchColumn(rssItem.toyoKeizai[0], rssItem.toyoKeizai[1]);
await rss.fetchColumn(rssItem.wired[0], rssItem.wired[1]);
// intを返さないと警告が出る https://qiita.com/bathtimefish/items/2ffc5ab6c6db8e59eb66
return 0
});
rss.ts
import admin = require("firebase-admin");
import rssParser = require("rss-parser");
/**
* パースしたアイテムをfirestoreで保存するデータに変換
* @param parsedItem パースしたアイテム
*/
const postToFireStoreData = (parsedItem: rssParser.Item): {} => {
let imageUrl = ""
// 正規表現でsrc内のurlを取得
if (parsedItem.content) {
const res = parsedItem.content.match("<img.*src\s*=\s*[\"|\'](.*?)[\"|\'].*>")
imageUrl = res ? res[1] : ""
}
return {
title: parsedItem.title || "",
summary: parsedItem.contentSnippet || "",
url: parsedItem.link || "",
date: parsedItem.isoDate ? new Date(parsedItem.isoDate) : "" ,
imgUrl: imageUrl,
category: parsedItem.categories || ""
};
};
const addArticle = async (articleData: {}) => {
const itemsRef = admin.firestore().collection("Articles");
await itemsRef.add(articleData).catch((error) => {
console.error("エラー Article書き込み:", error);
})
};
const fetchColumn = async (rssName: string, urlString: string) => {
const items: rssParser.Item[] = [];
const parser = new rssParser();
const feed = await parser.parseURL(urlString);
if (feed && feed.items) {
feed.items.forEach(item => {
items.push(item)
});
}
const firstItem = items[0]
const postData = postToFireStoreData(firstItem);
const itemsRef = admin
.firestore()
.collection("Rss")
.doc(`${rssName}`)
.collection("Items");
const querySnapShot = await itemsRef
.orderBy("date", "desc")
.limit(1)
.get()
.catch((error: Error) => {
console.log("エラー アイテム取得: ", error);
});
const latestItem: any | null = querySnapShot ? querySnapShot.docs[0] : null
const latestUrl = latestItem ? latestItem.data().url : "";
if (String(latestUrl) !== firstItem.link) {
await itemsRef
.add(postData)
.catch(error => {
console.log("エラー Document書き込み:", error);
});
// Articlesにデータを追加
await addArticle(postData)
}
};
export { fetchColumn };
Constant/rssItem.ts の例
// RSS情報
// はてな:総合
const hatena: [string, string] = [
"Hatena",
`http://feeds.feedburner.com/hatena/b/hotentry`
];
// ねとらぼ:今気になる・人に話したい旬のネタをお届けするネットニュース
const netlab: [string, string] = [
"Netlab",
`https://rss.itmedia.co.jp/rss/2.0/netlab.xml`
];
export {
hatena,
netlab
};