QueryList
是一套用于内容采集的PHP工具,它使用更加现代化的开发思想,语法简洁、优雅,可扩展性强。
相比传统的使用晦涩的正则表达式来做采集,QueryList使用了更加强大而优雅的CSS选择器来做采集,大大降低了PHP做采集的门槛,同时也让采集代码易读易维护
示例代码
先来感受一下使用 QueryList
来做采集是什么样子。
- 采集百度搜索结果列表的标题和链接。
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 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¶ms2=somevalue');
$ql->get('http://httpbin.org/get',[ 'param1' => 'testvalue', 'params2' => 'somevalue' ]);
$ql = QueryList::post('http://httpbin.org/post',[ 'param1' => 'testvalue', 'params2' => 'somevalue' ]);
|
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) ->concurrency(2) ->withOptions([ 'timeout' => 60 ]) ->withHeaders([ 'User-Agent' => 'QueryList' ]) ->success(function (QueryList $ql, Response $response, $index){ $data = $ql->queryData(); print_r($data); }) ->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 = 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
元素,用法同jQuery
的find()
方法。
实战 - 采集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>5月6日消息 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>根据天空新闻的报道,在位于巴基斯坦南部的纳瓦布沙在周一(4月30日)的时候出现了高达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>5月6日消息 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>根据天空新闻的报道,在位于巴基斯坦南部的纳瓦布沙在周一(4月30日)的时候出现了高达50.2度的气温,气象学家表示这或许是人类有史以来遇到的四月份最高的温度。</p> <p>法国气象局的气象学家卡比奇安在推特上表示,巴基斯坦的这个小城镇不但是有史以来亚洲遇到的最高的四月气温,更有可能是全球四月的最高温,而也有网友表示由于过于炎热的天气,当地已经有不少人因为中暑而丧命。</p> <p>全球极端天气专家克里斯托弗伯特也表示,四月份就达到50摄氏度极其罕见,纳瓦布沙的温度或将是人类有史以来遇到的温度最高的四月。农业学家表示巴基斯坦过高的温度会严重影响未来粮食的收割。</p> )
)
|
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&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'], '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条规则:
- 内容移除规则:选择器名前面添加减号(-),表示移除该标签以及标签内容。
- 内容保留规则:选择器名前面没有减号(-)(此时选择器只能为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 = [ 'content_html' => ['#content','html','a 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()方法
QueryList
的getData()
方法接收一个回调函数作为参数,这个回调函数用于遍历采集结果,并对结果进行处理,我们可以利用这个回调函数来过滤内容。
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 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();
|
- 设置输入输出编码,并移除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
引入了Laravel
中Collection
集合对象,它提供了一个更具可读性的、更便于处理数组数据的封装。下面通过几个例子来说明它的用法,更多用法可以去查看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); $link = $ql->find('a:eq(0)');
$link->attr('href', 'https://baidu.com'); $link->attr('alt', '百度');
$link->text('百度一下');
$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 = $ql->find('div:eq(0)'); $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
是完全模块化的设计,拥有强大的可扩展性。
例子
- 注册一个自定义的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(); $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 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() { $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); $item['local_path'] = $localPath;
return $item; }); $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
全局配置,避免重复操作。
QueryList
的config()
方法可用于全局配置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 {
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; }); }
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(); }
|
技巧
插件推荐