背景
旧東京工業大学のウェブサイトには便利なRSS機能があるが、東京科学大学の新しいサイトにはその機能がなくなっている。機能を補完するためにさまざまな実現方法を調査した結果、最終的にRSSHubを使用することに決定した。
RSSHubについて
簡単に言うと、RSSHubはウェブサイトからRSSリンクを生成するアプリである。既に多くのルーターが作成されているため、そのまま利用しても便利である。また、オープンソースプロジェクトであるため、自分でカスタムルーターを作成することも可能である。今回はオフィシャルパンフレットに従ってルーターを作ってみた。
環境構築
普通に、Node.jsをインストールして、pnpm iで構築できる。(ここでpnpmがなければ、先にnpm install pnpm)
Namespaceの作成
オフィシャルパンフレットに従って、簡単にできると思う。
name
は人が読むための名前であり、url
はプロトコルなし(つまりhttpなどなし)のリンクである。また、説明が必要であれば、description
に記載できる。
多言語対応もしており、(zh
、zh-TW
、ja
)の言語で多言語ドキュメントを作成可能である。
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Institute of Science Tokyo',
url: 'isct.ac.jp',
lang: 'ja',
description: `
:::tip
支持通过category参数筛选新闻类别。详情请查看[指南](https://docs.rsshub.app/zh/guide/parameters#%E5%86%85%E5%AE%B9%E8%BF%87%E6%BB%A4)。
You can filter news by category through the category parameter. For more information, please refer to the [guide](https://docs.rsshub.app/guide/parameters#filtering).
:::`,
ja: {
name: '東京科学大学',
},
};
Route本作成
Routeの定義やRadarなど
この部分もオフィシャルパンフレットに従って作ればいい。
path: '/news/:lang',
categories: ['university'],
example: '/isct/news/ja',
parameters: { lang: 'language, could be ja or en' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['www.isct.ac.jp/:lang/news'],
target: '/news/:lang',
},
],
name: 'News',
maintainers: ['catyyy'],
ウェブサイトの解析
ウェブサイト自体がAPIを通じてデータを取得している場合は、可能であればそのAPIを直接使用してデータを取得する方が良い。それができない場合は、HTMLデータを手動で解析しなければならない。最も簡単な方法は、F12を押して開発者ツールを開き、ネットワークオプションに切り替えてからページをリフレッシュすることです。これで、Fetch/XHRのセクションでウェブページのAPIリクエストを確認できる。
今回は運良く、東科大のニュースはこのAPIを使って取得されているニュースデータで、分類もこのAPIから取得されている。
しかし、残念ながら上記のAPIの返す値は一般的なJSON形式ではなく、HTMLテキスト形式である。そのため、解析して利用可能な形式に変換する必要がある。この部分でかなりの時間を費やしてしまった。ここで、私はTSのentitiesにあるHTML解析関数decodeを使った。もしこれを見て役立つ人がいれば、非常に嬉しく思う。
handler: async (ctx) => {
const { lang = 'ja' } = ctx.req.param();
const mediaResponse = await ofetch(`https://www.isct.ac.jp/expansion/get_media_list_json.php?lang_cd=${lang}`);
const tagResponse = await ofetch(`https://www.isct.ac.jp/expansion/get_tag_list_json.php?lang_cd=${lang}`);
const mediaData = JSON.parse(decode(mediaResponse));
const tagData = JSON.parse(decode(tagResponse));
const itemsArray: MediaItem[] = Object.values(mediaData);
const tagArray: TagItem[] = Object.values(tagData);
const tagIdNameMapping: { [key: string]: string } = {};
for (const item of Object.values(tagArray)) {
tagIdNameMapping[item.TAG_ID] = item.TAG_NAME;
}
const items = itemsArray.map((item) => ({
// タイトル
title: item.TITLE,
// リンク
link: 'news/' + item.MEDIA_CD,
// 本文
description: item.META_DESCRIPTION,
// リリース時間
pubDate: parseDate(item.PUBLISH_DATE),
// (オプション)作者
// author: item.user.login,
// (オプション)分類
category: item.MEDIA_TYPES ? [tagIdNameMapping[Number.parseInt(item.MEDIA_TYPES.replaceAll('"', ''), 10)]] : [],
}));
return {
// ウェブサイトのタイトル
title: `ISCT News - ${lang}`,
// ウェブサイトのリンク
link: `https://www.isct.ac.jp/${lang}/news`,
// 先に作成した内容
item: items,
};
},
最後に作成できたファイル
import { Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import { decode } from 'entities';
interface MediaItem {
ID: string;
TITLE: string;
PUBLISH_DATE: string;
META_DESCRIPTION: string;
MEDIA_CD: string;
MEDIA_TYPES: string;
}
interface TagItem {
TAG_ID: string;
TAG_NAME: string;
}
export const route: Route = {
path: '/news/:lang',
categories: ['university'],
example: '/isct/news/ja',
parameters: { lang: 'language, could be ja or en' },
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['www.isct.ac.jp/:lang/news'],
target: '/news/:lang',
},
],
name: 'News',
maintainers: ['catyyy'],
handler: async (ctx) => {
const { lang = 'ja' } = ctx.req.param();
const mediaResponse = await ofetch(`https://www.isct.ac.jp/expansion/get_media_list_json.php?lang_cd=${lang}`);
const tagResponse = await ofetch(`https://www.isct.ac.jp/expansion/get_tag_list_json.php?lang_cd=${lang}`);
const mediaData = JSON.parse(decode(mediaResponse));
const tagData = JSON.parse(decode(tagResponse));
const itemsArray: MediaItem[] = Object.values(mediaData);
const tagArray: TagItem[] = Object.values(tagData);
const tagIdNameMapping: { [key: string]: string } = {};
for (const item of Object.values(tagArray)) {
tagIdNameMapping[item.TAG_ID] = item.TAG_NAME;
}
const items = itemsArray.map((item) => ({
// タイトル
title: item.TITLE,
// リンク
link: 'news/' + item.MEDIA_CD,
// 本文
description: item.META_DESCRIPTION,
// リリース時間
pubDate: parseDate(item.PUBLISH_DATE),
// (オプション)作者
// author: item.user.login,
// (オプション)分類
category: item.MEDIA_TYPES ? [tagIdNameMapping[Number.parseInt(item.MEDIA_TYPES.replaceAll('"', ''), 10)]] : [],
}));
return {
// ウェブサイトのタイトル
title: `ISCT News - ${lang}`,
// ウェブサイトのリンク
link: `https://www.isct.ac.jp/${lang}/news`,
// 先に作成した内容
item: items,
};
},
};
提出
提出する前に、必ずScript Standardに従ってください。
テストが完了した後は、pull requestを作成し、要求に従ってフォームに必要事項を記入すれば、通常は1日以内にmergeされるはずである。
ここで終わり、人生初めてOSSに貢献できて嬉しい。
もし興味があれば、ぜひ試してみてください。
また、オフィシャルハンドブックには日本語がないようだので、もし何か質問があれば気軽に聞いてください。