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 } // Страничка имеет вид // //
1st Half
3 // - // 0 //
1'
 
Son Heung-Min(Kane H.)
8'
 
Kane H.(Son Heung-Min)
16'
 
Kane H.(Reguilon S.)
20'
 
(Tripping)Antonio M.
2nd Half
0 // - // 3 //
72'
 
(Roughing)Ogbonna A.
72'
 
Bale G. Bergwijn S.
73'
 
Winks H. Ndombele T.
77'
 
(Roughing)Soucek T.
77'
 
Yarmolenko A.Antonio M. 
77'
 
Lanzini M.Fornals P. 
80'
 
Lucas Son Heung-Min
82'
 
(Cresswell A.)Balbuena F.
85'
 
(Own goal)Sanchez D.
89'
 
(Holding)Masuaku A.
90'
 
Snodgrass R.Masuaku A. 
90+4'
 
Lanzini M.
90+5'
 
(Unsportsmanlike conduct)Lanzini M.
Match Information
Referee: Tierney P. (Eng), 
Venue: Tottenham Hotspur Stadium (London)
//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(` 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 - последние матчи команд (хозяев и гостей) // Внутри блока
= 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 } */