1595 lines
36 KiB
Go
Executable File
1595 lines
36 KiB
Go
Executable File
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 <span> not found in <td>: %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 <span class="p%d_%s">)`,
|
||
strings.ToUpper(side), period, period, side)
|
||
err = fmt.Errorf(errmsg)
|
||
return
|
||
}
|
||
|
||
textNode := spanTeamGoals.Get(0).FirstChild
|
||
if textNode == nil {
|
||
err = fmt.Errorf("Teams goals <span> 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 <a> 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 <a> content")
|
||
return
|
||
}
|
||
|
||
if textNode.Type != xhtml.TextNode {
|
||
err = fmt.Errorf("FirstChild of tag <a> 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 <a> 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("<td> total tag selection is nil")
|
||
return
|
||
}
|
||
|
||
tdNode := tdSelection.Get(0)
|
||
|
||
textNode := tdNode.FirstChild
|
||
|
||
if textNode == nil {
|
||
err = fmt.Errorf("Empty total's <td>")
|
||
return
|
||
}
|
||
|
||
if textNode.Type != xhtml.TextNode {
|
||
err = fmt.Errorf("First child of total's <td> 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("<td> total price tag selection is nil")
|
||
return
|
||
}
|
||
|
||
spanSelection := tdSelection.Find("span")
|
||
if spanSelection.Length() == 0 {
|
||
err = fmt.Errorf("<span> not found in <td>")
|
||
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 <span>")
|
||
return
|
||
}
|
||
|
||
if textNode.Type != xhtml.TextNode {
|
||
err = fmt.Errorf("First child of total's price <span> 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 команд из тега <script>.
|
||
// Гандбольные матчи хранят эти данные в json в теге <script>.
|
||
func parseTeamsOnMatchPageInJsVar(doc *goquery.Document) (home, away model.Team, err error) {
|
||
var jsonVar []byte
|
||
|
||
//doc.Find("script[type='text/javascript']").EachWithBreak(func(idx int, x *goquery.Selection) bool {
|
||
doc.Find("script").EachWithBreak(func(idx int, x *goquery.Selection) bool {
|
||
txt := x.Text()
|
||
txt = strings.TrimSpace(txt)
|
||
|
||
prefixes := []string{
|
||
"window.environment = ",
|
||
"var environment = ",
|
||
}
|
||
suffix := ";"
|
||
|
||
for _, prefix := range prefixes {
|
||
if strings.HasPrefix(txt, prefix) {
|
||
txt = strings.TrimPrefix(txt, prefix)
|
||
txt = strings.TrimSuffix(txt, suffix)
|
||
|
||
jsonVar = []byte(txt)
|
||
|
||
// JS код нашли - прерываем цикл
|
||
return false
|
||
}
|
||
}
|
||
|
||
// JS код не нашли - переходим к следующему SCRIPT тегу
|
||
return true
|
||
})
|
||
|
||
//fmt.Printf("%q\n\n", jsVar[:20])
|
||
|
||
var env jsVarEnvironment
|
||
|
||
err = json.Unmarshal([]byte(jsonVar), &env)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
h := env.ParticipantsData.Home[0]
|
||
a := env.ParticipantsData.Away[0]
|
||
|
||
home.TeamID = h.TeamID
|
||
home.CanonicalName = h.Name
|
||
|
||
away.TeamID = a.TeamID
|
||
away.CanonicalName = a.Name
|
||
return
|
||
}
|
||
|
||
/*
|
||
|
||
//var reChampID = regexp.MustCompile(`'/football/([^/]+)/([^/]+)/'`)
|
||
var reChampID = regexp.MustCompile(`'/([^/]+)/([^/]+)/([^/]+)/'`)
|
||
|
||
func parseTeamMatchInfo(doc *goquery.Document, sportID int) (zone model.TeamZone, champ model.TeamChamp, home, away model.Team, err error) {
|
||
spanList := doc.Find(".description__country")
|
||
if spanList.Length() == 0 {
|
||
err = fmt.Errorf("Tag .description__country not found")
|
||
return
|
||
}
|
||
|
||
span := spanList.Get(0)
|
||
|
||
dirtyZone := span.FirstChild
|
||
if dirtyZone == nil {
|
||
err = fmt.Errorf("Tag .description__country have not children")
|
||
return
|
||
}
|
||
|
||
if dirtyZone.Type != xhtml.TextNode {
|
||
err = fmt.Errorf("First child of tag .description__country must be TextNode, not %s",
|
||
nodeTypes[dirtyZone.Type])
|
||
return
|
||
}
|
||
|
||
txt := strings.TrimSpace(dirtyZone.Data)
|
||
txt = strings.TrimSuffix(txt, ":")
|
||
|
||
if txt == "" {
|
||
err = fmt.Errorf("Empty zone name found in tag .description__country")
|
||
return
|
||
}
|
||
|
||
txt = strings.ToLower(txt)
|
||
txt = strings.Title(txt)
|
||
|
||
zone.Name = txt
|
||
zone.SportID = sportID
|
||
|
||
a := dirtyZone.NextSibling
|
||
if a == nil {
|
||
err = fmt.Errorf("Tag <a> with champ info not found in the tag .description__country")
|
||
return
|
||
}
|
||
|
||
var onclick string
|
||
|
||
for _, attr := range a.Attr {
|
||
if strings.ToLower(attr.Key) == "onclick" {
|
||
onclick = attr.Val
|
||
break
|
||
}
|
||
}
|
||
|
||
if onclick == "" {
|
||
err = fmt.Errorf("Attr 'onclick' not found in the tag <a> in the .description__country tag")
|
||
return
|
||
}
|
||
// window.open('/football/england/premier-league/'); return false;
|
||
groups := reChampID.FindStringSubmatch(onclick)
|
||
if len(groups) != 4 {
|
||
err = fmt.Errorf("ChampID and ZoneID not found in 'onclick' attr: %s", onclick)
|
||
return
|
||
}
|
||
|
||
zone.ZoneID = fmt.Sprintf("/%s/%s", groups[1], groups[2])
|
||
champ.ChampID = fmt.Sprintf("/%s/%s/%s", groups[1], groups[2], groups[3])
|
||
// Не забываем!
|
||
champ.ZoneID = zone.ZoneID
|
||
|
||
if a.FirstChild == nil {
|
||
err = fmt.Errorf("Empty champ name in <a> tag")
|
||
return
|
||
}
|
||
|
||
dirtyChamp := a.FirstChild
|
||
if dirtyChamp.Type != xhtml.TextNode {
|
||
err = fmt.Errorf("Tag <a> content must be TextNode, not %s",
|
||
nodeTypes[dirtyChamp.Type])
|
||
return
|
||
}
|
||
|
||
parts := strings.Split(dirtyChamp.Data, "-")
|
||
|
||
// Возможный чемпионат 'Club Friendly', где нет раунда, поэтому нужна проверка
|
||
if len(parts) > 1 {
|
||
// Последний элемент - это раунд. Выкидываем
|
||
parts = parts[:len(parts)-1]
|
||
}
|
||
|
||
champ.Name = strings.TrimSpace(strings.Join(parts, "-"))
|
||
|
||
// ВАЖНО!
|
||
// Зону для команды узнаем на странице команды!
|
||
|
||
home, err = parseTeam(doc.Find(".tname-home"))
|
||
if err != nil {
|
||
err = fmt.Errorf("Parse home team: %s", err)
|
||
return
|
||
}
|
||
|
||
away, err = parseTeam(doc.Find(".tname-away"))
|
||
if err != nil {
|
||
err = fmt.Errorf("Parse away team: %s", err)
|
||
return
|
||
}
|
||
return
|
||
}
|
||
|
||
*/
|