celebratoday.go: parse out holidays via another method
This commit is contained in:
@@ -6,9 +6,9 @@ import (
|
|||||||
"aleesa-webapp-go/internal/log"
|
"aleesa-webapp-go/internal/log"
|
||||||
"aleesa-webapp-go/internal/pcachedb"
|
"aleesa-webapp-go/internal/pcachedb"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,6 +18,30 @@ import (
|
|||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Holy структура, описывающая искомый json-чик.
|
||||||
|
Holy struct {
|
||||||
|
AtContext string `json:"@context"`
|
||||||
|
AtType string `json:"@type"`
|
||||||
|
MainEntity []struct {
|
||||||
|
AtType string `json:"@type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
AcceptedAnswer struct {
|
||||||
|
AtType string `json:"@type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
UpVoteCount int `json:"upvoteCount"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
} `json:"acceptedAnswer"`
|
||||||
|
SuggestedAnswer []struct {
|
||||||
|
AtType string `json:"@type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
UpVoteCount int `json:"upvoteCount"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
} `json:"suggestedAnswer"`
|
||||||
|
} `json:"mainEntity"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Client обёртка для клиента сервиса celebratoday.com, работающая с кэшем. Запросы надо делать
|
// Client обёртка для клиента сервиса celebratoday.com, работающая с кэшем. Запросы надо делать
|
||||||
// именно через неё.
|
// именно через неё.
|
||||||
func Client(cfg *config.MyConfig) (string, error) {
|
func Client(cfg *config.MyConfig) (string, error) {
|
||||||
@@ -98,11 +122,9 @@ func Client(cfg *config.MyConfig) (string, error) {
|
|||||||
// APIClient клиент сервиса https://celebratoday.com/.
|
// APIClient клиент сервиса https://celebratoday.com/.
|
||||||
func APIClient(cfg *config.MyConfig) (string, error) {
|
func APIClient(cfg *config.MyConfig) (string, error) {
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
url = "https://celebratoday.com/ru/today"
|
url = "https://celebratoday.com/ru/today"
|
||||||
respBody string
|
respBody string
|
||||||
orangeClass int8
|
|
||||||
tgHref int8
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Формальная попытка притвориться браузером.
|
// Формальная попытка притвориться браузером.
|
||||||
@@ -117,93 +139,10 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
return "", fmt.Errorf("unable to GET %s: %w", url, err)
|
return "", fmt.Errorf("unable to GET %s: %w", url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var holidays []string
|
holidays, err := parser(respBody)
|
||||||
|
|
||||||
// Парсим body ответа от сервера.
|
if err != nil {
|
||||||
tokenizer := html.NewTokenizer(strings.NewReader(respBody))
|
return "", fmt.Errorf("Unable to parse response from celebratoday.com: %w", err)
|
||||||
|
|
||||||
for {
|
|
||||||
tokenType := tokenizer.Next()
|
|
||||||
|
|
||||||
if tokenType == html.ErrorToken {
|
|
||||||
err := tokenizer.Err()
|
|
||||||
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
// End of the file, break out of the loop.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenType == html.StartTagToken {
|
|
||||||
token := tokenizer.Token()
|
|
||||||
|
|
||||||
if token.Data == "a" {
|
|
||||||
for _, attribute := range token.Attr {
|
|
||||||
// Кнопка о том, что у нас было вчера.
|
|
||||||
if attribute.Key == "class" && attribute.Val == "text-orange-500 underline hover:no-underline" {
|
|
||||||
orangeClass++
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Наши ссылки находятся после первого и до второго упоминания о телеграме.
|
|
||||||
if attribute.Key == "href" && attribute.Val == "https://t.me/celebratoday" {
|
|
||||||
tgHref++
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if attribute.Key == "title" && tgHref == 1 && orangeClass == 2 {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(attribute.Val, "Праздники "):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Международные праздники "):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Всемирные праздники "):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Спортивные праздники "):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Медицинские праздники"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Подробнее про"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Необычные праздники"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Профессиональные праздники"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Национальные праздники"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Народные праздники"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
case strings.HasPrefix(attribute.Val, "Религиозные праздники"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
default:
|
|
||||||
holidays = append(holidays, strings.TrimSuffix(attribute.Val, " | celebratoday.com"))
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если мы встретили вторую ссылку на телеграм, то конечная, дальше нам не надо.
|
|
||||||
if tgHref >= 2 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(holidays) == 0 {
|
if len(holidays) == 0 {
|
||||||
@@ -235,4 +174,60 @@ func UpdateCache(cfg *config.MyConfig, eTime int64, value string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parser разбирает буфер с html-ем, выискивая праздники.
|
||||||
|
func parser(data string) ([]string, error) {
|
||||||
|
var (
|
||||||
|
holidays = make([]string, 0, 1)
|
||||||
|
err error
|
||||||
|
s = make([]Holy, 0, 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
doc, err := html.Parse(strings.NewReader(data))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return holidays, err
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(doc, &s)
|
||||||
|
|
||||||
|
// А теперь пробежимся по полученной структуре и вытянем из неё все строки с праздниками.
|
||||||
|
for _, ass := range s {
|
||||||
|
for _, entity := range ass.MainEntity {
|
||||||
|
for _, sugestedAnswer := range entity.SuggestedAnswer {
|
||||||
|
holidays = append(holidays, sugestedAnswer.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return holidays, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// traverse рекурсивная функция, ищущая <script type="application/ld+json"> в структуре документа. Там внутри будет json
|
||||||
|
// со списком праздников и это и есть цель нашего поиска.
|
||||||
|
func traverse(n *html.Node, h *[]Holy) {
|
||||||
|
// Если найденный элемент в поле Data содержит script (то есть это тэг <script>).
|
||||||
|
if n.Type == html.ElementNode && n.Data == "script" {
|
||||||
|
// Ищем атрибут type со значением application/ld+json.
|
||||||
|
for _, attr := range n.Attr {
|
||||||
|
if attr.Key == "type" && attr.Val == "application/ld+json" {
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
if c.Type == html.TextNode {
|
||||||
|
// Добавлять рановато, надо вначале попробовать распарсить этот json
|
||||||
|
// используя целевую структуру.
|
||||||
|
var holiday Holy
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(c.Data), &holiday); err == nil {
|
||||||
|
*h = append(*h, holiday)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
traverse(c, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
|
|||||||
Reference in New Issue
Block a user