Laravel Symfony_Crawler GuzzleHttp 爬虫 抓取行政区域

2018/05/29 修改抓取编码gb2312改gb18030

项目需要行政区域三级联动,刚好写个爬虫练练手。

Laravel 框架,安装的两个库

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

创建表


DROP TABLE IF EXISTS `area`;
CREATE TABLE `area` (
  `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `parent_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

DROP TABLE IF EXISTS `crawler`;
CREATE TABLE `crawler` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `status` int(11) DEFAULT '0',
  `data` text COLLATE utf8mb4_unicode_ci,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

创建对应Model

App\Model\Area.php

<?php

namespace App\Model;

use Illuminate\Database\Eloquent\Model;

class Area extends Model
{
    public $timestamps = false;

    protected $table = 'area';

    protected $keyType = 'string';

    protected $fillable = [
        'id', 'name', 'parent_id',
    ];
}

App\Model\Crawler.php

<?php

namespace App\Model;

use Illuminate\Database\Eloquent\Model;

class Crawler extends Model
{
    public $timestamps = false;

    protected $table = 'crawler';

    protected $fillable = [
        'id', 'status', 'data'
    ];
}

app/Console/Kernel.php 添加

    protected $commands = [
        'App\Console\Commands\CityCrawler',  
    ];

新建 App\Console\Commands\CityCrawler.php

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Symfony\Component\DomCrawler\Crawler;

use App\Model\Area;
use App\Model\Crawler as CrawlerTask;

use GuzzleHttp\Psr7;
use GuzzleHttp\Exception\RequestException;

// 流程:
//     1. func top 抓取行政区域省级, 每个省链接生成一次抓取任务,保存到任务表crawler。
//     2. 循环抓取 
//             1). 读取一条任务 select * from crawler where status = 0 limit 1; update crawler set status = 1 where id = 本次任务id;
//             2). 根据任务类型调用抓取方法 如 镇抓取:crawler_towntr 区抓取crawler_districts 城市抓取crawler_citys
//                 抓取方法中保存抓取到的行政区域数据到area表,并根据抓取行政区域下一级生成下一次抓取任务,存放任务表
class CityCrawler extends Command
{

    protected $signature = 'street:crawler';
    protected $description = 'Street Crawler';
    protected $start_url = 'http://www.stats.gov.cn/tjsj/tjbz/tjyqhdmhcxhfdm/2016/';
    protected $special_city = ['东莞市','中山市','嘉峪关市','三沙市','儋州市']; // 中国5个不设市辖区的地级市 

    public function handle()
    {
        //抓取省级行政区域
        $this->top(); 

        while (true) {
            $task_model = $this->task();

            if(empty($task_model)){
                return $this->info("End");
            }
            $task= json_decode($task_model->data, true);

            // 打印日志
            $this->info(implode(',', array_map(function($item){
                return $item['id'] . ' ' . $item['name'];
            }, $task['data'])));

            $status = call_user_func(array($this, 'crawler_' . $task['crawler']), $task);
            if($status){
                $this->finish($task_model);
            }else{
                var_dump($task, 'error');
                return false;
            }
            $this->info("sleep 1");
            sleep(1);
        }
    }


    public function finish($task){
        $task->status = 2;
        $task->save();
    }

    public function task($status = 0)
    {
        $task = CrawlerTask::where("status", $status)->first();
        $task->status = 1; // 进行中
        $task->save();
        return $task;
    }

    public function push($data)
    {
        $task = new CrawlerTask;
        $task->data = json_encode($data);
        $task->save();
    }

    // 第一个页面
    public function top()
    {
        $url = $this->start_url;

        $html = $this->send_http($url);

        $crawler = new Crawler();
        $crawler->addHtmlContent($html, 'gb18030');
        $crawler->filter('.provincetr')->filter('td > a')->each(function(Crawler $node, $i) use($url) {
            $text = $node->text();
            $href = $node->attr('href');
            $id = str_replace('.html', '', $href);
            
            $task = [
                'crawler' => 'citys',
                'remark' => '省',
                'url' => substr($url, 0, strrpos($url, '/')) . '/' . $href,
                'data' => [ [ 'name' => $text, 'id' => $id] ],
                'parent_id' => $id,
            ];
            $this->push($task);

            Area::create(
                [
                    'id' => $id,
                    'name' => $text,
                    'parent_id' => 0
                ]
            );

            $this->info($node->attr('href'));
            $this->info($text);
        });
    }


    public function crawler_towntr($task)
    {

        $url = $task['url'];

        if(!strpos($url, '.html')){
            $this->info('为空的直辖市');
            return true;
        }

        $html = $this->send_http($url);

        $crawler = new Crawler();
        $crawler->addHtmlContent($html, 'gb18030');
        $crawler->filter('.towntr')->each(function(Crawler $node, $i) use ($task, $url) {

            $code_node = $node->filter('td')->eq(0)->filter('a');
            $name_node = $node->filter('td')->eq(1)->filter('a');

            Area::create(
                [
                    'id' => $code_node->text(),
                    'name' => $name_node->text(),
                    'parent_id' => $task['parent_id']
                ]
            );

            $this->info($code_node->text() . '  ' . $name_node->text());
        });

        return true;
    }

    public function crawler_districts($task)
    {
        $url = $task['url'];
        $html = $this->send_http($url);

        $crawler = new Crawler();
        $crawler->addHtmlContent($html, 'gb18030');
        $crawler->filter('.countytr')->each(function(Crawler $node, $i) use ($task, $url) {


            $code_node = $node->filter('td')->eq(0)->filter('a');
            $name_node = $node->filter('td')->eq(1)->filter('a');
            
            //没有子节点
            if($code_node->count() == 0){   
                $code_node = $node->filter('td')->eq(0);
                $name_node = $node->filter('td')->eq(1);
            }else{

                $href = $code_node->attr("href");
                $data = $task['data'];
                $data[] = ['name' => $name_node->text(), 'id' => $code_node->text()] ;

                $new_task = [
                    'crawler' => 'towntr',
                    'remark' => '县 区',
                    'url' => substr($url, 0, strrpos($url, '/')) . '/' . $href,
                    'data' => $data,
                    'parent_id' => $code_node->text(),
                ];

                $this->push($new_task);                
            }

            $this->info($code_node->text() . '  ' . $name_node->text());

            Area::create(
                [
                    'id' => $code_node->text(),
                    'name' => $name_node->text(),
                    'parent_id' => $task['parent_id']
                ]
            );

        });
        return true;
    }

    public function crawler_citys($task)
    {
        $url = $task['url'];
        $html = $this->send_http($url);

        $crawler = new Crawler();
        $crawler->addHtmlContent($html, 'gb18030');
        $crawler->filter('.citytr')->each(function(Crawler $node, $i) use ($task, $url) {

            $code_node = $node->filter('td')->eq(0)->filter('a');
            $name_node = $node->filter('td')->eq(1)->filter('a');
            $href = $code_node->attr("href");

            $this->info($code_node->text() . '  ' . $name_node->text());

            Area::create(
                [
                    'id' => $code_node->text(),
                    'name' => $name_node->text(),
                    'parent_id' => $task['parent_id']
                ]
            );
            

            $data = $task['data'];
            $data[] = ['name' => $name_node->text(), 'id' => $code_node->text()] ;

            if(in_array($name_node->text(), $this->special_city)){
                $new_task = [
                    'crawler' => 'towntr',
                    'remark' => '特别的5个省地级市',
                    'url' => substr($url, 0, strrpos($url, '/')) . '/' . $href,
                    'data' => $data,
                    'parent_id' => $code_node->text(),
                ];                
            }else{

                $new_task = [
                    'crawler' => 'districts',
                    'remark' => '城市',
                    'url' => substr($url, 0, strrpos($url, '/')) . '/' . $href,
                    'data' => $data,
                    'parent_id' => $code_node->text(),
                ];

            }


            $this->push($new_task);

        });

        return true;
    }

    public function info($string, $verbosity = null)
    {
        $string = iconv( 'UTF-8', 'GB18030', $string); // cmd 中文gbk编码
        parent::line($string, 'info', $verbosity);
    }

    private function send_http($url)
    {   
        $user_agent_list = [
            'Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.04',
            'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.9 Safari/537.36',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36 OPR/48.0.2685.52',
            'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0',
            'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27',
            'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko',
        ];

        $user_agent = $user_agent_list[(time() % 6)];
        $timeout = 5; // 秒

        $client = new \GuzzleHttp\Client(['headers' => ['User-Agent' => $user_agent], 'timeout' => $timeout]);

        try {
            $res  = $client->request('GET', $url);
            $html =  (string)$res->getBody();

        } catch (RequestException $e) {
            // 抓取中会有404状态返回,再重新请求一次。
            $this->info(Psr7\str($e->getRequest()));
            if ($e->hasResponse()) {
                $this->info(Psr7\str($e->getResponse()));
            }

            $this->info("send_http timeout retry");
            $this->info("sleep 2s");
            sleep(2);
            $res  = $client->request('GET', $url);
            $html =  (string)$res->getBody();              
        }
        
        return $html;
    }

}

进目录运行

php artisan street:crawler

 

最后

------------------------------------------------------

数据有了全部写到一个json文件里,太大了1M多 :(

还是写成ajax从服务端读取三级联动数据。

area.js

// 1. 省加载 其他请选择
// 2. 省 change 触发加载市
// 3. 市触发加载区
// 4. 区加载触发街道
function area_init(param) {
    var area = this;
    area.area_not_filter = false;;
    area.prompt_html = '<option value="">-请选择-</option>';
    if (param.area_not_filter) {
        area.area_not_filter = param.area_not_filter;
    }
    if (param.province) {
        area.province_el = $(param.province);
    }
    if (param.city) {
        area.city_el = $(param.city);
    }
    if (param.district) {
        area.district_el = $(param.district);
    }
    if (param.street) {
        area.street_el = $(param.street);
    }
    area.load = function() {
        area.province_el.html(area.prompt_html);
        area.city_el.html(area.prompt_html);
        area.district_el.html(area.prompt_html);
        if (area.street_el) {
            area.street_el.html(area.prompt_html);
        }
        province_id = area.province_el.attr("data-value");
        city_id = area.city_el.attr("data-value");
        district_id = area.district_el.attr("data-value");
        if (area.street_el) {
            street_id = area.street_el.attr("data-value");
        }
        area.area_fill(0, 'province', area.province_el, province_id);
        province_id && area.area_fill(province_id, 'city', area.city_el, city_id);
        city_id && area.area_fill(city_id, 'district', area.district_el, district_id);
        if (area.street_el) {
            district_id && area.area_fill(district_id, 'street', area.street_el, street_id);
        }
    }
    area.bind = function() {
        area.province_el.change(function() {
            area.area_fill($(this).val(), 'city', area.city_el);
            area.city_el.html(area.prompt_html);
            area.district_el.html(area.prompt_html);
            if (area.street_el) {
                area.street_el.html(area.prompt_html);
            }
        });
        area.city_el.change(function() {
            area.area_fill($(this).val(), 'district', area.district_el);
            area.district_el.html(area.prompt_html);
            if (area.street_el) {
                area.street_el.html(area.prompt_html);
            }
        });
        if (area.street_el) {
            area.district_el.change(function() {
                area.area_fill($(this).val(), 'street', area.street_el);
                area.street_el.html(area.prompt_html);
            });
        }
    };
    area.area_fill = function(parent_id, level, el, active_id) {
        // value='' 不请求  ajax
        if (parent_id === '') {
            return false;
        }
        area.get_area(parent_id, level, function(list) {
            var province = area.prompt_html;
            $.each(list, function(i, n) {
                province += '<option ' + (active_id == n.id ? ' selected ' : ' ') + ' value="' + n.id + '">' + n.name + '</option>';
            });
            $(el).html(province);
        });
    }
    area.get_area = function(parent_id, level, callback) {
        var $url = '/ajax_area/' + level + '/' + parent_id;
        $.ajax({
            url: $url,
            type: 'get',
            success: function(res) {
                callback(res);
            }
        });
    }
    area.load();
    area.bind();
}

// 使用方法
// data-value 默认值
// 32 江苏省
// 320100000000 南京市
// 320102000000 玄武区
// <select name="province" data-value="32"></select>
// <select name="city" data-value="320100000000"></select>
// <select name="district" data-value="320102000000"></select>
// <select name="street" data-value=""></select>

// var area = new area_init(
//         {
//             province: "select[name='province']",
//             city: "select[name='city']",
//             district: "select[name='district']",
//             street: "select[name='street']"
//         }  
//     );

// 或者

// <select name="province" data-value="32"></select>
// <select name="city" data-value="320100000000"></select>

// var area = new area_init(
//         {
//             province: "select[name='province']",
//             city: "select[name='city']"
//         }  
//     );

// 如果需要js动态修改
// $('select[name="province"]').attr('data-value', data.province);
// $('select[name="city"]').attr('data-value', data.city);
// $('select[name="district"]').attr('data-value', data.district);
// area.load();

 

成品长这样哈

转载于:https://my.oschina.net/jszhang/blog/1595998

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
•SeimiCrawler一个敏捷强大的Java爬虫框架 •1.简介 •2.需要 •3.快速开始 ◦3.1.maven依赖 ◦3.2.在SpringBoot中 ◦3.3.常规用法 •4.原理 ◦4.1.基本原理 ◦4.2.集群原理 •5.如何开发 ◦5.1.约定 ◦5.2.第一个爬虫规则类-crawler ◾5.2.1.注解@Crawler ◾5.2.2.实现startUrls() ◾5.2.3.实现start(Response response) ◾5.2.4.Response数据提取 ◾5.2.4.1.内部属性一览 ◾5.2.5.回调函数 ◾5.2.6.Request内部一览 ◾5.2.7.自定义UserAgent(可选) ◾5.2.8.启用cookies(可选) ◾5.2.9.启用proxy(可选) ◾5.2.10.设置delay(可选)* ◾5.2.11.设置请求URL白名单匹配规则 ◾5.2.12.设置请求URL黑名单匹配规则 ◾5.2.13.设置动态代理 ◾5.2.14.是否开启系统去重 ◾5.2.15.关于自动跳转 ◾5.2.16.异常请求处理 ◾5.2.17.SeimiAgent支持 ◾5.2.17.1.基本配置 ◾5.2.17.1.1.直接运行 ◾5.2.17.1.2.SpringBoot项目 ◾5.2.17.2.使用 ◾5.2.18.启动爬虫系统 ◾5.2.18.1.SpringBoot(推荐) ◾5.2.18.2.直接运行,独立启动 ◦5.3.工程化打包部署 ◾5.3.1.SpringBoot(推荐) ◾5.3.2.独立直接运行 ◦5.4.定时调度 ◦5.5.自动解析Bean ◾5.5.1.注解@Xpath ◾5.5.2.使用 ◦5.6.拦截器 ◾5.6.1.注解@Interceptor ◾5.6.2.接口SeimiInterceptor ◾5.6.3.拦截器样例 ◦5.7.关于SeimiQueue ◾5.7.1.配置使用DefaultRedisQueue ◾5.7.1.1.SpringBoot项目 ◾5.7.1.2.直接运行(非SpringBoot) ◾5.7.2.自行实现SeimiQueue ◾5.7.3.SeimiQueue样例 ◦5.8.集成主流数据持久化 ◾5.8.1.准备工作 ◾5.8.2.写一个DAO ◾5.8.3.开始使用DAO ◦5.9.分布式 ◦5.10.通过http服务接口操作 ◾5.10.1.Request必填参数 ◾5.10.2.SpringBoot(推荐) ◾5.10.3.直接运行 ◾5.10.3.1.发送抓取请求 ◾5.10.3.2.接口描述 ◾5.10.3.3.查看抓取状态 •6.常见问题汇总 ◦6.1.如何设置网络代理 ◦6.2.如何开启cookie ◦6.3.如何启用分布式模式 ◾6.3.1.参考 ◾6.3.2.特别注意 ◦6.4.如何设置复杂的起始请求 •7.社区讨论 •8.项目源码

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值