celebratoday.go: parse out holidays via another method

This commit is contained in:
eleksir
2026-03-07 20:42:51 +00:00
parent bf2469a9db
commit 300e786293

View File

@@ -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: */