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を呼ぶ方法
php.ini
もしくはini_set
でmax_execution_timeを変更する方法
このうちset_time_limit
のドキュメントにこのような記述がある。
デフォルトの制限値は 30 秒です。 なお、php.iniでmax_execution_timeの 値が定義されている場合にはそれを用います。
これを僕は「常にmax_execution_timeが優先される」ように解釈した。調べてみた。
結論
「常にmax_execution_timeが優先される」ようなことはない。
set_time_limit
はini_set('max_execution_time', n)
のラッパーというだけな気がする。
調査
$ 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本体のソースを読む
ざっくりとしか読んでいないので間違いはあるかもしれない。
- ini_setはzend_alter_ini_entry_exを通して更新する
- set_time_limitはzend_alter_ini_entry_chars_exを呼ぶ
- 内部でini_setと同じくzend_alter_ini_entry_exを呼ぶ
set_time_limit
引数の値をそのまま使うので、どこかからデフォルトを持ってきてどうこうすることはない
処理的には両者に違いはほとんどない。
デフォルト値がどう関係するかといえば次に書く通り。
どちらも設定値を上書きするたびにカウンタはリセットされる
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-product
はutil.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)
とはいえDashでproduct
で引っ掛ければ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で実装
(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; --%'
PDO::ATTR_EMULATE_PREPARES=true
+ 文字列に直接パラメータを埋め込んで実行
華麗にインジェクションがキマる。
where name like '%'; delete from admins; --%'
PDO::ATTR_EMULATE_PREPARES=false
+ 文字列に直接パラメータを埋め込んで実行
本文に書いた通り複文を許さないので構文エラーになる。
PDO::ATTR_EMULATE_PREPARES=false
+ \DB::select($query, $params)
(プレースホルダーを通して実行した場合)
基本はこれで書くはず。もしくはEloquent使うなら意識すらしないかも。
構文エラーにならずPrepared Statement
とかも呼ばれて実行される。
Gaucheの無名関数について - lambda, ^, ^c, cut
背景
とあるネット記事のGaucheのコードを読んでいて^ _ ( 何か処理 )
というのがあってこれは何だと思って調べたメモです。
^
自体は知っていたのですが、^ _
という別の記法もあるのかなと不思議に思ったので。
無名関数
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)
余談: 調べ方
今回のような記号(^
)に関するリファレンスを調べる場合でもapropos
とinfo
で簡単に辿り着けるのがとても良いです。
; ^を含むリファレンスを探す 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に著作権発生してた。