Nokogiriを使って部分的にhtmlを編集する

Nokogiriを使ってhelperメソッドで特定のinputに属性(クラス名)を追加したくなったのでやってみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
html = <<HTML
<div>
  <input id="hoge" class="field">

HTML

doc = Nokogiri::HTML.parse(html)

doc.css("input#hoge").each do |input|
  input['class'] += " has_error"
end

doc.to_html # => "<!DOCTYPE html PUBLIC   "-//W3C//DTD HTML 4.0 Transitional//EN  "   "http://www.w3.org/TR/REC-html40/loose.dtd  ">  n<html><body>  n<div>  n  <input class=  "field  ">  n  n</body></html>  n"

Nokogiri::HTML.parseだとDOCTYPEとか とか余計なものがついてくる。

1
2
3
4
5
6
7
8
9
10
11
12
13
html = <<HTML
<div>
  <input id="hoge" class="field">

HTML

doc = Nokogiri::HTML::DocumentFragment.parse(html)

doc.css("input#hoge").each do |input|
  input['class'] += " has_error"
end

doc.to_html # => "<div>  n  <input id=  "hoge  " class=  "field has_error  ">  n  n"

そういう場合はDocumentFragmentを使うと部分的にhtmlを使える。


handlerbarsで現在のループ回数を知る

@indexで取れます。

1
2
3
4
5

  : 
  isFirst?: 
  isLast?: 

その他に@firstには最初の一回目のループの時にはtrueが
@lastには最後のループの時にはtrueが入るようです。
@indexは入力欄 _1とかの1とか表示する時に使えそうですね。

2次元配列とか多次元配列を回す時はこうですね。

1
2
3
4
5
6
7
8
9
多次元配列の回し方


  
    
    
    
  

ちなみにでコメントです。
という書き方もできます。


slimbarsを使ったらUnknown line indicator

勉強がてらRailsのプロジェクトでBackBone.jsでテンプレートにはhandlebarsを使って処理を書いてみました。
viewがslimなのでslimbarsで書いてます。

変数を出力しようとしたら以下のエラー
Unknown line indicator

viewファイルはこんな感じ

1
2
3
.hoge
  .fuga
    

ただしくはこう

1
2
3
.hoge
  .fuga
    | 

hamlbarsではこう書けてしまうので、slimで書く場合は注意が必要だなと思いました。

1
2
3
.hoge
  .fuga
    

小ネタも小ネタでした。

しかしeachの中ではslimの記法使えないのかな?
こうやらないと出力されない・・・

1
2
3
4
select
  | 
    <option value=""></option>
  | 

railsで任意のタイミングで処理を開始

おかあさんといっしょを娘が見るので、毎朝のテレビチャンネルはEテレです。

さてrailsでapp/model/concerns以下のモジュールを勝手にapp/modelにincludeしたいと考えました。
命名規則でファイルを拾って、includeすることにします。
例えば以下のファイル名をつけた場合に

app/model/user.rb
app/model/user auto load _concern.rb

userクラスに自動でuser auto load _concern.rbの内容がincludeされる感じです。

config/initializerに置きます。

1
2
3
4
5
6
7
8
9
10
11
12
  %w(models).each do |elem|
    Pathname.glob(Rails.root.join("app", elem, "concerns", "*")).each do |file|
      if file.file?
        file.basename.to_s.scan(/(.*)(_auto_load_concern.rb)$/) do |class_name|
          if class_name.present?
            model = class_name[0].classify.constantize
            model.class_eval {  include file.basename(".rb").to_s.classify.constantize }
          end
        end
      end
    end
  end

でもこのプロジェクトにはmodelクラスを拡張するプログラムがlib下にあります。
それを読み込まない限りはmodelを参照するとエラーになるようになっています(上記class _name[0].classify.constantizeでエラーとなる)

そこでlib以下のそのプログラム群を読み込んだ後に読み込むようにしたいと思います。

まず先ほどのコードを以下のようにActiveSupport.on _loadで囲みます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ActiveSupport.on_load(:hoge) do
  %w(models).each do |elem|
    Pathname.glob(Rails.root.join("app", elem, "concerns", "*")).each do |file|
      if file.file?
        file.basename.to_s.scan(/(.*)(_auto_load_concern.rb)$/) do |class_name|
          if class_name.present?
            model = class_name[0].classify.constantize
            model.class_eval {  include file.basename(".rb").to_s.classify.constantize }
          end
        end
      end
    end
  end
end

lib下のプログラム群で処理が終わった後に以下を差し込みます。

1
  ActiveSupport.run_load_hooks :hoge

そうするとrun load hooksが呼ばれたタイミングでhogeがついていてるActiveSupport.on _loadのブロック処理が開始されます。


コンソールでローディングを表示

rubyXLで100以上シートのあるエクセルファイルを読み出すスクリプトを書いていて、
現在の処理中のシート/シートの総数でプログレスバーを表示することはしたけど、
ファイル読み込み中(5~7秒ぐらいかかる)はなんにもコンソールに表示されない状態が物足りなくて、
ファイルの読み込み中はローディングチックなのを出すようにした。

読み込みと別でThread生成して、そのThread内で1秒毎に >と > >と > > >を出している。
読み込みが完了したらThreadをkillしてる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def display_with_arrow(&block)
  loading_thread = Thread.start{
                              arrow_progress = %w(> >> >>>).cycle
                              loop{
                                print "  r file reading #{sprintf("%-  s3s",arrow_progress.next)}"
                                sleep 1
                              }
                            }

  rtn = block.call
  Thread.kill(loading_thread)
  rtn
end

# 読み込み
workbook = display_with_arrow do
             RubyXL::Parser.parse("エクセルファイルへのパス")
           end

するとこうなる。
f:id:gogo  _sakura:20141106201955g:image


Axlsを使ってExcelでリスト(ドロップダウンするやつ)を作る

業務でエクセルを使うことになったので調べてました。

↓Qiitaの素晴らしいまとめ
RailsでExcelを扱うGemまとめ
http://qiita.com/Kta-M/items/02a2c41c5624f75498aa

↓情報はこの辺を見ました
http://stackoverflow.com/questions/17320645/axlsx-building-dynamic-columns

あまりエクセルのリストを作成する情報がなかったので書いておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require 'axlsx'
package = Axlsx::Package.new
sheet = package.workbook.add_worksheet(name: 'lists')

sheet.add_row(['品名', '単価', '数量', '計'])
sheet.add_row(['にんじん',    80, 1,      '=B2*C2'])
sheet.add_row(['たまねぎ',    50, 2,      '=B3*C3'])
sheet.add_row(['じゃがいも',  40, 2,      '=B4*C4'])
sheet.add_row(['牛肉',       200, 1,      '=B5*C5'])
sheet.add_row(['カレー粉',   150, 1,      '=B6*C6'])
sheet.add_row(['',            '', '総計', '=SUM(D2:D6)'])

sheet.add_data_validation("A10", {
  :type => :list,
  :formula1 => 'A2:A6',
  :showDropDown => false,
  :showErrorMessage => true,
  :errorTitle => '',
  :error => 'リストから選んで下さい',
  :errorStyle => :stop,
  :showInputMessage => true,
  :promptTitle => 'カレー具材',
  :prompt => 'カレーの具材を選んで下さい。'})

package.serialize('test.xlsx')

Facebookとのアプリの連携を解除する

調べてみたら自分所のDBからFacebookの情報を消すだけのものが多くて、大元のFacebookのアプリ設定から削除するのがあんまり載ってなかったようなきがするので

以下の感じで、連携解除ができます。

1
2
3
4
5
6
$facebook = new Facebook(array(
'appId' => アプリのapp_id,
'secret' => アプリのapp_secret
));

$facebook->api("/me/permissions", "delete", array('access_token' => ユーザーのアクセストークン));

珍しくPHPでした。


s3からcliでファイルをまとめて落としたり、正規表現で落とせたりするツール

s3 getter.rbとか名前つけて保存して
ruby s3
getter.rbとかで起動すると

最初にbucket選ぶ選択肢が出てきて選択すると、bucketのディレクトリかファイルか、ファイル全てダウンロードが選べ。
ディレクトリを選ぶと再帰的にまたディレクトリかファイルかファイル全てをダウンロードが選べます。

ファイルもしくは全てダウンロードを選ぶと、ダウンロードと同時に解答するか選べ(すいません。現状gzの解凍のみ)
また抽出するファイル名を正規表現で絞り込めるかを選べます。

動くの重視で片手間に作ったのでノー例外処理でございます。
gz圧縮以外にも対応したいとかあれば、ご自由に改変してお使いください。

Gist
https://gist.github.com/YoshitsuguFujii/349069b2e74c28153a17

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# coding: utf-8

require "aws-sdk"

S3_SAVE_DIR = "s3/"
ALL_FILE_DOWNLOAD = "全てのファイルを落とす"

s3 = AWS::S3.new(
  access_key_id: "YOUR AWS ACCESS KEY"
  secret_access_key: "YOUR AWS SECRET"
)

def show_item_with_pointer(iterator, console_print = true)
  rtn_hash = {}
  iterator.each_with_index do |value, idx|
    rtn_hash[idx.to_s] = value
    if console_print
      p "[" + (idx).to_s + "] " + value.inspect.to_s
    end
  end

  rtn_hash
end

def ls(tree)
  ls = []
  directories = tree.children.select(&:branch?).collect(&:prefix)
  files = tree.children.select(&:leaf?).collect(&:key)

  #all_select = files.length > 1 ?  [ALL_FILE_DOWNLOAD] : []
  all_select = [ALL_FILE_DOWNLOAD]
  ls = all_select.concat(directories.concat(files))
end

def get_or_into_directory_recursive(tree)
  file_and_directories = show_item_with_pointer(ls(tree))

  idx = gets_from_stdin("fileまたはdirectoryを数値で選択")

  file_or_directory = file_and_directories[idx]

  # select dir
  if file_or_directory.end_with?("/")
    tree = @bucket.as_tree({prefix: file_or_directory})
    objs = get_or_into_directory_recursive(tree)
  else
    objs = []
    # select all
    if file_or_directory == ALL_FILE_DOWNLOAD
      objs = get_objects(file_and_directories)
    # select file
    else
      objs << @bucket.objects[file_or_directory]
    end
  end

  return objs
end

# http://qiita.com/riocampos/items/cf71862bf975e13bdb4a
def progress_bar(i, max = 100)
  i = i.to_f
  max = max.to_f
  i = max if i > max
  percent = i / max * 100.0
  rest_size = 1 + 5 + 1 # space + progress_num + %
  bar_size = 79 - rest_size # (width - 1) - rest_size
  bar_str = '%-*s' % [bar_size, ('#' * (percent * bar_size / 100).to_i)]
  progress_num = '%3.1f' % percent
  print "  r#{bar_str} #{'%5s' % progress_num}%"
end


def get_objects(file_and_directories)
  objs = []
  file_and_directories.each do |idx, path|
    next if path == ALL_FILE_DOWNLOAD
    if path.end_with?("/")
      tree = @bucket.as_tree({prefix: path})
      recursive_file_and_directories = show_item_with_pointer(ls(tree), false)
      objs.concat(get_objects(recursive_file_and_directories))
    else
      objs << @bucket.objects[path]
    end
  end

  objs
end

def gets_from_stdin(message = nil)
  print message unless message.nil?
  str = STDIN.gets.chomp
end


def get_y_or_n_from_stdin(message)
  bol = ""
  while !%w(Y N).include?(bol.upcase)
    bol = gets_from_stdin("#{message}(y/n)")
  end
  bol.upcase == "Y"
end

all_buckets = show_item_with_pointer(s3.buckets)

idx = gets_from_stdin("bucketを数値で選択")

@bucket = all_buckets[idx]
tree = @bucket.as_tree

files = get_or_into_directory_recursive(tree)

s3_dir = File.expand_path(S3_SAVE_DIR)

# gz圧縮なら解凍するか問う
extract_bol = if files.any?{|file| file.key.include?("gz")}
                get_y_or_n_from_stdin("解凍を同時に行いますか?")
              else
                false
              end

# 正規表現抜き出しを行うか問う
regexp = nil
if get_y_or_n_from_stdin("正規表現によるファイル名絞り込みを行いますか?")
  regexp = Regexp.new(gets_from_stdin("正規表現を入力してください"))

  files = files.reject do |file|
    file_path = [s3_dir,file.key].join("/")
    File.basename(file_path).match(regexp).nil?
  end

end

# 保存処理
files.each.with_index(1) do |file, idx|
  file_path = [s3_dir,file.key].join("/")
  dir = File.dirname(file_path)
  FileUtils.mkdir_p(dir)

  File.open(file_path, 'wb') do |f|
    file.read do |chunk|
      f.write(chunk)
    end
  end

  if extract_bol
    `gunzip #{file_path}`
  end

  progress_bar((idx.to_f / files.length) * 100)
end

godでThe server is not available (or you do not have

色々と設定をいじっていて、godのプロセスが立ち上がらなくなった。

1
2
rbenv exec bundle exec god terminate
# => The server is not available (or you do not have permissions to access it)

多分rootとかで色々やった時に出きたpidファイルとかが悪さしてるんだろうなー。
って思ったけどどこにあるのか?

とりあえずRAILS _ROOTのtmpを見てみるけど。ない

軽くはまりつつも、/tmpにありました。
/tmp/god.12223.sock

コレを消して無事解決!


jQueryのappendが重かったので対応したこと

railsでチェックボックスで会社を選択して、ajax通信で返すjsの内容がこんな感じだった。

1
2
3
4
5
  <% @company.each do |company| %>
    <% company.member.each do |customer| %>
        $("#customer_area").append("<%= escape_javascript(render("make_customer_area", cid: customer.id)) %>");
    <% end %>
  <% end %>

重い。console.time仕込んで計測したらappendがめちゃめちゃ重い。
一回のappendで1秒ぐらいかかってた。

毎回appendじゃなくて、一回でappendしてみる

1
2
3
4
5
6
7
8
9
10
11
  var html = []
  var customer_area = $("#customer_area")
  <% @company.each do |company| %>
    <% company.member.each do |customer| %>
        html.push("<%= escape_javascript(render("make_customer_area", cid: customer.id)) %>");
    <% end %>
  <% end %>

  if ( html.length > 0 ){
    customer_area.append(html.join(''))
  }

あんまり変わらないなぁ。
というかappendやめよ。もうやめよ。

1
2
3
4
5
6
7
8
9
10
11
  var html = []
  var customer_area = document.getElementById("customer_area");
  <% @company.each do |company| %>
    <% company.member.each do |customer| %>
        html.push("<%= escape_javascript(render("make_customer_area", cid: customer.id)) %>");
    <% end %>
  <% end %>

  if ( html.length > 0 ){
    customer_area.innerHTML = customer_area.innerHTML + html.join('')
  }

約0.5秒ぐらい早くなった。いいね。
でもinnnerHTML使ってるから、何回か会社をクリックするとinnerHTMLにテキストが溜まりすぎてだんだん重くなる。
innnerHTML参照せずにappendChildで追加しよう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  var html = []
  var customer_area = document.getElementById("customer_area");
  <% @company.each do |company| %>
    <% company.member.each do |customer| %>
        html.push("<%= escape_javascript(render("make_customer_area", cid: customer.id)) %>");
    <% end %>
  <% end %>

  if ( html.length > 0 ){
    var element = document.createElement('div');
    element.innerHTML = html.join('');
    customer_area.appendChild(element);

  }

これで当初の半分ぐらいまでは処理時間が軽減されました。