ushumpei’s blog

生活で気になったことを随時調べて書いていきます。

Node.js でオブジェクトの配列からストリームを作成する

多分知っておくべきこととして、ストリームはバージョン 12.3.0 でかなり変化があった。この記事は 11.15.0 で書いている。

Node でオブジェクトの配列からストリームを作る方法。

const { Readable } = require("stream");

const readable = Readable.from({ objectMode: true, read() {} })
オブジェクトの配列.map((o) => readable.push(o))
readable.push(null)

12.3.0 以降 は以下で良い。

const { Readable } = require("stream");

const readable = Readable.from(オブジェクトの配列)

ストリーム自体の使い方があまりわかっていない。

とりあえず動かす

エラー時、ストリームを頑張って閉じなければいけないと思う、 destroy() とか使う。

const { Readable, Transform } = require("stream");

// テストデータ作成
const testData = new Array(1000)
  .fill("")
  .map((_, i) => ({ id: i, val: Math.random() }));
testData.push(null)

// Readable ストリーム作成
const readable = new Readable({
  objectMode: true,
  read() {},
});

// Transform ストリーム作成
const stringifyTransform = new Transform({
  transform(data, _encoding, callback) {
    this.push(JSON.stringify(data));
    callback();
  },
  objectMode: true,
});

// この辺微妙
stringifyTransform.on("error", () => readable.destroy());
process.stdout.on("error", () => {
  readable.destroy();
  stringifyTransform.destroy();
});

// 入力 -> 変換 -> 出力
readable.pipe(stringifyTransform).pipe(process.stdout);

// 入力へデータを入れる
testData.forEach((o) => readable.push(o));

標準出力にオブジェクトの文字列が表示される。

ストリームの閉じる閉じないの話

ストリームが閉じているかどうか調べる方法がわからない。イベントを監視していればできるがもっと良い方法はないだろうか? (12.3.0 以降とかだとプロパティがある)

以下はストリームの挙動を確認するための、ストリームを標準出力へパイプするコード。失敗させたりしてイベント拾う。

const { Readable } = require("stream");

// Readable ストリーム作成
const readable = new Readable({
  objectMode: true,
  read() {},
});

console.log(0, readable.readableFlowing);
// => null

const pipe = readable.pipe(process.stdout);

console.log(1, readable.readableFlowing);
// => true

console.log(2, readable.push("ok\n"));
// => true
// 出力あり

console.log(3, readable.push({}));
// => true
// pipe 先の process.stdout がエラー起こして失敗

pipe.on("error", (e) => {
  console.log(4, readable.readableFlowing);
  // => false

  console.log(5, readable.push("ok\n"));
  // => true
  // ただ pipe が切れているので出力はない

  readable.destroy();
  // readable を切る
});

readable.on("close", (e) => {
  console.log(6, "close"); // destroy() で close が発生する。pipe 先のエラーでは切れたりしないようだ
});

出力

0 null
1 true
2 true
3 true
ok
4 false
5 true
6 'close'
  • readableFlowing: パイプされているかどうか。最初 (0) では繋がっていないので null。繋がると (1) true、エラー発生後 (4) は切れてて false
  • pipeerror を拾うようにして、その中で各ストリームを切っていけば良いっぽい。

感想

API ドキュメントのストリーム部分 ちゃんと読んだほうがいいのだろうな、と思いました。英語読むのちょっと気合がいる。

疑問たくさんある

  • エラー時に transform も切る必要があるだろうか?
  • 各種イベントの正しい使い方。error 起きた時に閉じるとかちゃんとしなきゃダメだがあまり整理できてない。finish? end?
  • ストリーム作成時の read() {}pipe だと不要?実装してみる?
  • TransformReadableWritable を継承しているらしいが Writable っぽさが見えてない。
  • gzipTransform の実装どんなだ

fetchbody でストリーム取得できるから画像を canvas に書き込むとか楽しそう。何に使えるかはわからない。加工とかしてみる?RGB から一色なくすとかはできそう。ただそれは何のために?

ストリーム Java でしか使ったことがなかった

GZIPOutputStream と ByteArrayOutputStream と try with resources

Javatry with resources は途中で return してもリソースの close をしてくれるけど、なんか間違えた。

GZIPOutputStreamByteArrayOutputStream を使って以下のような、データを圧縮してバイト列にして返す処理を書いていました。( InputStream の部分は実際は外部のファイルのストリームとかになると思います)

String input = "hogehogehoge";
InputStream in = new ByteArrayInputStream(input.getBytes());

try (
  BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  ByteArrayOutputStream out = new ByteArrayOutputStream();
  BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(out)))
) {

  String line;
  while ((line = reader.readLine()) != null) {
    writer.write(line);
    writer.newLine();
  }

  return out.toByteArray();

} catch (IOException e) {
  e.printStackTrace();
  throw new RuntimeException(e);
}

すると次の例外が発生。

java.io.EOFException: Unexpected end of ZLIB input stream

これは GZIPOutputStream を閉じていないときに出る例外で、「 try with resources だから閉じられるのでは?」と思いましたが、 toByteArray しているタイミングはまだ処理が try を抜けていないので閉じられていなかったようです。

以下のように変更して解決しました。バイト列を取得するのは try を抜けてから行うようにしました。

String input = "hogehogehoge";
InputStream in = new ByteArrayInputStream(input.getBytes());

ByteArrayOutputStream out = new ByteArrayOutputStream();

try (
  BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(out)))
) {

  String line;
  while ((line = reader.readLine()) != null) {
    writer.write(line);
    writer.newLine();
  }

} catch (IOException e) {
  e.printStackTrace();
  throw new RuntimeException(e);
}
return out.toByteArray();

感想

すぐ終わると思って書いたコードが動かなくて夜が明けてしまった。

最近景気が悪いので、仕事がなくなったら各地のお祭りを巡る旅とかしたい。

修了証明書

Certificate of Completion

スライドとかでよかった気がするけど、なんとなく html で修了証明書を書いてみました。css は無限に時間を溶かすと思います。

See the Pen Certificate of Completion by ushumpei (@ushumpei) on CodePen.

MacChrome でしか確認していない。もしかしたら他の環境でフォントとかダメかもしれないです。

COURSE DETAILS (リボンの下の細かい文字) が短すぎると良い感じの形にならないです。三文くらいあると印刷した時に A4 サイズにうまくおさまる。

参考

ありがとうございました。

リボン

saruwakakun.com

リボンのコード、border で三角形を作るやつの応用で白い三角形を使って切り口を表現する。

mrtc.jp

画像使わないと縁取りかっこよくならないかと思ったけど、radial-gradientlinear-gradient を使って丸みを出している。ただコードちゃんと追えていない。

感想

効力のない、お気持ちのやつ。というか正式なフォーマットがどこかにあるんだろうな。人に教えるほど何かを極めているわけでもないのにどこへ向かっているんだろうか私は。

スマホで見るとビールのラベルっぽい。

VSCode で Ruby 書くための設定

PC 変えた時に vim & ruby のいい感じの設定が失われてしまいしばらく書かないから放置していたらまた書くことになり VSCode でなんとなく書いてたらいい加減不便さを感じてきたので本当にとりあえずの設定を調べました。(早口)

この辺ができるように設定。

  • 構文チェック
  • 補完と定義ジャンプ

あとこれは

でやってます。

やること

VSCodeRuby の拡張があるので入れる (名前そのまんま Ruby のやつ)

GitHub - rubyide/vscode-ruby: Provides Ruby language and debugging support for Visual Studio Code

拡張を動かすために rubocop があればいいそうなので入れる

gem install rubocop

VSCode の設定ファイルに以下を追加

    "ruby.lint": { "rubocop": true },
    "ruby.useLanguageServer": true,
    "ruby.intellisense": "rubyLocate",

説明

  1. "ruby.lint": { "rubocop": true }: Linter は rubocop
  2. "ruby.useLanguageServer": true: Language Server 使う (構文チェックしてくれるようになる)
  3. "ruby.intellisense": "rubyLocate": インテリセンスの設定 (ある程度、補完、定義ジャンプができるようになる)

終わり

rubyLocate のオプションには定義検索するパスが設定できるようで、うまくやれば結構便利になるかもです?脳死で初期設定できないと割と放置しがちな癖があり、よく無いですね。

open-uri の close とか

rubyopen-uriopen で、サイトをスクレイピングするプログラムを見かけたのだけれど、「close 呼ばなくていいの?」と思って色々調べたのでメモしておきます。結論としては、ブロック渡した時は呼ばなくていいけど基本的には呼ぶ。

調べたのは ruby 2.3 なので古くなるかもです、がそんなに変わらなそうな部分?

ブロック渡した時は呼ばなくていい

open メソッドの実装を見てみます。

> require 'open-uri'
=> true
> method(:open).source_location
=> ["/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/2.3.0/open-uri.rb", 29]

https://github.com/ruby/ruby/blob/ruby_2_3/lib/open-uri.rb#L29-L39

  def open(name, *rest, &block) # :doc:
    if name.respond_to?(:open)
      name.open(*rest, &block)
    elsif name.respond_to?(:to_str) &&
          %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name &&
          (uri = URI.parse(name)).respond_to?(:open)
      uri.open(*rest, &block)
    else
      open_uri_original_open(name, *rest, &block)
    end
  end

今回は引数として url 文字列の見渡すので、引数 name は URL として解釈され、URI#open が呼ばれるみたいです。そこだけ切り出して実行してみます。

> (uri = URI.parse('https://ushumpei.hatenablog.com')).respond_to?(:open)
=> true
> uri
=> #<URI::HTTPS https://ushumpei.hatenablog.com>
> uri.method(:open).source_location
=> ["/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/2.3.0/open-uri.rb", 716]

https://github.com/ruby/ruby/blob/ruby_2_3/lib/open-uri.rb#L716-L718

OpenURI::OpenRead#open が呼ばれるみたい。その中から OpenURI.open_uri が呼ばれている。

https://github.com/ruby/ruby/blob/ruby_2_3/lib/open-uri.rb#L132-L166 (抜粋)

    if block_given?
      begin
        yield io
      ensure
        if io.respond_to? :close!
          io.close! # Tempfile
        else
          io.close if !io.closed?
        end
      end
    else
      io
    end

153 行目で block_given? によりブロックが与えられていた時は close を呼ぶように実装されている (ローンパターンか)

なのでブロック渡せば close は明示的に呼ばなくてもいいことがわかりました。

気になったコード

html = open('https://ushumpei.hatenablog.com').read

え、これ絶対 close されないのでは?と思ったので lsof で開きっぱなしなことを確認しました。確認コマンドとその意味は以下の感じになります。

$ lsof -c ruby | grep open-uri | wc -l
  • ruby が開いているファイルを見たいので -c ruby でコマンド名を指定しました。
  • 一時ファイル名に open-uri が付いていたのでそれで絞り込み
  • 行数取得

該当コード実行前の lsof -c ruby | grep open-uri | wc -l0

該当コード実行 (irb は開きっぱなし)

> require 'open-uri'
=> true
> 10.times { |n| open('https://ushumpei.hatenablog.com').read }
=> 10

該当コード実行後の lsof -c ruby | grep open-uri | wc -l10irb を終了するとファイル数は 0 に戻りました。

ブロック渡した時は irb 開きっぱなしの状態でもファイル数は 0 のままです。

(注意: open-uriopen した時に、lsofopen-uri がひっついていている行が出力される、という事象は、ソースコードレベルでは確認しきれていません。ちょっとお粗末になり申し訳ないですが、「多分 open 時に作成された開かれているファイルの行だ」という予想に基づいている、と断っておかなければいけません)

まとめ

  • open(XXX).readopen(XXX) { |f| f.read } とかに変えよう
  • lsof って list open files なんですね。ポート使っているプロセス調べるコマンドかと思っていた。
  • というか linux が全てをファイルという形で抽象化しているアレのアレなのか。(よく知らない)
  • そういえば open って |ls とかでパイプつけたコマンド渡すと実行できたりしてアレらしいので、OpenURI.open_uri 呼んじゃった方がいいっぽい。

追記 2019/04/07

  • 可読性微妙かもだけど、 open(XXX).read の書き換え open(XXX, &:read) もいけるのか、 ruby すごい