python练手之微博爬虫

    从半年前声称完成python入门以来,从来没有进行过非API调用的python实战,之前的BP神经网络python版也只是用了pybrain包提供的API而已。惊觉这样下去可能直到我把python语法忘干净都不敢说自己真的掌握了python,但是仍然无所事事拖到大概20天前,室友实验室发下任务要写个Java爬虫,我才决定同时写个python爬虫看看能不能体现下python开发效率的优势。

1. 什么是网络爬虫

    在展开介绍我的python爬虫程序之前先来回顾一下网络爬虫实际上要做些什么事。
    从结果来看,网络爬虫是一种从互相关联的互联网网页上批量获取网页数据的技术手段。从流程上来看,爬虫程序是一个递归过程:

  • (1)请求网页数据,开始步骤(2);
  • (2)解析网页数据,分别解析出目的数据和请求下一个网页的超链接地址
  • (3)如果不再有符合条件的超链接地址则结束递归过程,否则回到步骤(1)。

    从图论的角度看,如果把网页看成搜索树的状态结点,那么常见的网络爬虫可以看成是一个单一分支的深度优先搜索,当然用于搜索引擎的网络爬虫则常常需要解析出网页中蕴含的多个搜索分支,这就更像我们熟悉的深度优先搜索了。
    怎么样,其实很简单吧。计算机技术中最通用的技术,其原理往往是最简单的。

2.为什么需要网络爬虫

    网络爬虫最常见的应用场景就是获取(遍历)多页数据列表中的所有数据,比如获取网易云音乐某个歌单中的所有音乐地址甚至音乐数据本身,又比如获取豆瓣电影某个主题下的所有电影。
    事实上,互联网时代到来之后,大数据时代接踵而至,计算机乃至其他领域的大量实验数据都来自互联网。除了直接从服务器获得数据之外,最普遍的数据获取途径就是通过最大的开放性数据来源WWW(世界范围Web)网络来请求数据了,毕竟比起HTTP协议,其他应用层通讯协议太多太杂太难分析,也不够开放。于是爬虫也成为了一种大量实验数据的获取手段。

3. 怎么实现网络爬虫

    我选择了python作为写网络爬虫的语言,事实上据我所知,Java、PHP、C++、Ruby、Perl等我们熟知的绝大多数完善的编程语言都能用来写网络爬虫,或者说,只要有网络通信库的语言都可以用来写网络爬虫。其实相对来说,python的网络通信库还是很发达的,再加上python语言语法的简洁性和数据结构操作天生的灵活性,我觉得用python写网络爬虫没准是最快的。(别说Java可以用封装好的现成爬虫Jar包,没有可比性)
    我们通过实例来了解网络爬虫的实现过程。我选择了新浪微博作为爬虫对象,具体来说是将特定新浪微博用户的微博主页作为爬取对象。这里有两点需要注意:
    首先,我选择的是微博移动版的页面而非PC版的页面作为爬取对象。移动版的页面结构非常简单而且不含任何JavaScript成分,微博数据相对很容易解析出来,而PC版的页面所有的微博数据都由JavaScript填充,虽然不至于找不到数据但是XML结构之外解析数据就几乎要全靠正则表达式了。此外虽然PC版的页面看起来蕴含更多信息,但是许多信息都是通过JavaScript,通过AJAX技术动态向服务器申请来的。如果选择PC版页面作为爬取对象,将大量增加页面解析难度,甚至可能需要让爬虫过程发生嵌套,使整个爬虫流程十分复杂。如果数据能从移动版页面获取,尽量不要选择解析PC版页面。
    然后,需要想办法获得登录微博后的cookie,或者cookie的必要字段。微博跟早期的百度贴吧不一样,不登录微博账号的话什么微博数据都看不到。平时我们的浏览器如果没有保存微博登录后的cookie,在访问包含微博数据的页面地址时,HTTP请求会被重定向到登录页面。只有在访问页面时令HTTP请求报文头中携带登录后的cookie,该请求才被服务器认定为合法请求,才会正常的返回被请求的页面。获取cookie有两种途径:

  • 通过各种浏览器的HTTP报文抓取插件,从HTTP请求的文件头中提取出现成的cookie;
  • 模拟微博账号登录过程,在登录完成之后的第一次页面访问请求的HTTP报文头中提取出cookie。

3.1 python下的HTTP请求

    python下如何完成一次HTTP请求并获得返回的页面数据?哦顺便一说我用的是python2,python3应该区别不大。

1
2
import urllib2
print urllib2.urlopen('www.baidu.com').read()

    完了?完了。是不是很简单?python大法好!开个玩笑不要在意,上面的两行代码只是实现了最简单的默认请求形式及返回数据的获取。实际上urllib2类的urlopen方法参数可以是一个url或者是一个封装好的Request对象,返回值是一个封装好的Response对象。python的网络编程确实很简单直观,我不想细说,至于urllib2的具体的各种相关python API,请自行查阅手册或者等下看我的源码。

3.2 页面解析

    页面解析是爬取页面数据后的后续操作,我们实际需要的数据往往蕴含在页面源码之中,最简单粗暴的解析方式就是观察页面源码然后用正则表达式匹配出需要的数据字段。此外既然页面源码的组织方式是HTML,而HTML是一种XML结构,那么我们就可以用一些XML解析工具比如BueatifulSoup、XPath来辅助页面解析。这些工具比较容易寻找到整个HTML文本数据中需要的标签,接下来再用正则表达式简单处理一下,数据就不难获得了。顺便一提,python的正则表达式包叫做re,用法参见手册

3.3 模拟登录

    固然,我们可以偷懒一直选择填入现成的cookie,但是须知,cookie都是有时限或者说寿命的,一定时间后,cookie会过期失效,不再合法。这时就需要重新登录获得一个新的cookie了。虽然cookie过期并没有那么快,但是总是需要有人在侧监视,随时更换cookie来维持比较长的爬虫过程,岂不是显得很low很不自动化?所以大部分比较大、比较正式的爬虫程序都会选择实现特定网站的模拟登录过程。
    具体到微博上来,weibo.com即PC版微博网站的登录过程网上有相当多的分析文章,比如这篇这篇这篇。读者需要注意的是,据我所知weibo.com的登录过程微调十分频繁,写一个weibo.com模拟登录可能几个月后就需要根据新浪的微调调整代码了。至于weibo.cn,网上简单的找找似乎没有发现分析文章。但是当时据我猜测,weibo.cn的登录过程应该比weibo.com简单(事实证明确实如此),登录过程微调后修改代码也会更容易。而登录后的cookie,经过我的实验,是通用的。也就是说不论在weibo.cn还是weibo.com,登录后产生的细节各不相同的cookie可能有一个共同部分是用来验证登录状态的(事实再次证明确实如此)。
    我使用的HTTP报文抓取插件是firefox下的HttpFox,同类插件还有httpwatch、firefox的自带工具、chrome的自带工具,甚至一些较新版本的IE自带工具。记录登录过程,分析跳转了哪些地址,有几次重定向,期间请求携带了哪些数据,cookie有什么变化。这里我简单说下weibo.cn的登录过程。
    总体来说weibo.cn的登录过程分成三个步骤,请求登录页面,获取一些post登录信息的必要参数;post登录信息表单;等待。
    登录页面中的action(post目标地址)、加后缀的密码数据键名(如password_2358)和一个叫做vk的数据都是提交登录表单时必要的。
    之后就要把表单加工成为可填入Request对象的形式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
form_data = {
'mobile': username,
password_name: password,
'remeber': 'on',
'backURL': 'http://weibo.cn/',
'backTitle': '微博',
'tryCount': '',
'vk': vk,
'submit': '登录'
}
form_data = urllib.urlencode(form_data)

    提交表单后新浪服务器会对这一请求进行4次重定向,注意每次重定向都需要提取并重填充cookie字段,因为python的默认重定向处理类不会重填cookie,重写的重定向处理方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyRedirectHandler(urllib2.HTTPRedirectHandler):
'''
继承urllib2.HTTPRedirectHandler类,封装http_error_302方法。
在自动执行重定向发送新的HTTP请求前提取附在请求头中的cookie,
提取cookie中模拟登录必需的字段并存储在类成员变量中。
'''

# 类成员变量,存储以登录状态访问新浪微博必需的cookie字段
cookie = ''

def http_error_302(self, req, fp, code, msg, headers):
'''
添加cookie处理过程,然后调用原http_error_302方法执行自动跳转。
'''

cookie = str(headers["Set-Cookie"])
if re.search('SUB=.+?;', cookie) is not None:
MyRedirectHandler.cookie = re.search('SUB=.+?(?=;)', cookie).group(0)
req.add_header("Cookie", cookie)
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)

    最后一次重定向几乎是一个纯JavaScript页面,做的事情是通过JavaScript跳转向一个地址。实验证明最后这一次跳转对于登录过程并没有什么意义,看起来是故弄玄虚。记得我们刚才重写的重定向处理方法吗?我们在重不断重填cookie的过程中也记录了cookie某个字段的更新情况,最后,我们手里有那个字段的最新值。这个cookie字段正是微博登录验证唯一必需的cookie字段。至于我怎么知道是这个字段,对照实验。
    是不是比weibo.com的登录过程简单多了?

3.4 可能的改进

    读者可能发觉了,每次请求的间隔长达几秒,这个爬虫程序与其说是一个高效获取数据的工具,不如说是一个模拟人类操作的脚本而已。简单概括一下就是,爬取页面太慢了。那么如何改进呢?并行化。模拟一个人的访问太慢,那么模拟更多人的访问就好。
    并行改进有几个层次,硬件架构的并行化(多核,流水线,长指令等等)、多线程、多进程和分布式。硬件层次的改进对我们这个爬虫程序意义不大,多线程在别的语言中当然可行,可是据我所知,python中的多线程性能收到了限制,python中应该尽量使用多进程。在多进程的基础上,实现完善的分布式通信,就能做分布式并行了。
    有读者可能会想到,直接把程序改成多进程的难道不是相当于加大了请求频率而已?是的,确实是这样,所以进程间应该“完全独立”,一个进程被新浪服务器封禁,不应该影响其他进程。这里就涉及一个问题,新浪服务器封禁的是微博账号还是IP地址呢?经过实验答案是IP地址,实际上,目前大多数服务器的反DDoS机制就是封IP。这就好办了,微博账号申请还相对麻烦,可是免费或收费的IP代理却很容易批量获得。比如这里,其中免费的可以直接爬取,肯花钱就能直接获得官方批量代理的接口。当然,广域网分布式具有天然的互异IP,也可以选择使用多台机器,不使用代理。

    全文基本没讲解代码是不是有点过分?这次确实有点偷懒,但是源码里注释写的还是挺详细了。上干货,我把完成的单进程python微博爬虫简单封装了一下,挂在了自家git仓库

Fork me on GitHub