QueryList 数据采集

QueryList是一套用于内容采集的PHP工具,它使用更加现代化的开发思想,语法简洁、优雅,可扩展性强。
相比传统的使用晦涩的正则表达式来做采集,QueryList使用了更加强大而优雅的CSS选择器来做采集,大大降低了PHP做采集的门槛,同时也让采集代码易读易维护

示例代码

先来感受一下使用 QueryList 来做采集是什么样子。

  1. 采集百度搜索结果列表的标题和链接。
  • 采集代码
1
2
3
4
5
6
7
8
9
$data = QueryList::get('https://www.baidu.com/s?wd=QueryList')
// 设置采集规则
->rules([
'title' => array('h3', 'text'),
'link' => array('h3>a', 'href')
])
->queryData();

print_r($data);
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Array
(
[0] => Array
(
[title] => QueryList|基于phpQuery的无比强大的PHP采集工具
[link] => http://www.baidu.com/link?url=GU_YbDT2IHk4ns1tjG2I8_vjmH0SCJEAPuuZN
)
[1] => Array
(
[title] => PHP 用QueryList抓取网页内容 - wb145230 - 博客园
[link] => http://www.baidu.com/link?url=zn0DXBnrvIF2ibRVW34KcRVFG1_bCdZvqvwIhUqiXaS
)
[2] => Array
(
[title] => 介绍- QueryList指导文档
[link] => http://www.baidu.com/link?url=pSypvMovqS4v2sWeQo5fDBJ4EoYhXYi0Lxx
)
//...
)
  1. 分别采集百度搜索结果列表的标题和链接。
  • 采集代码
1
2
3
4
5
6
7
$ql = QueryList::get('https://www.baidu.com/s?wd=QueryList');
// 获取搜索结果标题列表
$titles = $ql->find('h3>a')->texts();
// 获取搜索结果链接标题
$links = $ql->find('h3>a')->attrs('href');
print_r($titles);
print_r($links);
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Array
(
[0] => QueryList|简洁、优雅的PHP采集工具
[1] => phpQuery选择器 - QueryList 4.0 指导文档
[2] => php写爬虫进行采集 QueryList的使用 - CSDN博客
[3] => QueryList采集在线测试
[4] => 介绍- QueryList 4.0 指导文档
[5] => QueryList交流社区|基于phpQuery的无比强大的采集工具
[6] => 介绍- QueryList 3.0 指导文档
[7] => thinkphp5使用QueryList实现采集功能 - 坚持一点点 - 博客园
[8] => QueryList一个基于phpQuery的无比强大的采集工具 - ThinkPHP框架
[9] => php使用QueryList轻松采集JavaScript动态渲染页面 - QueryList - ...
)
Array
(
[0] => http://www.baidu.com/link?url=CNKBNz0t9t6YLmIfXjKYnIkcQ-JzNOpAyiAHPDSnlkmrEqMq5q9o44ElplTf7nON
[1] => http://www.baidu.com/link?url=VKDqdL3WXxuy0xV3uHMDXRrqQlWGhh4qMQ5h4UCBw0sRJvE9uLlMbr5fE_gsURX8oehsAyzi9_QxVuC1CBjoTa
[2] => http://www.baidu.com/link?url=rjDcaEbicrZjIG-iFJdkHJTWxoxYA2EBatxh-EyvMDdPMPxtOi8nDUi7UiuIgmW9X7o6CvcFUqPqCrqJp7M4FmRKpJ52-ceBowE0ek_jb5O
[3] => http://www.baidu.com/link?url=9FAlKAB_4xCVP1hv_RlpPN8ROxsTSTDHpnvvxYn4j_veTkhxHfaPHUFAtc8BctDmN9ZVigMS7ggaVy778zAMzK
[4] => http://www.baidu.com/link?url=CFOkrOHOFsWPddZC1fuRv8ZqwhbF7P6vH1Pg1covRawG6wsmszFW1qnxHf7mWKPM
[5] => http://www.baidu.com/link?url=7kCwV_WRMZjWAeyOWP3zfX4Jx21tPeZhmyuENciN86BBd_g8znMD3JgEEfvGRbNc
[6] => http://www.baidu.com/link?url=p3JenyGg7qtP7lSKXkbLM8_eGTzxzjJGch7__-8fmuIsZOdEQbCquS6P_NdR4LoG
[7] => http://www.baidu.com/link?url=_EJBv9sxVtGT1paHERifcDHEaG8twDHk-Av2JD5DlkJUvipLAdNqovTdXAxijcI3LTaC3F_jYuMkHuTOJ0ic7_
[8] => http://www.baidu.com/link?url=ad9pwRrrkyTVOB7ZMKN29XyLX1MsXRIFPbA0ldPLTQQ58Dnw_YpZFKJZwxZ-jfaL
[9] => http://www.baidu.com/link?url=mEjYM95SeHFYCnfITubUoTOj7XWR1NparEcb3hCGqPGv_uChSvVFat6xcvyCz_9mLogw5ol5gU_isHqYRTJj2q
)

安装QueryList

1
composer require jaeger/querylist

开发必备

  • 会使用Composer

  • 非常熟悉jQuery选择器CSS选择器

    QueryList的核心思想就是使用jQuery选择器来做采集,所以选择器语法会贯穿全文

使用

QueryList无框架依赖,可以灵活的嵌入到任何项目中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Http\Controllers;

use QL\QueryList;

class TestQueryList
{
public function index()
{
// 采集某页面所有的图片
$data = QueryList::get('http://cms.querylist.cc/bizhi/453.html')
->find('img')
->attrs('src');

print_r($data->all());
}
}

基础

HTTP客户端

HTTP客户端用于抓取网页HTML源码。

QueryList推荐使用GuzzleHttp来作为HTTP客户端,它功能强大、使用简单、支持异步和并发请求,GuzzleHttp使用文档:http://guzzle-cn.readthedocs.io/zh_CN/latest/
默认安装好QueryList之后就可以直接使用GuzzleHttp了:

1
2
3
4
5
6
$client = new GuzzleHttp\Client();
$res = $client->request('GET', 'https://www.baidu.com/s', [
'wd' => 'QueryList'
]);
$html = (string)$res->getBody();
$data = QueryList::html($html)->find('h3')->texts();

QueryList内置的HTTP客户端

为方便使用,QueryList基于GuzzleHttp封装了一些HTTP请求接口,并进行了简化,请求参数与GuzzleHttp一致,在请求参数上有什么不明白的地方可以直接查看GuzzleHttp文档。

目前封装的HTTP接口有:

  • get(): GET请求
  • post(): POST请求
  • postJson(): POST JSON请求
  • multiGet(): 并发GET请求
  • multiPost(): 并发POST请求

用法

get()方法和post()方法用法和参数完全一致,且共享cookie

1
2
3
4
5
6
7
8
9
10
11
12
$ql = QueryList::get('http://httpbin.org/get?param1=testvalue&params2=somevalue');
// 等价于
$ql->get('http://httpbin.org/get',[
'param1' => 'testvalue',
'params2' => 'somevalue'
]);

// 发送post请求
$ql = QueryList::post('http://httpbin.org/post',[
'param1' => 'testvalue',
'params2' => 'somevalue'
]);

自定义HTTP Header

1
2
3
4
5
6
7
8
9
10
11
12
13
$ql = QueryList::get('http://httpbin.org/get',[
'param1' => 'testvalue',
'params2' => 'somevalue'
],[
'headers' => [
'Referer' => 'https://querylist.cc/',
'User-Agent' => 'testing/1.0',
'Accept' => 'application/json',
'X-Foo' => ['Bar', 'Baz'],
// 携带cookie
'Cookie' => 'abc=111;xxx=222'
]
]);

更高级参数

还可以携带更多高级参数,如:设置超时时间、设置代理等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ql = QueryList::get('http://httpbin.org/get',[
'param1' => 'testvalue',
'params2' => 'somevalue'
],[
// 设置代理
'proxy' => 'http://222.141.11.17:8118',
//设置超时时间,单位:秒
'timeout' => 30,
'headers' => [
'Referer' => 'https://querylist.cc/',
'User-Agent' => 'testing/1.0',
'Accept' => 'application/json',
'X-Foo' => ['Bar', 'Baz'],
'Cookie' => 'abc=111;xxx=222'
]
]);

$ql->post('http://httpbin.org/post',[
'param1' => 'testvalue',
'params2' => 'somevalue'
],[
'proxy' => 'http://222.141.11.17:8118',
'timeout' => 30,
'headers' => [
'Referer' => 'https://querylist.cc/',
'User-Agent' => 'testing/1.0',
'Accept' => 'application/json',
'X-Foo' => ['Bar', 'Baz'],
'Cookie' => 'abc=111;xxx=222'
]
]);

并发请求(多线程请求)

简单用法,默认并发数为5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use GuzzleHttp\Psr7\Response;
use QL\QueryList;

$urls = [
'https://github.com/trending/go?since=daily',
'https://github.com/trending/html?since=daily',
'https://github.com/trending/java?since=daily'
];

QueryList::multiGet($urls)
->success(function(QueryList $sql, Response $response, $index) use ($urls) {
echo 'Current url: '.$urls[$index]."\r\n";
$data = $ql->find('h3>a')->texts();
print_r($data->all());
})->send();

更高级的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
se GuzzleHttp\Psr7\Response;
use QL\QueryList;

$urls = [
'https://github.com/trending/go?since=daily',
'https://github.com/trending/html?since=daily',
'https://github.com/trending/java?since=daily'
];

$rules = [
'name' => ['h3>a','text'],
'desc' => ['.py-1','text']
];
$range = '.repo-list>li';
QueryList::rules($rules)
->range($range)
->multiGet($urls)
// 设置并发数为2
->concurrency(2)
// 设置GuzzleHttp的一些其他选项
->withOptions([
'timeout' => 60
])
// 设置HTTP Header
->withHeaders([
'User-Agent' => 'QueryList'
])
// HTTP success回调函数
->success(function (QueryList $ql, Response $response, $index){
$data = $ql->queryData();
print_r($data);
})
// HTTP error回调函数
->error(function (QueryList $ql, $reason, $index){
// ...
})
->send();

连贯操作

post操作和get操作是cookie共享的,意味着你可以先调用post()方法登录,然后get()方法就可以采集所有登录后的页面。

1
2
3
4
5
6
$ql = QueryList::post('http://xxxx.com/login',[
'username' => 'admin',
'password' => '123456'
])->get('http://xxx.com/admin');

$ql->get('http://xxx.com/admin/page');

获取抓取到的HTML

使用getHtml()方法可以获取到get()post()方法返回的HTML内容,通常用于调试打印验证抓取结果等场景

1
2
$ql = QueryList::get('http://httpbin.org/get?param1=testvalue');
echo $ql->getHtml();

获取HTTP响应头等信息

如果你想获取HTTP响应头,如响应状态码,QueryList内置的HTTP客户端屏蔽了这部分功能,请直接使用GuzzleHttp来实现。

1
2
3
4
5
6
7
8
use GuzzleHttp\Client;

$client = new Client();
$response = $client->get('http://httpbin.org/get');
// 获取响应头部信息
$headers = $response->getHeaders();

print_r($headers);

自定义HTTP客户端

GuzzleHttp是一款功能非常强大的HTTP客户端,你想要的功能它几乎都有;但如果你还是想使用自己熟悉的HTTP客户端如:curl,那也是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function getHtml($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
curl_setopt($ch, CURLOPT_REFERER, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}

$html = getHtml('http://httpbin.org/get?param1=testvalue');
// 这种情况下允许你对HTML做一些额外的处理后,然后再把HTML传给QueryList对象
$html = str_replace('xxx','yyy',$html);
$ql = QueryList::html($html);
echo $ql->getHtml();

通过其他HTTP客户端获取源码,然后使用html()方法来设置html,html()方法除了可以接收一个完整的HTML网页外,还支持接收HTML片段:

1
2
3
4
5
6
7
8
9
10
11
12
$html = <<<STR
<div id="one">
<div class="two">
<a href="http://querylist.cc">QueryList官网</a>
<img src="http://querylist.com/1.jpg" alt="这是图片">
<img src="http://querylist.com/2.jpg" alt="这是图片2">
</div>
<span>其它的<b>一些</b>文本</span>
</div>
STR;

$ql = QueryList::html($html);

phpQuery有个bug,那就是当HTML中有它无法识别的特殊字符时,HTML就会被截断,导致最终的采集结果不正确,此时可以尝试使用正则或其它方式获取到要采集的内容的HTML片段,把这个HTML片段传给QueryList,从而可以解决这种场景下的问题。

采集单元素

QueryList有个find()方法,用于采集单个元素,它通过jQuery选择器选择DOM元素,用法同jQueryfind()方法。

实战 - 采集IT之家文章页

采集IT之家文章页

如图采集IT之家文章页的:文章标题、作者和正文内容。

  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use QL\QueryList;

public function test_query()
{
$ql = QueryList::get('https://www.ithome.com/html/discovery/358585.htm');

$data = [];
// 采集文章标题
$data['title'] = $ql->find('h1')->text();
// 采集文章作者
$data['author'] = $ql->find('#author_baidu>strong')->text();
// 采集文章内容
$data['content'] = $ql->find('.post_content')->html();

print_r($data);
}
  • 采集结果
1
2
3
4
5
6
7
8
9
10
Array
(
[title] => 巴基斯坦一城镇温度达50.2度:创下全球4月历史温度新高
[author] => 白猫
[content] => <p><a class="s_tag" href="https://www.ithome.com/" target="_blank">IT之家</a>56日消息 4月份就遇到超过50度的极端天气显然是不可想象的,不过这的的确确发生在我们的周围,目前在巴基斯坦的一个城镇,有气象观测站显示该地的温度最高达到50.2度,打破了全球有记录以来的四月最高温。</p>
<p><img src="//img.ithome.com/images/v2/t.png" w="600" h="400" class="lazy" title="巴基斯坦一城镇温度达50.2度:创下全球4月历史温度新高" data-original="https://img.ithome.com/newsuploadfiles/2018/3/20180323_103720_572.png" width="600" height="400"></p>
<p>根据天空新闻的报道,在位于巴基斯坦南部的纳瓦布沙在周一(430日)的时候出现了高达50.2度的气温,气象学家表示这或许是人类有史以来遇到的四月份最高的温度。</p>
<p>法国气象局的气象学家卡比奇安在推特上表示,巴基斯坦的这个小城镇不但是有史以来亚洲遇到的最高的四月气温,更有可能是全球四月的最高温,而也有网友表示由于过于炎热的天气,当地已经有不少人因为中暑而丧命。</p>
<p>全球极端天气专家克里斯托弗伯特也表示,四月份就达到50摄氏度极其罕见,纳瓦布沙的温度或将是人类有史以来遇到的温度最高的四月。农业学家表示巴基斯坦过高的温度会严重影响未来粮食的收割。</p>
)

采集列表

学习如何批量采集数据。
列表采集才是QueryList的核心功能,这里主要涉及到两个函数的用法:rules()range()

  • 采集代码(进一步优化采集单元素的代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function test_rule()
{
$url = 'https://www.ithome.com/html/discovery/358585.htm';

// 定义采集规则
$rules = [
// 采集文章标题
'title' => ['h1', 'text'],
// 采集文章作者
'author' => ['#author_baidu>strong', 'text'],
// 采集文章内容
'content' => ['.post_content', 'html']
];

$data = QueryList::get($url)->rules($rules)->query()->getData();
print_r($data->all());
}
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Array
(
[0] => Array
(
[title] => 巴基斯坦一城镇温度达50.2度:创下全球4月历史温度新高
[author] => 白猫
[content] => <p><a class="s_tag" href="https://www.ithome.com/" target="_blank">IT之家</a>56日消息 4月份就遇到超过50度的极端天气显然是不可想象的,不过这的的确确发生在我们的周围,目前在巴基斯坦的一个城镇,有气象观测站显示该地的温度最高达到50.2度,打破了全球有记录以来的四月最高温。</p>
<p><img src="//img.ithome.com/images/v2/t.png" w="600" h="400" class="lazy" title="巴基斯坦一城镇温度达50.2度:创下全球4月历史温度新高" data-original="https://img.ithome.com/newsuploadfiles/2018/3/20180323_103720_572.png" width="600" height="400"></p>
<p>根据天空新闻的报道,在位于巴基斯坦南部的纳瓦布沙在周一(430日)的时候出现了高达50.2度的气温,气象学家表示这或许是人类有史以来遇到的四月份最高的温度。</p>
<p>法国气象局的气象学家卡比奇安在推特上表示,巴基斯坦的这个小城镇不但是有史以来亚洲遇到的最高的四月气温,更有可能是全球四月的最高温,而也有网友表示由于过于炎热的天气,当地已经有不少人因为中暑而丧命。</p>
<p>全球极端天气专家克里斯托弗伯特也表示,四月份就达到50摄氏度极其罕见,纳瓦布沙的温度或将是人类有史以来遇到的温度最高的四月。农业学家表示巴基斯坦过高的温度会严重影响未来粮食的收割。</p>
)

)
  • $rules规则解释如下:
1
2
3
4
5
$rules = [
'规则名1' => ['选择器1','元素属性'],
'规则名2' => ['选择器2','元素属性'],
// ...
];

采集结果与前面的代码完全相同,注意这里的采集结果是一个二维数组。

queryData()语法糖

可能你会觉的列表采集的语法有一点点繁琐,如:

1
2
$rt = QueryList::get($url)->rules($rules)->query()->getData();
print_r($rt->all());

QueryList V4.0.4版本新增了一个queryData()语法糖来简化这种操作:

1
2
$rt = QueryList::get($url)->rules($rules)->queryData();
print_r($rt);

queryData()方法等同于query()->getData()->all()

列表采集

前面只说到采集文章页内容,通常情况下我们会先采集列表页,然后再循环采集列表中的每篇文章,采集列表需要用到range()函数来配合rules()函数。

  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function test_list()
{
$url = 'https://it.ithome.com/ityejie/';
// 元数据采集规则
$rules = [
// 采集文章标题
'title' => ['h2>a', 'text'],
// 采集链接
'link' => ['h2>a', 'href'],
// 采集缩略图
'img' => ['.list_thumbnail>img', 'src'],
// 采集文档简介
'desc' => ['.memo', 'text']
];

// 切片选择器
$range = '.ulcl';
$data = QueryList::get($url)->rules($rules)
->range($range)
->query()
->getData();

print_r($data->all());
}
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
Array
(
[0] => Array
(
[title] => 正式进军县镇市场,飞利浦“入座”苏宁零售云头等舱三星S10首销上演王者归来,苏宁门店被挤爆!苏宁维达战略签约,定下2019年销售翻番目标辱华风波代价大,D&amp;G创始人双双跌出福布斯榜单12家中国驻外使领馆开通支付宝,网友点名其他国家地区“赶紧跟上!”斯坦·李推特“死而复生”给《惊奇队长》打广告,粉丝怒喷运营团队熊猫直播官方宣布停服:主站“流浪计划”开启万事网联公司成立:注册资本10亿元,万事达持股51%董明珠:偷手机、捡手机不归还应受到更严厉的惩罚Valve承认裁员13人:主要为VR部门,公司不会有重大变化天猫上的女性创业者:涉足市场更纵深、初创者年轻化PHP 7.1.27/7.2.16/7.3.3发布商家网售伪造微信朋友圈照片视频素材包,3万多张卖一块一【更新】链家左晖被限制消费?官方回应:无实质关系,正向法院沟通TCL展示Alcatel 7手机:后置4800万像素,支持5G人民日报刊文:App收集使用个人信息必须有法律依据传罗永浩出售锤子空气净化器业务:原荣耀总裁刘江峰将接手消息:特斯拉将取消中国一线销售人员提成,并关闭线下门店小米9 “无闪尊享版”版后续:官方紧急调换,补偿一个保护套宣布起诉美国现场,华为“趁机”给Mate X打了个广告
[link] => https://www.ithome.com/0/413/173.htm
[img] => //img.ithome.com/images/v2/grey.gif
[desc] =>
)

)

数据是采集回来了,但我们发现有一点瑕疵,结果里面有一条结果是空的,且文章缩略图链接不正确。

  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function test_list()
{
$url = 'https://it.ithome.com/ityejie/';
// 元数据采集规则
$rules = [
// 采集文章标题
'title' => ['h2>a', 'text'],
// 采集链接
'link' => ['h2>a', 'href'],
// 采集缩略图,真正的图片链接在data-original属性上
'img' => ['.list_thumbnail>img', 'data-original'],
// 采集文档简介
'desc' => ['.memo', 'text']
];

// 切片选择器, 跳过第一条广告
$range = '.ulcl>li:gt(0)';
$data = QueryList::get($url)->rules($rules)
->range($range)
->query()
->getData();

print_r($data->all());
}
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Array
(
[0] => Array
(
[title] => 正式进军县镇市场,飞利浦“入座”苏宁零售云头等舱
[link] => https://www.ithome.com/0/413/173.htm
[img] => //img.ithome.com/newsuploadfiles/thumbnail/2019/3/413173_240.jpg
[desc] =>
)

[1] => Array
(
[title] => 三星S10首销上演王者归来,苏宁门店被挤爆!
[link] => https://www.ithome.com/0/413/171.htm
[img] => //img.ithome.com/newsuploadfiles/thumbnail/2019/3/413171_240.jpg
[desc] =>
)

[2] => Array
(
[title] => 苏宁维达战略签约,定下2019年销售翻番目标
[link] => https://www.ithome.com/0/413/170.htm
[img] => //img.ithome.com/newsuploadfiles/thumbnail/2019/3/413170_240.jpg
[desc] =>
)
// ...
)

就这样我们利用QueryList很轻松就采集到了IT之家的文章列表以及文章内容

关于方法的调用顺序

get()rules()range() 这几个方法都属于QueryList属性设置方法,所以调用顺序可以随意,所以下面这几种写法都是等价的:

1
2
3
QueryList::get($url)->rules($rules)->range($range)->query()->getData();
QueryList::rules($rules)->get($url)->range($range)->query()->getData();
QueryList::range($range)->rules($rules)->get($url)->query()->getData();

根据此特性,这里有些使用的小技巧:

  • 复用采集规则:针对同一个网站的多个结构相同的页面的采集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 待采集的同一个网站的网页集合
$urls = [
'http://xxx.com/1.html',
'http://xxx.com/2.html',
'http://xxx.com/3.html',
// ...
];

// 由于采集的都是同一个网站的网页,所以采集规则是可以复用的
$ql = QueryList::rules([...])->range('...');

foreach ($urls as $url) {
$data = $ql->get($url)->query()->getData();
// ...
}
  • 复用网页:针对同一个页面应用多套采集规则,避免重复抓取页面
1
2
3
4
5
6
7
8
9
10
$url = 'http://xxx.com/1.html';

// 抓取网页
$ql = QueryList::get($url);

// 应用第一种采集规则
$data1 = $ql->rules([...])->range('...')->query()->getData();

// 应用第二种采集规则
$data2 = $ql->rules([...])->range('...')->query()->getData();

内容过滤

从采集内容中移除掉多余无用内容。

很多时候我们采集回来的内容中会包含一些”杂质”,如果只是想要移除或替换内容中的某些关键词,直接用字符串替换函数就可以轻松解决,但往往实际情况没这么简单,下面就是一个典型的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$html =<<<STR
<div id="content">

<span class="tt">作者:xxx</span>

这是正文内容段落1.....

<span>这是正文内容段落2</span>

<p>这是正文内容段落3......</p>

<span>这是广告</span>
<p>这是版权声明!</p>
</div>
STR;

如上,正文内容中包含了作者信息、广告、版权声明等这些无用信息,我们需要从正文内容中过滤掉这些内容,这些内容是变化的,每篇文章都不一样,所以是无法直接用字符串替换函数去除的,QueryList提供了非常简单的去除方式,通过CSS选择器定位需要去除的内容,下面分别通过单元素采集列表采集两种场景来讲解内容过滤

单元素采集场景

前面的单元素采集篇章中有讲解到find()方法,这个方法返回的是一个Elements对象,这个对象拥有几乎所有与jQuery操作DOM完全相同的API,如果你对jQuery熟悉的话,就知道jQuery有一个remove()方法,用于移除元素,同样Elements对象也拥有这个方法,利用这个方法可以很容易的移除我们不需要的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public function test_remove()
{
$html = <<<STR
<div id="content">

<span class="tt">作者:xxx</span>

这是正文内容段落1.....

<span>这是正文内容段落2</span>

<p>这是正文内容段落3......</p>

<span>这是广告</span>
<p>这是版权声明!</p>
</div>
STR;
// 采集正文内容
$eles = QueryList::html($html)->find('#content');
// 选择正文内容中要移除的元素,并移除
$eles->find('.tt,span:last,p:last')->remove();
// 获取纯净的正文内容
$content = $eles->html();

print_r($content);
}
  • 采集结果
1
2
3
4
5
这是正文内容段落1.....

<span>这是正文内容段落2</span>

<p>这是正文内容段落3......</p>

列表采集场景

在前面的列表采集篇章中有讲解到rules()这个方法,它的参数是接收一个二维数组的采集规则,我们前面学到的采集规则形态是下面这样的:

1
2
3
4
5
$rules = [
'规则名1' => ['选择器1','元素属性'],
'规则名2' => ['选择器2','元素属性'],
// ...
];

下面是它的另一种形态:

1
2
3
4
5
$rules = [
'规则名1' => ['选择器1','元素属性','内容过滤选择器'],
'规则名2' => ['选择器2','元素属性','内容过滤选择器'],
// ...
];

内容过滤选择器参数就是用来过滤内容的,同时这种场景下也可以结合find()方法的remove()方法来过滤内容,下面来分别讲解。

第一种方法:使用内容过滤选择器参数

内容过滤选择器参数不光可以定义要移除的内容还可以定义要保留的内容,多个值之间用空格隔开,有如下2条规则:

  1. 内容移除规则:选择器名前面添加减号(-),表示移除该标签以及标签内容。
  2. 内容保留规则:选择器名前面没有减号(-)(此时选择器只能为HTML标签名,不支持其他选择器),当要采集的[元素属性] 值为text时表示需要保留的HTML标签以及内容,为html时表示要过滤掉的HTML标签但保留内容。
  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    public function test_list_remove1()
{
$html =<<<STR
<div id="content">

<span class="tt">作者:xxx</span>

这是正文内容段落1.....

<span>这是正文内容段落2</span>

<p>这是正文内容段落3......</p>

<span>这是广告</span>
<p>这是版权声明!</p>
</div>
STR;

// 采集规则
$rules = [
//设置了内容过滤选择器
'content' => ['#content','html','-.tt -span:last -p:last'],
];

$data = QueryList::rules($rules)->html($html)->query()->getData();

print_r($data->all());
}
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
Array
(
[0] => Array
(
[content] => 这是正文内容段落1.....

<span>这是正文内容段落2</span>

<p>这是正文内容段落3......</p>
)

)

采集结果与前面代码完全相同。

下面顺便演示一下内容保留规则的使用,请仔细观察采集结果来加深理解:

  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    public function test_list_remove2()
{
$html =<<<STR
<div id="content">

<span class="tt">作者:xxx</span>

这是正文内容段落1.....

<span>这是正文内容段落2</span>

<p>这是正文内容段落3......</p>

<a href="http://querylist.cc">QueryList官网</a>

<span>这是广告</span>
<p>这是版权声明!</p>
</div>
STR;

$rules = [
// 移除内容中所有的超链接,但保留超链接的内容,并移除内容中所有p标签,但保留p标签的内容
'content_html' => ['#content','html','a p'],
// 保留内容中的超链接,以及保留p标签及内容
'content_text' => ['#content','text','a p'],
];

$data = QueryList::rules($rules)->html($html)->query()->getData();

print_r($data->all());
}
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Array
(
[0] => Array
(
[content_html] => <span class="tt">作者:xxx</span>

这是正文内容段落1.....

<span>这是正文内容段落2</span>

这是正文内容段落3......

QueryList官网

<span>这是广告</span>
这是版权声明!

[content_text] => 作者:xxx

这是正文内容段落1.....

这是正文内容段落2

<p>这是正文内容段落3......</p>

<a href="http://querylist.cc">QueryList官网</a>

这是广告
<p>这是版权声明!</p>
)

)

第二种方式:结合remove()方法

QueryListgetData()方法接收一个回调函数作为参数,这个回调函数用于遍历采集结果,并对结果进行处理,我们可以利用这个回调函数来过滤内容。

  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    public function test_list_remove()
{
$html =<<<STR
<div id="content">

<span class="tt">作者:xxx</span>

这是正文内容段落1.....

<span>这是正文内容段落2</span>

<p>这是正文内容段落3......</p>

<span>这是广告</span>
<p>这是版权声明!</p>
</div>
STR;

$rules = [
'content' => ['#content','html']
];

$data = QueryList::rules($rules)
->html($html)
->query()
->getData(function($item){
$ql = QueryList::html($item['content']);
$ql->find('.tt,span:last,p:last')->remove();
$item['content'] = $ql->find('')->html();
return $item;
});

print_r($data->all());
}
1
    
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
Array
(
[0] => Array
(
[content] => 这是正文内容段落1.....

<span>这是正文内容段落2</span>

<p>这是正文内容段落3......</p>
)

)

处理乱码

内容乱码是采集过程中很常见的问题。

一.使用QueryList内置的乱码解决方案

1.使用编码转换插件,设置输入输出编码

1
2
3
4
5
6
7
8
9
10
11
12
    $html =<<<STR
<div>
<p>这是内容</p>
</div>
STR;
$rule = [
'content' => ['div>p:last','text']
];
$data = QueryList::html($html)->rules($rule)
->encoding('UTF-8','GB2312')
->query()
->getData();
  1. 设置输入输出编码,并移除html头部

如果设置输入输出参数仍然无法解决乱码,那就使用 removeHead()方法移除html头部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$html =<<<STR
<div>
<p>这是内容</p>
</div>
STR;
$rule = [
'content' => ['div>p:last','text']
];
$data = QueryList::html($html)->rules($rule)
->removeHead()->query()->getData();
// 或者
$data = QueryList::html($html)->rules($rule)
->encoding('UTF-8','GB2312')
->removeHead()
->query()
->getData();

二.自己手动转码页面,然后再把页面传给QueryList

1
2
3
4
5
6
7
$url = 'http://top.etao.com/level3.php?spm=0.0.0.0.Ql86zl&cat=16&show=focus&up=true&ad_id=&am_id=&cm_id=&pm_id=';
//手动转码
$html = iconv('GBK','UTF-8',file_get_contents($url));
$data = QueryList::html($html)->rules([
"text" => [".title a","text"]
])->query()->getData();
print_r($data);

处理采集结果

QueryList返回的集合数据均为Collection集合对象而非普通数组,目的就是为了方便处理采集结果数据。

QueryList引入了LaravelCollection集合对象,它提供了一个更具可读性的、更便于处理数组数据的封装。下面通过几个例子来说明它的用法,更多用法可以去查看Laravel文档。

Collection文档:https://d.laravel-china.org/docs/5.4/collections

例子

采集所有图片链接,采集目标:

1
2
3
4
5
6
7
8
9
10
11
$html =<<<STR
<div class="xx">
<img data-src="/path/to/1.jpg" alt="">
</div>
<div class="xx">
<img data-src="/path/to/2.jpg" alt="">
</div>
<div class="xx">
<img data-src="/path/to/3.jpg" alt="">
</div>
STR;
  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public function test_handle()
{
$html =<<<STR
<div class="xx">
<img data-src="/path/to/1.jpg" alt="">
</div>
<div class="xx">
<img data-src="/path/to/2.jpg" alt="">
</div>
<div class="xx">
<img data-src="/path/to/3.jpg" alt="">
</div>
STR;
$data = QueryList::html($html)->rules([
'image' => ['.xx>img','data-src']
])->query()->getData(function($item){
return $item;
});

print_r($data->all());
}
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Array
(
[0] => Array
(
[image] => /path/to/1.jpg
)
[1] => Array
(
[image] => /path/to/2.jpg
)
[2] => Array
(
[image] => /path/to/3.jpg
)
)

简化数据

如果我们想要的结果是一位数组,而非二位数组,那该怎么做呢?

可以使用flatten()方法将多维集合转为一维的,对上面的采集结果data进行处理:

1
print_r($data->flatten()->all());
  • 输出结果
1
2
3
4
5
6
Array
(
[0] => /path/to/1.jpg
[1] => /path/to/2.jpg
[2] => /path/to/3.jpg
)

截取数据

如果我们只想要前2条数据,其它数据都是多余的,那该怎么做呢?

take() 方法返回给定数量项目的新集合,对最初的采集结果data进行处理:

1
print_r($data->flatten()->take(2)->all());
  • 输出结果
1
2
3
4
5
Array
(
[0] => /path/to/1.jpg
[1] => /path/to/2.jpg
)

你也可以传入负整数从集合末尾开始获取指定数量的项目,下面获取data数据中最后2条数据:

1
print_r($data->flatten()->take(-2)->all());
  • 输出结果
1
2
3
4
5
Array
(
[1] => /path/to/2.jpg
[2] => /path/to/3.jpg
)

翻转数据顺序

某些情况下我们需要翻转数据顺序,比如:采集论坛的帖子列表,帖子默认是按照发布日期由新到旧排序的,但我们把这些数据存入数据库的时候,想要按照发布日期由旧到新存入

reverse() 方法用来倒转集合中项目的顺序:

1
print_r($data->flatten()->reverse()->all());
  • 输出结果
1
2
3
4
5
6
Array
(
[2] => /path/to/3.jpg
[1] => /path/to/2.jpg
[0] => /path/to/1.jpg
)

过滤数据

filter()方法用于按条件过滤数据,只保留满足条件的数据。

下面例子过滤掉图片路径为/path/to/2.jpg的值。

  • 采集代码
1
2
3
4
5
$dat = $data->filter(function ($item) {
return $item['image'] != '/path/to/2.jpg';
})->flatten()->all();

print_r($dat);
  • 采集结果
1
2
3
4
5
Array
(
[0] => /path/to/1.jpg
[1] => /path/to/3.jpg
)

遍历数据,依次处理每一项数据

map() 方法遍历集合并将每一个值传入给定的回调。该回调可以任意修改项目并返回,从而形成新的被修改过项目的集合。下面遍历data并补全图片链接地址:

  • 采集代码
1
2
3
4
5
6
$ret = $data->map(function($item) {
$item['image'] = 'http://xxx.com' . $item['image'];
return $item;
})->flatten()->all();

print_r($ret);
  • 采集结果
1
2
3
4
5
6
Array
(
[0] => http://xxx.com/path/to/1.jpg
[1] => http://xxx.com/path/to/2.jpg
[2] => http://xxx.com/path/to/3.jpg
)

连贯操作

Collection对象的所有方法都是可以连贯操作的,比如下面操作,先翻转数数据顺序,然后补全图片链接,最后截取前2条数据:

  • 采集代码
1
2
3
4
5
6
$res = $data->reverse()->map(function ($item) {
$item['image'] = 'http://xxx.com' . $item['image'];
return $item;
})->flatten()->take(2)->all();

print_r($res);
  • 采集结果
1
2
3
4
5
Array
(
[0] => http://xxx.com/path/to/3.jpg
[1] => http://xxx.com/path/to/2.jpg
)

进阶

元素操作

QueryList不仅可以读取DOM元素的属性值,还可以操作DOM元素。

替换元素属性值

attr()方法除了可以取DOM元素属性值外,还有第二个参数,用于设置元素属性值。

text()方法默认无参调用表示获取元素的纯文本内容,加个参数调用就表示设置元素的内容。

使用场景:比如采集文章时,下载文章中的图片,并替换文章中的图片路径为本地路径

  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public function test_attr()
{
$html =<<<STR
<div>
<a href="https://querylist.cc" alt="abc">QueryList</a>
</div>
STR;
$ql = QueryList::html($html);
// 获取a元素对象
$link = $ql->find('a:eq(0)');

// 设置元素属性值
$link->attr('href', 'https://baidu.com');
$link->attr('alt', '百度');

// 设置元素内容
$link->text('百度一下');
// html()方法用法与text()方法相同,唯一区别是可以用于设置元素的内容为HTML内容。
//$link->html('<p>百度一下</p>');

$data = $ql->find('div')->html();
print_r($data);
}
  • 采集结果
1
<a href="https://baidu.com" alt="百度">百度一下</a>

追加元素

append()方法用于追加元素。

  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public function test_append()
{
$html =<<<STR
<div>
<a href="https://querylist.cc" alt="abc">QueryList</a>
</div>
STR;
$ql = QueryList::html($html);
// 获取div元素对象
$div = $ql->find('div:eq(0)');
// 向div元素中追加一个img元素
$div->append('<img src="1.jpg" />');

$data = [];
$data[] = $div->find('img')->attr('src');
$data[] = $ql->find('div')->html();

print_r($data);
}
  • 采集结果
1
2
3
4
5
6
Array
(
[0] => 1.jpg
[1] => <a href="https://querylist.cc" alt="abc">QueryList</a>
<img src="1.jpg">
)

移除元素

remove()方法用于移除元素,常用于移除采集内容中的无关内容,在内容过滤章节有详细讲解到。

1
$ql->find('div')->remove('img');

替换元素

replaceWith()方法用于替换元素。

下面例子替换所有链接为文本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public function test_replaceWith()
{
$html =<<<STR
<div>
<a href="https://qq.com">QQ</a>
<a class="ql" href="https://querylist.cc" alt="abc">QueryList</a>
<a href="https://baidu.com">百度一下</a>
</div>
STR;
$ql = QueryList::html($html);

$ql->find('a')->map(function ($a) {
$text = $a->text();
$a->replaceWith('<span>'.$text.'</span>');
});

$data = $ql->find('div')->html();

print_r($data);
}
  • 采集结果
1
2
3
<span>QQ</span>
<span>QueryList</span>
<span>百度一下</span>

移除元素属性

removeAttr()方法可用来移除元素属性。

  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public function test_replaceWith()
{
$html =<<<STR
<div>
<a href="https://qq.com">QQ</a>
<a class="ql" href="https://querylist.cc" alt="abc">QueryList</a>
<a href="https://baidu.com">百度一下</a>
</div>
STR;
$ql = QueryList::html($html);

// 移除元素属性
$ql->find('a')->removeAttr('alt');

$data = $ql->find('div')->html();

print_r($data);
}
  • 采集结果
1
2
3
<a href="https://qq.com">QQ</a>
<a class="ql" href="https://querylist.cc">QueryList</a>
<a href="https://baidu.com">百度一下</a>

获取父元素、临近元素

parent()方法用于获取当前元素的父元素。

next()prev()方法用于获取当前元素临近的下一个元素和上一个元素。

使用场景:当你想选择的元素没有明显的特征,如:class、id等,此时就可以选择与之相关联的元素,通过关联元素选择到你想要选择的元素

  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public function test_replaceWith()
{
$html =<<<STR
<div>
<a href="https://qq.com">QQ</a>
<a class="ql" href="https://querylist.cc" alt="abc">QueryList</a>
<a href="https://baidu.com">百度一下</a>
</div>
STR;
$ql = QueryList::html($html)
$link = $ql->find('.ql');

$data = [];
// 获取父元素的内容
$data['parent'] = $link->parent()->html();
// 获取临近的下一个元素的内容
$data['next'] = $link->next()->text();
// 获取临近的前一根元素的属性
$data['prev'] = $link->prev()->attr('href');

print_r($data);
}
  • 采集结果
1
2
3
4
5
6
7
8
Array
(
[parent] => <a href="https://qq.com">QQ</a>
<a class="ql" href="https://querylist.cc" alt="abc">QueryList</a>
<a href="https://baidu.com">百度一下</a>
[next] => 百度一下
[prev] => https://qq.com
)

功能扩展

QueryList是完全模块化的设计,拥有强大的可扩展性。

例子

  1. 注册一个自定义的http网络操作方法到QueryList对象。
  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function test_bind()
{
// 采集开发者头条
$ql = QueryList::getInstance();
// 注册一个myHttp方法到QueryList对象
$data = $ql->bind('myHttp', function ($url) {
$html = file_get_contents($url);
$this->setHtml($html);

return $this;
});
// 然后就可以通过注册的名字来调用
$data = $ql->myHttp('https://toutiao.io')->find('h3 a')->texts();
print_r($data->all());

// 或者
$data = $ql->rules([
'title' => ['h3 a', 'text'],
'link' => ['h3 a', 'href']
])->myHttp('https://toutiao.io')->query()->getData();
print_r($data->all());
}
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Array
(
[0] => 用 500 行 Golang 代码实现高性能的消息回调中间件
[1] => 腾讯大神教你如何解决 Android 内存泄露
[2] => [译] 普通码农入门机器学习,必须掌握这些数据技能
[3] => 教你用 Carthage + RXSwift + MVVM + Moya + Router 写一个小说阅读 App
//...
)
Array
(
[0] => Array
(
[title] => 用 500 行 Golang 代码实现高性能的消息回调中间件
[link] => /k/u6hhfn
)
[1] => Array
(
[title] => 腾讯大神教你如何解决 Android 内存泄露
[link] => /k/abg526
)
[2] => Array
(
[title] => [译] 普通码农入门机器学习,必须掌握这些数据技能
[link] => /k/cnbt4o
)
[3] => Array
(
[title] => 教你用 Carthage + RXSwift + MVVM + Moya + Router 写一个小说阅读 App
[link] => /k/1aaumb
)
//....
)
  1. 自定义一个简单的图片下载功能。
  • 采集代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public function test_download()
{
// 采集并下载ZOL桌面壁纸
// 扩展一个图片下载功能
// 参数:$path 为图片本地保存路径
$query_list = QueryList::getInstance();
$ql = $query_list->bind('downloadImage', function ($path) {
$data = $this->getData()->map(function ($item) use ($path) {
// 获取图片
$img = file_get_contents($item['image']);
$localPath = $path . '/' . md5($img) . '.jpg';
// 保存图片到本地路径
file_put_contents($localPath, $img);
// data数组中新增一个自定义的本地路径字段
$item['local_path'] = $localPath;

return $item;
});
// 更新data属性
$this->setData($data);

return $this;
});

$img_path = base_path() . '/public/download';
$data = $ql->get('http://desk.zol.com.cn')->rules([
'image' => ['#newPicList img', 'src']
])->query()->downloadImage($img_path)->getData();

print_r($data->all());
}
  • 采集结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Array
(
[0] => Array
(
[image] => http://desk.fd.zol-img.com.cn/t_s208x130c5/g5/M00/0C/01/ChMkJ1nDaCOIatt0AAStbpl0q7sAAgrLABXih4ABK2G911.jpg
[local_path] => img/59561f7b8c122d529b9709fdc93283cd.jpg
)
[1] => Array
(
[image] => http://desk.fd.zol-img.com.cn/t_s208x130c5/g5/M00/04/0D/ChMkJ1mvUQ2IRSccAAIWHljxrrYAAgONAMJtn8AAhY2932.jpg
[local_path] => img/00bfaf54c930247815b6d906827600a9.jpg
)
[2] => Array
(
[image] => http://desk.fd.zol-img.com.cn/t_s208x130c5/g5/M00/04/00/ChMkJ1mtG--IPy-5AAOcpLiVZyQAAgLHwB3T3gAA5y8026.jpg
[local_path] => img/60ca7c8575da1f7746cb3e69918a7d68.jpg
)
// ...
)

全局配置

使用QueryList全局配置,避免重复操作。
QueryListconfig()方法可用于全局配置QueryList

示例

在项目的启动文件中全局注册一些QueryList插件和扩展一些功能,以Laravel框架为例,在AppServiceProvider.php文件的boot()方法中全局配置QueryList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use QL\QueryList;

class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot()
{
// 全局注册插件
QueryList::config()->use(My\MyPlugin::class, $arg1, $arg2, $arg3)
->use([
My\MyPlugin1::class,
My\MyPlugin2::class,
Other\OtherPlugin::class
]);
// 全局注册一个自定义的编码转换方法
QueryList::config()->bind('myEncode', function($outputEncoding, $inputEncoding) {
$html = iconv($inputEncoding,$outputEncoding. '//IGNORE', $this->getHtml());
$this->setHtml($html);
return $this;
});
}

/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}
  • 使用插件
1
2
3
4
5
6
public function test_plugin()
{
$data = QueryList::get('...')->myPlugin1('...')->rules('...')->queryData();

$data = QueryList::get('https://top.etao.com')->myEncode('UTF-8','GBK')->find('a')->texts();
}

技巧

插件推荐

Powered by Hexo and Hexo-theme-hiker

Copyright © 2017 - 2023 Keep It Simple And Stupid All Rights Reserved.

访客数 : | 访问量 :