base_scraper_service 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 69eb49ba30aa10adba230399397bc900df7f46b10da8c5ef7e02d00743fd7407
4
+ data.tar.gz: a7534672884d78323aa41838e1048c1cfff7a59d19a7f633034eaf9e8dc680d0
5
+ SHA512:
6
+ metadata.gz: b8957504e31a3a5519538bd7b5fecdb7837f502b5cdccea51c59d3c585bb26f5dc7642449808e3913d361ee778af74e06708dc54b5ed771cb8839434f1e7834c
7
+ data.tar.gz: 6227de3f572c770425145ef074dbbdbec0cebf8cd0c7f95ba4cc1d00a0ef24e60534e3537d5ae305d1a6af41688304b55fea00b4bd78b6a3b0e951ee5f28dea4
@@ -0,0 +1,28 @@
1
+ require_relative 'user_agent'
2
+ require "mechanize"
3
+ require "nokogiri"
4
+
5
+ module BaseScraper
6
+ class Service
7
+ def agent_object
8
+ agent = Mechanize.new
9
+ agent.read_timeout = 5
10
+ agent.open_timeout = 8
11
+ agent.keep_alive = false
12
+ agent.verify_mode = OpenSSL::SSL::VERIFY_NONE
13
+ agent.user_agent = UserAgent.random
14
+ agent.idle_timeout = 5
15
+ agent.pluggable_parser.default = Mechanize::Page
16
+
17
+ # Prevent from <Mechanize::Error: unsupported content-encoding: UTF-8>
18
+ content_encoding_hooks_func = lambda do |a, uri, resp, body_io|
19
+ if resp['Content-Encoding'].to_s == 'UTF-8'
20
+ resp['Content-Encoding'] = 'gzip'
21
+ end
22
+ end
23
+ agent.content_encoding_hooks << content_encoding_hooks_func
24
+
25
+ agent
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,94 @@
1
+ require 'sinatra'
2
+ require 'dotenv'
3
+ require "./#{ENV['SERVICE_NAME']}_offers_service"
4
+
5
+ configure { set :server, :puma }
6
+
7
+ class Base < Sinatra::Base
8
+
9
+ get "/#{ENV['SERVICE_NAME']}/isbn_offers" do
10
+ isbn = params[:isbn]
11
+ uuid = SecureRandom.uuid
12
+
13
+ @res = {
14
+ isbn: isbn,
15
+ uuid: uuid,
16
+ item_condition: params[:item_condition],
17
+ ship_type: params[:ship_type]
18
+ }
19
+
20
+ if valid_params?
21
+ service_response = Object.const_get(get_service_name).new(isbn, uuid).scrape_offers
22
+ offers = service_response[:offers]
23
+
24
+ offers = filter_offers(offers)
25
+ offers.sort_by! { |offer| offer[:total_price] }
26
+ offers_count = offers.count
27
+
28
+ @res[:status] = service_response[:status]
29
+ @res[:short_message] = service_response[:short_message]
30
+ @res[:long_message] = service_response[:long_message]
31
+ if @res[:status] == 200
32
+ @res[:offers_count] = offers.count
33
+ @res[:data] = { offers: offers }
34
+ end
35
+ else
36
+ @res[:status] = 400
37
+ @res[:short_message] = 'error'
38
+ end
39
+
40
+ halt @res[:status], @res.to_json
41
+ end
42
+
43
+ not_found do
44
+ send_file( 'public/404.html', { status: 404 })
45
+ end
46
+
47
+ def get_service_name
48
+ ENV['SERVICE_NAME'].capitalize + 'OffersService'
49
+ end
50
+
51
+ def send_sinatra_file(path)
52
+ file_path = File.join(File.dirname(__FILE__), 'public', path)
53
+ file_path = File.join(file_path, 'index.html') unless file_path =~ /\.[a-z]+$/i and !File.directory?(file_path)
54
+ File.exist?(file_path) ? send_file(file_path) : not_found
55
+ end
56
+
57
+ def filter_offers(offers)
58
+ offers = offers.select{ |offer| offer[:Item_Condition].downcase == params[:item_condition].downcase} if params[:item_condition].present?
59
+ offers = offers.select{ |offer| offer[:ship_type].downcase == params[:ship_type].downcase} if params[:ship_type].present?
60
+
61
+ offers
62
+ end
63
+
64
+ def constantize(lower_case_string)
65
+ "#{lower_case_string.split('_').collect(&:capitalize).join + 'OffersService'}".constantize
66
+ end
67
+
68
+ def valid_params?
69
+ valid_isbn?(params[:isbn]) &&
70
+ valid_book_condition?(params[:item_condition].to_s) &&
71
+ valid_ship_type?(params[:ship_type].to_s)
72
+ end
73
+
74
+ def valid_isbn?(isbn)
75
+ return true if StdNum::ISBN.valid?(isbn) && [10, 13].include?(isbn.size)
76
+
77
+ @res[:long_message] = 'invalid isbn'
78
+ false
79
+ end
80
+
81
+ def valid_book_condition?(item_condition)
82
+ return true if ['', 'used', 'new'].include?(item_condition.downcase)
83
+
84
+ @res[:long_message] = 'invalid item condition'
85
+ false
86
+ end
87
+
88
+ def valid_ship_type?(ship_type)
89
+ return true if ['', 'standard', 'expedited'].include?(ship_type.downcase)
90
+
91
+ @res[:long_message] = 'invalid ship type'
92
+ false
93
+ end
94
+ end
@@ -0,0 +1,90 @@
1
+ require 'mechanize'
2
+ require 'nokogiri'
3
+ require 'thread'
4
+ require 'fileutils'
5
+ require 'logger'
6
+ require "dotenv"
7
+ require 'csv'
8
+ require 'date'
9
+ require 'active_support/time'
10
+ require 'library_stdnums'
11
+ require 'base_scraper_service'
12
+ require_relative 'user_agent'
13
+ require_relative 'agent_object'
14
+
15
+ module BaseScraper
16
+ class Service
17
+ def send_request(agent, uri, params = nil, headers = {}, _optoins = {})
18
+ tries = 0
19
+ max_tries = 5
20
+ page = nil
21
+
22
+ refresh_agent(agent)
23
+ change_proxy(agent)
24
+ begin
25
+ page = params.nil? ? agent.get(uri, [], nil, headers) : agent.post(uri, params, headers)
26
+
27
+ rescue Mechanize::ResponseCodeError => e
28
+ change_proxy(agent)
29
+
30
+ tries += 1
31
+
32
+ agent.set_proxy(ENV["LPM_SERVER_URL"], ENV["LPM_SERVER_PORT"]) if tries >= 4
33
+
34
+ retry if tries < max_tries
35
+ rescue Exception => e
36
+ refresh_agent(agent)
37
+ change_proxy(agent)
38
+
39
+ tries += 1
40
+
41
+ agent.set_proxy(ENV["LPM_SERVER_URL"], ENV["LPM_SERVER_PORT"]) if tries >= 4
42
+
43
+ retry if tries < max_tries
44
+ puts "Message: #{e.message} from #{uri}, proxy: #{agent.proxy_addr}" if logs_enable?
45
+ end
46
+
47
+ page
48
+ end
49
+
50
+ def format_price(price)
51
+ return unless price.present?
52
+ price.to_s.strip.gsub("$", "").strip.to_f
53
+ end
54
+
55
+ def format_isbn(isbn)
56
+ StdNum::ISBN.convert_to_13(isbn)
57
+ end
58
+
59
+ def with_exception(long_message: "", status: 500, short_message: "exception")
60
+ return { offers: [], short_message: short_message, long_message: long_message, status: status }
61
+ end
62
+
63
+ def logs_enable?
64
+ ENV["ENABLE_LOGS"] == "true"
65
+ end
66
+
67
+ private
68
+
69
+ def refresh_agent(agent)
70
+ agent.cookie_jar.clear!
71
+ agent.history.clear
72
+ agent.user_agent = UserAgent.random
73
+ end
74
+
75
+ def get_proxy_index
76
+ (0..(@proxy_array.count - 1)).to_a.sample
77
+ end
78
+
79
+ def change_proxy(agent)
80
+ if @proxy_array.count > 0
81
+ cur_proxy_index = get_proxy_index
82
+ if @proxy_array[cur_proxy_index].present?
83
+ agent.set_proxy(@proxy_array[cur_proxy_index][0], @proxy_array[cur_proxy_index][1])
84
+ end
85
+ else
86
+ agent.set_proxy(ENV['LPM_SERVER_URL'], ENV['LPM_SERVER_PORT'])
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,348 @@
1
+ module BaseScraper
2
+ class Service
3
+ class LocationService
4
+ def initialize(address)
5
+ @address = address.to_s.gsub('.', '')
6
+ end
7
+
8
+ def extract_state
9
+ # 1- return state abbreviation if matched with state abbreviations
10
+ # 2- return state abbreviation if matched with state name
11
+ # 3- otherwise return empty string
12
+ state = us_states_hash.keys.select { |state_abbreviation| @address.upcase.include?(state_abbreviation) }.first
13
+ return state if state.present? && @address.length != 3
14
+ state = us_states_hash.select { |_, state| @address.include?(state) }.first&.first
15
+
16
+ return state.to_s
17
+ end
18
+
19
+ def extract_country
20
+ # 1- return country abbreviation if matched with country abbreviations
21
+ # 2- return country abbreviation if matched with country name
22
+ # 3- return USA if address contains a USA state
23
+ # 4- otherwise return empty string
24
+ country = countries_hash.keys.select { |country_abbreviation| @address.include?(country_abbreviation) }.first
25
+ puts country, @address
26
+ return country if country.present?
27
+ country = countries_hash.select { |_, country_name| @address.include?(country_name) }.first&.first
28
+ return country if country.present?
29
+
30
+ return 'USA' if extract_state.present?
31
+
32
+ return country.to_s
33
+ end
34
+
35
+ private
36
+
37
+ def us_states_hash
38
+ {
39
+ 'AL' => 'Alabama',
40
+ 'AK' => 'Alaska',
41
+ 'AZ' => 'Arizona',
42
+ 'AR' => 'Arkansas',
43
+ 'CA' => 'California',
44
+ 'CO' => 'Colorado',
45
+ 'CT' => 'Connecticut',
46
+ 'DE' => 'Delaware',
47
+ 'DC' => 'District Of Columbia',
48
+ 'FL' => 'Florida',
49
+ 'GA' => 'Georgia',
50
+ 'HI' => 'Hawaii',
51
+ 'ID' => 'Idaho',
52
+ 'IL' => 'Illinois',
53
+ 'IN' => 'Indiana',
54
+ 'IA' => 'Iowa',
55
+ 'KS' => 'Kansas',
56
+ 'KY' => 'Kentucky',
57
+ 'LA' => 'Louisiana',
58
+ 'ME' => 'Maine',
59
+ 'MD' => 'Maryland',
60
+ 'MA' => 'Massachusetts',
61
+ 'MI' => 'Michigan',
62
+ 'MN' => 'Minnesota',
63
+ 'MS' => 'Mississippi',
64
+ 'MO' => 'Missouri',
65
+ 'MT' => 'Montana',
66
+ 'NE' => 'Nebraska',
67
+ 'NV' => 'Nevada',
68
+ 'NH' => 'New Hampshire',
69
+ 'NJ' => 'New Jersey',
70
+ 'NM' => 'New Mexico',
71
+ 'NY' => 'New York',
72
+ 'NC' => 'North Carolina',
73
+ 'ND' => 'North Dakota',
74
+ 'OH' => 'Ohio',
75
+ 'OK' => 'Oklahoma',
76
+ 'OR' => 'Oregon',
77
+ 'PA' => 'Pennsylvania',
78
+ 'RI' => 'Rhode Island',
79
+ 'SC' => 'South Carolina',
80
+ 'SD' => 'South Dakota',
81
+ 'TN' => 'Tennessee',
82
+ 'TX' => 'Texas',
83
+ 'UT' => 'Utah',
84
+ 'VT' => 'Vermont',
85
+ 'VA' => 'Virginia',
86
+ 'WA' => 'Washington',
87
+ 'WV' => 'West Virginia',
88
+ 'WI' => 'Wisconsin',
89
+ 'WY' => 'Wyoming'
90
+ }
91
+ end
92
+
93
+ def countries_hash
94
+ {
95
+ 'AFG' => 'Afghanistan',
96
+ 'ALB' => 'Albania',
97
+ 'DZA' => 'Algeria',
98
+ 'ASM' => 'American Samoa',
99
+ 'AND' => 'Andorra',
100
+ 'AGO' => 'Angola',
101
+ 'AIA' => 'Anguilla',
102
+ 'ATA' => 'Antarctica',
103
+ 'ATG' => 'Antigua and Barbuda',
104
+ 'ARG' => 'Argentina',
105
+ 'ARM' => 'Armenia',
106
+ 'ABW' => 'Aruba',
107
+ 'AUS' => 'Australia',
108
+ 'AUT' => 'Austria',
109
+ 'AZE' => 'Azerbaijan',
110
+ 'BHS' => 'Bahamas',
111
+ 'BHR' => 'Bahrain',
112
+ 'BGD' => 'Bangladesh',
113
+ 'BRB' => 'Barbados',
114
+ 'BLR' => 'Belarus',
115
+ 'BEL' => 'Belgium',
116
+ 'BLZ' => 'Belize',
117
+ 'BEN' => 'Benin',
118
+ 'BMU' => 'Bermuda',
119
+ 'BTN' => 'Bhutan',
120
+ 'BOL' => 'Bolivia',
121
+ 'BES' => 'Bonaire, Sint Eustatius and Saba',
122
+ 'BIH' => 'Bosnia and Herzegovina',
123
+ 'BWA' => 'Botswana',
124
+ 'BVT' => 'Bouvet Island',
125
+ 'BRA' => 'Brazil',
126
+ 'IOT' => 'British Indian Ocean Territory',
127
+ 'BRN' => 'Brunei Darussalam',
128
+ 'BGR' => 'Bulgaria',
129
+ 'BFA' => 'Burkina Faso',
130
+ 'BDI' => 'Burundi',
131
+ 'CPV' => 'Cabo Verde',
132
+ 'KHM' => 'Cambodia',
133
+ 'CMR' => 'Cameroon',
134
+ 'CAN' => 'Canada',
135
+ 'CYM' => 'Cayman Islands',
136
+ 'CAF' => 'Central African Republic',
137
+ 'TCD' => 'Chad',
138
+ 'CHL' => 'Chile',
139
+ 'CHN' => 'China',
140
+ 'CXR' => 'Christmas Island',
141
+ 'CCK' => 'Cocos (Keeling) Islands',
142
+ 'COL' => 'Colombia',
143
+ 'COM' => 'Comoros',
144
+ 'COD' => 'Congo',
145
+ 'COG' => 'Congo',
146
+ 'COK' => 'Cook Islands',
147
+ 'CRI' => 'Costa Rica',
148
+ 'HRV' => 'Croatia',
149
+ 'CUB' => 'Cuba',
150
+ 'CUW' => 'Curaçao',
151
+ 'CYP' => 'Cyprus',
152
+ 'CZE' => 'Czechia',
153
+ 'CIV' => "Côte d'Ivoire",
154
+ 'DNK' => 'Denmark',
155
+ 'DJI' => 'Djibouti',
156
+ 'DMA' => 'Dominica',
157
+ 'DOM' => 'Dominican Republic',
158
+ 'ECU' => 'Ecuador',
159
+ 'EGY' => 'Egypt',
160
+ 'SLV' => 'El Salvador',
161
+ 'GNQ' => 'Equatorial Guinea',
162
+ 'ERI' => 'Eritrea',
163
+ 'EST' => 'Estonia',
164
+ 'SWZ' => 'Eswatini',
165
+ 'ETH' => 'Ethiopia',
166
+ 'FLK' => 'Falkland Islands',
167
+ 'FRO' => 'Faroe Islands',
168
+ 'FJI' => 'Fiji',
169
+ 'FIN' => 'Finland',
170
+ 'FRA' => 'France',
171
+ 'GUF' => 'French Guiana',
172
+ 'PYF' => 'French Polynesia',
173
+ 'ATF' => 'French Southern Territories',
174
+ 'GAB' => 'Gabon',
175
+ 'GMB' => 'Gambia',
176
+ 'GEO' => 'Georgia',
177
+ 'DEU' => 'Germany',
178
+ 'GHA' => 'Ghana',
179
+ 'GIB' => 'Gibraltar',
180
+ 'GRC' => 'Greece',
181
+ 'GRL' => 'Greenland',
182
+ 'GRD' => 'Grenada',
183
+ 'GLP' => 'Guadeloupe',
184
+ 'GUM' => 'Guam',
185
+ 'GTM' => 'Guatemala',
186
+ 'GGY' => 'Guernsey',
187
+ 'GIN' => 'Guinea',
188
+ 'GNB' => 'Guinea-Bissau',
189
+ 'GUY' => 'Guyana',
190
+ 'HTI' => 'Haiti',
191
+ 'HMD' => 'Heard Island and McDonald Islands',
192
+ 'VAT' => 'Holy See',
193
+ 'HND' => 'Honduras',
194
+ 'HKG' => 'Hong Kong',
195
+ 'HUN' => 'Hungary',
196
+ 'ISL' => 'Iceland',
197
+ 'IND' => 'India',
198
+ 'IDN' => 'Indonesia',
199
+ 'IRN' => 'Iran',
200
+ 'IRQ' => 'Iraq',
201
+ 'IRL' => 'Ireland',
202
+ 'IMN' => 'Isle of Man',
203
+ 'ISR' => 'Israel',
204
+ 'ITA' => 'Italy',
205
+ 'JAM' => 'Jamaica',
206
+ 'JPN' => 'Japan',
207
+ 'JEY' => 'Jersey',
208
+ 'JOR' => 'Jordan',
209
+ 'KAZ' => 'Kazakhstan',
210
+ 'KEN' => 'Kenya',
211
+ 'KIR' => 'Kiribati',
212
+ 'PRK' => "Korea",
213
+ 'KOR' => 'Korea',
214
+ 'KWT' => 'Kuwait',
215
+ 'KGZ' => 'Kyrgyzstan',
216
+ 'LAO' => "Lao People's Democratic Republic",
217
+ 'LVA' => 'Latvia',
218
+ 'LBN' => 'Lebanon',
219
+ 'LSO' => 'Lesotho',
220
+ 'LBR' => 'Liberia',
221
+ 'LBY' => 'Libya',
222
+ 'LIE' => 'Liechtenstein',
223
+ 'LTU' => 'Lithuania',
224
+ 'LUX' => 'Luxembourg',
225
+ 'MAC' => 'Macao',
226
+ 'MDG' => 'Madagascar',
227
+ 'MWI' => 'Malawi',
228
+ 'MYS' => 'Malaysia',
229
+ 'MDV' => 'Maldives',
230
+ 'MLI' => 'Mali',
231
+ 'MLT' => 'Malta',
232
+ 'MHL' => 'Marshall Islands',
233
+ 'MTQ' => 'Martinique',
234
+ 'MRT' => 'Mauritania',
235
+ 'MUS' => 'Mauritius',
236
+ 'MYT' => 'Mayotte',
237
+ 'MEX' => 'Mexico',
238
+ 'FSM' => 'Micronesia',
239
+ 'MDA' => 'Moldova',
240
+ 'MCO' => 'Monaco',
241
+ 'MNG' => 'Mongolia',
242
+ 'MNE' => 'Montenegro',
243
+ 'MSR' => 'Montserrat',
244
+ 'MAR' => 'Morocco',
245
+ 'MOZ' => 'Mozambique',
246
+ 'MMR' => 'Myanmar',
247
+ 'NAM' => 'Namibia',
248
+ 'NRU' => 'Nauru',
249
+ 'NPL' => 'Nepal',
250
+ 'NLD' => 'Netherlands',
251
+ 'NCL' => 'New Caledonia',
252
+ 'NZL' => 'New Zealand',
253
+ 'NIC' => 'Nicaragua',
254
+ 'NER' => 'Niger',
255
+ 'NGA' => 'Nigeria',
256
+ 'NIU' => 'Niue',
257
+ 'NFK' => 'Norfolk Island',
258
+ 'MNP' => 'Northern Mariana Islands',
259
+ 'NOR' => 'Norway',
260
+ 'OMN' => 'Oman',
261
+ 'PAK' => 'Pakistan',
262
+ 'PLW' => 'Palau',
263
+ 'PSE' => 'Palestine, State of',
264
+ 'PAN' => 'Panama',
265
+ 'PNG' => 'Papua New Guinea',
266
+ 'PRY' => 'Paraguay',
267
+ 'PER' => 'Peru',
268
+ 'PHL' => 'Philippines',
269
+ 'PCN' => 'Pitcairn',
270
+ 'POL' => 'Poland',
271
+ 'PRT' => 'Portugal',
272
+ 'PRI' => 'Puerto Rico',
273
+ 'QAT' => 'Qatar',
274
+ 'MKD' => 'Republic of North Macedonia',
275
+ 'ROU' => 'Romania',
276
+ 'RUS' => 'Russian Federation',
277
+ 'RWA' => 'Rwanda',
278
+ 'REU' => 'Réunion',
279
+ 'BLM' => 'Saint Barthélemy',
280
+ 'SHN' => 'Saint Helena, Ascension and Tristan da Cunha',
281
+ 'KNA' => 'Saint Kitts and Nevis',
282
+ 'LCA' => 'Saint Lucia',
283
+ 'MAF' => 'Saint Martin',
284
+ 'SPM' => 'Saint Pierre and Miquelon',
285
+ 'VCT' => 'Saint Vincent and Grenadines',
286
+ 'WSM' => 'Samoa',
287
+ 'SMR' => 'San Marino',
288
+ 'STP' => 'Sao Tome and Principe',
289
+ 'SAU' => 'Saudi Arabia',
290
+ 'SEN' => 'Senegal',
291
+ 'SRB' => 'Serbia',
292
+ 'SYC' => 'Seychelles',
293
+ 'SLE' => 'Sierra Leone',
294
+ 'SGP' => 'Singapore',
295
+ 'SXM' => 'Sint Maarten',
296
+ 'SVK' => 'Slovakia',
297
+ 'SVN' => 'Slovenia',
298
+ 'SLB' => 'Solomon Islands',
299
+ 'SOM' => 'Somalia',
300
+ 'ZAF' => 'South Africa',
301
+ 'SGS' => 'South Georgia and South Sandwich Islands',
302
+ 'SSD' => 'South Sudan',
303
+ 'ESP' => 'Spain',
304
+ 'LKA' => 'Sri Lanka',
305
+ 'SDN' => 'Sudan',
306
+ 'SUR' => 'Suriname',
307
+ 'SJM' => 'Svalbard and Jan Mayen',
308
+ 'SWE' => 'Sweden',
309
+ 'CHE' => 'Switzerland',
310
+ 'SYR' => 'Syrian Arab Republic',
311
+ 'TWN' => 'Taiwan',
312
+ 'TJK' => 'Tajikistan',
313
+ 'TZA' => 'Tanzania, United Republic of',
314
+ 'THA' => 'Thailand',
315
+ 'TLS' => 'Timor-Leste',
316
+ 'TGO' => 'Togo',
317
+ 'TKL' => 'Tokelau',
318
+ 'TON' => 'Tonga',
319
+ 'TTO' => 'Trinidad and Tobago',
320
+ 'TUN' => 'Tunisia',
321
+ 'TUR' => 'Turkey',
322
+ 'TKM' => 'Turkmenistan',
323
+ 'TCA' => 'Turks and Caicos Islands',
324
+ 'TUV' => 'Tuvalu',
325
+ 'UGA' => 'Uganda',
326
+ 'UKR' => 'Ukraine',
327
+ 'ARE' => 'United Arab Emirates',
328
+ 'GBR' => 'United Kingdom of Great Britain and Northern Ireland',
329
+ 'UMI' => 'United States Minor Outlying Islands',
330
+ 'USA' => 'United States of America',
331
+ 'URY' => 'Uruguay',
332
+ 'UZB' => 'Uzbekistan',
333
+ 'VUT' => 'Vanuatu',
334
+ 'VEN' => 'Venezuela',
335
+ 'VNM' => 'Viet Nam',
336
+ 'VGB' => 'Virgin Islands',
337
+ 'VIR' => 'Virgin Islands',
338
+ 'WLF' => 'Wallis and Futuna',
339
+ 'ESH' => 'Western Sahara',
340
+ 'YEM' => 'Yemen',
341
+ 'ZMB' => 'Zambia',
342
+ 'ZWE' => 'Zimbabwe',
343
+ 'ALA' => 'Åland Islands'
344
+ }
345
+ end
346
+ end
347
+ end
348
+ end