bnet_scraper 0.4.0 → 0.5.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.
- 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 [](http://travis-ci.org/agoragames/bnet_scraper)
|
1
|
+
# BnetScraper [](http://travis-ci.org/agoragames/bnet_scraper) [](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"
|