Ruby - csvをmarkdown形式のテーブル表記にするサンプルコード

探せば色々な言語のサンプルコードがある。
気分転換に自分でも書いてみた。

整形しないで出力

require 'csv'

def print_to_table(rows)
  header   = rows[0]
  contents = rows[1..-1]

  puts "|" + header.join("|") + "|"
  puts "|" + Array.new(header.length, "---").join("|") + "|"
  contents.each do |items|
    puts "|" + items.join("|") + "|"
  end
end

# 実行
csv = <<EOS
Title,T,Tit
Text,Long Long Text,Awesome
TextText,Text,Some
EOS
rows = CSV.parse(csv)

print_to_table(rows)
|Title|T|Tit|
|---|---|---|
|Text|Long Long Text|Awesome|
|TextText|Text|Some|

整形して出力

require 'csv'

def print_to_table_pretty(rows)
  # 幅に応じて空白を後ろに追加する
  def align_cell(str, width)
    # str + (" " * (width - str.length))でもいい
    str.ljust(width, " ")
  end

  column_widths = Array.new(rows[0].length)
  # 各列の最大幅を記録する
  rows.each do |row|
    row.each_with_index do |text, i|
      current_width    = column_widths[i] || 0
      column_widths[i] = [current_width, text.length].max
    end
  end

  # 各行を列ごとの最大幅に合わせながら出力する
  rows.each_with_index do |row, row_i|
    print "|"
    row.each_with_index do |text, y|
      print " " + align_cell(text, column_widths[y]) + " |"
    end
    puts

    # ヘッダー行の後に入れる --- の行
    if row_i == 0
      print "|"
      column_widths.each do |width|
        print " " + align_cell(("-" * width), width) + " |"
      end
      puts
    end
  end
end


# 実行
csv = <<EOS
Title,T,Tit
Text,Long Long Text,Awesome
TextText,Text,Some
EOS
rows = CSV.parse(csv)

print_to_table_pretty(rows)
| Title    | T              | Tit     |
| -------- | -------------- | ------- |
| Text     | Long Long Text | Awesome |
| TextText | Text           | Some    |

phpのini_set("max_execution_time",n)とset_time_limitの違いについて調べた

背景

phpスクリプトタイムアウト上限を伸ばすには2つの方法がある。

このうちset_time_limitのドキュメントにこのような記述がある。

デフォルトの制限値は 30 秒です。 なお、php.iniでmax_execution_timeの 値が定義されている場合にはそれを用います。

これを僕は「常にmax_execution_timeが優先される」ように解釈した。調べてみた。

結論

「常にmax_execution_timeが優先される」ようなことはない。
set_time_limitini_set('max_execution_time', n)のラッパーというだけな気がする。

調査

OSはMacphpのバージョンは7.0.14。

$ php -v
PHP 7.0.14 (cli) (built: Apr  1 2017 23:41:23) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies

まずは実際に挙動を確認

<?php
// 初期値を確認
var_dump(ini_get("max_execution_time"));

// ini_setで上限値を更新
ini_set("max_execution_time", 60);
var_dump(ini_get("max_execution_time"));

// set_time_limitで上限値を更新
set_time_limit(120);
var_dump(ini_get("max_execution_time"));

コマンドライン経由だとmax_execution_timeは0になるので、ビルトインサーバーを起動してリクエストを投げることで確認した。

-- ビルトインサーバー起動
$ php -S localhost:9000


-- 別コンソールからリクエスト実行
$ curl "http://localhost:9000/sample.php"
string(2) "30"
string(2) "60"
string(3) "120"

もし「max_execution_timeが優先される」なら最後の出力は30もしくは60になるはずだが、実際はちゃんと値が更新されている。
だとすると、ドキュメントの一文は何を意味するのか。

英語の原文を見る

英語のドキュメントだとこう書かれている。

The default limit is 30 seconds or, if it exists, the max_execution_time value defined in the php.ini.

「デフォルト値は30秒だが、もしphp.iniにmax_execution_timeの定義があればそれをデフォルト値とする」という意味に読める。
言葉の意味は分かるが、そもそもset_time_limitは引数が必須だ。このデフォルト値とは何のことなのか。一応php本体のコードも読むことにした。

PHP本体のソースを読む

ざっくりとしか読んでいないので間違いはあるかもしれない。

処理的には両者に違いはほとんどない。
デフォルト値がどう関係するかといえば次に書く通り。

どちらも設定値を上書きするたびにカウンタはリセットされる

set_time_limitのドキュメントより。

この関数がコールされた場合、 タイムアウトカウンタをゼロから再スタートします。 言いかえると、タイムアウトがデフォルトの 30 秒で スクリプト実行までに 25 秒かかる場合に、 set_time_limit(20) を実行すると、スクリプトは、 タイムアウトまでに全体で 45秒 の間実行されます。

つまり、以下のコードはタイムアウトにならずに無限ループになる。

<?php
while (true) {
    set_time_limit(1);
    // ini_setの場合も同じ
    // ini_set("max_execution_time", 1);
}

なお、これを実際に試すとプロセスを直接killしないとCPU100%でずっと走り続けるので注意。

本体のソースコードを読んで「これini_setも同様にゼロから再スタートするのでは」と思って試した。
この挙動はset_time_limitだけだと勝手に思っていたが、ini_set('max_execution_time', n)でも同様だった。
ドキュメント的にもmax_execution_timeの項にはset_time_limitを参照するように書かれているわけなので正しい。

ちなみに手元のphp.iniは最初からmax_execution_timeが30秒で設定されていたが、仮に設定がなくてもphp本体が30秒をデフォルト値にしている。

要は「タイムアウト値のデフォルトはmax_execution_timeに依存する」というだけの話だと思う。

GaucheでRubyのArray#product同等の関数

結論

Gaucheではcartesian-productを使うと良い。
以下は、この関数を見落としていたことによる悪戦苦闘の記録。

背景

業務中にちょっとしたスクリプトで「複数の配列の直積」が必要になった。

例:

  • 入力: [[1, 2], [3, 4], [5]]
  • 出力: [[1, 3, 5], [1, 4, 5], [2, 3, 5], [2, 4, 5]]

2配列ぐらいなら2重ループでもいいのだけど、6配列ぐらい必要だったのでループはきつかった。

RubyのArray#product

業務中は作業速度優先で、さっさとRubyで対応した。Rubyならそのものproductというのがある。

irb(main):001:0> [1, 2].product([3, 4], [5])
=> [[1, 3, 5], [1, 4, 5], [2, 3, 5], [2, 4, 5]]

Gaucheにproductがあるのか調べる

業務後にGaucheでどう書くか調べてみた。
冒頭に書いた通り標準ライブラリに存在するのだけど、この時点では見落としていた。

まず(apropos 'product)で引っかからないので早とちりしたが、ドキュメントにちゃんと書いてあった。

Macro: apropos pattern :optional module
名前がpatternにマッチするような定義された変数のリストを表示します。 moduleにモジュールオブジェクトまたはモジュール名を与えた場合は、 そのモジュール内で定義されている変数のみが表示されます。moduleが 省略された場合は、カレントモジュールから「見える」変数が全て表示されます。

カレントモジュールから「見える」変数が全て表示されます。

cartesian-productutil.combinationsにあるので、useしないと見えなかった。
(どこにあるか分からない段階でuse出来るわけはないので、これは仕方ないが)

gosh> (use util.combinations)
gosh> (apropos 'product)
cartesian-product              (util.combinations)
cartesian-product-for-each     (util.combinations)
cartesian-product-right        (util.combinations)
cartesian-product-right-for-each (util.combinations)

とはいえDashproductで引っ掛ければcartesian-productが引っかかるので、これは単純に見落とした。

そんなわけでproduct関数がないと思い込み、練習ついでにGaucheで書いてみようと思った。

Rubyで仮実装

最初からGaucheで書けるほど慣れていないので、まずはRubyで書いてみた。
自己再帰で書いたりしたのだけど最終的にはinjectを使うようにした。

# 2要素の直積
def product_2dim(xs, ys)
  xs.inject([]) do |acc, x|
    acc + ys.map do |y|
      [x, y].flatten
    end
  end
end

# injectで畳み込み
def product(*lists)
  lists.inject([[]]) {|acc, ls|
    product_2dim(acc, ls)
  }
end

Gaucheで実装

上記のRuby実装をGaucheで置き換えた。

(define (product . lis)
  (define (append-last a b)
    (if (null? a)
        (list b)
        (append a (cons b '()))))

  (define (product-2dim xs ys)
    (fold (^(y acc)
            (append acc (map (cut append-last y <>) xs)))
          '() ys))

  (fold product-2dim '(()) lis))

期待通りには動いているように見える。でも…

ベンチマーク取ってみた

(let ((a (iota 100 1))
      (b (iota 100 1))
      (c (iota 100 1)))
  (time (product a b c)))

; (time (product a b c))
;  real 203.288
;  user 279.670
;  sys   16.460

遅い。非効率な実装なのは感じていたけど、ここまで遅いとは。
ちなみにもちろんcartesian-product使えばちゃんと早い。

(use util.combinations)
(let ((a (iota 100 1))
      (b (iota 100 1))
      (c (iota 100 1)))
  (time (cartesian-product (list a b c))))

;(time (cartesian-product (list a b c)))
; real   0.347
; user   0.330
; sys    0.020

おわりに

自分で書いたproduct実装は遅いのを抜きにしても可読性が悪い。これ絶対数日後に読んでも何がしたいか解読に時間がかかるパターン。
色々書いて読んで覚えていくしかない。

cartesian-product自体Gaucheで書かれているので、とても参考になる。

(define (cartesian-product lol)
  (if (null? lol)
      (list '())
      (let ((l (car lol))
            (rest (cartesian-product (cdr lol))))
        (append-map!
         (lambda (x)
           (map (lambda (sub-prod) (cons x sub-prod)) rest))
         l))))

Laravelにおける複文のSQLインジェクション対策

背景

LaravelアプリケーションでSQLインジェクションのテストを行っていて、わざとSQLインジェクションが起こるようなコードを書いたのに実行されなくて調べた。
(まずはSQLインジェクションが発生することを確認してから、その対応を入れて発生しなくなったことを確認しようと思っていた)

SQLインジェクションのクエリ

典型的な以下の文字列を渡した。

'; delete from admins; -- 

だが実行してみると構文エラーが発生した。

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'delete from admins; --%'' at line 5 (SQL:

理由

LaravelはデフォルトでPDO::ATTR_EMULATE_PREPARESをfalseにしているため。
これがfalseだとMySQLで複文(;区切りで複数のクエリを発行すること)を許可しない。

参考:

試しにPDO::ATTR_EMULATE_PREPARESをtrueにしたら意図通りSQLインジェクションが発生した。

ということでLaravelはデフォルトで複文対策が出来ている模様。

おまけ

パターンごとにパケットキャプチャした結果。

PDO::ATTR_EMULATE_PREPARES=true + \DB::select($query, $params)(プレースホルダーを通して実行した場合)

Prepared Statementは呼ばれない。シングルクオートはエスケープされる。

where name like '%\'; delete from admins; --%'

f:id:kanno_kanno:20170518220118p:plain

PDO::ATTR_EMULATE_PREPARES=true + 文字列に直接パラメータを埋め込んで実行

華麗にインジェクションがキマる。

where name like '%'; delete from admins; --%'

f:id:kanno_kanno:20170518220635p:plain

PDO::ATTR_EMULATE_PREPARES=false + 文字列に直接パラメータを埋め込んで実行

本文に書いた通り複文を許さないので構文エラーになる。

f:id:kanno_kanno:20170518220734p:plain

PDO::ATTR_EMULATE_PREPARES=false + \DB::select($query, $params)(プレースホルダーを通して実行した場合)

基本はこれで書くはず。もしくはEloquent使うなら意識すらしないかも。
構文エラーにならずPrepared Statementとかも呼ばれて実行される。

f:id:kanno_kanno:20170518220442p:plain

Gaucheの無名関数について - lambda, ^, ^c, cut

背景

とあるネット記事のGaucheのコードを読んでいて^ _ ( 何か処理 )というのがあってこれは何だと思って調べたメモです。
^自体は知っていたのですが、^ _という別の記法もあるのかなと不思議に思ったので。

無名関数

公式ドキュメント: 4.3 手続きを作る

Gaucheの無名関数はlambdaで書きます。

; 無名関数の定義
gosh> (lambda (a b) (+ a b))
#<closure (#f a b)>

; 無名関数を即実行
gosh> ((lambda (a b) (+ a b)) 1 2)
3

lambda^という略記もあります。

Special Form: ^ formals body …

^はlambdaの短い別名です。これはGauche独自の拡張です。

gosh> (^(a b) (+ a b) 1 2)
#<closure (#f a b)>

gosh> ((^(a b) (+ a b)) 1 2)
3
gosh>

lambdaもしくは^は引数部分を(a)ではなくaにすると可変長引数を受け取ります。

variable : 手続きは不定個の引数を取ります。 実引数は新しいリストに集められて、そのリストがvaribleに束縛されます。

((lambda a a) 1 2 3) ⇒ (1 2 3)

; 引数をそのまま返すだけの無名関数
gosh> (^ (x) x)
#<closure (#f x)>

gosh> ((^ (x) x) 10)
10

; 引数定義をカッコで指定しないと、(#f . x)という定義になっている
gosh> (^ x x)
#<closure (#f . x)>

; 実行してみると、リストで渡っているのが分かる
gosh> ((^ x x) 10)
(10)
gosh> ((^ x x) 10 20)
(10 20)

; ちなみにもちろんカッコ付きの方だと引数の個数が合わないとエラーになる
gosh> ((^ (x) x) 10 20)
*** ERROR: wrong number of arguments: #f requires 1, but got 2
    While compiling "(standard input)" at line 8: ((^ (x) x) 10 20)
gosh>

冒頭の^ _はこれを意図した書き方だったのかもしれません。
他の言語でもイディオムとして良くあるように、引数を使わないことを意味するため_にしていたのだと思います。実際使っていなかったので。

またこれらとは別に、^cというのもあります。

Macro: ^c body …

(lambda (c) body …)の短縮表記です。 cには#[_a-z]に含まれる任意の一文字が使えます。

(map (^x (* x x)) ‘(1 2 3 4 5)) ⇒ (1 4 9 16 25)

; 以下の2つは同じなはず
gosh> ((^x (+ 1 2)))
3

gosh> ((^_ (+ 1 2)))
3

スペースがあると前述の(#f . x)になるので注意が必要です。最初この違いに気付けず、スペース付きのまま試して期待通りにいかず悩みました。

gosh> (^x (+ 1 2))
#<closure (#f x)>

gosh> (^ x (+ 1 2))
#<closure (#f . x)>

ちなみに部分適用に向いたcutというのもあります。

Macro: cut expr-or-slot expr-or-slot2 …

[SRFI-26] 手続きを簡潔に書ける便利なマクロです。 いわゆる部分適用を実現するために使えます。

; 引数なしの定義
gosh> (cut + 1 2)
#<closure (#f)>

; 定義して即実行
gosh> ((cut + 1 2))
3

; 2引数の定義
gosh> (cut + <> <>)
#<closure (#f #<identifier srfi-26##<identifier srfi-26#x.126a9c0>.1293ee0> #<identifier srfi-26##<identifier srfi-26#x.126a9c0>.1293e00>)>

gosh> ((cut + <> <>) 1 2)
3

本来はmapとかfor-eachとかそういうやつで使うべきな気がします。

; こう書くより
gosh> (map (^a (+ a 1)) '(1 2 3))
(2 3 4)

; こう書いた方が一時変数置かなくて済むのでスマートでしょ的な
gosh> (map (cut + <> 1) '(1 2 3))
(2 3 4)

余談: 調べ方

今回のような記号(^)に関するリファレンスを調べる場合でもaproposinfoで簡単に辿り着けるのがとても良いです。

; ^を含むリファレンスを探す
gosh> (apropos '^)
^                              (gauche)
^-generator                    (gauche)
^_                             (gauche)
^a                             (gauche)
^b                             (gauche)
^c                             (gauche)
^d                             (gauche)
^e                             (gauche)
^f                             (gauche)
^g                             (gauche)
^h                             (gauche)
^i                             (gauche)
^j                             (gauche)
^k                             (gauche)
^l                             (gauche)
^m                             (gauche)
^n                             (gauche)
^o                             (gauche)
^p                             (gauche)
^q                             (gauche)
^r                             (gauche)
^s                             (gauche)
^t                             (gauche)
^u                             (gauche)
^v                             (gauche)
^w                             (gauche)
^x                             (gauche)
^y                             (gauche)
^z                             (gauche)
define-^x                      (gauche)

gosh> (info '^)
; リファレンスが開く

余談

公開前にプレビューで推敲したらlambdaに著作権発生してた。

f:id:kanno_kanno:20170515231923p:plain