bnet_scraper 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +12 -0
- data/README.md +1 -1
- data/bnet_scraper.gemspec +3 -3
- data/lib/bnet_scraper/starcraft2.rb +42 -39
- data/lib/bnet_scraper/starcraft2/achievement.rb +24 -0
- data/lib/bnet_scraper/starcraft2/achievement_scraper.rb +50 -26
- data/lib/bnet_scraper/starcraft2/base_scraper.rb +34 -18
- data/lib/bnet_scraper/starcraft2/match_history_scraper.rb +22 -13
- data/lib/bnet_scraper/starcraft2/profile.rb +19 -1
- data/lib/bnet_scraper/starcraft2/profile_scraper.rb +102 -38
- data/lib/bnet_scraper/starcraft2/status_scraper.rb +4 -3
- data/spec/spec_helper.rb +1 -1
- data/spec/starcraft2/achievement_scraper_spec.rb +33 -23
- data/spec/starcraft2/match_history_scraper_spec.rb +2 -0
- data/spec/starcraft2/profile_scraper_spec.rb +18 -11
- data/spec/starcraft2/profile_spec.rb +13 -0
- data/spec/starcraft2/status_scraper_spec.rb +2 -2
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_achievements.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_leagues.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_match_history.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_matches.yml +0 -0
- data/spec/support/fixtures/vcr_cassettes/demon_profile.yml +1768 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_profile_leagues.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/full_demon_scrape.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/invalid_achievement.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/invalid_leagues.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/invalid_matches.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/invalid_profile.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/new_league.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/profile_invalid.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/profile_not_laddered.yml +0 -0
- data/{fixtures → spec/support/fixtures}/vcr_cassettes/realm_status.yml +0 -0
- metadata +86 -35
- data/fixtures/vcr_cassettes/demon_profile.yml +0 -1428
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Changelog!
|
2
2
|
|
3
|
+
## 0.5.0 (Mar 29 2013)
|
4
|
+
|
5
|
+
* Adds Heart of the Swarm Portrait Names
|
6
|
+
* Updates Achievement Progress Categories for Heart of the Swarm
|
7
|
+
* Adds Achievement to replace hash output
|
8
|
+
* Replace FakeWeb specs with VCR
|
9
|
+
* Typecast numerics to Fixnum instead of strings
|
10
|
+
* Typecast dates to Date instead of Strings (`Achievement#earned=`)
|
11
|
+
* Fix wins/losses in MatchHistoryScraper
|
12
|
+
* Adds Campaign Completion indicators (`Profile#campaign_completion`)
|
13
|
+
* Extensive internal refactoring and decoupling
|
14
|
+
|
3
15
|
## 0.4.0 (Feb 22 2013)
|
4
16
|
|
5
17
|
* Revamped ProfileScraper API
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# BnetScraper [![Build Status](https://secure.travis-ci.org/agoragames/bnet_scraper.png)](http://travis-ci.org/agoragames/bnet_scraper)
|
1
|
+
# BnetScraper [![Build Status](https://secure.travis-ci.org/agoragames/bnet_scraper.png)](http://travis-ci.org/agoragames/bnet_scraper) [![Code Climate](https://codeclimate.com/github/agoragames/bnet_scraper.png)](https://codeclimate.com/github/agoragames/bnet_scraper)
|
2
2
|
|
3
3
|
BnetScraper is a Nokogiri-based scraper of Battle.net profile information. Currently this only includes Starcraft2.
|
4
4
|
|
data/bnet_scraper.gemspec
CHANGED
@@ -3,9 +3,9 @@ $:.push File.expand_path("../lib", __FILE__)
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "bnet_scraper"
|
6
|
-
s.version = "0.
|
6
|
+
s.version = "0.5.0"
|
7
7
|
s.authors = ["Andrew Nordman"]
|
8
|
-
s.email = ["
|
8
|
+
s.email = ["cadwallion@gmail.com"]
|
9
9
|
s.homepage = "https://github.com/agoragames/bnet_scraper/"
|
10
10
|
s.summary = %q{Battle.net Profile Scraper}
|
11
11
|
s.description = %q{BnetScraper is a Nokogiri-based scraper of Battle.net profile information. Currently this only includes Starcraft2.}
|
@@ -18,8 +18,8 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.add_runtime_dependency 'nokogiri'
|
19
19
|
s.add_runtime_dependency 'faraday'
|
20
20
|
s.add_development_dependency 'rake'
|
21
|
-
s.add_development_dependency 'rspec'
|
22
21
|
s.add_development_dependency 'fakeweb'
|
22
|
+
s.add_development_dependency 'rspec'
|
23
23
|
s.add_development_dependency 'vcr'
|
24
24
|
s.add_development_dependency 'pry'
|
25
25
|
end
|
@@ -38,47 +38,50 @@ module BnetScraper
|
|
38
38
|
# I decided th pad the arrays even if there are no images to make various
|
39
39
|
# helping functionality (e.g. retrieving position for a name) easier.
|
40
40
|
# I've also kept them in 6x6 here for better overview.
|
41
|
-
PORTRAITS = [
|
42
|
-
# http://eu.battle.net/sc2/static/local-common/images/sc2/portraits/0-75.jpg?v42
|
43
|
-
['Kachinsky', 'Cade', 'Thatcher', 'Hall', 'Tiger Marine', 'Panda Marine',
|
44
|
-
'General Warfield', 'Jim Raynor', 'Arcturus Mengsk', 'Sarah Kerrigan', 'Kate Lockwell', 'Rory Swann',
|
45
|
-
'Egon Stetmann', 'Hill', 'Adjutant', 'Dr. Ariel Hanson', 'Gabriel Tosh', 'Matt Horner',
|
46
|
-
# Could not identify in order: Raynor in a Suit? Bullmarine? Nova?
|
47
|
-
# Fiery Marine?
|
48
|
-
'Tychus Findlay', 'Zeratul', 'Valerian Mengsk', 'Spectre', '?', '?',
|
49
|
-
'?', '?', 'SCV', 'Firebat', 'Vulture', 'Hellion',
|
50
|
-
'Medic', 'Spartan Company', 'Wraith', 'Diamondback', 'Probe', 'Scout'],
|
51
|
-
|
52
|
-
# http://eu.battle.net/sc2/static/local-common/images/sc2/portraits/1-75.jpg?v42
|
53
|
-
# Special Rewards - couldn't identify most of these.
|
54
|
-
['?', '?', '?', '?', '?', 'PanTerran Marine',
|
55
|
-
'?', '?', '?', '?', '', '',
|
56
|
-
'', '', '', '', '', '',
|
57
|
-
'', '', '', '', '', '',
|
58
|
-
'', '', '', '', '', '',
|
59
|
-
'', '', '', '', '', ''],
|
60
|
-
|
61
|
-
# http://eu.battle.net/sc2/static/local-common/images/sc2/portraits/2-75.jpg?v42
|
62
|
-
['Ghost', 'Thor', 'Battlecruiser', 'Nova', 'Zealot', 'Stalker',
|
63
|
-
'Phoenix', 'Immortal', 'Void Ray', 'Colossus', 'Carrier', 'Tassadar',
|
64
|
-
'Reaper', 'Sentry', 'Overseer', 'Viking', 'High Templar', 'Mutalisk',
|
65
|
-
# Unidentified: Bird? Dog? Robot?
|
66
|
-
'Banshee', 'Hybrid Destroyer', 'Dark Voice', '?', '?', '?',
|
67
|
-
# Unidentified: Worgen? Goblin? Chef?
|
68
|
-
'Orian', 'Wolf Marine', 'Murloc Marine', '?', '?', 'Zealot Chef',
|
69
|
-
# Unidentified: KISS Marine? Dragon Marine? Dragon? Another Raynor?
|
70
|
-
'Stank', 'Ornatus', '?', '?', '?', '?'],
|
71
41
|
|
72
|
-
|
73
|
-
[
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
42
|
+
PORTRAITS = [
|
43
|
+
[
|
44
|
+
'Kachinsky', 'Cade', 'Thatcher', 'Hall', 'Tiger Marine', 'Panda Marine',
|
45
|
+
'General Warfield', 'Jim Raynor', 'Arcturus Mengsk', 'Sarah Kerrigan', 'Kate Lockwell', 'Rory Swann',
|
46
|
+
'Egon Stetmann', 'Hill', 'Adjutant', 'Dr. Ariel Hanson', 'Gabriel Tosh', 'Matt Horner',
|
47
|
+
'Tychus Findlay', 'Zeratul', 'Valerian Mengsk', 'Spectre', 'Raynor Marine', 'Tauren Marine',
|
48
|
+
'Night Elf Banshee', 'Diablo Marine', 'SCV', 'Firebat', 'Vulture', 'Hellion',
|
49
|
+
'Medic', 'Spartan Company', 'Wraith', 'Diamondback', 'Probe', 'Scout'
|
50
|
+
],
|
51
|
+
[
|
52
|
+
'Tauren Marine', 'Night Elf Banshee', 'Diablo Marine', 'Worgen Marine', 'Goblin Marine', 'PanTerran Marine',
|
53
|
+
'Wizard Templar', 'Tyrael Marine', 'Witch Doctor Zergling', 'Stank', 'Night Elf Templar', 'Infested Orc',
|
54
|
+
'', 'Diablo Marine', '', 'Pandaren Firebat', 'Prince Valerian', 'Zagara',
|
55
|
+
'Lasarra', 'Dehaka', 'Infested Stukov', 'Mira Horner', 'Primal Queen', 'Izsha',
|
56
|
+
'Abathur', 'Ghost Kerrigan', 'Zurvan', 'Narud', '', '',
|
57
|
+
'', '', '', '', '', ''
|
58
|
+
],
|
59
|
+
[
|
60
|
+
'Ghost', 'Thor', 'Battlecruiser', 'Nova', 'Zealot', 'Stalker',
|
61
|
+
'Phoenix', 'Immortal', 'Void Ray', 'Colossus', 'Carrier', 'Tassadar',
|
62
|
+
'Reaper', 'Sentry', 'Overseer', 'Viking', 'High Templar', 'Mutalisk',
|
63
|
+
'Banshee', 'Hybrid Destroyer', 'Dark Voice', 'Urubu', 'Lyote', 'Automaton 2000',
|
64
|
+
'Orian', 'Wolf Marine', 'Murloc Marine', 'Worgen Marine', 'Goblin Marine', 'Zealot Chef',
|
65
|
+
'Stank', 'Ornatus', 'Facebook Corps Members', 'Lion Marines', 'Dragons', 'Raynor Marine'
|
66
|
+
],
|
67
|
+
[
|
68
|
+
'Urun', 'Nyon', 'Executor', 'Mohandar', 'Selendis', 'Artanis',
|
69
|
+
'Drone', 'Infested Colonist', 'Infested Marine', 'Corruptor', 'Aberration', 'Broodlord',
|
70
|
+
'Overmind', 'Leviathan', 'Overlord', 'Hydralisk Marine', "Zer'atai Dark Templar", 'Goliath',
|
71
|
+
'Lenassa Dark Templar', 'Mira Han', 'Archon', 'Hybrid Reaver', 'Predator', 'Unknown',
|
72
|
+
'Zergling', 'Roach', 'Baneling', 'Hydralisk', 'Queen', 'Infestor',
|
73
|
+
'Ultralisk', 'Queen of Blades', 'Marine', 'Marauder', 'Medivac', 'Siege Tank'
|
74
|
+
],
|
75
|
+
[
|
76
|
+
'Level 3 Zealot', 'Level 5 Stalker', 'Level 8 Sentinel', 'Level 11 Immortal', 'Level 14 Oracle', 'Level 17 High Templar',
|
77
|
+
'Level 21 Tempest', 'Level 23 Colossus', 'Level 27 Carrier', 'Level 29 Zeratul', 'Level 3 Marine', 'Level 5 Marauder',
|
78
|
+
'Level 8 Hellbat', 'Level 11 Widow Mine', 'Level 14 Medivac', 'Level 17 Banshee', 'Level 21 Ghost', 'Level 23 Thor',
|
79
|
+
'Level 27 Battlecruiser', 'Level 29 Raynor', 'Level 3 Zergling', 'Level 5 Roach', 'Level 8 Hydralisk', 'Level 11 Locust',
|
80
|
+
'Level 14 Swarm Host', 'Level 17 Infestor', 'Level 21 Viper', 'Level 23 Broodlord', 'Level 27 Ultralisk', 'Level 29 Kerrigan',
|
81
|
+
'Protoss Champion',' Terran Champion', 'Zerg Champion', '', '', ''
|
82
|
+
]
|
80
83
|
]
|
81
|
-
|
84
|
+
|
82
85
|
# This is a convenience method that chains calls to ProfileScraper,
|
83
86
|
# followed by a scrape of each league returned in the `leagues` array
|
84
87
|
# in the profile_data. The end result is a fully scraped profile with
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'date'
|
2
|
+
module BnetScraper
|
3
|
+
module Starcraft2
|
4
|
+
class Achievement
|
5
|
+
attr_accessor :title, :description
|
6
|
+
attr_reader :earned
|
7
|
+
|
8
|
+
def initialize options = {}
|
9
|
+
options.each_key do |key|
|
10
|
+
self.send "#{key}=", options[key]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def earned= date
|
15
|
+
@earned = convert_date date
|
16
|
+
end
|
17
|
+
|
18
|
+
def convert_date date
|
19
|
+
month, day, year = date.scan(/(\d+)\/(\d+)\/(\d+)/).first.map(&:to_i)
|
20
|
+
Date.new year, month, day
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'bnet_scraper/starcraft2/achievement'
|
2
|
+
|
1
3
|
module BnetScraper
|
2
4
|
module Starcraft2
|
3
5
|
# This pulls achievement information for an account. Note that currently only returns the overall achievements,
|
@@ -24,10 +26,11 @@ module BnetScraper
|
|
24
26
|
# ],
|
25
27
|
# progress: {
|
26
28
|
# liberty_campaign: '1580',
|
27
|
-
#
|
29
|
+
# swarm_campaign: '480',
|
30
|
+
# matchmaking: '1100',
|
28
31
|
# custom_game: '330',
|
29
|
-
#
|
30
|
-
#
|
32
|
+
# arcade: '660',
|
33
|
+
# exploration: '170'
|
31
34
|
# }
|
32
35
|
# }
|
33
36
|
class AchievementScraper < BaseScraper
|
@@ -42,6 +45,8 @@ module BnetScraper
|
|
42
45
|
end
|
43
46
|
|
44
47
|
# retrieves the account's achievements overview page HTML for scraping
|
48
|
+
#
|
49
|
+
# @return [Nokogiri::HTML] The parsed HTML document
|
45
50
|
def get_response
|
46
51
|
response = Faraday.get "#{profile_url}achievements/"
|
47
52
|
if response.success?
|
@@ -52,40 +57,59 @@ module BnetScraper
|
|
52
57
|
end
|
53
58
|
|
54
59
|
# scrapes the recent achievements from the account's achievements overview page
|
60
|
+
#
|
61
|
+
# @return [Array<Achievement>] Array of recent achievements
|
55
62
|
def scrape_recent
|
56
63
|
@recent = []
|
57
|
-
|
58
|
-
achievement =
|
59
|
-
|
60
|
-
if div
|
61
|
-
achievement[:title] = div.css("div > div").inner_text.strip
|
62
|
-
achievement[:description] = div.inner_text.gsub(achievement[:title], '').strip
|
63
|
-
achievement[:earned] = response.css("#recent-achievements span")[(num*3)+1].inner_text
|
64
|
-
|
65
|
-
@recent << achievement
|
66
|
-
end
|
64
|
+
response.css(".recent-tile").size.times do |num|
|
65
|
+
achievement = extract_recent_achievement num
|
66
|
+
@recent.push(achievement) if achievement
|
67
67
|
end
|
68
|
+
|
68
69
|
@recent
|
69
70
|
end
|
70
71
|
|
71
|
-
#
|
72
|
+
# Scrapes recent achievement by position in the sidebar
|
73
|
+
#
|
74
|
+
# @param [Fixnum] achievement position number, top-down
|
75
|
+
# @return [Achievement] achievement object containing all achievement information
|
76
|
+
def extract_recent_achievement num
|
77
|
+
if div = response.css("#achv-recent-#{num}")
|
78
|
+
Achievement.new({
|
79
|
+
title: div.children[1].inner_text,
|
80
|
+
description: div.children[2].inner_text.strip,
|
81
|
+
earned: response.css(".recent-tile")[num].css('span')[1].inner_text
|
82
|
+
})
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
|
88
|
+
# Scrapes the progress of each achievement category from the account's achievements
|
89
|
+
# overview page and returns them as a hash
|
90
|
+
#
|
91
|
+
# @return [Hash] Hash of achievement indicators broken down by category
|
72
92
|
def scrape_progress
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
93
|
+
keys = [:liberty_campaign, :swarm_campaign, :matchmaking, :custom_game, :arcade, :exploration]
|
94
|
+
index = 1
|
95
|
+
|
96
|
+
@progress = keys.inject({}) do |hash, key|
|
97
|
+
hash[key] = response.css(".progress-tile:nth-child(#{index}) .profile-progress span").inner_text.to_i
|
98
|
+
index += 1
|
99
|
+
|
100
|
+
hash
|
101
|
+
end
|
81
102
|
end
|
82
103
|
|
83
|
-
#
|
104
|
+
# Scrapes the showcase achievements from the account's achievements overview page
|
105
|
+
#
|
106
|
+
# @return [Array<Achievement>] Array containing all the showcased achievements
|
84
107
|
def scrape_showcase
|
85
108
|
@showcase = response.css("#showcase-module .progress-tile").map do |achievement|
|
86
|
-
|
87
|
-
|
88
|
-
|
109
|
+
Achievement.new({
|
110
|
+
title: achievement.css('.tooltip-title').inner_text.strip,
|
111
|
+
description: achievement.children[3].children[2].inner_text.strip
|
112
|
+
})
|
89
113
|
end
|
90
114
|
@showcase
|
91
115
|
end
|
@@ -19,25 +19,41 @@ module BnetScraper
|
|
19
19
|
|
20
20
|
def initialize options = {}
|
21
21
|
if options[:url]
|
22
|
-
|
23
|
-
if extracted_data
|
24
|
-
@region = REGION_DOMAINS[extracted_data[1]]
|
25
|
-
@bnet_id = extracted_data[3]
|
26
|
-
@bnet_index = extracted_data[4]
|
27
|
-
@account = extracted_data[5]
|
28
|
-
@url = options[:url]
|
29
|
-
else
|
30
|
-
raise BnetScraper::InvalidProfileError, "URL provided does not match Battle.net format"
|
31
|
-
end
|
22
|
+
extract_data_from_url options[:url]
|
32
23
|
elsif options[:bnet_id] && options[:account]
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
24
|
+
extract_data_from_options options
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Extracts information about the account from the URL string
|
29
|
+
#
|
30
|
+
# @param [String] url of the Battle.net profile page
|
31
|
+
def extract_data_from_url url
|
32
|
+
extracted_data = url.match(/http:\/\/(.+)\/sc2\/(.+)\/profile\/(.+)\/(\d{1})\/(.[^\/]+)\//)
|
33
|
+
if extracted_data
|
34
|
+
@region = REGION_DOMAINS[extracted_data[1]]
|
35
|
+
@language = extracted_data[2]
|
36
|
+
@bnet_id = extracted_data[3]
|
37
|
+
@bnet_index = extracted_data[4]
|
38
|
+
@account = extracted_data[5]
|
39
|
+
@url = url
|
40
|
+
else
|
41
|
+
raise BnetScraper::InvalidProfileError, "URL provided does not match Battle.net format"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Extracts information about the account from an options hash
|
46
|
+
#
|
47
|
+
# @param [Hash] hash of Battle.net account infomation
|
48
|
+
def extract_data_from_options options
|
49
|
+
@bnet_id = options[:bnet_id]
|
50
|
+
@account = options[:account]
|
51
|
+
@region = options[:region] || 'na'
|
52
|
+
|
53
|
+
if options[:bnet_index]
|
54
|
+
@bnet_index = options[:bnet_index]
|
55
|
+
else
|
56
|
+
set_bnet_index
|
41
57
|
end
|
42
58
|
end
|
43
59
|
|
@@ -18,7 +18,7 @@ module BnetScraper
|
|
18
18
|
# ]
|
19
19
|
# }
|
20
20
|
class MatchHistoryScraper < BaseScraper
|
21
|
-
attr_reader :matches, :
|
21
|
+
attr_reader :matches, :response
|
22
22
|
|
23
23
|
# account's match history URL
|
24
24
|
def match_url
|
@@ -39,24 +39,33 @@ module BnetScraper
|
|
39
39
|
def scrape
|
40
40
|
get_response
|
41
41
|
@matches = []
|
42
|
-
@wins = 0
|
43
|
-
@losses = 0
|
44
42
|
|
45
43
|
response.css('.match-row').each do |m|
|
46
|
-
|
47
|
-
match = Match.new
|
48
|
-
|
49
|
-
cells = m.css('td')
|
50
|
-
match.map_name = cells[1].inner_text
|
51
|
-
match.type = cells[2].inner_text
|
52
|
-
match.outcome = (cells.css('.match-loss') ? :win : :loss)
|
53
|
-
match.date = cells[4].inner_text.strip
|
54
|
-
|
55
|
-
@matches << match
|
44
|
+
@matches.push extract_match_info m
|
56
45
|
end
|
57
46
|
|
58
47
|
@matches
|
59
48
|
end
|
49
|
+
|
50
|
+
def wins
|
51
|
+
@wins ||= @matches.count { |m| m.outcome == :win }
|
52
|
+
end
|
53
|
+
|
54
|
+
def losses
|
55
|
+
@losses ||= @matches.count { |m| m.outcome == :loss }
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_match_info m
|
59
|
+
match = Match.new
|
60
|
+
|
61
|
+
cells = m.css('td')
|
62
|
+
match.map_name = cells[1].inner_text
|
63
|
+
match.type = cells[2].inner_text
|
64
|
+
match.outcome = (cells.css('.match-win')[0] ? :win : :loss)
|
65
|
+
match.date = cells[4].inner_text.strip
|
66
|
+
|
67
|
+
match
|
68
|
+
end
|
60
69
|
end
|
61
70
|
end
|
62
71
|
end
|
@@ -5,7 +5,8 @@ module BnetScraper
|
|
5
5
|
attr_accessor :portrait, :url, :achievement_points, :current_solo_league,
|
6
6
|
:highest_solo_league, :current_team_league, :highest_team_league,
|
7
7
|
:career_games, :games_this_season, :terran_swarm_level, :protoss_swarm_level,
|
8
|
-
:zerg_swarm_level, :leagues, :swarm_levels
|
8
|
+
:zerg_swarm_level, :leagues, :swarm_levels, :terran_campaign_completion,
|
9
|
+
:zerg_campaign_completion
|
9
10
|
|
10
11
|
def initialize options = {}
|
11
12
|
options.each_key do |key|
|
@@ -40,6 +41,23 @@ module BnetScraper
|
|
40
41
|
terran: @terran_swarm_level
|
41
42
|
}
|
42
43
|
end
|
44
|
+
|
45
|
+
def campaign_completion
|
46
|
+
{
|
47
|
+
terran: @terran_campaign_completion,
|
48
|
+
zerg: @zerg_campaign_completion
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def completed_campaign campaign, difficulty = :normal
|
53
|
+
difficulties = [:unearned, :normal, :hard, :brutal]
|
54
|
+
ranking = campaign_completion[campaign]
|
55
|
+
if difficulties.index(ranking) >= difficulties.index(difficulty)
|
56
|
+
true
|
57
|
+
else
|
58
|
+
false
|
59
|
+
end
|
60
|
+
end
|
43
61
|
end
|
44
62
|
end
|
45
63
|
end
|
@@ -42,84 +42,148 @@ module BnetScraper
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def scrape
|
45
|
-
|
45
|
+
html = retrieve_data
|
46
|
+
|
47
|
+
get_profile_data html
|
48
|
+
get_portrait html
|
49
|
+
get_solo_league_info html
|
50
|
+
get_team_league_info html
|
51
|
+
get_swarm_levels html
|
52
|
+
get_campaign_completion html
|
46
53
|
get_league_list
|
47
54
|
|
48
55
|
@profile
|
49
56
|
end
|
50
57
|
|
51
|
-
#
|
52
|
-
|
58
|
+
# Retrieves the HTML document and feed into Nokogiri
|
59
|
+
#
|
60
|
+
# @return [Nokogiri::HTML] HTML document of Profile
|
61
|
+
def retrieve_data
|
53
62
|
response = Faraday.get profile_url
|
54
63
|
|
55
64
|
if response.success?
|
56
|
-
|
57
|
-
|
58
|
-
@profile.achievement_points = html.css("#profile-header h3").inner_html()
|
59
|
-
@profile.career_games = html.css(".career-stat-block:nth-child(4) .stat-value").inner_html()
|
60
|
-
@profile.games_this_season = html.css(".career-stat-block:nth-child(5) .stat-value").inner_html()
|
61
|
-
|
62
|
-
get_portrait html
|
63
|
-
get_solo_league_info html
|
64
|
-
get_team_league_info html
|
65
|
-
get_swarm_levels html
|
65
|
+
Nokogiri::HTML(response.body)
|
66
66
|
else
|
67
67
|
raise BnetScraper::InvalidProfileError
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
+
# Scrapes the Achievement Points, Career Games, and Games this Season from Profile
|
72
|
+
#
|
73
|
+
# @param [Nokogiri::HTML] Profile html object to scrape from
|
74
|
+
def get_profile_data html
|
75
|
+
@profile.achievement_points = html.css("#profile-header h3").inner_html()
|
76
|
+
@profile.career_games = html.css(".career-stat-block:nth-child(4) .stat-value").inner_html()
|
77
|
+
@profile.games_this_season = html.css(".career-stat-block:nth-child(5) .stat-value").inner_html()
|
78
|
+
end
|
79
|
+
|
80
|
+
# Extracts background spritesheet and sprite coordinates to map to a multidimensional
|
81
|
+
# array of portrait names. The first index is the spritesheet page, the second index
|
82
|
+
# is the position within the spritesheet
|
83
|
+
#
|
84
|
+
# @param [Nokogiri::XML] html node
|
85
|
+
# @return [String] Portrait name
|
71
86
|
def get_portrait html
|
72
|
-
# Portraits use spritemaps, so we extract positions and map to
|
73
|
-
# PORTRAITS.
|
74
87
|
@profile.portrait = begin
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
PORTRAITS[portrait_map.to_i][portrait_position-1]
|
88
|
+
portrait_info = extract_portrait_info html
|
89
|
+
position = get_portrait_position html, portrait_info
|
90
|
+
PORTRAITS[portrait_info[0].to_i][position-1]
|
79
91
|
rescue
|
80
92
|
nil
|
81
93
|
end
|
82
94
|
end
|
83
95
|
|
96
|
+
# Extracts portrait information (spritesheet page, portsize size, X, Y) from HTML page
|
97
|
+
#
|
98
|
+
# @param [Nokogiri::XML] html node
|
99
|
+
# @return [Fixnum, Fixnum, Fixnum, Fixnum] Array of sprite information
|
100
|
+
def extract_portrait_info html
|
101
|
+
html.css("#portrait .icon-frame").attr('style').to_s.scan(/url\('.+(\d+)-(\d+)\.jpg'\) ([\-\d]+)px ([\-\d]+)px/).flatten
|
102
|
+
end
|
103
|
+
|
104
|
+
# Translates x/y positions of the background spritesheet into an array index. There are
|
105
|
+
# 6 pictures per row, but X/Y is in pixels, so account for portrait size in the incrementor
|
106
|
+
#
|
107
|
+
# @param [Nokogiri::HTML] html node
|
108
|
+
# @param [Fixnum] size of portrait in pixels
|
109
|
+
# @return [Fixnum] index position of portrait spritesheet
|
110
|
+
def get_portrait_position html, portrait_info
|
111
|
+
size = portrait_info[1].to_i
|
112
|
+
x = portrait_info[2].to_i
|
113
|
+
y = portrait_info[3].to_i
|
114
|
+
|
115
|
+
((-y/size) * 6) + (-x/size + 1)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Extracts the current and highest ever solo league achieved
|
119
|
+
#
|
120
|
+
# @param [Nokogiri::XML] html node
|
84
121
|
def get_solo_league_info html
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
122
|
+
@profile.highest_solo_league = get_highest_league_info :solo, html
|
123
|
+
@profile.current_solo_league = get_current_league_info :solo, html
|
124
|
+
end
|
125
|
+
|
126
|
+
# Extracts the current and highest ever team league achieved
|
127
|
+
#
|
128
|
+
# @param [Nokogiri::XML] html node
|
129
|
+
def get_team_league_info html
|
130
|
+
@profile.highest_team_league = get_highest_league_info :team, html
|
131
|
+
@profile.current_team_league = get_current_league_info :team, html
|
132
|
+
end
|
133
|
+
|
134
|
+
# Extracts the highest league achieved for a given league type
|
135
|
+
#
|
136
|
+
# @param [Symbol] league to be scraped. Values: :solo or :team
|
137
|
+
# @param [Nokogiri::HTML] html ode to scrape
|
138
|
+
# @return [String] League of Ladder
|
139
|
+
def get_highest_league_info league_type, html
|
140
|
+
if div = html.css("#best-finish-#{league_type.upcase} div")[0]
|
141
|
+
div.children[2].inner_text.strip
|
92
142
|
else
|
93
|
-
|
94
|
-
@profile.current_solo_league = "Not Yet Ranked"
|
143
|
+
"Not Yet Ranked"
|
95
144
|
end
|
96
145
|
end
|
97
146
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
147
|
+
# Extracts the current league achieved for a given league type
|
148
|
+
#
|
149
|
+
# @param [Symbol] league to be scraped. Values: :solo or :team
|
150
|
+
# @param [Nokogiri::HTML] html node to scrape
|
151
|
+
# @return [String] League of Ladder
|
152
|
+
def get_current_league_info league_type, html
|
153
|
+
if div = html.css("#best-finish-#{league_type.upcase} div")[0].children[8]
|
154
|
+
div.inner_text.strip
|
106
155
|
else
|
107
|
-
|
108
|
-
@profile.current_team_league = "Not Yet Ranked"
|
156
|
+
"Not Yet Ranked"
|
109
157
|
end
|
110
158
|
end
|
111
159
|
|
160
|
+
# Extracts the HotS Swarm Levels for each race
|
161
|
+
#
|
162
|
+
# @param [Nokogiri::HTML] html node to scrape
|
112
163
|
def get_swarm_levels html
|
113
164
|
@profile.protoss_swarm_level = get_swarm_level :protoss, html
|
114
165
|
@profile.terran_swarm_level = get_swarm_level :terran, html
|
115
166
|
@profile.zerg_swarm_level = get_swarm_level :zerg, html
|
116
167
|
end
|
117
168
|
|
169
|
+
# Extracts the swarm level for a given race
|
170
|
+
#
|
171
|
+
# @param [Symbol] race to determine the swarm level of
|
172
|
+
# @param [Nokogiri::HTML] html node to scrape from
|
173
|
+
# @return [Fixnum] Swarm Level
|
118
174
|
def get_swarm_level race, html
|
119
175
|
level = html.css(".race-level-block.#{race} .level-value").inner_html
|
120
176
|
level.match(/Level (\d+)/)[1].to_i
|
121
177
|
end
|
122
178
|
|
179
|
+
# Extracts completion level of the SC2 Campaigns
|
180
|
+
#
|
181
|
+
# @param [Nokogiri::HTML] html node to scrape
|
182
|
+
def get_campaign_completion html
|
183
|
+
@profile.terran_campaign_completion = html.css('.campaign-wings-of-liberty .badge')[0].attr('class').split[1].to_sym
|
184
|
+
@profile.zerg_campaign_completion = html.css('.campaign-heart-of-the-swarm .badge')[0].attr('class').split[1].to_sym
|
185
|
+
end
|
186
|
+
|
123
187
|
# scrapes the league list from account's league page and sets an array of URLs
|
124
188
|
def get_league_list
|
125
189
|
response = Faraday.get profile_url + "ladder/leagues"
|