需求来源,需要用PHP解析HTML提取我想要的数据

用PHP写网站爬虫的时候,需要把爬取的网页进行解析,提取里面想要的数据,这个过程叫做网页HTML中数据结构化。
很多人应该知道用phpQuery像JQuery一样的语法进行网页处理,抽取想要的数据。
但是在复杂一些的场景phpQuery并不能很好的完成工作,说简单点就是复杂场景不好用。
有没有更好的方式呢,我们看看商业爬虫软件是怎么做的。

他山之石,商业爬虫怎么做的

市面上商业爬虫怎么做HTML解析抽取数据的

看看市面上商业爬虫怎么做HTML解析结构化数据字段抽取
通过观察市面上商业爬虫工具GooSeeker、神箭手、八爪鱼,可以知道他们都用一个叫做XPath表达式的方式提取需要的字段数据。
那么可以判断使用XPath对HTML解析进行数据定位提取就是通行最佳的方式之一。

PHP中的XPath支持

XPath表达式可以查找HTML节点或元素,是一种路径表达语言。
那么需要先学习下XPath的基础,花个1-2小时入门,XPath就是页面数据提取能力的最佳内功之一,这个时间值得花。
既然用XPath提取页面数据是通行的方式,那么PHP中支持XPath的扩展包是什么呢?
为了帮大家节约时间,Symfony DomCrawler 就是PHP中最佳XPath包之一,直接用他吧,Symfony出品质量可是有目共睹,PHP热门框架laravel都用Symfony的包。
Symfony DomCrawler官方文档介绍的实例有限,建议有需要的情况下把代码读下,更能熟知新用法。

撸起袖子干,用DomCrawler做XPath HTML页面解析结构化数据抽取

基本思路

在Chrome浏览器中安装”XPath Helper”插件(XPath Helper怎么使用见参考资料)
打开需要解析的网站页面编写和测试XPath表达式
在PHP代码用DomCrawler使用上XPath表达式抽取想要的字段数据

实例演示

解析《神偷奶爸3》页面的的电影介绍信息
在Chrome中用编写测试XPath表达式

xpath helper使用


在项目下用composer安装guzzlehttp/guzzle(http client)、symfony/dom-crawler(Symfony DomCrawler)

composer require  guzzlehttp/guzzle
composer require  symfony/dom-crawler

下面直接上代码,在代码中用注释做说明
php Douban.php

<?php
/**
 * Created by PhpStorm.
 * User: wwek
 * Date: 2017/7/9
 * Time: 21:41
 */

require __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Client;
use Symfony\Component\DomCrawler\Crawler;

print_r(json_encode(Spider(), JSON_UNESCAPED_UNICODE));
//print_r(Spider());

function Spider()
{
    //需要爬取的页面
    $url = 'https://movie.douban.com/subject/25812712/?from=showing';

    //下载网页内容
    $client   = new Client([
        'timeout' => 10,
        'headers' => ['User-Agent' => 'Mozilla/5.0 (compatible; Baiduspider-render/2.0; +http://www.baidu.com/search/spider.html)',
        ],
    ]);
    $response = $client->request('GET', $url)->getBody()->getContents();

    //进行XPath页面数据抽取
    $data    = []; //结构化数据存本数组
    $crawler = new Crawler();
    $crawler->addHtmlContent($response);

    try {
        //电影名称
        //网页结构中用css选择器用id的比较容易写xpath表达式
        $data['name'] = $crawler->filterXPath('//*[@id="content"]/h1/span[1]')->text();
        //电影海报
        $data['cover'] = $crawler->filterXPath('//*[@id="mainpic"]/a/img/@src')->text();
        //导演
        $data['director'] = $crawler->filterXPath('//*[@id="info"]/span[1]/span[2]')->text();
        //多个导演处理成数组
        $data['director'] = explode('/', $data['director']);
        //过滤前后空格
        $data['director'] = array_map('trim', $data['director']);

        //编剧
        $data['cover'] = $crawler->filterXPath('//*[@id="info"]/span[2]/span[2]/a')->text();
        //主演
        $data['mactor'] = $crawler->filterXPath('//*[@id="info"]/span[contains(@class,"actor")]/span[contains(@class,"attrs")]')->text();
        //多个主演处理成数组
        $data['mactor'] = explode('/', $data['mactor']);
        //过滤前后空格
        $data['mactor'] = array_map('trim', $data['mactor']);

        //上映日期
        $data['rdate'] = $crawler->filterXPath('//*[@id="info"]')->text();
        //使用正则进行抽取
        preg_match_all("/(\d{4})-(\d{2})-(\d{2})\(.*?\)/", $data['rdate'], $rdate); //2017-07-07(中国大陆) / 2017-06-14(安锡动画电影节) / 2017-06-30(美国)
        $data['rdate'] = $rdate[0];
        //简介
        //演示使用class选择器的方式
        $data['introduction'] = trim($crawler->filterXPath('//div[contains(@class,"indent")]/span')->text());

        //演员
        //本xpath表达式会得到多个对象结果,用each方法进行遍历
        //each是传入的参数是一个闭包,在闭包中使用外部的变量使用use方法,并使用变量指针
        $crawler->filterXPath('//ul[contains(@class,"celebrities-list from-subject")]/li')->each(function (Crawler $node, $i) use (&$data) {
            $actor['name']   = $node->filterXPath('//div[contains(@class,"info")]/span[contains(@class,"name")]/a')->text(); //名字
            $actor['role']   = $node->filterXPath('//div[contains(@class,"info")]/span[contains(@class,"role")]')->text(); //角色
            $actor['avatar'] = $node->filterXPath('//a/div[contains(@class,"avatar")]/@style')->text(); //头像
            //background-image: url(https://img3.doubanio.com/img/celebrity/medium/5253.jpg) 正则抽取头像图片
            preg_match_all("/((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+\.(jpg|jpeg|gif|png)/", $actor['avatar'], $avatar);
            $actor['avatar'] = $avatar[0][0];
            //print_r($actor);
            $data['actor'][] = $actor;
        });

    } catch (\Exception $e) {

    }

    return $data;

}

执行结果

{
    "name": "神偷奶爸3 Despicable Me 3",
    "cover": "https://img3.doubanio.com/view/movie_poster_cover/lpst/public/p2469070974.webp",
    "director": [
        "凯尔·巴尔达",
        "皮艾尔·柯芬"
    ],
    "mactor": [
        "史蒂夫·卡瑞尔",
        "克里斯汀·韦格",
        "崔·帕克",
        "米兰达·卡斯格拉夫",
        "拉塞尔·布兰德",
        "迈克尔·贝亚蒂",
        "达纳·盖尔",
        "皮艾尔·柯芬",
        "安迪·尼曼"
    ],
    "rdate": [
        "2017-07-07(中国大陆)",
        "2017-06-14(安锡动画电影节)",
        "2017-06-30(美国)"
    ],
    "introduction": "  《神偷奶爸3》将延续前两部的温馨、搞笑风格,聚焦格鲁和露西的婚后生活,继续讲述格鲁和三个女儿的爆笑故事。“恶棍”奶爸格鲁将会如何对付大反派巴萨扎·布莱德,调皮可爱的小黄人们又会如何耍贱卖萌,无疑让全球观众万分期待。该片配音也最大程度沿用前作阵容,史蒂夫·卡瑞尔继续为男主角格鲁配音,皮埃尔·柯芬也将继续为经典角色小黄人配音,而新角色巴萨扎·布莱德则由《南方公园》主创元老崔·帕克为其配音。",
    "actor": [
        {
            "name": "皮艾尔·柯芬 ",
            "role": "导演",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/1389806916.36.jpg"
        },
        {
            "name": "凯尔·巴尔达 ",
            "role": "导演",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/51602.jpg"
        },
        {
            "name": "史蒂夫·卡瑞尔 ",
            "role": "饰 Gru / Dru",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/15731.jpg"
        },
        {
            "name": "克里斯汀·韦格 ",
            "role": "饰 Lucy Wilde",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/24543.jpg"
        },
        {
            "name": "崔·帕克 ",
            "role": "饰 Balthazar Bratt",
            "avatar": "https://img3.doubanio.com/img/celebrity/medium/5253.jpg"
        },
        {
            "name": "米兰达·卡斯格拉夫 ",
            "role": "饰 Margo",
            "avatar": "https://img1.doubanio.com/img/celebrity/medium/1410165824.37.jpg"
        }
    ]
}

经验

网页上数据是动态变化的,没获取到的字段需要用try catch进行异常处理,这样程序就不会崩溃
XPath表达式孰能生巧基本能应付绝大多数的需求
某些数据如果用XPath表达式也不好取,或者取出来的数据还需要加工的,用正则表达式处理,用preg_match_all进行抽取,用preg_replace进行替换
用strip_tags()函数去除HTML、XML以及PHP的标签,加参数可以保留标签去除,如处理文章内容strip_tags($str, "<p><img><strong>"),后留后面参数中的标签
一些常用的正则表达式


$str=preg_replace("/\s+/", " ", $str); //过滤多余回车 $str=preg_replace("/<[ ]+/si","<",$str); //过滤<__("<"号后面带空格) $str=preg_replace("/<\!--.*?-->/si","",$str); //注释 $str=preg_replace("/<(\!.*?)>/si","",$str); //过滤DOCTYPE $str=preg_replace("/<(\/?html.*?)>/si","",$str); //过滤html标签 $str=preg_replace("/<(\/?head.*?)>/si","",$str); //过滤head标签 $str=preg_replace("/<(\/?meta.*?)>/si","",$str); //过滤meta标签 $str=preg_replace("/<(\/?body.*?)>/si","",$str); //过滤body标签 $str=preg_replace("/<(\/?link.*?)>/si","",$str); //过滤link标签 $str=preg_replace("/<(\/?form.*?)>/si","",$str); //过滤form标签 $str=preg_replace("/cookie/si","COOKIE",$str); //过滤COOKIE标签 $str=preg_replace("/<(applet.*?)>(.*?)<(\/applet.*?)>/si","",$str); //过滤applet标签 $str=preg_replace("/<(\/?applet.*?)>/si","",$str); //过滤applet标签 $str=preg_replace("/<(style.*?)>(.*?)<(\/style.*?)>/si","",$str); //过滤style标签 $str=preg_replace("/<(\/?style.*?)>/si","",$str); //过滤style标签 $str=preg_replace("/<(title.*?)>(.*?)<(\/title.*?)>/si","",$str); //过滤title标签 $str=preg_replace("/<(\/?title.*?)>/si","",$str); //过滤title标签 $str=preg_replace("/<(object.*?)>(.*?)<(\/object.*?)>/si","",$str); //过滤object标签 $str=preg_replace("/<(\/?objec.*?)>/si","",$str); //过滤object标签 $str=preg_replace("/<(noframes.*?)>(.*?)<(\/noframes.*?)>/si","",$str); //过滤noframes标签 $str=preg_replace("/<(\/?noframes.*?)>/si","",$str); //过滤noframes标签 $str=preg_replace("/<(i?frame.*?)>(.*?)<(\/i?frame.*?)>/si","",$str); //过滤frame标签 $str=preg_replace("/<(\/?i?frame.*?)>/si","",$str); //过滤frame标签 $str=preg_replace("/<(script.*?)>(.*?)<(\/script.*?)>/si","",$str); //过滤script标签 $str=preg_replace("/<(\/?script.*?)>/si","",$str); //过滤script标签 $str=preg_replace("/javascript/si","Javascript",$str); //过滤script标签 $str=preg_replace("/vbscript/si","Vbscript",$str); //过滤script标签 $str=preg_replace("/on([a-z]+)\s*=/si","On\\1=",$str); //过滤script标签 $str=preg_replace("/&#/si","&#",$str); //过滤script标签,如javAsCript:alert(

参考资料

官方文档The DomCrawler Component
八爪鱼的了解XPath常用术语和表达式解析 十分钟轻松入门
gooseeker的xpath基础知识培训
神箭手的常用的辅助开发工具
XPath Helper:chrome爬虫网页解析工具 Chrome插件图文教程
在PHP中,您如何解析和处理HTML/XML?

扩展阅读

关于反爬虫,看这一篇就够了
最好的语言PHP + 最好的前端测试框架Selenium = 最好的爬虫(上)
QueryList 简单、 灵活、强大的PHP采集工具,让采集更简单一点。