TextDecoderStream を見つけた
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ fetch で得られる ReadableStream は Uint8Array でデータを受け取れる
◆ stream で処理するとき マルチバイト文字の途中で分割されてたら文字列化でおかしくなる
◆ TextDecoder の stream モードでデコードもできるけど TextDecoderStream を使うほうが便利
◆ stream で処理するとき マルチバイト文字の途中で分割されてたら文字列化でおかしくなる
◆ TextDecoder の stream モードでデコードもできるけど TextDecoderStream を使うほうが便利
stream とマルチバイト文字列
stream でバイナリデータを受け取って 全部を受け取るのを待たず 受け取るたびに文字列として処理をするとき 日本語が入るとうまくいかないときがありますUTF-8 で日本語は 3 バイト文字なので 3 バイト中のどこかでチャンクが分かれてしまう場合です
こういう感じです
text: abあcd
↓
chunk1: [a, b, あ(1), あ(2)]
chunk2: [あ(3), c, d]
Node.js の場合の対処方法は stream に encoding を設定すれば 文字列として有効な単位で受け取れます(詳細)
ブラウザで
これをブラウザでもやりたいですstream から受け取れるデータは Uint8Array になってます
const { body } = await fetch("")
const reader = body.getReader()
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log(value)
}
Uint8Array(740) […]
Uint8Array(4096) […]
Uint8Array(65536) […]
Uint8Array(872) […]
バイナリデータとして複数に分かれているので このままそれぞれを文字列化したら問題が出る場合があるはずです
Node.js みたいに encoding を設定できることを期待したのですが response.body で得られる ReadableStream を見ても encoding を設定できそうなプロパティやメソッドがありません
TextDecoder 側で対応できないかなと思って引数を調べてみると stream モードがありました
普通に TextDecoder を使うと マルチバイト文字の途中で別れたらこうなります
const decoder = new TextDecoder()
console.log(decoder.decode(Uint8Array.from([227, 129, 130, 227])))
console.log(decoder.decode(Uint8Array.from([129, 132, 227, 129, 134])))
あ�
��う
stream に対応させるとこうなります
const decoder = new TextDecoder()
console.log(decoder.decode(Uint8Array.from([227, 129, 130, 227]), { stream: true }))
console.log(decoder.decode(Uint8Array.from([129, 132, 227, 129, 134]), { stream: true }))
あ
いう
中途半端な部分は decoder のインスタンス内部に残って 次の decode メソッドの呼び出し時に結合されてデコードされます
TextEncoder や TextDecoder はインスタンス化しなくてもただの関数で十分だと思ってましたが こういうところで内部状態を持つことが考慮されてたのですね
これに対応させるとこうなります
const { body } = await fetch("")
const reader = body.getReader()
const decoder = new TextDecoder()
while (true) {
const { value, done } = await reader.read()
if (done) break
const text = decoder.decode(value, { stream: true })
console.log(text)
}
正常な場合なら↑のコードで最後まで読み取ったとき decoder 内に途中までのデータが残らないので大丈夫ですが 末尾にマルチバイト文字の途中までがあるような壊れたデータも考えるなら while を抜けたあとや if (done) のブロック内で
const text = decoder.decode(new Uint8Array())
console.log(text)
を実行して残ったデータがあれば�を出力できます
const decoder = new TextDecoder()
console.log(decoder.decode(Uint8Array.from([227, 129, 130, 227]), { stream: true }))
console.log(decoder.decode(new Uint8Array()))
あ
�
TextDecoderStream
ここまでで満足してたのですが MDN で TextDecoder を見てたときに TextDecoderStream というのを見つけましたstream のまま変換できるみたいです
const { body } = await fetch("")
const stream = body.pipeThrough(new TextDecoderStream())
const reader = stream.getReader()
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log(value)
}
<!doctype html><html ....
変換後の stream も ReadableStream なので getReader で reader を作って中身を読めます
このとき read で読み取ったデータは文字列になってます
fetch だと分割されたときにうまくいってるのか確認しづらいので ReadableStream を自作して確認します
まず Uint8Array のままの場合です
const stream = new ReadableStream({
async start(controller) {
await new Promise(r => setTimeout(r, 1000))
controller.enqueue(Uint8Array.from([227, 129, 130, 227]))
await new Promise(r => setTimeout(r, 1000))
controller.enqueue(Uint8Array.from([129, 132, 227, 129, 134]))
await new Promise(r => setTimeout(r, 1000))
controller.close()
},
})
const reader = stream.getReader()
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log(value)
}
Uint8Array(4) [227, 129, 130, 227]
Uint8Array(5) [129, 132, 227, 129, 134]
これに TextDecoderStream を通します
const stream = new ReadableStream({
async start(controller) {
await new Promise(r => setTimeout(r, 1000))
controller.enqueue(Uint8Array.from([227, 129, 130, 227]))
await new Promise(r => setTimeout(r, 1000))
controller.enqueue(Uint8Array.from([129, 132, 227, 129, 134]))
await new Promise(r => setTimeout(r, 1000))
controller.close()
},
})
const str_stream = stream.pipeThrough(new TextDecoderStream())
const reader = str_stream.getReader()
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log(value)
}
あ
いう
文字化けしたりせず 「い」 が表示されてますね
結構前から使えた
MDN を見ると TextDecoderStream は Chrome 71 から使えるようなってたようですTextDecoder は 38 なので それよりはずっと後ですが 今はもう 115 なので結構前から使えた機能だったようですね