我试着使用 Apache Arrow

听说Apache Arrow很不错,所以我立刻开始研究了一下。

Apache Arrow是什么?

→ Apache Arrow 的官方网站链接:https://arrow.apache.org

目标是在语言(项目)之间优化和加速数据交换(大致如此,可能有所不同)

顺便提一下,最近参与的一个项目中,我们用Ruby整理数据,然后用Python进行机器学习。在那个时候,我们将Ruby端整理好的数据写入到文件中,然后在Python端读取该文件进行处理,将结果写入文件,最后在Ruby端读取并在Javascript(浏览器)中进行图形化处理。

在这种场合可以使用吗?
所以,我试试看。

用 Ruby 语言处理 Apache Arrow

由于宝石(Gem)中存在,因此进行安装。

source "https://rubygems.org"

gem "rake"
gem "red-arrow"
$ bundle install --path vendor/bundler

安装完成了。
赶快来试试吧。

# -*- coding: utf-8 -*-
require "arrow"

class RedArrowTest

  # ------------------------------------------------
  def read(streamname)
    input = Arrow::MemoryMappedInputStream
    input.open(streamname) do |inp|
      reader = Arrow::RecordBatchStreamReader.new(inp)
      fields = reader.schema.fields
      p "fields: #{fields.collect{|c| c.name }}"
      reader.each do |r|
        p [r]
      end
    end
  end

  def readfile(filename)
    input = Arrow::MemoryMappedInputStream
    input.open(filename) do |inp|
      reader = Arrow::RecordBatchFileReader.new(inp)
      fields = reader.schema.fields
      p "fileds: #{fields.collect{|c| c.name }}"
      reader.each do |r|
        p [r]
      end
    end
  end

  # ------------------------------------------------
  def write(streamname, schema, columns)
    output = Arrow::FileOutputStream
    output.open(streamname, false) do |oup|
      writer = Arrow::RecordBatchStreamWriter.open(oup, schema) do |wrt|
        recordbatch = Arrow::RecordBatch.new(schema, columns[0].count, columns)
        wrt.write_record_batch(recordbatch)
      end
    end
  end

  def writefile(filename, schema, columns)
    output = Arrow::FileOutputStream
    output.open(filename, false) do |oup|
      writer = Arrow::RecordBatchFileWriter.open(oup, schema) do |wrt|
        recordbatch = Arrow::RecordBatch.new(schema, columns[0].count, columns)
        wrt.write_record_batch(recordbatch)
      end
    end
  end

  # ------------------------------------------------
  # sample data
  def createdata
    fields = [ Arrow::Field.new("name", :string),
               Arrow::Field.new("age", :int8) ]
    schema = Arrow::Schema.new(fields)

    names = ["taro", "jiro", "hanako", "tsukiko", "saburo"]
    ages = [26, 21, 24, 17, 10]

    columns = [ Arrow::StringArray.new(names),
                Arrow::Int8Array.new(ages) ]

    return schema, columns
  end    
end

# ----------------------------------------------------------------
# run

ra = RedArrowTest.new

# create sample data :p
schema, columns = ra.createdata

ra.write("hoge", schema, columns)
ra.read("hoge")

#ra.writefile("hoge", schema, columns)
#ra.readfile("hoge")

看起来有通过文件和内存两种方式。
→ read、write是通过内存操作,readfile、writefile是通过文件操作。

但不管怎样,这两种方式最终都会生成文件。。。
内容似乎有点不同,但是,暂时还是先使用内存方式吧。

开始执行吧。

$ bundle exec ruby test.rb
"fields: [\"name\", \"age\"]"
[#<Arrow::RecordBatch:0x7feaf2057500 ptr=0x7feaf30f0f90 name:   [
    "taro",
    "jiro",
    "hanako",
    "tsukiko",
    "saburo"
  ]
age:   [
    26,
    21,
    24,
    17,
    10
  ]
>]

成功了。
实际上,在当前目录下创建了一个名为hoge的文件,并且它看起来是存在的。

用Javascript实现Apache Arrow。

既然可以在语言之间交换数据,那么我会尝试用 JavaScript 读取由 Ruby 创建的数据。

Node.js 是一种以 JavaScript 编写的开放源代码的跨平台 JavaScript 运行环境。

我曾经看到写着可以在JavaScript中使用,所以以为它也可以轻松地在浏览器端使用,但似乎完全不是这样。

首先,我们要安装 Node.js。
由于我使用的是 Mac,所以我会通过 Homebrew 进行安装。

有一种名为 nodebrew 的工具可以管理 Node.js 的各种包和其他内容。我们可以使用它来安装 Apache Arrow。

$ brew install nodebrew
$ mkdir -p ~/.nodebrew/src
$ nodebrew install-binary latest  ← nodebrew install latest でもいいらしい。
install-binary はバイナリをインストール。install はソースからインストールらしい。。
$ nodebrew list
v11.1.0

current: none

好的,已經安裝完成。
不過現在還不能使用,需要進行設定。

$ nodebrew use 11.1.0
use v11.1.0

将路径添加到~/.bash_profile中

# Node.js
export PATH="$HOME/.nodebrew/current/bin:$PATH"

暂时,我们已成功安装了 Node.js

我尝试使其能在浏览器端使用。

我在互联网上做了各种搜索,但没有找到直接在浏览器端使用的方法。作为替代,有人通过在Node.js上建立服务器,然后使用jQuery访问该服务器的方式来传递数据,所以我决定模仿这种方法。

用的框架

使用 Node.js 的包管理工具是 npm。通过它可以安装各种包。

看起来需要在当前目录下创建一个名为 node_modules 的文件夹,然后把相关文件保存在里面。
因此,首先我们需要进入项目文件夹然后进行安装。

$ cd appfolder
$ npm install express

让我们也安装Apache Arrow。

$ npm install apache-arrow

建立服务器

"use strict"
// apache-arrow service :p

const express = require("express");
const port = 8010;
const app = express();
app.use(express.static("./public"));

// allow CrossSiteScript
app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
});

// say hello
app.get("/", (req, res) => {
    res.send("Hello, world!");
});

// arrow
app.get("/input", (req, res) => {
    const fs = require("fs");
    const aa = require("apache-arrow");
    const path = "./";
    const encoding = "binary";

    var filename = req.query.n;
    //console.log("filename: " + filename);

    // result data
    var result = {};

    if(filename != undefined){
        try {
            var adata = fs.readFileSync(path + filename);
            var table = aa.Table.from([adata]);
            var schema = table.schema;      
            var fldnames = new Array();
            for(var i = 0; i < schema.fields.length; i++){
                fldnames.push(schema.fields[i].name);
                result[schema.fields[i].name] = new Array();
            }
            for(var j = 0; j < fldnames.length; j++){
                var column = table.getColumn(fldnames[j]);
                for(var i = 0; i < column.length; i++){
                    result[fldnames[j]].push(column.get(i));
                    //console.log(column.get(i));
                }
            }   
        } catch(ex){
            // nop :p
        }   
    }
    res.json(result);
});

// run service!
app.listen(port);

我会让它跑起来。

$ node arrow_service.js
スクリーンショット 2018-11-06 0.04.04.png

通过Ajax访问

我們將使用jQuery進行訪問。
首先,在application.js中創建一個函數。

function arrowInput(arrowname) {    
    var result;
    $.ajax({ type: "GET",
         url: "http://localhost:8010/input",
         data: { n: arrowname },
         async: false,
         dataType: "json",
         success: function(data){
         result = data;
         }
       });
    return result;
}

这次我们将Ajax从异步改为同步。在这方面,根据项目需要进行一些调整试试看。

然后我们会开始制作HTML部分。

<html>
  <head>
    <title>Red Arrow Test</title>
    <script type="text/javascript" src="/javascripts/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="/javascripts/application.js"></script>
  </head>
  <body>
    <h3>Red Arrow Test</h3>

<script type="text/javascript">
  var data = arrowInput("hoge");
  console.log(data);
</script>

  </body>
</html>

让我们试着执行一下吧。

スクリーンショット 2018-11-06 10.09.59.png

哦,看起来挺不错的。

总结

我使用Apache Arrow在Ruby中创建的数据可以通过Javascript(浏览器端)获取。
虽然我不确定这是否是正确的用法,但数据交互确实变得更加容易了。我打算再稍微等一下看看。

不过,关于删除不再需要的数据的时机(垃圾收集),这个手动的方式怎么样呢?

追加修改

我修改了上述的Ruby程序中的read方法。
此外,我还创建了一个方法来将其转换为Arrow格式的数据,因此我会添加进去。

我已经修改了代码,使其能够通过哈希返回读取的数据。

class RedArrowTest
  def read(streamname)
    result = Hash.new
    input = Arrow::MemoryMappedInputStream
    input.open(streamname) do |inp|
      reader = Arrow::RecordBatchStreamReader.new(inp)
      fields = reader.schema.fields
      reader.each do |r|
        fields.each do |fld|
          result[fld.name] = Array.new if result[fld.name].nil?
          result[fld.name] += r.send(fld.name).collect{|c| c}
        end
      end
    end
    return result
  end
end

执行结果如下。

$ bundle exec ruby test.rb

{"name"=>["taro", "jiro", "hanako", "tsukiko", "saburo"], "age"=>[26, 21, 24, 17, 10]}

下一步是创建一个从哈希数据生成Arrow格式数据的方法。

class RedArrowTest
  # data -> { fieldname1 => Array, fieldname2 => Array, ... }
  def create_arrow_data(data)
    fldnames = data.keys
    fldtypes = Array.new
    fldnames.each do |fname|
      v = data[fname][0]
      case v.class.to_s
      when "Integer"
        ma = data[fname].max
        mi = data[fname].min
        ftype = :int64
        if (mi >= 0 && ma <= 0xffffffff) ||
           (mi >= (0x8000000 * -1) && ma <= 0x7fffffff)
          ftype = :int32
        end
        if (mi >= 0 && ma <= 0xffff) || (mi >= (0x8000 * -1) && ma <= 0x7fff)
          ftype = :int16
        end
        if (mi >= 0 && ma <= 0xff) || (mi >= (0x80 * -1) && ma <= 0x7f)
          ftype = :int8
        end
      when "Date"
        ftype = :date32
      when "Time", "DateTime"
        ftype = :timestamp
      when "Float"
        ftype = :float
      when "TrueClass", "FalseClass"
        ftype = :boolean
      else
        ftype = :string
      end
      fldtypes.push(ftype)
    end

    # create schema & columns
    fields = Array.new
    columns = Array.new
    fldnames.each_with_index do |fname, i|
      if fldtypes[i] == :timestamp
        fields.push(Arrow::Field.new(fname.to_s,
                                     Arrow::TimestampDataType.new(:second)))
        obj = eval("Arrow::#{fldtypes[i].to_s.to_camel}Array.new(:second, data[fname])")
      else
        fields.push(Arrow::Field.new(fname.to_s, fldtypes[i]))
        obj = eval("Arrow::#{fldtypes[i].to_s.to_camel}Array.new(data[fname])")
      end
      columns.push(obj)
    end
    schema = Arrow::Schema.new(fields)    
    return schema, columns
  end
end

以下是执行结果。

# 以下のように修正してね。

ra = RedArrowTest.new

data = { :string => ["taro", "jiro", "saburo"],
         :integer => [24, 21, 100024],
         :time => [Time.now, Time.now, Time.now],
         :date => [Date.new(2018, 11, 6), Date.new(2018, 11, 7), Date.new],
         :float => [12.0, 23.2, 100024.23],
         :boolean => [true, false, false]
       }
schema, columns = ra.create_arrow_data(data)

ra.write("tara", schema, columns)
p ra.read("tara")
$ bundle exec ruby test.rb

{"string"=>["taro", "jiro", "saburo"], "integer"=>[24, 21, 100024], "time"=>[2018-11-07 09:34:41 +0900, 2018-11-07 09:34:41 +0900, 2018-11-07 09:34:41 +0900], "date"=>[#<Date: 2018-11-06 ((2458429j,0s,0n),+0s,2299161j)>, #<Date: 2018-11-07 ((2458430j,0s,0n),+0s,2299161j)>, #<Date: -4712-01-01 ((0j,0s,0n),+0s,2299161j)>], "float"=>[12.0, 23.200000762939453, 100024.2265625], "boolean"=>[true, false, false]}

听起来不错呢。先试试在Javascript(浏览器端)确认一下吧。

<h3>Red Arrow Test</h3>

<script type="text/javascript">
  var data = arrowInput("tara");  ← tara  に変更
  console.log(data);
</script>
スクリーンショット 2018-11-07 9.38.59.png

暂时就这样吧。

广告
将在 10 秒后关闭
bannerAds