Python数据抓取实战:从视频元数据到存储全流程解析
2026/9/8 17:41:10 网站建设 项目流程

2026年,后台私信里被问得最多的数据需求之一,就是用Python做视频平台的数据抓取与分析。这里的“数据抓取”指的是一套把网页或接口里公开的结构化信息,变成可复用数据集的工程流程,不是什么黑科技。真正想研究视频平台内容生态的人,需要的往往就是最基础的“标题、播放量、发布时间、标签”这类公开元数据,而这些数据恰好都在页面上公开暴露着。

如果你也想做类似的事,这篇文章能帮你把环境、请求、解析、清洗、存储这条链路完整走一遍。我尽量用一个常见视频卡片页面的结构来做演示,不讲任何绕过访问限制或平台风控的方法,只谈公开数据怎么被规规矩矩地拿下来、洗干净、存起来。这套方法论对绝大多数公开信息站点都是通用的,学会之后你就能自己迁移。

1. 先聊两句大背景:2026年的数据抓取到底在做什么

1.1 视频内容研究最需要的那批公开元数据

先说个场景:你给一个内容团队做竞品观察,每周要统计一批频道的新视频更新情况,包括标题写法、发布时间、播放量涨速、标签怎么排。这些数据如果靠人工一个个页面去复制粘贴,一个人一天也做不了几十条,而且特别容易抄错。

于是自然想到用Python写一段自动采集脚本。模板大概是:给一个频道页或搜索页的地址,脚本帮你把里面每条视频的ID、标题、播放量、时长、发布时间都提出来,存进一张表里,最后用Excel或SQL做分析。

这类需求听起来很“爬虫”,但拆开看就是几件事:发请求拿HTML或JSON、解析目标字段、清洗格式、落库。难点全在细节上,不在代码本身。尤其到了2026年,各大站点前端框架迭代很快,页面结构经常变,真正能支撑起长期采集任务的,是一个健壮的采集框架,而不是某个写死的选择器。

1.2 本教程的合规前提与技术范围

在写任何一行代码之前,我要先把边界说清楚:本文只讨论采集公开数据,不做任何登录绕过、验证码破解、风控对抗或者对目标平台施加压力的操作。如果你的目标数据在登录之后才能看到,或者平台的服务条款明确不允许采集,那这条线就别跨过去。

优先查询目标平台是否提供了官方接口。对视频平台来说,如果官方接口能满足你的研究需求,它永远优于页面抓取方案。原因很简单:接口配额以内的请求是合规且稳定的,页面抓取则随时可能因为前端改版而失效,还要额外处理一堆HTML解析问题。真正的爬虫代码,是在“没有官方接口”或者“官方接口覆盖不了需求”的时候才派上用场。

合规前提下,还有一条铁律叫守法采集节奏。后面我会具体讲请求间隔和并发控制,这里先记住一个判断标准:你的脚本跑起来之后,不应该让目标站点感觉到“你在采集”。如果对方产生了这种感觉并采取了措施,通常说明你已经踩线了。

2. 一次配好环境:Python安装、虚拟环境与VSCode配置

2.1 解释器安装与PATH环境变量

很多人对爬虫的第一道心理门槛不是代码,而是环境。Python装了一半、pip用不了、在终端里敲python没反应,这些都是我见过的高频问题。

安装本身不难,去python.org下载最新的稳定版安装包,双击运行。关键的一步是在安装向导第一屏勾选“Add python.exe to PATH”,这个选项默认是没勾上的,不勾的话后续在终端里执行python命令会提示找不到程序,新手往往会卡在这里很久。

装完之后,打开终端或命令提示符验证一下:

python --version pip --version

如果能正常打印出版本号,说明解释器和包管理工具都已经可用。如果提示“python不是内部或外部命令”,大概率就是PATH没配上,最简单的办法是重装一遍并勾上那个选项,比手动改环境变量省心得多。

2.2 venv虚拟环境为什么值得

装好Python之后,我的建议是每做一个项目就建一个独立的虚拟环境。举个例子,你手头有一个旧项目依赖requests 2.20,新项目却需要requests 2.32,如果全部装到全局环境,旧项目可能直接跑不起来。

虚拟环境就是用来隔离这种依赖冲突的,相当于给每个项目单独开一间“工具房”,里面装的库互不干扰。具体操作:

mkdir video-crawler cd video-crawler python -m venv .venv

创建完成后需要激活环境。Windows下执行:

.venv\Scripts\activate

macOS或Linux下执行:

source .venv/bin/activate

激活后终端前面会出现一个(.venv)前缀,表示你当前就在虚拟环境里。接下来安装依赖就会装进这个环境的“工具房”,不会污染全局。

2.3 VSCode里的解释器选择与调试配置

编辑器方面,我建议用VSCode,免费且对Python的支持做得很好。装好两个扩展:Python和Python Debugger,基本就够用了。

建好项目目录后,在VSCode里打开它,然后按快捷键Ctrl+Shift+P,输入“Python: Select Interpreter”,选择刚才创建的.venv环境。这一步容易漏选,漏选的话VSCode会用全局Python来跑代码,结果就是终端里明明能用requests,编辑器里却报ModuleNotFoundError。

一切就绪后,先安装我们后面会用到的库:

pip install requests beautifulsoup4 httpx tenacity

requests负责HTTP请求,beautifulsoup4负责HTML解析,httpx是requests的现代替代品,支持HTTP/2,某些场景更顺手,tenacity是重试库。装完这几样,就可以进入真正的请求环节了。

3. 把请求发出去:requests层级的细节决定了爬虫稳不稳

3.1 从最小请求开始:请求头、超时与编码

很多新手写第一个爬虫,代码长这样:

import requests resp = requests.get("https://example.com/videos") print(resp.text)

这段代码在实验室里没问题,但拿到真实站点上就是四处碰壁。原因在于它缺少了几个关键的东西:自定义的User-Agent、超时时间、明确的编码处理。正常的浏览器请求会带上一堆请求头,服务器对这些头是有预期的。如果请求头缺失或看起来像脚本,对方可能直接返回403。

我通常这样组织最小请求:

import requests 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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } url = "https://example.com/videos" resp = requests.get(url, headers=headers, timeout=(3.05, 10)) resp.raise_for_status() resp.encoding = resp.apparent_encoding

timeout为什么要写一个元组?因为连接超时和读取超时是两回事。(3.05, 10)的意思是:连接阶段最多等3.05秒,一旦连上了,读取数据最多等10秒。很多爬虫崩溃都是因为只写了timeout=10,结果服务器一直不返回数据,脚本就卡在那里,最终被流程卡死。

编码问题同样隐蔽。resp.text默认会猜一次编码,但对中文页面经常猜错,出现乱码。resp.apparent_encoding是基于内容做的二次判断,比默认猜测准得多。赋值给resp.encoding之后再访问resp.text,中文就能正常显示了。

3.2 Session、重试与错误处理的实战组合

真实采集任务不可能只发一次请求。通常需要连续访问几十上百个页面,这时候用裸的requests.get就是浪费资源,等于每次请求都重新走一遍TCP握手和TLS协商。

正确做法是用requests.Session()维护一个会话对象,它可以复用底层的连接池:

import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry_strategy = Retry( total=3, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET"], backoff_factor=1, ) adapter = HTTPAdapter(max_retries=retry_strategy) session.mount("https://", adapter) session.mount("http://", adapter) session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept-Language": "zh-CN,zh;q=0.9", })

这段代码里的Retry策略是重点。total=3表示最多重试3次,status_forcelist里放的是需要重试的状态码:429是请求过于频繁,500/502/503/504是服务端异常。backoff_factor=1表示每次重试的等待时间按1秒、2秒、4秒递增,避免立刻重试又立刻失败。

请求发出后,还要养成检查状态码的习惯。常见的几种情况:

状态码含义处理方式
200正常返回继续解析
301/302重定向requests默认跟随,一般不用管
403拒绝访问大概率被识别出非浏览器身份,先停下来检查请求头
404页面不存在目标可能改版或变量拼接错
429请求过于频繁立即停止,等待Retry-After头指示的时间

3.3 动态渲染页面:浏览器自动化的适用边界

2026年的前端页面大量使用JavaScript渲染,你会发现requests拿回来的HTML里根本没有视频标题,只有一个空壳div,数据是脚本加载完后才填进去的。

面对这种页面,很多人的第一反应是上浏览器自动化。这确实是一条路,Playwright写起来也简单:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto("https://example.com/videos", timeout=30000) html = page.content() browser.close()

但我更建议你先打开浏览器的开发者工具,切到Network面板,刷新页面,找那个真正返回数据的XHR或Fetch请求。绝大多数站点都是“前端SPA + 后端JSON接口”的架构,数据其实就在某个接口里躺着。你要做的就是把requests的目标从HTML页面换成那个JSON接口,直接拿结构化数据,比渲染完再解析HTML快一个数量级,也稳定得多。

浏览器自动化的正确使用场景是:接口加密特别复杂、完全找不到数据来源,或者需要处理复杂的用户交互。对这些场景来说,Playwright是兜底方案,而不是首选方案,因为它的资源开销和运行稳定性都不如纯HTTP请求。

4. 解析视频元数据:HTML与JSON两条路线的实操

4.1 BeautifulSoup四类操作:从标题到列表解析

假设目标页面里有一段公开的视频卡片结构,大概是这样的:

<li class="video-card">from bs4 import BeautifulSoup soup = BeautifulSoup(html_text, "html.parser") # 1. 找单个元素 card = soup.find("li", class_="video-card") # 2. 用CSS选择器找嵌套元素 title_tag = card.select_one(".video-title") # 3. 拿文本 title = title_tag.get_text(strip=True) # 4. 拿属性 video_id = card.get("data-video-id") href = title_tag.get("href")

注意事项里有个高频坑:BeautifulSoup的find方法里,class参数是class_,后面带下划线,因为class是Python关键字。如果你写class="video-card",会直接报TypeError。

选完元素后,还有一件必须做的是判空。页面结构一旦调整,某个class没匹配上,title_tag就会变成None,这时候再调用.get_text()会报AttributeError。养成习惯:

if title_tag is None: continue # 记录日志后跳过这一条

这个判断虽然简单,但能让你在目标站点改版时快速定位问题,而不是等到整个采集任务崩掉之后才回头找是哪个字段出了问题。

4.2 JSON数据解析:以视频详情接口结构为例

很多视频平台的详情页里会有一段内嵌的JSON数据,字段组织方式相当规整。假设你拿到的是这样一段示意结构:

{ "videoDetails": { "videoId": "dQw4w9WgXcQ", "title": "Rick Astley - Never Gonna Give You Up", "lengthSeconds": "213", "viewCount": "1520000000", "keywords": ["rick astley", "pop music"], "shortDescription": "Rick Astley official music video..." } }

解析JSON就远没有HTML解析那么费劲,Python标准库里的json模块就够用。只要把接口返回的text通过json.loads转成字典,剩下的就是逐层取值:

import json data = json.loads(resp.text) details = data["videoDetails"] video_id = details["videoId"] title = details["title"] view_count = details["viewCount"] keywords = details["keywords"]

这里值得注意的一个坑是字段类型。viewCount是字符串而不是数字,这在很多接口中都很常见,因为数字超过一定位数后,JSON解析成整数可能丢失精度,所以后端干脆用字符串来传。你后面做排序和统计时,一定要先int()转换。

lengthSeconds同理,是字符串表示的秒数。拿到之后如果需要显示成”3:33”这种时长格式,自己写个转换函数就行;反过来,如果页面上显示的是“3:33”,需要存成秒数,那就要解析时分秒格式。

4.3 脏数据清洗:播放量缩写、ISO时长与HTML实体

真实世界的数据永远比文档样例脏。以视频元数据为例,你会遇到的典型脏数据有三类。

第一类是播放数和点赞数的缩写格式,页面上展示的是“1.5M views”、“4.5K likes”、“2.3B”,入库前需要转换成整数,否则排序和画图都没法做:

def parse_compact_number(text: str) -> int: text = text.strip().lower().replace(",", "") multiplier_map = {"k": 1_000, "m": 1_000_000, "b": 1_000_000_000} if text[-1:] in multiplier_map: number_part = float(text[:-1]) return int(number_part * multiplier_map[text[-1]]) return int(float(text))

转换前记得先把逗号去掉,因为有的站点会把大数显示成“1,234,567 views”。

第二类是ISO 8601时长格式。视频接口里时长通常不是“3:33”,而是“PT3M33S”这种格式,字母P和T是固定前缀,中间分别用H、M、S表示时分秒。为了统一入库,需要把它转成秒数:

import re def duration_to_seconds(duration: str) -> int: match = re.fullmatch( r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", duration.strip() ) if not match: return 0 hours = int(match.group(1) or 0) minutes = int(match.group(2) or 0) seconds = int(match.group(3) or 0) return hours * 3600 + minutes * 60 + seconds

这样处理后,无论来源页面的展示格式怎么变,你数据库里存的始终是标准化的整数秒数。

第三类是HTML实体和emoji。有些标题里会带&或'之类的HTML实体,HTML解析时通常能被自动转换,但JSON里手动拼接的HTML片段偶尔会漏转。稳妥的做法是再补一道html.unescape()的保险。至于emoji和不可见字符,看具体场景决定是保留还是过滤,如果标题要用于分词统计,建议把emoji替换成空格而不是直接删除,否则相邻词汇会被错误拼接在一起。

5. 落库与批量抓取:存储设计、断点续传与节流

5.1 CSV的三件套:utf-8-sig、newline=""、字段统一起名

数据解析出来后的第一步是落盘。如果只是几十条数据,CSV就够了。写CSV时我建议直接套用下面这个三件套组合:

import csv fieldnames = ["video_id", "title", "view_count", "duration_sec", "published_at"] with open("videos.csv", "w", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerow({ "video_id": video_id, "title": title, "view_count": view_count, "duration_sec": duration_sec, "published_at": published_at, })

encoding用utf-8-sig而不是utf-8,是因为前者会写入一个BOM头,Excel打开时才能正确识别UTF-8编码的中文,不会出现乱码。

newline=""是为了避免在Windows平台上每写入一行就多出一个空行。这个坑几乎所有Python新手都踩过,但只有在读取CSV的时候才会发现数据间隔着一堆空行,原因就是没写newline=""。

CSV方案最怕的其实是半途崩溃。如果目标站点改了页面结构,脚本跑到一半报错退出,你已经抓了300条数据全在内存里,没来得及落盘,结果前功尽弃。所以更稳妥的做法是每解析完一条就立即写入文件,而不是统一攒到最后。

5.2 SQLite去重表:用video_id做主键

当数据量到几千条以后,CSV方案的缺点开始显现:追加数据麻烦、去重麻烦、按照时间范围查询也麻烦。这种情况下换SQLite很合适,它是Python内置支持的数据库,不依赖任何额外服务,相当于一个单文件版数据库。

建表的核心是选对主键。视频卡片里那个唯一的video_id就是天然主键,用它来防止重复录入:

import sqlite3 conn = sqlite3.connect("videos.db") conn.execute(""" CREATE TABLE IF NOT EXISTS videos ( video_id TEXT PRIMARY KEY, title TEXT, view_count INTEGER, duration_sec INTEGER, published_at TEXT, fetched_at TEXT ) """) conn.commit()

每次解析出一条新数据,都用INSERT OR IGNORE写入。这样重复抓取时,已经存在的video_id会被自动忽略,不会产生重复行:

conn.execute( "INSERT OR IGNORE INTO videos " "(video_id, title, view_count, duration_sec, published_at, fetched_at) " "VALUES (?, ?, ?, ?, ?, ?)", (video_id, title, view_count, duration_sec, published_at, fetched_at) ) conn.commit()

批量采集的流程有了这套表结构之后,可以实现真正的断点续传:启动脚本时先查一次数据库,把已经抓过的video_id读进一个set,然后正常开始遍历目标列表,遇到set里已经存在的ID就跳过。这样即使脚本中途被杀掉,下次重新执行也能接着上次的进度继续跑,不用从头再来。

偶尔碰到修改历史数据的场景,比如要回补某条视频的最新播放量,可以再加一个UPSERT语句或者先查再更新的逻辑。这里不展开,但思路已经有了:数据表始终以video_id为唯一业务键,所有更新的最终目的都是让同一条视频的数据保持最新最全。

5.3 多线程并发:该抢的时候抢,该让的时候让

抓个二三十个页面,单纯用单线程加循环就够了,最大的问题是慢:每个请求之间就算只等1秒,20个页面也要20秒。如果目标是几百上千个视频详情页,单线程显然不现实。

这时可以用Python的concurrent.futures来做一小撮并发,把单位时间内的吞吐提上去。常见写法:

import time import random from concurrent.futures import ThreadPoolExecutor, as_completed def fetch_one(video_id: str): time.sleep(random.uniform(1.0, 2.5)) url = f"https://example.com/videos/{video_id}" resp = session.get(url, timeout=(3.05, 10)) resp.raise_for_status() return parse_video(resp.json()) video_ids = ["video_001", "video_002"] # 换成真实任务里的ID列表 with ThreadPoolExecutor(max_workers=4) as executor: futures = {executor.submit(fetch_one, vid): vid for vid in video_ids} for future in as_completed(futures): row = future.result() save_to_db(row)

max_workers=4是我做采集任务时的习惯上限,原因很现实:并发数过高会导致目标站点的反爬更容易触发,而且你自己机器的CPU和网络带宽也有限,并发8和并发16在真实抓取场景下经常没有明显速度差异,反而显著增加被封风险。

time.sleep(random.uniform(1.0, 2.5))这行也不要觉得多余。它做的是把请求间隔变成1到2.5秒之间的随机值,避免出现规律的节奏,让采集行为看起来更接近真实用户浏览页面。

如果你一定要追求极高的抓取效率,请先问自己一个问题:这个数据真的需要这么急吗?每天更新的频道列表,定时任务慢慢抓完全来得及,没必要为了早几分钟拿数据而承担被封的风险。

6. 避坑清单与迁移心法

6.1 本地没反应或者超时的排查路径

写爬虫时最让人烦躁的不是语法错误,而是代码明明没报错,却拿不到数据。我建议遇到这类情况,先按下面的顺序排查,不要东改一行西改一行。

第一步看页面结构,用浏览器打开目标地址,按F12进入开发者工具,确认你要的数据到底在HTML源码里还是JS渲染后才出现。如果源码里没有,就切到Network面板,刷新页面,找那个XHR请求,确认数据是不是通过接口返回的。

第二步看请求差异。如果你的代码请求被拒绝,对比一下浏览器实际发出的请求头和你代码里发送的请求头差异,尤其是User-Agent、Referer、Accept-Language这些字段。大多数时候补上Referer就能解决,因为部分站点的接口会校验来源页面。

第三步加入日志。不要直接print一堆原始响应,而是打印响应状态码、响应头里的Content-Type、响应前500个字符,这样能快速判断拿回来的到底是不是预期数据。

如果代码能拿到HTML但解析出来的字段全是空的,九成是页面结构改了,你用的选择器已经匹配不到节点。这时候重新看一遍页面源码,更新对应选择器就行。

6.2 高频错误速查表

直接给你一张可以贴在电脑前的错误对照表:

报错信息常见原因处理方式
ModuleNotFoundError: No module named 'requests'当前环境没装依赖库检查虚拟环境是否激活,重新pip install
requests.exceptions.ConnectionError目标服务不可达,或者连接被重置检查URL、网络连通性,确认不是请求太频繁被临时断连
requests.exceptions.ReadTimeout服务器响应太慢调大读取超时时间,或者减少并发数
json.JSONDecodeError响应内容不是合法JSON先打印resp.text前200个字符确认返回的是什么
AttributeError: 'NoneType' object has no attribute 'xxx'BeautifulSoup没找到对应节点回到页面源码确认选择器是否已失效
UnicodeDecodeError编码判断错误把resp.encoding显式设置为utf-8试一下
KeyError: 'videoId'JSONG数据里没有这个字段打印出所有键名,确认接口字段是否变动

这些错误里,最值得养成条件反射的是第二类ConnectionError。很多人一看到连接错误就以为是代码问题,其实往往是采集频率过高触发了服务端的临时断连。这时候最正确的操作是停下来等几分钟,让服务端恢复正常,而不是立刻加并发。

6.3 把这套方法迁移到任意目标站点的步骤模板

最后说下这套方法怎么复用到别的站点上。很多读者手里真实的目标可能不是视频站点,而是一个行业资讯站、一个公开数据平台,甚至是一个政府公开数据页面。

迁移的步骤其实完全一致。第一步,打开目标页面,按F12找到数据来源,是HTML源码直接包含数据,还是XHR接口返回JSON。第二步,拿到一个样例数据后,把JSON或HTML存成本地文件,用Python离线解析字段,确保每个目标字段都能准确提取。第三步,写请求代码时把目标地址、请求头、参数都配置化,不要写死在解析函数里。第四步,先小规模试跑10条数据,检查字段完整性和编码问题。第五步,确认没问题后再加上数据库存储和去重逻辑,把并发维持在合理范围。最后设置采集频率和定时任务,跑起来之后持续观察日志。

这个过程里最容易忽视的是第二步里“存本地文件离线解析”这个动作。这样做的好处是,解析代码和网络请求解耦,你可以在完全不消耗目标站点资源的情况下反复调试选择器。等选择器调好了,再把网络请求加回来,出错概率会小很多。

我个人的习惯是每个采集项目都建一个samples/目录,把第一次拿到的真实页面HTML和接口JSON存下来。这些样例文件有两个用处:一是源站改版后可以用来对照定位问题;二是写解析代码时不用反复请求目标站点,本地就能调试。可以说,我维护最久的几个采集项目,最后靠的都不是某个高级技巧,而是这套把样例数据本地化的笨办法。

做数据抓取这些年,我最大的体会是:数据抓取从来不应该是猫鼠游戏,而应该是对公开互联网信息的整理和抽取。每次动手前先问自己一句,拿这份数据的目的到底是什么,是不是非抓不可,有没有更合规的获取方式。把这些问题想清楚之后,你写的代码才会又稳又长久。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询