3
3

More than 3 years have passed since last update.

Node.jsでFetch APIで取得したShift_JISのテキストをTextDecoderではなくnpmのパッケージを使ってデコードする

Posted at

Overview

Node.jsでFetchでHTMLのデータを取得してresponse.text()でテキストを取得したら文字化けが:scream:
どうしてだろうと思ったらHTMLがShift-JIS、かつtext()はまさかのUTF-8専用とのこと。
https://developer.mozilla.org/ja/docs/Web/API/Body/text

レスポンスは常に UTF-8 としてデコードします。

さすがモダンなAPIだな、この割り切り嫌いじゃない:wink:

それならばとブラウザ同様TextDecoderを使ってShift-JISに変換しようとしたらエラー[ERR_ENCODING_NOT_SUPPORTED]が:fearful:
Node.jsのドキュメントみてもShift-JISサポートあるやん!
https://nodejs.org/docs/latest/api/util.html#util_class_util_textdecoder

ググってみたらICUとかいうのが必要なようで…
マイナーなデータは別途提供するから自分で取り込んでねとごもっともな意見。
http://var.blog.jp/archives/80396639.html

私はCloud Build(CI/CD)やCloud Functions(FaaS)上でも動作するようなお手軽なのを求めているため、ICUは使わない方向で検討しました。

ちなみにブラウザの場合は、ブラウザ自体が多数の文字コードを扱っているため工夫いらずで変換できる模様。
@kerupani129さんが記事を書かれています。
https://qiita.com/kerupani129/items/6646eb920c23658bc525

Target reader

  • Shift_JISのテキストをnpmのパッケージだけでデコードしたい方

Prerequisite

  • バックエンドはCloud Build(CI/CD)やCloud Functions(FaaS)を利用する、つまりNodeの起動オプションを指定するようなことはできない。
  • Node.jsのバージョンはCloud Functionに依存しているため、現時点ではV10系とする。
  • ソースコードはimport/exportを使ってブラウザと記述を統一できるよう、esmというパッケージを利用している。

Body

2つの選択肢

npmだけで行こうとすると選択肢は二つある。

  • full-icuをインストールしてTextDecoderを利用する。
  • iconv-liteの類の独自の文字コード変換で処理する。

今回何より大事にしたいのはどこでもインストールエラー等なく動くことを最優先とする。
前者のfull-icuはICUをダウンロードするようで、サイズの大きさの懸念と、何よりダウンロードがCloud Buildで利用できるかの懸念がある。
ローカルマシンと比較してフルマネージドサービスではいろいろと制約があるため、インストール時に権限等により失敗することが少なくない。
また、Cloud FunctionsではNode.jsの起動オプションは指定できず、環境変数についても動作するか不明。

ということで、後者のパッケージで文字コード変換を完結するものを利用する。

iconv-lite

iconv-liteの週間ダウンロード数は2千万でversionが1.00に到達していないが十分すぎる人気。
https://www.npmjs.com/package/iconv-lite

Node.jsなのでパッケージサイズは気にならないが、念のため調べるとMINIFIED + GZIPPEDで150KBなので、フロントで利用するわけじゃないので合格。
package.json覗いてみたところ、依存関係も1つと素晴らしい。
https://bundlephobia.com/result?p=iconv-lite@latest

そして、私が作ったFetchAPIのソースコード。
Shift-JIS対策だけではなく、タイムアウトや認証エラーが含まれるため少し複雑になっている。
Shift-JISの変換は、fetchText()のoption.sjisの部分で、iconv-liteのdecode()を利用しているだけ。
decode()にはBufferを渡す必要があるため、Buffer.from()でresponseを変換している。

responseをみて文字コード指定も考えたが、ヘッダーになかったり指定が間違っていることもありえるので、割り切ってShift-JISのURLの場合にはsjisオプションを付与してコールしている。

utils/fetch.js

import iconv from 'iconv-lite';
import fetch from 'node-fetch';
import AbortController from 'abort-controller';
// for "reason: unable to verify the first certificate"
// see: https://github.com/node-fetch/node-fetch/issues/15#issuecomment-533869809
import https from "https";
const agent = new https.Agent({
    rejectUnauthorized: false
});

const fetchCore = async (url, option = {}) => {
    // set timeout after 15s
    const controller = new AbortController();
    const timeout = setTimeout(() => {
        controller.abort();
    }, option.timeout || 15000);

    try {
        const response = await fetch(url, {
            method: 'GET',
            mode: 'cors',
            cache: 'default',
            agent: option.resolveUnauthorized ? agent : undefined,
            signal: controller.signal, // for timeout
            ...option,
        });

        if (!response.ok) {
            const description = `status code:${response.status} , text:${response.statusText}`;
            throw new Error(description);
        }

        return response;

    } finally {
        clearTimeout(timeout);
    }
}

const fetchJson = async (url, option) => {
    const response = await fetchCore(url, option);
    return await response.json();
}

const fetchText = async (url, option) => {
    const response = await fetchCore(url, option);

    if (option.sjis) {
        return iconv.decode(Buffer.from(await response.arrayBuffer()), "shift_jis")
    }

    return await response.text();
}

export { fetchJson, fetchText };

Conclusion

ブラウザを使っているともはや文字コードなんて気にすることもなかったが、改めブラウザ大変だなと実感。
Shift-JISはあと十年くらいは生存していそうなので、CI/CD環境下ではiconv-lite様様といったところ。

Have a great day!

3
3
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
3
3