用nodejs和python实现一个爬虫来爬网站(智联招聘)的信息

<small>
最近研究了一下网站爬虫,觉得python和nodejs都有优点,所以我决定实现一个爬虫,用python来抓取网页的源代码,用nodejs的cheerio模块来获取源代码内的数据。正好我有明年换工作的打算,于是我选择爬智联招聘的网站。
代码地址:https://github.com/duan602728596/ZhiLianUrllib</small>

1.用python进行一个http请求

# coding: utf-8
# http.py

import sys
import types
import urllib
import urllib2

# 获取传递的参数
# @param argv[0]{string}:脚本名称
# @param argv[1]{string}:请求方式,get或post
# @param argv[2]{string}:请求地址
# @param argv[3]{string}:请求的数据
argv = {
    'filename': sys.argv[0],
    'method': sys.argv[1],
    'url': sys.argv[2],
    'data': sys.argv[3],
}


class Http:
    # 初始化数据
    def __init__(self, method, url, data = ''):
        self.method = method            # 请求的类型
        self.url = url                  # 请求的地址
        self.data = self.getData(data)  # 请求的数据
        # 请求头
        self.header = {
            'Accept-Encoding': 'deflate',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36',
            'cache-control': 'no-cache',
        }
    # 获取请求数据的
    def getData(self, data):
        if type(data) is types.StringType:
            gd = data
        elif type(data) is types.DictionaryType:
            gd = urllib.urlencode(data)
        else:
            gd = ''
        return gd
    # get
    def get(self):
        if self.data == '':
            u = self.url
        else:
            u = self.url + '?' + self.data
        request = urllib2.Request(u)
        response = urllib2.urlopen(request)
        return response.read()
    # post
    def post(self):
        request = urllib2.Request(self.url, self.data, self.header)
        response = urllib2.urlopen(request)
        return response.read()
    # init
    def init(self):
        if self.method == 'get':
            self.result = self.get()
        elif self.method == 'post':
            self.result = self.post()
        else:
            self.result = ''

# 初始化请求
http = Http(argv['method'], argv['url'], argv['data'])
http.init()
text = http.result

# 输出请求
print(text)

在该脚本中,使用sys库获取命令行传递的各种参数,使用types库进行数据类型的判断,使用urllib库和urllib2库进行网页内容的抓取。传递的参数有请求的方式、请求的url地址、请求的数据。初始化后,根据传递的请求方式决定执行get请求还是post请求,执行请求后将结果输出出来,传回nodejs程序中。

2.nodejs和python实现通信

/**
 * pyhttp.js
 *
 * 与python脚本通信,进行一个请求
 * @param info{object}:与python脚本通信的配置
 * @param callback{function}:通信完成后执行的事件,传递参数为返回的数据
 */

const childProcess = require('child_process');


function pyhttp(info, callback){
    /* 发送请求 */
    return new Promise((resolve, reject)=>{
        // cmd
        const cps = childProcess.spawn('python', [
            // avgs
            info.file,
            info.method,
            info.url,
            info.data
        ]);
        // 储存文本
        let txt = '';

        // 错误
        cps.stderr.on('data', function(data){
            reject(data);
        });

        // 获取数据
        cps.stdout.on('data', function(data){
            txt += data;
        });

        // 获取完数据
        cps.on('exit', function(code){
            resolve(txt);
        });

    }).then(callback).catch((error)=>{
        console.log(error);
    });
}

module.exports = pyhttp;

在nodejs脚本中执行其他脚本并返回执行结果,使用child_process模块,语法为** child_process.spawn(command, [args], [options]) command是命令,args是参数。在这里我遇到了一个小小的坑,我之前是用的child_process.exec(command, [options], callback),但是这个的返回值是有大小限制的,因为网站的源代码比较大,导致报错。用child_process.spawn(command, [args], [options])**或者重新设置返回值大小可解决。调用pyhttp.js需要传递两个参数,第一个参数是运行python脚本的命令配置,第二个参数是回调函数,,传递脚本的运行结果。

3.对源代码进行处理


/**
 * deal.js
 *
 * 处理数据
 * @param dealText{string}:获取到的页面源代码
 * @param ishref{boolean}:是否获取下一页的地址,默认为false,不获取
 */

const cheerio = require('cheerio');


/* 提取冒号后面的文本 */
const mhtext = text => text.replace(/.+:/, '');

function each($, ishref = false){
    const a = [];
    // 获取table
    const $table = $('#newlist_list_content_table').children('table');
    for(let i = 0, j = $table.length; i < j; i++){
        const $this = $table.eq(i);
        const $tr = $this.children('tr'),
            $tr0 = $tr.eq(0),
            $tr1 = $tr.eq(1);
        const $span =  $tr1.children('td').children('div').children('div').children('ul').children('li').children('span');

        if($this.children('tr').children('th').length <= 0){
            a.push({
                // 职位招聘
                'zwzp': $tr0.children('.zwmc').children('div').children('a').html(),
                // 招聘地址
                'zpdz': $tr0.children('.zwmc').children('div').children('a').prop('href'),
                // 反馈率
                'fklv': $tr0.children('.fk_lv').children('span').html(),
                // 公司名称
                'gsmc': $tr0.children('.gsmc').children('a').html(),
                // 工作地点
                'gzdd': $tr0.children('.gzdd').html(),
                // 进入地址
                'zldz': $tr0.children('.gsmc').children('a').prop('href'),
                // 公司性质
                'gsxz': mhtext($span.eq(1).html()),
                // 公司规模
                'gsgm': mhtext($span.eq(2).html())
            });
        }
    }

    const r = {};
    r['list'] = a;
    if(ishref != false){
        r['href'] = $('.pagesDown').children('ul').children('li').children('a').eq(2).prop('href').replace(/&p=\d/, '');
    }
    return r;
}

function deal(dealText, ishref = false){
    const $ = cheerio.load(dealText, {
        decodeEntities: false
    });


    return each($, ishref);
}

module.exports = deal;

deal.js用cheerio模块来对抓取到的源代码进行处理。传递参数dealText为源代码,ishref 为是否抓取分页的地址。
注意,在用cheerio模块来获取数据时有一个问题,

const cheerio = require('cheerio');
const html = `<div id="demo">
                <ul>
                  <li>1</li>
                  <li>2</li>
                  <li>3</li>
                </ul> 
              </div>`;
const $ = cheerio.load(html);
/*  获取li */
$('#demo').children('li');                // 这样是获取不到li的
$('#demo').children('ul').children('li'); // 获取到了li

虽然cheerio的语法和jquery一样,但是原理千差万别,因为网页的数据被解析成了object对象,所以必须通过子节点一级一级向下查找,不能跳级。
数据处理:公司性质和公司规模删除掉了:和:前面的文字,下一页的url地址删除掉&p=\d参数,该参数是分页参数。

4.nodejs和python实现通信

/* app.js */
const fs = require('fs');
const pyhttp = require('./pyhttp');
const deal = require('./deal');
const _result = {};

/**
 * 请求地址和参数
 *
 * jl:地点
 * kw:职位关键字
 * sf:工资范围下限
 * st:工资范围上限
 * el:学历
 * et:职位类型
 * pd:发布时间
 * p:  分页page
 * ct:公司性质
 * sb:相关度
 * we: 工作经验
 *
 */

const info = (url, method = 'get', data = '')=>{
    return {
        // python脚本
        file: 'http.py',
        // 请求类型
        method: method,
        // 请求地址
        url: url,
        // 请求数据
        data: data
    }
};

const page = 4; // 循环次数

// 回调
const callback = (text)=>{
    return new Promise((resolve, reject)=>{
        resolve(text);
    });
};

pyhttp(info(encodeURI('http://sou.zhaopin.com/jobs/searchresult.ashx?' +
                       'jl=北京&kw=web前端&sm=0&sf=10001&st=15000&el=4&we=0103&isfilter=1&p=1&et=2')), function(text){

    const p0 = deal(text, true);
    _result.list = p0.list;

    const n = [];
    for(let i = 0; i < page; i++){
        n.push(pyhttp(info(`${p0.href}&p=${i + 2}`)), callback);
    }

    Promise.all(n).then((result)=>{
        for(let i in result){
            _result.list = _result.list.concat(deal(result[i]).list);
        }
    }).then(()=>{
        fs.writeFile('./result/result.js', `window._result = ${JSON.stringify(_result, null, 4)};`, (error)=>{
            if(error){
                console.log(error);
            }else{
                console.log('写入数据成功!');
            }
        });
    });
});

将pyhttp.js和deal.js包含进来后,首先对智联的搜索页进行一次请求,回调函数内处理返回的源代码,将第一页数据添加到数组,并且获取到了分页的地址,使用Promise.all并行请求第2页到第n页,回调函数内对数据进行处理并添加到数组中,将数据写入result.js里面(选择js而不是json是为了便于数据在html上展现)。
获取到的数据:

1.jpg

5.页面上展现数据

/* 渲染单个数据 */
const Group = React.createClass({
    // 处理a标签
    dela: str => str.replace(/<a.*>.*<\/a>/g, ''),
    // 处理多出来的标签
    delb: str => str.replace(/<\/?[^<>]>/g, '),
    render: function(){
        return (<tr key={this.props.key}>
            <td><a href={this.props.obj.zpdz} target='_blank'>{this.delb(this.props.obj.zwzp)}</a></td>
            <td>{this.props.obj.fklv}</td>
            <td>{this.dela(this.props.obj.gsmc)}</td>
            <td>{this.props.obj.gzdd}</td>
            <td><a href={this.props.obj.zldz} target='_blank'>{decodeURI(this.props.obj.zldz)}</a></td>
            <td>{this.props.obj.gsxz}</td>
            <td>{this.props.obj.gsgm}</td>
        </tr>);
    }
});

/* 表格类 */
const Table = React.createClass({
    // 渲染组
    group: function(){
        return window._result.list.map((object, index)=>{
            return (<Group key={index} obj={object} />);
        });
    },
    render: function(){
        return (
            <table className='table table-bordered table-hover table-condensed table-striped'>
                <thead>
                    <tr className='info'>
                        <th className='td0'>职位</th>
                        <th className='td1'>反馈率</th>
                        <th className='td2'>公司名称</th>
                        <th className='td3'>工作地点</th>
                        <th className='td4'>智联地址</th>
                        <th className='td5'>公司性质</th>
                        <th className='td6'>公司规模</th>
                    </tr>
                </thead>
                <tbody>{this.group()}</tbody>
            </table>
        );
    }
});

ReactDOM.render(
    <Table />,
    document.getElementById('result')
);

在页面上展示数据,使用react和bootstrap。其中在展示时,公司名称发现有无用a标签,职位内有b标签,使用正则表达式删除它们。
页面结果:


2.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容

  • 英国人Robert Pitt曾在Github上公布了他的爬虫脚本,导致任何人都可以容易地取得Google Plus...
    LucasHC阅读 1,893评论 1 25
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,970评论 6 13
  • 那些在别人听起来很开心的歌,其实不知道承载了自己多少故事。 那些日子 一幕幕在脑海里浮现。。可是却谁都不能说,。。
    旻序阅读 240评论 0 0
  • 我的文章《小跳琼》将在起点中文网上发布所以简书上的文章内容将被删除,我的笔名一律改为芜芫梦蝶,且我的文章目前只有《...
    芜芫梦蝶阅读 221评论 0 0
  • 2017年8月20日赖渝蓉种种子第二十天 发心:我今不是为了我个人而闻思修,而是为了六道轮回一切如母有情众生,愿一...
    晓茂阅读 125评论 0 3