
简介本资源是一个面向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 解决的就是这些“工程化”问题。它的核心优势在于异步并发机制Scrapy 底层基于 Twisted 异步框架默认的并发数是 16也就是说它可以在同一个进程里同时处理 16 个请求而不是像普通 requests 脚本那样逐个等待。这个效率差距在数据量大了之后非常明显。中间件体系下载中间件可以统一处理 UA 轮换、代理切换、重试、请求头定制不需要散落在业务代码里。Item Pipeline 管道数据清洗、去重、入库可以拆成一个个独立的 Pipeline代码结构清晰后面加新功能只需要加一个 Pipeline 类。内置去重机制利用 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;q0.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( urlapi_url, headersself.get_headers(), callbackself.parse, dont_filterFalse, errbackself.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( urlself.build_next_url(next_cursor), headersself.get_headers(), callbackself.parse, )有几个注意点JsonRequest 比 Request 更适合接口请求因为它会默认把 Content-Type 设置为 application/json有些后端接口会校验这个头。dont_filter 要按场景设置。对于翻页请求URL 中 cursor 参数不同不会重复但对于某些返回同样 URL 的请求需要思考是否需要去重。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 NoneUA 池数量不需要太多关键是不同 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_delaycrawler.settings.getfloat(RANDOM_DELAY_MIN, 1.0), max_delaycrawler.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视频唯一 IDtitlestring视频描述文字author_nicknamestring作者昵称author_uidstring作者唯一 IDlike_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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( hostcrawler.settings.get(REDIS_HOST, localhost), portcrawler.settings.getint(REDIS_PORT, 6379), dbcrawler.settings.getint(REDIS_DB, 0), decode_responsesTrue, ) 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 这些操作绝对不能碰做爬虫的人多少都会遇到一些“灰色手段”我给自己的项目立了几条红线这些红线也是你如果要在公开展示源码时需要守住的底线不采集私密账号数据。设置为私密的账号其内容本来就不打算对外公开展示强行去拿属于越权。不绕过登录态。平台要求登录才能看的内容要么走官方开放 API要么不要。不要用“模拟登录 保持 Cookie”的方式强行抓取需要登录才能访问的数据。不破解验证码、不操作滑块。这些属于平台明确的风控对抗行为一旦做了就不仅仅是技术问题了。不用于商业不正当竞争。采集到的公开数据如果用于抄袭、抹黑、批量搬运等行为同样有法律风险。不采集用户敏感个人信息。虽然公开页面可能有昵称和头像但不要刻意去拼接、挖掘手机号、住址等隐私数据。写爬虫的人应该比谁都清楚“数据是把双刃剑”。我把这些写进项目的 README 里代码仓库里也放了一份《数据使用合规说明》。这不是做样子而是真的在提醒自己技术能力越强越要谨慎。4.3 合规红线robots、频率与数据使用边界合规不仅仅是“不碰私密数据”。即使你采集的数据全是公开的也要考虑以下几个维度robots 协议虽然抖音的 robots.txt 可能没有明确允许爬虫但作为从业者你应该主动查看并理解站点声明。这里我不去深究法律定性只说常识即便技术上能爬到也不代表应该高频抓取。请求频率不要让你的爬虫给对方服务器造成明显压力。正常浏览页面的频率是几秒一次你的爬虫如果每秒钟发十几个请求那就是另一种性质了。我在项目里设置的合理目标是单 IP 每秒不超过 0.5 个请求单日总量不超过一万。数据使用边界采集到的公开数据可以用于个人学习、学术研究但如果要对外发布或商用一定要确认数据的版权归属尤其是视频内容和用户创作。很多人只关心“能不能爬到”忽略了“爬到之后能不能用”等收到侵权通知就晚了。尊重“别碰”的提示当接口开始返回风控页面或要求验证时这就是平台在告诉你“你太快了、该停一下了”。正确做法是停下来而不是去研究怎么绕过。我项目里的风控检测逻辑遇到这种情况会自动暂停任务并发送告警而不是无限重试。合规风险不是一篇文章能讲完的但只要你守住上面几条大部分坑都能避开。5. 实操中踩过的坑与排查思路5.1 请求超时与IP封禁的排查这个项目最开始在本地跑的时候经常出现请求超时。日志里全是twisted.web._newclient.TimeoutError: User timeout caused。第一反应是 IP 被限制了但其实很多时候并不是而是并发数太高加上网络抖动导致的。排查步骤我整理成了固定的流程先用命令行 curl 模拟同一接口看是否能正常返回。如果能说明服务端没封你问题出在爬虫配置。看 Scrapy 日志里的下载失败统计。scrapy crawl douyin_spider -s DOWNLOAD_TIMEOUT15可以设置超时时间但不要设置得太长否则单次卡住会占住并发槽位。检查CONCURRENT_REQUESTS配置。我一开始为了追求速度把它调到 32结果频繁超时降到 8 之后稳定很多。看是否触发了代理。如果你通过代理池发请求代理本身不稳定也会导致超时这种情况要把超时时间定在 10 到 15 秒并增加重试次数。另外要注意Scrapy 默认重试只会重试某些状态码。对于“返回 200 但实际是风控页面”的情况重试机制是不生效的必须自己在解析层判断。我在 parse 里面加了这样的逻辑if data.get(status_code) ! 0 or 验证 in response.text: spider.crawler.engine.close_spider(self, reasontrigger_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_LEVELDEBUG但生产环境跑定时任务时我坚持用 INFO并且只保留 Error 级别的日志到文件避免日志文件膨胀太快。日志配置在settings.py里这样写LOG_LEVEL INFO LOG_FILE logs/scrapy.log LOG_STDOUT False如果你遇到解析字段死活取不到的情况不要反复改代码跑全量直接在 shell 里先确认 response.text 是否符合预期再一步步找 XPath 或者 JSON 路径这样能省至少一半的排查时间。做这种项目我的经验是先把能跑通的“最小版本”做出来再逐步加功能。一开始就追求什么都要往往会陷入接口分析、反爬对抗的泥潭里出不来。我最初也只是先采集基本的视频 ID 和标题跑通了之后才一步步加评论数、封面、话题这些字段。每一步加完都跑一遍验证确保改动不会破坏前面已有的功能。技术上的细节我在项目源码包的 README 里写得更全包括完整的依赖列表、MySQL 建表语句、Scrapy 配置说明、常见异常对照表。如果你也要做一个类似的采集项目建议先把这篇文章里提到的几个重点——请求头完整性、延迟策略、去重机制、数据库唯一键、日志规范——梳理清楚再开始动代码。这些地方做得扎实你的爬虫才能真正从“能跑”进化到“能长期稳定地跑”。本文还有配套的精品资源点击获取