Documentation
¶
Overview ¶
Package forage is a scraping engine: a Client that fetches a URL politely, identifies itself honestly, and reports what it got.
Get picks an identity from the pool, checks robots.txt, waits its turn for the host, fetches over plain HTTP with forage's own User-Agent, classifies the page, and retries with backoff after a network error or a server error. A block or challenge is never retried: it stops the Client from sending any further request to that host and is reported to the caller as a *BlockedError. When a browser is configured it is used for pages that need JavaScript.
The parts live in subpackages and can be used on their own: fetch, browser, session, pace, detect, extract, queue and metrics. Site-specific parsing belongs in the application, not here.
Example ¶
The README's minimal example. It compiles as part of the test suite and does not run, because it reaches a real site.
package main
import (
"bytes"
"context"
"errors"
"fmt"
"log"
"github.com/pigfox/forage"
"github.com/pigfox/forage/extract"
)
func main() {
client, err := forage.New(forage.Config{})
if err != nil {
log.Fatal(err)
}
res, err := client.Get(context.Background(), "https://example.com/")
if errors.Is(err, forage.ErrHostBlocked) {
log.Fatal(err) // the site said no; this client will not ask it again
}
if err != nil {
log.Fatal(err) // no response at all, or robots.txt said no
}
fmt.Println(res.Status, res.Outcome, res.Identity.ID, len(res.Body))
product := extract.Set{Fields: []extract.Field{
{Name: "title", Required: true, Selectors: []extract.Selector{extract.Text("h1")}},
{Name: "link", Selectors: []extract.Selector{extract.Attr("a", "href")}},
}}
rec, err := product.ExtractHTML(bytes.NewReader(res.Body))
if errors.Is(err, extract.ErrMissingField) {
log.Fatal(err) // the markup changed: fail loudly
}
fmt.Println(rec["title"], rec["link"])
}
Output:
Example (Configured) ¶
A fuller setup: your own User-Agent, several identities, a slower pace for one host, and headless Chrome for pages that need JavaScript. A host that blocks or challenges the client is not asked again.
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/pigfox/forage"
"github.com/pigfox/forage/browser"
"github.com/pigfox/forage/pace"
"github.com/pigfox/forage/session"
)
func main() {
if err := configured(context.Background()); err != nil {
log.Fatal(err)
}
}
func configured(ctx context.Context) error {
pool, err := session.NewPool(session.Config{
Size: 3,
Language: "en-GB",
Timezone: "Europe/London",
})
if err != nil {
return err
}
limiter := pace.NewLimiter(1, 1)
limiter.SetHost("slow.example", 0.2, 1)
chrome, err := browser.New(browser.Options{})
if err != nil {
return err
}
defer chrome.Close()
client, err := forage.New(forage.Config{
UserAgent: "examplebot/1.0 (+mailto:ops@example.com)",
Pool: pool,
Limiter: limiter,
Browser: chrome,
Retry: pace.Policy{MaxAttempts: 4},
})
if err != nil {
return err
}
res, err := client.Get(ctx, "https://example.com/app")
var blocked *forage.BlockedError
if errors.As(err, &blocked) {
fmt.Println("stopped at", blocked.Host, blocked.Outcome)
return nil
}
if err != nil {
return err
}
fmt.Println(res.Via, res.Outcome)
return nil
}
Output:
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrDisallowed = errors.New("forage: disallowed by robots.txt")
ErrDisallowed is returned, wrapped in a *DisallowedError, when robots.txt forbids the URL.
var ErrHostBlocked = errors.New("forage: host blocked this client")
ErrHostBlocked is returned, wrapped in a *BlockedError, when a host has blocked or challenged this Client.
Functions ¶
This section is empty.
Types ¶
type BlockedError ¶ added in v0.2.0
type BlockedError struct {
Host string // lowercase host, with the port if the URL had one
URL string // the page that was blocked
Status int // its status code
Outcome detect.Outcome // detect.Blocked or detect.Captcha
At time.Time // when the block was seen, in UTC
}
BlockedError reports the block or challenge that stopped a Client from sending further requests to a host.
func (*BlockedError) Error ¶ added in v0.2.0
func (e *BlockedError) Error() string
func (*BlockedError) Is ¶ added in v0.2.0
func (e *BlockedError) Is(target error) bool
Is makes errors.Is(err, ErrHostBlocked) true.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client fetches pages. It is safe for concurrent use.
func (*Client) BlockedHosts ¶ added in v0.2.0
func (c *Client) BlockedHosts() []BlockedError
BlockedHosts returns every host this Client has stopped sending requests to, sorted by host.
func (*Client) Extract ¶
Extract runs a selector set over a Result's body and counts a parse failure against the host and identity when a required field is missing.
func (*Client) ExtractAll ¶
ExtractAll is Extract for a list of items, such as search results.
func (*Client) Get ¶
Get fetches rawURL. It returns a Result whenever some attempt got a response — check Outcome and Status. It returns an error when no attempt got a response at all, when robots.txt forbids the URL, when ctx ends, or when the host blocked this Client.
A block or challenge is final. The first time a host answers with one, Get returns that page together with a *BlockedError, and from then on every Get to the same host returns a *BlockedError at once without sending anything. The request is never retried, not through another identity and not through the browser. The stop lasts as long as the Client does; BlockedHosts lists the hosts it covers.
type Config ¶
type Config struct {
// HTTP is the primary fetcher. Default fetch.New(fetch.Options{}).
HTTP fetch.Fetcher
// Browser, when set, renders pages that need JavaScript. A
// *browser.Browser is the usual choice. Nil means HTTP only. A page
// that was blocked or challenged is never sent to the Browser.
Browser fetch.Fetcher
// NeedsJS decides whether a page needs the browser. Default
// detect.NeedsJS.
NeedsJS func(detect.Page) bool
// UserAgent, when set, replaces the User-Agent on every request.
// Otherwise the identity's profile decides, and that is
// fetch.DefaultUserAgent unless the pool was given another profile.
// Name your crawler and give a contact address, such as
// "examplebot/1.0 (+mailto:ops@example.com)".
UserAgent string
// Pool supplies identities. Default: one direct identity with
// fetch.DefaultProfile.
Pool *session.Pool
// Limiter spaces requests per host. Default one request per second
// per host.
Limiter *pace.Limiter
// Retry decides retries and their backoff. Default three attempts.
Retry pace.Policy
// Detector classifies pages. Default detect.Default().
Detector detect.Detector
// Metrics receives counts. Default metrics.Nop.
Metrics metrics.Hook
// IgnoreRobots turns the robots.txt check off. It is on by default.
IgnoreRobots bool
// RobotsAgent is the product token matched against robots.txt groups.
// Default "forage", which falls back to the "*" group.
RobotsAgent string
// RobotsTTL is how long a robots.txt is cached. Default 24h.
RobotsTTL time.Duration
}
Config wires a Client. Every field is optional.
type DisallowedError ¶
type DisallowedError struct {
URL string
}
DisallowedError names the URL robots.txt forbids.
func (*DisallowedError) Error ¶
func (e *DisallowedError) Error() string
func (*DisallowedError) Is ¶
func (e *DisallowedError) Is(target error) bool
Is makes errors.Is(err, ErrDisallowed) true.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package browser fetches pages with headless Chrome, for sites that build their content with JavaScript.
|
Package browser fetches pages with headless Chrome, for sites that build their content with JavaScript. |
|
Package detect classifies a fetched page as usable, blocked, a captcha or challenge, or suspiciously empty.
|
Package detect classifies a fetched page as usable, blocked, a captcha or challenge, or suspiciously empty. |
|
Package extract pulls named fields out of HTML with CSS selectors.
|
Package extract pulls named fields out of HTML with CSS selectors. |
|
Package fetch is a plain HTTP client that identifies itself honestly.
|
Package fetch is a plain HTTP client that identifies itself honestly. |
|
Package metrics counts what a scraper does, per host and per identity.
|
Package metrics counts what a scraper does, per host and per identity. |
|
Package pace decides when the next request may go: a token bucket per host, jittered exponential backoff between attempts, a retry policy, and a robots.txt check.
|
Package pace decides when the next request may go: a token bucket per host, jittered exponential backoff between attempts, a retry policy, and a robots.txt check. |
|
Package queue holds scrape jobs: a URL and an optional payload, leased to one worker at a time, retried on failure, and moved to a dead-letter state after too many attempts.
|
Package queue holds scrape jobs: a URL and an optional payload, leased to one worker at a time, retried on failure, and moved to a dead-letter state after too many attempts. |
|
Package session keeps a pool of identities — a proxy (or a direct connection) and a cookie jar each, all sending the same honest header profile, language and timezone — and tracks how well each one is doing.
|
Package session keeps a pool of identities — a proxy (or a direct connection) and a cookie jar each, all sending the same honest header profile, language and timezone — and tracks how well each one is doing. |