Files

1104 lines
41 KiB
Go
Raw Permalink Normal View History

2026-07-21 03:49:47 +03:00
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 incid
//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
}
*/