hatebu_entry 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0aab4957200e35089c390385b7783515754062e9
4
+ data.tar.gz: 34e4799ece62fabbfecaa37c488ed7a53a6b383d
5
+ SHA512:
6
+ metadata.gz: 7a780f297fd288a18adfecbdf8fc17acd19c6f012fe129d15154229094468bb443ef1e77ac9a8ec608b0e17634a6e8cfbaa10590abfb96456eef27408cd78be5
7
+ data.tar.gz: 06a8e2c519b77497d4c88a48904670bef01b5bee8fed6ca31cd5548c0ac00ae2d581a313d190af8684e4a5ae0f082db4198781beb9ab0830a2c4b9902da5871b
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hatebu_entry.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 kyoendo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # HatebuEntry
2
+
3
+ HatebuEntry is a tool for retrieving and handling Hatena Bookmark entry lists written in Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'hatebu_entry'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install hatebu_entry
18
+
19
+ ## Usage
20
+
21
+ Try this;
22
+
23
+ ```ruby
24
+ require 'hatebu_entry'
25
+
26
+ uri = 'http://d.hatena.ne.jp'
27
+ hent = HatebuEntry.new(uri)
28
+
29
+ puts hent.entries
30
+ ```
31
+
32
+ You will get like this.
33
+
34
+  6691: 僕は自分が思っていたほどは頭がよくなかった - しのごの... (http://d.hatena.ne.jp/tictac/20120110/p1)
35
+  5788: 「 2 」か「 9 」で割ってみる - ナイトシフト (http://d.hatena.ne.jp/nightshift/20090121/1232521713)
36
+  5605: 読みやすい文章を書くための技法 - RyoAnna’s iPhone Blog (http://d.hatena.ne.jp/RyoAnna/20100824/1282660678)
37
+  4483: この「いじめ対策」はすごい! - 森口朗公式ブログ (http://d.hatena.ne.jp/moriguchiakira/20090520)
38
+  4167: 知らないと損する英語の速読方法(1) - 一法律学徒の英語... (http://d.hatena.ne.jp/kousuke-i/20081203/1228314824)
39
+  3973: パワポでもここまでできる!米財務省から学べる美しい資... (http://d.hatena.ne.jp/stj064/20120401/p1)
40
+  3968: デジタル一眼レフカメラの基礎から実践まで - #RyoAnnaBlog (http://d.hatena.ne.jp/RyoAnna/20120501/1335884196)
41
+  3853: 『忙しい人』と『仕事ができる人』の20の違い (http://d.hatena.ne.jp/favre21/20070927)
42
+  3717: MacBook Air 11インチ欲しい!とは - はてなキーワード (http://d.hatena.ne.jp/keyword/MacBook%20Air%2011%A5%A4%A5%F3%A5%C1%CD%DF%A4%B7%A4%A4%A1%AA)
43
+  3715: おさえておきたいメールで使う敬語 - かみんぐあうとっ (http://d.hatena.ne.jp/komoko-i/20110524/p1)
44
+
45
+
46
+ ## Contributing
47
+
48
+ 1. Fork it
49
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
50
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
51
+ 4. Push to the branch (`git push origin my-new-feature`)
52
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/hatebu_entry ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'hatebu_entry'
4
+
5
+ HatebuEntry::Command.start(ARGV)
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hatebu_entry/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hatebu_entry"
8
+ spec.version = HatebuEntry::VERSION
9
+ spec.authors = ["kyoendo"]
10
+ spec.email = ["postagie@gmail.com"]
11
+ spec.description = %q{HatebuEntry is a tool for retrieving and handling Hatena Bookmark entry lists written in Ruby.}
12
+ spec.summary = %q{Hatena bookmark entry list handler}
13
+ spec.homepage = "https://github.com/melborne/hatebu_entry"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = '>= 1.9.3'
22
+ spec.add_runtime_dependency 'nokogiri'
23
+ spec.add_runtime_dependency 'thor'
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "fakeweb", "~> 1.3"
28
+ end
@@ -0,0 +1,47 @@
1
+ require 'thor'
2
+
3
+ class HatebuEntry::Command < Thor
4
+ desc "get URL", "Get Hatena bookmark entries for URL"
5
+ option :pages, aliases:"-p", default:0
6
+ option :sort, aliases:"-s", default:"count"
7
+ def get(url)
8
+ entries = HatebuEntry.new(url, options[:sort])
9
+ .entries(options[:pages].to_i)
10
+ pretty_print begin
11
+ options[:sort]=='count' ? entries.sort_by { |ent| -ent.count } : entries
12
+ end
13
+ rescue
14
+ abort "something go wrong."
15
+ end
16
+
17
+ desc "merge *URLs", "Merge counts for same entries on several URLs"
18
+ option :pages, aliases:"-p", default:1
19
+ option :sort, aliases:"-s", default:"count"
20
+ def merge(*urls)
21
+ abort "At least 2 urls needed." if urls.size < 2
22
+ entries = urls.map do |url|
23
+ HatebuEntry.new(url, options[:sort]).entries options[:pages].to_i
24
+ end
25
+ merged = entries.inject {|mem, ent| HatebuEntry::Entry.merge(mem, ent) }
26
+ pretty_print merged.sort_by { |ent| -ent.count }
27
+ end
28
+
29
+ desc "version", "Show HatebuEntry version"
30
+ def version
31
+ puts "HatebuEntry #{HatebuEntry::VERSION} (c) 2013 kyoendo"
32
+ end
33
+ map "-v" => :version
34
+
35
+ no_commands do
36
+ def pretty_print(entries)
37
+ lines = entries.map do |ent|
38
+ "%5d: %s (%s)" % [ent.count, c(ent.title), ent.link]
39
+ end
40
+ puts lines
41
+ end
42
+
43
+ def c(str, n='32')
44
+ "\e[#{n}m#{str}\e[0m"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ class HatebuEntry
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,144 @@
1
+ require "hatebu_entry/version"
2
+ require 'hatebu_entry/command'
3
+ require 'open-uri'
4
+ require 'json'
5
+ require 'date'
6
+ require 'cgi'
7
+
8
+ require 'nokogiri'
9
+
10
+ class HatebuEntry
11
+ # link: uri string
12
+ # count: integer
13
+ # title: string
14
+ class Entry < Struct.new(:link, :count, :title)
15
+ alias :url :link
16
+ alias :bookmarks :count
17
+ def date
18
+ @date ||= Date.parse(link)
19
+ rescue ArgumentError
20
+ nil
21
+ end
22
+
23
+ def to_s
24
+ "%5d: %s (%s)" % [count, title, link]
25
+ end
26
+
27
+ # to find same entry but other hosts
28
+ def homogeneous?(other)
29
+ return false if self == other
30
+ return nil if [self, other].any? { |e| e.date.nil? }
31
+ self.date == other.date &&
32
+ title_similar?(self.title, other.title)
33
+ end
34
+ alias :same? :homogeneous?
35
+
36
+ def title_similar?(a, b)
37
+ a, b = [a, b].map { |str| str.gsub(/\w+/, '')[0..5] }
38
+ a == b
39
+ end
40
+ private :title_similar?
41
+
42
+ class MergeError < StandardError; end
43
+
44
+ # Merge its count
45
+ def merge(other)
46
+ count = self.count + other.count
47
+ if block_given? && !yield(self, other)
48
+ raise MergeError, "They can't merge"
49
+ else
50
+ Entry.new(self.link, count, self.title)
51
+ end
52
+ end
53
+
54
+ # Merge two entry lists
55
+ def self.merge(ls_a, ls_b)
56
+ if [ls_a, ls_b].all? { |ls| ls.is_a? Entry }
57
+ raise ArgumentError, 'Arguments must be entry objects'
58
+ end
59
+ entries = []
60
+ ls_a.each do |a|
61
+ if m = ls_b.detect { |b| a.homogeneous? b }
62
+ entries.push a.merge(m)
63
+ ls_b.delete(m)
64
+ else
65
+ entries.push a
66
+ end
67
+ end
68
+ entries + ls_b
69
+ end
70
+ end
71
+
72
+ attr_accessor :params
73
+ #sort: count, eid or hot
74
+ def initialize(site, sort='count')
75
+ @params = {url: site, sort: sort, of: 0*20}
76
+ end
77
+
78
+ def entries(pages=0)
79
+ if pages <= 0
80
+ get_entries(:jsonp) # get 10 entries with jsonp
81
+ else
82
+ # get 20 entries per page with html
83
+ mutex = Mutex.new
84
+ pages.times.map { |i|
85
+ Thread.fork(i) do |_i|
86
+ mutex.synchronize {
87
+ params.update(of: _i*20)
88
+ get_entries(:html)
89
+ }
90
+ end
91
+ }.map(&:value).flatten
92
+ end
93
+ end
94
+
95
+ def get_entries(api)
96
+ entries =
97
+ case api
98
+ when :jsonp then parse_jsonp(call_hatena_entry_api :jsonp)
99
+ when :html then parse_html(call_hatena_entry_api :html)
100
+ end
101
+ entries.map { |h| Entry.new h['link'], Integer(h['count']), h['title'] }
102
+ end
103
+
104
+ def call_hatena_entry_api(api)
105
+ uri = {jsonp: build_uri('/json?'), html: build_uri('?')}[api]
106
+ get uri
107
+ end
108
+
109
+ def get(uri)
110
+ open(uri).read
111
+ rescue => e
112
+ abort "HTTP Access Error: #{e.response}"
113
+ end
114
+
115
+ HatenaURI = "http://b.hatena.ne.jp/entrylist"
116
+
117
+ def build_uri(joint, params=@params)
118
+ HatenaURI + joint + build_params(params)
119
+ end
120
+
121
+ def build_params(params)
122
+ params.map { |k, v| "#{h k}=#{h v}" } * '&'
123
+ end
124
+
125
+ def h(str)
126
+ CGI.escape(str.to_s)
127
+ end
128
+
129
+ def parse_jsonp(jsonp)
130
+ jsonp.scan(/{.+?}/).map { |data| JSON.parse data }
131
+ end
132
+
133
+ def parse_html(html)
134
+ entries = []
135
+ doc = Nokogiri::HTML(html)
136
+ doc.css('li.entry-unit').each do |ent|
137
+ count = ent['data-bookmark-count'].to_i
138
+ a = ent.at('.entry-contents a')
139
+ title, href = a['title'], a['href']
140
+ entries << {'link' => href, 'count' => count, 'title' => title}
141
+ end
142
+ entries
143
+ end
144
+ end
data/sample/sample.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'hatebu_entry'
2
+
3
+ githubio = 'http://melborne.github.io'
4
+ githubcom = 'http://melborne.github.com'
5
+ hatena = 'http://d.hatena.ne.jp/keyesberry'
6
+
7
+ def get_entries(url_list, pages=1)
8
+ url_list.map do |url|
9
+ puts "Bookmark entries retrieving from #{url}..."
10
+ HatebuEntry.new(url).entries(pages).tap do |s|
11
+ puts "#{s.size} entries retrieved."
12
+ end
13
+ end
14
+ end
15
+
16
+ gitio_ent, gitcom_ent, hatena_ent = get_entries([githubio, githubcom, hatena], 5)
17
+
18
+ puts "\nMerging entries..."
19
+
20
+ entries = HatebuEntry::Entry.merge(gitio_ent, gitcom_ent)
21
+ entries = HatebuEntry::Entry.merge(entries, hatena_ent)
22
+
23
+ puts "\nFollowing is top 20 entries."
24
+ puts
25
+
26
+ puts entries.lazy
27
+ .sort_by{ |e| -e.count }
28
+ .map { |h| "%i: %s (%s)" % [h.count, h.title, h.link] }
29
+ .take(20)
@@ -0,0 +1,149 @@
1
+ require 'spec_helper'
2
+
3
+ describe HatebuEntry do
4
+ it 'should have a version number' do
5
+ HatebuEntry::VERSION.should_not be_nil
6
+ end
7
+
8
+ before(:each) do
9
+ FakeWeb.clean_registry
10
+ site = "http://melborne.github.com"
11
+ @hent = HatebuEntry.new(site)
12
+ end
13
+
14
+ describe '#build_json_uri' do
15
+ it 'returns json uri for Hatena bookmark entry' do
16
+ expected = "http://b.hatena.ne.jp/entrylist/json?" +
17
+ "url=http%3A%2F%2Fmelborne.github.com&sort=count&of=0"
18
+ expect(@hent.build_uri '/json?').to eq expected
19
+ end
20
+ end
21
+
22
+ describe '#call_hatena_entry_api' do
23
+ it 'returns jsonp data of Hatena bookmark info' do
24
+ uri = @hent.build_uri('/json?')
25
+ mock_hatebu_entry_api(uri)
26
+ expect(@hent.call_hatena_entry_api :jsonp).to match /^\(\[{"link":.+}\]\);$/
27
+ end
28
+
29
+ it 'returns empty jsonp data with a no bookmarked site' do
30
+ site = "http://melborne.github.co.jp"
31
+ hent = HatebuEntry.new(site)
32
+ mock_hatebu_entry_api('http://sample.com', 'no_entry.jsonp')
33
+ expect(hent.call_hatena_entry_api :jsonp).to eq "([]);"
34
+ end
35
+ end
36
+
37
+ describe '#get_entries' do
38
+ context 'gets Bookmark entries with jsonp' do
39
+ it 'returns entries with an array' do
40
+ uri = @hent.build_uri('/json?')
41
+ mock_hatebu_entry_api(uri)
42
+ entries = @hent.get_entries(:jsonp)
43
+ expect(entries.size).to eq 10
44
+ expect(entries.first).to be_instance_of(HatebuEntry::Entry)
45
+ expect(entries.first.count).to eq 1377
46
+ end
47
+ end
48
+
49
+ context 'gets Bookmark entries with html' do
50
+ it 'returns entries with an array' do
51
+ uri = @hent.build_uri('?')
52
+ mock_hatebu_entry_api(uri, 'hatebu_entry0.html')
53
+ entries = @hent.get_entries(:html)
54
+ expect(entries.size).to eq 20
55
+ expect(entries.first).to be_instance_of(HatebuEntry::Entry)
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '#parse_jsonp' do
61
+ it 'returns an array of hashes containing entry data' do
62
+ str = fixture('hatebu_entry.jsonp')
63
+ entries = @hent.parse_jsonp(str)
64
+ expect(entries).to be_instance_of(Array)
65
+ expect(entries.first).to be_instance_of(Hash)
66
+ end
67
+ end
68
+
69
+ describe '#parse_html' do
70
+ it 'returns an array of hashes containing entry data' do
71
+ str = fixture('hatebu_entry0.html')
72
+ entries = @hent.parse_html(str)
73
+ expect(entries).to be_instance_of(Array)
74
+ expect(entries.first).to be_instance_of(Hash)
75
+ end
76
+ end
77
+
78
+ describe '#entries' do
79
+ it 'returns entries with jsonp' do
80
+ uri = @hent.build_uri('/json?')
81
+ mock_hatebu_entry_api(uri)
82
+ entries = @hent.entries
83
+ expect(entries.size).to eq 10
84
+ end
85
+
86
+ context 'get entries with html' do
87
+ before(:each) do
88
+ uri = @hent.build_uri('?')
89
+ 3.times do |i|
90
+ @hent.params.update(of: i*20)
91
+ mock_hatebu_entry_api(uri, "hatebu_entry#{i}.html")
92
+ end
93
+ end
94
+
95
+ it 'returns first 20 entries' do
96
+ entries = @hent.entries(1)
97
+ expect(entries.size).to eq 20
98
+ end
99
+
100
+ it 'returns first 60 entries' do
101
+ entries = @hent.entries(3)
102
+ expect(entries.size).to eq 60
103
+ expect(entries.last.title).to match /Graphviz/
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ describe HatebuEntry::Entry do
110
+ let(:ent) { HatebuEntry::Entry }
111
+ before(:each) do
112
+ @ent1 = ent.new("http://d.hatena.ne.jp/keyesberry/20130304/p1", 20,"知って得する!55のRubyのトリビアな記法")
113
+ @ent2 = ent.new("http://melborne.github.io/2013/03/04/ruby/", 10,"知って得する!55のRubyのトリビアな記法")
114
+ @ent3 = ent.new("http://melborne.github.com/2013/03/05/ruby/", 10,"知って得する!55のRubyのトリビアな記法")
115
+ @ent4 = ent.new("http://melborne.github.com/2013/03/04/ruby/", 10,"55のRubyのトリビアな記法")
116
+ end
117
+
118
+ describe '#homogeneous?' do
119
+ it 'returns true when date and title are same' do
120
+ expect(@ent1.homogeneous? @ent2).to be_true
121
+ end
122
+
123
+ it 'returns false when title are same but date are not' do
124
+ expect(@ent1.homogeneous? @ent3).to be_false
125
+ end
126
+
127
+ it 'returns false when date are same but title are not' do
128
+ expect(@ent1.homogeneous? @ent4).to be_false
129
+ end
130
+ end
131
+
132
+ describe '#merge' do
133
+ it 'merge others count amount to self count' do
134
+ merged = @ent1.merge(@ent2)
135
+ expect(merged.count).to eq 30
136
+ end
137
+
138
+ context 'merge when these are homogeneous' do
139
+ it 'should success when they are homogeneous' do
140
+ merged = @ent1.merge(@ent2) { |a, b| a.homogeneous? b }
141
+ expect(merged.count).to eq 30
142
+ end
143
+
144
+ it 'should fail when they are not homogeneous' do
145
+ expect{ @ent1.merge(@ent3) { |a, b| a.homogeneous? b } }.to raise_error(HatebuEntry::Entry::MergeError)
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'hatebu_entry'
3
+ require 'fakeweb'
4
+
5
+ module HelperMethods
6
+ def fixture(name)
7
+ File.read("#{__dir__}/support/#{name}")
8
+ end
9
+
10
+ def mock_hatebu_entry_api(uri, fix='hatebu_entry.jsonp')
11
+ response ||= fixture(fix)
12
+ FakeWeb.register_uri(:get, uri, :body => response)
13
+ rescue Errno::ENOENT
14
+ response = fixture("no_entry.jsonp")
15
+ retry
16
+ end
17
+ end
18
+
19
+ RSpec.configuration.include(HelperMethods)
@@ -0,0 +1 @@
1
+ ([{"link":"http://melborne.github.com/2012/04/09/to-newbie/","count":"1377","title":"これからRubyを始める人たちへ"},{"link":"http://melborne.github.com/2013/03/04/ruby-trivias-you-should-know-4/","count":"633","title":"知って得する!55のRubyのトリビアな記法"},{"link":"http://melborne.github.com/2013/02/25/i-wanna-say-something-about-rubys-case/","count":"514","title":"Rubyのcaseを〇〇(言語名)のswitch文だと思っている人たちにぼ..."},{"link":"http://melborne.github.com/2011/06/22/21-Ruby-21-Trivia-Notations-you-should-know-in-Ruby/","count":"442","title":"知って得する21のRubyのトリビアな記法"},{"link":"http://melborne.github.com/2012/09/09/understand-js-oop-with-ruby-brain/","count":"435","title":"Ruby脳が理解するJavaScriptのオブジェクト指向"},{"link":"http://melborne.github.com/2012/12/25/ebooks-for-learning-ruby/","count":"269","title":"今年の冬休みに電子書籍であなたがRubyを習得しなければい..."},{"link":"http://melborne.github.com/2012/07/16/ruby-methods-analysis/","count":"236","title":"Ruby、君のオブジェクトはなんて呼び出せばいいの?"},{"link":"http://melborne.github.com/2013/01/21/why-fp-with-ruby/","count":"187","title":"Rubyを使って「なぜ関数プログラミングは重要か」を読み解..."},{"link":"http://melborne.github.com/2012/09/15/understand-js-oop-with-ruby-brain-2/","count":"178","title":"Ruby脳が理解するJavaScriptのオブジェクト指向(その2)"},{"link":"http://melborne.github.com/2013/01/24/csv-table-method-is-awesome/","count":"145","title":"Ruby標準添付ライブラリcsvのCSV.tableメソッドが最強な件につ..."}]);