fop 0.1.0

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.
@@ -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: []