🦕

DenoでZennのAPIモジュールを作る

2021/06/26に公開

Zennのページのソースを見ていて、<script id="__NEXT_DATA__"></script>要素を見つけました。
Next.jsだとこれが入るらしいです。知りませんでした。

ページで表示しているユーザーや記事のデータがJSON形式で埋め込まれているようです。
これを使えば、データを取得することができそうだと思い、Denoで作ってみました。

Deno DOMでHTML要素を取り出す

ページデータの取得には以前紹介したKyが使えます。
https://zenn.dev/kawarimidoll/articles/13c3f75f6f22d6

今回は、取得できたHTMLの文字列から<script id="__NEXT_DATA__"></script>要素を取り出し、JSON.parse()にかければページのデータを取得できるはずです。

この際、要素を正規表現で取り出すことも出来なくないですが、JSON文字列内に任意の文字が含まれうることを考えると、かなりのregexちからが必要とされるでしょう。

今回は単なる文字列ではなくHTMLなので、Deno DOMを使うことが出来ます。

https://deno.land/x/deno_dom

ここで提供されているDOMParserを使うと、HTML形式の文字列をDOMに変換し、getElementById()などを使って要素を選択することが出来ます。

やってみましょう。

zenn_api.ts
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 }とかでページングしたい
  • エラーハンドリングも必要

ということで以下のようになりました。

zenn_api.ts
- 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/xnest.landにてモジュールが公開されています。
https://deno.land/x
https://nest.land/gallery

しかし、他のリポジトリから直接読み込むことも出来ます。
今回は勉強用なのでモジュールライブラリに公開することはせず、GitHubからモジュールを読み込んでみることにしました。

先程のzenn_api.tszennApi()と、ついでにZENN_ROOTexportします。

zenn_api.ts
// (略)

- console.log(await zennApi("kawarimidoll"));
+ export { ZENN_ROOT, zennApi };

スタイルガイドに従い、エントリーポイントはindexではなくmod.tsにします。

https://deno.land/manual@v1.11.2/contributing/style_guide#do-not-use-the-filename-codeindextscodecodeindexjscode

これをリポジトリに置きます。

mod.ts
export { ZENN_ROOT, zennApi } from "./zenn_api.ts";

今回はパブリックリポジトリとしましたが、トークンの設定を行えばプライベートリポジトリからも読み込み可能です。

こちらのページに公開されています。
https://github.com/kawarimidoll/deno-zenn-api/blob/main/mod.ts

GitHubの場合、このページのパスのblobrawにするとファイルにアクセスできます。
ということで、以下のようにしてモジュールを使用できます。

main.ts
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プロジェクトのデータを取得することもできそうですね。
応用を探してみると面白いかもしれません。

今回作ったリポジトリはこちらです。
https://github.com/kawarimidoll/deno-zenn-api

参考

途中で気づきましたが、既に似ていることをやってた方がいました…
https://zenn.dev/hellorusk/articles/db83908e15b6f1

https://qiita.com/windchime-yk/items/991c9d9213c8bb467dde

Discussion