hatebu_entry 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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メソッドが最強な件につ..."}]);