Overview
Node.jsでFetchでHTMLのデータを取得してresponse.text()
でテキストを取得したら文字化けが
どうしてだろうと思ったらHTMLがShift-JIS、かつtext()
はまさかのUTF-8専用とのこと。
https://developer.mozilla.org/ja/docs/Web/API/Body/text
レスポンスは常に UTF-8 としてデコードします。
さすがモダンなAPIだな、この割り切り嫌いじゃない
それならばとブラウザ同様TextDecoderを使ってShift-JISに変換しようとしたらエラー[ERR_ENCODING_NOT_SUPPORTED]が
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オプションを付与してコールしている。
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!