14
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Firebase, Cloud Functions, TypeScript】定期実行を利用してRSSからデータを取得しよう!

Last updated at Posted at 2019-07-29

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時に)
日付の他にも曜日、月を指定することも可能

参考:cron.yaml リファレンス

定期実行の処理は以下のように簡単にかけます!

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
};
14
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?