dak ブログ

python、rubyなどのプログラミング、MySQL、サーバーの設定などの備忘録。レゴの写真も。

ruby でのシンプルな連結リストの実装

2021-06-18 20:45:18 | ruby
ruby でシンプルな連結リストを実装し、配列との性能比較を行ったメモ。

データを読み込んで集計処理を行う場合に、配列にデータを格納しますが、
配列が最大サイズに達すると、サイズを拡大して再度データを格納するため、
実行時間が大幅に長くなる場合があります。

そこで、1から1千万の整数を生成して、以下の方法で処理時間を比較してみました。
・方法1: 配列に値を全て格納してから出力
・方法2: 連結リストに値を全て格納し、配列に変換してから出力
 ※連結リストの実装例は最後に記します。

実行結果では、方法2は方法1の約1/2の実行時間でした。
以下、各方法のプログラムと、実行時間です。

■方法1: 配列に値を全て格納してから出力
  num = 10000000

  # データ登録
  arr = Array.new
  1.upto(num) do |i|
    arr.push(i)
  end

  # 表示
  arr.each do |elem|
    printf("%s\n", elem)
  end
実行時間
real    0m36.069s
user    0m35.909s
sys     0m0.156s


■方法2: 連結リストに値を全て格納し、配列に変換してから出力
  num = 10000000

  # データ登録
  require 'list'
  list = List.new
  1.upto(num) do |i|
    list.push(i)
  end

  # リストを配列に変換
  arr = list.to_a()

  # 出力
  arr.each do |elem|
    printf("%s\n", elem)
  end
実行時間
real    0m18.227s
user    0m17.907s
sys     0m0.316s


■連結リストの実装例
データを逐次追加するのが主要な使い方のため、連結リストの末尾に値を追加する push と、
連結リストを配列に変換する to_a のみを実装。
class List
  class Cell
    attr_accessor :val
    attr_accessor :next

    def initialize(val = nil)
      @val = val
      @next = nil
    end
  end

  def initialize()
    @num = 0
    @head = nil
    @tail = @head
  end

  def push(val)
    if @head.nil?
      @tail = @head = Cell.new(val)
    else
      @tail.next = Cell.new(val)
      @tail = @tail.next
    end

    @num += 1
  end

  def to_a()
    arr = Array.new(@num)

    i = 0
    cell = @head
    while cell
      arr[i] = cell.val
      cell = cell.next
      i += 1
    end

    return arr
  end
end


rubyのQueueを使ったプログラムのプロファイルをとってみた

2021-03-28 14:12:10 | ruby
rubyのQueueを使ったプログラムののプロファイルをとってみました。

rubyでプロファイルをとるには、以下のように -r profile を指定します。
ruby -r profile {プログラム名}

stderr にプロファイル結果が出力されます。

プロファイル取得対象のプログラムは以下の通りです。
stdin から1行読み込み、キューに push し、各スレッドは キューからpopしたデータを出力します。
require 'thread'

num_threads = 3
q = Queue.new()

ts = []
0.upto(num_threads) do |i|
  ts[i] = Thread.new() do |t|
    while true do
      str = q.pop()
      break if ! str

      #item = itemstr.split(/\t/, -1)
      printf("thread %i [%s]\n", i, str)
    end
  end
end

STDIN.each do |line|
  line.chomp!
  q.push(line)
end

上記のプログラム test1.rb を以下のように実行すると、profile.txt にます。
ruby -e '0.upto(10000) { |i| printf("%d\n", i) }' \
| ruby -r profile test1.rb \
> /dev/null \
2> profile.txt

profile.txt のプロファイルは以下のようになりました。(前半のみです)
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 33.05     0.39      0.39    18415     0.02     0.09  Mutex#synchronize
 18.64     0.61      0.22     8414     0.03     0.27  Queue#pop
 10.17     0.73      0.12    10001     0.01     0.10  Queue#push
  6.78     0.81      0.08     8414     0.01     0.01  Kernel.printf
  6.78     0.89      0.08    18419     0.00     0.01  Mutex#lock
  6.78     0.97      0.08    18415     0.00     0.00  Mutex#unlock
  4.24     1.02      0.05    10689     0.00     0.00  Array#empty?
  4.24     1.07      0.05    18415     0.00     0.00  Array#shift
  1.69     1.09      0.02     8414     0.00     0.00  BasicObject#!
  1.69     1.11      0.02    10001     0.00     0.00  String#chomp!
  1.69     1.13      0.02    12276     0.00     0.00  Array#push
  1.69     1.15      0.02     8414     0.00     0.00  IO#write
  0.85     1.16      0.01     2275     0.00     0.00  Thread#current
  0.85     1.17      0.01     2271     0.00     0.02  Mutex#sleep
  0.85     1.18      0.01        1    10.00    50.00  IO#each

Mutex、Queueで時間がかかっていることがわかります。

grepでキャプチャした文字列にマッチさせる方法

2018-06-28 22:43:24 | ruby
grepでキャプチャした文字列にマッチさせる方法です。

以下のようなファイルで1カラム目と2カラム目が同じ文字列の行を
抽出したい場合には、grep でキャプチャを使った正規表現で
抽出することができます。
$ cat test.txt
123   123
123   456
abc   abc
abc   def

$ cat test.txt | grep -E '^([0-9a-z]+)[[:space:]]+\1'
123   123
abc   abc




~を含まない正規表現

2013-09-09 23:20:20 | ruby
rubyの正規表現で、~を含まない正規表現は以下のように記述します。

/^(?:(?!マッチさせたくない文字列).)*$/

例えば「Android」を含まない文字列にマッチさせたい場合には、

/^(?:(?!Android).)*$/

と記述します。

「Android」か「iPhone」を含まない文字列にマッチさせたい場合には

/^(?:(?!Android|iPhone).)*$/

と記述します。




rubyのCGIExtでのURLデコード

2012-01-15 22:42:06 | ruby
rubyでWebサーバのアクセスログを分析するのに、CGI.parse でリクエストパラメータをデコードするプログラムを書きましたが、CGI.parse は分析自体のプログラムよりも CGI.parse に時間がかかっていました。

そこで、「ruby CGI 遅い」で検索してみたところ、CGIExt が見つかったので、こちらを利用してみたところ、パラメータのデコードにかかる時間が大幅に短縮されました。
require 'cgi' の後に require 'cgiext' とするだけでよいのもプログラムの修正が少なくて便利です。

http://cgiext.rubyforge.org/

■CGIを利用した場合
●プログラム
#!/usr/local/bin/ruby

require 'cgi'
require 'time'

# HIRAGANA=にほん&KATAKANA=ニホン&KANJI=日本
paramstr = 'HIRAGANA=%e3%81%ab%e3%81%bb%e3%82%93&KATAKANA=%e3%83%8b%e3%83%9b%e3\
%83%b3&KANJI=%e6%97%a5%e6%9c%ac'

start_time = Time.now

1.upto(10000) do |i|
CGI.parse(paramstr)
end

end_time = Time.now
print("time: #{end_time - start_time}\n")

●実行結果
time: 0.466363


■CGIExtを利用した場合
●プログラム
#!/usr/local/bin/ruby

require 'cgi'
require 'cgiext'
require 'time'

# HIRAGANA=にほん&KATAKANA=ニホン&KANJI=日本
paramstr = 'HIRAGANA=%e3%81%ab%e3%81%bb%e3%82%93&KATAKANA=%e3%83%8b%e3%83%9b%e3\
%83%b3&KANJI=%e6%97%a5%e6%9c%ac'

start_time = Time.now

1.upto(10000) do |i|
CGI.parse(paramstr)
end

end_time = Time.now
print("time: #{end_time - start_time}\n")

●実行結果
time: 0.070527





rubyでsocketでサーバに接続してからforkした場合の挙動

2011-05-13 01:26:37 | ruby
rubyでsocketでサーバに接続した後に、(1)forkし、(2)一方のプロセスでsocketをクローズ、(3)他方のプロセスでsocketに対して読み書きを行おうとするとエラーとなります。

同じ理由でOpen3.popen3で存在しないプログラムを実行しようとしてエラーが発生した場合、それまでに接続していたコネクションは切断されてしまいます。


【プログラム】
#!/usr/local/bin/ruby

require 'socket'

def receive(sock)
while true
str = sock.recv(4096)
break if str == ''
print("[#{Process.pid}] #{str}\n")
end
end

sock = TCPSocket.new('localhost', 80)
pid = fork
if pid
# 親プロセス
begin
# 送信
print("[parent] #{Process.pid}\n")
sock.write("GET /nikeda/test/test.cgi HTTP/1.0\n\n")
print("[parent] send\n")

sleep(3)

# 受信
recieve(sock)
rescue
print("[parent] exception\n")
end
else
# 子プロセス
print("[child] end\n")
exit(0)
end

【実行結果】
$ ./test1.rb
[child] end
[parent] 19582
[parent] send
[parent] exception






rubyでflockを使った排他制御(その2)

2011-04-19 00:17:44 | ruby
先日の『rubyでflockを使った排他制御』では、排他されているプロセスはずっと待ち状態になってしまいます。
そのため、いくつかのプロセスが常に動作している環境では、ずっと待ち状態になってしまう可能性があります。

ブロックされている場合の処理を記述したい場合には、flock の引数に File::LOCK_NB を指定します。

■プログラム例
#!/usr/local/bin/ruby
#
# flock による排他制御
# usage: flock_nb.rb {ID} {retry} {interval}
#

$KCODE = 'u'
require 'jcode'
require 'time'

# lock
# @param lockfile
# @param num_retry リトライ回数
# @param interval リトライ間隔
# @return lockfileのstream / nil
def lock(lockfile, num_retry, interval)
st = File.open(lockfile, 'a')
if ! st
STDERR.print("failed to open\n")
return nil
end

0.upto(num_retry) do |i|
begin
res = st.flock(File::LOCK_EX | File::LOCK_NB)
return st if res

print("[#{Time.now().to_s}] sleep [#{ARGV[0]}]\n")
sleep(interval)
rescue
STDERR.print("failed to lock\n")
return nil
end
end

return nil
end

# unlock
# @param lockfile
# @return true / false
def unlock(st)
begin
st.flock(File::LOCK_UN)
st.close
rescue
STDERR.print("failed to unlock")
return false
end

return true
end

sleep(5)

lock_st = lock('lockfile', ARGV[1].to_f, ARGV[2].to_f)
if ! lock_st
print("[#{Time.now().to_s}] failed to lock [#{ARGV[0]}]\n")
exit
end

print("[#{Time.now().to_s}] get lock [#{ARGV[0]}]\n")
sleep(10)
print("[#{Time.now().to_s}] release lock [#{ARGV[0]}]\n")

unlock(lock_st)

■実行結果
$ ./flock_nb.rb 1 3 3 &
$ ./flock_nb.rb 2 3 3 &
$ ./flock_nb.rb 3 3 3 &
[Tue Apr 19 00:15:43 +0900 2011] get lock [1]
[Tue Apr 19 00:15:46 +0900 2011] sleep [2]
[Tue Apr 19 00:15:48 +0900 2011] sleep [3]
[Tue Apr 19 00:15:49 +0900 2011] sleep [2]
[Tue Apr 19 00:15:51 +0900 2011] sleep [3]
[Tue Apr 19 00:15:52 +0900 2011] sleep [2]
[Tue Apr 19 00:15:53 +0900 2011] release lock [1]
[Tue Apr 19 00:15:54 +0900 2011] get lock [3]
[Tue Apr 19 00:15:55 +0900 2011] sleep [2]
[Tue Apr 19 00:15:58 +0900 2011] failed to lock [2]
[Tue Apr 19 00:16:04 +0900 2011] release lock [3]




rubyでflockを使った排他制御

2011-04-16 21:30:01 | ruby
rubyでプロセス間で排他制御を行うのに、一番最初に思いつくのはロックファイルを作成する方法でしょう。
ロックファイルを使う方法では、ロックファイルがある場合にはsleepして、再びロックファイルの存在をチェックし、ロックファイルがない場合にはロックファイルを作成します。

しかし、ロックファイルの作成後にエラーが発生して、ロックファイルはあってもロックをかけたプロセスが異常終了した場合には、ロックファイルが残ったままになって、他のプロセスがロックを獲得できなくなってしまいます。
ロックファイルにプロセスIDを書き込んでおけば、ロックファイルに記述されたプロセスIDが存在しないことをチェックできるので、エラー処理に役立ちますが、少し面倒ではないでしょうか。

File#flockを使って排他制御を行うと、以下の理由でプログラムが少し楽になるでしょうか。
・ruby インタプリタが終了した場合にロックが解除されるため、ロックファイルを削除する必要がない。
・排他ロックを実行すると、ロックが解除されるまでプロセスが待機状態になり、ロックが解除されればロックを獲得できます。
sleepして、再度ロックファイルをチェックするような処理は不要です。

■プログラム

#!/usr/local/bin/ruby
#
# flock による排他制御
#
# usage: flock.rb {ID}

$KCODE = 'u'
require 'jcode'
require 'time'

# 指定ファイルでロック
# @param lockfile
# @return lockfileのstream / nil
def lock(lockfile)
st = File.open(lockfile, 'a')
if ! st
STDERR.print("failed to open\n")
return nil
end

begin
st.flock(File::LOCK_EX)
rescue
STDERR.print("failed to lock\n")
return nil
end

return st
end

# アンロック
# @param st
# @return true / false
def unlock(st)
begin
st.flock(File::LOCK_UN)
st.close
rescue
STDERR.print("failed to unlock")
return false
end

return true
end

sleep(5)

# ロック
lock_st = lock('lockfile')

# 排他制御
print("[#{Time.now().to_s}] get lock [#{ARGV[0]}]\n")
sleep(10)
print("[#{Time.now().to_s}] release lock [#{ARGV[0]}]\n")

# アンロック
unlock(lock_st)

■実行結果
$ ./flock.rb 1&
$ ./flock.rb 2&
$ ./flock.rb 3&
[Sat Apr 16 21:26:33 +0900 2011] get lock [1]
[Sat Apr 16 21:26:43 +0900 2011] release lock [1]
[Sat Apr 16 21:26:43 +0900 2011] get lock [2]
[Sat Apr 16 21:26:53 +0900 2011] release lock [2]
[Sat Apr 16 21:26:53 +0900 2011] get lock [3]
[Sat Apr 16 21:27:03 +0900 2011] release lock [3]

rubyのCGIでプロファイルをとる方法

2011-04-15 01:52:34 | ruby
コマンドラインで ruby プログラムのプロファイルをとるには、以下のように -r profile を指定します。
(-r と profile の間の空白文字は無くてもかまいません)

$ ruby -r profile {プログラム名}

プロファイル結果は標準エラーに出力されます。


CGI の場合には、CGIの先頭行で以下を記述します。
-r と profile の間に空白があるとエラーになります。

#!/usr/local/bin/ruby -rprofile

プロファイル結果はコマンドラインでは標準エラーに出力されていたので、CGI ではプロファイル結果は /var/log/httpd/error_log.YYYYMMDD に出力されます。

rubyのhashでmax

2011-04-10 23:32:45 | ruby
rubyのhashで、値の最大値を取得する方法です。

$ irb
irb(main):001:0> h = { 'a' => 10, 'b' => 100, 'c' => 50 }
=> {"a"=>10, "b"=>100, "c"=>50}

irb(main):002:0> h.max { |a, b| a[1] <=> b[1] }
=> ["b", 100]

値が最大のキーと値のペアの配列が取得できます。
配列ですので、以下のようにすれば、キーまたは値だけを取得することもできます。

irb(main):003:0> (h.max { |a, b| a[1] <=> b[1] })[0]
=> "b"

irb(main):004:0> (h.max { |a, b| a[1] <=> b[1] })[1]
=> 100

ちなみに、キーの最大値を取得するなら、以下のようにします。
irb(main):005:0> h.max { |a, b| a[0] <=> b[0] }
=> ["c", 50]

rubyでxlsファイルをtsvファイルに変換

2011-03-22 23:33:27 | ruby
rubyでxlsファイルをtsvファイルに変換するプログラムです。
標準入力からxlsファイルを読み込む場合には、Spreadsheet.openで直接読み込もうとするとエラーになるため、いったんTempfileに書き込んでおいて、Tempfileから読み直すようにしています。

#!/usr/local/bin/ruby
#
# usage: xls_to_tsv.rb [xls] > {tsv}
#

$KCODE = 'u'
require 'jcode'
require 'rubygems'
require 'spreadsheet'

# 引数
xls_file = nil
if ARGV.length == 0
tf = Tempfile.new('xls_to_tsv')
data = STDIN.read
tf.write(data)
tf.rewind
xls_file = tf
elsif ARGV.length == 1
xls_file = ARGV[0]
else
STDERR.print("usage: xls_to_tsv.rb [xls] > {tsv}\n")
exit(1)
end

# sheet
book = Spreadsheet.open(xls_file)
sheet = book.worksheet(0)

0.upto(sheet.row_count - 1) do |r|
tsv_values = []

0.upto(sheet.column_count - 1) do |c|
if sheet[r, c].class == Spreadsheet::Formula
value = sheet[r, c].value.to_s
elsif ! sheet[r, c]
value = ''
else
value = sheet[r, c].to_s
end

tsv_values.push(value)
end

print(tsv_values.join("\t") + "\n")
end

# 標準入力の場合
if ARGV.length == 0
xls_file.close
end

rubyでtsvファイルからxlsファイルを作成(その2)

2011-03-21 11:49:13 | ruby
rubyでtsvファイルからxlsファイルを生成するプログラムで、Spreadsheet::Workbook#writeで標準出力に出力しようとするとエラーになりますが、データをTempfileに出力して、Tempfileからデータを読み直して標準出力に出力することができます。


#!/usr/local/bin/ruby
#
# usage: tsv_to_xls.rb [xls] < {tsv}
#

$KCODE = 'u'
require 'jcode'
require 'rubygems'
require 'spreadsheet'

# 引数
xls_file = nil
if ARGV.length == 0
xls_file = Tempfile.new('tsv_to_xls')
elsif ARGV.length == 1
xls_file = ARGV[0]
else
STDERR.print("usage: tsv_to_xls.rb [xls] < {tsv}\n")
exit(1)
end

# sheet
book = Spreadsheet::Workbook.new
sheet = book.create_worksheet

# tsv 読み込み
STDIN.each_with_index do |line, r|
a = line.chomp.split("\t")
a.each_with_index do |value, c|
sheet[r, c] = value
end
end

# 出力
book.write(xls_file)

# 標準出力へ
if ARGV.length == 0
xls_file.rewind
data = xls_file.read
STDOUT.write(data)
end

rubyでcsvをtsvに変換

2011-03-06 14:56:02 | ruby
rubyでcsvを扱うのにcsvライブラリが便利です。
エスケープ処理まで考えると、単純に文字列を split(",") するわけにはいかないので。

自分的には csv よりも tsv の方が好きなので、csv ファイルは tsv にしてから処理します。
というわけで、csv ライブラリのサンプルとして tsv に変換するプログラムです。


#!/usr/local/bin/ruby

require 'csv'

CSV.open(ARGV[0], 'r') do |row|
print(row.join("\t") + "\n")
end



row は CSV::Cell の配列です。


標準入力から csv を読み込む場合には、CSV::IOReader を使います。


#!/usr/local/bin/ruby

require 'csv'

CSV::IOReader.new(STDIN).each do |row|
STDOUT.print(row.join("\t") + "\n")
end



rubyでexcelファイルをダウンロードさせるCGI

2011-02-22 01:01:30 | ruby
rubyのCGIでexcelファイルをダウンロードさせるようにする方法です。
Spreadsheet::Workbook#write() の引数に STDOUT を指定するとエラーになるので、Tempfile に書き出しておいて rewind してから read して出力しています。
普通のファイルに書き込むと、エラー処理などが面倒ですが、Tempfile ならファイルが残らないので便利です。


#!/usr/local/bin/ruby

KCODE = 'u'
require 'jcode'
require 'cgi'
require 'tempfile'
require 'rubygems'
require 'spreadsheet'

# シートを作成
book = Spreadsheet::Workbook.new
sheet = book.create_worksheet
sheet[0, 0] = 'テスト1'
sheet[0, 1] = 'テスト2'

# シートを出力
Tempfile.open('/tmp') do |tf|
book.write(tf)
tf.rewind

# ダウンロードファイル名
file_name = "download.xls"

print("Content-Type: application/octet-stream\n")
print("Pragma: private\n")
print("Content-Disposition: attachment; filename=\"#{file_name}\"\n")
print("\n")
print(tf.read)
end




rubyでtsvファイルからxlsファイルを作成

2011-02-10 20:32:45 | ruby
ruby の spreadsheet ライブラリで tsv ファイルを xls ファイルに変換する方法です。
ちなみに、xls ファイルの出力を標準出力にしようとしましたが、Spreadsheet::Workbook#write の引数に STDOUT を指定するとエラーになってしまいました。


#!/usr/local/bin/ruby
#
# usage: tsv_to_xls.rb {xls}
#

$KCODE = 'u'
require 'jcode'
require 'rubygems'
require 'spreadsheet'

# 出力先
xls_file = ARGV[0]

# sheet
book = Spreadsheet::Workbook.new
sheet = book.create_worksheet

# tsv 読み込み
STDIN.each_with_index do |line, r|
a = line.chomp.split("\t")
a.each_with_index do |value, c|
sheet[r, c] = value
end
end

# 出力
book.write(xls_file)