一、为什么需要抓取 Pinterest
Pinterest 是一个以图片为核心的视觉发现平台,用户把灵感整理成「图钉(Pin)」和「板块(Board)」。对做选品调研、设计灵感收集、竞品分析或数据集构建的开发者来说,Pinterest 上的公开内容是一座很有价值的素材库。
但在动手之前,需要先明确两件事:抓什么和怎么抓才合规。本文会从页面结构、数据来源、抓取实现和常见坑四个角度,给出一套可落地的实践方案。
提示:Pinterest 的用户内容受版权保护,抓取到的图片和文本仅建议用于个人研究、学习或已获授权的场景,请遵守目标站点的服务条款与 robots 协议,控制请求频率,不要对站点造成压力。
二、先搞清楚 Pinterest 的数据结构
Pinterest 的核心对象只有三类,理解它们之间的关系,抓取思路就清晰了。
| Pin(图钉) | 单条图片内容,包含图片、标题、描述、来源链接、作者等字段 | /pin/{pin_id}/ |
| Board(板块) | 用户创建的主题集合,包含多个 Pin | /{username}/{board_name}/ |
| User(用户) | 账号主页,聚合其所有板块 | /{username}/ |
抓取路径通常是:用户主页 → 板块列表 → 板块内 Pin 列表 → 单个 Pin 详情 → 图片原图。实际项目中,多数需求只需要走到「板块内 Pin 列表 + 图片地址」这一步。
三、三种主流抓取方式对比
3.1 直接解析 HTML 页面
最简单的方式是用 HTTP 客户端请求页面,再用解析库提取 DOM 中的图片和链接。优点是实现快,缺点是 Pinterest 是典型的前端渲染应用,首屏 HTML 里往往只有少量数据,且页面结构会随版本变化。
3.2 拦截页面内嵌的 JSON 数据
Pinterest 页面会把首屏数据以 JSON 形式内嵌在 HTML 中(常见于 __PWS_DATA__ 之类的脚本块),这是性价比最高的方式:一次请求就能拿到结构化的 Pin 列表,字段完整、无需渲染。
3.3 调用内部接口 / 浏览器自动化
滚动加载更多内容时,页面会请求分页接口。通过浏览器自动化工具(如 Playwright)监听网络请求,可以捕获这些接口的响应。这种方式最稳定,但资源开销也最大,适合数据量较大或需要登录态的场景。
| HTML 解析 | 低 | 低 | 快速验证、少量数据 |
| 内嵌 JSON | 中 | 中 | 批量抓取板块 Pin 列表 |
| 浏览器自动化 | 高 | 高 | 需要滚动加载、登录态 |
四、实战:抓取一个板块下的所有图钉
下面用 Python 演示「内嵌 JSON + 滚动分页」的组合方案。核心思路是:先请求板块页拿到首屏数据,再模拟滚动请求分页接口,直到没有更多数据。
4.1 环境准备
pip install requests beautifulsoup4 lxml
4.2 解析首屏内嵌数据
import json
import re
import requests
from bs4 import BeautifulSoup
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
),
"Accept-Language": "en-US,en;q=0.9",
}
def fetch_board_html(board_url: str) -> str:
resp = requests.get(board_url, headers=HEADERS, timeout=15)
resp.raise_for_status()
return resp.text
def extract_initial_state(html: str) -> dict:
"""从页面中提取内嵌的 JSON 初始状态。"""
soup = BeautifulSoup(html, "lxml")
for script in soup.find_all("script"):
text = script.string or ""
if "__PWS_DATA__" in text:
match = re.search(r"__PWS_DATA__\\s*=\\s*(\\{.*\\})\\s*;?\\s*$", text, re.S)
if match:
return json.loads(match.group(1))
return {}
注意:内嵌变量的名称和结构会随站点版本调整,实际使用时建议先手动打开页面源码确认,再写正则或改用更宽松的提取逻辑。
4.3 递归提取 Pin 信息
内嵌 JSON 的层级较深,与其硬编码路径,不如写一个递归遍历函数,按字段名匹配 Pin 对象,这样对结构变化更鲁棒。
def walk(node, results):
"""递归遍历 JSON,收集所有 Pin 对象。"""
if isinstance(node, dict):
# Pin 对象通常带有 id 和 images 字段
if "id" in node and "images" in node and isinstance(node["images"], dict):
results.append(node)
for value in node.values():
walk(value, results)
elif isinstance(node, list):
for item in node:
walk(item, results)
def parse_pin(pin: dict) -> dict:
images = pin.get("images", {})
origin = images.get("orig", {}) or images.get("736x", {})
return {
"pin_id": pin.get("id"),
"title": pin.get("title") or pin.get("grid_title"),
"description": pin.get("description"),
"link": pin.get("link"),
"image_url": origin.get("url"),
"width": origin.get("width"),
"height": origin.get("height"),
}
4.4 下载图片
import os
import time
def download_image(url: str, save_dir: str, filename: str) -> str:
os.makedirs(save_dir, exist_ok=True)
path = os.path.join(save_dir, filename)
resp = requests.get(url, headers=HEADERS, timeout=20, stream=True)
resp.raise_for_status()
with open(path, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
time.sleep(0.5) # 控制频率,避免给站点造成压力
return path
def main(board_url: str, save_dir: str = "pins"):
html = fetch_board_html(board_url)
state = extract_initial_state(html)
raw_pins = []
walk(state, raw_pins)
seen = set()
for raw in raw_pins:
pin = parse_pin(raw)
if not pin["pin_id"] or pin["pin_id"] in seen:
continue
seen.add(pin["pin_id"])
if pin["image_url"]:
download_image(pin["image_url"], save_dir, f"{pin['pin_id']}.jpg")
print(pin["pin_id"], pin["title"])
print(f"共处理 {len(seen)} 个图钉")
if __name__ == "__main__":
main("https://www.pinterest.com/{username}/{board_name}/")
五、处理滚动加载与分页
板块页首屏通常只返回几十个 Pin,剩余内容需要滚动触发。有两种处理方式:
- 接口分页:在浏览器开发者工具的 Network 面板中筛选 XHR 请求,找到返回 Pin 列表的接口,观察其 bookmark 或 cursor 参数,循环请求直到返回空值。
- 浏览器自动化:用 Playwright 打开页面,循环执行滚动并等待新元素出现,同时监听响应事件收集数据。
from playwright.sync_api import sync_playwright
def crawl_with_browser(board_url: str, max_scroll: int = 20):
collected = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(user_agent=HEADERS["User-Agent"])
def on_response(response):
if "/resource/BoardFeedResource/" in response.url:
try:
collected.append(response.json())
except Exception:
pass
page.on("response", on_response)
page.goto(board_url, wait_until="networkidle")
for _ in range(max_scroll):
page.mouse.wheel(0, 3000)
page.wait_for_timeout(1500)
browser.close()
return collected
这种方式的好处是不用逆向接口参数,坏处是速度慢、占用内存高,建议配合 max_scroll 上限和异常重试使用。
六、常见问题与避坑建议
6.1 请求被限制或返回空数据
Pinterest 对高频访问较敏感。建议:使用真实浏览器 UA、控制并发(单线程 + 随机延时)、复用 Session 保持 Cookie、必要时降低抓取量。出现 429 时应立即退避,而不是加大重试频率。
6.2 图片地址失效
Pinterest 的图片 CDN 地址带有尺寸前缀(如 /236x/、/736x/、/originals/)。抓取时优先取 orig 或 originals 版本,若失效可尝试替换路径前缀获取其他尺寸。
6.3 字段缺失
并非所有 Pin 都有标题和描述,部分为纯图片。解析时统一用 or 兜底,避免 KeyError 中断整个流程。
6.4 页面结构变化
内嵌 JSON 的变量名和层级会随版本更新。建议把「提取逻辑」和「字段映射」拆成独立函数,结构变化时只改一处;同时加入日志,便于快速定位解析失败的原因。
七、数据整理与后续利用
抓取只是第一步,把数据整理成可用格式才有价值。建议把结果统一写入 CSV 或 JSON Lines,字段包括 pin_id、title、description、link、image_url、board、crawl_time。
import csv
def save_to_csv(records, path="pins.csv"):
if not records:
return
with open(path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=list(records[0].keys()))
writer.writeheader()
writer.writerows(records)
有了结构化数据,就可以进一步做图片去重(按 URL 或感知哈希)、主题聚类、配色分析等下游任务。
八、总结
抓取 Pinterest 的关键不在于写多少代码,而在于选对数据入口:优先解析内嵌 JSON,滚动加载用接口分页或浏览器自动化兜底。同时务必控制请求频率、尊重版权与站点条款,把抓取范围限定在公开内容和合理用途内。
按本文的流程,你可以先跑通单个板块的抓取,再逐步扩展到多板块、多用户,最后接入存储与分析环节,形成一条完整的数据流水线。





