fop 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fcf19e8fcf71e09014e4401f353d50e7598f174e44f70ae6954938d0baaf23d0
4
+ data.tar.gz: 427ea3b525cfb67a95b7b7aeba49afdb2d233c8fce121be6fb3a668188979166
5
+ SHA512:
6
+ metadata.gz: 191c6a6288663a77c93514dd3f03ecdc632769251824473d47e93bb8c7085017d367f75ece4a3cba572ec7ba327d80d8a4a318f3ce6231c6a1333009892f03b3
7
+ data.tar.gz: a68c2b3698ac4913fa6cedc89557fe0f2db6af66b6e590e0b991c807b6b0c3c8eca2030b3c9fcd55bd104c2052531aa846d743d8611a160e9b8c3a5267593b3c
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+ before_install: gem install bundler -v 1.16.0
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in fop.gemspec
6
+ gemspec
@@ -0,0 +1,39 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fop (0.1.0)
5
+ nokogiri
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.3)
11
+ mini_portile2 (2.3.0)
12
+ nokogiri (1.8.1)
13
+ mini_portile2 (~> 2.3.0)
14
+ rake (12.3.0)
15
+ rspec (3.7.0)
16
+ rspec-core (~> 3.7.0)
17
+ rspec-expectations (~> 3.7.0)
18
+ rspec-mocks (~> 3.7.0)
19
+ rspec-core (3.7.0)
20
+ rspec-support (~> 3.7.0)
21
+ rspec-expectations (3.7.0)
22
+ diff-lcs (>= 1.2.0, < 2.0)
23
+ rspec-support (~> 3.7.0)
24
+ rspec-mocks (3.7.0)
25
+ diff-lcs (>= 1.2.0, < 2.0)
26
+ rspec-support (~> 3.7.0)
27
+ rspec-support (3.7.0)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ bundler
34
+ fop!
35
+ rake
36
+ rspec (~> 3.0)
37
+
38
+ BUNDLED WITH
39
+ 1.16.0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Sorah Fukumori
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,68 @@
1
+ # Fop: Scraping client for Japan Airlines FOP/Mileage calculator
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'fop'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install fop
18
+
19
+ ## Usage
20
+
21
+ ```
22
+ fop = Fop::Client.new
23
+
24
+ p fop.valid_cards
25
+ p fop.valid_statuses
26
+
27
+ ##
28
+
29
+ p fop.valid_intl_airports_for_earning #=> Array<Airport>
30
+ p fop.valid_intl_fares #=> Array<Fare>
31
+
32
+ p fop.intl_search(
33
+ from: 'TYO', # or Airport object
34
+ to: 'SFO', # or Airport object
35
+ fare: :first, # or Fare object
36
+ card: :global, # or CardType object
37
+ status: :sapphire, # or Status object
38
+ )
39
+
40
+ ##
41
+
42
+ p fop.valid_dom_airports #=> Array<Airport>
43
+ p fop.valid_dom_classes #=> Array<SeatClass>
44
+ p fop.valid_dom_fares #=> Array<Fare>
45
+
46
+ p fop.intl_search(
47
+ from: 'TYO', # or Airport object
48
+ to: 'SFO', # or Airport object
49
+ class: 'J', # or SeatClass object
50
+ fare: :discount_other, # or Fare object
51
+ card: :global, # or CardType object
52
+ status: :sapphire, # or Status object
53
+ )
54
+ ```
55
+
56
+ ## Development
57
+
58
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
59
+
60
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
61
+
62
+ ## Contributing
63
+
64
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sorah/fop.
65
+
66
+ ## License
67
+
68
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -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
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fop"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,28 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "fop/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fop"
8
+ spec.version = Fop::VERSION
9
+ spec.authors = ["Sorah Fukumori"]
10
+ spec.email = ["her@sorah.jp"]
11
+
12
+ spec.summary = %q{scraping client of Japan Airlines FOP/Mileage calculator}
13
+ spec.homepage = "https://github.com/sorah/fop"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "nokogiri"
24
+
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ end
@@ -0,0 +1,6 @@
1
+ require "fop/version"
2
+ require "fop/client"
3
+
4
+ module Fop
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,208 @@
1
+ require 'json'
2
+ require 'nokogiri'
3
+ require 'uri'
4
+ require 'open-uri'
5
+ require 'net/http'
6
+ require 'net/https'
7
+
8
+ module Fop
9
+ module StringIntoCode
10
+ def to_s
11
+ code
12
+ end
13
+ end
14
+
15
+ Airport = Struct.new(:code, :area, :name)
16
+ Status = Struct.new(:code, :name)
17
+ CardType = Struct.new(:code, :name)
18
+ Fare = Struct.new(:code, :name, :remark)
19
+ SeatClass = Struct.new(:code, :name)
20
+
21
+ [Airport, Status, CardType, Fare, SeatClass].each do |k|
22
+ k.class_eval do
23
+ include StringIntoCode
24
+ end
25
+ end
26
+
27
+ Result = Struct.new(:miles, :fop, :flight_miles, :flight_miles_remark, :standard_flight_miles, :bonus_miles, :bonus_miles_remark, :fop_rate, :fop_bonus, :fop_bonus_remark)
28
+
29
+ class Error < StandardError; end
30
+ class DataParseError < Error; end
31
+ class InvalidAirport < Error; end
32
+ class ConditionError < Error; end
33
+
34
+ ENDPOINT_URI = URI("https://www.jal.co.jp/cgi-bin/jal/milesearch/save/flt_mile_save.cgi")
35
+ DATA_JS_URI = URI("https://www.jal.co.jp/jmb/milesearch/js/mile_search_jp.js")
36
+
37
+ class Client
38
+ def initialize()
39
+ end
40
+
41
+ def dom_search(from:, to:, class:, fare:, card: '-', status: '-')
42
+ klass = binding.local_variable_get(:class)
43
+
44
+ unless valid_dom_airports.any?{ |_| _.code == from.to_s }
45
+ raise InvalidAirport, "#{from.inspect} is invalid airport"
46
+ end
47
+ unless valid_dom_airports.any?{ |_| _.code == to.to_s }
48
+ raise InvalidAirport, "#{to.inspect} is invalid airport"
49
+ end
50
+
51
+ page = post_form(
52
+ cmd: 'do_search',
53
+ TYPE: 'D',
54
+ external: '',
55
+ CityFrom: from.to_s,
56
+ CityTo: to.to_s,
57
+ F_CLASS: klass.to_s,
58
+ F_JAL_CARD: card.to_s,
59
+ F_JAL_CARD_STATUS: status.to_s,
60
+ F_FARE: fare.to_s,
61
+ )
62
+ parse_result(page.at('#contentDom'))
63
+ end
64
+
65
+ def intl_search(from:, to:, fare:, card: '-', status: '-')
66
+ from_airport = valid_intl_airports_for_earning.find{ |_| _.code == from.to_s }
67
+ to_airport = valid_intl_airports_for_earning.find{ |_| _.code == to.to_s }
68
+ unless from_airport
69
+ raise InvalidAirport, "#{from.inspect} is invalid airport"
70
+ end
71
+ unless to_airport
72
+ raise InvalidAirport, "#{to.inspect} is invalid airport"
73
+ end
74
+
75
+ page = post_form(
76
+ cmd: 'do_search',
77
+ TYPE: 'I',
78
+ external: '',
79
+ AreaFrom: from_airport.area,
80
+ AreaTo: to_airport.area,
81
+ CityFrom: from_airport.code,
82
+ CityTo: to_airport.code,
83
+ F_JAL_CARD: card.to_s,
84
+ F_JAL_CARD_STATUS: status.to_s,
85
+ F_FARE: fare.to_s,
86
+ )
87
+ parse_result(page.at('#contentInt'))
88
+ end
89
+
90
+ def parse_result(page)
91
+ if page.inner_text.include?("ご入力いただいた条件では検索できませんでした")
92
+ raise ConditionError, "Calculation not available for given query"
93
+ end
94
+
95
+ miles, fop = page.search('.milecount').map{ |_| _.inner_text.strip.gsub(/,/, '').to_i }
96
+ standard_flight_miles = page.search('.milecount')[0].parent.children[1].inner_text.gsub(/[^\d]+/,'').to_i
97
+
98
+ miles_breakdown, fop_breakdown = page.search('.Flightmilebns').map do |elem|
99
+ elem.search('.FlightmilebnsItem').reject{ |_| _['class'].include?('Mark') }
100
+ end
101
+
102
+ flight_miles_elem = miles_breakdown[0]
103
+ flight_miles = flight_miles_elem.at('.FlightmilebnsNum').inner_text.strip.gsub(/,/, '').to_i
104
+ flight_miles_remark = flight_miles_elem.at('.FlightmilebnsTitle').inner_text.gsub(/\s+|\r?\n/, ' ')
105
+
106
+ bonus_miles_elem = miles_breakdown[1]
107
+ if bonus_miles_elem
108
+ bonus_miles = bonus_miles_elem.at('.FlightmilebnsNum').inner_text.strip.gsub(/,/, '').to_i
109
+ bonus_miles_remark = bonus_miles_elem.at('.FlightmilebnsTitle').inner_text.gsub(/\s+|\r?\n/, ' ')
110
+ end
111
+
112
+ fop_rate_elem = fop_breakdown[1]
113
+ fop_rate = fop_rate_elem.at('.FlightmilebnsNum').inner_text.strip.gsub(/,/, '').to_f
114
+
115
+ fop_bonus_elem = fop_breakdown[2]
116
+ if fop_bonus_elem
117
+ fop_bonus = fop_bonus_elem.at('.FlightmilebnsNum').inner_text.strip.gsub(/,/, '').to_i
118
+ fop_bonus_remark = fop_bonus_elem.at('.FlightmilebnsTitle').inner_text.gsub(/\s+|\r?\n/, ' ')
119
+ end
120
+
121
+ Result.new(miles, fop, flight_miles, flight_miles_remark, standard_flight_miles, bonus_miles, bonus_miles_remark, fop_rate, fop_bonus, fop_bonus_remark)
122
+ end
123
+
124
+ def valid_cards
125
+ @valid_card_types ||= form_page.search('#intCardtype option').map do |option|
126
+ CardType.new(option['value'], option.inner_text.strip)
127
+ end
128
+ end
129
+
130
+ def valid_statuses
131
+ @valid_statuses ||= parse_data_js("status_hash").fetch('g_club').map do |(name, code)|
132
+ Status.new(code.to_s, name)
133
+ end
134
+ end
135
+
136
+ def valid_intl_airports_for_earning
137
+ @valid_airports_for_earning ||= parse_data_js("save_city_hash").flat_map do |area, airports|
138
+ airports.map do |(name, code)|
139
+ Airport.new(code, area, name)
140
+ end
141
+ end
142
+ end
143
+
144
+ def valid_dom_airports
145
+ @valid_dom_airports ||= parse_data_js("dom_city").map do |(name, code)|
146
+ Airport.new(code, nil, name)
147
+ end
148
+ end
149
+
150
+ def valid_dom_classes
151
+ @valid_dom_classes ||= form_page.search('#domClass option').map do |option|
152
+ SeatClass.new(option['value'], option.inner_text.strip)
153
+ end
154
+ end
155
+
156
+ def valid_dom_fares
157
+ @valid_dom_fares ||= begin
158
+ feelist = form_page.search('.feelist')[0].search('tr').map do |tr|
159
+ tr.search('td').map(&:inner_text).map(&:strip)
160
+ end.reject(&:empty?).to_h
161
+
162
+ form_page.search('#domFare option').map do |option|
163
+ name = option.inner_text.strip
164
+ remark = feelist[name]
165
+ Fare.new(option['value'], name, remark)
166
+ end
167
+ end
168
+ end
169
+
170
+ def valid_intl_fares
171
+ @valid_intl_fares ||= begin
172
+ feelist = form_page.search('.feelist')[1].search('tr').map do |tr|
173
+ tr.search('td').map(&:inner_text).map(&:strip)
174
+ end.reject(&:empty?).to_h
175
+
176
+ form_page.search('#intFare option').map do |option|
177
+ name = option.inner_text.strip
178
+ remark = feelist[name]
179
+ Fare.new(option['value'], name, remark)
180
+ end
181
+ end
182
+ end
183
+
184
+
185
+ def parse_data_js(variable)
186
+ m = data_js.match(/^var #{Regexp.escape(variable)} = (.+?\r?\n[\]}]);$/m)
187
+ raise DataParseError, "Failed to extract #{variable.inspect} from data_js" unless m
188
+ js = m[1]
189
+ json_like = js.
190
+ gsub(/^\s*"(.+?)"\s*:|^\s*(.+?)\s*:/,'"\1\2":')
191
+ JSON.parse json_like
192
+ rescue JSON::ParserError
193
+ raise DataParseError, "Failed to parse #{variable.inspect} from data_js"
194
+ end
195
+
196
+ def post_form(data)
197
+ Nokogiri::HTML(Net::HTTP.post_form(ENDPOINT_URI, data).tap(&:value).body.encode("UTF-8", "Shift_JIS"))
198
+ end
199
+
200
+ def form_page
201
+ @form_page ||= Nokogiri::HTML(open(ENDPOINT_URI, 'r', &:read).encode("UTF-8", "Shift_JIS"))
202
+ end
203
+
204
+ def data_js
205
+ @data_js ||= open(DATA_JS_URI, 'r', &:read).encode("UTF-8", "Shift_JIS")
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,3 @@
1
+ module Fop
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fop
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sorah Fukumori
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description:
70
+ email:
71
+ - her@sorah.jp
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".travis.yml"
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - bin/console
85
+ - bin/setup
86
+ - fop.gemspec
87
+ - lib/fop.rb
88
+ - lib/fop/client.rb
89
+ - lib/fop/version.rb
90
+ homepage: https://github.com/sorah/fop
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.7.3
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: scraping client of Japan Airlines FOP/Mileage calculator
114
+ test_files: []