简介:这是一份面向Python初学者与Scrapy框架入门者的抖音数据采集练手项目源码,聚焦于理解动态网页数据抓取、分布式爬虫结构设计及定时任务集成等核心实践场景。资源包共29个文件,含14个核心Python脚本(如spiders/、pipelines.py、settings.py等构成完整Scrapy项目骨架)、5个XML配置文件(支撑IDEA开发环境与项目元数据管理)、1个README.md说明文档及LICENSE协议文件,整体仅30KB,轻量易读。已有1234人学习下载,适合在本地快速部署调试。读者可直接获得一个具备真实业务逻辑的爬虫工程:支持按“热门挑战”“热门音乐”两级链接抓取、视频数据结构化解析、MongoDB持久化存储,并通过APScheduler实现每日凌晨自动更新;代码中还内嵌user_agents轮换与基础中间件逻辑,为后续反爬进阶提供可扩展基线。
1. 抖音数据爬虫为什么不能只靠 Scrapy?——一个真实项目里被反复打脸的「静态假设」陷阱
你下载了名为基于python和scrapy框架的抖音数据爬虫项目源码.zip的压缩包,解压后看到scrapy.cfg、spiders/、items.py,甚至还有pip install scrapy的 README ——但运行起来却连首页都抓不到,或者只拿到一堆空 div 和window.__INITIAL_STATE__ = null。这不是你代码写错了,而是你掉进了抖音反爬最基础也最致命的认知陷阱:把抖音当普通静态网站来爬。抖音(含抖音极速版)从 2021 年起全面转向 SSR + CSR 混合渲染,核心 feed 流、用户主页、评论区全部由前端 JS 动态注入,且关键字段(如 video_id、aweme_id、signature)经多层混淆加密。Scrapy 本身不执行 JS,它拿到的只是初始 HTML 骨架,真正的数据藏在后续 XHR 请求或 WebSocket 帧里。这个项目标题里的「Scrapy」不是技术栈终点,而是整个链路的起点——它必须和 Playwright(或 Puppeteer、Selenium)协同工作,用 Scrapy 管理调度、去重、管道,用 Playwright 负责真实浏览器上下文中的页面加载、滚动触发、API 拦截与签名生成。适合两类人:一是想快速验证抖音公开页(如搜索页、话题页)结构的新手,二是已有 Scrapy 工程经验、正卡在「如何把动态渲染结果喂给 Scrapy Pipeline」的老手。本文不讲理论模型,只拆解一个能跑通、能调参、能进生产环境的真实路径。
2. 用 Scrapy + Playwright 在本地跑通抖音搜索页:最小可行命令与三步初始化
要让这个.zip项目真正动起来,第一步不是改 spider,而是重建运行时环境。很多用户解压即scrapy crawl douyin_spider,失败后反复重装 Scrapy,却忽略了抖音爬虫对底层驱动和 JS 执行环境的强依赖。以下是我在线上服务器和本地 M1 Mac 上均验证通过的初始化流程,全程无 GUI 依赖,适配 Linux/macOS/Windows(WSL2)。
2.1 初始化 Python 环境与核心依赖
提示:不要用
conda或全局 pip 安装 Playwright 相关包。Scrapy 与 Playwright 对 asyncio 事件循环、chromium 版本、SSL 证书处理存在隐式冲突,必须用虚拟环境隔离。
# 创建干净虚拟环境(Python 3.9–3.11 均可,推荐 3.10) python -m venv ./venv_douyin source ./venv_douyin/bin/activate # Linux/macOS # venv_douyin\Scripts\activate.bat # Windows # 先装 Scrapy(带 twisted 依赖) pip install --upgrade pip pip install scrapy==2.8.0 # 注意:2.9+ 对异步 pipeline 改动大,本项目适配 2.8.0 # 再装 Playwright 及其浏览器(关键!必须指定 chromium,且禁用 GUI) pip install playwright==1.40.0 # 与 Scrapy 2.8.0 兼容性最佳 playwright install --with-deps chromium # --with-deps 解决 libglib、libnss 等系统库缺失问题为什么选 Playwright 而非 Selenium?
- Selenium 启动 ChromeDriver 时需手动管理版本匹配,而 Playwright 自带浏览器二进制,
playwright install即完成; - Playwright 的
page.route()可直接拦截并修改 XHR 响应,比 Selenium 的requests拦截更底层; - Scrapy 的
CrawlerProcess与 Playwright 的asyncio事件循环可共存,而 Selenium 的WebDriver是阻塞式,易导致 Scrapy 的CONCURRENT_REQUESTS失效。
2.2 修改 settings.py:启用 Playwright 中间件并关闭默认下载器
原.zip包中settings.py通常只配置了USER_AGENT和ROBOTSTXT_OBEY = False,这远远不够。必须显式禁用 Scrapy 默认的HttpDownloadHandler,将请求交由 Playwright 处理:
# settings.py # --- 关键配置段 --- # 禁用默认下载器 DOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", } # 启用 Playwright 中间件(注意:必须在 DOWNLOADER_MIDDLEWARES 顶部) DOWNLOADER_MIDDLEWARES = { "scrapy_playwright.middlewares.PlaywrightMiddleware": 543, } # Playwright 专属配置 PLAYWRIGHT_BROWSER_TYPE = "chromium" PLAYWRIGHT_LAUNCH_OPTIONS = { "headless": True, # 必须为 True,否则无法在服务器运行 "args": [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage", "--disable-extensions", "--disable-background-networking", "--disable-default-apps", ], } # 设置超时(抖音页面加载慢,尤其首次) PLAYWRIGHT_DEFAULT_TIMEOUT = 30000 # 单位毫秒注意:scrapy-playwright是独立 PyPI 包,需额外安装:
pip install scrapy-playwright==0.0.10 # 严格对应 Scrapy 2.8.02.3 编写第一个可运行的 Spider:抓取抖音搜索页关键词结果
原.zip包中spiders/douyin_spider.py往往直接start_urls = ['https://www.douyin.com/search/xxx'],这是错误的起点。抖音搜索页 URL 会重定向到带?aid=...参数的地址,且真实数据由/api/search/item/接口返回。正确做法是:用 Playwright 加载搜索页 → 触发滚动加载更多 → 拦截/api/search/item/请求 → 提取 JSON 响应。
# spiders/douyin_search_spider.py import json import scrapy from scrapy_playwright.page import PageMethod class DouyinSearchSpider(scrapy.Spider): name = "douyin_search" def start_requests(self): # 搜索关键词:用 urlencode 处理中文,避免 URL 编码错误 keyword = "python教程" url = f"https://www.douyin.com/search/{keyword}?type=video" yield scrapy.Request( url=url, meta={ "playwright": True, "playwright_page_methods": [ # 等待搜索结果容器出现(抖音搜索页 DOM 特征) PageMethod("wait_for_selector", "div[data-testid='search-result-list']"), # 滚动到底部触发下一页(模拟用户行为) PageMethod("evaluate", "window.scrollTo(0, document.body.scrollHeight)"), # 等待新内容加载(关键!否则拿到的是旧数据) PageMethod("wait_for_timeout", 2000), ], "playwright_context_kwargs": { "ignore_https_errors": True, # 抖音部分接口走自签名证书 } }, callback=self.parse_search_results, ) def parse_search_results(self, response): # 此处 response.body 是完整 HTML,但我们要的是 XHR 数据 # 实际项目中,应在 Playwright 中拦截 /api/search/item/ 并存入 context # 这里简化:提取页面中已渲染的视频卡片(仅作验证) for item in response.css("div[data-testid='card-item']"): yield { "title": item.css("div[data-testid='video-desc']::text").get(), "author": item.css("a[href*='user'] span::text").get(), "duration": item.css("span[data-testid='video-duration']::text").get(), "url": response.urljoin(item.css("a::attr(href)").get()), }逻辑说明:
PageMethod("wait_for_selector", ...)确保 Playwright 等待目标 DOM 出现后再继续,避免因 JS 渲染延迟导致空抓;PageMethod("evaluate", ...)执行原生 JS 滚动,比page.mouse.wheel()更稳定;playwright_context_kwargs中的ignore_https_errors=True是必须项,抖音部分 API 使用内部 CA 签发证书;- 当前
parse_search_results只解析已渲染 DOM,实际生产环境应结合page.route()拦截/api/search/item/并response.json()提取原始数据,避免 HTML 解析失真。
3. 抖音签名(signature)生成:绕过_signature参数的三种落地方案
抖音所有核心接口(如/aweme/v1/web/aweme/post/,/aweme/v1/web/user/profile/preview/)都要求携带_signature参数,该参数由前端 JS 动态生成,包含时间戳、设备 ID、用户行为指纹等,且每 30 秒失效。这是.zip包中最常被注释掉或硬编码的「玄学模块」。不解决 signature,你的爬虫永远只能抓首页骨架。以下是三种可立即复用的方案,按稳定性排序:
3.1 方案一:复用抖音 Web 端 JS 签名算法(推荐新手)
抖音 Web 端 signature 生成逻辑已开源分析,核心函数generateSignature位于https://sf16-muse-va.ibytedtos.com/obj/muse-maliva-web/xxx.js(URL 动态变化)。我们不逆向,而是直接调用:
# utils/signature_generator.py import execjs import requests def get_douyin_signature(url: str, user_agent: str = None) -> str: """ 调用抖音官方 JS 生成 signature :param url: 目标请求 URL(不含 query 参数) :param user_agent: 浏览器 UA,影响 signature 生成 :return: _signature 字符串 """ # 获取最新 signature JS(缓存 1 小时,避免频繁请求) js_url = "https://sf16-muse-va.ibytedtos.com/obj/muse-maliva-web/maliva_web_1.0.0.js" js_content = requests.get(js_url, timeout=10).text # 编译 JS(需提前安装 nodejs) ctx = execjs.compile(js_content) # 调用 generateSignature 方法(参数顺序必须严格) sig = ctx.call("generateSignature", url, user_agent or "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36") return sig # 在 spider 中使用 def start_requests(self): base_url = "https://www.douyin.com/aweme/v1/web/aweme/post/" params = {"sec_uid": "MS4wLjABAAAA...", "count": "20", "max_cursor": "0"} url_with_params = base_url + "?" + "&".join([f"{k}={v}" for k, v in params.items()]) signature = get_douyin_signature(url_with_params) full_url = url_with_params + f"&_signature={signature}" yield scrapy.Request(url=full_url, callback=self.parse_aweme_list)参数说明:
url必须是完整 URL(含 query),因为 JS 中会url.split('?')[0]提取 path;user_agent必须与 Playwright 启动时一致,否则 signature 校验失败;execjs底层调用 Node.js,需确保系统已安装node -v≥ 16.x。
3.2 方案二:用 Playwright 注入 JS 并读取 window._signature(推荐熟手)
避免 Node.js 依赖,直接在浏览器上下文中执行:
# 在 spider 的 start_requests 中 yield scrapy.Request( url="https://www.douyin.com/", meta={ "playwright": True, "playwright_page_methods": [ PageMethod("evaluate", """ // 注入 signature 生成函数(精简版) window.genSig = function(url) { const t = Date.now(); const r = Math.floor(Math.random() * 1000); const e = 'douyin_web'; const n = btoa(url + t + r + e).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); return n + '_' + t; }; """), PageMethod("wait_for_timeout", 1000), ], }, callback=self.generate_signature_for_api, ) def generate_signature_for_api(self, response): # 在回调中获取 signature page = response.meta["playwright_page"] sig = page.evaluate("window.genSig('https://www.douyin.com/aweme/v1/web/aweme/post/?sec_uid=xxx')") # 构造真实请求...注意:此方案生成的 signature 仅为示意,真实抖音 signature 含更复杂混淆(如 canvas 指纹、WebGL 渲染特征),但对非高频请求已足够。
3.3 方案三:代理池 + 真实设备抓包(推荐高并发生产环境)
当 signature 失效频率升高(如每 10 秒),JS 方案会因执行延迟导致批量请求失败。此时应放弃「生成」,转为「复用」:
- 使用 Fiddler/Charles 抓取手机抖音 App 或 PC 客户端真实请求;
- 提取
X-Signature、X-Bogus、Cookie(尤其是msToken、odin_tt); - 构建代理池,每个代理绑定一组有效凭证,轮询使用;
- 用
scrapy.downloadermiddlewares.retry.RetryMiddleware配合RETRY_HTTP_CODES = [403, 412]自动切换代理。
提示:抖音对
X-Bogus的校验比_signature更严,必须同时携带。.zip包中若含bogus.py,大概率已过期,建议直接抓包替换。
4. 避坑:抖音爬虫的五个血泪经验,每一条都让我重装三次系统
4.1 现象:Scrapy 日志显示200 OK,但response.text是空字符串或{"status_code":10000,"status_msg":"验证失败"}
原因:未设置Referer或Origin头。抖音服务端校验请求来源,Referer必须为https://www.douyin.com/,Origin必须为https://www.douyin.com。
解决:在start_requests中显式添加:
yield scrapy.Request( url=full_url, headers={ "Referer": "https://www.douyin.com/", "Origin": "https://www.douyin.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", }, callback=self.parse_data, )4.2 现象:Playwright 启动报错Error: Failed to launch chromium because executable doesn't exist
原因:playwright install下载的 chromium 二进制被杀毒软件误删,或 WSL2 中/tmp权限不足。
解决:
- Linux/macOS:
playwright install chromium --force-download强制重装; - WSL2:在
~/.playwright目录下手动创建chromium-xxxxxx文件夹,并chmod 755; - Windows:以管理员身份运行
playwright install chromium。
4.3 现象:抓取用户主页时,response.css("div[data-testid='user-bio']::text").get()总是None
原因:抖音用户主页采用「骨架屏 + 懒加载」,生物信息(bio)、粉丝数等字段在window.__INITIAL_STATE__中,但该对象被 JS 动态覆写,Scrapy 拿到的是初始空值。
解决:改用 Playwright 提取page.evaluate("window.__INITIAL_STATE__"),再用 Python 解析 JSON:
initial_state = page.evaluate("window.__INITIAL_STATE__") if initial_state and "userInfo" in initial_state: bio = initial_state["userInfo"]["user_info"]["signature"]4.4 现象:pip install scrapy-playwright报错ModuleNotFoundError: No module named 'twisted'
原因:Scrapy 2.8.0 依赖twisted>=22.2.0,而某些pip版本会跳过依赖检查。
解决:分步安装:
pip install twisted==22.10.0 pip install scrapy==2.8.0 pip install scrapy-playwright==0.0.104.5 现象:爬取视频列表时,aweme_id字段为空或重复
原因:抖音对aweme_id做了 base64 编码混淆,原始 ID 存于item['aweme_id'],但展示用 ID 是item['id'](即aweme_id的 base64 变体)。
解决:统一用item['aweme_id']作为主键,若需生成分享链接,用f"https://www.douyin.com/video/{item['aweme_id']}",而非item['id']。
5. 用 SQLAlchemy 储存爬虫数据:从 raw JSON 到结构化表的四步映射
.zip包中常见pipelines.py直接写入 CSV 或 MongoDB,但抖音数据字段多、嵌套深(如statistics、author、music),CSV 易丢字段,MongoDB 查询成本高。SQLAlchemy 结合declarative_base可实现强类型约束与高效 JOIN,以下是针对抖音视频数据的落地实践。
5.1 设计核心表结构:覆盖 90% 公开字段
| 表名 | 字段 | 类型 | 说明 |
|---|---|---|---|
videos | aweme_id | String(32), PK | 抖音视频唯一 ID,不可为空 |
title | Text | 视频标题,支持 emoji | |
desc | Text | 视频描述(可能含换行) | |
duration | Integer | 时长(秒) | |
create_time | DateTime | 发布时间(UTC) | |
statistics | JSON | 点赞、评论、转发数(JSON 字符串) | |
authors | sec_uid | String(64), PK | 用户唯一标识 |
nickname | String(128) | 昵称 | |
verified | Boolean | 是否认证 | |
follower_count | BigInteger | 粉丝数(可能超 int32) | |
video_author | aweme_id | String(32), FK | 关联 videos.aweme_id |
sec_uid | String(64), FK | 关联 authors.sec_uid |
注意:
statistics字段设为 JSON 类型,避免为每个统计字段建单独列(抖音字段常增删,如新增collect_count)。
5.2 编写 SQLAlchemy Model 与 Pipeline
# models.py from sqlalchemy import create_engine, Column, String, Text, Integer, DateTime, Boolean, BigInteger, ForeignKey, JSON from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime Base = declarative_base() class Video(Base): __tablename__ = "videos" aweme_id = Column(String(32), primary_key=True) title = Column(Text) desc = Column(Text) duration = Column(Integer) create_time = Column(DateTime) statistics = Column(JSON) class Author(Base): __tablename__ = "authors" sec_uid = Column(String(64), primary_key=True) nickname = Column(String(128)) verified = Column(Boolean, default=False) follower_count = Column(BigInteger) class VideoAuthor(Base): __tablename__ = "video_author" aweme_id = Column(String(32), ForeignKey("videos.aweme_id"), primary_key=True) sec_uid = Column(String(64), ForeignKey("authors.sec_uid"), primary_key=True) # pipelines.py from sqlalchemy.exc import IntegrityError from models import Video, Author, VideoAuthor, engine class SqlitePipeline: def __init__(self): self.Session = sessionmaker(bind=engine) def process_item(self, item, spider): session = self.Session() try: # 插入视频 video = Video( aweme_id=item["aweme_id"], title=item.get("title", ""), desc=item.get("desc", ""), duration=item.get("duration", 0), create_time=datetime.fromtimestamp(item.get("create_time", 0)), statistics=item.get("statistics", {}), ) session.add(video) # 插入作者(忽略重复) author = Author( sec_uid=item["author"]["sec_uid"], nickname=item["author"].get("nickname", ""), verified=item["author"].get("verified", False), follower_count=item["author"].get("follower_count", 0), ) session.merge(author) # merge 替代 add,避免重复主键异常 # 关联关系 session.add(VideoAuthor( aweme_id=item["aweme_id"], sec_uid=item["author"]["sec_uid"] )) session.commit() except IntegrityError as e: session.rollback() spider.logger.warning(f"Duplicate key ignored: {e}") finally: session.close() return item5.3 在 settings.py 中启用 Pipeline
# settings.py ITEM_PIPELINES = { "myproject.pipelines.SqlitePipeline": 300, } # 初始化数据库(首次运行自动建表) from models import Base, engine Base.metadata.create_all(engine)5.4 验证数据完整性:用 SQL 查询替代 print()
爬虫跑完后,别再print(item),直接查库验证:
-- 查看最近 5 条视频及其作者昵称 SELECT v.title, v.duration, a.nickname, a.follower_count FROM videos v JOIN video_author va ON v.aweme_id = va.aweme_id JOIN authors a ON va.sec_uid = a.sec_uid ORDER BY v.create_time DESC LIMIT 5; -- 统计各作者视频数(验证关联是否正确) SELECT a.nickname, COUNT(*) as video_count FROM authors a JOIN video_author va ON a.sec_uid = va.sec_uid GROUP BY a.nickname ORDER BY video_count DESC;6. 抖音数据清洗的三个硬核技巧:从 raw response 到可分析字段
抖音原始 API 返回的 JSON 是「黑匣子」:字段名不规范(如user_infovsauthor)、数值类型混乱(粉丝数为字符串"1234567")、时间戳格式不一(create_time: 1678886400vspublish_time: "2023-03-15T10:24:00Z")。我花两个月整理出三条必做清洗规则,现在每天省下 2 小时 debug 时间。
6.1 统一时间字段:用dateutil.parser处理所有时间变体
抖音返回的时间字段至少有 5 种格式:Unix timestamp(int)、ISO8601 字符串、中文日期字符串("2023年3月15日")、相对时间("3天前")、空值(null)。硬编码datetime.fromtimestamp()或strptime()必翻车。
from dateutil import parser from datetime import datetime def parse_douyin_time(time_value) -> datetime: """统一解析抖音所有时间格式""" if time_value is None: return datetime.utcnow() if isinstance(time_value, (int, float)): # Unix timestamp(秒级或毫秒级) if time_value > 1e10: # 毫秒级 return datetime.fromtimestamp(time_value / 1000) else: return datetime.fromtimestamp(time_value) if isinstance(time_value, str): try: return parser.parse(time_value) except (ValueError, TypeError): # 处理中文日期:"2023年3月15日" → "2023-03-15" if "年" in time_value and "月" in time_value and "日" in time_value: cleaned = time_value.replace("年", "-").replace("月", "-").replace("日", "") return datetime.strptime(cleaned, "%Y-%m-%d") return datetime.utcnow() return datetime.utcnow() # 在 pipeline 中调用 item["create_time"] = parse_douyin_time(raw_item.get("create_time"))6.2 标准化数字字段:用re.sub(r"[^\d.]", "", s)提取纯数字
抖音的follower_count、like_count常带单位:"123.4万"、"23.5亿"、"1,234,567"。直接int()必报错。
import re def clean_number(text: str) -> float: """提取并转换抖音数字字段""" if not text: return 0.0 # 移除所有非数字字符(保留 . 和 -) digits = re.sub(r"[^\d.-]", "", str(text)) if not digits: return 0.0 # 处理单位缩写 if "万" in str(text): return float(digits) * 10000 if "亿" in str(text): return float(digits) * 100000000 if "千" in str(text): return float(digits) * 1000 return float(digits) # 示例 clean_number("123.4万") # → 1234000.0 clean_number("23.5亿") # → 2350000000.0 clean_number("1,234,567") # → 1234567.06.3 提取视频标签:从desc字段中 regex 匹配#xxx并去重
抖音用户习惯在描述末尾加话题标签,如"...#python #爬虫 #Scrapy"。这是高价值字段,但.zip包中常被忽略。
import re def extract_hashtags(desc: str) -> list: """从 desc 中提取所有 # 标签""" if not desc: return [] # 匹配 # 后非空格字符,直到空格或标点 tags = re.findall(r"#(\w+)", desc) # 去重并转小写(抖音标签不区分大小写) return list(set(tag.lower() for tag in tags)) # 在 item loader 中 item["hashtags"] = extract_hashtags(item.get("desc", ""))最后说个真实教训:我曾以为 signature 是最大难点,结果上线三天后发现 70% 的数据丢失,排查发现是statistics字段里comment_count有时为null,有时为"0",有时为0,没做类型统一就直接int()导致 pipeline crash。现在我的原则是:所有字段入库前必须经过clean_*()函数,宁可填None,绝不让原始值裸奔。希望帮到你。
本文还有配套的精品资源,点击获取