ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python数据抓取实战:从视频元数据到存储全流程解析

Python数据抓取实战:从视频元数据到存储全流程解析 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\activatemacOS或Linux下执行source .venv/bin/activate激活后终端前面会出现一个(.venv)前缀表示你当前就在虚拟环境里。接下来安装依赖就会装进这个环境的“工具房”不会污染全局。2.3 VSCode里的解释器选择与调试配置编辑器方面我建议用VSCode免费且对Python的支持做得很好。装好两个扩展Python和Python Debugger基本就够用了。建好项目目录后在VSCode里打开它然后按快捷键CtrlShiftP输入“Python: Select Interpreter”选择刚才创建的.venv环境。这一步容易漏选漏选的话VSCode会用全局Python来跑代码结果就是终端里明明能用requests编辑器里却报ModuleNotFoundError。一切就绪后先安装我们后面会用到的库pip install requests beautifulsoup4 httpx tenacityrequests负责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/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } url https://example.com/videos resp requests.get(url, headersheaders, timeout(3.05, 10)) resp.raise_for_status() resp.encoding resp.apparent_encodingtimeout为什么要写一个元组因为连接超时和读取超时是两回事。(3.05, 10)的意思是连接阶段最多等3.05秒一旦连上了读取数据最多等10秒。很多爬虫崩溃都是因为只写了timeout10结果服务器一直不返回数据脚本就卡在那里最终被流程卡死。编码问题同样隐蔽。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( total3, status_forcelist[429, 500, 502, 503, 504], allowed_methods[GET], backoff_factor1, ) adapter HTTPAdapter(max_retriesretry_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;q0.9, })这段代码里的Retry策略是重点。total3表示最多重试3次status_forcelist里放的是需要重试的状态码429是请求过于频繁500/502/503/504是服务端异常。backoff_factor1表示每次重试的等待时间按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(headlessTrue) page browser.new_page() page.goto(https://example.com/videos, timeout30000) html page.content() browser.close()但我更建议你先打开浏览器的开发者工具切到Network面板刷新页面找那个真正返回数据的XHR或Fetch请求。绝大多数站点都是“前端SPA 后端JSON接口”的架构数据其实就在某个接口里躺着。你要做的就是把requests的目标从HTML页面换成那个JSON接口直接拿结构化数据比渲染完再解析HTML快一个数量级也稳定得多。浏览器自动化的正确使用场景是接口加密特别复杂、完全找不到数据来源或者需要处理复杂的用户交互。对这些场景来说Playwright是兜底方案而不是首选方案因为它的资源开销和运行稳定性都不如纯HTTP请求。4. 解析视频元数据HTML与JSON两条路线的实操4.1 BeautifulSoup四类操作从标题到列表解析假设目标页面里有一段公开的视频卡片结构大概是这样的li classvideo-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(stripTrue) # 4. 拿属性 video_id card.get(data-video-id) href title_tag.get(href)注意事项里有个高频坑BeautifulSoup的find方法里class参数是class_后面带下划线因为class是Python关键字。如果你写classvideo-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( rPT(?:(\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, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnamesfieldnames) 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 fhttps://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_workers4) 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_workers4是我做采集任务时的习惯上限原因很现实并发数过高会导致目标站点的反爬更容易触发而且你自己机器的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 installrequests.exceptions.ConnectionError目标服务不可达或者连接被重置检查URL、网络连通性确认不是请求太频繁被临时断连requests.exceptions.ReadTimeout服务器响应太慢调大读取超时时间或者减少并发数json.JSONDecodeError响应内容不是合法JSON先打印resp.text前200个字符确认返回的是什么AttributeError: NoneType object has no attribute xxxBeautifulSoup没找到对应节点回到页面源码确认选择器是否已失效UnicodeDecodeError编码判断错误把resp.encoding显式设置为utf-8试一下KeyError: videoIdJSONG数据里没有这个字段打印出所有键名确认接口字段是否变动这些错误里最值得养成条件反射的是第二类ConnectionError。很多人一看到连接错误就以为是代码问题其实往往是采集频率过高触发了服务端的临时断连。这时候最正确的操作是停下来等几分钟让服务端恢复正常而不是立刻加并发。6.3 把这套方法迁移到任意目标站点的步骤模板最后说下这套方法怎么复用到别的站点上。很多读者手里真实的目标可能不是视频站点而是一个行业资讯站、一个公开数据平台甚至是一个政府公开数据页面。迁移的步骤其实完全一致。第一步打开目标页面按F12找到数据来源是HTML源码直接包含数据还是XHR接口返回JSON。第二步拿到一个样例数据后把JSON或HTML存成本地文件用Python离线解析字段确保每个目标字段都能准确提取。第三步写请求代码时把目标地址、请求头、参数都配置化不要写死在解析函数里。第四步先小规模试跑10条数据检查字段完整性和编码问题。第五步确认没问题后再加上数据库存储和去重逻辑把并发维持在合理范围。最后设置采集频率和定时任务跑起来之后持续观察日志。这个过程里最容易忽视的是第二步里“存本地文件离线解析”这个动作。这样做的好处是解析代码和网络请求解耦你可以在完全不消耗目标站点资源的情况下反复调试选择器。等选择器调好了再把网络请求加回来出错概率会小很多。我个人的习惯是每个采集项目都建一个samples/目录把第一次拿到的真实页面HTML和接口JSON存下来。这些样例文件有两个用处一是源站改版后可以用来对照定位问题二是写解析代码时不用反复请求目标站点本地就能调试。可以说我维护最久的几个采集项目最后靠的都不是某个高级技巧而是这套把样例数据本地化的笨办法。做数据抓取这些年我最大的体会是数据抓取从来不应该是猫鼠游戏而应该是对公开互联网信息的整理和抽取。每次动手前先问自己一句拿这份数据的目的到底是什么是不是非抓不可有没有更合规的获取方式。把这些问题想清楚之后你写的代码才会又稳又长久。
返回列表