Unofficial. Not affiliated with, endorsed by, or sponsored by Google. "Google Lens" is a trademark of Google LLC. This is an independent, community-built client for research and educational use.
Unofficial Google Lens visual product search over plain HTTP. Give it an image, get back the products Google Lens sees in it — exact product titles plus external merchant/result links. No SerpApi bill, and none of the OCR-only limitations of the free Lens libraries.
pip install "chrome-lens-search[browser]"
glens sneaker.jpgThat's it. The first lookup launches Chrome once to create a reusable Google session — it tries headless first and may briefly show a window if Google blocks the headless attempt. Every lookup after that is a plain, fast HTTP request (~2 s), no browser, no Selenium in the hot path.
Or from Python — paths, image URLs, or raw bytes all work:
import glens
result = glens.search("sneaker.jpg") # local file
result = glens.search("https://example.com/sneaker.jpg") # or an image URL
if result: # None when the lookup fails
print(result.top_title) # "Nike Air Max 90 Men's Shoes"
for m in result.matches:
print(f"[{m.domain}] {m.title} -> {m.url}")[nike.com] Nike Air Max 90 Men's Shoes -> https://www.nike.com/t/...
[stockx.com] Nike Air Max 90 -> https://stockx.com/...
[goat.com] Air Max 90 -> https://www.goat.com/...
Google Lens is uniquely good at naming the exact product in a photo. But:
- SerpApi's Lens endpoint is paid.
- The free Python Lens libraries (
chrome-lens-pyet al.) do OCR/translation only — they can't give you the product/shopping matches. - Lens from a plain server just gets blocked.
This library talks to Google's own Lens upload endpoint (the image goes only to Google, never a third-party host) and reads the product matches straight back.
pip install "chrome-lens-search[browser]" # recommended: works out of the boxThe [browser] extra (Selenium + undetected-chromedriver) is only used to create — and occasionally refresh — the Google session. On a machine that already has a session jar (see GLENS_COOKIE_FILE below), the slim core is enough:
pip install chrome-lens-search # plain-HTTP core only (needs an existing jar)glens sneaker.jpg # pretty-printed results
glens https://example.com/sneaker.jpg # image URLs work too
glens sneaker.jpg --json # machine-readable output
glens sneaker.jpg --backend req -v # fast path only, with progress logs
glens sneaker.jpg --max-matches 5 --max-titles 3
glens sneaker.jpg --lang de # prefer German result pages
glens sneaker.jpg --cache # reuse recent results for this image(glens search sneaker.jpg also works, if you prefer the explicit subcommand.)
Pretty output shows the top title, the frequency-ranked titles, and the merchant links; --json emits the full LensResult (add --raw to include every harvested anchor). Exit code 0 means results were found, 1 means no results, so it scripts cleanly:
glens photo.jpg --json > result.json || echo "nothing matched"Google only server-renders the product tiles for a JS-verified session. A cold requests client just gets an "enable JavaScript" shell. The trick:
- Browser (once): drive a real undetected-chromedriver session, upload the image, harvest the rendered results — and save the google.com cookies + user-agent to a small jar.
- Requests (every time after): replay those cookies over plain
requests. Google now serves the full product HTML to a browser-less client. Fast, no window, no Selenium in the hot path. - Self-healing: when the jar goes stale, Google serves the degraded JS-gate page; the
reqpath detects it by content (too few product links, not by sniffing for a marker) and transparently falls back to the browser, which re-mints the jar. You never manage cookies by hand.
So: first call launches Chrome; subsequent calls are just HTTP until the cookies expire.
backend |
Behaviour |
|---|---|
"auto" (default) |
Try req, fall back to browser if there's no/stale jar. |
"req" |
Plain HTTP only. Returns None if the jar is missing/stale (won't launch Chrome). |
"browser" |
Always drive Chrome; also re-mints the jar. |
glens.search("img.jpg", backend="req") # fast path only
glens.search("img.jpg", backend="browser") # force a fresh browser passThe browser backend defaults to the bundled builder, but you can inject any factory that returns a Selenium-compatible driver — handy if you already run a stealth/persistent Chrome:
def my_driver():
return make_my_undetected_chrome()
glens.search("img.jpg", driver_factory=my_driver)search() returns a LensResult (or None on failure — it never raises for ordinary network/parse errors):
@dataclass
class LensResult:
top_title: str | None # best single product name
titles: list[str] # frequency-ranked product names
matches: list[Match] # external merchant/result links
raw_anchors: list[dict] # every anchor harvested, unfiltered
results_url: str | None # internal, session-bound Google URL (logging only; not browser-openable)
@dataclass
class Match:
title: str
url: str
domain: strtitles are frequency-ranked: the exact product repeats across tiles, so it rises to the top on its own.
For batches there's search_many, which runs the images in order through the same in-process lock (be gentle — see the caveats):
results = glens.search_many(["a.jpg", "b.jpg"], backend="auto") # list of LensResult | NoneThe API is built to be dropped into your own code: one call in, one dataclass (or None) out, nothing raises for ordinary failures. Two helpers cover the ops side:
import glens
# At startup / deploy time: make sure the fast path is ready.
if not glens.status()["req_ready"]:
glens.warmup() # one headless Chrome run (needs [browser])
# In your pipeline: pure HTTP from here on, ~2-3s per lookup.
for path in my_images:
result = glens.search(path, backend="req")
if result:
print(path, "->", result.top_title)glens.status()— JSON-safe health dict: is the session jar valid, how old is it, is the browser extra installed, is the fast path (req_ready) available. Same thing on the CLI:glens status --json(exit 0 = ready), perfect for container health checks.glens.warmup()— mints the jar with one browser run and returnsTruewhen ready; a no-op if the jar already exists. CLI:glens warmup. Run it once at container start or CI setup, then usebackend="req"at runtime so production code can never accidentally launch Chrome.- Headless servers without Chrome: run
glens warmupon any machine that has Chrome, ship the jar file (GLENS_COOKIE_FILE), and the server only ever needs the slimrequestscore. - Batch jobs: see examples/batch_folder.py — folder in, CSV of product matches out. Calls serialise through an in-process lock; keep it to one image at a time and don't hammer.
For machine consumption, result.to_dict() gives you JSON-ready output, and the CLI's --json + exit codes (0 results / 1 none / 2 usage) script cleanly.
Everything is a per-call argument; env vars are just the defaults.
| Argument | Env var | Default |
|---|---|---|
backend |
GLENS_BACKEND |
auto |
cookie_file |
GLENS_COOKIE_FILE |
~/.cache/glens/cookies.json |
max_matches |
GLENS_MAX_MATCHES |
20 |
max_titles |
GLENS_MAX_TITLES |
6 |
timeout |
GLENS_RESULT_TIMEOUT |
25 |
lang |
GLENS_LANG |
(none) — preferred response language via Accept-Language, e.g. de, pt-BR |
use_cache |
GLENS_CACHE |
off |
| — | GLENS_CACHE_TTL |
86400 — cache entry lifetime in seconds |
| — | GLENS_CACHE_DIR |
~/.cache/glens/results |
| — | GLENS_HEADLESS |
1 |
| — | GLENS_CHROME_PROFILE |
(none) — persistent Chrome profile dir |
Pointing GLENS_CHROME_PROFILE at a persistent directory lets the verified session survive across runs, keeping the fast path fast.
lang asks Google for a response language via the fast path's Accept-Language header:
glens.search("img.jpg", lang="de") # prefer German result pagesBest-effort by design: Google decides, and the country/market always follows your IP. (There's deliberately no hl/gl URL override — the Lens results URL is session-signed and Google answers any query tampering, on either the upload or the results request, with a 403. Live-tested.)
use_cache=True (or --cache, or GLENS_CACHE=1) stores the harvested anchors per image+locale (SHA-256 keyed) under ~/.cache/glens/results and reuses them for 24 hours (GLENS_CACHE_TTL). Great for dev loops and re-runs — it avoids re-uploading the same image to Google. max_matches/max_titles still apply per call; only the raw harvest is cached. An explicit backend="browser" always skips the cache read (a successful browser run still refreshes the cached entry), so forcing a jar re-mint keeps working with caching on.
"No results" on the very first run → the first lookup needs Chrome to create the session. Install the extra and retry: pip install "chrome-lens-search[browser]". The CLI prints this hint when it applies.
Want a server with no Chrome at all? Run one lookup on any machine that has Chrome, then copy the jar (~/.cache/glens/cookies.json) to the server and point GLENS_COOKIE_FILE at it. Use backend="req" so the server never tries to launch a browser. When the jar eventually goes stale, re-mint it the same way.
Nothing prints / want to see progress? Add -v on the CLI, or in Python: logging.basicConfig(level=logging.INFO) — all progress goes to the glens logger.
- This scrapes an undocumented Google endpoint. It is not an official API, and it's against Google's Terms of Service. Use it where that's acceptable to you.
- It will break. Google changes the endpoint, the JS-gate behaviour, and the markup without notice. When that happens the harvesting is deliberately selector-light (anchors + aria-labels, no hardcoded class names) so it degrades rather than shatters — but expect to patch it. PRs welcome.
- Results are noisy by design. The parser favours recall; verify matches downstream if you need precision.
- Be gentle. One image per lookup, a module lock serialises calls in-process. Don't hammer it.
- Guide — the long-form walkthrough: the fast/slow hybrid, automating glens in your code, deployment recipes (Docker, Chrome-less servers, CI), the CLI in depth, troubleshooting, and how it works under the hood.
- API Reference — every function, dataclass, environment variable, CLI flag, and exit code, with exact signatures and return/error semantics.
- Best Practices — what to actually do: backend choice, minting vs serving, being gentle, handling
None, caching strategy, deployment, jar hygiene, security, and testing your integration.
pip install -e ".[dev]"
pytest # hermetic: no network, no browser
ruff check .See CONTRIBUTING.md — especially the rules about synthetic test fixtures (never commit Google content).
MIT — see LICENSE.
Not affiliated with, endorsed by, or sponsored by Google. "Google Lens" is a trademark of Google LLC.