package flashscore import ( "encoding/json" "fmt" "html" "regexp" "strconv" "strings" "time" "github.com/PuerkitoBio/goquery" xhtml "golang.org/x/net/html" "gordenko.dev/dima/flashscore/model" "gordenko.dev/dima/pretty" ) // Парсит кастомный формат Flashscore. На выходе список матчей, // без группировок по турнирам, раундам /* func parseListOfScheduledMatches(content string, sportID int) (result []ScheduledMatch, err error) { matches := strings.Split(content, SymbolTilda) for _, match := range matches { match = strings.TrimSpace(match) if match == "" { continue } pairs := strings.Split(match, SymbolNot) sm := ScheduledMatch{ SportID: sportID, } for _, pair := range pairs { pair = strings.TrimSpace(pair) if pair == "" { continue } kv := strings.Split(pair, SymbolDivide) if len(kv) != 2 { err = fmt.Errorf("Wrong key/value: %s", pair) return } key := strings.TrimSpace(kv[0]) value := strings.TrimSpace(kv[1]) switch key { case "AA": // sm.MatchID = value case "AD": var timestamp int64 timestamp, err = strconv.ParseInt(value, 10, 64) if err != nil { err = fmt.Errorf("Parse start time: %s; unixtime=%s", err, value) return } sm.StartTime = time.Unix(timestamp, 0) } } if sm.MatchID != "" && !sm.StartTime.IsZero() { // Помимо матчей встречаются чемпионаты, поэтому эти поля могут быть пустыми result = append(result, sm) } } return } */ //ZA zone: champ name extended //ZL "champID" /* AA: 2XhOe000 AD: 1595772000 ADE: 1595772000 AB: 1 CR: 1 AC: 1 CX: Southampton ER: Round 38 RW: 0 AX: 0 BX: -1 WQ: WM: SOU AE: Southampton JA: fu16YetP WU: southampton WN: SHU AF: Sheffield Utd JB: bDRnslpP WV: sheffield-utd AW: 1 AN: n Расшифровка: ZA - champ name AA - ID матча, нужен чтобы получить статистику по матчу AD / ADE - время начала матча в UTC CX - home team AF - away team WU - home team url part (нужно ли ?) WV - away team url part (нужно ли ?) ER - раунд (можно не парсить, но сгруппировать) */ //var reRoundNumber = regexp.MustCompile(`(?i)Round\s+(\d+)`) // parseEncodedFixtures - парсит строку (html unescaped) //func parseEncodedFixtures(content string) (rounds []FootballRound, err error) { func parseMatchesResults(trs *goquery.Selection) (results []model.TeamMatch, err error) { for idx := range trs.Nodes { var m model.TeamMatch m, err = parseMatchResultTr(trs.Eq(idx)) if err != nil { err = fmt.Errorf("parseTr: %s", err) return } results = append(results, m) } return } var reMatchIDinMatchResultTr = regexp.MustCompile(`(?i)open\('g_\d+_([^']+)'`) func parseMatchResultTr(trSelection *goquery.Selection) (m model.TeamMatch, err error) { onclickContent, exists := trSelection.Attr("onclick") if !exists { err = fmt.Errorf("onclick attr not found in match result tr") return } groups := reMatchIDinMatchResultTr.FindStringSubmatch(onclickContent) if len(groups) != 2 { err = fmt.Errorf("MatchID not found in onclick attr of match result tr") return } m.MatchID = groups[1] //fmt.Printf("onclick: %s, exists: %t\n", val, exists) //spanSelection := trSelection.Find("span") tdSelection := trSelection.Find("td") // Достаем дату spanList := tdSelection.Eq(0).Find("span") if spanList == nil { // return } //timestampStr := spanList.Get(0).FirstChild.Data //fmt.Printf("Date: %s\n", timestampStr) /* m.StartTime, err = strconv.ParseInt(timestampStr, 10, 64) if err != nil { err = fmt.Errorf("Parse timestamp string: %s; timestamp=%s", err, timestampStr) return } */ var score string score, err = parseTdContent(tdSelection.Eq(4)) if err != nil { err = fmt.Errorf("Get score: %s", err) return } m.HomeGoals, m.AwayGoals, err = parseScore(score) if err != nil { err = fmt.Errorf("parseScore: %s; score=%s", err, score) return } // ВАЖНО! m.IsMatchScoreLoaded = true return } func parseTdContent(tdSelection *goquery.Selection) (team string, err error) { // Достаем home spanList := tdSelection.Find("span") if spanList.Length() == 0 { var tagContent string tagContent, err = getTagContent(tdSelection) if err != nil { err = fmt.Errorf("getTagContent: %s", err) return } err = fmt.Errorf("Tag not found in : %s", tagContent) return } strongList := spanList.Find("strong") if strongList.Length() == 0 { team = spanList.Get(0).FirstChild.Data } else { /* firstStrong := strongList.Get(0) ///.FirstChild.Data if firstStrong == nil { panic("firstS strong = nil") } */ team = strongList.Get(0).FirstChild.Data } return } func parsePeriodTeamGoals(doc *goquery.Document, period int, side string) (goals int, err error) { spanTeamGoals := doc.Find(fmt.Sprintf("span.p%d_%s", period, side)) if spanTeamGoals.Length() == 0 { errmsg := fmt.Sprintf(`%s team goals for period %d not found (tag )`, strings.ToUpper(side), period, period, side) err = fmt.Errorf(errmsg) return } textNode := spanTeamGoals.Get(0).FirstChild if textNode == nil { err = fmt.Errorf("Teams goals doesn't have a child node") return } goalsStr := strings.TrimSpace(textNode.Data) goals, err = strconv.Atoi(goalsStr) if err != nil { err = fmt.Errorf("Parse team goals: %s; text=%q", err, goalsStr) return } return } func getTagContent(tagSelection *goquery.Selection) (_ string, err error) { txt, err := tagSelection.Html() if err != nil { return } return html.UnescapeString(txt), nil } func parseScore(score string) (homeGoals, awayGoals int, err error) { list := strings.Split(score, ":") if len(list) != 2 { err = fmt.Errorf("Wrong score string") return } homeGoals, err = strconv.Atoi(strings.TrimSpace(list[0])) if err != nil { return } awayGoals, err = strconv.Atoi(strings.TrimSpace(list[1])) if err != nil { return } return } var reDirtyTeamID = regexp.MustCompile(`'/team([^']+)'`) func parseTeam(divList *goquery.Selection) (team model.Team, err error) { if divList.Length() == 0 { err = fmt.Errorf("Team div not found") return } aList := divList.First().Find("a.participant-imglink") if aList.Length() == 0 { err = fmt.Errorf("a.participant-imglink not found") return } a := aList.Get(0) textNode := a.FirstChild if textNode == nil { err = fmt.Errorf("a.participant-imglink have not content") return } if textNode.Type != xhtml.TextNode { err = fmt.Errorf("First child of a.participant-imglink must be TextNode, not %s", nodeTypes[textNode.Type]) return } team.CanonicalName = strings.TrimSpace(textNode.Data) onclick, exists := aList.Eq(0).Attr("onclick") if !exists { err = fmt.Errorf("a.participant-imglink have not 'onclick' attr") return } groups := reDirtyTeamID.FindStringSubmatch(onclick) if len(groups) != 2 { err = fmt.Errorf("TeamID in 'onclick' attr not found") return } str := groups[1] // На всякий случай обрезаем слэш в конце str = strings.TrimSuffix(str, "/") team.TeamID = str //parts := strings.Split(str, "/") // Последняя секция - это teamID //team.TeamID = parts[len(parts)-1] return } func parseZoneOnTeamPage(doc *goquery.Document, sportID int) (zone model.TeamZone, err error) { h2List := doc.Find("h2.tournament") if h2List.Length() == 0 { err = fmt.Errorf("Tag h2.tournament not found") return } aList := h2List.First().Find("a") if aList.Length() == 0 { err = fmt.Errorf("Tags not found in h2.tournament tag") return } for i := 0; i < aList.Length(); i++ { a := aList.Eq(i) href, exists := a.Attr("href") if !exists { continue } href = strings.TrimSpace(href) prefix, ok := sportPrefixes[sportID] if !ok { panic(fmt.Sprintf("Bug: sportID %d hasn't sport prefix", sportID)) } href = strings.TrimSuffix(href, "/") if href == prefix { continue } if !strings.HasPrefix(href, prefix) { continue } if href == "" { continue } // Мы нашли zoneID // Достаем имя зоны textNode := aList.Get(i).FirstChild if textNode == nil { err = fmt.Errorf("Zone name not found in content") return } if textNode.Type != xhtml.TextNode { err = fmt.Errorf("FirstChild of tag must be TextNode, not %s", nodeTypes[textNode.Type]) return } zone = model.TeamZone{ ZoneID: href, Name: strings.TrimSpace(textNode.Data), SportID: sportID, } return } //err = fmt.Errorf("ZoneID not found in list") return } // Футбол, парсим HTML код func parseFootballTeamScoreByPeriods(doc *goquery.Document) (m teamScoreByPeriods, hasScoreByPeriods bool, err error) { // Счет 1го тайма m.P1HomeGoals, err = parsePeriodTeamGoals(doc, 1, "home") if err != nil { return } m.P1AwayGoals, err = parsePeriodTeamGoals(doc, 1, "away") if err != nil { return } // Счет 2го тайма m.P2HomeGoals, err = parsePeriodTeamGoals(doc, 2, "home") if err != nil { return } m.P2AwayGoals, err = parsePeriodTeamGoals(doc, 2, "away") if err != nil { return } hasScoreByPeriods = true return } // ODDS // Футбол, парсим HTML код func parseOddsTotal(doc *goquery.Document) (totals []Total, err error) { // 2.5 => *Total totalMap := make(map[string]*Total) divTotal := doc.Find("#block-under-over-ft") if divTotal.Length() == 0 { err = fmt.Errorf("#block-under-over-ft not found") return } tdList := divTotal.First().Find("td.bookmaker") if tdList.Length() == 0 { err = fmt.Errorf("List of td.bookmaker in #block-under-over-ft not found") return } for idx := 0; idx < tdList.Length(); idx++ { //tdList.Each(func(idx int, tdSelection *goquery.Selection) { var offer TotalOffer //.Get(0).Data //.Html() //fmt.Printf("%s\n", txt) //fmt.Printf("%v\n", tdSelection) //tdSelection.Get(0) //tdSelection.Next() tdSelection := tdList.Eq(idx) offer.Bookmaker, err = getBookmaker(tdSelection) if err != nil { return } priceTd := tdSelection.Next() var totalValue string totalValue, err = getTotalValue(priceTd) if err != nil { err = fmt.Errorf("get total value: %s", err) return } priceTd = priceTd.Next() var isRemovedByBookmaker bool isRemovedByBookmaker, offer.Over, err = getTotalPrice(priceTd) if err != nil { err = fmt.Errorf("get over price: %s", err) return } if isRemovedByBookmaker { continue } priceTd = priceTd.Next() isRemovedByBookmaker, offer.Under, err = getTotalPrice(priceTd) if err != nil { err = fmt.Errorf("get under price: %s", err) return } if isRemovedByBookmaker { continue } total, ok := totalMap[totalValue] if !ok { total = &Total{ Total: totalValue, } totalMap[totalValue] = total } total.Offers = append(total.Offers, offer) } for _, ptotal := range totalMap { totals = append(totals, *ptotal) } return } func getTotalValue(tdSelection *goquery.Selection) (total string, err error) { if tdSelection == nil { err = fmt.Errorf(" total tag selection is nil") return } tdNode := tdSelection.Get(0) textNode := tdNode.FirstChild if textNode == nil { err = fmt.Errorf("Empty total's ") return } if textNode.Type != xhtml.TextNode { err = fmt.Errorf("First child of total's must be TextNode, not %s", nodeTypes[textNode.Type]) return } total = strings.TrimSpace(textNode.Data) return } // Цена может быть перечеркнута, значит ее нужно выкинуть из результата func getTotalPrice(tdSelection *goquery.Selection) (isRemovedByBookmaker bool, price float64, err error) { if tdSelection == nil { err = fmt.Errorf(" total price tag selection is nil") return } spanSelection := tdSelection.Find("span") if spanSelection.Length() == 0 { err = fmt.Errorf(" not found in ") return } if spanSelection.First().HasClass("not-published") { isRemovedByBookmaker = true return } span := spanSelection.Get(0) textNode := span.FirstChild if textNode == nil { err = fmt.Errorf("Empty total's price ") return } if textNode.Type != xhtml.TextNode { err = fmt.Errorf("First child of total's price must be TextNode, not %s", nodeTypes[textNode.Type]) return } priceStr := strings.TrimSpace(textNode.Data) if priceStr == "-" { isRemovedByBookmaker = true return } price, err = strconv.ParseFloat(priceStr, 64) if err != nil { err = fmt.Errorf("Parse price: %s; priceStr=%q", err, priceStr) return } return } func parseHandballMatchScore(str string) (homeGoals, awayGoals int, err error) { //fmt.Printf("SCORE: %s\n", str) list, err := ParseFlashscoreEncoding(str) if err != nil { return } // DE home с учетом ОТ // DF away с учетом ОТ // DG home основное время // DH away основное время var isHomeGoalsFound, isAwayGoalsFound bool for _, m := range list { if m["DG"] != "" { homeGoals, err = strconv.Atoi(m["DG"]) if err != nil { err = fmt.Errorf("Atoi: %s; string=%s", err, m["DG"]) return } isHomeGoalsFound = true //fmt.Printf("DH: %s, goals: %d\n", m["DH"], awayGoals) } if m["DH"] != "" { awayGoals, err = strconv.Atoi(m["DH"]) if err != nil { err = fmt.Errorf("Atoi: %s; string=%s", err, m["DH"]) return } isAwayGoalsFound = true //fmt.Printf("DG: %s, goals: %d\n", m["DG"], homeGoals) } } if isHomeGoalsFound && isAwayGoalsFound { return } err = fmt.Errorf("Home goals or/and Away goals not found") return } func parseHandballTeamScoreByPeriods(str string) (stat teamScoreByPeriods, hasScoreByPeriods bool, err error) { // AC÷3¬ вероятно статус // BA÷15¬ p1 home // BB÷10¬ p1 away // ~BC÷15¬ p2 home // BD÷14¬ p2 away // ~A1÷¬~ var isP1HomeFound, isP1AwayFound, isP2HomeFound, isP2AwayFound bool blocks := strings.Split(str, SymbolTilda) for _, block := range blocks { block = strings.TrimSpace(block) if block == "" { continue } pairs := strings.Split(block, SymbolNot) for _, pair := range pairs { pair = strings.TrimSpace(pair) if pair == "" { continue } kv := strings.Split(pair, SymbolDivide) if len(kv) != 2 { err = fmt.Errorf("Wrong key/value: %s", pair) return } key := strings.TrimSpace(kv[0]) value := strings.TrimSpace(kv[1]) switch key { case "BA": stat.P1HomeGoals, err = strconv.Atoi(value) if err != nil { err = fmt.Errorf("Atoi: %s; string=%s", err, value) return } isP1HomeFound = true case "BB": stat.P1AwayGoals, err = strconv.Atoi(value) if err != nil { err = fmt.Errorf("Atoi: %s; string=%s", err, value) return } isP1AwayFound = true case "BC": stat.P2HomeGoals, err = strconv.Atoi(value) if err != nil { err = fmt.Errorf("Atoi: %s; string=%s", err, value) return } isP2HomeFound = true case "BD": stat.P2AwayGoals, err = strconv.Atoi(value) if err != nil { err = fmt.Errorf("Atoi: %s; string=%s", err, value) return } isP2AwayFound = true } } } if isP1HomeFound && isP1AwayFound && isP2HomeFound && isP2AwayFound { hasScoreByPeriods = true return } // В этом месте нет ошибки, но hasScoreByPeriods = false return } type PeriodScore struct { PeriodNumber int HomeScore int AwayScore int } func parseTennisTeamScoreByPeriods(str string) (periods []PeriodScore, hasScoreByPeriods bool, err error) { // Счет 2:1, по сетам: 7-6(7-2) 4-6 6-2 // Тильда ~ вероятно разделяет периоды // AC÷3¬ вероятно статус // BA÷7¬ s1 home // DA÷7¬ s1 tiebreak home // BB÷6¬ s1 away // DB÷2¬ s1 tiebreak away // RC÷1:05¬ // ~ // BC÷4¬ s2 home // BD÷6¬ s2 away // RD÷0:50¬ // ~ // BE÷6¬ s3 home // BF÷2¬ s3 away // RE÷0:43¬ // ~ // RB÷2:38¬ // ~ // PSPH÷WC¬ // PSPA÷11¬ // A1÷¬~ // Счет 3:2, 3-6 7-6(7-5) 7-6(7-4) 0-6 6-3 // AC÷3¬ // BA÷3¬ // BB÷6¬ // RC÷0:36¬ продолжительность периода // ~ // BC÷7¬ // DC÷7¬ // BD÷6¬ // DD÷5¬ // RD÷1:18¬ // ~ // BE÷7¬ // DE÷7¬ // BF÷6¬ // DF÷4¬ // RE÷1:08¬ // ~ // BG÷0¬ s4 home // BH÷6¬ s4 away // RF÷0:29¬ // ~ // BI÷6¬ s5 home // BJ÷3¬ s5 away // RG÷0:38¬ // ~ // RB÷4:09¬ продолжительность матча // ~ // PSPH÷20¬ // PSPA÷12¬ // A1÷¬~ blocks := strings.Split(str, SymbolTilda) for _, block := range blocks { block = strings.TrimSpace(block) if block == "" { continue } var ( period PeriodScore periodSignature string ) pairs := strings.Split(block, SymbolNot) for _, pair := range pairs { pair = strings.TrimSpace(pair) if pair == "" { continue } kv := strings.Split(pair, SymbolDivide) if len(kv) != 2 { err = fmt.Errorf("Wrong key/value: %s", pair) return } key := strings.TrimSpace(kv[0]) value := strings.TrimSpace(kv[1]) switch key { case "BA", "BC", "BE", "BG", "BI": period.HomeScore, err = strconv.Atoi(value) if err != nil { err = fmt.Errorf("Atoi: %s; string=%s", err, value) return } // записываем в начало, чтобы независимо от порядка полей в исходной // строке получить одинаковую подпись periodSignature = key + periodSignature case "BB", "BD", "BF", "BH", "BJ": period.AwayScore, err = strconv.Atoi(value) if err != nil { err = fmt.Errorf("Atoi: %s; string=%s", err, value) return } // дописываем в конец, чтобы независимо от порядка полей в исходной // строке получить одинаковую подпись periodSignature += key } } switch periodSignature { case "BABB": period.PeriodNumber = 1 case "BCBD": period.PeriodNumber = 2 case "BEBF": period.PeriodNumber = 3 case "BGBH": period.PeriodNumber = 4 case "BIBJ": period.PeriodNumber = 5 default: if periodSignature != "" { // Что-то нашло но не то, что нужно (например только home), // поэтому обработку прерываем return } } if period.PeriodNumber > 0 { periods = append(periods, period) } } if len(periods) > 0 { // Периоды найдены, обработчка не была прервана, значит все ок hasScoreByPeriods = true } return } //type FlashscoreEncodedData func ParseFlashscoreEncoding(str string) (_ []map[string]string, err error) { var list []map[string]string blocks := strings.Split(str, SymbolTilda) for _, block := range blocks { block = strings.TrimSpace(block) if block == "" { continue } pairs := strings.Split(block, SymbolNot) m := make(map[string]string) for _, pair := range pairs { pair = strings.TrimSpace(pair) if pair == "" { continue } kv := strings.Split(pair, SymbolDivide) if len(kv) != 2 { err = fmt.Errorf("Wrong key/value: %s", pair) return } key := strings.TrimSpace(kv[0]) value := strings.TrimSpace(kv[1]) m[key] = value } list = append(list, m) } return list, nil } func parseFlashscoreEncodedOdds(str string) (odds Odds, err error) { decoded, err := ParseFlashscoreEncoding(str) if err != nil { return } var ( market string period string total *Total totals = make(map[string]*Total) h1Totals = make(map[string]*Total) ) for _, m := range decoded { if market == "" { // Ищем рынок market = getMarket(m["OAI"]) } else if period == "" { // Рынок есть - ищем период if m["OAI"] != "" { // Нужный период не нашли, но встретили новый рынок market = getMarket(m["OAI"]) period = "" } else if m["OBI"] != "" { period = getPeriod(m["OBI"]) } } else { // Есть и рынок и период switch market { case "1x2": // Накапливаем offers букмекеров пока не встретим OAI или OBI if m["OAI"] != "" { // Встретили новый рынок market = getMarket(m["OAI"]) period = "" } else if m["OBI"] != "" { // Встретили новый период period = getPeriod(m["OBI"]) } else { // Ищем предложения букмекеров if m["OG"] == "0" { // Перечеркнутое предложение continue } if m["XA"] != "" && m["XB"] != "" && m["XC"] != "" { winner, perr := parseFlashscoreEncodedWinner3Way(m) if perr != nil { // log fmt.Println(perr) } else { odds.Winner = append(odds.Winner, winner) } } } case "12": // Накапливаем offers букмекеров пока не встретим OAI или OBI if m["OAI"] != "" { // Встретили новый рынок market = getMarket(m["OAI"]) period = "" } else if m["OBI"] != "" { // Встретили новый период period = getPeriod(m["OBI"]) } else { // Ищем предложения букмекеров if m["OG"] == "0" { // Перечеркнутое предложение continue } if m["XB"] != "" && m["XC"] != "" { winner, perr := parseFlashscoreEncodedWinner2Way(m) if perr != nil { // log fmt.Println(perr) } else { odds.Winner2Way = append(odds.Winner2Way, winner) } } } case "total": switch period { case "ft": // Накапливаем offers букмекеров пока не встретим OAI или OBI if m["OAI"] != "" { // Встретили новый рынок market = getMarket(m["OAI"]) period = "" total = nil //totalValue = "" } else if m["OBI"] != "" { period = getPeriod(m["OBI"]) total = nil } else if m["OCT"] == "Total" || m["OCT"] == "Games" { // Total - handball, football; Games - tennis totalValue := m["OC"] total = totals[totalValue] if total == nil { total = &Total{ Total: totalValue, } totals[totalValue] = total } } else { if m["XB"] != "" && m["XC"] != "" { isRemovedByBookmaker, offer, perr := parseFlashscoreEncodedTotalOffer(m) if perr != nil { // log fmt.Println(perr) } else { if !isRemovedByBookmaker { if total != nil { total.Offers = append(total.Offers, offer) } } } } } case "1hf", "set1": // Накапливаем offers букмекеров пока не встретим OAI или OBI if m["OAI"] != "" { // Встретили новый рынок market = getMarket(m["OAI"]) period = "" total = nil //totalValue = "" } else if m["OBI"] != "" { period = getPeriod(m["OBI"]) total = nil } else if m["OCT"] == "Total" || m["OCT"] == "Games" { // Total - handball, football; Games - tennis totalValue := m["OC"] total = totals[totalValue] if total == nil { total = &Total{ Total: totalValue, } h1Totals[totalValue] = total } } else { if m["XB"] != "" && m["XC"] != "" { isRemovedByBookmaker, offer, perr := parseFlashscoreEncodedTotalOffer(m) if perr != nil { // log fmt.Println(perr) } else { if !isRemovedByBookmaker { if total != nil { total.Offers = append(total.Offers, offer) } } } } } default: panic(fmt.Sprintf("Bug: unknown total period %q", period)) } } } } for _, ptotal := range totals { odds.FullTimeTotal = append(odds.FullTimeTotal, *ptotal) } for _, ptotal := range h1Totals { odds.H1Total = append(odds.H1Total, *ptotal) } return } // little helper func getMarket(code string) string { switch code { // football, handball case "1x2": return "1x2" // football, handball, tennis case "under-over": return "total" // tennis case "moneyline": return "12" default: return "" } } // little helper func getPeriod(code string) string { switch code { // football, handball, tennis case "ft": return "ft" // football, handball case "1hf": return "1hf" // tennis case "set1": return "set1" default: return "" } } func parseFlashscoreEncodedWinner3Way(m map[string]string) (winner Winner3Way, err error) { winner.Bookmaker, err = strconv.Atoi(m["OE"]) if err != nil { err = fmt.Errorf("Parse bookmakerID: %s; string=%s", err, m["OE"]) return } winner.Home, err = parseFlashscoreEncodedPrice(m["XA"]) if err != nil { return } winner.Away, err = parseFlashscoreEncodedPrice(m["XC"]) if err != nil { return } winner.Draw, err = parseFlashscoreEncodedPrice(m["XB"]) if err != nil { return } return } func parseFlashscoreEncodedWinner2Way(m map[string]string) (winner Winner2Way, err error) { winner.Bookmaker, err = strconv.Atoi(m["OE"]) if err != nil { err = fmt.Errorf("Parse bookmakerID: %s; string=%s", err, m["OE"]) return } winner.Home, err = parseFlashscoreEncodedPrice(m["XB"]) if err != nil { return } winner.Away, err = parseFlashscoreEncodedPrice(m["XC"]) if err != nil { return } return } func parseFlashscoreEncodedTotalOffer(m map[string]string) (isRemovedByBookmaker bool, offer TotalOffer, err error) { offer.Bookmaker, err = strconv.Atoi(m["OE"]) if err != nil { err = fmt.Errorf("Parse bookmakerID: %s; string=%s", err, m["OE"]) return } offer.Over, err = parseFlashscoreEncodedPrice(m["XB"]) if err != nil { return } offer.Under, err = parseFlashscoreEncodedPrice(m["XC"]) if err != nil { return } // Перечеркнутое предложение if m["OG"] == "0" { isRemovedByBookmaker = true } return } // Функция пасит простые поля вида "1.44" и более сложные вида "2.24[u]2.39" func parseFlashscoreEncodedPrice(str string) (price float64, err error) { str = strings.TrimSpace(str) list := strings.Split(str, "]") // Последний элемент str = list[len(list)-1] price, err = strconv.ParseFloat(str, 64) if err != nil { err = fmt.Errorf("Parse price: %s; string=%s", err, str) return } return } /////////////// // little helper func getH2HTab(tabName string) string { tabName = strings.ToUpper(tabName) if strings.Contains(tabName, "OVERALL") { return "overall" } return "" } // little helper func getH2HList(listName, homeTeamName, awayTeamName string) string { // Приводим все строки к единому регистру listName = strings.ToUpper(listName) homeTeamName = strings.ToUpper(homeTeamName) awayTeamName = strings.ToUpper(awayTeamName) // 3 списка, примеры: // // Last matches: HSG WETZLAR // Last matches: MT MELSUNGEN // Head-to-head matches: HSG WETZLAR - MT MELSUNGEN //fmt.Printf("listName: '%s'; home='%s'; away='%s'\n", listName, homeTeamName, awayTeamName) // 'HEAD-TO-HEAD MATCHES' если h2h матчей нет if strings.Contains(listName, homeTeamName) { if strings.Contains(listName, awayTeamName) { // Обе команды return "h2h" } // Только home return "home" } if strings.Contains(listName, awayTeamName) { // Только away return "away" } return "" } func parseFlashscoreEncodedH2H(str, homeTeamName, awayTeamName string) (homeTeamMatches, awayTeamMatches, h2hMatches []model.TeamMatch, err error) { decoded, err := ParseFlashscoreEncoding(str) if err != nil { return } pretty.PPrintln("DECODED", decoded) var ( // Tabs на H2H странице: Overall, HSG Wetzlar - Home, MT Melsungen - Away tab string // Списки последних матчей: хозяев, гостей, h2h list string ) for _, m := range decoded { if tab == "" { // Ищем Tab tab = getH2HTab(m["KA"]) } else if list == "" { // Рынок есть - ищем период if m["KA"] != "" { // Нужный период не нашли, но встретили новый рынок tab = getH2HTab(m["KA"]) list = "" } else if m["KB"] != "" { list = getH2HList(m["KB"], homeTeamName, awayTeamName) } } else { // Есть и рынок и список if m["KA"] != "" { // Нужный период не нашли, но встретили новый рынок tab = getH2HTab(m["KA"]) list = "" } else if m["KB"] != "" { list = getH2HList(m["KB"], homeTeamName, awayTeamName) } else { if m["KC"] != "" && m["KP"] != "" { //startTime, _ := strconv.ParseInt(m["KC"], 10, 64) match := model.TeamMatch{ MatchID: m["KP"], //ChampID: m["KJ"] + " - " + m["KK"], Чисто для дебага можно выводить } switch list { case "home": homeTeamMatches = append(homeTeamMatches, match) case "away": awayTeamMatches = append(awayTeamMatches, match) case "h2h": h2hMatches = append(h2hMatches, match) default: panic(fmt.Sprintf("Bug: unknown Tab's listName %q", list)) } } } } } return } //ZA zone: champ name extended //ZL "champID" /* AA: 2XhOe000 AD: 1595772000 ADE: 1595772000 AB: 1 CR: 1 AC: 1 CX: Southampton ER: Round 38 RW: 0 AX: 0 BX: -1 WQ: WM: SOU AE: Southampton JA: fu16YetP WU: southampton WN: SHU AF: Sheffield Utd JB: bDRnslpP WV: sheffield-utd AW: 1 AN: n Расшифровка: ZA - champ name AA - ID матча, нужен чтобы получить статистику по матчу AD / ADE - время начала матча в UTC CX - home team AF - away team WU - home team url part (нужно ли ?) WV - away team url part (нужно ли ?) ER - раунд (можно не парсить, но сгруппировать) */ func parseFlashscoreEncodedAllGames(str string, sportID int) (matches []ScheduledMatch, err error) { decoded, err := ParseFlashscoreEncoding(str) if err != nil { return } var ( champID string zone model.TeamZone fullChampName string ) for _, m := range decoded { zl := m["ZL"] // если поле непустое - словарь описывает чемпионат, а не матч if zl != "" { champID = strings.TrimSpace(zl) champID = strings.TrimSuffix(champID, "/") // Пример /football/vietnam/v-league-1 parts := strings.Split(champID, "/") if len(parts) < 3 { continue } zone.ZoneID = strings.Join(parts[:3], "/") parts = strings.Split(m["ZA"], ":") zone.Name = strings.TrimSpace(strings.Title(strings.ToLower(parts[0]))) zone.SportID = sportID fullChampName = m["ZA"] } else { //if m["AA"] != "" && m["AF"] != "" && m["CX"] != "" { if m["AA"] != "" && m["AD"] != "" { startTime, _ := strconv.ParseInt(m["AD"], 10, 64) match := ScheduledMatch{ MatchID: m["AA"], StartTime: time.Unix(startTime, 0), ChampID: champID, Zone: zone, SportID: sportID, FullChampName: fullChampName, } matches = append(matches, match) } } } return } // Служебные структуры для метода parseTeamsOnMatchPage type jsObjectTeam struct { TeamID string `json:"id"` Name string `json:"name"` } type jsObjectParticipantsData struct { Home []jsObjectTeam `json:"home"` Away []jsObjectTeam `json:"away"` } type jsVarEnvironment struct { ParticipantsData jsObjectParticipantsData `json:"participantsData"` } // parseTeamsOnMatchPageInJsVar - метод извлекает имена и TeamID команд из тега