分类目录归档:未分类

scrapy打造分布式带自动登陆的企业信息爬虫

题记:

  最近再做一个征信的项目,当查询企业征信时需要输入公司名称全称和企业所在的城市,而且你还不能输错,我就不想多吐槽了。反正,我就是感觉不爽,怎么办,自己动手,丰衣足食。就自己来获取下这些数据吧。

正文:

  为了获取全国企业的简要信息,我开始从网上找有这些信息的网站,最后找到了几个: 一个是企查查,一个是知企业,还有就是各个城市自己的工商系统。最后,我就选择先从知企业这里获取到公司的基本信息和公司所在地信息。

  分析完需求,开始进行技术选型: 爬虫就选用功能强大的scrapy,爬取的数据结构类似文档类型,选用mongodb比较合适。考虑到数据量比较大,大致计算下单机跑要很多天,程序中断等异常情况很容易发生,所以记录下程序爬取的记录很有必要,分析目标网站,每个公司由32位16进制字符标识,这样的话,存储几百万数据到内存中占用的空间也不大,就直接丢到redis中吧。然后爬虫本身加一个布隆过滤,已经爬过的就不在爬取。

  首先配置mongo,一开始我希望把redis装在开放到公网的树莓派上(路由器刷华硕固件后,开启ngrok将内网的树莓派等设备映射到公网),但是即使是树莓派3了,arm也已经是v7了,支持mongodb3时还是有点力所不能及(因为希望用mongodb3以上的WiredTiger引擎,看官方数据,这个比默认的MMAP引擎性能上提升了将近30%)。折腾了一番,无果,就放到阿里云上吧,相关配置如下:

systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log
storage:
  dbPath: /var/lib/mongo
  journal:
    enabled: true
  engine: wiredTiger
  wiredTiger:
    engineConfig:
      cacheSizeGB: 1
      #cache_size: 300M
      directoryForIndexes: true
    collectionConfig:
      blockCompressor: snappy
    indexConfig:
      prefixCompression: true
processManagement:
  fork: true  # fork and run in background
  pidFilePath: /var/run/mongodb/mongod.pid
net:
  port: 27017
  bindIp: 0.0.0.0
security:
  authorization: enabled

。之后还发生了个小插曲,爬虫跑起来一段时间后我的主机竟然不能连接上去了,看监控信息内存爆掉了,看mongo文档,wiredTiger 引擎如果不配置cacheSizeGB 这个参数,则默认使用40% 系统内存,不过如果40%内存的值小于1G,就直接使用1G。果然很霸道,我这个机器的内存只有2G呀,mongo 引擎都要1G,再加上mongo自身需要的内存,大约1.3G就这样没了,而我这个机器上还跑的有别的程序,所以内存很容易就跑光了。最不可思议的是cacheSizeGB 这个参数是个整型,这样一来,你至少也得配1G,看来只能妥协了,加点内存吧,于是开启swap,添加1G的磁盘容量为swap内存,暂时解决了这个问题。

爬虫基本结构构造如下: 从首页初始化后,选择各个省份,然后进入到城市、地区,然后把这个页面上企业的url加入到爬取队列中,因为这个页面是分页的,所以分析完这个页面继续进入到下个页面进行分析,核心代码如下:

    """
    分析省份详情
    """
    def parse_province(self, response):
        # 获取省份名称,回调进入城市分析

    """
    分析城市详情
    """
    def parse_city(self, response):
        # 获取城市名称,回调进入区域分析

    """
    分析区域详情
    """
    def parse_area(self, response):
        # 获取区域名称,回调页面分析

    """
    分析页面上企业名单, 如果有下一页, 则在下一页继续执行本方法
    """
    def parse_page(self, response):
        xxx
        ar_corp = corp.get_corp_list(response)
        for corp_url in ar_corp:
            corp_id = corp.get_corp_id_by_url(corp_url)
            yield Request("%s%s" % (self.url_prefix, corp_url),
                          meta={"province": province,
                                "city": city,
                                "area": area,
                                "corp_id": corp_id}, callback=self.parse_corp)
        # 将下一页加入到爬取队列
        next_page = corp.get_next_page_url(response)
        if next_page:
            yield Request("%s%s" % (self.url_prefix, next_page),
                          meta={"province": province,
                                "city": city,
                                "area": area}, callback=self.parse_page)

    """
    分析企业详情
    """
    def parse_corp(self, response, meta=False):
        # 拼装item

  当我的爬虫愉快地爬了几天后,突然报异常了:页面上一些元素不能找到。赶紧到网站上一看,管理员在此期间,加了登陆限制,非登录用户只能查看很少一部分数据。还好登陆机制不太复杂,赶紧加了自动登陆的代码,在爬虫初始化后触发自动登陆,拿到授权cookie后再进行爬取。示例如下:

def start_requests(self):
        logging.info("start to login ...")
        return [Request("http://www.zhiqiye.com/index.html",
                    callback=self.post_login)]

def post_login(self, response):
    current = int(time.time() * 1000)
        return [FormRequest.from_response(response,
                                        url="http://www.zhiqiye.com/account/login/",
                                        method="POST",
                                        formdata={
                                            'time': "%s" % current,
                                            'username': self.user_name,
                                            'pass': self.password,
                                            'f': 'true'
                                        },
                                        dont_filter=True,
                                        callback=self.after_login)]

不过,爬取了几个小时候,发现又断了,看日志,继续报元素找不到异常。分析下来,原因是session过期了,看来需要做登陆态检查了。这里花了一番功夫,本想检查到登出后重新触发初始化就行了,然后发现并不行,后来了解到parse函数中做了return操作,这样就会触发数据保存并中断回调的操作,所以并不能在这里加回调操作。最终找到解决办法(v站讨论地址:https://www.v2ex.com/t/302345)就是定义一个重新登陆的函数,检测到登出态(通过捕捉爬虫元素报IndexError异常的方式)时调用这个函数(其实这里少了一个步骤:把本次失败的请求重新加入到爬虫队列中)。 示例如下:

try:
            corp = CorpParseModel()
            corp.get_corp_tips(response, item)
            corp.get_contact(response, item)
            corp.get_commercial(response, item)
            corp.get_corp_info(response, item)
            corp.get_relate_corp(response, item)
            return item
except IndexError:
            self.log("------------- start to login again -------------")
            if is_retry:
                self.log("------ retry login and fails ------")
            else:
                return self.post_login(response, True)

最后把mongo、redis 添加密码访问和ip限制,然后开放到公网,这样就可以多个设备一起愉快地爬取了。

心得:

1. 开发过程中注意对异常的处理,确保系统的健壮性。前期一定要有比较完整的日志收集,出问题时可以快速定位问题、分析问题。

2. scrapy 中可以使用 inspect_response 调试正则、附带的参数、cookie等信息,方便观察请求的细节。

3. 开发爬虫时可以定义一个测试用的爬虫,新的功能开发和测试可以现在测试爬虫完成后再放入到目标爬虫。

4. 监控很重要,要实时监控我们的系统运行的状态。开发这个爬虫项目时我加了个获取爬虫状态的功能到我的微信公众号,只要输入相关指令即可获取爬虫当前的状态(爬取总数、各省份爬取数量、模糊搜索等)

附:

1. 爬虫地址:https://github.com/liyj144/corporation_spider

2. 微信机器人地址(包含爬虫状态监控):https://github.com/liyj144/weixin_robot

vue 介绍

  这两天把玩了一个vue 这个前端小框架。作为一个面向前端展示层的框架,vue的表现确实出众。本系列文章将会简单介绍一下这个技术栈。本文第一篇,介绍一下vue的基本概念。

  vue的基本思想是: 采用 数据驱动 + 组件化 的方式进行前端界面开发, 特点是面向视图层,轻量级,方便与其他库结合使用。同时存在的一些不足有成熟的组件较少;而本身比较轻量,默认不包含路由、ajax等功能,需要开发人员造一些轮子,同时也缺少一些大型项目经验的分享(对比angular)。另提一点,作者是个国内小伙,在国内社区(知乎、V2、微博等)比较活跃,方便交流。

  首先介绍下数据驱动,vue的数据驱动采用来MVVM的模型结构,通过VM(ViewModel)连通前端view和数据层model,图示如下:

举例说明, 首先定义一个view层(你可以定义一个1.html):

<div id="example-1">
  Hello {{ name }}
</div>

然后定义数据model层:(你可以定义一个1.js)

var indexData = {
    name: 'Vue.js'
}

然后定义VM层来连接数据和页面,达到通过数据控制页面展示的效果:(下面的同样放入到1.js)

var indexVM = new Vue({
    el: ‘#example-1,
    data: indexData
})

这样就完成了我们第一个入门demo。 后继如果需要更改数据的内容, 只需要直接更改model的值即可,当然,推荐使用VM开放出来的方法和属性来进行修改:

方式1: indexData.name = 'liyj'   
方式2:  indexVM.$data.name = 'good'  (其中 indexVM.$data === indexData)
方式3(推荐): indexVM.$set("name", "exp")

简单了解完数据驱动、VM数据连接和绑定后,我们开始详细介绍数据驱动的一些知识点,只要分以下四个方面:数据控制、表单、动画效果 和 消息传递。

一、 数据控制

首先就是通过控制model中的数据,可以改变view层显示的内容,这点上面已经介绍了(通过VM的set方法来改变model的数据值).然后在VM中也定义了很多方法来改变视图层的相关内容,常用的方法如下:

data: Object或者Function, 数据model,Vue实例的数据对象。定义组件时类型为函数,返回原始数据对象。

props:Array 或者Object, 包含期望使用的父组件数据的属性,用于类型检查、验证,默认值等。

methods: Object类型, 实例方法。

computed: Object类型,实例计算属性值。getter和setter的this自动绑定到实例。

watch: Object类型,键是观察表达式,值是对应回调,主要功能是值发生变化时做一些触发性的动作,类似angular中使用watch定义的一些方法。

还有一些组特有的方法比如 filters,components 等,在示例中可以慢慢体会。

二、表单

view层比较重要的一块是表单,所以主要是单向数据绑定(注意这个单向指的是数据是由上到下,由父到子的,而子节点的数据改动不会影响到父节点)的vue也定义了v-model这个指令来在表单控件元素上创建双向数据绑定(关于双向数据绑定可以看一篇文章 AngularJS的数据双向绑定是怎么实现的,他的实现是事件触发的,而不是定时检测的。这种设计是一个比较优雅的理念,永远不要让我们的架构引入一堆不可控的组件比如定时器、消费队列等,相反,我们可以通过消息驱动、任务驱动和事件驱动等达到这一目的,后继会开一篇文章来介绍一下任务系统聊聊这个话题)。然后表单组件还会有一些事件触发的绑定,属性更新的绑定(普通组件也会需要到),vue中使用v-on和v-bind 这些指令来实现: v-bind 绑定属性: v-bind:href / :href, v-bind:class / :class, v-on 绑定事件: v-on:click / @click, v-on:submit / @submit。 绑定示例如下:

1. <input type="text" v-model="name" />
2. <div
      :class="{bold: isFolder}"
      @click="toggle"
      @dblclick.stop="changeType">
      {{model.name}}
      <span v-if="isFolder">[{{open ? '-' : '+'}}]</span>
    </div>
3. <ul v-show="open" v-if="isFolder">
      <item
        class="item"
        v-for="model in model.children"
        :model="model">
      </item>
      <li @click="addChild">+</li>
    </ul>

三、动画效果

页面展示要实现一些绚丽效果,动画是必不可少的。vue中使用过渡的概念来定义动画,通过在目标元素上添加transition属性来实现过渡的效果。定义过渡效果时主要关系enter 和 leave 这两个特性。然后我们就可以通过常用的几种前端动画实现方式来定义动画了:1. 通过class切换组件入和出。2. 设置CSS帧动画。3.通过JS动画实现。 示例如下:

1. 切换class
Vue.transition('expand', {
	enterClass: 'ex_in',
	leaveClass: 'ex_out'
});
css 定义:
.expand-transition {
	  transition: all .3s ease;
	  height: 30px;
	  padding: 10px;
	  background-color: #eee;
	  overflow: hidden;
}
.ex_in, .ex_out {
	  height: 0;
	  padding: 0 10px;
	  opacity: 0;
}
2. css帧动画:
.bounce-transition {
	  display: inline-block; /* 否则 scale 动画不起作用 */
	}
	.bounce-enter {
	  animation: bounce-in .5s;
	}
	.bounce-leave {
	  animation: bounce-out .5s;
	}
	@keyframes bounce-in {
	  0% {
	    transform: scale(0);
	  }
	  50% {
	    transform: scale(1.5);
	  }
	  100% {
	    transform: scale(1);
	  }
	}
	@keyframes bounce-out {
	  0% {
	    transform: scale(1);
	  }
	  50% {
	    transform: scale(1.5);
	  }
	  100% {
	    transform: scale(0);
	  }
	}
3. JS 动画:
Vue.transition('fade', {
	css: false,
	enter: function(el, done){
		$(el)
      		.css('opacity', 0)
      		.animate({ opacity: 1 }, 1000, done);
	},
	enterCancelled: function(el){
		$(el).stop();
	},
	leave: function(el, done){
		$(el).animate({ opacity: 0 }, 1000, done);
	},
	leaveCancelled: function(el){
		$(el).stop();
	}
})

四、数据传递

这里说的数据传递主要是组件间的数据传递。vue中组件在父子之间是单向传递的(广播的方式),由父到子,而在不同组件之间,作用域是孤立的。子组件的数据默认是不能流向父组件的,在程序中可以定义成消息触发的方式来传递到父组件(消息分发的方式),这样就消除了数据传递混乱的现象(父组件的数据可能被未知的子组件改变)。总结下来就是:

•1. 组件之间作用域是孤立的

•2. 父组件可以通过props 传递数据到子组件

•3. 子组件可以通过 this.$parent 和 this.$root 来访问父组件和根实例,但是尽量避免这样做

•4. 子组件可以通过事件和父组件通信

说完数据驱动,就很有必要在说一说今天的另一个重头:组件化。

vue中,我们希望页面是由一个个组件构成的(当然,组件中可以包含组件,比如一般我们会定义一个大的组件叫app,里面再包含页面上的其他组件),每个组件定义自己的VM,然后程序中通过控制VM来完成前端的逻辑。

组件可以定义为全局的或者局部的,建议公共组件定义为全局的,其他的定义为局部的。示例如下:

// global
// define global component:
var MyComponent = Vue.extend({
	template: '<p>{{msg}}</p>',
	props: ['msg']
});
// register component global
Vue.component('my-component', MyComponent);
// define VM:
new Vue({
	el: '#example-4'
})


// local
// define VM and component:
new Vue({
    el: ‘#example-4’,
    components : {
         “myComponent”: {
            props: ['msg'],
            template: '<span>{{ msg }}</span>'
        }
    }
})

然后页面上就可以通过 

<my-component msg="Hello World!"></my-component>

来引入了。

关于vue的介绍就基本到这里了,本篇文章的ppt和代码见github: https://github.com/liyj144/vue_introduction .

枝连效应小谈

      今天来公司时看到地铁站里红烧牛肉面的广告, 突发奇想: 广告行业为什么不枝连在一起来宣传? 比如卖汽车的,一般会有一个长得很帅的男模,这时为什么不顺便做服装的生意。一般情况下一个广告(3分钟以内),确实很难让人记住一大推的品牌,但是只有一个也会很无聊, 我想1-3分钟来熟悉3个品牌是很好的, 这三个产品(当然没有竞争关系,而且会有一些相似之处,比如用户品味、依赖关系等)可以一起投资来做一个效果很不错的广告,而且用户也会感觉很亲切,因为团结和结合的东西一般都会很亲民。所以,广告行业也需要打破公司间的壁垒,携手走向共赢。

关键字: 枝连、共赢、广告

udp包长度问题追踪

因为日常开发中我的应用日志一般都是通过udp包在网络上传输的,这样的话,当包长大于一定长度就会报错:

Traceback (most recent call last):
  File "test.py", line 8, in <module>
    sock.sendto(msg, ("127.0.0.1", 5555))
socket.error: [Errno 40] Message too long

测试下来发现,发送内容大于等于8185时就会报错,8184是个最大的安全值。测试程序如下:

client:

# coding=utf-8
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
msg = ""
for i in xrange(0, 1000):
  #msg += "中国你好啊,"
  msg += "hello,"
msg += "\n"
sock.sendto(msg, ("127.0.0.1", 5555))

server 端监听:

nc -ul 127.0.0.1 5555

发现这个现象后从网上找原因,首先是了解到 MTU(Maximum Transmission Unit,最大传输单元)。这个值一般设置都是1500 bytes. 可以通过ipconfig 或是 netstat -i 来查看各个网卡这个值的设置情况。然后去掉IP数据报的首部为20字节,UDP数据报的首部8字节,剩余1472字节用来存储数据。我们可以通过ping(ICMP协议,和UDP一样,报头也占用8个字节)来断定实际包传输情况。

找一个server机(IP 172.19.44.10):

root@jrtool ~]# netstat -i
Kernel Interface table
Iface       MTU Met    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0       1500   0 605731701      0      0      0 663673344      0      0      0 BMRU
lo        16436   0   614404      0      0      0   614404      0      0      0 LRU

然后在其他机器ping:


在server端:

可见本机发UDP包,最大可可发 65507 bytes (udp 理论长度 2^16 – udp head – iphead 也就是 65535-28=65507), 夸机器发包, 会比MTU(1500 大一些),可以达到 8184 ( 2^13 – 8 ). 

结论:

1. udp包最好不要超过1480 (IP 要占去20 bytes)

2. Internet上的标准MTU值为576字节,所以我建议在进行Internet的UDP编程时.    最好将UDP的数据长度控件在548字节(576-8-20)以内

3. udp包长在1500 到 65535 bytes 之间时,一般也可以接收,和发送的buffer设置有关,也和平台有关。

4. udp日志的设置最大就为8184 即可。

5. 系统的MTU是可以设置的,方法是: sudo ifconfig eth0 mtu 5000

 

参考: http://stackoverflow.com/questions/11809727/sendto-returns-values-greater-than-mtu-on-sock-dgram-udp-socket  

ES6 新特性1 — 箭头函数

今天无意看到一种新的方法定义方式,感觉还挺好玩的。记录如下:
 

var sum = (x, y)  =>  x + y;
sum(1,2)  //  result is 3

其中箭头方法是es6 引入的新的方法。 看到这个片段还是在看一个node的优秀项目介绍时看评论见到的(原文在 http://www.csdn.net/article/2013-12-17/2817827)。 感觉箭头方法在表示lambda时还挺方便的。就记录下使用中的一些技巧:

英文地址: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

基本语法如下: (示例部分参考 http://www.cnblogs.com/snandy/p/4403111.html

//基本语法为 (变量列表) => { 语句 }  |  表达式:
(param1, param2, paramN) => { statements }
(param1, param2, paramN) => expression
   // 等同于: => { return expression; }

// 单变量时括号可省略
singleParam => { statements }
singleParam => expression

// 没有参数时需要加上括号:
() => { statements }

// 高级用法:
//返回一个方法:
params => ({foo: bar})

// 支持不定变量的表达方式
(param1, param2, ...rest) => { statements }

//返回对象时需要用小括号包起来(大括号被占用解释为代码块)
var getHash = arr => {
    // ...
    return ({
        name: 'test',
        age: 33
    })
}

//直接作为事件handler
document.addEventListener('click', ev=> {
    console.log(ev)
})

//数组排序(实现cmp函数)
var arr = [1, 9 , 2, 4, 3, 8].sort((a, b)=> {
    if (a - b &gt; 0 ) {
        return 1
    } else {
        return -1
    }
})
arr // [1, 2, 3, 4, 8, 9]

//数组排序复习
function sortAsc(a,b)
{
return a - b;
}
function sortDesc(a,b)
{
return b - a;
}
var arr = new Array(6)
arr[0] = "10"
arr[1] = "5"
arr[2] = "40"
arr[3] = "25"
arr[4] = "1000"
arr[5] = "1"
arr.sort(sortAsc) // ["1", "5", "10", "25", "40", "1000"]
arr.sort(sortDesc) // ["1000", "40", "25", "10", "5", "1"]

//typeof运算符和普通的function一样
var func = a => a
console.log(typeof func); // "function"

// instanceof也返回true,表明也是Function的实例
console.log(func instanceof Function); // true

//箭头函数不能用new
var Person = (name, age) =>  {
    this.name = name
    this.age = age
}
var p = new Func('John', 33) // error

 

这个语法对方法简写和方法内的this带了了影响:

方法简写:

var a = [
  "Hydrogen",
  "Helium",
  "Lithium",
  "Beryl­lium"
];

var a2 = a.map(function(s){ return s.length });

var a3 = a.map( s => s.length );

this语法:

通过箭头方法的使用, 方法可以在内部访问自己的上下文,示例如下:

// to call object's variables
//In ECMAScript 3/5, this issue was fixed by assigning the value in this to a variable that could be closed over
function Person() {
  var self = this; // Some choose `that` instead of `self`. 
                   // Choose one and be consistent.
  self.age = 0;

  setInterval(function growUp() {
    // The callback refers to the `self` variable of which
    // the value is the expected object.
    self.age++;
  }, 1000);
}

// now
function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| properly refers to the person object
  }, 1000);
}

var p = new Person();

// strict mode 不受影响:
var f = () => {'use strict'; return this};
f() === window; // or the global object


//调用call 或 apply, 调用只是传参,对整体的this不会有影响

var adder = {
  base : 1,
    
  add : function(a) {
    var f = v => v + this.base;
    return f(a);
  },

  addThruCall: function(a) {
    var f = v => v + this.base;
    var b = {
      base : 2
    };
            
    return f.call(b, a);
  }
};

console.log(adder.add(1));         // This would log to 2
console.log(adder.addThruCall(1)); // This would log to 2 still

 

一些方便的示例:

// An empty arrow function returns undefined
let empty = () => {};

(() => "foobar")() // returns "foobar" 

var simple = a => a > 15 ? 15 : a; 
simple(16); // 15
simple(10); // 10

let max = (a, b) => a > b ? a : b;

// Easy array filtering, mapping, ...

var arr = [5, 6, 13, 0, 1, 18, 23];
var sum = arr.reduce((a, b) => a + b);  // 66
var even = arr.filter(v => v % 2 == 0); // [6, 0, 18]
var double = arr.map(v => v * 2);       // [10, 12, 26, 0, 2, 36, 46]