Files
reactor-crw/crawler.go
T
arsam 21721b51d1 Import source: crawler
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:18:52 +03:30

151 lines
4.0 KiB
Go

package reactor_crw
import (
"fmt"
"io"
"strconv"
"reactor-crw/parser"
)
// Transport is an interface that wraps all network related operations performed
// by crawler.
type Transport interface {
FetchData(url string) (io.ReadCloser, error)
}
// HtmlCrawler allows crawling HTML pages using provided transport.Transport and
// parser.Parser. The crawler doesn't do anything with the content itself it only
// gathers the collection of sources links.
type HtmlCrawler struct {
// Transport performs all network request.
Transport Transport
// Parser contains parser.Parser implementation for HTML.
Parser parser.Parser
// MultiPage allows applying the crawler for multiple pages depending on its
// value. If its value set to true the crawler will try to calculate the
// number of available pages using the source page.
MultiPage bool
}
// Fetch retrieves content sources from the page using the path value. Depending
// on HtmlCrawler.MultiPage it may fetch content links from multiple pages.
func (c *HtmlCrawler) Fetch(path string, search []string) (parser.QueryResult, error) {
if c.MultiPage {
return c.multiPage(path, search)
}
collectedData := make(parser.QueryResult)
err := c.fetch(path, search, collectedData)
if err != nil {
return nil, err
}
return collectedData, nil
}
// fetchMultiPage will fetch content sources from multiple pages. It'll try to
// retrieve the number of pages and consequently crawl all pages.
func (c *HtmlCrawler) multiPage(path string, search []string) (parser.QueryResult, error) {
maxPage, err := c.resolveMaxPage(path)
if err != nil {
return nil, err
}
collectedData := make(parser.QueryResult)
for p := 1; p <= maxPage; p++ {
err = c.fetch(fmt.Sprintf("%s/%d", path, p), search, collectedData)
if err != nil {
return nil, err
}
}
return collectedData, nil
}
func (c *HtmlCrawler) fetch(path string, search []string, qr parser.QueryResult) error {
body, err := c.Transport.FetchData(path)
if err != nil {
return err
}
defer func(b io.ReadCloser) {
_ = b.Close()
}(body)
err = c.Parser.FindAttrMap(body, buildQuery(search), qr)
if err != nil {
return fmt.Errorf("cannot apply crawler: %w", err)
}
return nil
}
func (c *HtmlCrawler) resolveMaxPage(path string) (int, error) {
const htmlPagination = ".pagination_expanded .current"
body, err := c.Transport.FetchData(path)
if err != nil {
return 0, err
}
defer func(b io.ReadCloser) {
_ = b.Close()
}(body)
pa, err := c.Parser.FindContent(body, htmlPagination)
if err != nil {
return 0, err
}
intPa, err := strconv.Atoi(pa)
if err != nil {
return 0, fmt.Errorf("cannot prosess pagination: %w", err)
}
return intPa, nil
}
// query represents a search query that will be applied against the parser.
// Each query has a query itself to find corresponding elements, its attr,
// content of which should be returned, contentType marks a type of content
// and used for fetching specific types only.
type query struct {
contentType string
query string
attr string
}
var queries = []query{
{"image", ".post_content .image > img", "src"},
{"image", ".post_content .image > a", "href"},
{"gif", ".post_content .video_gif_source", "href"},
{"mp4", ".post_content .video_gif source[type='video/mp4']", "src"},
{"webm", ".post_content .video_gif source[type='video/webm']", "src"},
}
// buildQuery builds a parser.QueryAttrMap according to provided search list.
// The resulting parser.QueryAttrMap will contain only those queries that meet
// required content types.
//
// Example: buildQuery([]string{"image", "mp4"}).
func buildQuery(search []string) parser.QueryAttrMap {
qa := parser.QueryAttrMap{}
searchMap := make(map[string]struct{}, len(search))
for _, s := range search {
searchMap[s] = struct{}{}
}
for _, sq := range queries {
if _, ok := searchMap[sq.contentType]; ok {
qa[sq.query] = sq.attr
}
}
return qa
}