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 } // Страничка имеет вид // //