news 2026/8/31 17:06:12

Python爬虫实战:基于Scrapy框架的抖音公开数据采集与反爬应对

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:基于Scrapy框架的抖音公开数据采集与反爬应对

简介:本资源是一个面向Python初学者的Scrapy框架实战项目,聚焦抖音平台公开数据的采集与结构化存储,适用于刚掌握基础语法、希望通过真实场景理解分布式爬虫设计逻辑的学习者。项目通过抓取抖音搜索页的“热门挑战”与“热门音乐”入口,按参与人数筛选高热度话题并递进抓取关联视频信息,最终存入MongoDB,配合APScheduler实现每日凌晨定时更新,虽未覆盖全量数据但完整呈现了从目标分析、请求构造、中间件配置到数据管道落地的全流程开发链路。压缩包共29个文件,含14个核心Python源码(如spiders、pipelines、settings等模块)、5个XML配置文件(IDE及项目元数据)、1个README说明文档和1个LICENSE协议文件,整体仅30KB,轻量易读。已有1214人学习下载,读者可直接运行调试,深入理解User-Agent轮换、反爬应对策略、MongoDB异步写入及Scrapy项目目录组织规范。 最近翻到一个以前整理的抖音数据采集项目源码包,标题写着“基于python和scrapy框架的抖音数据爬虫项目源码.zip”,自己都愣了一下——这东西居然还在硬盘里躺着。想了想,干脆把它重新梳理一遍,顺便把整个设计思路、踩坑记录和可以复用的代码片段都写出来。如果你正准备用 Python 和 Scrapy 做短视频平台的数据采集,或者已经在做但总被各种反爬问题折磨,这篇文章应该能省你不少时间。

先说明一点:这个项目定位是采集公开可访问的数据,例如某个话题下的公开视频信息、公开评论内容、用户主页展示的基础资料等。所有涉及账号登录态、隐私数据、平台付费内容的部分,从一开始就做了隔离处理,项目里没有也不打算加上这些能力。技术本身是中性的,但使用边界得自己守住。

1. 这个项目到底在做什么:需求拆解与技术选型

1.1 需求拆解:采集"公开数据"而不是"破解数据"

动手写代码之前,最忌讳的就是拿到一个“爬抖音”的需求就直接开始写。你得先把需求拆清楚。我当初接到的原始需求是:给一个做短视频趋势分析的团队提供数据支撑,他们需要知道某些热门话题下视频的标题、作者昵称、点赞数、评论数、发布时间、视频无水印播放地址这些字段,用于做竞品分析和内容选题参考。

这里有个关键点:这些数据在 Web 端、分享页面上是公开展示的,任何人打开网页都能看到,采集这类公开信息并不涉及对平台安全机制的绕过。但如果你想要的数据是“某个用户设置了私密的收藏列表”“某个直播间的高热度实时数据”“付费课程的内容”,那就不属于这个项目的范围了。技术手段再强,也不能去碰这些边界。

拆解出具体字段后,还要想清楚采集频率和规模。在我这个项目里,目标是每天采集 5000 到 10000 条公开视频信息,单次任务运行时间控制在 2 小时以内,对实时性要求不高,但对稳定性和去重率要求比较高。这就决定了后面的技术选型。

1.2 为什么是 Python + Scrapy,而不是 requests + 手写循环

很多人一开始会写一个 requests 脚本,循环请求、解析、保存,看起来简单直接。但一旦需要采集的数据量上来,你会立刻遇到这些问题:

  • 请求失败重试逻辑得自己写,而且容易写得很糙;
  • 并发控制稍不注意就会被封;
  • 爬虫跑挂了没人知道,重启后从头来;
  • 数据去重、管道清洗、入库逻辑全塞在一个脚本里,维护成本爆炸。

Scrapy 解决的就是这些“工程化”问题。它的核心优势在于:

  1. 异步并发机制:Scrapy 底层基于 Twisted 异步框架,默认的并发数是 16,也就是说它可以在同一个进程里同时处理 16 个请求,而不是像普通 requests 脚本那样逐个等待。这个效率差距在数据量大了之后非常明显。
  2. 中间件体系:下载中间件可以统一处理 UA 轮换、代理切换、重试、请求头定制,不需要散落在业务代码里。
  3. Item Pipeline 管道:数据清洗、去重、入库可以拆成一个个独立的 Pipeline,代码结构清晰,后面加新功能只需要加一个 Pipeline 类。
  4. 内置去重机制:利用 scrapy 的 RFPDupeFilter 可以根据请求 URL 做指纹去重,大部分场景下够用了。

所以我的结论是:如果你只是临时跑几十条数据,requests 完全够;但如果是要持续、稳定、可维护地采集,直接上 Scrapy 是值得的,后面省下的维护时间远远超过你学框架的时间。

1.3 整体架构和技术栈

这个项目的最终架构是这样组织的:

模块选择说明
核心框架Scrapy 2.5+负责调度、抓取、解析核心流程
数据解析Scrapy Selector + 正则结合 XPath 和 CSS 选择器,特殊字段用正则处理
动态内容Playwright 按需接管只有极少数页面是纯 JS 渲染时才启用,日常不用
数据存储MySQL 5.7 / 8.0用于存储结构化数据,方便分析团队直接查库
去重存储Redis使用 scrapy-redis 的去重指纹模块做增量去重
定时调度crontab + shell 脚本每天凌晨执行一次增量采集任务
监控告警钉钉机器人 Webhook任务异常时发送通知

这套组合的好处是:Scrapy 负责主要抓取,Redis 负责去重和分布式扩展,MySQL 负责存储,每个组件都是围绕“稳定采集”这个目标服务的。后面所有的开发都是在这个骨架上往里填肉。

2. Scrapy 爬虫核心链路拆解:从请求到入库

2.1 抓包与接口分析:先看数据从哪来

做任何爬虫,第一步都不是写代码,而是搞清楚页面数据到底是怎么加载出来的。打开浏览器开发者工具,切到 Network 面板,刷新页面,你会看到大量请求。我通常会先过滤出 XHR/Fetch 请求,因为这些往往就是数据的真正来源。

抖音 Web 端和其他大多数现代站点一样,采用的是接口返回 JSON 的方式。页面上你看到的“标题、点赞数、评论数”,很多都来自一个固定的 API 接口,返回结构是标准的 JSON,包含视频描述、统计信息、作者信息、视频地址等。找到这个接口以后,不要急着写代码,先在浏览器里把请求头完整复制下来,逐个字段去理解。

我一般会重点看这几个字段:

  • User-Agent:客户端标识,平台端会用它判断请求来自浏览器还是脚本;
  • Referer:来源页面,很多接口会校验这个字段是否合法;
  • Cookie:如果是需要登录态的数据,Cookie 必不可少,但前面说了,这个项目不碰登录态;
  • Request Method:是 GET 还是 POST,参数怎么传的。

这里要强调一个细节:浏览器调试面板里看到的请求头顺序并没什么影响,但请求头的完整性很重要。少了 Referer,或者 UA 用的是默认 Python-requests 的 UA,大概率直接返回异常。下面这段代码是我在项目里实际用的请求头配置方式:

def get_default_headers() -> dict: return { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/122.0.0.0 Safari/537.36" ), "Referer": "https://www.douyin.com/", "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", }

另外,部分接口的响应是加密或混淆过的。遇到这种情况,我建议你先在 Network 面板里找到发起请求的 JS 文件,断点调试一下看看参数是怎么生成的。但提醒一句:如果你的目的是正常的数据分析,而不是专门研究逆向,遇到无解的加密参数时最好绕开,不要死磕。一个项目里总有替代方案,比如换一个数据源页面,或者直接用 Playwright 渲染后取值。我在这个项目里就遇到过解析参数的情况,后面会专门说。

2.2 爬虫主体代码实现与请求头处理

Scrapy 的爬虫类继承自scrapy.Spider,核心方法是start_requests()parse()start_requests()负责生成初始请求,parse()负责解析响应。

这个项目的爬虫主流程是这样的:

import scrapy from scrapy.http import JsonRequest class DouyinSpider(scrapy.Spider): name = "douyin_spider" def start_requests(self): # 从任务队列中读取要采集的话题或用户ID tasks = self.settings.get("TASK_LIST", []) for task in tasks: api_url = task["api_url"] yield JsonRequest( url=api_url, headers=self.get_headers(), callback=self.parse, dont_filter=False, errback=self.handle_error, ) def parse(self, response): data = response.json() video_list = data.get("data", {}).get("list", []) if not video_list: self.logger.warning("当前请求未返回视频列表,可能触发风控") return for item in video_list: yield self.extract_video_item(item) # 如果还有下一页,继续请求 next_cursor = data.get("data", {}).get("cursor", "") if next_cursor: yield JsonRequest( url=self.build_next_url(next_cursor), headers=self.get_headers(), callback=self.parse, )

有几个注意点:

  1. JsonRequest 比 Request 更适合接口请求,因为它会默认把 Content-Type 设置为 application/json,有些后端接口会校验这个头。
  2. dont_filter 要按场景设置。对于翻页请求,URL 中 cursor 参数不同,不会重复;但对于某些返回同样 URL 的请求,需要思考是否需要去重。
  3. errback 一定要写。没有错误回调的爬虫,请求失败后你只能通过日志找问题,调试体验非常痛苦。我在 errback 里通常加上对 Response 状态码的判断,同时对超时类异常单独记录。

在解析字段时,我会把每个字段的获取代码独立成函数。例如获取视频封面图:

def extract_cover_url(self, item: dict) -> str: try: cover_url = item["video"]["cover"]["url_list"][0] except (KeyError, IndexError, TypeError): cover_url = "" return cover_url

这样做的目的是避免某个字段解析异常导致整个 item 失败。数据采集项目里,脏数据是常态,防御性编程非常关键。

2.3 中间件:UA池、延迟与重试策略

Scrapy 的下载中间件(Downloader Middleware)是在请求发送给下载器之前、响应返回给爬虫之前执行的钩子。它在整个框架里属于“兵家必争之地”,很多爬虫稳定性问题都出在这里。

我这个项目里写了三个中间件,分别负责 UA 轮换、代理切换、请求延迟控制。先看 UA 轮换的代码:

import random from scrapy import signals class RandomUserAgentMiddleware: USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/121.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Edg/122.0", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36", ] def process_request(self, request, spider): request.headers["User-Agent"] = random.choice(self.USER_AGENTS) return None

UA 池数量不需要太多,关键是不同 UA 的浏览器版本、操作系统不要全都一样,最好是 Chrome/Edge/Firefox 混着来。我第一次做的时候从网上找了一堆乱七八糟的 UA 列表,结果反而因为 UA 和系统环境不匹配被识别出来。现在只用这几个常用的,稳定很多。

请求延迟控制也很重要。Scrapy 本身有DOWNLOAD_DELAY配置,但它是固定延迟,不够灵活。我写了一个带随机抖动的延迟中间件:

class RandomDelayMiddleware: def __init__(self, min_delay, max_delay): self.min_delay = min_delay self.max_delay = max_delay @classmethod def from_crawler(cls, crawler): return cls( min_delay=crawler.settings.getfloat("RANDOM_DELAY_MIN", 1.0), max_delay=crawler.settings.getfloat("RANDOM_DELAY_MAX", 3.0), ) def process_request(self, request, spider): delay = random.uniform(self.min_delay, self.max_delay) spider.logger.debug(f"当前请求延迟 {delay:.2f}s") time.sleep(delay) return None

之所以用随机延迟,是因为固定频率的请求在服务端看来很有规律,容易被识别;而完全无延迟的高并发又会压垮对方服务器。随机延时是在效率和安全性之间的折中方案。实际项目中,我把延迟范围设在 1 到 3 秒之间,单机单 IP 每天跑几千条数据没太大问题。

3. 数据解析与存储设计:字段、去重与增量更新

3.1 视频信息字段的抽取思路

拿到接口返回的 JSON 后,第一件事是把它存成 JSON 文件留档,然后再开始写解析逻辑。这样做的好处是,如果后面字段拿错了,可以直接对照原始数据排查,不用再重新发一次请求。

我在这个项目里定的字段表如下:

字段名类型说明
aweme_idstring视频唯一 ID
titlestring视频描述文字
author_nicknamestring作者昵称
author_uidstring作者唯一 ID
like_countint点赞数
comment_countint评论数
share_countint分享数
create_timedatetime发布时间
video_play_urlstring无水印播放地址
video_cover_urlstring封面图地址
topic_liststring关联话题,逗号分隔
crawl_timedatetime抓取时间

其中video_play_url是从接口返回的地址里进一步解析出来的。很多情况下,接口返回的播放地址可能带有额外的 token 参数,这些参数会过期。因此存储时需要把完整地址存下来,同时也存一份纯地址,后面做数据清洗时再修正。

字段抽取有几个容易踩的坑:

  • like_count这类数字字段,接口里可能直接返回字符串,也可能是数字,需要统一转成 int;
  • 作者信息有时候是嵌套对象,直接取不到,要先做存在性判断;
  • 话题列表可能是[{"hashtag_name": "xxx"}]这种结构,需要先提取再进行拼接。

我在项目里封装了一个extract_video_item方法,专门处理这些坑:

def extract_video_item(self, item: dict) -> dict: author_info = item.get("author", {}) or {} topic_list_raw = item.get("text_extra", []) or [] topic_names = [ topic.get("hashtag_name", "") for topic in topic_list_raw if topic.get("hashtag_name") ] return { "aweme_id": item.get("aweme_id", ""), "title": item.get("desc", "").strip(), "author_nickname": author_info.get("nickname", ""), "author_uid": author_info.get("uid", ""), "like_count": int(item.get("statistics", {}).get("digg_count", 0) or 0), "comment_count": int(item.get("statistics", {}).get("comment_count", 0) or 0), "share_count": int(item.get("statistics", {}).get("share_count", 0) or 0), "create_time": datetime.fromtimestamp(item.get("create_time", 0)), "video_play_url": self.extract_play_url(item.get("video", {})), "video_cover_url": self.extract_cover_url(item.get("video", {})), "topic_list": ",".join(topic_names), "crawl_time": datetime.now(), }

注意看,我在每个嵌套字段上都做了空值兜底。你不确定接口什么情况下会缺字段,所以最稳妥的方式是“把它当成一个随时会缺胳膊少腿的对象来处理”。

3.2 Item Pipeline 与 MySQL 存储

Scrapy 的 Item Pipeline 是一条流水线,item 会依次经过你定义的每个 Pipeline,每个 Pipeline 可以决定是继续传递还是丢弃。我把它切成三段:去重 Pipeline、清洗 Pipeline、存储 Pipeline。

看存储 Pipeline 的实现,这是这个项目里最基础的代码之一:

import pymysql from twisted.enterprise import adbapi class MySQLStorePipeline: def __init__(self, db_pool): self.db_pool = db_pool @classmethod def from_crawler(cls, crawler): db_config = { "host": crawler.settings.get("MYSQL_HOST", "localhost"), "port": crawler.settings.getint("MYSQL_PORT", 3306), "user": crawler.settings.get("MYSQL_USER", "root"), "password": crawler.settings.get("MYSQL_PASSWORD", ""), "database": crawler.settings.get("MYSQL_DB", "douyin"), "charset": "utf8mb4", } db_pool = adbapi.ConnectionPool("pymysql", **db_config) return cls(db_pool) def process_item(self, item, spider): # 使用线程池异步执行 SQL,避免阻塞爬虫主流程 query = """ INSERT INTO video_info (aweme_id, title, author_nickname, author_uid, like_count, comment_count, share_count, create_time, video_play_url, video_cover_url, topic_list, crawl_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ params = ( item["aweme_id"], item["title"], item["author_nickname"], item["author_uid"], item["like_count"], item["comment_count"], item["share_count"], item["create_time"], item["video_play_url"], item["video_cover_url"], item["topic_list"], item["crawl_time"], ) return self.db_pool.runQuery(query, params)

我用了adbapi.ConnectionPool,这是 Twisted 提供给异步程序访问数据库的连接池方式。如果不这么做,在 Pipeline 里直接用 pymysql 执行同步 SQL 会阻塞事件循环,导致爬虫性能明显下降。

建表 SQL 长这样:

CREATE TABLE IF NOT EXISTS video_info ( id INT AUTO_INCREMENT PRIMARY KEY, aweme_id VARCHAR(64) NOT NULL, title VARCHAR(1024) DEFAULT '', author_nickname VARCHAR(255) DEFAULT '', author_uid VARCHAR(64) DEFAULT '', like_count INT DEFAULT 0, comment_count INT DEFAULT 0, share_count INT DEFAULT 0, create_time DATETIME DEFAULT NULL, video_play_url TEXT, video_cover_url TEXT, topic_list VARCHAR(2048) DEFAULT '', crawl_time DATETIME DEFAULT NULL, UNIQUE KEY uk_aweme_id (aweme_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

uk_aweme_id唯一键很重要。即使应用层去重没兜住,数据库层也能挡住重复数据,相当于多了一层防线。

3.3 去重策略与增量采集设计

采集任务最怕重复数据。同一个视频第一次采集的时候没有人知道,第二次再采的时候如果不做去重,数据库里就会出现大量重复记录,分析结果也会失真。

这个项目用的是“Redis 指纹 + Scrapy 默认去重”双保险。Scrapy 的RFPDupeFilter会对请求 URL 做 SHA1 指纹,如果 URL 相同则不再请求。但这里有个问题:抖音的翻页接口 URL 带有 cursor 参数,每次翻页 URL 都不一样,所以针对视频本身的去重不能依赖 URL,要在 Item 层面做。

我在去重 Pipeline 里用 Redis 的SADD命令,把视频aweme_id作为成员加入 Set 集合。如果返回值是 0,说明这个 ID 已经存在,就丢给DropItem异常处理:

from scrapy.exceptions import DropItem import redis class DuplicatePipeline: def __init__(self, redis_client): self.redis_client = redis_client @classmethod def from_crawler(cls, crawler): redis_client = redis.Redis( host=crawler.settings.get("REDIS_HOST", "localhost"), port=crawler.settings.getint("REDIS_PORT", 6379), db=crawler.settings.getint("REDIS_DB", 0), decode_responses=True, ) return cls(redis_client) def process_item(self, item, spider): added = self.redis_client.sadd("douyin:video_ids", item["aweme_id"]) if added == 0: raise DropItem(f"重复视频: {item['aweme_id']}") return item

增量采集的核心思想就是:每次任务启动时,只需要从上次采集的结束位置继续,而不是每次都从第一页开始。我把每个任务的起始 cursor 存在 Redis 的 Hash 里,每次采集完成后更新:

def save_task_cursor(self, task_name: str, cursor: str): self.redis_client.hset("douyin:task_cursors", task_name, cursor) def get_task_cursor(self, task_name: str) -> str: return self.redis_client.hget("douyin:task_cursors", task_name) or "0"

这样配合 crontab 定时任务,每天就能稳定地增量采集新数据,而不是每次全量扫描。

4. 反爬机制的规律性认识与合规底线

4.1 常见的反爬手段和对应的合理策略

任何有一定规模的平台都会做反爬,抖音也不例外。但反爬不是铁板一块,它是有规律可循的。这个项目实际踩过的问题,我整理成了下面几个大类,以及对应的处理策略:

反爬手段现象合理的应对思路
请求头校验返回 403 或空数据伪装完整浏览器请求头,尤其是 UA、Referer
频率限制请求过快后返回验证页随机延迟、限制并发数、任务分散到不同时间段
IP 封禁后端返回“访问异常”降低单 IP 请求量,必要时使用代理池(注意合规性)
签名参数接口参数包含加密字段分析 JS 生成逻辑,或改用渲染方式直接取数据
数据接口改版原有字段消失或返回格式变化建立接口监控,定期检查字段兼容性

这里要强调:我没有用任何“绕过验证码”“破解签名”的手段。遇到滑块验证这类强交互验证,我的策略是停止当前任务,降低频率,等待风控解除。因为这已经超出正常数据采集的边界了,继续硬刚得不偿失。

4.2 这些操作绝对不能碰

做爬虫的人多少都会遇到一些“灰色手段”,我给自己的项目立了几条红线,这些红线也是你如果要在公开展示源码时需要守住的底线:

  1. 不采集私密账号数据。设置为私密的账号,其内容本来就不打算对外公开展示,强行去拿属于越权。
  2. 不绕过登录态。平台要求登录才能看的内容,要么走官方开放 API,要么不要。不要用“模拟登录 + 保持 Cookie”的方式强行抓取需要登录才能访问的数据。
  3. 不破解验证码、不操作滑块。这些属于平台明确的风控对抗行为,一旦做了,就不仅仅是技术问题了。
  4. 不用于商业不正当竞争。采集到的公开数据如果用于抄袭、抹黑、批量搬运等行为,同样有法律风险。
  5. 不采集用户敏感个人信息。虽然公开页面可能有昵称和头像,但不要刻意去拼接、挖掘手机号、住址等隐私数据。

写爬虫的人应该比谁都清楚“数据是把双刃剑”。我把这些写进项目的 README 里,代码仓库里也放了一份《数据使用合规说明》。这不是做样子,而是真的在提醒自己:技术能力越强,越要谨慎。

4.3 合规红线:robots、频率与数据使用边界

合规不仅仅是“不碰私密数据”。即使你采集的数据全是公开的,也要考虑以下几个维度:

  • robots 协议:虽然抖音的 robots.txt 可能没有明确允许爬虫,但作为从业者,你应该主动查看并理解站点声明。这里我不去深究法律定性,只说常识:即便技术上能爬到,也不代表应该高频抓取。
  • 请求频率:不要让你的爬虫给对方服务器造成明显压力。正常浏览页面的频率是几秒一次,你的爬虫如果每秒钟发十几个请求,那就是另一种性质了。我在项目里设置的合理目标是:单 IP 每秒不超过 0.5 个请求,单日总量不超过一万。
  • 数据使用边界:采集到的公开数据可以用于个人学习、学术研究,但如果要对外发布或商用,一定要确认数据的版权归属,尤其是视频内容和用户创作。很多人只关心“能不能爬到”,忽略了“爬到之后能不能用”,等收到侵权通知就晚了。
  • 尊重“别碰”的提示:当接口开始返回风控页面或要求验证时,这就是平台在告诉你“你太快了、该停一下了”。正确做法是停下来,而不是去研究怎么绕过。我项目里的风控检测逻辑遇到这种情况会自动暂停任务并发送告警,而不是无限重试。

合规风险不是一篇文章能讲完的,但只要你守住上面几条,大部分坑都能避开。

5. 实操中踩过的坑与排查思路

5.1 请求超时与IP封禁的排查

这个项目最开始在本地跑的时候,经常出现请求超时。日志里全是twisted.web._newclient.TimeoutError: User timeout caused。第一反应是 IP 被限制了,但其实很多时候并不是,而是并发数太高加上网络抖动导致的。

排查步骤我整理成了固定的流程:

  1. 先用命令行 curl 模拟同一接口,看是否能正常返回。如果能,说明服务端没封你,问题出在爬虫配置。
  2. 看 Scrapy 日志里的下载失败统计。scrapy crawl douyin_spider -s DOWNLOAD_TIMEOUT=15可以设置超时时间,但不要设置得太长,否则单次卡住会占住并发槽位。
  3. 检查CONCURRENT_REQUESTS配置。我一开始为了追求速度,把它调到 32,结果频繁超时,降到 8 之后稳定很多。
  4. 看是否触发了代理。如果你通过代理池发请求,代理本身不稳定也会导致超时,这种情况要把超时时间定在 10 到 15 秒,并增加重试次数。

另外要注意,Scrapy 默认重试只会重试某些状态码。对于“返回 200 但实际是风控页面”的情况,重试机制是不生效的,必须自己在解析层判断。我在 parse 里面加了这样的逻辑:

if data.get("status_code") != 0 or "验证" in response.text: spider.crawler.engine.close_spider(self, reason="trigger_risk_control")

这样一旦检测到风控,就停止爬虫,避免继续请求造成更严重的后果。

5.2 数据字段缺失与动态渲染

有一次我采集到的数据里,很多视频的封面图字段是空的。排查了一遍,发现接口在某些情况下不会返回video.cover,但在详情页里能看到封面。这就涉及到动态渲染问题。

抖音的 Web 端页面其实分两种:一种是接口直接返回的完整 JSON,一种是需要 JS 执行后才会渲染 DOM 的页面。如果你想从 DOM 里取数据,直接用 Scrapy 的 XPath 是拿不到的,因为 Scrapy 不具备执行 JS 的能力。这种情况下,我引入了 Playwright 来兜底,但只在 Scrapy 解析不到数据时才启用。

基本思路是:在下载中间件里判断当前请求是否属于“需要渲染”的 URL,如果是,则交给 Playwright 加载页面,等 Network 空闲后直接返回渲染后的 HTML。但这种方式开销很大,一个页面加载可能要好几秒,所以我在项目里默认关闭,只有特定任务才开启。日常采集的接口型任务完全不用它。

5.3 调试技巧:scrapy shell 与日志级别

最后分享一个非常实用的调试技巧。很多人写 Scrapy 爬虫都是直接scrapy crawl,报错了就看日志,但这样效率太低。我的习惯是先用scrapy shell单步调试:

scrapy shell "https://www.douyin.com/"

进入 shell 后,你可以直接输入response.status查看返回码,用response.xpath(...)测试选择器,还能自由操作fetch()方法换 URL 调试。所有的解析逻辑,都应该先在这里验证通过,再写回爬虫代码里。

另外一个常用技巧是控制日志级别。Scrapy 默认的日志级别是 INFO,调试时把它调成 DEBUG 能看到更多请求细节:

scrapy crawl douyin_spider -s LOG_LEVEL=DEBUG

但生产环境跑定时任务时,我坚持用 INFO,并且只保留 Error 级别的日志到文件,避免日志文件膨胀太快。日志配置在settings.py里这样写:

LOG_LEVEL = "INFO" LOG_FILE = "logs/scrapy.log" LOG_STDOUT = False

如果你遇到解析字段死活取不到的情况,不要反复改代码跑全量,直接在 shell 里先确认 response.text 是否符合预期,再一步步找 XPath 或者 JSON 路径,这样能省至少一半的排查时间。

做这种项目,我的经验是:先把能跑通的“最小版本”做出来,再逐步加功能。一开始就追求什么都要,往往会陷入接口分析、反爬对抗的泥潭里出不来。我最初也只是先采集基本的视频 ID 和标题,跑通了之后才一步步加评论数、封面、话题这些字段。每一步加完都跑一遍验证,确保改动不会破坏前面已有的功能。

技术上的细节,我在项目源码包的 README 里写得更全,包括完整的依赖列表、MySQL 建表语句、Scrapy 配置说明、常见异常对照表。如果你也要做一个类似的采集项目,建议先把这篇文章里提到的几个重点——请求头完整性、延迟策略、去重机制、数据库唯一键、日志规范——梳理清楚,再开始动代码。这些地方做得扎实,你的爬虫才能真正从“能跑”进化到“能长期稳定地跑”。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 17:06:05

AI智能体Hermes桌面端全自动安装与测试指南

这次我们来看 Hermes 的桌面端安装。最近关于 Hermes、DeepSeek Hermes、Hermes Agent 的讨论热度不低,不少人把它和 Codex 桌面端放在一起对比。核心关注点其实就几个:它到底解决什么问题,“全自动安装”是不是真的省事,装完之后…

作者头像 李华
网站建设 2026/8/31 17:04:53

航模飞机图纸大全:从图纸预处理到飞行调试的全流程实战指南

简介:本资源是一套面向机械设计、飞行器工程、航空航天及相关专业学习者与航模爱好者的高质量航模飞机图纸合集,涵盖从基础练习到进阶建模的完整技术参考,有效解决三维建模、结构分析、手工制作及飞行原理验证等实践需求。压缩包共包含2000个…

作者头像 李华
网站建设 2026/8/31 17:04:36

构建产物交付包部署全攻略:从解压到稳定运行

简介:本资源是一组专为Cesium平台优化的厦门3D建筑物测试数据,面向地理信息、Web三维可视化及数字孪生领域的开发者与学习者,用于快速掌握3DTiles格式加载、大规模建筑模型渲染与性能调优等核心技能。压缩包共109个文件,含108个.b…

作者头像 李华
网站建设 2026/8/31 17:04:14

DSP28335数字电源LLC谐振变换器软启动程序设计与实现

简介:本资源是一套面向电力电子工程师与嵌入式开发者、专为TMS320F28335 DSP平台设计的全桥LLC谐振变换器数字控制软启动程序,解决高频开关电源启动过程中电流冲击大、器件应力高、系统易震荡等工程痛点。压缩包共151个文件,含9个核心C源码、…

作者头像 李华
网站建设 2026/8/31 17:04:02

多单元混合架构与六分频技术:全频音质拉满的声学设计逻辑

Maven III 的核心设计思路很直接:用四种单元混合架构加六分频,去解决“全频音质拉到高水准”这个 HiFi 设备最难回答的问题。多单元与多分频在高端耳机和桌面音箱里已经不新鲜,但真正能把它们做好的产品并不多。关键不只在于堆单元&#xff0…

作者头像 李华
网站建设 2026/8/31 17:03:55

从树莓派小车到数据清洗:具身智能入门实践路线

过去一年,具身智能从一个偏学术的概念,快速变成一级市场最拥挤的赛道之一。多支由高校教授创办的团队先后完成大额融资,公开报道中提及的融资总额已经达到百亿元量级。很多人因此产生一个印象:具身智能是一门好生意。但对正在学习…

作者头像 李华