Captchas
Captchas
solveCaptcha() handles the kind of captcha where you read text from an image and type it into a box. It screenshots the captcha image, reads it, and types the answer into an input.
Scope, stated up front. This does not solve reCAPTCHA or hCaptcha image grids, and nothing in this package pretends to. Those need a different approach entirely.
OCR, the default
The OCR packages are optional. Install them with:
php artisan larascraper:install --captcha
That adds tesseract.js and jimp. They are left out of the standard install so projects that never solve a captcha stay lean. If they are missing, the fetch fails with a message pointing at that command rather than something obscure.
protected function handle(string $url): ScraperResponse { return $this->scrape($url) ->solveCaptcha('#captcha-img', '#captcha-input', [ 'whitelist' => 'abcdefghijklmnopqrstuvwxyz0123456789', 'psm' => 8, // tesseract page segmentation mode 'threshold' => 150, // binarization threshold ]) ->clickAndWait('#submit') ->crawl(ResultCrawler::class) ->run(); }
crop, scale, contrast and lang are also accepted.
The options are where most of the accuracy lives. A whitelist that matches the character set the site actually uses removes whole classes of misread, and psm: 8 tells tesseract it is looking at a single word rather than a page of prose.
The vision solver
Distorted captchas that tesseract struggles with are usually read in a single attempt by a vision model. Set 'solver' => 'vision' to send the screenshot to OpenAI instead of running OCR. It needs no extra Node packages, since it uses fetch, but each solve is an API call with a per-call cost:
return $this->scrape($url) ->solveCaptcha('#captcha-img', '#captcha-input', [ 'solver' => 'vision', 'apiKey' => '...', // or set OPENAI_API_KEY in the environment 'model' => 'gpt-4o-mini', // default; any vision-capable model works // 'strip' => false, // keep punctuation (the default strips it) ]) ->clickAndWait('#submit') ->crawl(ResultCrawler::class) ->run();
The default solver stays 'ocr'; 'vision' is opt-in per call.
A transient OpenAI error, a 429 rate limit or a 5xx, yields an empty answer rather than an exception, so a surrounding repeatUntil() simply tries again. A bad key or any other 4xx surfaces as an error, because retrying will not fix it.
Always pair it with a loop
Neither solver is perfect, so a single attempt is not a strategy. Wrap it in repeatUntil() and let the page tell you when it worked:
use EduLazaro\Larascraper\Support\Condition; ->repeatUntil( Condition::selectorMissing('#captcha-img'), fn ($b) => $b ->solveCaptcha('#captcha-img', '#captcha-input') ->clickAndWait('#verify'), max: 6, delay: 1500, )
When the site regenerates the image on every attempt, re-navigate inside the loop so each pass starts from fresh server state, and guard the solve with when() so a pass that happens to arrive without a captcha does not break:
->repeatUntil( Condition::captured(), fn ($b) => $b ->visit($viewerUrl) ->when( Condition::selectorExists('img[src*="captcha"]'), fn ($c) => $c->solveCaptcha('img[src*="captcha"]', 'input[name=captcha]'), ) ->submit('form') ->capture('application/pdf'), max: 8, delay: 400, )
Cost and courtesy
A captcha is the site saying it would rather you did not. Solving one is a decision, not a technicality: check the terms you are operating under, keep the request rate low, and treat max as a budget rather than a formality. Throttling belongs here as much as anywhere. See Proxies and throttling.