DenoでZennのAPIモジュールを作る
Zennのページのソースを見ていて、<script id="__NEXT_DATA__"></script>
要素を見つけました。
Next.jsだとこれが入るらしいです。知りませんでした。
ページで表示しているユーザーや記事のデータがJSON形式で埋め込まれているようです。
これを使えば、データを取得することができそうだと思い、Denoで作ってみました。
Deno DOMでHTML要素を取り出す
ページデータの取得には以前紹介したKyが使えます。
今回は、取得できたHTMLの文字列から<script id="__NEXT_DATA__"></script>
要素を取り出し、JSON.parse()
にかければページのデータを取得できるはずです。
この際、要素を正規表現で取り出すことも出来なくないですが、JSON文字列内に任意の文字が含まれうることを考えると、かなりのregexちからが必要とされるでしょう。
今回は単なる文字列ではなくHTMLなので、Deno DOMを使うことが出来ます。
ここで提供されているDOMParser
を使うと、HTML形式の文字列をDOMに変換し、getElementById()
などを使って要素を選択することが出来ます。
やってみましょう。
import { DOMParser, ky } from "./api/deps.ts";
const ZENN_ROOT = "https://zenn.dev";
const zennApi = async (path = "") => {
const html = await ky(path, { prefixUrl: ZENN_ROOT }).text();
const dom = new DOMParser().parseFromString(html, "text/html");
if (!dom) {
throw new Error("Dom parse failed");
}
const data = dom.getElementById("__NEXT_DATA__");
if (!data) {
throw new Error("There is no data field");
}
return JSON.parse(data.innerText);
};
console.log(await zennApi("kawarimidoll"));
前述の通り、id="__NEXT_DATA__"
の要素を探し、JSON.parse()
で変換しています。
実行してみます。
❯ deno run --allow-net zenn_api.ts
{
props: {
pageProps: {
user: {
id: 39895,
username: "kawarimidoll",
name: "kawarimidoll",
(略)
},
articles: [
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object]
],
itemType: "articles"
}
},
page: "/[username]",
query: { username: "kawarimidoll" },
buildId: "VEZIl64_cqCl2dus4P4tW",
isFallback: false,
gip: true,
scriptLoader: []
}
ページの内容が取得できました。
同様に、await zennApi()
でトップページ、await zennApi("articles")
で記事一覧ページのデータが取得できます。
API取得内容の調整
結果を見て、いくつかの点に気づきました。
-
buildId
以下はページの生成に関わる項目っぽいので、APIとして取得しなくても良さそう - メインの内容は
props.pageProps
なので、もう少し階層が浅いほうが扱いやすそう -
{ page: 2 }
とかでページングしたい - エラーハンドリングも必要
ということで以下のようになりました。
- import { DOMParser, ky } from "./deps.ts";
+ import { DOMParser, ky, SearchParamsOption } from "./deps.ts";
const ZENN_ROOT = "https://zenn.dev/";
- const zennApi = async (path = "") => {
- const html = await ky(path, { prefixUrl: ZENN_ROOT }).text();
+ const callAPI = async (path = "", searchParams: SearchParamsOption) => {
+ const html = await ky(path, { prefixUrl: ZENN_ROOT, searchParams }).text();
const dom = new DOMParser().parseFromString(html, "text/html");
if (!dom) {
throw new Error("Dom parse failed");
}
const data = dom.getElementById("__NEXT_DATA__");
if (!data) {
throw new Error("There is no data field");
}
- return JSON.parse(data.innerText);
+ const { props, page, query } = JSON.parse(data.innerText);
+ return { ...props.pageProps, page, query };
};
+ const zennApi = async (page = "", query = {}) => {
+ try {
+ return await callAPI(page, query);
+ } catch (error) {
+ return { error: error.toString(), page, query };
+ }
+ };
console.log(await zennApi("kawarimidoll"));
GitHubからモジュールを読み込む
Denoはdeno.land/x
やnest.land
にてモジュールが公開されています。
しかし、他のリポジトリから直接読み込むことも出来ます。
今回は勉強用なのでモジュールライブラリに公開することはせず、GitHubからモジュールを読み込んでみることにしました。
先程のzenn_api.ts
のzennApi()
と、ついでにZENN_ROOT
をexport
します。
// (略)
- console.log(await zennApi("kawarimidoll"));
+ export { ZENN_ROOT, zennApi };
スタイルガイドに従い、エントリーポイントはindex
ではなくmod.ts
にします。
これをリポジトリに置きます。
export { ZENN_ROOT, zennApi } from "./zenn_api.ts";
今回はパブリックリポジトリとしましたが、トークンの設定を行えばプライベートリポジトリからも読み込み可能です。
こちらのページに公開されています。
GitHubの場合、このページのパスのblob
をraw
にするとファイルにアクセスできます。
ということで、以下のようにしてモジュールを使用できます。
import { zennApi } from "https://github.com/kawarimidoll/deno-zenn-api/raw/main/mod.ts";
console.log(await zennApi("topics"));
console.log(await zennApi("scraps/explore"));
console.log(await zennApi("books", { order: "alltime", page: 2 }));
実行してみます。
❯ deno run --allow-net main.ts
{
topics: [
{
id: 66,
name: "javascript",
displayName: "JavaScript",
taggingsCount: 1505,
imageUrl: "https://storage.googleapis.com/zenn-topics/javascript.png"
},
(略)
取得できました。これでzenn-api.ts
がローカルになくても実行できます。
おわりに
APIというかHTMLページのパースですが、自作モジュールを作って使うことができました。
再利用したいスクリプトを公開しておくといった場合に便利そうです。
もちろん、しっかりやるならバージョニングなどの整備が必要ですが。
また、今回と同様の手法で他のNextプロジェクトのデータを取得することもできそうですね。
応用を探してみると面白いかもしれません。
今回作ったリポジトリはこちらです。
参考
途中で気づきましたが、既に似ていることをやってた方がいました…
Discussion