💥

RSSをJSONに変換して返すだけ!(CloudFlare Wokers KV/ Hono)

2024/09/10に公開

RSSを取得してJSONにして返すカンタンな仕組みが欲しかったので、作ってみました。

CloudFlare Workers + Honoでは、DOMパーサーもxml2jsもうまく動かなかったので、RSSの内容を正規表現で抽出する環境に依存しない機能を作成しました。(ほとんどAIが作ってくれた。)
また、リクエストの度にRSSをJSONに整形して返していると、処理に時間がかかるので、CloudFlare KVに整形したデータを保存して、リクエストにはこちらを返すようなプログラムも実装されています。
キャッシュされたデータは有効期限をつけることで、時間毎にRSS内容を取得・キャッシュの更新してくれます。注意点として、有効期限が切れた後、最初のリクエストはレスポンスに時間がかかります。

参考

Hono + Cloudflare Workersでいい感じにpost cacheする

RSSの内容を正規表現で解析する

RSSの解析は、ほとんど形式が決まっています。<items>タグをグローバルサーチして、その中身を取得します。
中身のデータは扱いやすい、string型で取得します。

// RSS パーサー
type RSSItem = {
  title: string;
  link: string;
  pubDate: string;
}

async function parseRSStoJson(text:string): Promise<string> {
  const items:RSSItem[] = [];
  // <items>の検索・正規表現
  const itemsRegex = /<item>([\s\S]*?)<\/item>/g;
  let match;
  while((match = itemsRegex.exec(text)) !== null) {
    const itemContent = match[1];
    const titleMatch = itemContent.match(/<title>([\s\S]*?)<\/title>/);
    const linkMatch = itemContent.match(/<link>([\s\S]*?)<\/link>/);
    const pubDateMatch = itemContent.match(/<pubDate>([\s\S]*?)<\/pubDate>/);

    // CDATAセクションを削除
    let title = titleMatch ? titleMatch[1].trim() : '';
    title = title.replace(/<!\[CDATA\[(.*?)\]\]>/, '$1');

    // HTTPヘッダー形式の文字列をDate型に変換して、更にDate型をYYYY/MM/dd に変換後にstring型に戻しています。
    let pubDate = pubDateMatch ? pubDateMatch[1].trim() : '';
    const date = new Date(pubDate);
    pubDate = `${date.getFullYear()}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`;

    items.push({
      title,
      link: linkMatch ? linkMatch[1].trim() : '',
      pubDate,
    });
  }
  return JSON.stringify(items);
};

CloudFlare KVにデータをキャッシュする。

KVに保存するデータは、URL data metadata(キャッシュの有効期限)となっています。

type Bindings = {
  MY_KV_NAMESPACE: KVNamespace
}

interface CacheMetadata {
  expiresAt: number;
}

interface CacheResult {
  cache: string | null;
  isExpired: boolean;
}

//RSSの更新はそんな頻繁には、行われないので更新頻度の多い人でも一日~半日、少ない人は半月以上に設定してつかってください
const ttl:number = 10 * 1000; // 10s TEST用

const createCache = async(kv: KVNamespace, pathname: string, ttl: number):Promise<string> => {
  const res = await fetch(pathname);
  const rss = await res.text();
  const data = await parseRSStoJson(rss);
  const expiresAt = Date.now() + ttl;
  await kv.put(pathname, data, {
    metadata: {
      expiresAt,
    },
  });
  console.log("Create Cache!" );
  return data;
}

const getCache = async(kv: KVNamespace, pathname: string):Promise<CacheResult> => {
  const { value, metadata } = await kv.getWithMetadata<CacheMetadata>(pathname);
  console.log("現在時刻:" + Date.now(), "キャッシュの有効期限:" + (metadata?.expiresAt ?? 'unknown'));

  // pathnameが一致しない、またはmetaDataが存在しない場合
  if (!value || !metadata) {
    return {
      cache: null,
      isExpired: true,
    };
  }

  return {
    cache: value,
    isExpired: metadata.expiresAt < Date.now(),
  };
};

const app = new Hono<{ Bindings: Bindings }>()

app.use('*', logger())

app.get("/", async (c) => {
  const pathname:string = 'https://zenn.dev/masterak/feed';
  const { cache, isExpired } = await getCache(c.env.MY_KV_NAMESPACE, pathname);
  
  //isExpiredがtrueの場合
  if (isExpired) {
    const res = await createCache(c.env.MY_KV_NAMESPACE, pathname, ttl);
    return c.html(res);
  }

  //cacheが存在し、isExpiredがfalseの場合
  if (cache && !isExpired) {
    return c.html(cache);
  }
});

export default app;

最初のアクセスは時間がかかるので、キャッシュの更新はCronで行い、リクエストにはキャッシュのデータだけを返すだけという仕組みにも出来ます

Demo

デモサイト4

Github

https://github.com/masterak-902/hsweb-portfolio-5/tree/main/src

GitHubで編集を提案

Discussion