前阵子整理本地收藏夹,发现自己攒了不少影视资源相关的站点。收藏的时候一家一个链接,真要找起来才知道什么叫乱得离谱——有的站点一个月没登就失效了,有的换了域名,有的是在手机上收藏的电脑上根本没同步。我当时正好有台闲置的服务器,装了套PHP环境,干脆自己写了个自用的影视资源收藏导航页,把这些影视资源网站按分类、优先级、可用状态统一管起来。于是就有了这个项目:php收藏影视,十个值得收藏的影视资源网站。
这篇文章不只是给你看十个网站的清单,更是一份“用PHP从零搭一个收藏导航工具”的实操记录。适合这么几类人看:一是没事折腾PHP、想练手数据库和curl的人;二是收藏夹常年堆积几百条链接但找不到工具的影迷;三是想快速验证“一张表 + 几个页面”能不能解决真实问题的同学。整个项目只用了PHP自带的PDO、curl、json,没有引入任何框架,部署到宝塔或者phpStudy里都能跑。
1. 项目需求与整体设计思路
1.1 为什么用PHP而不是别的方案
收藏导航这类小工具,本质就是“一张表几个页面”,PHP天然适合这种轻量CRUD场景。我的理由很直接:PHP部署太省事了,传统LAMP环境到处都是,不挑机器;内置的PDO、curl、json系列函数齐全,不需要额外装模块;改个页面模板就能随时加功能,从写代码到跑起来半小时内完成。
拿它跟其他方案对比过,各有取舍:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 纯静态HTML | 零依赖、打开快 | 没法筛选排序、没法自动查失效 | 纯粹当书架用,不折腾 |
| PHP + MySQL | 轻量、部署方便、生态成熟 | 并发性能一般 | 个人收藏、家庭小站 |
| Python Flask | 写起来舒服、思路现代 | 服务器上要单独装Python环境 | 本来就有Python环境的人 |
| Node Express | 前后端一体、ESM生态好 | 内存占用偏高,配置略繁琐 | 想在工具里加实时推送玩法 |
我当时的需求其实很简单:收藏、分类、搜索、检测站点有没有失效、数据能备份、一键跳转。PHP+MySQL全部覆盖,而且这些功能全部用原生写法就能完成,没必要为一个个人工具上框架。数据量常年也就几百条收藏,MySQL性能完全够用,不需要上ES,不需要Redis,更不需要微服务那一套。
1.2 需求拆解:收藏工具到底要做什么
我开工前把需求写在了纸上,避免写着写着往“大而全”跑。最终落地到功能层面,就是下面这六块:
- 收藏新增:把标题、URL、分类、一句话描述、优先级入库。
- 分类展示:按影视资源类型筛选,比如官方平台、数据库、素材、字幕、社区。
- 关键词搜索:搜站点名称、URL和描述,快速定位。
- 批量状态检测:定期探测收藏的站点是否还能访问。
- 导入导出:收藏数据导成JSON,方便备份和换设备迁移。
- 安全跳转:点击跳转外站,不暴露多余信息,顺便记录点击次数。
也就是说,这不是一个“网址导航”项目,而是一个“带健康监控的个人收藏夹”。当时最吸引我的其实是第六个功能:站点失效检测。收藏夹里躺着一堆不知道还活不活的链接,手动一个个点开太蠢了,用curl批量探测能自动标出失效和不失效,这一块我在后面详细讲。
2. 核心功能实现:收藏与数据管理
2.1 数据库设计与字段取舍
表结构我一开始就按最朴素的单表来做,没有搞分类表、标签表那套复杂关系。影视收藏的数据量小,硬拆多张表只会让代码徒增JOIN,没有任何收益。最终表结构是这样的:
CREATE TABLE `fav_sites` ( `id` int(11) NOT NULL AUTO_INCREMENT, `category` varchar(50) NOT NULL DEFAULT 'other' COMMENT '分类:official/database/trailer等', `name` varchar(100) NOT NULL COMMENT '站点名称', `url` varchar(255) NOT NULL COMMENT '站点URL', `desc` text COMMENT '一句话描述', `priority` tinyint(3) NOT NULL DEFAULT 5 COMMENT '优先级1-10,数字越大越靠前', `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1正常,0失效', `click_count` int(10) NOT NULL DEFAULT 0 COMMENT '点击次数', `last_check` datetime DEFAULT NULL COMMENT '最后检测时间', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_url` (`url`), KEY `idx_category` (`category`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏的影视资源站点';几个细节说说我的设计意图。url加了唯一索引,这样数据库层面就杜绝了重复收藏,后面代码里查重只是多一道保险。category用varchar直接存英文枚举值,比存数字代号再关联字典表直观得多——我在PHP里写代码时直接写'trailer'比写3好理解。click_count是后来加的,不用额外建日志表就能看哪些站点用得最多。status默认为1,也就是新增的收藏默认视为正常,检测任务跑过之后再把失效的改成0。
2.2 PHP连接MySQL的坑与正确姿势
数据库连接这一步看着简单,实际坑不少。最大的坑就是中文乱码。很多人建库时用了utf8,插入中文后查询乱码,折腾半天,其实是字符集不一致。正确的做法是DSN里显式带上charset=utf8mb4,建表语句里也指定相同的字符集,PHP文件保存为UTF-8无BOM,三层统一,中文描述就稳了。
<?php // db.php 数据库连接 $host = '127.0.0.1'; $port = 3306; $dbname = 'media_shelf'; $user = 'root'; $pass = 'your_password'; $dsn = "mysql:host=$host;port=$port;dbname=$dbname;charset=utf8mb4"; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; try { $pdo = new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { // 生产环境建议记录日志,不要直接把异常信息输出给用户 error_log($e->getMessage()); exit('数据库连接失败,请检查配置。'); }PDO::ATTR_EMULATE_PREPARES一定要设成false。这个选项控制是否模拟预处理,设成false以后,参数会真正以预处理方式发送给MySQL,能有效防止SQL注入。个人工具虽然不追求高安全等级,但“不要让你的SQL变成用户输入的一部分”这个习惯在什么时候都不多余。连接异常时我用了error_log记录详细原因,页面上只给用户一个模糊提示,避免把账号密码泄露到页面里。
2.3 收藏入库的完整流程
收藏功能是所有操作的核心入口。完整流程是:表单提交 -> CSRF Token验证 -> URL规范化 -> 查重 -> 入库。每一个步骤都有原因。
CSRF验证其实非常轻量:页面加载时生成一个Token存Session,提交时比对表单Token和Session值。个人工具虽然不太可能被打CSRF,但收藏操作如果接口随便能被第三方页面触发,你莫名其妙就会多出一堆收藏垃圾。防范成本只有几行代码,值得做。
// 收藏新增入口 add.php session_start(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!isset($_POST['token']) || $_POST['token'] !== ($_SESSION['token'] ?? '')) { exit('请求来源异常,请刷新页面重试'); } $name = trim($_POST['name'] ?? ''); $url = trim($_POST['url'] ?? ''); $desc = trim($_POST['desc'] ?? ''); $category = trim($_POST['category'] ?? 'other'); $priority = (int)($_POST['priority'] ?? 5); if ($name === '' || $url === '') { exit('站点名称和地址不能为空'); } $result = add_site($name, $url, $desc, $category, $priority); // 根据$result跳转或输出提示 }add_site函数是收藏逻辑的核心,URL规范化这步容易被忽略。用户很可能会输入“www.example.com”或直接粘贴“example.com/x/y”,不处理直接入库会导致批量检测时curl报错。我统一补全协议头,顺手用strtolower对域名部分做小写处理,避免同一站点输入大小写不同重复入库。
function add_site($name, $url, $desc = '', $category = 'other', $priority = 5) { global $pdo; $url = trim($url); // 补全协议头 if (!preg_match('#^https?://#i', $url)) { $url = 'http://' . $url; } // 查重 $stmt = $pdo->prepare('SELECT id FROM fav_sites WHERE url = ? LIMIT 1'); $stmt->execute([$url]); if ($stmt->fetch()) { return ['code' => 0, 'msg' => '这个地址已经在收藏夹里了']; } $stmt = $pdo->prepare( 'INSERT INTO fav_sites (name, url, `desc`, category, priority, status, last_check) VALUES (?, ?, ?, ?, ?, 1, NULL)' ); $stmt->execute([$name, $url, $desc, $category, $priority]); return ['code' => 1, 'msg' => '收藏成功']; }这里有个PHP写法的细节:desc是MySQL的保留字,INSERT时要用反引号包裹,否则语法报错。我在SQL里写了`desc`,这个反引号在视觉上很容易被忽略,但少了它SQL直接跑不起来。还有一个点是优先级priority,我为它定的语义是“数字越大越靠前”,后面列表排序时直接ORDER BY priority DESC就行,不用额外转换逻辑。
3. 批量检测、搜索与前端交互
3.1 站点失效检测:curl_multi并发探测
这是整个工具里技术含量最高的一块,也是最实用的。收藏夹里一两百个链接,如果用人肉点的办法去看哪个失效了,下午的时间就全搭进去了。PHP的curl_multi_*系列函数可以并发发包,一次请求多个站点,速度非常快。
// check.php 批量检测站点状态 function check_sites($items) { $mh = curl_multi_init(); $handles = []; foreach ($items as $id => $url) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => false, CURLOPT_NOBODY => true, // 只拿响应头,不下载正文 CURLOPT_FOLLOWLOCATION => true, // 跟随跳转 CURLOPT_TIMEOUT => 5, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36' ]); curl_multi_add_handle($mh, $ch); $handles[$id] = $ch; } do { $status = curl_multi_exec($mh, $active); if ($active) { curl_multi_select($mh); } } while ($active && $status === CURLM_OK); $results = []; foreach ($handles as $id => $ch) { $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $errno = curl_errno($ch); // errno等于0说明请求无传输错误,此时HTTP_CODE才有参考意义 $results[$id] = $errno === 0 ? $httpCode : 0; curl_multi_remove_handle($mh, $ch); curl_close($ch); } curl_multi_close($mh); return $results; }关于为什么要设CURLOPT_NOBODY => true:用GET方法访问一个页面会把整个HTML都下载下来,如果某个站点资源很大,探测就会又慢又耗流量。NOBODY改成HEAD请求,只获取响应头,服务器返回的状态码足以判断站点是否正常。实测下来,一次检测70个站点,并发模式下总耗时在3到5秒,换成串行循环的话可能等到一分钟开外。
关于检测频率,血的教训是不要跑得太勤。第一次上线时我先设了每10分钟自动探测一次,结果第二天那几个影视站全部把我的IP封了,页面直接403。后来我改成每天凌晨3点跑一次,检测结果只更新last_check和status两个字段,服务器压力小,站点的反爬阈值也摸清楚了。个人工具的核心思路是“低调、低频、高效”,不要让你的收藏夹变成捅马蜂窝的那根棍子。
3.2 状态码怎么判定有效还是失效
拿到HTTP状态码后,不能简单认为“200就是正常,其他全部失效”。实际跑了两轮就发现很多站点返回30x跳转,特别是官方平台域名经常换,但从www.example.com跳到www.example.tv,页面依然能用,这是正常的短跳转,curl已经按FOLLOWLOCATION自动追进去了。真正要标记为失效的是这样几类:
| 情况 | 常见HTTP状态码 | 处理方式 |
|---|---|---|
| 正常访问 | 200 | 状态正常 |
| 临时/永久跳转 | 301、302 | 视为正常(已跟随跳转) |
| 资源被删除 | 410 | 标记失效 |
| 访问被拒绝 | 403 | 标记为“疑似反爬”,下次换UA再试 |
| 找不到页面 | 404 | 先标记失效,手动确认 |
| 网络层错误 | 0 | 标记失效,稍后再补测 |
所以更新状态的逻辑我做了两层:如果返回200或者跟随跳转后得到200,直接标记正常;如果返回403这类“服务器拒绝”的结果,不要急着标失效,因为可能是本次探测UA被拦了,我会在第二天检测时换个浏览器UA再试一次。只有连续两次都拿不到有效响应,才把status改成0。
3.3 搜索与分类筛选
列表页的搜索函数是整个工具使用频率最高的功能。它支持三个筛选维度:分类、关键词、状态。关键词匹配范围覆盖站点名称、URL和描述。这里容易踩的坑是用户输入的搜索词可能包含LIKE通配符,比如搜“100%”时%会被当成通配符导致全部匹配。正确的做法是先把通配符转义掉。
function get_sites($category = '', $keyword = '', $status = '') { global $pdo; $sql = 'SELECT * FROM fav_sites WHERE 1=1'; $params = []; if ($category !== '' && $category !== 'all') { $sql .= ' AND category = ?'; $params[] = $category; } if ($keyword !== '') { $kw = '%' . str_replace(['%', '_'], ['\%', '\_'], $keyword) . '%'; $sql .= ' AND (name LIKE ? OR url LIKE ? OR `desc` LIKE ?)'; $params[] = $kw; $params[] = $kw; $params[] = $kw; } if ($status !== '' && $status !== 'all') { $sql .= ' AND status = ?'; $params[] = (int)$status; } $sql .= ' ORDER BY priority DESC, id DESC'; $stmt = $pdo->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(); }这个函数写在functions.php里,列表页、搜索页共用同一份逻辑。用PDO预处理动态拼接的参数,全程走?占位符传递,不直接拼字符串,安全性有保障。str_replace转义通配符那行代码短但是关键,没有它搜索功能会在特殊字符上翻车。
3.4 列表页视图与跳转统计
列表页面我用最简单的PHP模板输出,每一行数据卡片包含名称、描述、分类标签、状态角标、优先级数字和操作按钮。输出时要统一做HTML转义,防止收藏数据里混入脚本代码,这是XSS防护的基本功。收藏工具虽然是自己用,但如果页面被分享出去,转义习惯就能避免很多问题。
点击跳转我做了个小小的中间层:列表里的链接不直接指向外站,而指向go.php?id=xxx,在这个跳转页面里做两件事——先给对应记录的click_count字段加1,然后通过header('Location: ' . $url)完成跳转。这样做的好处有两点,一是能统计哪些站点真的在用,二是以后如果想加“访问前提示确认”之类的功能,只需要改中间层一处。
3.5 数据备份:JSON导出与导入
备份功能是我第三个版本才加上的,但它是整个工具里存活价值最高的功能。为什么?因为收藏夹内容一旦积累到几百个链接,全部手动重新输入一遍是非常痛苦的事。JSON导出导入方案把迁移时间从一小时压缩到一分钟。
function export_json() { global $pdo; $sites = $pdo->query('SELECT name, url, `desc`, category, priority FROM fav_sites')->fetchAll(); // JSON_UNESCAPED_UNICODE 防止中文被转成 \uXXXX 编码 return json_encode($sites, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } function import_json($json) { global $pdo; $data = json_decode($json, true); if (!is_array($data)) { return 0; } $count = 0; $stmt = $pdo->prepare('INSERT IGNORE INTO fav_sites (name, url, `desc`, category, priority, status) VALUES (?, ?, ?, ?, ?, 1)'); foreach ($data as $item) { $stmt->execute([ $item['name'], $item['url'], $item['desc'] ?? '', $item['category'] ?? 'other', (int)($item['priority'] ?? 5) ]); $count += $stmt->rowCount(); } return $count; }导出时JSON_UNESCAPED_UNICODE这个选项很关键。不传的话,PHP的json_encode会把中文描述转成\uXXXX形式,导出的文件能看但没法编辑;传了之后保留中文原样,备份文件打开就是可读文本。导入用的是INSERT IGNORE,配合数据库的uk_url唯一索引,重复收藏的链接自动跳过,不会产生脏数据。我实际把这份JSON存在网盘和本地两个地方,换机器部署新环境时,建好表跑一次导入脚本,所有收藏原样恢复。
3.6 顺便说说接口跨域
如果你的列表页和PHP接口不在一台服务器上,就会遇到跨域问题。PHP处理起来很简单,接口返回前加上CORS头就行。
header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Origin: *'); // 如果是JSONP场景,老接口兼容写法 $callback = $_GET['callback'] ?? ''; if ($callback) { echo $callback . '(' . json_encode($data, JSON_UNESCAPED_UNICODE) . ')'; } else { echo json_encode($data, JSON_UNESCAPED_UNICODE); }Access-Control-Allow-Origin: *适合公开接口,如果只给自家前端页面用,更稳的是指定具体域名,避免跨站请求恶意调用你的接口。JSONP算是老方案了,现在项目里有CORS基本用不上,但如果你维护一些别人写的传统PHP接口,还是会看到这种callback参数写法,知道是怎么回事就行。
4. 十个值得收藏的影视资源网站分类参考
4.1 我实际收藏了什么,以及为什么值得收藏
工具搭好了,接下来要喂数据。标题里说“十个值得收藏的影视资源网站”,我在实际采集时并不是看到一个收藏一个,而是按“类目”去收录的。因为影视相关内容实在太多,只收藏站点不分类的话,和以前的浏览器收藏夹没有区别。
我最后沉淀出这十个类目,每个类目对应一个“方向”,经过筛选后留存下来的站点才是值得长期跟踪的:
| 分类标识 | 类目方向 | 为什么值得收藏 | 我设置的分级 |
|---|---|---|---|
| official | 大型正版平台官网 | 内容更新稳定、域名变动少、最可靠 | 10 |
| database | 影视数据库与评分站 | 查资料、看评分、作品关联信息最全 | 9 |
| trailer | 官方预告片与物料站 | 新片信息第一手来源,素材正规可用 | 8 |
| material | 公开样片与测试素材 | 开发者测试播放器、CDN时需要无版权争议的视频 | 7 |
| subtitle | 字幕资源站 | 配合本地播放器使用,解决外文片字幕问题 | 7 |
| public | 公版影片与老片修复库 | 版权过期作品,可放心收藏和引用 | 6 |
| static | 剧照海报素材库 | 写文章配图、做海报墙需要高清剧照 | 6 |
| news | 影视资讯站 | 行业动态、档期变动、票房数据集中 | 5 |
| community | 影迷论坛与讨论区 | 淘片线索、版本对比、幕后信息都在讨论里 | 5 |
| tool | 本地媒体管理工具官网 | 服务端媒体库、下载整理工具,配合自建家庭影院 | 8 |
这里要强调一个原则:收录站点时优先选官方、公开、无版权争议的资源,这类站在长期使用中稳定性最高。比如公版电影资源站,因为版权过期,域名被投诉关停的概率低;而随意转码的盗版资源站,换个域名是家常便饭,今天收藏明天失效,维护成本极高。我第一版收藏夹里就混了不少来路不明的站,检测结果一片飘红,后来按这个原则清洗了一遍,健康率大幅提升。
4.2 收藏一批数据后的筛选思路
收藏进去只是第一步,真正有价值的是定期清洗。我的做法是每个季度跑一次完整检测,把status=0的失效站点拉出来人工确认。确认时重点看三件事:这个站是不是真没了,只是临时扛不住流量挂了;有没有备用域名,有的话顺手更新URL;如果彻底消失,果断删除记录。
这十个类目扫码下来,你会发现它们覆盖的不只是“看片”这一个环节,而是从“发现新片”到“确认资料”再到“观看收藏”的完整链路。这也是我觉得收藏影视资源网站最有价值的地方:你收藏的不是一个个孤立的链接,而是一套信息获取的渠道组合。工具只是帮你管理这套组合,真正的筛选判断还是得靠自己的使用习惯来沉淀。
5. 常见问题与排查技巧
5.1 实际运行中遇到的典型问题
项目跑起来之后,各种小问题接踵而至。我把几个最常见的问题整理成了一张速查表,照着排查基本都能解决:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 中文保存后变乱码 | 字符集不一致 | PHP文件、DSN、数据库表全部统一为utf8mb4 |
| curl检测全部返回200但站实际打不开 | 站点有防盗链,静默返回200 | 加Referer头,配合浏览器UA再试 |
| 检测结果全是403 | 同一IP请求太频繁被封 | 降低检测频率,改成每日一次,加随机间隔 |
| 列表页空白但数据库有数据 | PHP报错被隐藏 | 开启display_errors临时排查,查看error_log |
| 重复收藏还能进库 | 唯一索引没生效 | 检查uk_url索引是否创建成功,业务层也要查重 |
| 页面导入JSON后分类全部变成other | 导入数据里没有category字段 | 老备份文件补上默认分类字段再导入 |
| 页面跳转提示“过多重定向” | 站点强制HTTPS但检测时走了HTTP | URL规范化时优先保留https,跟随跳转开启即可 |
5.2 环境与版本问题
有次在旧服务器上部署时,发现PHP版本停留在5.6,PDO的ERRMODE_EXCEPTION虽然能用,但很多新特性缺失,curl_multi_select在超长阻塞时表现也不稳。后来我直接把PHP版本升级到7.4以上,语法写起来更顺手,性能也有明显提升。如果你用的是宝塔面板或者phpStudy,升级PHP版本就是点两下的事,但升级后要把PHP的扩展开关确认一遍——pdo_mysql、curl、openssl这三个扩展缺一不可。
5.3 个人PHP工具的安全底线
虽然只是自己用的收藏工具,但安全习惯还是要养成的。我的底线是三条:一是不在页面上暴露PHP报错信息,开发期开着display_errors方便排查,上线后一定关掉,错误记录全部走error_log;二是不把SQL拼接当成常规操作,任何外部数据都通过PDO预处理传入;三是源码文件和备份的JSON不放公共目录,部署时放到网站根目录外的子目录或者设置目录访问权限。
这里多说一句,如果你的机器部署过一段时间的PHP项目,偶尔可以在日志里看到被扫描的痕迹。个人小工具更要把入口收敛好,不需要对外暴露的页面都用Session校验拦上,收藏操作也加了CSRF Token,做到这两点基本能挡住绝大多数瞎试的请求。
最后留个尾
这个工具我实际用了小半年,最深的体会是:收藏工具千万别贪功能,能用一张表解决的问题就不要上多表,能本地跑完的检测就不要上队列。现在这版PHP+MySQL+curl的组合已经稳定运行很久,日常维护就是每季度跑一次检测、看看有没有失效站点,剩下的时间它安安静静躺在服务器角落。
最后再分享一个习惯:每次往收藏夹里加站点时,顺手补一句描述和分类,这一下看似多余,但在一个月之后整理时,救命程度堪比备份。影视资源网站那么多,真正值得长期跟着走的其实就那么几个类目,认准方向再动手收藏,比见链接就存要高效得多。我就是靠这套办法,把之前满屏失效链接的收藏夹,慢慢整顿成了现在这个一眼能看穿全局的小工具。