wayfarer 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/actions/setup-mise/action.yaml +22 -0
- data/.github/workflows/push.yaml +89 -0
- data/.github/workflows/release.yaml +2 -2
- data/.gitignore +1 -0
- data/AGENTS.md +265 -0
- data/CLAUDE.md +1 -0
- data/Dockerfile +1 -0
- data/Gemfile.lock +5 -1
- data/docs/cookbook/batch_routing.md +3 -1
- data/docs/cookbook/consent_screen.md +3 -1
- data/docs/cookbook/executing_javascript.md +9 -3
- data/docs/cookbook/navigation.md +9 -3
- data/docs/cookbook/querying_html.md +9 -3
- data/docs/cookbook/screenshots.md +6 -2
- data/docs/guides/cli.md +1 -1
- data/docs/guides/debugging.md +1 -1
- data/docs/guides/development.md +18 -26
- data/docs/guides/handlers.md +1 -1
- data/docs/guides/jobs.md +13 -9
- data/docs/guides/middleware.md +134 -0
- data/docs/guides/navigation.md +6 -3
- data/docs/guides/networking/capybara.md +3 -1
- data/docs/guides/networking/custom_adapters.md +3 -3
- data/docs/guides/networking/ferrum.md +3 -1
- data/docs/guides/networking/http.md +4 -2
- data/docs/guides/networking/selenium.md +7 -3
- data/docs/guides/pages.md +4 -2
- data/docs/guides/routing.md +16 -8
- data/docs/guides/tasks.md +1 -1
- data/docs/guides/tutorial.md +73 -1
- data/docs/guides/user_agents.md +10 -10
- data/docs/index.md +1 -1
- data/lib/wayfarer/base.rb +6 -10
- data/lib/wayfarer/cli.rb +19 -1
- data/lib/wayfarer/gc.rb +0 -1
- data/lib/wayfarer/handler.rb +7 -5
- data/lib/wayfarer/logging.rb +3 -4
- data/lib/wayfarer/middleware/controller.rb +9 -8
- data/lib/wayfarer/middleware/dependency_graph.rb +181 -0
- data/lib/wayfarer/routing/matchers/query.rb +3 -2
- data/lib/wayfarer.rb +23 -2
- data/mise.toml +2 -0
- data/mkdocs.yml +1 -0
- data/rake/lint.rake +16 -0
- data/spec/spec_helpers.rb +1 -1
- data/spec/wayfarer/cli/routing_spec.rb +36 -6
- data/spec/wayfarer/integration/dependency_graph_spec.rb +111 -0
- data/spec/wayfarer/middleware/controller_spec.rb +34 -9
- data/spec/wayfarer/middleware/dependency_graph_spec.rb +367 -0
- data/spec/wayfarer/routing/integration_spec.rb +18 -0
- data/spec/wayfarer/routing/matchers/query_spec.rb +18 -0
- data/wayfarer.gemspec +1 -1
- metadata +11 -3
- data/.github/workflows/lint.yaml +0 -27
- data/.github/workflows/tests.yaml +0 -21
data/docs/guides/development.md
CHANGED
|
@@ -2,21 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Release Procedure
|
|
4
4
|
|
|
5
|
-
1.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
git checkout master
|
|
13
|
-
git pull origin master --rebase
|
|
14
|
-
bundle exec rake build
|
|
15
|
-
gem push build/wayfarer-*.gem
|
|
16
|
-
bundle exec rake clean
|
|
17
|
-
git tag <VERSION>
|
|
18
|
-
git push origin <VERSION>
|
|
19
|
-
```
|
|
5
|
+
1. Bump versions:
|
|
6
|
+
* `Wayfarer::VERSION` in `lib/wayfarer.rb`
|
|
7
|
+
* RubyGem version in `wayfarer.gemspec`
|
|
8
|
+
* Run `bundle install` to regenerate `Gemfile.lock`
|
|
9
|
+
2. Push to `master` or `develop` and run the manual `release` worflow
|
|
20
10
|
|
|
21
11
|
## Conventions and guidelines
|
|
22
12
|
|
|
@@ -27,10 +17,11 @@ git push origin <VERSION>
|
|
|
27
17
|
|
|
28
18
|
### Navigate the web along URL patterns
|
|
29
19
|
|
|
30
|
-
URLs are less prone to change than served markup.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
URLs are less prone to change than served markup. There are also SEO
|
|
21
|
+
incentives to keep their paths stable. Since websites naturally implement
|
|
22
|
+
architectural URL patterns like REST on the application layer, URL structure
|
|
23
|
+
reflects internal domain concepts necessarily, for which Wayfarer's URL-based
|
|
24
|
+
routing is designed.
|
|
34
25
|
|
|
35
26
|
### Follow URLs verbatim as they appear in responses
|
|
36
27
|
|
|
@@ -49,17 +40,18 @@ is ephemeral.
|
|
|
49
40
|
|
|
50
41
|
There are two core features that depend on Redis. First, per-batch acylicity is
|
|
51
42
|
achieved by maintaining the set of processed URLs per batch in Redis.
|
|
52
|
-
There's no option to follow links in a cyclic manner. Second,
|
|
53
|
-
requires updating an integer value in Redis, and batch completion
|
|
54
|
-
useful feature, since most crawls should end eventually, and often you want
|
|
55
|
-
know when.
|
|
43
|
+
There's no configuration option to follow links in a cyclic manner. Second,
|
|
44
|
+
batch completion requires updating an integer value in Redis, and batch completion
|
|
45
|
+
is a very useful feature, since most crawls should end eventually, and often you want
|
|
46
|
+
to know when.
|
|
56
47
|
|
|
57
48
|
### No configuration files
|
|
58
49
|
|
|
59
|
-
Wayfarer can be configured through `Wayfarer.config` only,
|
|
60
|
-
may contain Ruby objects that don't de/serialize well,
|
|
50
|
+
Wayfarer can be configured through `Wayfarer.config` in Ruby code only,
|
|
51
|
+
because `Wayfarer.config` may contain Ruby objects that don't de/serialize well,
|
|
52
|
+
such as `Proc`s or `Set`s.
|
|
61
53
|
|
|
62
|
-
###
|
|
54
|
+
### Other features that are out of scope
|
|
63
55
|
|
|
64
56
|
Wayfarer won't provide:
|
|
65
57
|
|
data/docs/guides/handlers.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Handlers
|
|
2
2
|
|
|
3
|
-
Handlers are like [jobs](
|
|
3
|
+
Handlers are like [jobs](jobs.md) but they don't inherit from `ActiveJob::Base`
|
|
4
4
|
which is why they can't affect the message queue directly themselves.
|
|
5
5
|
Instead, jobs and handlers can route tasks to other handlers. Handlers
|
|
6
6
|
themselves have routes, but they can be bypassed.
|
data/docs/guides/jobs.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Jobs
|
|
2
2
|
|
|
3
3
|
Jobs are [Active Job](https://edgeguides.rubyonrails.org/active_job_basics.html)s
|
|
4
|
-
that use a DSL to process [tasks](
|
|
4
|
+
that use a DSL to process [tasks](tasks.md) that they read from a message
|
|
5
5
|
queue.
|
|
6
6
|
|
|
7
7
|
Instead of implementing Active Job's `#perform` method yourself, you declare
|
|
8
|
-
[routes](
|
|
8
|
+
[routes](routing.md) to instance methods, like web applications route incoming
|
|
9
9
|
requests. Only URLs that match a route are retrieved and processed. All other
|
|
10
10
|
URLs are considered successfully processed. The action has access to the
|
|
11
|
-
retrieved [page](
|
|
11
|
+
retrieved [page](pages.md), the [user agent](user_agents.md) that retrieved the
|
|
12
12
|
page and the current task:
|
|
13
13
|
|
|
14
14
|
```ruby
|
|
@@ -47,7 +47,7 @@ Instead of a generated UUID, you can also set your own batch:
|
|
|
47
47
|
DummyJob.crawl("https://example.com", batch: "my-batch")
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
You can also use Wayfarer's [CLI](
|
|
50
|
+
You can also use Wayfarer's [CLI](cli.md) to enqueue a task:
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
53
|
wayfarer enqueue --batch my-batch DummyJob "https://example.com"
|
|
@@ -74,7 +74,7 @@ end
|
|
|
74
74
|
|
|
75
75
|
## Accessing the current task
|
|
76
76
|
|
|
77
|
-
If the task's URL matched a [route](
|
|
77
|
+
If the task's URL matched a [route](routing.md), the URL is retrieved over the network,
|
|
78
78
|
and the method that was routed to is called. The task is available as `#task`:
|
|
79
79
|
|
|
80
80
|
```ruby
|
|
@@ -92,7 +92,7 @@ end
|
|
|
92
92
|
|
|
93
93
|
## Accessing the current page
|
|
94
94
|
|
|
95
|
-
You have access to the retrieved [page](
|
|
95
|
+
You have access to the retrieved [page](pages.md):
|
|
96
96
|
|
|
97
97
|
```ruby
|
|
98
98
|
class DummyJob < ActiveJob::Base
|
|
@@ -184,7 +184,9 @@ Wayfarer relies on ActiveJob's [error handling methods](https://guides.rubyonrai
|
|
|
184
184
|
* `retry_on` to retry jobs a number of times on certain errors:
|
|
185
185
|
|
|
186
186
|
```ruby
|
|
187
|
-
class DummyJob <
|
|
187
|
+
class DummyJob < ActiveJob::Base
|
|
188
|
+
include Wayfarer::Base
|
|
189
|
+
|
|
188
190
|
retry_on MyError, attempts: 3 do |job, error|
|
|
189
191
|
# This block runs once all 3 attempts have failed
|
|
190
192
|
# (1 initial attempt + 2 retries)
|
|
@@ -195,7 +197,9 @@ Wayfarer relies on ActiveJob's [error handling methods](https://guides.rubyonrai
|
|
|
195
197
|
* `discard_on` to throw away jobs on certain errors:
|
|
196
198
|
|
|
197
199
|
```ruby
|
|
198
|
-
class DummyJob <
|
|
200
|
+
class DummyJob < ActiveJob::Base
|
|
201
|
+
include Wayfarer::Base
|
|
202
|
+
|
|
199
203
|
discard_on MyError do |job, error|
|
|
200
204
|
# This block runs once and buries the job
|
|
201
205
|
end
|
|
@@ -205,7 +209,7 @@ Wayfarer relies on ActiveJob's [error handling methods](https://guides.rubyonrai
|
|
|
205
209
|
## Recreating user agents on certain errors
|
|
206
210
|
|
|
207
211
|
You can configure a list of exception classes upon which user agents
|
|
208
|
-
get recreated (see [User agent API]()):
|
|
212
|
+
get recreated (see [User agent API](user_agents.md)):
|
|
209
213
|
|
|
210
214
|
```ruby
|
|
211
215
|
Wayfarer.config[:network][:renew_on] = [MyIrrecoverableError]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Middleware
|
|
2
|
+
|
|
3
|
+
Wayfarer processes [tasks](tasks.md) through a middleware chain, an ordered
|
|
4
|
+
pipeline of middleware classes. Each middleware receives a task, can modify it,
|
|
5
|
+
and yields to the next middleware in the chain. If a middleware doesn't yield,
|
|
6
|
+
the chain halts.
|
|
7
|
+
|
|
8
|
+
A middleware is a class that implements a `call` method. It receives a `task`
|
|
9
|
+
argument and must `yield` to continue the middleware chain.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class MyCustomMiddleware
|
|
13
|
+
def call(task)
|
|
14
|
+
# Pre-processing: optionally set task metadata
|
|
15
|
+
task[:started_at] = Time.now.utc
|
|
16
|
+
|
|
17
|
+
# Yield to continue the chain (or not)
|
|
18
|
+
# Downstream middleware can access `task[:started_at]`
|
|
19
|
+
yield if block_given?
|
|
20
|
+
|
|
21
|
+
# Post-processing
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
!!! info "Task metadata `#task[]=` is ephemeral"
|
|
27
|
+
|
|
28
|
+
Anything you assign to a task at runtime isn't serialized to the message
|
|
29
|
+
queue. For example in the case of a retry, no task metadata can be restored.
|
|
30
|
+
|
|
31
|
+
A `DependencyGraph` manages the chain ordering by resolving `before:` and
|
|
32
|
+
`after:` constraints between middlewares.
|
|
33
|
+
|
|
34
|
+
## Default middleware chain
|
|
35
|
+
|
|
36
|
+
Jobs that `#!ruby include Wayfarer::Base` get a default chain of 10 middlewares:
|
|
37
|
+
|
|
38
|
+
1. `Redis`: connects to Redis
|
|
39
|
+
2. `UriParser`: parses the task URL
|
|
40
|
+
3. `Normalize`: normalizes the URL
|
|
41
|
+
4. `Dedup`: deduplicates tasks
|
|
42
|
+
5. `BatchCompletion`: tracks batch progress
|
|
43
|
+
6. `Stage`: collects staged URLs
|
|
44
|
+
7. `Router`: matches the URL to an action
|
|
45
|
+
8. `UserAgent`: retrieves the page
|
|
46
|
+
9. `ContentType`: filters by Content-Type
|
|
47
|
+
10. `Dispatch`: calls the routed action
|
|
48
|
+
|
|
49
|
+
[Handlers](handlers.md) that `#!ruby include Wayfarer::Handler` get a shorter chain:
|
|
50
|
+
|
|
51
|
+
1. `ContentType`
|
|
52
|
+
2. `Router`
|
|
53
|
+
3. `Dispatch`
|
|
54
|
+
|
|
55
|
+
`#!ruby Wayfarer.config[:middleware][:base]` and `#!ruby
|
|
56
|
+
Wayfarer.config[:middleware][:handler]` declare these defaults.
|
|
57
|
+
|
|
58
|
+
## Adding middleware
|
|
59
|
+
|
|
60
|
+
Use the `use` class method to add a middleware to a job's chain. You can
|
|
61
|
+
declare ordering constraints with `before:` and `after:`:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
class MyJob < ActiveJob::Base
|
|
65
|
+
include Wayfarer::Base
|
|
66
|
+
|
|
67
|
+
use MyCustomMiddleware, after: [Wayfarer::Middleware::Router]
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
!!! info "Auto-registration"
|
|
72
|
+
|
|
73
|
+
If you reference a middleware in `before:` or `after:` that hasn't been
|
|
74
|
+
explicitly added, it's registered automatically with no constraints.
|
|
75
|
+
|
|
76
|
+
## Removing middleware
|
|
77
|
+
|
|
78
|
+
Use `middleware.remove` to remove a middleware from the chain. All
|
|
79
|
+
`before:` and `after:` references to the removed middleware are automatically
|
|
80
|
+
cleaned up.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class NoDedupJob < ActiveJob::Base
|
|
84
|
+
include Wayfarer::Base
|
|
85
|
+
|
|
86
|
+
self.middleware = middleware.remove(Wayfarer::Middleware::Dedup)
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Replacing middleware
|
|
91
|
+
|
|
92
|
+
Use `middleware.replace` to swap a middleware. The replacement inherits
|
|
93
|
+
the exact same `before:` and `after:` constraints. The method updates all
|
|
94
|
+
references to the old middleware in other entries to point to the replacement.
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class TestJob < ActiveJob::Base
|
|
98
|
+
include Wayfarer::Base
|
|
99
|
+
|
|
100
|
+
self.middleware = middleware.replace(
|
|
101
|
+
Wayfarer::Middleware::Redis,
|
|
102
|
+
MyInMemoryState
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
!!! attention "`replace` inherits constraints"
|
|
108
|
+
|
|
109
|
+
If you need different constraints for the new middleware, use `remove`
|
|
110
|
+
followed by `add` instead.
|
|
111
|
+
|
|
112
|
+
## The dependency graph
|
|
113
|
+
|
|
114
|
+
The dependency graph is immutable. Every operation (`add`, `remove`, `replace`)
|
|
115
|
+
returns a new instance which is why you always assign the result back:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
self.middleware = middleware.add(MyMiddleware, after: [Wayfarer::Middleware::Router])
|
|
119
|
+
self.middleware = middleware.remove(Wayfarer::Middleware::Dedup)
|
|
120
|
+
self.middleware = middleware.replace(OldMiddleware, NewMiddleware)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Circular dependencies raise a `#!ruby Wayfarer::Middleware::DependencyGraph::CyclicDependencyError`.
|
|
124
|
+
|
|
125
|
+
## Modifying the global default
|
|
126
|
+
|
|
127
|
+
You can modify the default middleware chain globally in an initializer. Any job
|
|
128
|
+
class defined after this point uses the modified default:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# config/initializers/wayfarer.rb
|
|
132
|
+
Wayfarer.config[:middleware][:base] = Wayfarer.config[:middleware][:base]
|
|
133
|
+
.add(MyGlobalMiddleware, after: [Wayfarer::Middleware::Router])
|
|
134
|
+
```
|
data/docs/guides/navigation.md
CHANGED
|
@@ -12,7 +12,9 @@ URL does not enqueue it immediately. Instead, the URL is added to a processing
|
|
|
12
12
|
set first.
|
|
13
13
|
|
|
14
14
|
```ruby
|
|
15
|
-
class DummyJob <
|
|
15
|
+
class DummyJob < ActiveJob::Base
|
|
16
|
+
include Wayfarer::Base
|
|
17
|
+
|
|
16
18
|
route.to :index
|
|
17
19
|
|
|
18
20
|
def index
|
|
@@ -35,7 +37,6 @@ def index
|
|
|
35
37
|
end
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
See also: [Performance: Stage less URLs](/guides/performance)
|
|
39
40
|
|
|
40
41
|
!!! attention "Failing action methods do not enqueue tasks"
|
|
41
42
|
|
|
@@ -59,7 +60,9 @@ job's routes do not match the URL. When the task gets consumed, the URL does not
|
|
|
59
60
|
get fetched and the action method not called.
|
|
60
61
|
|
|
61
62
|
```ruby
|
|
62
|
-
class DummyJob <
|
|
63
|
+
class DummyJob < ActiveJob::Base
|
|
64
|
+
include Wayfarer::Base
|
|
65
|
+
|
|
63
66
|
route do
|
|
64
67
|
host "example.com", path: "/users/:user_id", to: :user
|
|
65
68
|
end
|
|
@@ -7,7 +7,9 @@ applications which adds a nice API that also works well for web scraping.
|
|
|
7
7
|
Wayfarer.config[:network][:agent] = :capybara
|
|
8
8
|
# Wayfarer.config[:capybara][:driver] = ...
|
|
9
9
|
|
|
10
|
-
class DummyJob <
|
|
10
|
+
class DummyJob < ActiveJob::Base
|
|
11
|
+
include Wayfarer::Base
|
|
12
|
+
|
|
11
13
|
route.to :index
|
|
12
14
|
|
|
13
15
|
def index
|
|
@@ -15,7 +15,7 @@ as they navigate to a URL
|
|
|
15
15
|
Because spawning browser processes or instantiating HTTP clients is expensive,
|
|
16
16
|
Wayfarer keeps user agents in a pool and reuses them across jobs. This means
|
|
17
17
|
that browser state carries over between jobs, as each job checks out a previous
|
|
18
|
-
job's user agent. When exceptions are raised, you must [handle them](
|
|
18
|
+
job's user agent. When exceptions are raised, you must [handle them](../jobs.md#handling-errors).
|
|
19
19
|
|
|
20
20
|
Only on certain irrecoverable errors are individual user agents
|
|
21
21
|
destroyed and recreated. For example when a browser process crashes, it is
|
|
@@ -62,7 +62,7 @@ exception classes upon which an instance of the user agent should get recreated
|
|
|
62
62
|
### Stateless interface
|
|
63
63
|
|
|
64
64
|
In addition to the base interface, stateless user agents implement `#fetch(instance, url)`
|
|
65
|
-
which fetches [pages](../pages) or indicates redirects:
|
|
65
|
+
which fetches [pages](../pages.md) or indicates redirects:
|
|
66
66
|
|
|
67
67
|
* `#create()` __(required)__
|
|
68
68
|
* `#fetch(instance, url)` __(required)__: Called to retrieve a URL. Responses with a
|
|
@@ -93,7 +93,7 @@ instance callbacks:
|
|
|
93
93
|
* `#create()` __(required)__
|
|
94
94
|
* `#navigate(instance, url)` __(required)__: Navigates the user agent to the given URL.
|
|
95
95
|
* `#live(instance)` __(required)__: Turns the current user agent state
|
|
96
|
-
into a [page](../pages) by calling `success(url:, body:, status_code:, headers:)`.
|
|
96
|
+
into a [page](../pages.md) by calling `success(url:, body:, status_code:, headers:)`.
|
|
97
97
|
* `#destroy(instance)` (optional)
|
|
98
98
|
* `::renew_on` (optional)
|
|
99
99
|
|
|
@@ -6,10 +6,12 @@ also alongside automated browsers.
|
|
|
6
6
|
## Ad-hoc GET requests
|
|
7
7
|
|
|
8
8
|
When automating browsers, it can be useful to additionally retrieve another page
|
|
9
|
-
or even the current page over plain HTTP. Jobs can fetch URLs to [pages](
|
|
9
|
+
or even the current page over plain HTTP. Jobs can fetch URLs to [pages](../pages.md) with `#http`:
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
|
-
class DummyJob <
|
|
12
|
+
class DummyJob < ActiveJob::Base
|
|
13
|
+
include Wayfarer::Base
|
|
14
|
+
|
|
13
15
|
route.to :index
|
|
14
16
|
|
|
15
17
|
def index
|
|
@@ -9,7 +9,9 @@ so:
|
|
|
9
9
|
```ruby
|
|
10
10
|
Wayfarer.config[:network][:agent] = :selenium
|
|
11
11
|
|
|
12
|
-
class
|
|
12
|
+
class DummyJob < ActiveJob::Base
|
|
13
|
+
include Wayfarer::Base
|
|
14
|
+
|
|
13
15
|
route.to :index
|
|
14
16
|
|
|
15
17
|
def index
|
|
@@ -29,7 +31,9 @@ process.
|
|
|
29
31
|
```ruby
|
|
30
32
|
Wayfarer.config[:network][:agent] = :selenium
|
|
31
33
|
|
|
32
|
-
class DummyJob <
|
|
34
|
+
class DummyJob < ActiveJob::Base
|
|
35
|
+
include Wayfarer::Base
|
|
36
|
+
|
|
33
37
|
route.to :index
|
|
34
38
|
|
|
35
39
|
def index
|
|
@@ -39,7 +43,7 @@ process.
|
|
|
39
43
|
end
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
!!! note "Consider using [Ferrum](
|
|
46
|
+
!!! note "Consider using [Ferrum](ferrum.md) instead if Google Chrome suits your needs."
|
|
43
47
|
Use Ferrum if you want to automate Google Chrome. It provides superior
|
|
44
48
|
stability and a richer feature set compared to Selenium drivers.
|
|
45
49
|
|
data/docs/guides/pages.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Pages
|
|
2
2
|
|
|
3
3
|
A page is the immutable state of the contents behind a URL at a point in time,
|
|
4
|
-
retrieved by a [user agent](
|
|
4
|
+
retrieved by a [user agent](user_agents.md). In other words, a page is an HTTP
|
|
5
5
|
response, or the state of a remotely controlled browser.
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
@@ -82,7 +82,9 @@ To get a page that reflects the current browser state, set the `#!ruby :live`
|
|
|
82
82
|
keyword:
|
|
83
83
|
|
|
84
84
|
```ruby
|
|
85
|
-
class DummyJob <
|
|
85
|
+
class DummyJob < ActiveJob::Base
|
|
86
|
+
include Wayfarer::Base
|
|
87
|
+
|
|
86
88
|
route.to :index
|
|
87
89
|
|
|
88
90
|
def index
|
data/docs/guides/routing.md
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
# Routing
|
|
2
2
|
|
|
3
3
|
Wayfarer equips jobs with a declarative routing DSL that maps URLs to actions.
|
|
4
|
-
Actions are instance methods denoted by symbols, or [handlers](
|
|
5
|
-
[Pages](
|
|
4
|
+
Actions are instance methods denoted by symbols, or [handlers](handlers.md).
|
|
5
|
+
[Pages](pages.md) are only retrieved from URLs which map to an action.
|
|
6
6
|
|
|
7
|
-
!!!
|
|
7
|
+
!!! warning "Routed URLs are normalized"
|
|
8
8
|
|
|
9
|
-
By default, Wayfarer [applies some transformations to each URL](
|
|
10
|
-
into a canonical form.
|
|
9
|
+
By default, Wayfarer [applies some transformations to each URL](tasks.md#url-normalization) to bring it
|
|
10
|
+
into a canonical form. __Routing happens based on this canonical form__,
|
|
11
|
+
not on the verbatim `task.url`.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
This means that for example a route declaration doesn't match the URL
|
|
14
|
+
`https://www.example.com`, since the URL's hostname is normalized to `example.com`.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
route.host "www.example.com"
|
|
18
|
+
```
|
|
19
|
+
You may need to pass the `wayfarer route` CLI sub-command the `-r FILE` option
|
|
20
|
+
and point to the file that configures `Wayfarer.config`.
|
|
13
21
|
|
|
14
22
|
A job's route declarations equate to a predicate tree.
|
|
15
23
|
When a URL is routed, the predicate tree is searched depth-first. If a
|
|
16
|
-
matching leaf predicate is found, the
|
|
24
|
+
matching leaf predicate is found, the downmost path's action is dispatched.
|
|
17
25
|
You can extract data from URL path segments and query parameters and
|
|
18
26
|
access it through `params` in jobs or handlers.
|
|
19
27
|
|
|
@@ -75,7 +83,7 @@ flowchart LR
|
|
|
75
83
|
classDef unvisited fill:#BDBDBD,stroke:#BDBDBD,color:#616161
|
|
76
84
|
```
|
|
77
85
|
|
|
78
|
-
??? note "You can also visualise a job's routing tree with with the [`route` CLI subcommand](
|
|
86
|
+
??? note "You can also visualise a job's routing tree with with the [`route` CLI subcommand](cli.md)"
|
|
79
87
|
|
|
80
88
|
```sh
|
|
81
89
|
wayfarer route DummyJob -r dummy_job.rb http://localhost:9000/users/42/gallery
|
data/docs/guides/tasks.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Tasks
|
|
2
2
|
|
|
3
3
|
Tasks are the immutable units of work read from a message queue and processed by
|
|
4
|
-
[jobs](
|
|
4
|
+
[jobs](jobs.md). A task consists of two strings:
|
|
5
5
|
|
|
6
6
|
* The __URL__ to process
|
|
7
7
|
* The __batch__ the task belongs to
|
data/docs/guides/tutorial.md
CHANGED
|
@@ -60,7 +60,79 @@ bundle exec wayfarer perform -r dummy_job.rb DummyJob https://example.com
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
If you don't provide a batch, Wayfarer uses a generated UUID instead.
|
|
63
|
-
We could have also used `DummyJob.crawl
|
|
63
|
+
We could have also used `DummyJob.crawl("https://example.com")` in a Ruby script to enqueue the job programmatically.
|
|
64
64
|
|
|
65
|
+
## Accessing page data
|
|
65
66
|
|
|
67
|
+
The `page` method returns an object representing the HTTP response or browser state.
|
|
68
|
+
Wayfarer automatically parses HTML, XML, and JSON responses.
|
|
66
69
|
|
|
70
|
+
For HTML pages, `page.doc` returns a [Nokogiri](https://nokogiri.org/) document, and `page.meta` returns a [MetaInspector](https://github.com/jaimeiniesta/metainspector) object for easy access to metadata.
|
|
71
|
+
|
|
72
|
+
Let's update `dummy_job.rb` to print the page title:
|
|
73
|
+
|
|
74
|
+
```ruby title="dummy_job.rb"
|
|
75
|
+
require "activejob"
|
|
76
|
+
require "wayfarer"
|
|
77
|
+
|
|
78
|
+
class DummyJob < ActiveJob::Base
|
|
79
|
+
include Wayfarer::Base
|
|
80
|
+
|
|
81
|
+
route.to :index
|
|
82
|
+
|
|
83
|
+
def index
|
|
84
|
+
puts "Title: #{page.doc.title.strip}"
|
|
85
|
+
puts "H1: #{page.doc.at('h1')&.text&.strip}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Running `perform` again will now show the title of the page you crawled.
|
|
91
|
+
|
|
92
|
+
## Crawling and Staging
|
|
93
|
+
|
|
94
|
+
To crawl more pages, we use the `stage` method. This adds new URLs to the current batch.
|
|
95
|
+
Wayfarer handles deduplication, ensuring that we don't get stuck in loops processing the same URL multiple times within the same batch.
|
|
96
|
+
|
|
97
|
+
We can use `page.meta.links.internal` to easily find all links on the current page that point to the same domain.
|
|
98
|
+
|
|
99
|
+
Update `dummy_job.rb` to crawl recursively:
|
|
100
|
+
|
|
101
|
+
```ruby title="dummy_job.rb"
|
|
102
|
+
require "activejob"
|
|
103
|
+
require "wayfarer"
|
|
104
|
+
|
|
105
|
+
class DummyJob < ActiveJob::Base
|
|
106
|
+
include Wayfarer::Base
|
|
107
|
+
|
|
108
|
+
route.to :index
|
|
109
|
+
|
|
110
|
+
def index
|
|
111
|
+
puts "Crawling #{page.url}"
|
|
112
|
+
|
|
113
|
+
# Stage all internal links found on the page
|
|
114
|
+
stage page.meta.links.internal
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Executing the crawl
|
|
120
|
+
|
|
121
|
+
The `wayfarer perform` command we used earlier only executes a single job. To run a full crawl where staged jobs are picked up and processed, we use `wayfarer execute`.
|
|
122
|
+
|
|
123
|
+
This command starts an in-process async executor that will continue running until all jobs in the batch are complete.
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
bundle exec wayfarer execute -r dummy_job.rb DummyJob https://example.com
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
You will see output indicating that multiple pages are being crawled as Wayfarer follows the internal links.
|
|
130
|
+
|
|
131
|
+
## Next steps
|
|
132
|
+
|
|
133
|
+
Now that you have a basic crawler running, you can explore more advanced features:
|
|
134
|
+
|
|
135
|
+
* **[Routing](routing.md)**: Learn how to route different URLs to different methods or handler classes.
|
|
136
|
+
* **[Pages](pages.md)**: Dive deeper into the `Page` object and custom response parsers.
|
|
137
|
+
* **[Networking](user_agents.md)**: Configure Wayfarer to use Headless Chrome (via Ferrum or Selenium) for pages that require JavaScript.
|
|
138
|
+
* **[Configuration](configuration.md)**: Customize concurrency, user agents, and request headers.
|
data/docs/guides/user_agents.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# User agents
|
|
2
2
|
|
|
3
|
-
User agents are used by [jobs](
|
|
4
|
-
[page](
|
|
3
|
+
User agents are used by [jobs](jobs.md) to retrieve the contents behind a URL into a
|
|
4
|
+
[page](pages.md), for example a remotely controlled Firefox process or a Ruby HTTP client.
|
|
5
5
|
|
|
6
6
|
User agents are kept in a connection pool and all user agents in the pool
|
|
7
7
|
share the same type and configuration. You can add your own custom user agents by implementing
|
|
8
|
-
the [user agent API](
|
|
8
|
+
the [user agent API](networking/custom_adapters.md).
|
|
9
9
|
|
|
10
10
|
Wayfarer comes with the following built-in user agents:
|
|
11
11
|
|
|
12
|
-
* [`:http`](http.md) (default)
|
|
13
|
-
* [`:ferrum`](ferrum.md) to automate Google Chrome
|
|
14
|
-
* [`:selenium`](selenium.md) to automate a variety of browsers
|
|
15
|
-
* [`:capybara`](capybara.md) to use Capybara sessions
|
|
12
|
+
* [`:http`](networking/http.md) (default)
|
|
13
|
+
* [`:ferrum`](networking/ferrum.md) to automate Google Chrome
|
|
14
|
+
* [`:selenium`](networking/selenium.md) to automate a variety of browsers
|
|
15
|
+
* [`:capybara`](networking/capybara.md) to use Capybara sessions
|
|
16
16
|
|
|
17
17
|
Configure the user agent with the global configuration option:
|
|
18
18
|
|
|
@@ -35,7 +35,7 @@ class DummyJob < ActiveJob::Base
|
|
|
35
35
|
end
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
You can also implement [custom user agents](
|
|
38
|
+
You can also implement [custom user agents](networking/custom_adapters.md) to support
|
|
39
39
|
your own HTTP client or browser automation service/protocol.
|
|
40
40
|
|
|
41
41
|
### Ad-hoc HTTP requests
|
|
@@ -71,7 +71,7 @@ Wayfarer.config[:network][:http_headers] = { "User-Agent" => "MyCrawler" }
|
|
|
71
71
|
|
|
72
72
|
Since user agents are expensive to create, especially in the case of browser
|
|
73
73
|
processes, Wayfarer keeps user agents within a connection pool. When a job
|
|
74
|
-
performs and needs to retrieve the [page](
|
|
74
|
+
performs and needs to retrieve the [page](pages.md) for its task URL, an agent
|
|
75
75
|
is checked out from the pool, and checked back in when the routed action method
|
|
76
76
|
returns.
|
|
77
77
|
|
|
@@ -94,7 +94,7 @@ Wayfarer.config[:network][:pool][:size] = Sidekiq.options[:concurrency]
|
|
|
94
94
|
The same browser session is used across jobs. This means that the browser
|
|
95
95
|
is not closed between jobs, and that the browser's state carries over from
|
|
96
96
|
job to job. You may account for this by resetting the browser's state
|
|
97
|
-
according to your needs, for which you can use [callbacks](
|
|
97
|
+
according to your needs, for which you can use [callbacks](callbacks.md).
|
|
98
98
|
|
|
99
99
|
### `UserAgentTimeoutError`: avoiding pool contention
|
|
100
100
|
|
data/docs/index.md
CHANGED
|
@@ -9,7 +9,7 @@ hide:
|
|
|
9
9
|
## Ruby web crawling framework built on [ActiveJob]() and [Redis]()
|
|
10
10
|
|
|
11
11
|
<small>
|
|
12
|
-
[Read the tutorial](
|
|
12
|
+
[Read the tutorial](guides/tutorial.md){ .md-button .md-button--primary }
|
|
13
13
|
</small>
|
|
14
14
|
|
|
15
15
|
=== "Command line"
|
data/lib/wayfarer/base.rb
CHANGED
|
@@ -116,16 +116,12 @@ module Wayfarer
|
|
|
116
116
|
# chain included from {Controller}
|
|
117
117
|
alias_method :perform, :call
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
use Wayfarer::Middleware::Router
|
|
126
|
-
use Wayfarer::Middleware::UserAgent
|
|
127
|
-
use Wayfarer::Middleware::ContentType
|
|
128
|
-
use Wayfarer::Middleware::Dispatch
|
|
119
|
+
class_attribute :middleware,
|
|
120
|
+
default: Wayfarer.config[:middleware][:base],
|
|
121
|
+
instance_accessor: false,
|
|
122
|
+
instance_predicate: false
|
|
123
|
+
|
|
124
|
+
include middleware.api
|
|
129
125
|
end
|
|
130
126
|
|
|
131
127
|
class_methods do
|