google-browser-scraper 0.0.3__tar.gz → 0.0.4__tar.gz
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.
- {google_browser_scraper-0.0.3/src/google_browser_scraper.egg-info → google_browser_scraper-0.0.4}/PKG-INFO +42 -23
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/README.md +41 -22
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/pyproject.toml +1 -1
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/__init__.py +1 -1
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/browser.py +48 -4
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/cli.py +27 -4
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/links.py +3 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/pool.py +35 -10
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/relay.py +23 -10
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/scraper.py +309 -65
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/warmup.py +1 -2
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4/src/google_browser_scraper.egg-info}/PKG-INFO +42 -23
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/fakegoogle.py +11 -0
- google_browser_scraper-0.0.4/tests/test_browser_local.py +149 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/test_engines_local.py +1 -0
- google_browser_scraper-0.0.4/tests/test_pool.py +245 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/test_relay.py +23 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/test_scraper.py +195 -8
- google_browser_scraper-0.0.3/tests/test_browser_local.py +0 -76
- google_browser_scraper-0.0.3/tests/test_pool.py +0 -105
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/LICENSE +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/MANIFEST.in +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/setup.cfg +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/__main__.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/classify.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/doctor.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/engines.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/mcp.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/output.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/parse.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/proxy.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/server.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper.egg-info/SOURCES.txt +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper.egg-info/dependency_links.txt +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper.egg-info/entry_points.txt +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper.egg-info/requires.txt +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper.egg-info/top_level.txt +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/pages.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/stdio_mcp_child.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/test_classify.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/test_cli_and_output.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/test_links.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/test_parse.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/test_proxy.py +0 -0
- {google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/tests/test_serve_and_mcp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-browser-scraper
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Google Search results from a real browser through your own sticky proxy, with captchas and blocks reported explicitly.
|
|
5
5
|
Author: NodeMaven
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,10 @@ Requires-Dist: pytest>=8; extra == "dev"
|
|
|
29
29
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
30
30
|
Dynamic: license-file
|
|
31
31
|
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="https://raw.githubusercontent.com/nodemaven/google-browser-scraper/main/assets/banner.png" alt="Google Browser Scraper">
|
|
34
|
+
</p>
|
|
35
|
+
|
|
32
36
|
# google-browser-scraper
|
|
33
37
|
|
|
34
38
|
Google Search results from a real browser, through your own sticky proxy.
|
|
@@ -36,9 +40,15 @@ Google Search results from a real browser, through your own sticky proxy.
|
|
|
36
40
|
Plain HTTP is no longer a reliable way to scrape Google: it often expects a
|
|
37
41
|
browser that runs JavaScript, and a fresh browser on a fresh proxy exit can be
|
|
38
42
|
refused quickly. This tool takes a browser path instead. It warms a sticky proxy
|
|
39
|
-
session with ordinary browsing,
|
|
40
|
-
whether Google served results or refused, and writes one
|
|
41
|
-
page. Captchas, blocks and failures are reported as such, never as "no results".
|
|
43
|
+
session with ordinary browsing, opens Google's results page for each query in
|
|
44
|
+
that browser, checks whether Google served results or refused, and writes one
|
|
45
|
+
JSON record per results page. Captchas, blocks and failures are reported as such, never as "no results".
|
|
46
|
+
|
|
47
|
+
**Run it on Windows** - a desktop or a Windows VPS. That is where it is tested
|
|
48
|
+
and where Google serves it. On a Linux server without a GPU, Google refused
|
|
49
|
+
almost every search in testing; Chrome has no WebGL there, which is the main
|
|
50
|
+
suspect, so Linux and Docker are experimental for now. `google-browser-scraper
|
|
51
|
+
doctor` shows what your machine looks like to a website.
|
|
42
52
|
|
|
43
53
|
## Install
|
|
44
54
|
|
|
@@ -74,8 +84,8 @@ Success is a `results.jsonl` with one JSON object per results page:
|
|
|
74
84
|
|
|
75
85
|
The first query takes a few minutes, because a new exit is warmed up first, and
|
|
76
86
|
the warm-up costs noticeably more traffic than a search. Later queries on the
|
|
77
|
-
same exit take seconds. A summary of pages served and traffic used
|
|
78
|
-
the end of every run.
|
|
87
|
+
same exit take seconds. A summary of pages served and traffic used, with the
|
|
88
|
+
hosts that took most of it, is printed at the end of every run.
|
|
79
89
|
|
|
80
90
|
With NodeMaven, the session id is handled for you and the credentials come from
|
|
81
91
|
the environment:
|
|
@@ -116,7 +126,8 @@ shopping, images and other special blocks are not parsed.
|
|
|
116
126
|
|
|
117
127
|
Each record also has an `exit` block: which proxy session answered and how many
|
|
118
128
|
bytes the page cost. `search_metadata.timings` says where the page's time went,
|
|
119
|
-
in seconds per step (`type`, `load`, `ready`, `read`, `parse`, `links
|
|
129
|
+
in seconds per step (`type`, `load`, `ready`, `read`, `parse`, and `links`, which
|
|
130
|
+
overlaps the next query and is not part of `total_time_taken`). A query
|
|
120
131
|
that failed is a record too, with an `error`.
|
|
121
132
|
`--format csv` writes the organic results as a table, and `--price-per-gb` adds
|
|
122
133
|
the cost per 1000 pages to the summary.
|
|
@@ -124,8 +135,12 @@ the cost per 1000 pages to the summary.
|
|
|
124
135
|
## How it works
|
|
125
136
|
|
|
126
137
|
1. **New identity**: a sticky proxy session and a fresh browser profile.
|
|
127
|
-
2. **Warm-up**: a few ordinary pages
|
|
128
|
-
3. **Search**:
|
|
138
|
+
2. **Warm-up**: a few ordinary pages.
|
|
139
|
+
3. **Search**: each query opens Google's results page directly, which is
|
|
140
|
+
faster than the search box and skips the autocomplete traffic of typing.
|
|
141
|
+
`--direct follow-ups` sends an exit's first query through the box and
|
|
142
|
+
`--direct none` every query; `--input` sets how a query goes into the box
|
|
143
|
+
(`type` key by key, `insert` all at once, `words` word by word).
|
|
129
144
|
4. **Check before parsing**: the page is classified first, so a captcha or a
|
|
130
145
|
block is reported as one and never parsed as an empty result.
|
|
131
146
|
5. **Keep or drop**: an exit Google answers is reused for the next queries; an
|
|
@@ -134,20 +149,22 @@ the cost per 1000 pages to the summary.
|
|
|
134
149
|
|
|
135
150
|
A served exit is kept for up to 50 queries (`--max-per-exit`). Every new exit
|
|
136
151
|
pays for a warm-up, so a short cap mostly buys more warm-up traffic. A browser
|
|
137
|
-
error on a search is retried once
|
|
138
|
-
|
|
152
|
+
error on a search is retried once on the same exit (through Google's front page
|
|
153
|
+
when queries go through the search box) before the exit is dropped; a network
|
|
154
|
+
error drops it at once.
|
|
139
155
|
|
|
140
156
|
`--workers N` searches on N exits at once, one browser each, sharing one list
|
|
141
|
-
of queries. Each browser needs memory and CPU of its own
|
|
142
|
-
refused
|
|
157
|
+
of queries. Each browser needs memory and CPU of its own. The run stops when
|
|
158
|
+
exit after exit is refused or unreachable with no worker served in between.
|
|
143
159
|
|
|
144
160
|
The command line copies the cookies of the first exit Google serves and
|
|
145
161
|
starts up to 10 new exits from that copy (`--clone N`, `--clone 0` turns it
|
|
146
|
-
off), on new sticky sessions, instead of warming each one
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
off), on new sticky sessions, instead of warming each one; all workers share
|
|
163
|
+
the copy. Google's acceptance follows the cookies more than the address, so a
|
|
164
|
+
copy usually starts out served, and costs a few MB instead of a full warm-up.
|
|
165
|
+
A copy refused on its first query ends the copying; the copied cookies are
|
|
166
|
+
used for three hours at most (`--clone-max-age`). A copy waits 40-60 seconds
|
|
167
|
+
after its browser starts before the first search.
|
|
151
168
|
|
|
152
169
|
`--spare 1` warms the next exit in the background once the current one is
|
|
153
170
|
served, so a retired exit is replaced without waiting for a warm-up. A spare
|
|
@@ -155,8 +172,9 @@ costs a warm-up's traffic whether it is used or not.
|
|
|
155
172
|
|
|
156
173
|
Google wraps result links in encrypted `/goto` redirects. Each one is resolved
|
|
157
174
|
with a small request through a separate short-lived proxy session, a few at a
|
|
158
|
-
time, so link resolution does not touch the warmed browser session.
|
|
159
|
-
|
|
175
|
+
time, so link resolution does not touch the warmed browser session. It runs in
|
|
176
|
+
the background while the browser goes on to the next query; records still come
|
|
177
|
+
out in the order each browser searched them. `--no-resolve-links` skips it.
|
|
160
178
|
|
|
161
179
|
Traffic goes through a small local relay that counts bytes and blocks a large
|
|
162
180
|
model download Chrome otherwise makes on every fresh profile.
|
|
@@ -226,9 +244,10 @@ Google refuses, and tells you what each page cost.
|
|
|
226
244
|
|
|
227
245
|
## Status
|
|
228
246
|
|
|
229
|
-
Early.
|
|
230
|
-
without a GPU, Chrome has no WebGL, which a normal
|
|
231
|
-
Docker image turns on a software renderer for it
|
|
247
|
+
Early. Windows is the tested and recommended platform. **Linux and Docker are
|
|
248
|
+
experimental**: on a machine without a GPU, Chrome has no WebGL, which a normal
|
|
249
|
+
desktop always has; the Docker image turns on a software renderer for it, and
|
|
250
|
+
whether Google serves that is not measured yet. The `cloak` engine
|
|
232
251
|
(`--engine cloak`) is experimental too; `patchright` is the default.
|
|
233
252
|
|
|
234
253
|
Google changes its pages often. If results come back empty or wrong, run with
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/nodemaven/google-browser-scraper/main/assets/banner.png" alt="Google Browser Scraper">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# google-browser-scraper
|
|
2
6
|
|
|
3
7
|
Google Search results from a real browser, through your own sticky proxy.
|
|
@@ -5,9 +9,15 @@ Google Search results from a real browser, through your own sticky proxy.
|
|
|
5
9
|
Plain HTTP is no longer a reliable way to scrape Google: it often expects a
|
|
6
10
|
browser that runs JavaScript, and a fresh browser on a fresh proxy exit can be
|
|
7
11
|
refused quickly. This tool takes a browser path instead. It warms a sticky proxy
|
|
8
|
-
session with ordinary browsing,
|
|
9
|
-
whether Google served results or refused, and writes one
|
|
10
|
-
page. Captchas, blocks and failures are reported as such, never as "no results".
|
|
12
|
+
session with ordinary browsing, opens Google's results page for each query in
|
|
13
|
+
that browser, checks whether Google served results or refused, and writes one
|
|
14
|
+
JSON record per results page. Captchas, blocks and failures are reported as such, never as "no results".
|
|
15
|
+
|
|
16
|
+
**Run it on Windows** - a desktop or a Windows VPS. That is where it is tested
|
|
17
|
+
and where Google serves it. On a Linux server without a GPU, Google refused
|
|
18
|
+
almost every search in testing; Chrome has no WebGL there, which is the main
|
|
19
|
+
suspect, so Linux and Docker are experimental for now. `google-browser-scraper
|
|
20
|
+
doctor` shows what your machine looks like to a website.
|
|
11
21
|
|
|
12
22
|
## Install
|
|
13
23
|
|
|
@@ -43,8 +53,8 @@ Success is a `results.jsonl` with one JSON object per results page:
|
|
|
43
53
|
|
|
44
54
|
The first query takes a few minutes, because a new exit is warmed up first, and
|
|
45
55
|
the warm-up costs noticeably more traffic than a search. Later queries on the
|
|
46
|
-
same exit take seconds. A summary of pages served and traffic used
|
|
47
|
-
the end of every run.
|
|
56
|
+
same exit take seconds. A summary of pages served and traffic used, with the
|
|
57
|
+
hosts that took most of it, is printed at the end of every run.
|
|
48
58
|
|
|
49
59
|
With NodeMaven, the session id is handled for you and the credentials come from
|
|
50
60
|
the environment:
|
|
@@ -85,7 +95,8 @@ shopping, images and other special blocks are not parsed.
|
|
|
85
95
|
|
|
86
96
|
Each record also has an `exit` block: which proxy session answered and how many
|
|
87
97
|
bytes the page cost. `search_metadata.timings` says where the page's time went,
|
|
88
|
-
in seconds per step (`type`, `load`, `ready`, `read`, `parse`, `links
|
|
98
|
+
in seconds per step (`type`, `load`, `ready`, `read`, `parse`, and `links`, which
|
|
99
|
+
overlaps the next query and is not part of `total_time_taken`). A query
|
|
89
100
|
that failed is a record too, with an `error`.
|
|
90
101
|
`--format csv` writes the organic results as a table, and `--price-per-gb` adds
|
|
91
102
|
the cost per 1000 pages to the summary.
|
|
@@ -93,8 +104,12 @@ the cost per 1000 pages to the summary.
|
|
|
93
104
|
## How it works
|
|
94
105
|
|
|
95
106
|
1. **New identity**: a sticky proxy session and a fresh browser profile.
|
|
96
|
-
2. **Warm-up**: a few ordinary pages
|
|
97
|
-
3. **Search**:
|
|
107
|
+
2. **Warm-up**: a few ordinary pages.
|
|
108
|
+
3. **Search**: each query opens Google's results page directly, which is
|
|
109
|
+
faster than the search box and skips the autocomplete traffic of typing.
|
|
110
|
+
`--direct follow-ups` sends an exit's first query through the box and
|
|
111
|
+
`--direct none` every query; `--input` sets how a query goes into the box
|
|
112
|
+
(`type` key by key, `insert` all at once, `words` word by word).
|
|
98
113
|
4. **Check before parsing**: the page is classified first, so a captcha or a
|
|
99
114
|
block is reported as one and never parsed as an empty result.
|
|
100
115
|
5. **Keep or drop**: an exit Google answers is reused for the next queries; an
|
|
@@ -103,20 +118,22 @@ the cost per 1000 pages to the summary.
|
|
|
103
118
|
|
|
104
119
|
A served exit is kept for up to 50 queries (`--max-per-exit`). Every new exit
|
|
105
120
|
pays for a warm-up, so a short cap mostly buys more warm-up traffic. A browser
|
|
106
|
-
error on a search is retried once
|
|
107
|
-
|
|
121
|
+
error on a search is retried once on the same exit (through Google's front page
|
|
122
|
+
when queries go through the search box) before the exit is dropped; a network
|
|
123
|
+
error drops it at once.
|
|
108
124
|
|
|
109
125
|
`--workers N` searches on N exits at once, one browser each, sharing one list
|
|
110
|
-
of queries. Each browser needs memory and CPU of its own
|
|
111
|
-
refused
|
|
126
|
+
of queries. Each browser needs memory and CPU of its own. The run stops when
|
|
127
|
+
exit after exit is refused or unreachable with no worker served in between.
|
|
112
128
|
|
|
113
129
|
The command line copies the cookies of the first exit Google serves and
|
|
114
130
|
starts up to 10 new exits from that copy (`--clone N`, `--clone 0` turns it
|
|
115
|
-
off), on new sticky sessions, instead of warming each one
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
off), on new sticky sessions, instead of warming each one; all workers share
|
|
132
|
+
the copy. Google's acceptance follows the cookies more than the address, so a
|
|
133
|
+
copy usually starts out served, and costs a few MB instead of a full warm-up.
|
|
134
|
+
A copy refused on its first query ends the copying; the copied cookies are
|
|
135
|
+
used for three hours at most (`--clone-max-age`). A copy waits 40-60 seconds
|
|
136
|
+
after its browser starts before the first search.
|
|
120
137
|
|
|
121
138
|
`--spare 1` warms the next exit in the background once the current one is
|
|
122
139
|
served, so a retired exit is replaced without waiting for a warm-up. A spare
|
|
@@ -124,8 +141,9 @@ costs a warm-up's traffic whether it is used or not.
|
|
|
124
141
|
|
|
125
142
|
Google wraps result links in encrypted `/goto` redirects. Each one is resolved
|
|
126
143
|
with a small request through a separate short-lived proxy session, a few at a
|
|
127
|
-
time, so link resolution does not touch the warmed browser session.
|
|
128
|
-
|
|
144
|
+
time, so link resolution does not touch the warmed browser session. It runs in
|
|
145
|
+
the background while the browser goes on to the next query; records still come
|
|
146
|
+
out in the order each browser searched them. `--no-resolve-links` skips it.
|
|
129
147
|
|
|
130
148
|
Traffic goes through a small local relay that counts bytes and blocks a large
|
|
131
149
|
model download Chrome otherwise makes on every fresh profile.
|
|
@@ -195,9 +213,10 @@ Google refuses, and tells you what each page cost.
|
|
|
195
213
|
|
|
196
214
|
## Status
|
|
197
215
|
|
|
198
|
-
Early.
|
|
199
|
-
without a GPU, Chrome has no WebGL, which a normal
|
|
200
|
-
Docker image turns on a software renderer for it
|
|
216
|
+
Early. Windows is the tested and recommended platform. **Linux and Docker are
|
|
217
|
+
experimental**: on a machine without a GPU, Chrome has no WebGL, which a normal
|
|
218
|
+
desktop always has; the Docker image turns on a software renderer for it, and
|
|
219
|
+
whether Google serves that is not measured yet. The `cloak` engine
|
|
201
220
|
(`--engine cloak`) is experimental too; `patchright` is the default.
|
|
202
221
|
|
|
203
222
|
Google changes its pages often. If results come back empty or wrong, run with
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "google-browser-scraper"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.4"
|
|
8
8
|
description = "Google Search results from a real browser through your own sticky proxy, with captchas and blocks reported explicitly."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
{google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/__init__.py
RENAMED
|
@@ -7,7 +7,7 @@ for record in Scraper(proxy, Settings(pages=2)).run(["best running shoes"]):
|
|
|
7
7
|
print(record["organic_results"])
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.0.
|
|
10
|
+
__version__ = "0.0.4"
|
|
11
11
|
|
|
12
12
|
from .classify import Verdict, classify # noqa: E402
|
|
13
13
|
from .parse import parse_serp # noqa: E402
|
{google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/browser.py
RENAMED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
- `locale` is never set: setting it through the context makes the main thread
|
|
7
7
|
and Web Workers disagree. `timezone_id` goes through CDP emulation and agrees
|
|
8
8
|
in workers, and is off unless asked for.
|
|
9
|
-
- The query is typed into the box
|
|
9
|
+
- The first query on an exit is typed into the box. Later ones on the same
|
|
10
|
+
exit can go straight to `/search?q=` (`direct=True`): once Google has served
|
|
11
|
+
the exit, it serves those as readily, faster and without the autocomplete
|
|
12
|
+
traffic each keystroke fires.
|
|
10
13
|
"""
|
|
11
14
|
|
|
12
15
|
from __future__ import annotations
|
|
@@ -16,8 +19,12 @@ import shutil
|
|
|
16
19
|
import tempfile
|
|
17
20
|
import time
|
|
18
21
|
from dataclasses import dataclass, field, replace
|
|
22
|
+
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
19
23
|
|
|
20
24
|
HOME_URL = "https://www.google.com/?hl=en"
|
|
25
|
+
#: How a query gets into the search box: key by key, all at once, or one word
|
|
26
|
+
#: at a time.
|
|
27
|
+
INPUT_MODES = ("type", "insert", "words")
|
|
21
28
|
SEARCH_BOX = "textarea[name='q'], input[name='q']"
|
|
22
29
|
READY = "#rso, #search"
|
|
23
30
|
# Reject first: it sets the smaller cookie. The middle selector is the
|
|
@@ -69,7 +76,11 @@ class BrowserSession:
|
|
|
69
76
|
browser_args: list[str] | tuple[str, ...] = (),
|
|
70
77
|
rng: random.Random | None = None,
|
|
71
78
|
profile_dir: str | None = None,
|
|
79
|
+
input_mode: str = "type",
|
|
72
80
|
) -> None:
|
|
81
|
+
if input_mode not in INPUT_MODES:
|
|
82
|
+
raise ValueError(f"input mode must be one of {', '.join(INPUT_MODES)}")
|
|
83
|
+
self._input = input_mode
|
|
73
84
|
if any(a.startswith(("--disable-features", "--enable-features")) for a in browser_args):
|
|
74
85
|
raise ValueError(
|
|
75
86
|
"--disable-features/--enable-features would replace the browser's "
|
|
@@ -163,8 +174,11 @@ class BrowserSession:
|
|
|
163
174
|
|
|
164
175
|
# -- searching -----------------------------------------------------------
|
|
165
176
|
|
|
166
|
-
def search(self, query: str) -> Fetched:
|
|
167
|
-
"""Type `query` into Google's box and return the page it led to
|
|
177
|
+
def search(self, query: str, direct: bool = False) -> Fetched:
|
|
178
|
+
"""Type `query` into Google's box and return the page it led to; with
|
|
179
|
+
`direct`, open the results page for it instead."""
|
|
180
|
+
if direct:
|
|
181
|
+
return self._search_direct(query)
|
|
168
182
|
page = self.page
|
|
169
183
|
timings: dict[str, float] = {}
|
|
170
184
|
mark = time.monotonic()
|
|
@@ -200,6 +214,22 @@ class BrowserSession:
|
|
|
200
214
|
fetched = self._snapshot(nav.value, dismissed)
|
|
201
215
|
return replace(fetched, timings={**timings, **fetched.timings})
|
|
202
216
|
|
|
217
|
+
def _search_direct(self, query: str) -> Fetched:
|
|
218
|
+
home = urlsplit(HOME_URL)
|
|
219
|
+
url = urlunsplit(
|
|
220
|
+
(home.scheme, home.netloc, "/search", urlencode({"q": query, "hl": "en"}), "")
|
|
221
|
+
)
|
|
222
|
+
mark = time.monotonic()
|
|
223
|
+
response = self.page.goto(url, wait_until="domcontentloaded", timeout=NAV_TIMEOUT_MS)
|
|
224
|
+
# A fresh exit can be shown the consent wall instead of results; clear
|
|
225
|
+
# it and open the results again, as the typed path does on the front page.
|
|
226
|
+
dismissed = self._dismiss_consent()
|
|
227
|
+
if dismissed:
|
|
228
|
+
response = self.page.goto(url, wait_until="domcontentloaded", timeout=NAV_TIMEOUT_MS)
|
|
229
|
+
load = round(time.monotonic() - mark, 2)
|
|
230
|
+
fetched = self._snapshot(response, dismissed)
|
|
231
|
+
return replace(fetched, timings={"load": load, **fetched.timings})
|
|
232
|
+
|
|
203
233
|
def _box_visible(self) -> bool:
|
|
204
234
|
try:
|
|
205
235
|
return any(h.is_visible() for h in self.page.query_selector_all(SEARCH_BOX))
|
|
@@ -220,9 +250,23 @@ class BrowserSession:
|
|
|
220
250
|
# so the typing replaces it.
|
|
221
251
|
if box.input_value():
|
|
222
252
|
box.press("ControlOrMeta+a")
|
|
223
|
-
|
|
253
|
+
if self._input == "insert":
|
|
254
|
+
page.keyboard.insert_text(query)
|
|
255
|
+
elif self._input == "words":
|
|
256
|
+
words = query.split(" ")
|
|
257
|
+
for i, word in enumerate(words):
|
|
258
|
+
last = i == len(words) - 1
|
|
259
|
+
page.keyboard.insert_text(word if last else word + " ")
|
|
260
|
+
if not last:
|
|
261
|
+
time.sleep(self._rng.uniform(0.15, 0.5))
|
|
262
|
+
else:
|
|
263
|
+
box.type(query, delay=self._rng.randint(45, 140))
|
|
224
264
|
return box
|
|
225
265
|
|
|
266
|
+
def blank(self) -> None:
|
|
267
|
+
"""Leave the current page, stopping whatever it is still loading."""
|
|
268
|
+
self.page.goto("about:blank")
|
|
269
|
+
|
|
226
270
|
def next_page(self) -> Fetched | None:
|
|
227
271
|
"""Click "Next" on the current results page, if there is one."""
|
|
228
272
|
link = self.page.query_selector("a#pnnext")
|
{google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/cli.py
RENAMED
|
@@ -9,13 +9,13 @@ import shlex
|
|
|
9
9
|
import sys
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
-
from . import __version__, engines, warmup
|
|
12
|
+
from . import __version__, browser, engines, warmup
|
|
13
13
|
from .classify import classify
|
|
14
14
|
from .output import WRITERS, open_writer, use_utf8
|
|
15
15
|
from .parse import parse_serp
|
|
16
16
|
from .pool import Pool
|
|
17
17
|
from .proxy import NodeMavenSource, ProxyTemplate
|
|
18
|
-
from .scraper import ExitsRefused, Scraper, Settings
|
|
18
|
+
from .scraper import DIRECT_MODES, ExitsRefused, Scraper, Settings
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -138,9 +138,29 @@ def _scraper_options(p: argparse.ArgumentParser) -> None:
|
|
|
138
138
|
p.add_argument(
|
|
139
139
|
"--clone-max-age",
|
|
140
140
|
type=float,
|
|
141
|
-
default=
|
|
141
|
+
default=180.0,
|
|
142
142
|
metavar="MIN",
|
|
143
|
-
help="minutes copied cookies stay in use (default
|
|
143
|
+
help="minutes copied cookies stay in use (default 180)",
|
|
144
|
+
)
|
|
145
|
+
p.add_argument(
|
|
146
|
+
"--input",
|
|
147
|
+
choices=browser.INPUT_MODES,
|
|
148
|
+
default="type",
|
|
149
|
+
help="how a query is entered into Google's search box: typed key by key "
|
|
150
|
+
"(default), inserted at once, or inserted word by word",
|
|
151
|
+
)
|
|
152
|
+
p.add_argument(
|
|
153
|
+
"--direct",
|
|
154
|
+
choices=DIRECT_MODES,
|
|
155
|
+
default="all",
|
|
156
|
+
help="which queries open the results page directly instead of going through "
|
|
157
|
+
"the search box: all (default), all but an exit's first, or none",
|
|
158
|
+
)
|
|
159
|
+
p.add_argument(
|
|
160
|
+
"--stop-after-read",
|
|
161
|
+
action="store_true",
|
|
162
|
+
help="leave each results page for about:blank once it is read (experimental: "
|
|
163
|
+
"saves traffic Google's late scripts would spend)",
|
|
144
164
|
)
|
|
145
165
|
p.add_argument(
|
|
146
166
|
"--gap",
|
|
@@ -214,6 +234,9 @@ def _build(args, *, pages: int = 1, worker: int | None = None) -> tuple[Scraper,
|
|
|
214
234
|
spare=args.spare,
|
|
215
235
|
clones=args.clone,
|
|
216
236
|
clone_max_age=args.clone_max_age * 60,
|
|
237
|
+
input=args.input,
|
|
238
|
+
direct=args.direct,
|
|
239
|
+
stop_after_read=args.stop_after_read,
|
|
217
240
|
gap_seconds=args.gap,
|
|
218
241
|
headless=args.headless,
|
|
219
242
|
channel=args.channel,
|
{google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/links.py
RENAMED
|
@@ -93,6 +93,9 @@ class LinkResolver:
|
|
|
93
93
|
def bytes(self) -> int:
|
|
94
94
|
return self._relay.total if self._relay is not None else 0
|
|
95
95
|
|
|
96
|
+
def hosts(self) -> dict[str, int]:
|
|
97
|
+
return self._relay.hosts() if self._relay is not None else {}
|
|
98
|
+
|
|
96
99
|
def resolve(self, goto_url: str) -> str | None:
|
|
97
100
|
if not self.enabled:
|
|
98
101
|
return None
|
{google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/pool.py
RENAMED
|
@@ -5,15 +5,21 @@ page and the gap between queries, not the machine. Running `workers` scrapers
|
|
|
5
5
|
side by side multiplies throughput by roughly that number until the machine
|
|
6
6
|
runs out of CPU or memory for the browsers.
|
|
7
7
|
|
|
8
|
-
Each worker is an ordinary `Scraper` with its own identities, spares
|
|
9
|
-
|
|
10
|
-
sync API is bound to the thread that starts
|
|
11
|
-
browsers on its own thread, so
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
nobody
|
|
8
|
+
Each worker is an ordinary `Scraper` with its own identities, spares and
|
|
9
|
+
traffic count. The workers share the queue of queries, one breaker and one
|
|
10
|
+
cookie-jar template. Playwright's sync API is bound to the thread that starts
|
|
11
|
+
it, and every worker starts its browsers on its own thread, so no browser is
|
|
12
|
+
shared across threads.
|
|
13
|
+
|
|
14
|
+
The breaker counts refused exits across the pool and any served page resets
|
|
15
|
+
it: one worker drawing bad exits while another keeps being served is not a bad
|
|
16
|
+
hour. When nobody is served for `breaker` exits in a row the whole pool stops,
|
|
17
|
+
since the other workers would only spend traffic finding the same thing.
|
|
18
|
+
Queries nobody answered by then are listed in `not_run`.
|
|
19
|
+
|
|
20
|
+
The template is shared for the same reason: a served cookie jar is worth as
|
|
21
|
+
much to every worker, and a worker whose copy ran out should start from the
|
|
22
|
+
other worker's jar rather than warm fresh exits.
|
|
17
23
|
"""
|
|
18
24
|
|
|
19
25
|
from __future__ import annotations
|
|
@@ -24,7 +30,7 @@ import time
|
|
|
24
30
|
from collections.abc import Callable, Iterable, Iterator
|
|
25
31
|
from typing import Any
|
|
26
32
|
|
|
27
|
-
from .scraper import ExitsRefused, Scraper, Stats
|
|
33
|
+
from .scraper import Breaker, ExitsRefused, Scraper, Stats, Templates
|
|
28
34
|
|
|
29
35
|
MAX_WORKERS = 16
|
|
30
36
|
_DONE = object()
|
|
@@ -75,6 +81,22 @@ class Pool:
|
|
|
75
81
|
out: queue.Queue[Any] = queue.Queue()
|
|
76
82
|
stopped: list[ExitsRefused] = []
|
|
77
83
|
self._scrapers = [self._make(i) for i in range(self.workers)]
|
|
84
|
+
first = self._scrapers[0].settings
|
|
85
|
+
for scraper in self._scrapers[1:]:
|
|
86
|
+
s = scraper.settings
|
|
87
|
+
if (s.clones, s.clone_max_age, s.breaker) != (
|
|
88
|
+
first.clones,
|
|
89
|
+
first.clone_max_age,
|
|
90
|
+
first.breaker,
|
|
91
|
+
):
|
|
92
|
+
raise ValueError(
|
|
93
|
+
"workers share one cookie template and one breaker, so clones, "
|
|
94
|
+
"clone_max_age and breaker must be the same for every worker"
|
|
95
|
+
)
|
|
96
|
+
templates = Templates(first.clones, first.clone_max_age)
|
|
97
|
+
breaker = Breaker(first.breaker)
|
|
98
|
+
for scraper in self._scrapers:
|
|
99
|
+
scraper.share(breaker, templates)
|
|
78
100
|
threads = [
|
|
79
101
|
threading.Thread(
|
|
80
102
|
target=self._work,
|
|
@@ -100,6 +122,7 @@ class Pool:
|
|
|
100
122
|
self._stop.set()
|
|
101
123
|
for t in threads:
|
|
102
124
|
t.join()
|
|
125
|
+
templates.drop("run finished", lambda event: None)
|
|
103
126
|
while not todo.empty():
|
|
104
127
|
self.not_run.append(todo.get_nowait())
|
|
105
128
|
if stopped:
|
|
@@ -158,5 +181,7 @@ def merge_stats(parts: Iterable[Stats]) -> Stats:
|
|
|
158
181
|
):
|
|
159
182
|
setattr(merged, name, getattr(merged, name) + getattr(s, name))
|
|
160
183
|
merged.counted = merged.counted and s.counted
|
|
184
|
+
for host, n in s.bytes_by_host.items():
|
|
185
|
+
merged.bytes_by_host[host] = merged.bytes_by_host.get(host, 0) + n
|
|
161
186
|
merged._open.extend(s._open)
|
|
162
187
|
return merged
|
{google_browser_scraper-0.0.3 → google_browser_scraper-0.0.4}/src/google_browser_scraper/relay.py
RENAMED
|
@@ -60,6 +60,7 @@ class Relay:
|
|
|
60
60
|
self.tunnels = 0
|
|
61
61
|
self.failures = 0
|
|
62
62
|
self.blocked = 0
|
|
63
|
+
self._hosts: dict[str, int] = {}
|
|
63
64
|
#: Status line of the last CONNECT the upstream proxy refused, such as
|
|
64
65
|
#: `HTTP/1.1 402 traffic exhausted`. The browser only reports a failed
|
|
65
66
|
#: tunnel; this is the proxy's reason.
|
|
@@ -100,6 +101,15 @@ class Relay:
|
|
|
100
101
|
with self._lock:
|
|
101
102
|
return self.bytes_up + self.bytes_down
|
|
102
103
|
|
|
104
|
+
def hosts(self) -> dict[str, int]:
|
|
105
|
+
"""Bytes both ways per target host, the CONNECT exchange included.
|
|
106
|
+
|
|
107
|
+
A plain-HTTP connection the browser keeps alive for requests to other
|
|
108
|
+
hosts is counted under its first host; that traffic is a few small
|
|
109
|
+
revocation checks and redirects."""
|
|
110
|
+
with self._lock:
|
|
111
|
+
return dict(self._hosts)
|
|
112
|
+
|
|
103
113
|
# -- serving ---------------------------------------------------------------
|
|
104
114
|
|
|
105
115
|
def _accept_loop(self) -> None:
|
|
@@ -162,9 +172,9 @@ class Relay:
|
|
|
162
172
|
f"{self._auth}Proxy-Connection: Keep-Alive\r\n\r\n"
|
|
163
173
|
).encode()
|
|
164
174
|
upstream.sendall(request)
|
|
165
|
-
self._count(up=len(request))
|
|
175
|
+
self._count(up=len(request), host=host)
|
|
166
176
|
reply = _read_head(upstream)
|
|
167
|
-
self._count(down=len(reply))
|
|
177
|
+
self._count(down=len(reply), host=host)
|
|
168
178
|
if _status(reply) != 200:
|
|
169
179
|
# Pass the gateway's own answer through: its status is the
|
|
170
180
|
# evidence (a bad parameter is 406 or 407 on some gateways).
|
|
@@ -184,12 +194,13 @@ class Relay:
|
|
|
184
194
|
except OSError:
|
|
185
195
|
_close(upstream)
|
|
186
196
|
return
|
|
187
|
-
self._pump(client, upstream)
|
|
197
|
+
self._pump(client, upstream, host)
|
|
188
198
|
|
|
189
199
|
def _forward(self, client: socket.socket, head: str) -> None:
|
|
190
200
|
"""Plain HTTP in absolute-URI form (revocation checks, http:// redirects)."""
|
|
191
201
|
target = urlsplit(head.split("\r\n", 1)[0].split(" ")[1])
|
|
192
|
-
|
|
202
|
+
host = (target.hostname or "").lower()
|
|
203
|
+
if host in self.block_hosts:
|
|
193
204
|
self._refuse(client)
|
|
194
205
|
return
|
|
195
206
|
lines = [ln for ln in head.split("\r\n") if not ln.lower().startswith("proxy-")]
|
|
@@ -207,13 +218,13 @@ class Relay:
|
|
|
207
218
|
rebuilt = lines[0] + "\r\n" + self._auth + "\r\n".join(lines[1:])
|
|
208
219
|
data = rebuilt.encode("latin-1")
|
|
209
220
|
upstream.sendall(data)
|
|
210
|
-
self._count(up=len(data))
|
|
221
|
+
self._count(up=len(data), host=host)
|
|
211
222
|
except OSError:
|
|
212
223
|
self._fail()
|
|
213
224
|
return
|
|
214
|
-
self._pump(client, upstream)
|
|
225
|
+
self._pump(client, upstream, host)
|
|
215
226
|
|
|
216
|
-
def _pump(self, client: socket.socket, upstream: socket.socket) -> None:
|
|
227
|
+
def _pump(self, client: socket.socket, upstream: socket.socket, host: str) -> None:
|
|
217
228
|
"""Copy both ways until either end closes, counting per chunk so a
|
|
218
229
|
keep-alive tunnel's bytes land on the page that used them."""
|
|
219
230
|
client.settimeout(None)
|
|
@@ -231,19 +242,21 @@ class Relay:
|
|
|
231
242
|
return
|
|
232
243
|
if source is client:
|
|
233
244
|
upstream.sendall(data)
|
|
234
|
-
self._count(up=len(data))
|
|
245
|
+
self._count(up=len(data), host=host)
|
|
235
246
|
else:
|
|
236
247
|
client.sendall(data)
|
|
237
|
-
self._count(down=len(data))
|
|
248
|
+
self._count(down=len(data), host=host)
|
|
238
249
|
except OSError:
|
|
239
250
|
pass
|
|
240
251
|
finally:
|
|
241
252
|
_close(upstream)
|
|
242
253
|
|
|
243
|
-
def _count(self, *, up: int = 0, down: int = 0) -> None:
|
|
254
|
+
def _count(self, *, up: int = 0, down: int = 0, host: str = "") -> None:
|
|
244
255
|
with self._lock:
|
|
245
256
|
self.bytes_up += up
|
|
246
257
|
self.bytes_down += down
|
|
258
|
+
if host:
|
|
259
|
+
self._hosts[host] = self._hosts.get(host, 0) + up + down
|
|
247
260
|
|
|
248
261
|
|
|
249
262
|
def _read_head(sock: socket.socket) -> str:
|