Files
flashscore/api.go
2026-07-21 03:49:47 +03:00

1104 lines
41 KiB
Go
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package flashscore
import (
"bytes"
"fmt"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"gordenko.dev/dima/flashscore/model"
webspider "gordenko.dev/dima/spider"
)
// ListOfScheduledMatches - загружает списки матчей на сегодня и завтра
func (s *Flashscore) ListOfScheduledMatches(sportID int) (result []ScheduledMatch, err error) {
spider, err := webspider.NewSpider(nil)
if err != nil {
panic(err)
}
var (
referer string
urls []string
)
switch sportID {
case Football:
urls = []string{
footballAllGamesTodayURL,
footballAllGamesTomorrowURL,
}
referer = "https://www.flashscore.com/football/"
case Handball:
urls = []string{
handballAllGamesTodayURL,
handballAllGamesTomorrowURL,
}
referer = "https://www.flashscore.com/handball/"
case Tennis:
urls = []string{
tennisAllGamesTodayURL,
tennisAllGamesTomorrowURL,
}
referer = "https://www.flashscore.com/tennis/"
}
// Загружаем AJAX страницы со списками матчей
for _, allGamesURL := range urls {
var (
buf []byte
list []ScheduledMatch
)
buf, err = spider.Get(allGamesURL, map[string]string{
"referer": "https://d.flashscore.com/x/feed/proxy-local",
"x-requested-with": "XMLHttpRequest",
// Важное поле! Код можно найти в исходниках либо подсмотреть. Без кода
// сервер отдает 401 ошибку
"x-fsign": feedSign,
"x-referer": referer,
"x-geoip": "1",
})
if err != nil {
err = fmt.Errorf("Load all Games feed: %s", err)
return
}
//fmt.Printf("%s\n\n", buf)
list, err = parseFlashscoreEncodedAllGames(string(buf), sportID)
if err != nil {
err = fmt.Errorf("parseListOfScheduledMatches: %s", err)
return
}
result = append(result, list...)
}
return
}
func (s *Flashscore) SyncUpcomingMatchReport(report *UpcomingMatchReport) (err error) {
spider, err := webspider.NewSpider(nil)
if err != nil {
panic(err)
}
// Проверяем зону
storedZone, err := s.model.GetTeamZone(report.Zone.ZoneID)
if err != nil {
return
}
if storedZone == nil {
err = s.model.AddTeamZone(report.Zone)
if err != nil {
return
}
}
// Проверяем чемпионат
storedChamp, err := s.model.GetTeamChamp(report.Champ.ChampID)
if err != nil {
return
}
if storedChamp == nil {
// Загружаем страницу чемпионата
var champ model.TeamChamp
champ, err = s.loadTeamChamp(report.Champ.ChampID)
if err != nil {
return
}
err = s.model.AddTeamChamp(champ)
if err != nil {
return
}
report.Champ = champ
} else {
report.Champ = *storedChamp
}
if !report.IsTeamsLoaded {
var storedHome, storedAway *model.Team
report.Home, report.Away, err = loadHomeAndAwayTeams(spider, report.MatchID, report.SportID)
if err != nil {
err = fmt.Errorf("loadHomeAndAwayTeams: %s", err)
return
}
// Не забываем добавить зону!
report.Home.ZoneID = report.Zone.ZoneID
report.Away.ZoneID = report.Zone.ZoneID
storedHome, err = s.model.GetTeam(report.Home.TeamID)
if err != nil {
return
}
if storedHome == nil {
err = s.model.AddTeam(report.Home)
if err != nil {
return
}
}
storedAway, err = s.model.GetTeam(report.Away.TeamID)
if err != nil {
return
}
if storedAway == nil {
err = s.model.AddTeam(report.Away)
if err != nil {
return
}
}
// Команды успешно загружены
report.IsTeamsLoaded = true
}
if !report.IsHistoryLoaded {
// ВАЖНО!
// На странице AllGames можно получить названия команд, но там нет TeamID!
// Находим на странице названия и TeamID.
report.HomeTeamMatches, report.AwayTeamMatches, report.H2H, err = loadH2H(
spider, report.MatchID, report.SportID,
report.Home.CanonicalName, report.Away.CanonicalName)
if err != nil {
err = fmt.Errorf("Load H2H: %s", err)
return
}
// STATS
switch report.SportID {
case Handball, Football:
report.HomeTeamMatches = s.addScoreStatToTeamMatches(report.HomeTeamMatches, report.SportID)
if err != nil {
return
}
report.AwayTeamMatches = s.addScoreStatToTeamMatches(report.AwayTeamMatches, report.SportID)
if err != nil {
return
}
report.H2H = s.addScoreStatToTeamMatches(report.H2H, report.SportID)
if err != nil {
return
}
//buf, _ = json.MarshalIndent(report.HomeTeamMatches, "", " ")
//fmt.Printf("HOME:\n\n%s\n\n", buf)
//buf, _ = json.MarshalIndent(report.AwayTeamMatches, "", " ")
//fmt.Printf("AWAY:\n\n%s\n\n", buf)
//buf, _ = json.MarshalIndent(report.H2H, "", " ")
//fmt.Printf("H2H:\n\n%s\n\n", buf)
case Tennis:
// passs
}
report.IsHistoryLoaded = true
}
// ODDS (Загружаем всегда, чтобы иметь свежие данные! )
var (
someErr error
funcName string
)
switch report.SportID {
case Football:
funcName = "loadFootballOdds"
report.Odds, someErr = loadFootballOdds(spider, report.MatchID)
case Handball:
funcName = "loadHandballOdds"
report.Odds, someErr = loadHandballOdds(spider, report.MatchID)
case Tennis:
funcName = "loadTennisOdds"
report.Odds, someErr = loadHandballOdds(spider, report.MatchID)
default:
panic(fmt.Sprintf("Bug: odds loader for the sportID %d is undefined", report.SportID))
}
// Если не загрузились odds - это не страшно. Ошибку возвращать нельзя,
// ибо IsCompleted станет false
if someErr != nil {
s.logger.Printf("%s: %s", funcName, someErr)
return
}
return
}
/*
func (s *Flashscore) parseMatchInfo(doc *goquery.Document, sportID int) (zone model.TeamZone, champ model.TeamChamp, home, away model.Team, err error) {
// Home team
home, err = parseTeam(doc.Find(".tname-home"))
if err != nil {
err = fmt.Errorf("Parse home team: %s", err)
return
}
// Away team
away, err = parseTeam(doc.Find(".tname-away"))
if err != nil {
err = fmt.Errorf("Parse away team: %s", err)
return
}
///////////////////////////
// Проверяем чемпионат
storedChamp, err := s.model.GetTeamChamp(champ.ChampID)
if err != nil {
return
}
if storedChamp == nil {
// Проверяем зону
var storedZone *model.TeamZone
storedZone, err = s.model.GetTeamZone(zone.ZoneID)
if err != nil {
return
}
if storedZone == nil {
err = s.model.AddTeamZone(zone)
if err != nil {
return
}
}
err = s.model.AddTeamChamp(champ)
if err != nil {
return
}
} else {
// Сайт меняет названия чемпионатов по ходу чемпионата. Например:
// Лига Европы 1/128
// Лига Европы. Плэй-офф 1/4
//
// Поэтому названия нужно обновлять
champ.Name = strings.TrimSpace(champ.Name)
if storedChamp.Name != champ.Name {
err = s.model.UpdateTeamChampName(champ)
if err != nil {
return
}
}
}
storedHome, err := s.model.GetTeam(home.TeamID)
if err != nil {
return
}
if storedHome == nil {
home.ZoneID, err = s.loadTeamZoneID(home.TeamID, sportID)
if err != nil {
err = fmt.Errorf("loadTeamZoneID: %s; teamID=%s", err, home.TeamID)
return
}
err = s.model.AddTeam(home)
if err != nil {
return
}
} else {
home.ZoneID = storedHome.ZoneID
}
storedAway, err := s.model.GetTeam(away.TeamID)
if err != nil {
return
}
if storedAway == nil {
away.ZoneID, err = s.loadTeamZoneID(away.TeamID, sportID)
if err != nil {
err = fmt.Errorf("loadTeamZoneID: %s; teamID=%s", err, away.TeamID)
return
}
err = s.model.AddTeam(away)
if err != nil {
return
}
} else {
away.ZoneID = storedAway.ZoneID
}
return
}
*/
func (s *Flashscore) loadTeamZoneID(teamID string, sportID int) (_ string, err error) {
teamURL := fmt.Sprintf(teamURLPattern, teamID)
var buf []byte
buf, err = s.spider.Get(teamURL, nil)
if err != nil {
err = fmt.Errorf("Load footballTeamURL: %s; url=%s", err, teamURL)
return
}
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(buf))
if err != nil {
err = fmt.Errorf("NewDocumentFromReader: %s", err)
return
}
zone, err := parseZoneOnTeamPage(doc, sportID)
if err != nil {
err = fmt.Errorf("parseZoneIDOnTeamPage: %s; teamID=%s", err, teamID)
return
}
storedZone, err := s.model.GetTeamZone(zone.ZoneID)
if err != nil {
return
}
if storedZone == nil {
err = s.model.AddTeamZone(zone)
if err != nil {
return
}
}
return zone.ZoneID, nil
}
func (s *Flashscore) addScoreStatToTeamMatches(before []model.TeamMatch, sportID int) (after []model.TeamMatch) {
for idx, result := range before {
// Сокращаем количество матчей для анализа
if idx >= 10 {
return
}
var (
loadErr error
// Для логов
loadFuncName string
homeGoals, awayGoals int
// Чтобы узнать что какие-то данные были загружены и можно сохранить
// результат в БД
isFlagsChanged bool
)
storedMatch, err := s.model.GetTeamMatch(result.MatchID)
if err != nil {
s.logger.Printf("GetTeamMatch: %s\n", err)
} else {
if storedMatch != nil {
result = *storedMatch
}
}
// СУПЕР ВАЖНО!
// Если счет в матче отсканирован заранее, например в одной из таблиц H2H,
// важно выставить флаг IsMatchScoreLoaded = true. Иначе, например, для
// футбола в следующем блоке кода счет будет перезаписан пустыми значениями.
if !result.IsMatchScoreLoaded {
// Основной счет
switch sportID {
case Football:
//loadFuncName = "loadFootballTeamScoreByPeriods"
//stat, loadErr = loadFootballTeamScoreByPeriods(s.spider, result.MatchID)
case Handball:
loadFuncName = "loadHandballMatchScore"
homeGoals, awayGoals, loadErr = loadHandballMatchScore(s.spider, result.MatchID)
default:
panic(fmt.Sprintf("Bug: missing data loader for the sportID %d is undefined", sportID))
}
if loadErr != nil {
// Только логгируем
s.logger.Printf("%s: %s; matchID=%s\n", loadFuncName, loadErr, result.MatchID)
// Переходим к следующему матчу
continue
}
result.HomeGoals = homeGoals
result.AwayGoals = awayGoals
// Флаг, который покажет, что матч нужно пересканировать, если счет не загружен
result.IsMatchScoreLoaded = true
isFlagsChanged = true
}
if !result.IsScoreByPeriodsLoaded {
var (
stat teamScoreByPeriods
hasScoreByPeriods bool
)
// Отдельные функции потому что, статистика по футболу возвращается
// на HTML странице, по гандболу - в "фирменном" Flashscore формате.
// И те и другие данные нужно загружать по разным адресам.
// Счет по периодам/таймам
switch sportID {
case Football:
loadFuncName = "loadFootballTeamScoreByPeriods"
stat, hasScoreByPeriods, loadErr = loadFootballTeamScoreByPeriods(s.spider, result.MatchID)
case Handball:
loadFuncName = "loadHandballTeamScoreByPeriods"
stat, hasScoreByPeriods, loadErr = loadHandballTeamScoreByPeriods(s.spider, result.MatchID)
default:
panic(fmt.Sprintf("Bug: missing data loader for the sportID %d is undefined", sportID))
}
if loadErr != nil {
// Только логгируем
s.logger.Printf("%s: %s; matchID=%s\n", loadFuncName, loadErr, result.MatchID)
// Переходим к следующему матчу
continue
}
if hasScoreByPeriods {
result.P1HomeGoals = stat.P1HomeGoals
result.P1AwayGoals = stat.P1AwayGoals
result.P2HomeGoals = stat.P2HomeGoals
result.P2AwayGoals = stat.P2AwayGoals
result.HasScoreByPeriods = true
}
// Частый вариант в гандболе - когда статистика по периодам успешно
// загружена, но данных нет. Поэтому ставим отдельно 2 флага:
// - нужно ли загружать снова
// - статистика по периодам есть/нет
result.IsScoreByPeriodsLoaded = true
isFlagsChanged = true
}
if isFlagsChanged && result.IsMatchScoreLoaded && result.IsScoreByPeriodsLoaded {
// На ошибку не проверяем ибо это просто кэш
s.model.AddTeamMatch(result)
}
after = append(after, result)
// Засыпаем на 0.5 сек.
time.Sleep(500 * time.Millisecond)
}
return
}
func loadFootballTeamScoreByPeriods(spider *webspider.Spider, matchID string) (_ teamScoreByPeriods, hasScoreByPeriods bool, err error) {
// Загружаем AJAX страницу со статистикой
url := fmt.Sprintf(footballMatchSummaryURLPattern, matchID)
var (
buf []byte
)
buf, err = spider.Get(url, map[string]string{
"x-requested-with": "XMLHttpRequest",
"x-fsign": feedSign,
"x-geoip": "1",
"x-referer": fmt.Sprintf(h2hLandingURLPattern, matchID),
})
if err != nil {
return
}
// Страничка имеет вид
//
// <div class="detailMS"><div class="detailMS__incidentsHeader stage-12"><div class="detailMS__headerText">1st Half</div><div class="detailMS__headerScore"><span
// class="p1_home"
// title=""
// >3
// </span> -
// <span
// class="p1_away"
// title=""
// >0
// </span></div></div><div class="detailMS__incidentRow incidentRow--home odd"><div class="time-box">1'</div><div title="Heung-Min Son (Tottenham) with a wonderful&lt;br /&gt;piece of finishing. He makes a yard for&lt;br /&gt;himself after a well-taken pass inside the&lt;br /&gt;box and nets into the bottom right corner." onmouseover="tt.show(this, event, true)" onmouseout="tt.hide(this)" class="icon-box soccer-ball"><span class="icon soccer-ball">&nbsp;</span></div><span class="participant-name"><a href="#" onclick="window.open('/player/son-heung-min/Mgg9oPeM/'); return false;">Son Heung-Min</a></span><span class="assist note-name">(<a href="#" onclick="window.open('/player/kane-harry/v5HSlEAa/'); return false;">Kane H.</a>)</span></div><div class="detailMS__incidentRow incidentRow--home even"><div class="time-box">8'</div><div title="Harry Kane (Tottenham) breaks through before&lt;br /&gt;collecting a brilliant pass and producing&lt;br /&gt;a precise strike to beat the goalkeeper.&lt;br /&gt;The ball went into the bottom left corner." onmouseover="tt.show(this, event, true)" onmouseout="tt.hide(this)" class="icon-box soccer-ball"><span class="icon soccer-ball">&nbsp;</span></div><span class="participant-name"><a href="#" onclick="window.open('/player/kane-harry/v5HSlEAa/'); return false;">Kane H.</a></span><span class="assist note-name">(<a href="#" onclick="window.open('/player/son-heung-min/Mgg9oPeM/'); return false;">Son Heung-Min</a>)</span></div><div class="detailMS__incidentRow incidentRow--home odd"><div class="time-box">16'</div><div title="A perfect lofted pass from Sergio Reguilon&lt;br /&gt;finds Harry Kane (Tottenham), who jumps&lt;br /&gt;highest and steers a close-range header&lt;br /&gt;into the left side of the goal. His effort&lt;br /&gt;was both strong and precise, and Lukasz&lt;br /&gt;Fabianski had no chance of stopping that.&lt;br /&gt;3:0." onmouseover="tt.show(this, event, true)" onmouseout="tt.hide(this)" class="icon-box soccer-ball"><span class="icon soccer-ball">&nbsp;</span></div><span class="participant-name"><a href="#" onclick="window.open('/player/kane-harry/v5HSlEAa/'); return false;">Kane H.</a></span><span class="assist note-name">(<a href="#" onclick="window.open('/player/reguilon-sergio/l0lnJteM/'); return false;">Reguilon S.</a>)</span></div><div class="detailMS__incidentRow incidentRow--away even"><div class="time-box">20'</div><div title="This yellow card was deserved. The tackle&lt;br /&gt;by Michail Antonio (West Ham) was quite&lt;br /&gt;harsh and Paul Tierney didn't hesitate to&lt;br /&gt;show him a yellow card." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box y-card"><span class="icon y-card">&nbsp;</span></div><span class="subincident-penalty subincident-name">(Tripping)</span><span class="participant-name"><a href="#" onclick="window.open('/player/antonio-michail/p8tt85Xn/'); return false;">Antonio M.</a></span></div><div class="detailMS__incidentsHeader stage-13"><div class="detailMS__headerText">2nd Half</div><div class="detailMS__headerScore"><span
// class="p2_home"
// title=""
// >0
// </span> -
// <span
// class="p2_away"
// title=""
// >3
// </span></div></div><div class="detailMS__incidentRow incidentRow--away odd"><div class="time-box">72'</div><div title="Angelo Ogbonna (West Ham) is booked after&lt;br /&gt;bringing down an opponent. Paul Tierney&lt;br /&gt;had an easy-decision to make." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box y-card"><span class="icon y-card">&nbsp;</span></div><span class="subincident-penalty subincident-name">(Roughing)</span><span class="participant-name"><a href="#" onclick="window.open('/player/ogbonna-angelo/xtEnn3rQ/'); return false;">Ogbonna A.</a></span></div><div class="detailMS__incidentRow incidentRow--home even"><div class="time-box">72'</div><div title="The referee stops play so that a substitution&lt;br /&gt;can be made and Gareth Bale (Tottenham)&lt;br /&gt;replaces Steven Bergwijn." onmouseover="tt.show(this, event, true)" onmouseout="tt.hide(this)" class="icon-box substitution-in"><span class="icon substitution-in">&nbsp;</span></div><span class="substitution-in-name"><a href="#" onclick="window.open('/player/bale-gareth/4SPjLLiR/'); return false;">Bale G.</a></span><span class="substitution-out-name"><span class="icon substitution-out">&nbsp;</span><a href="#" onclick="window.open('/player/bergwijn-steven/Kdj2xxJR/'); return false;">Bergwijn S.</a></span></div><div class="detailMS__incidentRow incidentRow--home odd"><div class="time-box">73'</div><div title="Jose Mourinho has decided to make a change.&lt;br /&gt;Harry Winks (Tottenham) replaces Tanguy&lt;br /&gt;Ndombele." onmouseover="tt.show(this, event, true)" onmouseout="tt.hide(this)" class="icon-box substitution-in"><span class="icon substitution-in">&nbsp;</span></div><span class="substitution-in-name"><a href="#" onclick="window.open('/player/winks-harry/MRebAusr/'); return false;">Winks H.</a></span><span class="substitution-out-name"><span class="icon substitution-out">&nbsp;</span><a href="#" onclick="window.open('/player/ndombele-tanguy/GEIWE1pD/'); return false;">Ndombele T.</a></span></div><div class="detailMS__incidentRow incidentRow--away even"><div class="time-box">77'</div><div title="The foul by Tomas Soucek (West Ham) is worthy&lt;br /&gt;of a card and a yellow is duly shown by&lt;br /&gt;Paul Tierney." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box y-card"><span class="icon y-card">&nbsp;</span></div><span class="subincident-penalty subincident-name">(Roughing)</span><span class="participant-name"><a href="#" onclick="window.open('/player/soucek-tomas/8zWzYFRF/'); return false;">Soucek T.</a></span></div><div class="detailMS__incidentRow incidentRow--away odd"><div class="time-box">77'</div><div title="Substitution. Andriy Yarmolenko (West Ham)&lt;br /&gt;has come on for Michail Antonio." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box substitution-in"><span class="icon substitution-in">&nbsp;</span></div><span class="substitution-in-name"><a href="#" onclick="window.open('/player/yarmolenko-andriy/O00J9ZLN/'); return false;">Yarmolenko A.</a></span><span class="substitution-out-name"><a href="#" onclick="window.open('/player/antonio-michail/p8tt85Xn/'); return false;">Antonio M.</a><span class="icon substitution-out">&nbsp;</span></span></div><div class="detailMS__incidentRow incidentRow--away even"><div class="time-box">77'</div><div title="Here is a change. Pablo Fornals is going&lt;br /&gt;off and David William Moyes gives the last&lt;br /&gt;tactical orders to Manuel Lanzini (West&lt;br /&gt;Ham)." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box substitution-in"><span class="icon substitution-in">&nbsp;</span></div><span class="substitution-in-name"><a href="#" onclick="window.open('/player/lanzini-manuel/QT3lIj4N/'); return false;">Lanzini M.</a></span><span class="substitution-out-name"><a href="#" onclick="window.open('/player/fornals-pablo/CfghB0gf/'); return false;">Fornals P.</a><span class="icon substitution-out">&nbsp;</span></span></div><div class="detailMS__incidentRow incidentRow--home odd"><div class="time-box">80'</div><div title="The referee stops play so that a substitution&lt;br /&gt;can be made and Lucas (Tottenham) comes&lt;br /&gt;onto the pitch for Heung-Min Son." onmouseover="tt.show(this, event, true)" onmouseout="tt.hide(this)" class="icon-box substitution-in"><span class="icon substitution-in">&nbsp;</span></div><span class="substitution-in-name"><a href="#" onclick="window.open('/player/lucas/EcmXnnoo/'); return false;">Lucas</a></span><span class="substitution-out-name"><span class="icon substitution-out">&nbsp;</span><a href="#" onclick="window.open('/player/son-heung-min/Mgg9oPeM/'); return false;">Son Heung-Min</a></span></div><div class="detailMS__incidentRow incidentRow--away even"><div class="time-box">82'</div><div title="Perfectly taken free kick! Aaron Cresswell&lt;br /&gt;sends a cross in and Fabian Balbuena (West&lt;br /&gt;Ham) leaps high to connect with it on the&lt;br /&gt;edge of the 6-yard box. His close-range&lt;br /&gt;header goes inside the right post and he&lt;br /&gt;makes it 3:1." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box soccer-ball"><span class="icon soccer-ball">&nbsp;</span></div><span class="assist note-name">(<a href="#" onclick="window.open('/player/cresswell-aaron/fcMqoMcH/'); return false;">Cresswell A.</a>)</span><span class="participant-name"><a href="#" onclick="window.open('/player/balbuena-fabian/ELMR4PLj/'); return false;">Balbuena F.</a></span></div><div class="detailMS__incidentRow incidentRow--away odd"><div class="time-box">85'</div><div title="It's an own goal! An unlucky moment for&lt;br /&gt;Davinson Sanchez (Tottenham), who tries&lt;br /&gt;to clear a cross into the six-yard box but&lt;br /&gt;heads the ball behind his own keeper. The&lt;br /&gt;score is 3:2." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box soccer-ball-own"><span class="icon soccer-ball-own">&nbsp;</span></div><span class=" note-name">(Own goal)</span><span class="participant-name"><a href="#" onclick="window.open('/player/sanchez-davinson/dbPhv0VQ/'); return false;">Sanchez D.</a></span></div><div class="detailMS__incidentRow incidentRow--away even"><div class="time-box">89'</div><div title="Paul Tierney shows the yellow card to Arthur&lt;br /&gt;Masuaku (West Ham) for a heavy challenge." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box y-card"><span class="icon y-card">&nbsp;</span></div><span class="subincident-penalty subincident-name">(Holding)</span><span class="participant-name"><a href="#" onclick="window.open('/player/masuaku-arthur/4YoV0hmC/'); return false;">Masuaku A.</a></span></div><div class="detailMS__incidentRow incidentRow--away odd"><div class="time-box">90'</div><div title="The referee signals a substitution. Robert&lt;br /&gt;Snodgrass (West Ham) is brought on as a&lt;br /&gt;substitute for Arthur Masuaku." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box substitution-in"><span class="icon substitution-in">&nbsp;</span></div><span class="substitution-in-name"><a href="#" onclick="window.open('/player/snodgrass-robert/6XPsH2mg/'); return false;">Snodgrass R.</a></span><span class="substitution-out-name"><a href="#" onclick="window.open('/player/masuaku-arthur/4YoV0hmC/'); return false;">Masuaku A.</a><span class="icon substitution-out">&nbsp;</span></span></div><div class="detailMS__incidentRow incidentRow--away even"><div class="time-box-wide">90+4'</div><div title="Manuel Lanzini (West Ham) finishes with&lt;br /&gt;a fabulous long-range hammer that bounces&lt;br /&gt;off the post and into the top right corner.&lt;br /&gt;What a wonderful goal. The keeper was completely&lt;br /&gt;helpless." onmouseover="tt.show(this, event, false)" onmouseout="tt.hide(this)" class="icon-box soccer-ball"><span class="icon soccer-ball">&nbsp;</span></div><span class="participant-name"><a href="#" onclick="window.open('/player/lanzini-manuel/QT3lIj4N/'); return false;">Lanzini M.</a></span></div><div class="detailMS__incidentRow incidentRow--away odd"><div class="time-box-wide">90+5'</div><div class="icon-box y-card"><span class="icon y-card">&nbsp;</span></div><span class="subincident-penalty subincident-name">(Unsportsmanlike conduct)</span><span class="participant-name"><a href="#" onclick="window.open('/player/lanzini-manuel/QT3lIj4N/'); return false;">Lanzini M.</a></span></div></div><div class="match-information-wrapper"><div class="parts match-information"><div><div class="stage-header"><div class="h-part">Match Information</div></div><div class="match-information-data"><div class="content">Referee: Tierney P. (Eng),&nbsp;</div><div class="content">Venue: Tottenham Hotspur Stadium (London)</div></div></div></div></div><div id="secret_hash"></div>
//fmt.Printf("SCORE BY PERIODS:\n%s\n\n", buf)
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(buf))
if err != nil {
err = fmt.Errorf("goquery.NewDocumentFromReader: %s", err)
return
}
return parseFootballTeamScoreByPeriods(doc)
}
// Football
func loadFootballOdds(spider *webspider.Spider, matchID string) (odds Odds, err error) {
url := fmt.Sprintf(footballOddsURLPattern, matchID)
buf, err := spider.Get(url, map[string]string{
"x-requested-with": "XMLHttpRequest",
"x-fsign": feedSign,
"x-geoip": "1",
"referer": fmt.Sprintf(h2hLandingURLPattern, matchID),
})
if err != nil {
err = fmt.Errorf("Load football odds: %s; url=%s", err, url)
return
}
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(buf))
if err != nil {
err = fmt.Errorf("goquery.NewDocumentFromReader: %s", err)
return
}
odds.FullTimeTotal, err = parseOddsTotal(doc)
if err != nil {
err = fmt.Errorf("parseOddsTotal: %s", err)
return
}
// odds 1x2 full time
divOdds1x2FT := doc.Find("#block-1x2-ft")
if divOdds1x2FT.Length() == 0 {
err = fmt.Errorf(`Div #block-1x2-ft not found`)
return
}
tableOdds1x2FT := divOdds1x2FT.First().Find("#odds_1x2")
if tableOdds1x2FT.Length() == 0 {
err = fmt.Errorf(`<table id="odds_1x2"> not found`)
return
}
odds.Winner, err = parseOdds1x2(tableOdds1x2FT)
if err != nil {
err = fmt.Errorf("parseOdds1x2: %s", err)
return
}
// /fmt.Printf("%# v\n", pretty.Formatter(offers))
return
}
// Handball
func loadHandballOdds(spider *webspider.Spider, matchID string) (odds Odds, err error) {
url := fmt.Sprintf(handballOddsURLPattern, matchID)
buf, err := spider.Get(url, map[string]string{
"x-requested-with": "XMLHttpRequest",
"x-fsign": feedSign,
"x-geoip": "1",
"referer": fmt.Sprintf(h2hLandingURLPattern, matchID),
})
if err != nil {
err = fmt.Errorf("Load handball odds: %s; url=%s", err, url)
return
}
odds, err = parseFlashscoreEncodedOdds(string(buf))
if err != nil {
return
}
return
}
func loadHandballMatchScore(spider *webspider.Spider, matchID string) (homeGoals, awayGoals int, err error) {
url := fmt.Sprintf(handballMatchScoreURLPattern, matchID)
buf, err := spider.Get(url, map[string]string{
"x-requested-with": "XMLHttpRequest",
"x-fsign": feedSign,
"x-geoip": "1",
"x-referer": fmt.Sprintf(h2hLandingURLPattern, matchID),
})
if err != nil {
return
}
return parseHandballMatchScore(string(buf))
}
func loadHandballTeamScoreByPeriods(spider *webspider.Spider, matchID string) (_ teamScoreByPeriods, hasScoreByPeriods bool, err error) {
// Загружаем AJAX страницу со статистикой
url := fmt.Sprintf(handballScoreByPeriodsURLPattern, matchID)
buf, err := spider.Get(url, map[string]string{
"x-requested-with": "XMLHttpRequest",
"x-fsign": feedSign,
"x-geoip": "1",
"x-referer": fmt.Sprintf(h2hLandingURLPattern, matchID),
})
if err != nil {
return
}
return parseHandballTeamScoreByPeriods(string(buf))
}
func loadFootballH2H(spider *webspider.Spider, matchID string) (homeTeamMatches, awayTeamMatches, h2hMatches []model.TeamMatch, err error) {
// Загружаем AJAX страницу с таблицами матчей
url := fmt.Sprintf(footballH2HTablesURLPattern, matchID)
buf, err := spider.Get(url, map[string]string{
"referer": "https://d.flashscore.com/x/feed/proxy-local",
"x-requested-with": "XMLHttpRequest",
// Важное поле! Код можно найти в исходниках либо подсмотреть. Без кода
// сервер отдает 401 ошибку
"x-fsign": feedSign,
"x-referer": fmt.Sprintf(h2hLandingURLPattern, matchID),
"x-geoip": "1",
})
if err != nil {
err = fmt.Errorf("load h2hTables: %s", err)
return
}
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(buf))
if err != nil {
err = fmt.Errorf("goquery.NewDocumentFromReader: %s", err)
return
}
// var (dirtyTeamID string
// OVERALL - последние матчи команд (хозяев и гостей)
// Внутри блока <div id="tab-h2h-overall" будут 2 таблицы:
// <table class="head_to_head h2h_home"
// <table class="head_to_head h2h_away"
//
// Строки (tr) внутри таблицы имеют вид:
// <tr class="odd highlight"
// <tr class="even highlight"
overall := doc.Find("#tab-h2h-overall")
if overall.Length() == 0 {
err = fmt.Errorf("Div #tab-h2h-overall not found")
return
}
overall = overall.First()
// Последние матчи хозяев (во всех турнирах, на выезде и в гостях)
home := overall.Find(".h2h_home")
if home.Length() == 0 {
err = fmt.Errorf("Table .h2h_home not found")
return
}
homeTeamMatches, err = parseMatchesResults(home.First().Find(".highlight"))
if err != nil {
return
}
// Последние матчи AWAY команды (во всех турнирах, на выезде и в гостях)
away := overall.Find(".h2h_away")
if away.Length() == 0 {
err = fmt.Errorf("Table .h2h_away not found")
return
}
awayTeamMatches, err = parseMatchesResults(away.First().Find(".highlight"))
if err != nil {
return
}
// Последние личные встречи (во всех турнирах, на выезде и в гостях)
h2h := overall.Find(".h2h_mutual")
if home.Length() == 0 {
err = fmt.Errorf("Table .h2h_mutual not found")
return
}
h2hMatches, err = parseMatchesResults(h2h.First().Find(".highlight"))
if err != nil {
return
}
return
}
func loadHandballH2H(spider *webspider.Spider, matchID, homeTeamName, awayTeamName string) (homeTeamMatches, awayTeamMatches, h2hMatches []model.TeamMatch, err error) {
// Загружаем AJAX страницу с историей, закодированную в формате Flashscore
url := fmt.Sprintf(handballH2HTablesURLPattern, matchID)
buf, err := spider.Get(url, map[string]string{
"referer": "https://d.flashscore.com/x/feed/proxy-local",
"x-requested-with": "XMLHttpRequest",
// Важное поле! Код можно найти в исходниках либо подсмотреть. Без кода
// сервер отдает 401 ошибку
"x-fsign": feedSign,
"x-referer": fmt.Sprintf(h2hLandingURLPattern, matchID),
"x-geoip": "1",
})
if err != nil {
err = fmt.Errorf("load h2hTables: %s", err)
return
}
//fmt.Printf("%s\n\n", buf)
homeTeamMatches, awayTeamMatches, h2hMatches, err = parseFlashscoreEncodedH2H(string(buf), homeTeamName, awayTeamName)
return
}
func (s *Flashscore) loadTeamChamp(champID string) (champ model.TeamChamp, err error) {
// Вычисляем zoneID из champID
parts := strings.Split(champID, "/")
if len(parts) < 3 {
err = fmt.Errorf("Wrong champID %q", champID)
return
}
zoneID := strings.Join(parts[:3], "/")
// Загружаем AJAX страницу с историей, закодированную в формате Flashscore
url := fmt.Sprintf(teamChampURLPattern, champID)
buf, err := s.spider.Get(url, map[string]string{
//"referer": "https://d.flashscore.com/x/feed/proxy-local",
//"x-requested-with": "XMLHttpRequest",
// Важное поле! Код можно найти в исходниках либо подсмотреть. Без кода
// сервер отдает 401 ошибку
//"x-fsign": feedSign,
//"x-referer": fmt.Sprintf(h2hLandingURLPattern, matchID),
//"x-geoip": "1",
})
if err != nil {
err = fmt.Errorf("Load team champ page: %s", err)
return
}
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(buf))
if err != nil {
return
}
// First - на всякий случай (если будет более 1 элемента на странице)
f := doc.Find("div.teamHeader__name").First()
champName := f.Text()
champName = strings.TrimSpace(champName)
return model.TeamChamp{
ChampID: champID,
Name: champName,
ZoneID: zoneID,
}, nil
}
/*
func (s *Flashscore) ListFootballChampNextRoundFixtures(champID string) (_ FootballRound, err error) {
url := fmt.Sprintf(fixturesURLPattern, champID)
buf, err := s.spider.Get(url, nil)
if err != nil {
return
}
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(buf))
if err != nil {
return
}
divEncodedFixtures := doc.Find("#tournament-page-data-fixtures")
if divEncodedFixtures.Length() == 0 {
err = fmt.Errorf("tag #tournament-page-data-fixtures not found")
return
}
node := divEncodedFixtures.Get(0).FirstChild
if node == nil {
// ВАЖНО!
// Это не ошибка - div пустой - матчей нет!
//err = fmt.Errorf("First child of #tournament-page-data-fixtures tag is nil")
return
}
if node.Type != xhtml.TextNode {
err = fmt.Errorf("First child of #tournament-page-data-fixtures tag is not TextNode, but %s",
nodeTypes[node.Type])
return
}
round, err := parseNextRoundFixtures(html.UnescapeString(node.Data))
if err != nil {
err = fmt.Errorf("parseNextRoundFixtures: %s", err)
return
}
return round, nil
}
*/
/*
func (s *Flashscore) ListFootballChampFixtures(champID string) (rounds []FootballRound, err error) {
url := fmt.Sprintf(fixturesURLPattern, champID)
buf, err := s.spider.Get(url, nil)
if err != nil {
return
}
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(buf))
if err != nil {
return
}
divEncodedFixtures := doc.Find("#tournament-page-data-fixtures")
if divEncodedFixtures.Length() == 0 {
err = fmt.Errorf("tag #tournament-page-data-fixtures not found")
return
}
node := divEncodedFixtures.Get(0).FirstChild
if node.Type != xhtml.TextNode {
err = fmt.Errorf("First child of #tournament-page-data-fixtures tag is not TextNode, but %s",
nodeTypes[node.Type])
return
}
rounds, err = parseEncodedFixtures(html.UnescapeString(node.Data))
if err != nil {
err = fmt.Errorf("parseEncodedFixtures: %s", err)
return
}
return
}
*/
/*
func (s *Flashscore) parseMatchInfo(doc *goquery.Document, sportID int) (zone model.TeamZone, champ model.TeamChamp, home, away model.Team, err error) {
zone, champ, home, away, err = parseTeamMatchInfo(doc, sportID)
if err != nil {
return
}
// Проверяем чемпионат
storedChamp, err := s.model.GetTeamChamp(champ.ChampID)
if err != nil {
return
}
if storedChamp == nil {
// Проверяем зону
var storedZone *model.TeamZone
storedZone, err = s.model.GetTeamZone(zone.ZoneID)
if err != nil {
return
}
if storedZone == nil {
err = s.model.AddTeamZone(zone)
if err != nil {
return
}
}
err = s.model.AddTeamChamp(champ)
if err != nil {
return
}
} else {
// Сайт меняет названия чемпионатов по ходу чемпионата. Например:
// Лига Европы 1/128
// Лига Европы. Плэй-офф 1/4
//
// Поэтому названия нужно обновлять
champ.Name = strings.TrimSpace(champ.Name)
if storedChamp.Name != champ.Name {
err = s.model.UpdateTeamChampName(champ)
if err != nil {
return
}
}
}
storedHome, err := s.model.GetTeam(home.TeamID)
if err != nil {
return
}
if storedHome == nil {
home.ZoneID, err = s.loadTeamZoneID(home.TeamID, sportID)
if err != nil {
err = fmt.Errorf("loadTeamZoneID: %s; teamID=%s", err, home.TeamID)
return
}
err = s.model.AddTeam(home)
if err != nil {
return
}
} else {
home.ZoneID = storedHome.ZoneID
}
storedAway, err := s.model.GetTeam(away.TeamID)
if err != nil {
return
}
if storedAway == nil {
away.ZoneID, err = s.loadTeamZoneID(away.TeamID, sportID)
if err != nil {
err = fmt.Errorf("loadTeamZoneID: %s; teamID=%s", err, away.TeamID)
return
}
err = s.model.AddTeam(away)
if err != nil {
return
}
} else {
away.ZoneID = storedAway.ZoneID
}
return
}
*/
/*
func (s *Flashscore) addSummaryToFootballMatches(before []model.TeamMatch, sportID int) (hasLostMatch bool, after []model.TeamMatch, err error) {
for idx, result := range before {
// Сокращаем количество матчей для анализа
if idx >= 10 {
return
}
// Проверяем в БД
var storedMatch *model.TeamMatch
storedMatch, err = s.model.GetTeamMatch(result.MatchID)
if err != nil {
return
}
if storedMatch != nil {
// Матч найден в БД, заменяем неполную версию в памяти на полную версию из БД
after = append(after, *storedMatch)
} else {
// Матч не найден в БД. Нужно загрузить недостающую информацию:
// - статистика голов по таймам
// - teamID для хозяев и гостей
// - zoneID, champID
// Загружаем Landing
landingURL := fmt.Sprintf(h2hLandingURLPattern, result.MatchID)
var (
buf []byte
doc *goquery.Document
)
buf, err = s.spider.Get(landingURL, nil)
if err != nil {
err = fmt.Errorf("load h2hLanding: %s", err)
return
}
doc, err = goquery.NewDocumentFromReader(bytes.NewBuffer(buf))
if err != nil {
err = fmt.Errorf("goquery.NewDocumentFromReader: %s", err)
return
}
var (
champ model.TeamChamp
home, away model.Team
)
// Зона не нужна, ибо матч относится к чемпионату
_, champ, home, away, err = s.parseMatchInfo(doc, Football)
if err != nil {
err = fmt.Errorf("parseFootballMatchInfo: %s", err)
return
}
// Важно, если сохраняем результат матча в БД
var (
stat teamScoreByPeriods
loadErr error
loadFuncName string
)
// Отдельные функции потому что, статистика по футболу возвращается
// на HTML странице, по гандболу - в "фирменном" Flashscore формате.
// И те и другие данные нужно загружать по разным адресам.
switch sportID {
case Football:
loadFuncName = "loadFootballTeamScoreByPeriods"
stat, loadErr = loadFootballTeamScoreByPeriods(s.spider, result.MatchID)
case Handball:
loadFuncName = "loadHandballTeamScoreByPeriods"
stat, loadErr = loadHandballTeamScoreByPeriods(s.spider, result.MatchID)
default:
panic(fmt.Sprintf("Bug: missing data loader for the sportID %d is undefined", sportID))
}
if loadErr != nil {
// Только логгируем
s.logger.Printf("%s: %s; matchID=%s\n", loadFuncName, loadErr, result.MatchID)
// Флаг, который покажет, что матч нужно пересканировать, из-за того, что
// не все матчи загружены
hasLostMatch = true
} else {
result.P1HomeGoals = stat.P1HomeGoals
result.P1AwayGoals = stat.P1AwayGoals
result.P2HomeGoals = stat.P2HomeGoals
result.P2AwayGoals = stat.P2AwayGoals
result.HasStats = true
err = s.model.AddTeamMatch(result)
if err != nil {
return
}
after = append(after, result)
}
time.Sleep(1 * time.Second)
}
}
return
}
*/