Laravel+QueryList电影站源码:自适应内容聚合实战

📅 发布时间:2026/10/13 16:25:08
Laravel+QueryList电影站源码:自适应内容聚合实战
简介这份资源是一套基于 Laravel 与 QueryList 框架搭建的影视站点源码面向希望快速上线影视内容站点的开发者与站长尤其适合具备一定 PHP 基础、想省去从零开发成本的中级用户。源码主打自动采集影视资源前端采用电脑与手机自适应布局并支持自行添加尝鲜设置、对接微信自定义回复图片等扩展玩法可用于搭建个人影视导航或内容聚合平台。压缩包为 rar 格式整体约 20.74MB文件包内附有安装说明便于按文档完成部署与调试。目前已有 550 人学习下载说明该方案在同类影视建站需求中具备一定参考价值。读者可从中获取完整的站点目录结构、采集逻辑实现思路、前后端自适应模板以及微信接口对接示例适合对照源码理解 Laravel 项目组织方式与 QueryList 采集流程并在此基础上二次开发或排错调试。1. 电影站源码-laravelquerylist框架-自适应一套能跑起来的内容聚合方案很多人第一次看到「电影站源码-laravelquerylist框架-自适应」这个组合第一反应是「这不就是个采集站模板吗」。但真正动手搭过的人知道Laravel 负责的是整站骨架——路由、ORM、模板、队列、缓存、后台权限QueryList 负责的是把外部页面里的结构化数据抠出来转成模型能直接入库的数组。两者拼在一起才是一套能长期维护的内容聚合系统而不是一次性爬完就烂尾的脚本。这套方案适合三类人想学 Laravel 实战但缺一个完整业务场景的后端开发者需要把多个来源的影视元数据标题、封面、简介、播放地址、分类统一到自己库里的站长以及想研究「采集 去重 自适应前端」这条链路的工程师。它解决的核心问题不是「怎么爬」而是「爬完之后怎么让数据像正常业务数据一样被管理、被检索、被展示」。下面按选型、落地、避坑、进阶的顺序拆开讲。2. 为什么是 Laravel 加 QueryList选型逻辑与最小可跑环境2.1 两个框架各自负责什么边界在哪Laravel 在这套系统里承担的是「业务层」用户表、影片表、分类表、播放源表、采集任务表都由 Eloquent 管理前台列表页、详情页、搜索页走 Blade 或 API 输出后台用中间件做权限隔离采集任务用队列异步执行避免请求超时。QueryList 承担的是「抽取层」给它一个 URL 和一组选择器规则它返回 DOM 节点集合再通过map或each把节点转成数组。两者的衔接点通常是一个 Service 类比如MovieSpiderService内部调用 QueryList外部返回标准数组给 Laravel 的 Model 去updateOrCreate。边界要划清楚QueryList 不负责去重、不负责限速、不负责重试这些都得在 Laravel 侧用队列、缓存锁和异常处理来做。很多新手把重试逻辑写进 QueryList 的onError回调里结果队列一堆积就雪崩。常见做法是 QueryList 只做「抓取 解析」失败就抛异常由 Laravel 的 Job 决定重试几次、间隔多久。2.2 环境准备与依赖安装的最小命令集先确认 PHP 版本。Laravel 10 要求 PHP 8.1 以上QueryList 4.x 对 PHP 7.4 到 8.2 都兼容。数据库用 MySQL 5.7 或 8.0 都行但注意utf8mb4字符集影视标题里经常有生僻字和 emoji。# 创建 Laravel 项目假设已装 Composer composer create-project laravel/laravel movie-aggregator cd movie-aggregator # 安装 QueryList 核心包 composer require jaeger/querylist # 安装 Redis 扩展用于队列和缓存按系统实际包名调整 # Ubuntu/Debian 示例 sudo apt install php8.1-redis # 安装后重启 PHP-FPM sudo systemctl restart php8.1-fpm # 配置 .env 中的数据库和队列 # DB_CONNECTIONmysql # DB_DATABASEmovie_agg # QUEUE_CONNECTIONredis # CACHE_DRIVERredis安装完成后用php artisan queue:work启动队列监听。这里有个血泪经验队列驱动别用database采集任务量大时jobs表会锁得你怀疑人生Redis 驱动稳定得多。参数上queue:work加--tries3 --timeout120超时设 120 秒是因为部分来源页面加载慢QueryList 默认的 Guzzle 超时是 30 秒需要在实例化时单独调。2.3 数据库表结构的最小设计至少需要四张表movies影片主表、movie_sources播放源一个影片多个源、categories分类、spider_tasks采集任务记录。movies表的关键字段包括title、slug、cover_url、description、category_id、source_hash。source_hash是去重核心用「标题 年份 来源站标识」做 MD5入库前先查这个哈希是否存在。// database/migrations/xxxx_create_movies_table.php 片段 Schema::create(movies, function (Blueprint $table) { $table-id(); $table-string(title)-index(); $table-string(slug)-unique(); $table-string(cover_url)-nullable(); $table-text(description)-nullable(); $table-unsignedBigInteger(category_id)-nullable(); $table-string(source_hash, 32)-unique(); // 去重关键 $table-timestamps(); });source_hash加唯一索引配合updateOrCreate就能实现「存在则更新不存在则插入」。注意别用title做唯一索引不同来源的同一部影片标题可能有细微差异比如多一个「HD」后缀用哈希更稳。3. 用 QueryList 写采集规则从选择器到入库的完整链路3.1 一个可复用的采集 Service 类不要在每个 Job 里重复写 QueryList 的初始化代码。抽一个基类把 Guzzle 的超时、User-Agent、重试次数统一配置。下面是一个最小可用的采集服务采集列表页里的影片卡片。?php namespace App\Services; use QL\QueryList; use GuzzleHttp\Client; class MovieSpiderService { protected QueryList $ql; public function __construct() { // 自定义 Guzzle 客户端控制超时和请求头 $client new Client([ timeout 30, headers [ User-Agent Mozilla/5.0 (compatible; MovieAggregator/1.0), ], ]); $this-ql new QueryList(); $this-ql-setClient($client); } /** * 采集列表页返回影片数组 * param string $url 列表页地址 * param array $rules 选择器规则 * return array */ public function fetchList(string $url, array $rules): array { // 注意QueryList 的 rules 里 key 是字段名value 是选择器数组 $data $this-ql-get($url)-rules($rules)-query()-getData(); return $data-all(); } }逻辑说明setClient替换默认的 Guzzle 实例这样超时和 UA 可控。rules的格式是[title [.card-title, text], cover [.card-img img, src]]第二个元素是属性名text表示取文本src表示取图片地址。参数上timeout设 30 秒是折中值太短容易漏采太长会拖垮队列UA 建议带上项目标识方便对方站长识别减少被封概率。3.2 列表页与详情页的两级采集列表页通常只有标题和封面简介和播放地址在详情页。做法是先用fetchList拿到详情页链接再对每个链接发起第二次请求。这里必须用队列不能在同一个请求里循环否则 100 个影片就能让 PHP 超时。// app/Jobs/SpiderDetailJob.php namespace App\Jobs; use App\Services\MovieSpiderService; use App\Models\Movie; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class SpiderDetailJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $tries 3; public $timeout 120; protected string $detailUrl; public function __construct(string $detailUrl) { $this-detailUrl $detailUrl; } public function handle(MovieSpiderService $spider) { $rules [ title [.detail-title, text], description [.detail-desc, text], cover [.detail-cover img, src], play_url [.play-list a, href], ]; $data $spider-fetchList($this-detailUrl, $rules); if (empty($data)) { throw new \Exception(采集为空: . $this-detailUrl); } $item $data[0]; $hash md5($item[title] . | . parse_url($this-detailUrl, PHP_URL_HOST)); Movie::updateOrCreate( [source_hash $hash], [ title trim($item[title]), cover_url $item[cover] ?? , description $item[description] ?? , slug str_slug($item[title]) . - . substr($hash, 0, 6), ] ); } }逻辑说明updateOrCreate的第一个参数是查询条件第二个是更新或插入的值。source_hash用标题加来源域名生成保证同一来源的同一影片不会重复插入。tries3表示失败重试三次timeout120给慢页面留足时间。注意play_url如果有多条getData()返回的是数组需要单独处理这里只取了第一条做示例。3.3 采集频率控制与队列并发参数QueryList 本身不限速限速得靠 Laravel 的队列和缓存。常见做法是在 Job 的handle开头用Cache::lock加一个按域名维度的锁同一域名同时只允许一个采集任务执行间隔用sleep或RateLimiter控制。use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\RateLimiter; public function handle(MovieSpiderService $spider) { $host parse_url($this-detailUrl, PHP_URL_HOST); // 每个域名每分钟最多 20 次请求 $executed RateLimiter::attempt( spider: . $host, 20, function () use ($spider) { // 实际采集逻辑 }, 60 ); if (! $executed) { // 超出限制释放回队列稍后重试 $this-release(30); } }参数说明RateLimiter::attempt的第二个参数是允许次数第三个是回调第四个是时间窗口秒。release(30)表示 30 秒后重新入队。队列 worker 数量建议从 2 个起步php artisan queue:work --queuespider,default把采集任务单独放一个队列避免和普通业务任务抢资源。4. 自适应前端与数据展示让采集来的内容像原生数据4.1 自适应布局的三个断点与模板组织「自适应」在这类站点里通常指前台模板能在手机、平板、桌面三种宽度下正常显示。用 Blade 加 CSS Grid 就能做不需要引入重型前端框架。断点设 640px、1024px、1280px 三档列表页用grid-template-columns: repeat(auto-fill, minmax(160px, 1fr))详情页播放器区域用aspect-ratio保持比例。/* resources/css/app.css 片段 */ .movie-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; } media (min-width: 1024px) { .movie-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } } .player-wrapper { aspect-ratio: 16 / 9; width: 100%; }逻辑说明auto-fill加minmax让列数随容器宽度自动变化手机上一列或两列桌面四到六列。aspect-ratio是现代浏览器都支持的属性比 padding-top 百分比方案更直观。注意采集来的封面图尺寸不一加object-fit: cover避免拉伸变形。4.2 播放地址的存储与前端渲染采集来的播放地址通常是第三方页面的链接不是直链。存储时用单独的movie_sources表一个影片对应多条记录字段包括movie_id、source_name、play_url、sort_order。前端渲染时按sort_order排序用选项卡切换不同来源。// 详情页控制器片段 public function show($slug) { $movie Movie::where(slug, $slug) -with([sources function ($q) { $q-orderBy(sort_order); }]) -firstOrFail(); return view(movie.show, compact(movie)); }模板里遍历$movie-sources每个源渲染一个按钮点击后把play_url塞进 iframe 的src。注意 iframe 要加referrerpolicyno-referrer否则部分来源会校验来源页。参数上sort_order默认 0采集时按页面出现顺序赋值方便人工调整。4.3 搜索与分类的索引优化采集来的数据量大了之后搜索会变慢。title字段加普通索引如果要做模糊搜索MySQL 的LIKE %关键词%用不上索引常见做法是加一个search_text字段把标题、简介、分类拼在一起用全文索引或者交给外部搜索引擎。小规模站点用WHERE title LIKE ?加LIMIT就够了但记得在movies表上对category_id和created_at建联合索引列表页按分类筛选再按时间排序时能走索引。-- 联合索引示例 ALTER TABLE movies ADD INDEX idx_category_created (category_id, created_at DESC);注意DESC索引在 MySQL 8.0 才真正生效5.7 会忽略。如果还在用 5.7把created_at换成id倒序也能达到类似效果。5. 避坑与排查采集站最容易翻车的五个地方5.1 现象采集一段时间后全部返回空数据原因通常是目标站加了反爬识别了你的 UA 或请求频率。解决先换 UA 测试如果恢复说明是 UA 被标记如果换 UA 无效检查是否 IP 被限。降低频率把RateLimiter的每分钟次数从 20 降到 5观察是否恢复。另外 QueryList 默认不携带 Cookie部分站点需要先访问首页拿 Cookie可以在 Guzzle 客户端里开cookies true。5.2 现象入库时唯一索引冲突报错原因source_hash生成规则里包含了来源域名但同一部影片从不同来源采集时哈希不同理论上不会冲突。冲突往往是因为同一来源的同一影片被两个 Job 同时处理。解决在 Job 里用Cache::lock(hash: . $hash, 10)加锁或者把updateOrCreate包在try-catch里捕获QueryException后忽略。更彻底的做法是队列 worker 设为单进程处理同一来源。5.3 现象详情页采集到的播放地址是空数组原因选择器写错了或者页面结构是 JavaScript 渲染的。QueryList 基于 Guzzle 抓的是原始 HTML不执行 JS。解决先用浏览器查看页面源码不是审查元素确认播放地址是否在初始 HTML 里。如果确实是 JS 渲染QueryList 无能为力需要换用支持无头浏览器的方式但那会大幅增加资源消耗建议优先找有服务端渲染的来源站。5.4 现象队列任务积压CPU 跑满原因queue:work启动太多或者单个 Job 里循环请求太多页面。解决控制 worker 数量采集队列单独用 1 到 2 个 worker把批量采集拆成单个 Job一个 Job 只处理一个详情页。另外检查是否有 Job 陷入死循环重试failed_jobs表里如果有大量相同异常先修异常再重启队列。5.5 现象前端封面图加载慢或 403原因采集时直接存了对方站的图片地址对方做了防盗链。解决在 Laravel 侧加一个图片代理路由用 Guzzle 带上正确的 Referer 去请求图片再输出给前端。或者采集时就把图片下载到本地storage目录用本地地址。后者更稳但要注意磁盘空间和清理策略。// 图片代理路由示例 Route::get(/img-proxy, function (\Illuminate\Http\Request $request) { $url $request-query(url); $client new \GuzzleHttp\Client([headers [Referer parse_url($url, PHP_URL_SCHEME) . :// . parse_url($url, PHP_URL_HOST)]]); $resp $client-get($url); return response($resp-getBody(), 200) -header(Content-Type, $resp-getHeaderLine(Content-Type)); });注意这个代理路由要做白名单校验只允许代理采集时记录过的域名否则会变成公开代理被人滥用。6. 进阶技巧用自定义 QueryList 扩展处理复杂页面6.1 当选择器不够用时写一个自定义采集器有些来源站的播放地址藏在>$rules [ cover [.card-img, data-src, , function ($content) { // 补全相对路径 if (strpos($content, http) ! 0) { return https://example-source.com . $content; } return $content; }], ];第四个参数是回调$content是选择器取到的原始值。这个技巧在处理懒加载图片时特别有用很多站点把真实地址放在>$emptyCount collect($data)-filter(function ($item) { return empty($item[title]) || empty($item[play_url]); })-count(); if ($emptyCount / max(count($data), 1) 0.5) { \Log::warning(采集规则可能失效, [url $this-detailUrl, empty_rate $emptyCount / count($data)]); }这个习惯帮我省了很多事后排查的时间。规则失效往往不是突然全空而是先部分字段变空有日志就能提前发现。6.3 一个验证采集质量的小脚本写完规则后别急着入库先用一个 Artisan 命令跑单页验证。命令里调用MovieSpiderService把结果打印成表格人工核对字段是否对得上。// app/Console/Commands/TestSpider.php public function handle(MovieSpiderService $spider) { $rules [ title [.detail-title, text], cover [.detail-cover img, src], ]; $data $spider-fetchList(https://example-source.com/detail/123, $rules); $this-table(array_keys($data[0] ?? []), $data); }跑php artisan spider:test就能看到输出。这个命令我一般会保留在项目里每次对方站改版先跑它确认规则再改 Job 里的正式规则。参数上测试地址建议固定一个已知存在的详情页方便对比历史输出。最后说个我自己的习惯采集站最怕的不是技术难而是「规则悄悄失效了没人知道」。所以我会把采集命中率做成一个简单的监控面板每天扫一眼。如果某个来源的命中率从 95% 掉到 30%不用等用户反馈自己就先发现了。这套 Laravel 加 QueryList 的组合骨架搭好之后真正花时间的永远是规则维护和异常处理而不是框架本身。希望帮到你。本文还有配套的精品资源点击获取