Python爬虫实战:监控视频网站更新与通知推送
最近接了个项目需求一句话就能说清用爬虫监控某个网络视频站点的最近更新。听起来简单真做起来还是有不少坑要踩的。这类需求其实很常见比如你想追某个剧、某个UP主或者某个栏目不想每天手动刷新页面最好有程序替你盯着一有更新就通知你。我在做这个项目时把整个流程拆成了几个部分确定抓什么、怎么抓、怎么判断“新”、以及最后怎么通知。整个过程用的就是Python生态里最基础的几个库——requests、re、json这些说不上高深但很考验对HTTP协议、响应解析和反爬对策的理解。这篇文章我就把完整的思路、实操步骤和踩坑记录都写出来给有类似需求的读者一个可以直接参考的方案。先说清楚一个原则我做的这个爬虫是合法合规的更新检测只抓取页面上公开的元数据标题、发布时间、简介这些不下载视频文件不绕过登录不破解付费内容。而且目标站点的robots协议允许爬取抓取频率也控制在很低的水平不会给服务器造成负担。如果你要抓的站点有明确禁止爬取的声明那建议先停手或者联系站长获得授权。完整项目我放在了GitHub上链接在文末下面所有代码片段都是亲测可运行的Python版本用的3.10依赖只有requests、beautifulsoup4和lxml三个。1. 项目整体思路与需求拆解1.1 核心需求解析到底要爬什么先别急着写代码得把需求聊清楚。用户说“爬取网络视频最近更新”这里面的关键词是“最近”和“更新”。我理解的核心诉求是持续监控一个视频网站当目标视频有新的一集、新的一期或者新的内容发布时第一时间检测到并通知用户。这里要区分三种不同的场景很多人混为一谈全站更新流网站上有个“最近更新”栏目把整个站最新发布的内容按时间排序展示。这种最简单爬一个列表页就行。指定剧集/频道更新只关注某个特定剧集或某个创作者的最新动态。这种需要先定位到详情页或创作者主页再解析里面的展示列表。视频资源站采集站更新这种场景更多是获取视频源地址或资源链接通常会涉及盗版或版权问题我不建议碰除非你是该资源站的授权合作方。我做的是第一种和第二种的结合版先爬全站“最近更新”列表页提取所有条目然后对重点关注的几个详情页单独设置高频轮询。这样既覆盖了新上架的内容也照顾了重点追踪的需求。1.2 为什么选择“轻量爬虫元数据检测”而不是下载器很多人一听到“爬虫爬取视频”第一反应是用you-get、youtube-dl这类下载工具或者直接模拟浏览器接管媒体流。这个思路在项目立项阶段就被我否决了原因有三点版权风险未经授权下载视频文件尤其是影视剧、综艺这类商业内容法律风险极大。而抓取公开页面上展示的标题、时间和简介属于常规的信息采集合规边界要清晰得多。技术成本直接抓视频流意味着要处理m3u8分片、加密、防盗链、鉴权等一系列问题开发周期至少翻倍而且视频网站的反爬重点就集中在媒体流接口上一不留神就会触发风控。实际需求不匹配用户说要“知道最近更新了什么”而不是“帮我下载下来”。所以检测的粒度是元数据有没有新条目、新条目是什么这就够了。最终方案是requests BeautifulSoup 做页面解析抓列表条目的标题、日期、链接用数据库做去重有新增就推送通知。这个方案轻量、可维护、跑起来稳定。1.3 技术选型分析requests、selenium和scrapy怎么选选型这块我仔细权衡过。目标视频站是个传统型的Web站点内容主要是服务端渲染的HTML没有复杂的JS动态加载所以最基础的requests就够了。方案优点缺点适用场景requests BeautifulSoup轻量、上手快、资源占用小无法执行JS、容易被JS渲染站点卡住服务端渲染的普通HTML页面selenium chromedriver能完整执行浏览器环境、抗JS反爬强重、慢、内存大、易被网站检测需要登录或大量JS动态渲染的站点scrapy框架异步并发、扩展性好、自带去重和调度学习曲线陡、小项目显笨重大规模分布式抓取、长期运营的采集系统这个项目的数据量小、轮询频率低用scrapy属于杀鸡用牛刀。而selenium虽然功能强但这里没有复杂的渲染场景反而会增加性能开销和被风控的概率。requests完全够用。如果你遇到的目标站是JS动态加载的比如接口返回JSON、前端渲染可以把解析部分换成selenium或者用requests直接调内部API这个我在后面的扩展章节会细说。2. 核心实现细节与难点解析2.1 目标站点分析与合规检查动手写代码之前我花了不少时间做侦察。视频站点“最近更新”栏目通常在首页或者独立的“更新”标签页。我先用浏览器的开发者工具F12详细看了一下网络请求页面是GET请求直接返回HTML没有额外的XHR请求这就说明数据在HTML里。合规检查方面我做了三件事查看网站的robots.txt确认允许爬虫访问的路径。查看网站的使用条款确认没有明确禁止自动采集。评估抓取频率做一个有礼貌的爬虫把请求间隔设置在3秒以上一天下来总请求量也才几百次对一个普通视频站来说毫无压力。这一步很多人会忽略但恰恰是区分“技术练习”和“违规采集”的关键分界线。爬虫没有问题有问题的是爬取方式、爬取范围和数据的用途。2.2 核心难点一请求头伪装与Web Driver检测第一个遇到的坑是请求头。最开始我用requests直接访问服务器返回403。原因很简单很多站点都会校验User-Agent和Referer没有这些浏览器特征的请求会被直接拒绝。解决方法是构造一份“浏览器味很浓”的请求头HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, Referer: https://example.com/, Upgrade-Insecure-Requests: 1, }这里有个容易踩的坑Accept-Encoding请求头里带上br之后服务器可能返回Brotli压缩格式。requests库默认不会解压br会导致你拿到的响应是乱码。解决办法是要么去掉br要么加上brotli库。我在项目里选择了去掉br只保留gzip, deflate免得额外引依赖。反爬严格一点的站点还可能会检测HTTP/2指纹、浏览器插件特征、Canvas指纹等如果你的目标站在这个级别requests方案就不太行了。那时候该换selenium就得换但这属于少数情况。2.3 核心难点二响应解析与定位BeautifulSoup的坑拿到HTML之后最核心的工作就是从结构里准确地提取每一条视频信息。视频站“最近更新”列表的DOM结构通常是这样的一个div.list容器下面若干div.item每个item里有一个a标签链接、一个img标签封面图、一个p.video-name标题、一个span.update-time更新时间。当时用BeautifulSoup写选择器时踩了一个很隐蔽的坑。起初我用find_all(div, class_item)结果发现只能匹配到前几个条目后面全部漏掉了。排查半天发现问题出在YouTube风格的外链视频卡片上——这个站点对视频数量超过某个阈值时列表里的某些条目类名会动态变化比如item item-card和item item-card-x通过部分匹配item就切断了。解决方法是用CSS选择器的属性包含匹配soup.select(div[class*item])这个*的意思是“属性值中包含子串”能智能匹配所有变形类名。类似的坑还有如果不小心把图片懒加载的>CREATE TABLE IF NOT EXISTS videos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, url TEXT UNIQUE NOT NULL, published_at TEXT, first_seen_at TEXT DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT CURRENT_TIMESTAMP );核心的去重逻辑是解析出的每一条视频先按URL查一次库不存在就INSERT并标记为“新增”存在就跳过。这样重复抓取不会引入脏数据而且全程内存占用极小。3. 完整实操过程与代码实现3.1 环境准备Python环境与依赖安装项目用Python 3.10开发环境是Windows 11但代码本身就跨平台的Linux和macOS都能跑。依赖一共三个requestsHTTP请求、beautifulsoup4页面解析、lxmlBeautifulSoup的解析器引擎速度比默认的html.parser快很多。python -m venv venv pip install requests beautifulsoup4 lxml如果你已经有老环境了建议新建一个虚拟环境避免版本冲突。我这台机器一开始用了个老Python 3.7环境requests版本太旧后面76行有个SSL证书问题折腾了很久。所以环境一定不要偷懒直接用最新的。3.2 核心代码框架这是整个项目的核心骨架下面是我项目的精简版代码保留最核心的爬取和检测部分。先看全貌再逐段解释。import requests import sqlite3 import time import re from datetime import datetime from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.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, } # 目标站的“最近更新”栏目页 TARGET_URL https://example.com/update DB_NAME video_updates.db def init_db(): conn sqlite3.connect(DB_NAME) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS videos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, url TEXT UNIQUE NOT NULL, published_at TEXT, first_seen_at TEXT DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() conn.close() def fetch_page(): resp requests.get(TARGET_URL, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def parse_video_items(html): soup BeautifulSoup(html, lxml) items [] for block in soup.select(div[class*item]): title_tag block.select_one(p.video-name a, a.video-name, h3 a) if title_tag is None: continue title title_tag.get_text(stripTrue) url title_tag.get(href) if url.startswith(//): url https: url elif url.startswith(/): url https://example.com url # 尝试提取发布时间 time_tag block.select_one(span.update-time, time) published if time_tag: published time_tag.get_text(stripTrue) if title and url: items.append({title: title, url: url, published_at: published}) return items def save_and_get_new(items): conn sqlite3.connect(DB_NAME) cursor conn.cursor() new_items [] for item in items: # 先按URL去重 cursor.execute(SELECT 1 FROM videos WHERE url?, (item[url],)) if cursor.fetchone() is None: cursor.execute( INSERT INTO videos (title, url, published_at) VALUES (?, ?, ?), (item[title], item[url], item[published_at]), ) new_items.append(item) conn.commit() conn.close() return new_items def run_once(): print(f[{datetime.now().strftime(%Y-%m-%d %H:%M:%S)}] 开始抓取 {TARGET_URL}) html fetch_page() items parse_video_items(html) print(f解析到 {len(items)} 条视频信息) new_items save_and_get_new(items) if new_items: print(f检测到 {len(new_items)} 条新更新) for item in new_items: print(f - {item[title]} | {item[url]} | {item[published_at]}) else: print(没有新增内容。) return new_items def run_loop(interval300): init_db() while True: try: run_once() except Exception as e: print(f抓取异常: {e}) print(f休眠 {interval} 秒...) time.sleep(interval) if __name__ __main__: run_loop(interval300)3.3 代码细节详解为什么这么写这个代码里藏着很多细节值得单独说。关于编码resp.encoding resp.apparent_encoding这一步很容易被忽略。视频站如果声明了charsetUTF-8但实际返回的是GBK编码不强制转码就会得到一堆乱码。apparent_encoding是requests基于响应字节自动推断的编码优先用它。关于链接补全网站页面里的href经常是相对路径或者协议相对路径//example.com/v/123如果直接存进数据库后面的通知推送、去重比对都会出问题。所以要做一个补全判断以//开头的拼上https:以/开头的拼上域名根地址。关于超时设置timeout10不是随手写的。网络请求必须设置超时否则requests在网络异常时可能一直挂起。如果爬虫进程崩了或者卡住就白跑了。10秒对于一个普通视频站页面来说是合理的等待时间。关于去重时机先查库再插入保证了UNIQUE约束不会因为竞态条件报错。SQLite的INSERT OR IGNORE也能做到同样的效果但我想在插入前就拿到新增的条目列表所以用了“先查询后插入”的写法逻辑上更直观。3.4 运行测试与观察记录我用一个虚构的视频站做了测试。第一次运行效果[2024-05-18 10:30:01] 开始抓取 https://example.com/update 解析到 24 条视频信息 检测到 24 条新更新 - 动画《星际探险》第10集 | https://example.com/v/1001 | 2024-05-18 10:20 - 纪录片《深海秘境》第3集 | https://example.com/v/1002 | 2024-05-18 09:15 ...第二次运行时这24条全部被去重识别为“已存在”输出为“没有新增内容”。这就验证了去重逻辑是有效的。我在服务器上连续跑了3天稳定性还算满意——没有内存泄漏没有请求崩溃。唯一遇到的异常是有一天网络抖动requests抛了个ConnectionError但因为外层有try-except程序没有退出休眠结束后继续跑。这个容错机制对长期运行的爬虫来说必不可少。4. 通知推送的实现与升级策略4.1 推送通知的几种方式对比光在控制台打印新更新还不够一个合格的更新监控爬虫要能在有新增内容时主动推给用户。常见的通知渠道有这几种渠道实现难度实时性依赖邮件SMTP低分钟级smtplib需邮箱授权码企业微信/钉钉机器人低秒级webhook地址Server酱微信推送极低秒级一个URL手机App通知推送到手机中秒级第三方推送服务我的选择是企业微信机器人。原因很现实注册一个企业微信内部群、添加机器人只需要5分钟而且拿到webhook地址后只需要往URL里POST一段JSON不需要处理邮件服务器的SMTP配置也不存在进垃圾箱的问题。4.2 企业微信机器人推送代码企业微信机器人的调用很简单核心逻辑就是向webhook地址提交一个JSON体def send_wx_notification(title, desc, url): webhook https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key你的KEY data { msgtype: text, text: { content: f【视频更新通知】\n{title}\n{desc}\n{url} } } resp requests.post(webhook, jsondata, timeout5) resp.raise_for_status()这里有个很关键的细节请求体一定要用jsondata而不是datajson.dumps(data)。requests库的json参数会自动帮我们把字典序列化成JSON并正确设置Content-Type为application/json。如果用字符串就会因为编码或格式问题被微信服务端拒绝。4.3 让爬虫定时执行调度方案的两种玩法这个脚本写完之后剩下一个问题怎么让它定时运行最简单的方案是用Python的time.sleep()在循环里一直跑。上文代码里的run_loop(interval300)就是干这个的。这种方式适合轻量场景但要小心进程被杀掉后没有任何自动恢复机制。更好的方案是落在系统层面Linux服务器上用cron表达式每5分钟调用一次python main.py但这种方式每次运行都要重新解析一次页面相对耗费资源。Windows任务计划程序方式和Linux的cron类似。用watchdog库监控文件变化这个高级玩法适合配置文件驱动的场景普通项目用不上。我的线上部署方式是写一个run_once()版本配置一个cron任务*/5 * * * *调用。这样每次检测都是独立进程即使某一次异常崩溃下次cron还会拉起新的检测天然具备自愈能力。4.4 调度版本的关键改造点这里有个不能忽略的细节run_once()版本需要重新初始化数据库连接吗要但不用重新建表。我的做法是让run_once()每次运行时自动调用init_db()因为建表语句带了IF NOT EXISTS重复调用不会报错还能保证表结构始终存在。这个小小的设计让cron方案和循环方案都能安全共用同一个入口函数。5. 常见问题与排查技巧实录5.1 服务器返回403 Forbidden怎么办这是爬虫新手遇到最多的拦路虎。403代表服务器知道你在请求但拒绝给你服务。我用一个排查清单来定位问题User-Agent问题服务器检查请求头发现是python-requests而不是正常浏览器直接拒绝。解决方案就是上文写好的一整套浏览器请求头。Cookie问题有些站点的首次访问会种一个Cookie比如first_access1第二次请求带上了这个Cookie才给正常内容。解决方法是先用session对象session.get()访问一次再带着cookie请求目标页面。IP级封禁如果你已经用浏览器请求头但还是403那多半是服务器检测到同一个IP的请求频率过高触发了封禁。这时候就只能退而求其次降低频率或者使用代理池。我项目里遇到的是第一种用了完整请求头后就解决了。之后就把请求头单独抽出来放在常量里所有请求复用。5.2 页面解析返回空列表解析出来的items列表是空的这种问题比403更隐蔽因为代码没报错但就是没数据。我遇到过三个原因选择器写错了视频站的DOM结构改了以前匹配的CSS路径失效了。排查方法是在解析前把HTML保存到本地文件用浏览器开发者工具逐步排查正确的选择器。内容在iframe里这是视频网站很常用的套路尤其是一些嵌入的播放器页面。主页面里只有一个iframe标签视频信息全在iframe加载的子页面里。处理方式是先找到iframe的src再用requests去请求这个子页面重新解析。页面异步请求页面本身是空壳数据是网页加载完成后通过XHR接口异步拿到的。这种情况用requests就看不到了要抓隐藏的API接口。具体做法是打开浏览器的Network面板过滤XHR请求找到返回JSON的那个URL直接用requests调这个API接口解析速度反而比解析HTML更快。5.3 标题乱码问题第一次跑的时候页面返回的内容在终端里是一堆怀疑人生。这多半是编码识别错误。我上面代码里已经强行设置resp.encoding resp.apparent_encoding但这招也不是万能的。还有一种更变态的情况是同一个页面里混合了多种编码比如HTML声明UTF-8但部分字段是GBK刺出来的这种情况只能针对性地做局部解码或者用re.sub清洗掉非法字符。5.4 反爬升级Selenium什么时候必须上如果你的目标站在检测到你的请求特征后开始在页面里植入WebAssembly混淆、行为验证码、或者要求执行JS才渲染内容那么requests方案就彻底不适用了。这时候就得切换到selenium undetected_chromedriver模式。我给当时一个朋友的项目做过同样的处理用selenium把页面渲染完成后直接执行driver.page_source抓取最终的DOM再扔给BeautifulSoup解析。注意一个细节selenium模式下尽量不要直接点击页面按钮容易触发验证码而是用execute_script(window.scrollTo(0, document.body.scrollHeight))做滚动加载模拟正常用户浏览轨迹。但也要明白让爬虫越来越复杂的本质是在跟网站的防御技术追逐。如果目标网站的防御很强强到你需要一天到晚处理验证码那就该停下来反思是不是该改用官方API或者与网站合作而不是继续在灰色地带走钢丝。5.5 非技术性的大坑要遵守网站的协议最后说一个很多人不爱听但必须说的话。爬虫本身是一项技术是中性的。但技术放在不正当的场景里就会害人害己。我在博客里写爬虫教程一定是要求读者遵守三个底线遵守robots协议网站已经明确说明不允许抓取的页面不碰。控制请求频率不要因为你的脚本造成目标网站负载过高。把请求间隔设置到3秒以上这是基本的礼貌。不用爬来的数据做二次牟利尤其是视频内容未经授权抓取、存储、分发视频文件法律上很容易定义成侵权行为。我的爬虫只取元数据不碰视频流这个边界从一开始就定了。5.6 遇到动态API接口时的正确姿势上文提过有些页面是API接口返回JSON的。实际项目里这种做法其实最“香”——接口返回结构化JSON解析比HTML轻松太多连BeautifulSoup都省了。比如某个视频站的更新接口是https://example.com/api/update?page1直接requests.get这个地址拿到的就是{ code: 0, data: { list: [ {title: 第10集, url: /v/1001, published_at: 2024-05-18 10:20} ] } }这种格式解析起来就简单了直接data[data][list]遍历就行而且接口一般还分页支持增量拉取。所以现在很多爬虫项目都优先找API而不是硬啃HTML。找API的方法就是看浏览器Network面板——在“最近更新”栏目往下滚动时能发现一个XHR请求刷新了页面内容那个请求的URL就是你要的接口地址。6. 扩展进阶把监控脚本升级成更强大的系统6.1 多站点监控统一配置与并发调度如果只是监控一个站点上面的代码已经完全够用了。但需求变多之后你会想监控多个视频站、多个频道。这时候就要考虑把“配置”和“代码”分离。我的做法是写一个简单的配置文件用Python的configparser或直接写一个dict里面维护一份站点列表。每个站点记录自己的URL、选择器、请求间隔、通知webhook。这样再加一个新站点只需要在配置里加一段代码完全不用动。并发这块不建议用多线程。视频站的接口通常不欢迎高并发多个站点同时请求还会把自己的请求频率推上去。我用的方案是协程信号量用asyncio和aiohttp设置一个全局的信号量控制在5个并发以内既快了又不会触发风控。6.2 可视化面板给非技术人员看更新数据机器人推送适合技术人员自己调试但如果这个工具要给运营同事用就免不了要做一个可视化界面。我后来抽空给这个项目写了个简单的Flask页面用SQLite作为数据源展示最近更新列表、更新趋势图、历史记录查询。技术点其实不难Flask里写一个/路由查询videos表最近24小时的记录渲染成一个简单的表格。你要更进一步还能用ECharts画个更新柱状图但这就是纯前端工作了。如果不想写Web界面直接用一个现成的SQLite浏览器软件看数据库记录也够了。6.3 保存历史趋势不止检测更新还能做数据分析爬虫跑久了积累的数据会变成一笔资产你能看到某个节目在一天里哪个时间段更新最频繁能看到某个创作者发布内容的节奏。这些都是原始站点不会直接告诉你的信息。数据积累思路比较简单在videos表里再加一个update_count字段每次检测到已存在的视频但发布时间有变化时就把计数加一。这样能区分“真正的第一发布”和“内容修改”。视频站偶尔会重剪、重编码导致发布时间更新这个字段能帮你看到哪些视频在反复修改。6.4 对接主流生态Server酱、钉钉、邮件一网打尽上面介绍了企业微信推送。如果你的环境没法用企业微信我整理了一份用Python发邮件的方案配合SMTP也很简单import smtplib from email.mime.text import MIMEText from email.header import Header def send_mail(title, content): msg MIMEText(content, plain, utf-8) msg[Subject] Header(title, utf-8) msg[From] senderexample.com msg[To] receiverexample.com with smtplib.SMTP_SSL(smtp.example.com, 465) as smtp: smtp.login(senderexample.com, 授权码) smtp.send_message(msg)服务器端记得只放行发件端口和必须的IP白名单避免自己的服务器变成垃圾邮件出口。7. 我的实操体会与建议这个项目跑通之后我最大的感受是爬虫的核心能力不在爬而在稳。代码能跑通一次不算什么难的是它能在无人值守的情况下连续跑一个月、三个月期间不挂不崩、数据不重不漏。所以我把大量精力花在了异常处理、去重策略和请求频率的控制上——这些边角功夫才是决定项目能不能长期运转的关键。另外这个方案里有一个很容易被忽略的精妙之处检测更新用的是元数据而不是文件指纹。这意味着即使网站改版了推流地址、换了播放器、加了防盗链只要页面信息结构没变我的爬虫就完全不受影响。这也让我体会到一个项目选择“做什么”往往比“怎么做”更重要——做一个轻量的元数据提醒器比做一个负重前行的下载器优雅得多。还有一个小技巧是运行过程中学到的把项目部署到服务器之后记得写一个看门狗脚本间隔3分钟检查一下爬虫进程是否存活发现挂了就自动拉起。我开始几天没有这个机制某次网络波动导致进程崩溃后监控整整停摆了12个小时等我发现才补救错过了好几条重要更新。最后如果你只是想追踪某个剧集的更新我这里的“最近更新列表”思路也完全可以适配改成先查详情页再把详情页里找到的所有分集记录进videos表判断逻辑照样通用。你也可以把这个项目再扩展成RSS订阅源让其他阅读器直接订阅你的监控结果。爬虫能解锁的玩法远比想象中多关键是每一个方向都要守住合规的底线做到技术精进和行为克制并行。