DEV Community

Natalie Chen
Natalie Chen

Posted on

Llama Web Scraping with Scrapeless: Why Rendering Changes Everything

Llama Web Scraping

TL;DR:

  • One rendering request separates an empty extraction from a clean result, and this walkthrough quantifies the difference. A standard GET against a JavaScript product catalog exposes only one unresolved template placeholder. Requesting the identical URL through the Scrapeless Universal Scraping API with js_render produces 13 product spans, from which Llama correctly returned all 12 genuine products.
  • Llama 4, rather than Llama 3, is the current inexpensive generation. The live OpenRouter catalog lists meta-llama/llama-4-scout as the lowest-cost current-generation Llama. It belongs to a newer family than the Llama 3.1 model still used in many tutorials.
  • The model excluded a malformed template row without extra guidance. Although the rendered document contains 13 product-name spans, one still holds the unresolved ${product.name} expression. Llama returned exactly 12 products and quietly omitted that false record.
  • This article follows the hosted route rather than local inference. The existing Web Scraping with LLaMA 3 guide runs Llama locally with Ollama. This one sends Llama 4 requests to a cloud API.
  • A native Llama API key is not required for the same workflow. The OpenRouter example executes the equivalent request using meta-llama/llama-4-scout and includes output captured from the live run.

Can Llama scrape websites?

A hosted Llama service can read supplied text and convert it into fields, but it cannot retrieve pages, run client-side JavaScript, or preserve a browsing session. The same boundary applies when the model runs locally through Ollama. Changing where the model weights execute does not give them access to HTTP or a browser; local and cloud options differ only in where inference takes place.

The site's earlier Web Scraping with LLaMA 3 article covers the local approach: llama3.1:8b on Ollama, connected to Selenium for product pages. This tutorial examines the complementary architecture, using a hosted Llama 4 model with no local download or GPU. Its demonstration page was selected specifically to show why retrieval and rendering matter. For the wider concept of using language models as parsers, see the LLM scraper overview.

Install

The openai package supports OpenRouter's OpenAI-compatible endpoint, while requests handles retrieval:

pip install "llama-api-client==0.6.0" "openai==2.48.0" requests
Enter fullscreen mode Exit fullscreen mode

Configure

Set the credentials used by the native Llama and Scrapeless examples:

export LLAMA_API_KEY="your_llama_api_key"
export SCRAPELESS_API_KEY="sk_your_scrapeless_key"
Enter fullscreen mode Exit fullscreen mode

Get a page Llama can actually read

The target is a public rendering-practice page built to make the retrieval gap visible. Its product grid has no real records until browser-side JavaScript fills it. Before that execution, the source contains only an unresolved template literal. The following script compares both versions and writes the rendered page that is suitable for extraction:

# fetch_rendered.py — plain GET vs server-side rendering, same URL
import os

import requests

URL = "https://www.scrapingcourse.com/javascript-rendering"
MARKER = 'class="product-name"'

plain = requests.get(URL, timeout=60).text
print(f"plain GET: {len(plain):,} chars | product-name spans: {plain.count(MARKER)}")

resp = requests.post(
    "https://api.scrapeless.com/api/v2/unlocker/request",
    headers={
        "Content-Type": "application/json",
        "x-api-token": os.environ["SCRAPELESS_API_KEY"],
    },
    json={
        "actor": "unlocker.webunlocker",
        "input": {"url": URL, "method": "GET", "js_render": True},
    },
    timeout=120,
)
resp.raise_for_status()
rendered = resp.json().get("data", "")
print(f"rendered:  {len(rendered):,} chars | product-name spans: {rendered.count(MARKER)}")

with open("page.html", "w", encoding="utf-8") as f:
    f.write(rendered)
Enter fullscreen mode Exit fullscreen mode

For the plain request, the script reports product-name spans: 1. That match is not a product; it is the unresolved <span class="product-name">${product.name}</span> template. After rendering, the count rises to 13. This difference is the document lifecycle described by the HTML scripting specification: some content exists only after scripts modify the DOM.

The Universal Scraping API performs rendering and proxy routing within that single POST. The saved page.html now contains content a language model can meaningfully parse.

Basic implementation: Llama as the extractor

Meta's official Llama API supports structured responses with response_format={"type": "json_schema", "json_schema": {...}}, as shown in the Meta Llama API Python client repository. Unlike endpoints that accept a simple json_object flag, the native service expects a schema-guided format. OpenRouter currently identifies meta-llama/llama-4-scout as the least expensive Llama 4-generation model, while Meta's native endpoint uses the longer Llama-4-Scout-17B-16E-Instruct-FP8 identifier for its counterpart.

Note: Running the native example requires a funded LLAMA_API_KEY. The next section sends the same extraction through OpenRouter and includes output from an executed request.

# extract_llama.py — native Llama API extraction (requires LLAMA_API_KEY)
import json
import os

from llama_api_client import LlamaAPIClient

client = LlamaAPIClient(api_key=os.environ["LLAMA_API_KEY"])

page_html = open("page.html", encoding="utf-8").read()

response = client.chat.completions.create(
    model="Llama-4-Scout-17B-16E-Instruct-FP8",
    max_completion_tokens=2000,
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "Products",
            "schema": {
                "type": "object",
                "properties": {
                    "products": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {"name": {"type": "string"}, "price_usd": {"type": "number"}},
                            "required": ["name", "price_usd"],
                        },
                    }
                },
                "required": ["products"],
            },
        },
    },
    messages=[
        {"role": "system", "content": "Extract every real product from this page. Ignore any unrendered template placeholders."},
        {"role": "user", "content": page_html},
    ],
)

data = json.loads(response.completion_message.content.text)
print(f"extracted {len(data['products'])} products")
Enter fullscreen mode Exit fullscreen mode

The native client's response layout is important: the returned message text appears at response.completion_message.content.text. That is different from the choices[0].message.content property used by OpenAI-compatible clients.

No native key? Run it through OpenRouter

OpenRouter exposes Llama through the same OpenAI-compatible chat-completions interface used for other hosted models. The next program performs retrieval and extraction in a single file and is the version used for the live test:

# extract_openrouter.py — the same extraction, executed via OpenRouter
import json
import os

import requests
from openai import OpenAI

URL = "https://www.scrapingcourse.com/javascript-rendering"

resp = requests.post(
    "https://api.scrapeless.com/api/v2/unlocker/request",
    headers={
        "Content-Type": "application/json",
        "x-api-token": os.environ["SCRAPELESS_API_KEY"],
    },
    json={"actor": "unlocker.webunlocker", "input": {"url": URL, "method": "GET", "js_render": True}},
    timeout=120,
)
resp.raise_for_status()
page_html = resp.json().get("data", "")

client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])

completion = client.chat.completions.create(
    model="meta-llama/llama-4-scout",
    temperature=0,
    max_tokens=2000,
    response_format={"type": "json_object"},
    messages=[
        {
            "role": "system",
            "content": 'Extract every product. Reply ONLY with JSON: {"products":[{"name":str,"price_usd":number}]}.',
        },
        {"role": "user", "content": page_html},
    ],
)

data = json.loads(completion.choices[0].message.content)
print(f"extracted {len(data['products'])} products from the rendered page")
for row in data["products"]:
    print(json.dumps(row, ensure_ascii=False))
Enter fullscreen mode Exit fullscreen mode

The live request produced exactly 12 real products and did not include the placeholder:

extracted 12 products from the rendered page
{"name": "Chaz Kangeroo Hoodie", "price_usd": 52}
{"name": "Teton Pullover Hoodie", "price_usd": 70}
Enter fullscreen mode Exit fullscreen mode

There are 13 product-name elements in the rendered source, but one still contains the literal ${product.name} template expression. Llama recognized that string as scaffolding and excluded it without receiving a special warning about the malformed row. The entire request used 5,924 tokens.

Advanced patterns

  • Count a stable element before accepting the model's record total. Comparing one raw-template match with 13 rendered spans exposed both the need for JavaScript execution and the presence of one invalid row. This inexpensive check can reveal collection problems that model output alone will not.
  • Treat markers as candidates, not guaranteed records. The surviving ${product.name} value illustrates a broader issue: even rendered HTML can contain framework templates that were never hydrated with actual data.
  • Iterate page by page in Python. Page boundaries provide clean record boundaries. Sending multiple pages in a single prompt makes it harder to determine where one product set ends and another begins.
  • Choose cloud or local inference based on operations and volume. The Ollama workflow in the companion guide avoids usage-based token fees but needs a GPU-capable host. A hosted API removes the hardware requirement and charges for tokens. The extraction capability and fetch-first architecture remain the same.

Troubleshooting

  • A rendered page still produces template syntax for a field. Search the source for literal placeholders such as ${...}, {{...}}, or <%...%> before blaming the model. Client frameworks can leave unused templates in the DOM even after other records render correctly.
  • A normal requests.get call returns an empty or nearly empty product grid. That is a common signature of client-rendered content. Enable js_render: true in the retrieval request rather than attempting to extract from the initial HTML.
  • The response is not valid JSON. Verify that response_format is present. Without structured-output guidance, an OpenAI-compatible endpoint may wrap the data in explanation or return prose.
  • The same input gives slightly different results. Set temperature=0; extraction should behave like deterministic transcription rather than open-ended generation.

Conclusion

The one-versus-13 comparison captures the need for a rendering layer. A plain HTTP request sees a template placeholder where the completed page contains products; only a render-aware fetch produces HTML worth extracting. Once that page existed, the lowest-cost current Llama 4 tier handled it correctly on the first request: 12 actual products, no record generated from the leftover template, and fewer than 6,000 tokens in total.

Whether inference runs in the cloud or locally, Llama was not the missing component. The incomplete page was.

Ready to Feed Llama Real Pages?

The retrieval component used here is the Universal Scraping API. Its available request volumes are listed on the pricing page, and the developer documentation describes each unlocker.webunlocker parameter used by the scripts.

FAQ

Q: Can Llama scrape websites by itself?

No, regardless of where it runs. A hosted or local Llama model extracts information from text it receives; it does not request URLs, render JavaScript, or maintain browser sessions. A complete workflow needs a fetch layer that supplies faithful rendered content.

Q: Which Llama model should I use for web-scraping extraction?

On OpenRouter, use meta-llama/llama-4-scout. It is the least expensive current Llama 4-generation model in the live catalog and returned all 12 genuine products in this test. Tutorials centered on Llama 3.1 refer to an earlier generation.

Q: Should I run Llama locally or through a cloud API for scraping?

Both choices follow the same retrieve-then-extract architecture. Ollama eliminates per-token charges but requires an appropriate local machine and a model download. The hosted workflow used here needs no local GPU, though it charges by token. The companion Web Scraping with LLaMA 3 article covers local deployment in detail.

Q: Why did the model skip one of the products on the rendered page?

Because the thirteenth element was not a product. The executed page contained 13 product-name spans, but one held the unresolved ${product.name} template string. Llama correctly treated that value as framework scaffolding and returned the other 12 records.

Q: Is scraping with Llama legal?

Running extraction locally or in the cloud does not change the rules for data collection. Retrieve only public pages, follow website terms and the Robots Exclusion Protocol, use reasonable request volumes, and process personal information under applicable law.

Top comments (0)