骆驼网页抓取:渲染为何决定数据
Senior Cybersecurity Analyst
TL;DR:
- “无法提取”和“干净提取”之间的差距仅为一次渲染调用,而本指南对此进行了准确测量。 对于一个仅用JavaScript的产品目录进行简单的GET请求会返回一个未渲染的模板占位符;而通过Scrapeless Universal Scraping API使用
js_render访问同一URL则返回13个真实的产品span,并且Llama从中正确提取了12个真实产品。 - Llama 4是当前的廉价版本,而不是Llama 3。
meta-llama/llama-4-scout是当前代中最便宜的Llama,位于实时OpenRouter目录中——与大多数现有教程仍使用的Llama 3.1名称属于一个不同、更现代的家族。 - 模型自动过滤掉了一个损坏的模板行。 渲染的页面实际上包含13个
product-namespan;其中一个是未被激活的${product.name}占位符,而Llama的提取则准确返回了12个真实产品,跳过了假产品,而无需被指示。 - 这是云端路径,而不是本地路径。 本网站上有一个单独的指南,使用LLaMA 3进行网络爬虫,涵盖了通过Ollama在本地运行Llama;而本指南则是通过托管的API运行Llama 4。
- 还没有本地Llama API密钥?相同的请求可以通过OpenRouter运行。 本指南在
meta-llama/llama-4-scout上实时执行了该请求,并显示了捕获的输出。 - 在抓取方面免费开始。 在app.scrapeless.com创建您的Scrapeless API密钥。
Llama能抓取网站吗?
一个托管的Llama端点读取文本并返回字段;它不抓取页面,不执行其JavaScript,也不保持会话。这在模型通过Ollama本地运行或通过云API运行时都是如此——模型权重并不改变HTTP工作的方式。本地和云路径之间的区别仅在于推理发生的位置。
使用LLaMA 3进行网络爬虫已经在本网站上,涵盖了本地路径:通过Ollama使用llama3.1:8b,配合Selenium抓取产品页面。本指南涵盖了另一半——通过API访问的托管Llama 4模型,无需下载本地模型和GPU要求,针对一个特别选择的不同演示目标以证明抓取层的重要性。对于更广泛的类别问题,LLM爬虫解释器一般涵盖了LLM作为解析工具的应用。
安装
openai涵盖了OpenRouter路径(Llama在这里通过OpenAI兼容的界面提供),而requests涵盖了抓取层:
bash
pip install "llama-api-client==0.6.0" "openai==2.48.0" requests
配置
bash
export LLAMA_API_KEY="your_llama_api_key"
export SCRAPELESS_API_KEY="sk_your_scrapeless_key"
获取Llama能实际读取的页面
演示目标是一个公开的渲染实践页面,专门构建用于准确展示此差距:它的产品网格在客户端JavaScript填充之前是空的,而原始HTML则包含一个未执行的模板字符串。一个脚本展示了差异,并保存值得提取的版本:
python
# fetch_rendered.py —_plain GET与服务器端渲染,相同的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)
运行结果打印 product-name spans: 1 对于未渲染的抓取 —— 那个单一的命中是一个未执行的 <span class="product-name">${product.name}</span> 模板,而不是实际数据 —— 并且对渲染后的结果打印 product-name spans: 13。两者之间的差距正是文档生命周期 HTML 脚本规范 定义的内容:只有在脚本对 DOM 运行后才存在的内容。渲染和代理路由在那一个 POST 请求的服务器端发生 —— 通用抓取 API 是抓取层,而 page.html 现在是模型可以提取的内容。
基本实现:Llama 作为提取器
Meta 的官方 Llama API 通过 response_format={"type": "json_schema", "json_schema": {...}} 接受结构化输出,依照 Meta 的 Llama API Python 客户端库 中发布的 SDK —— 此端点上没有裸 json_object 模式,只有受 schema 指导的输出。当前 OpenRouter 目录将 meta-llama/llama-4-scout 列为最便宜的 Llama 4 代模型;在 Meta 自家的原生 API 上,相应的模型 ID 采用了更完整的 Llama-4-Scout-17B-16E-Instruct-FP8 形式。
注意:这个代码块需要一个带有信用的
LLAMA_API_KEY—— 本指南不假设的唯一前提条件。下一部分通过 OpenRouter 实时运行相同的提取,并捕捉输出。
python
# extract_llama.py — 原生 Llama API 提取(需要 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": "从此页面提取所有真实产品。忽略任何未渲染的模板占位符。"},
{"role": "user", "content": page_html},
],
)
data = json.loads(response.completion_message.content.text)
print(f"提取了 {len(data['products'])} 个产品")
注意响应的形状:Meta 的原生客户端返回 response.completion_message.content.text 下的消息,而不是 OpenAI 兼容客户端使用的 choices[0].message.content 路径。
没有原生密钥?通过 OpenRouter 运行
OpenRouter 在与本系列其他模型相同的 OpenAI 兼容聊天补全界面下提供 Llama。这是本指南真实执行的版本,在一个自包含的脚本中进行抓取和提取:
python
# extract_openrouter.py — 通过 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": '提取每个产品。仅以 JSON 回复:{"products":[{"name":str,"price_usd":number}]}。',
},
{"role": "user", "content": page_html},
],
)
data = json.loads(completion.choices[0].message.content)
print(f"从渲染页面提取了 {len(data['products'])} 个产品")
for row in data["products"]:
print(json.dumps(row, ensure_ascii=False))
实时运行提取了确切的 12 个真实产品,没有任何占位符:
text
从渲染页面提取了 12 个产品
{"name": "查兹袋鼠连帽衫", "price_usd": 52}
{"name": "泰顿套头连帽衫", "price_usd": 70}
两者都使用相同的获取-然后-提取架构。通过 Ollama 的本地方式避免了按 token 收费,但需要支持 GPU 的主机和模型下载;本指南的云路径不需要本地硬件,但按 token 收费。伴随的帖子 Web Scraping with LLaMA 3 对本地路径进行了深入介绍。
问:为什么模型在渲染页面上跳过了其中一个产品?
因为它不是一个真实的产品。本指南的实时运行中的渲染 HTML 包含 13 个 product-name span,但其中一个是未被初始化的 ${product.name} 模板占位符,遗留自页面的客户端框架——模型正确地将其排除在提取的 12 个产品之外。
问:使用 Llama 进行抓取合法吗?
提取层不会改变云或本地路径上的收集规则。只抓取公共页面,尊重网站条款和由 机器人排除协议 标准化的机器人指令,保持数量在合理范围内,并根据适用法律处理任何个人数据。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



