DeviantArt 是全球最大的数字艺术家在线社区之一,数百万成员每天发布绘画、插图、摄影、像素艺术和概念设计作品。它的公开搜索页和画廊页是一份有用的索引,能看出哪些风格正在流行、谁在某个题材下发布作品,以及一个作品集是如何组织的,而这恰恰是研究项目、个人灵感板或视觉趋势研究想要以编程方式读取的那类目录,而不是手动滚动浏览。
本指南将向你展示如何用 Python 从 DeviantArt 抓取图片。你会构建一个小巧、可直接运行的抓取脚本,它通过 Crawling API 获取渲染后的公开搜索页,用 BeautifulSoup 解析每个结果,采集作品的元数据,并把缩略图文件下载到本地。整个演示始终限定在公开画廊页面范围内,而结尾附近的合法性章节并非套话:DeviantArt 上的作品由其创作者拥有版权,所以在把它指向任何真实内容之前,请先阅读那一部分。
你将构建什么
一个 Python 脚本:它接收一个公开的 DeviantArt 搜索 URL,通过 Crawling API 取回渲染后的 HTML,并为结果网格中的每件作品提取一条结构化记录。我们将以一次关键词搜索作为贯穿全文的示例,从每张结果卡片抓取以下字段:
- 标题 作品的标题,例如 "Owl #6"。
- 作者 作者的 DeviantArt 用户名,从作品链接中读取。
- 图片 URL 结果网格中显示的缩略图来源。
- 页面 URL 指向单件作品页面的链接。
- 收藏数 当卡片暴露它时的公开收藏数。
- 浏览量 当卡片暴露它时的公开浏览量。
为什么普通请求在 DeviantArt 上会失败
如果你用一个简单的 HTTP 客户端请求 DeviantArt 搜索 URL,你会得到一个状态为 200 的响应,但正文中几乎没有任何作品数据。有两点对你不利。首先,DeviantArt 用 JavaScript 在浏览器中构建它的结果网格,所以最初的 HTML 只是一个空壳,要等页面脚本运行后才会填充进去。其次,该平台会迅速标记自动化流量:数据中心 IP 以及看起来不像真实浏览器的请求模式,在到达渲染后的缩略图之前就会被发起验证质询或被封禁。
所以一个能正常工作的 DeviantArt 抓取器在一次请求中需要两样东西:一个真正能渲染页面的浏览器,以及一个被平台读作真实访客的 IP。你可以自己用无头浏览器加上一池轮换住宅代理把它拼起来,但把它们缝合在一起并保持其健康运行才是大部分工作量所在。Crawling API 把两者折叠进单次调用:你把带 JavaScript token 的 URL 发给它,它在一个可信 IP 背后渲染页面,并把处理完的 HTML 返回给你解析。
Crawlbase 提供两种 token。普通 token 获取静态 HTML;JavaScript(JS)token 会先在真实浏览器中渲染页面。DeviantArt 的搜索网格是客户端渲染的,所以这里你需要 JS token。使用普通 token 返回的是和普通抓取一样的空壳,里面没有任何东西可供解析。你可以从 1,000 次免费请求起步,无需信用卡。
前置条件
在写任何代码之前,你需要准备好几样东西。它们都花不了多少时间。
Python 基础。你应当能够自如地编写并运行 Python 脚本,以及用 pip 安装软件包。如果你对 BeautifulSoup 还很陌生,我们的在 Python 中使用 BeautifulSoup 指南涵盖了本教程默认你已掌握的解析基础。
Python 3.8 或更高版本。用 python --version 确认你的版本。如果你还没有,请从 python.org 安装,或通过 Anaconda 这类发行版安装。
一个 Crawlbase 账户和 JS token。注册、打开你的仪表盘,并从账户文档页复制你的 JavaScript(JS)token。把这个 token 当成密码对待:它用于认证你的请求,所以不要把它纳入版本控制。
搭建项目
创建一个虚拟环境,让项目依赖保持隔离,然后安装抓取器所需的库。
python --version python -m venv deviantart_env source deviantart_env/bin/activate pip install crawlbase beautifulsoup4 requests
在 Windows 上,用 deviantart_env\Scripts\activate 激活环境,而不是那一行 source。三个依赖各司其职:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML,让你能用 CSS 选择器逐个抽取字段,而 requests 则在最后一步把图片文件流式写入磁盘。
第 1 步:获取渲染后的搜索页
先从拿到处理完的页面开始。导入 CrawlingAPI 类,用你的 JS token 初始化它,并请求一个由关键词构建的搜索 URL。DeviantArt 的搜索路径是 /search?q=KEYWORD,所以一次对 "fantasy" 的查询会给你一个公开结果的网格。在解析之前检查状态,能让失败显式暴露而不是悄无声息。
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) base_url = "https://www.deviantart.com" def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("latin1") print(f"Request failed: {response['status_code']}") return None if __name__ == "__main__": keyword = "fantasy" search_url = f"{base_url}/search?q={keyword}" html = crawl(search_url) print(html[:500] if html else "No HTML returned")
对于客户端渲染的目标,两个等待选项很关键。ajax_wait 等待异步内容加载完成,而 page_wait 在加载后固定等待若干毫秒,让那些延迟渲染的缩略图在页面被捕获前出现。5 秒是个合理的起点;如果网格返回为空就把它调高。正文以 latin1 解码,让原始字节干净地透传过去。用 python scraper.py 运行脚本,你应当看到真实的作品标记,而不是普通抓取返回的空壳。这能在你写下任何一个选择器之前确认渲染是有效的。
DeviantArt 的搜索网格需要在一次调用里,于一个可信 IP 背后渲染页面。Crawling API 接收一个 JS token,在真实浏览器中运行页面,在服务端轮换住宅 IP,并交给你处理完的 HTML,于是你就省去了自己运行无头浏览器集群和代理池的麻烦。先在免费档位上把它指向一个公开的搜索页吧。
第 2 步:用 BeautifulSoup 解析作品卡片
拿到渲染后的 HTML 后,把它加载进 BeautifulSoup 并按选择器抽取每个结果。DeviantArt 把每张缩略图包裹在一个作品链接中,所以你一次性选出所有这些链接,然后从每一个里读取相同的字段。在浏览器里右键点击一张缩略图,选择 "Inspect",你会发现类似下面这样的结构。
<a data-hook="deviation_link" href="https://www.deviantart.com/siobhan-o-wisp/art/Owl-6-925596734" aria-label="Owl #6 by Siobhan-o-wisp, visual art"> <div data-testid="thumb" typeof="ImageObject"> <img alt="Owl #6" src="src_url_here" property="contentUrl" /> </div> </a>
这就从一张卡片里给了你所需的一切。锚点在 href 中携带作品页面 URL,在 aria-label 中携带标题和作者,而嵌套的 img 在 src 中携带缩略图。缩略图图片的 CSS 选择器是 a[data-hook="deviation_link"] img[property="contentUrl"],而你可以通过图片的 alt 属性拿到作品标题。下面就是解析器。
from bs4 import BeautifulSoup def artist_from_url(page_url): # DeviantArt page URLs look like /<username>/art/<slug> parts = page_url.split("/") return parts[3] if len(parts) > 3 else None def count_for(link, label): el = link.select_one(f'span[aria-label*="{label}"]') return el.get_text(strip=True) if el else None def parse_artworks(html): soup = BeautifulSoup(html, "html.parser") links = soup.select('a[data-hook="deviation_link"]') artworks = [] for link in links: img = link.select_one('img[property="contentUrl"]') if not img: continue page_url = link.get("href") artworks.append({ "title": img.get("alt"), "artist": artist_from_url(page_url), "image_url": img.get("src"), "page_url": page_url, "favourites": count_for(link, "Favourites"), "views": count_for(link, "Views"), }) return artworks
标题来自图片的 alt 属性,作者来自作品 URL 中的用户名片段,而缩略图来自 src。收藏数和浏览量住在一些带标签的小 span 里;count_for 按 aria-label 文本查询,当某张卡片不暴露那个数字时返回 None,所以缺失的计数永远不会让循环崩溃。并不是每个结果都会在网格里标出它的统计数据,这就是为什么两者都是可选字段。
DeviantArt 的类名和内部属性会在没有任何通知的情况下变化。这里稳定的钩子是 data-hook="deviation_link" 和 property="contentUrl",它们映射到公开的 schema 标记,但请把每个选择器都当成一个起始模板,而不是一份契约。当某个字段对每张卡片都返回 None 时,在浏览器开发者工具里重新检视一张实时缩略图并更新选择器。对任何生产级抓取器来说,定期维护都是常态。
第 3 步:把抓取器组合起来
现在把获取和解析接进一个可直接运行的脚本,加上分页让你可以读取不止第一个网格,并把结果写入 JSON。DeviantArt 用一个 &page= 参数为搜索分页,所以一个在页码上的小循环就能采集到更广的一批结果。请求之间稍作停顿,让你不至于在一个紧凑的循环里猛打站点。
import json import time from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) base_url = "https://www.deviantart.com" def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("latin1") print(f"Request failed: {response['status_code']}") return None def artist_from_url(page_url): parts = page_url.split("/") return parts[3] if len(parts) > 3 else None def count_for(link, label): el = link.select_one(f'span[aria-label*="{label}"]') return el.get_text(strip=True) if el else None def parse_artworks(html): soup = BeautifulSoup(html, "html.parser") links = soup.select('a[data-hook="deviation_link"]') artworks = [] for link in links: img = link.select_one('img[property="contentUrl"]') if not img: continue page_url = link.get("href") artworks.append({ "title": img.get("alt"), "artist": artist_from_url(page_url), "image_url": img.get("src"), "page_url": page_url, "favourites": count_for(link, "Favourites"), "views": count_for(link, "Views"), }) return artworks def main(): keyword = "fantasy" total_pages = 2 results = [] for page in range(1, total_pages + 1): url = f"{base_url}/search?q={keyword}&page={page}" html = crawl(url) if html: results.extend(parse_artworks(html)) time.sleep(3) with open("deviantart_data.json", "w") as f: json.dump(results, f, indent=2) print(f"Saved {len(results)} artworks") if __name__ == "__main__": main()
每一页共享同样的卡片结构,所以你已经写好的解析器无需改动就能跨所有页面工作。请求之间的 time.sleep(3) 让一次多页运行不至于猛打站点。只在你确实需要时才把 total_pages 调高,并在过程中盯住状态码。
输出长什么样
用 python scraper.py 运行完整脚本,你会为每件作品得到一条干净的结构化记录,可直接写入 JSON、CSV 或数据库。
[ { "title": "Owl #6", "artist": "siobhan-o-wisp", "image_url": "https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/.../owl_6.jpg", "page_url": "https://www.deviantart.com/siobhan-o-wisp/art/Owl-6-925596734", "favourites": "412", "views": "3.1K" }, { "title": "Magic Forest", "artist": "postapodcast", "image_url": "https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/.../magic_forest.png", "page_url": "https://www.deviantart.com/postapodcast/art/Magic-Forest", "favourites": null, "views": null } ]
这些图片 URL 指向 DeviantArt 的 CDN(wixmp.com 主机),而在不暴露统计数据的卡片上,收藏数和浏览量会返回为 null,这是符合预期的。手里有了这份列表,你就可以为元数据建立索引、研究它,或者继续去下载缩略图文件。
第 4 步:下载图片文件
这些记录保存的是缩略图 URL,而不是图片字节。要把文件存到本地,用 requests 把每个 URL 流式写入磁盘。分块流式处理让内存即便面对较大的图片也保持平稳,而 raise_for_status 把一次失败的下载变成一个被捕获的错误,而不是一个悄无声息的空文件。
import os import requests def download_image(url, save_path): try: response = requests.get(url, stream=True, timeout=30) response.raise_for_status() with open(save_path, "wb") as file: for chunk in response.iter_content(chunk_size=8192): file.write(chunk) print(f"Saved {save_path}") except requests.exceptions.RequestException as e: print(f"Error downloading {url}: {e}") def download_all(artworks, folder="downloaded_images"): os.makedirs(folder, exist_ok=True) for i, art in enumerate(artworks): url = art.get("image_url") if not url: continue artist = art.get("artist") or "unknown" path = os.path.join(folder, f"{artist}_{i}.jpg") download_image(url, path)
在抓取之后调用 download_all(results),每张缩略图就会落到一个 downloaded_images 文件夹里,以艺术家的用户名和一个序号命名。按艺术家命名让署名信息附着在文件上,这在这里很重要,因为这些图片中的每一张都属于创作它的人。在保存任何你打算用来不止是瞥一眼的东西之前,请先阅读下一节。
保持不被封禁
即便处理好了渲染,DeviantArt 仍会监视具有抓取器特征的流量。几个习惯能让一次运行保持健康,且它们适用于任何难啃的目标。
-
给请求设置节奏。在一个紧凑的循环里猛打搜索页是最快招致限速的方式。循环里的
time.sleep正是为此而设;把它留着。 - 依靠轮换。一池住宅 IP 把请求分散到许多真实用户地址上,让没有任何单个 IP 触发速率限制。Crawling API 替你处理这件事;如果你自己搭建技术栈,这就是要做对的部分。
- 读懂状态码。一次开始返回验证质询或错误的运行,是在告诉你当前的速率或 IP 档位已经不够了。把它当成该退一步的信号,而不是可以忽略的噪声。
关于更广的实战手册,请参阅如何在不被封禁的情况下抓取网站。如果你的目标像 DeviantArt 那样是客户端渲染的,我们关于抓取 JavaScript 网站的指南解释了为什么渲染很重要。而如果你更愿意让自己的流量经由一个轮换池,而不是使用托管 API,那么 Smart AI Proxy(也称 AI Proxy)会以一个即插即用的代理端点形式,给你与之相同的住宅 IP 轮换。
抓取 DeviantArt 合法吗?
这是在一个创作平台上最要紧的部分,所以别略读它。本指南里的技术工作是简单的那一半;权利问题才是难的那一半。抓取 DeviantArt 是否被允许,取决于 DeviantArt 的服务条款、你所在的司法管辖区,以及你拿数据做什么。DeviantArt 的条款对自动化访问设有限制,所以无论你的工具多么谨慎,抓取都可能违反那些条款。请阅读 DeviantArt 的服务条款及其 robots.txt,并把两者都当成你所采集内容的边界。
这里最重要的一个事实:DeviantArt 上的每件作品都由创作它的艺术家拥有版权。采集一个公开的缩略图 URL 或一个标题是一回事;你随后拿那张图片做什么是另一回事,而法律在意这其中的区别。未经创作者的明确许可,不要再分发、再发布、转售下载的作品,也不要拿它来训练模型。把你的工作限定在为元数据建立索引、为个人、研究或真正内部的研究保存缩略图,并让艺术家的名字始终附着,这样署名就永远不会丢失。被抓取的图片不是获得授权的图片,而 "它是公开的" 也不是一份许可。
本指南有意限定在公开画廊页和搜索页,因为那是让这项工作站得住脚的界线。它不涉及登录之后的任何内容、任何成人内容门控的内容、账户或个人数据,也不涉及任何绕过认证或内容门控的尝试。只针对公开、无门控的作品元数据。如果你的项目需要的不止这些,受认可的途径是 DeviantArt 官方的 DeviantArt OAuth API,它在平台实际授予的条款下暴露作品和元数据。对于任何超出个人用途的情形,尤其是商业再利用,正确的路径是官方 API 加上艺术家的许可,而不是更巧妙的抓取器。
核心要点
- DeviantArt 是客户端渲染的。普通抓取返回一个空壳,所以你必须先渲染搜索网格再解析它。
-
你需要渲染和可信 IP 二者兼具。带 JS token 的 Crawling API 在一次调用里同时做到两点;
ajax_wait和page_wait控制它等待内容的时长。 -
由 BeautifulSoup 完成抽取。选出每一个
a[data-hook="deviation_link"],然后从每一个里读取标题、作者、图片 URL、页面 URL,以及任何公开的收藏数或浏览量。 -
通过流式处理下载。用带
stream=True的requests把缩略图分块写入磁盘,按艺术家命名,让署名始终附着。 - 作品是有版权的。守在公开、无门控的页面上,未经许可绝不再分发、转售下载的图片或拿它来训练,并对任何超出个人或研究用途的情形使用官方的 DeviantArt OAuth API。
常见问题
为什么普通抓取从 DeviantArt 返回不到作品?
因为 DeviantArt 用 JavaScript 在客户端构建它的搜索网格。最初的 HTML 是一个空壳,要等页面脚本在浏览器中运行后才会填充,所以一个原始的 HTTP 请求返回状态 200,而网格是空的。要拿到真实数据,你必须先渲染页面,而这正是 Crawling API 的 JS token 替你处理的事。
对 DeviantArt 我该用普通 token 还是 JS token?
JS token。普通 token 获取静态 HTML,而在 DeviantArt 的搜索页上那就是普通抓取返回的同一个空壳。JS token 会先在真实浏览器中渲染页面再交回 HTML,所以当 BeautifulSoup 解析时缩略图及其元数据都在。
我该如何抓取超出第一页的结果?
DeviantArt 用一个 &page= 参数为搜索分页。在你需要的页码上循环,为每一页构建一个像 /search?q=fantasy&page=2 这样的 URL,通过 Crawling API 获取它,并运行同一个解析器。请求之间保留一个短暂的 sleep,让一次多页运行不至于猛打站点。
我的选择器对每张卡片都返回 None。是什么变了?
几乎可以肯定是 DeviantArt 的标记变了。它的类名和内部属性会在没有任何通知的情况下变化,所以上个月还能用的选择器可能会失效。schema 钩子 data-hook="deviation_link" 和 property="contentUrl" 是最耐久的,但当某个字段返回为空时,请在浏览器开发者工具里重新检视一张实时缩略图并更新选择器。
我能重用或出售从 DeviantArt 下载的图片吗?
不能,未经艺术家许可不行。每件作品都由其创作者拥有版权,所以下载一个公开的缩略图并不授予你再分发、再发布、转售或训练它的许可。把下载限定在个人、研究或内部研究,为艺术家署名,并对任何再利用通过官方渠道取得明确许可或使用一份授权安排。
有没有我应该改用的官方 DeviantArt API?
有。DeviantArt 提供一个 OAuth API,它在平台直接授予的条款下暴露作品及其元数据。对于任何超出随意个人使用的情形,尤其是商业项目,官方的 DeviantArt OAuth API 是受认可的途径,也是让你站在服务条款与版权两者正确一边的那一个。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
