Compare commits
2 Commits
170d0d0ed4
...
300e786293
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
300e786293 | ||
|
|
bf2469a9db |
@@ -6,9 +6,9 @@ import (
|
||||
"aleesa-webapp-go/internal/log"
|
||||
"aleesa-webapp-go/internal/pcachedb"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -18,6 +18,30 @@ import (
|
||||
"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, работающая с кэшем. Запросы надо делать
|
||||
// именно через неё.
|
||||
func Client(cfg *config.MyConfig) (string, error) {
|
||||
@@ -101,8 +125,6 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
||||
ctx = context.Background()
|
||||
url = "https://celebratoday.com/ru/today"
|
||||
respBody string
|
||||
orangeClass int8
|
||||
tgHref int8
|
||||
)
|
||||
|
||||
// Формальная попытка притвориться браузером.
|
||||
@@ -117,78 +139,10 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
||||
return "", fmt.Errorf("unable to GET %s: %w", url, err)
|
||||
}
|
||||
|
||||
var holidays []string
|
||||
holidays, err := parser(respBody)
|
||||
|
||||
// Парсим body ответа от сервера.
|
||||
tokenizer := html.NewTokenizer(strings.NewReader(respBody))
|
||||
|
||||
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
|
||||
|
||||
default:
|
||||
holidays = append(holidays, strings.TrimSuffix(attribute.Val, " | celebratoday.com"))
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Если мы встретили вторую ссылку на телеграм, то конечная, дальше нам не надо.
|
||||
if tgHref >= 2 {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Unable to parse response from celebratoday.com: %w", err)
|
||||
}
|
||||
|
||||
if len(holidays) == 0 {
|
||||
@@ -220,4 +174,60 @@ func UpdateCache(cfg *config.MyConfig, eTime int64, value string) error {
|
||||
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: */
|
||||
|
||||
Reference in New Issue
Block a user