jiannei / laravel-crawler
crawler
Requires
- php: ^8.1
- ext-dom: *
- php-webdriver/webdriver: ^1.14
- symfony/dom-crawler: ^6.2
Requires (Dev)
- guzzlehttp/guzzle: ^7.5
- laravel/pint: ^1.4
- orchestra/testbench: ^8.0
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
一个基于 Pattern 的 Laravel 爬虫包。
📝 版本说明: 当前文档对应 v3 开发版。v3 按 Laravel 通用目录重新组织,公开命名空间与 v2 不兼容,文档中的类名均以 v3 为准。
介绍
laravel-crawler 把「请求 → 解析 → 存储 → 消费」这条链路收进一个包。
用 Pattern 描述抓什么、怎么解析:
- Pattern 可以直接传给
Crawler::pattern(),也可以存到文件源或数据库源,再用Crawler::json()按 key 运行 Crawler::fetch()和Crawler::new()返回一个Page,继续使用 Symfony DomCrawler 的filter(),并额外提供parse()、remove()、attrs()、texts()、htmls()RunOptions在每次调用时叠加 query、HTTP options 和 before / after 钩子,钩子不会泄漏到下一次调用- 抓取状态码非 2xx 时抛
FetchFailed;Pattern、配置或钩子返回值不合法时抛InvalidPattern - Task 按 cron 表达式调度;一次 Task run 的抓取在数据库事务之外执行,完成后状态、运行日期和 Records 一起提交
crawler:record为待消费的 Record 派发消费任务;消费失败保留 pending,并把异常写入crawl_records.exception
概览
- Pattern 驱动:支持 inline pattern、文件源、数据库源和 RSS
- 规则表达力:
[selector, attribute]、位置first/last/ 数字、闭包转换、字段 dot 嵌套 - Page 解析:基于 Symfony DomCrawler,支持
filter()、parse()、remove()、attrs()、texts()、htmls() - 请求控制:
RunOptions合并 pattern 与每次调用的 query / options,支持 before / after 钩子 - Fetch seam:
Contracts\Fetcher端口,内置Http\HttpFetcher,可在测试中绑定自己的 adapter - 消费 seam:
Contracts\ConsumeService端口,Concerns\Consumable按 pattern 的consumekey 解析回调 - Task 调度:
crawler:task按 cron 表达式派发到期的 active Task - 数据源同步:
crawler:task --action=import/export在文件源和数据库源之间镜像 Pattern - 结构化调试日志:
crawler.fetch.debug打开后记录 URL、query、脱敏 options、状态、耗时、响应头和 body 长度 - JS 渲染:
Crawler::chrome()通过Contracts\Browser获取渲染后的 HTML,crawler:server启动chromedriver
环境要求
- PHP
^8.4,需要ext-dom - 一个 Laravel 应用;Service Provider 和
Crawleralias 通过 Laravel Package Discovery 自动注册 - 使用
Crawler::chrome()或crawler:server时,需要本地有chromedriver - 使用 Task / Record 时,需要运行 queue worker
依赖版本:
| 依赖 | 版本 |
|---|---|
| PHP | ^8.4 |
| ext-dom | * |
| symfony/dom-crawler | ^8.1 |
| php-webdriver/webdriver | ^1.16 |
安装
composer require jiannei/laravel-crawler -vvv
只运行 inline pattern 或直接 fetch() 时,不需要数据库。需要存储 Task、Record 或使用数据库源时,发布配置、迁移和 Pattern 文件:
php artisan vendor:publish --tag=crawler php artisan migrate
vendor:publish --tag=crawler 会发布:
config/crawler.phpdatabase/migrations/*_create_crawl_tasks_table.phpdatabase/migrations/*_create_crawl_records_table.phpresources/crawler.json
配置
手动注册(可选)
如果关闭了 Laravel Package Discovery,手动注册 Service Provider 和 alias:
// config/app.php 'providers' => [ Jiannei\LaravelCrawler\Providers\LaravelServiceProvider::class, ], 'aliases' => [ 'Crawler' => Jiannei\LaravelCrawler\Facades\Crawler::class, ],
环境变量
CRAWLER_SOURCE_CHANNEL=file CRAWLER_LOG_LEVEL=debug
配置项
| 配置项 | 默认值 | 说明 |
|---|---|---|
crawler.fetch.debug |
false |
为每次 fetch 记录一条结构化日志。 |
crawler.source.default |
env('CRAWLER_SOURCE_CHANNEL', 'file') |
当前生效的 Pattern 数据源 channel。 |
crawler.source.channels |
file、database |
数据源 driver 及其参数。 |
crawler.consume.service |
'' |
ConsumeService 实现类;只抓取不消费时留空。 |
crawler.log |
每日写入 storage/logs/crawler.log |
crawler 日志 channel。 |
crawler.guzzle.options |
超时和请求头 | 传给 Laravel HTTP client / Guzzle 的选项。 |
crawler.chrome |
server、binary、arguments、wait、log | Chrome 渲染与 crawler:server 配置。 |
crawler.rss |
channel 和 items 规则 |
Crawler::rss() 的默认 Pattern。 |
使用
扩展包自带较完整的测试用例 tests,也可以直接从下面的示例开始。
直接运行 Pattern
use Jiannei\LaravelCrawler\Facades\Crawler; $items = Crawler::pattern([ 'url' => 'https://github.com/trending', 'query' => ['since' => 'daily'], 'group' => [ 'selector' => 'article', 'rules' => [ 'repo' => ['h1 a', 'href'], 'description' => ['p', 'text'], ], ], ]); // Illuminate\Support\Collection of arrays $items->first()['repo'];
等价 JSON 结构:
[
{
"repo": "/jiannei/laravel-crawler",
"description": "A pattern-driven crawler for Laravel."
}
]
抓取页面并解析
$page = Crawler::fetch('https://example.com', ['page' => 2], ['timeout' => 10]); $title = $page->filter('h1')->text(); $items = $page->filter('.item')->parse([ 'title' => ['h2', 'text'], 'link' => ['a', 'href'], ]);
Crawler::fetch() 返回 Jiannei\LaravelCrawler\Parsing\Page;当响应状态码不是 2xx 时抛 Jiannei\LaravelCrawler\Exceptions\FetchFailed。
构建本地 Page
$page = Crawler::new('<html><body><h1>Hello</h1></body></html>'); $title = $page->filter('h1')->text();
Page 在 Symfony DomCrawler 之上增加了 parse()、remove()、attrs()、texts() 和 htmls():
$hrefs = $page->filter('a')->attrs('href'); $headings = $page->filter('h2')->texts(); $html = $page->filter('#content')->remove([ '.ad', ['p', 'outerHtml', 'last'], ]);
运行已存储 Pattern
存储的 Pattern 放在 crawler.source.default 指定的 channel 中。默认 file channel 读取 resources/crawler.json:
[
{
"key": "github:trending",
"url": "https://github.com/trending",
"expression": "0 * * * *",
"query": { "since": "daily" },
"group": {
"selector": "article",
"rules": {
"repo": ["h1 a", "href"],
"description": ["p", "text"]
}
}
}
]
$items = Crawler::json('github:trending');
Crawler::json($key) 在 key 不存在时抛 InvalidPattern。如果存储的 Pattern 带有非空 rss key,会走 RSS 解析流程。
解析 RSS
$feed = Crawler::rss('https://example.com/feed.xml'); $feed->get('channel'); // ['title' => ..., 'link' => ...] $feed->get('items'); // list of item arrays
等价 JSON 结构:
{
"channel": {
"title": "Example feed",
"link": "https://example.com"
},
"items": [
{ "title": "First post", "link": "https://example.com/1" }
]
}
当 feed 结构不同,可在第二个参数传入自定义 Pattern:
$feed = Crawler::rss('https://example.com/feed.xml', [ [ 'alias' => 'channel', 'selector' => 'channel', 'rules' => [ 'title' => ['title', 'text'], 'link' => ['link', 'text'], ], ], [ 'alias' => 'items', 'selector' => 'channel item', 'rules' => [ 'title' => ['title', 'text'], 'link' => ['link', 'text'], 'pubDate' => ['pubDate', 'text'], ], ], ]);
默认 RSS Pattern 为 crawler.rss。
Pattern 参考
规则
一条规则可以是字符串字面量,也可以是数组:
'rules' => [ // 字面量 'source' => 'github', // [selector, attribute] 'title' => ['h1', 'text'], 'link' => ['h1 a', 'href'], // [selector, attribute, position] 'first_paragraph' => ['p', 'text', 'first'], 'last_paragraph' => ['p', 'html', 'last'], 'second_item' => ['li', 'text', 1], // [selector, attribute, position, closure] 'slug' => ['h1', 'text', 'first', function ($node, $value) { return (string) $value->slug(); }], // 字段名支持 dot 语法,会构建嵌套数组 'meta.author' => ['#author', 'text'], ],
selector使用 Symfony DomCrawler 的 CSS selector 语法attribute可以是text、html、outerHtml,或其他 DOM 属性,如href、srcposition接受数字、first或last;省略时取第一个匹配- 闭包接收当前 item 节点和已提取的值(
Illuminate\Support\Stringable) - selector 没有匹配到节点时,字段值为
null
Group 形态
Pattern 支持四种形态。
无 group —— 整页产出 1 条 item:
[
'url' => 'https://example.com/article',
'rules' => [
'title' => ['h1', 'text'],
'body' => ['#content', 'html'],
],
]
单个 group —— 每个匹配产出 1 条 item:
[
'url' => 'https://example.com/list',
'group' => [
'selector' => '.item',
'rules' => [
'title' => ['h2 a', 'text'],
'link' => ['h2 a', 'href'],
],
],
]
多个 group —— 以 alias 为 key,每个 value 是一个 Collection:
[
'url' => 'https://example.com/page',
'group' => [
[
'alias' => 'list',
'selector' => '.item',
'rules' => ['title' => ['h2 a', 'text']],
],
[
'alias' => 'single',
'selector' => '#single',
'rules' => ['title' => ['#title', 'text']],
],
],
]
RSS —— group 需要是列表;channel alias 取单条,其他 alias 取数组:
[
'url' => 'https://example.com/feed.xml',
'rss' => true,
'group' => [
[
'alias' => 'channel',
'selector' => 'channel',
'rules' => ['title' => ['title', 'text']],
],
[
'alias' => 'items',
'selector' => 'channel item',
'rules' => ['title' => ['title', 'text']],
],
],
]
存储 Pattern 的字段
key:必填,唯一标识expression:cron 表达式;文件源可选,导入数据库源时默认* * * * *consume:消费回调方法名;留空或省略时使用defaultCallback
运行时选项与钩子
每次调用的输入和钩子通过 Jiannei\LaravelCrawler\Data\RunOptions 传入:
use Jiannei\LaravelCrawler\Data\FetchResponse; use Jiannei\LaravelCrawler\Data\RunOptions; $items = Crawler::json('github:trending', new RunOptions( query: ['spoken_language_code' => 'zh'], options: ['timeout' => 10], before: function (string $url, array $query, array $options) { return [$url, $query + ['since' => 'weekly'], $options]; }, after: function (FetchResponse $response) { return $response; }, ));
先合并 Pattern 的 query 和 options,再叠加每次调用的 RunOptions。
before(string $url, array $query, array $options): array必须返回[$url, $query, $options]after(FetchResponse $response): FetchResponse必须返回FetchResponse- 钩子返回值不合法时抛
InvalidPattern
自定义 HTTP Fetcher
抓取过程通过 Jiannei\LaravelCrawler\Contracts\Fetcher 端口完成。可以在测试里绑定自己的 adapter,或替换传输层:
namespace App\Crawlers; use Jiannei\LaravelCrawler\Contracts\Fetcher; use Jiannei\LaravelCrawler\Data\FetchResponse; class FakeFetcher implements Fetcher { public function fetch(string $url, array|string|null $query = null, array $options = []): FetchResponse { return new FetchResponse(200, '<html><body><h1>Fake</h1></body></html>'); } }
$this->app->instance(Fetcher::class, new FakeFetcher());
默认 adapter 是 Jiannei\LaravelCrawler\Http\HttpFetcher,它使用 Laravel HTTP client,并读取 crawler.guzzle.options。
Records 与消费
一次 Task run 会把每条抓取结果存成 CrawlRecord。crawler:record 为每条待消费 Record 派发一个消费任务:
php artisan crawler:record
把 crawler.consume.service 配成实现了 Jiannei\LaravelCrawler\Contracts\ConsumeService 的类:
namespace App\Crawlers; use Jiannei\LaravelCrawler\Concerns\Consumable; use Jiannei\LaravelCrawler\Contracts\ConsumeService; use Jiannei\LaravelCrawler\Models\CrawlRecord; use Jiannei\LaravelCrawler\Models\CrawlTask; class TrendingConsumer implements ConsumeService { use Consumable; protected function storeTrending(): callable { return function (CrawlRecord $record, CrawlTask $task): bool { TrendingRepository::updateOrCreate( ['url' => $record->content['repo']], ['description' => $record->content['description'] ?? null], ); return true; }; } protected function defaultCallback(): callable { return fn (CrawlRecord $record, CrawlTask $task): bool => true; } }
// config/crawler.php 'consume' => [ 'service' => App\Crawlers\TrendingConsumer::class, ],
consume() 的返回值语义:
true:Record 标记为已消费,同时清除之前的 exceptionfalse:Record 保持 pending,且不记录 exception,等待下次重试- 抛出异常:回调所在事务回滚,异常消息写入
crawl_records.exception,Record 保持 pending
使用 Consumable 时,Pattern 的 consume key 决定调用哪个方法;留空或省略时调用 defaultCallback。
调度
crawler:task 为到期的 active Task 派发 TaskRun;crawler:record 为待消费 Record 派发 RecordConsume。把它们加入调度,并运行 queue worker:
// routes/console.php use Illuminate\Support\Facades\Schedule; Schedule::command('crawler:task')->everyMinute()->withoutOverlapping(); Schedule::command('crawler:record')->everyMinute()->withoutOverlapping();
php artisan queue:work
命令
| 命令 | 说明 |
|---|---|
php artisan crawler:task |
派发到期且 active 的 Task。 |
php artisan crawler:task {name} |
在到期时派发指定名称的 Task。 |
php artisan crawler:task --action=import |
把文件源镜像到数据库源;文件中不存在的 Task 会被置为 inactive。 |
php artisan crawler:task --action=export |
把 active 的数据库源镜像到文件源。 |
php artisan crawler:record |
为每条待消费 Record 派发一个消费任务。 |
php artisan crawler:record {name} --limit=500 |
只消费指定 Task,并限制单次处理的 Record 数量。 |
php artisan crawler:server |
启动配置中的 chromedriver。 |
php artisan crawler:server --path=/usr/local/bin |
为 chromedriver 二进制文件指定目录前缀。 |
crawler:server会先结束已有的chromedriver进程,再按crawler.chrome配置启动新的进程。
Pattern 数据源
默认数据源 channel 是 file:
'source' => [ 'default' => env('CRAWLER_SOURCE_CHANNEL', 'file'), 'channels' => [ 'file' => [ 'driver' => Jiannei\LaravelCrawler\Sources\FilePatternSource::class, 'path' => resource_path('crawler.json'), ], 'database' => [ 'driver' => Jiannei\LaravelCrawler\Sources\DatabasePatternSource::class, 'model' => Jiannei\LaravelCrawler\Models\CrawlTask::class, ], ], ],
file读写 JSON 数组,每个 Pattern 以key作为唯一标识database读取 active 的CrawlTask,并按nameupsert- 生产环境可设置
CRAWLER_SOURCE_CHANNEL=database,再用--action=import/--action=export在两个数据源之间同步
数据库表
发布的迁移会创建两张表:
| 表 | 字段 |
|---|---|
crawl_tasks |
name(唯一)、expression(cron)、active、status、pattern(JSON)、exception、previous_run_date、next_run_date、timestamps |
crawl_records |
task_id、consumed、content(JSON)、exception、timestamps |
Task 处于 active 且 next_run_date 为 null 或已过期时会被视为到期。导入进来的 Task 初始 next_run_date 为 null,会被下一次 crawler:task 拾取。status 使用 Jiannei\LaravelCrawler\Enums\TaskStatus(Pending、Succeeded、Failed)。
JS 渲染页面
使用 Crawler::chrome() 通过 Chrome 渲染页面,再把渲染后的 HTML 包装成 Page:
use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverExpectedCondition; use Jiannei\LaravelCrawler\Facades\Crawler; $page = Crawler::chrome( 'https://example.com/spa', WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::cssSelector('#app')), ); $title = $page->filter('h1')->text();
启动本地 chromedriver:
php artisan crawler:server
浏览器相关配置在 crawler.chrome。crawler.chrome.server.url 只能包含 scheme 和 host,端口单独配置在 crawler.chrome.server.port。
Debug 日志
把 crawler.fetch.debug 设为 true,每次 crawler fetch 都会向 crawler channel 写一条结构化日志:
- URL、query、脱敏后的 HTTP options
- 响应状态码、耗时、响应头、body 长度
authorization、cookie、token、password、secret 等敏感 option key 会被脱敏,响应 body 不会写入日志。crawler.guzzle.options.debug 是另一个独立的 Guzzle debug 选项。
crawler 日志 channel 由 crawler.log 生成,默认按天写入 storage/logs/crawler.log。
协议
MIT 许可证(MIT)。有关更多信息,请参见 协议文件。