我写了一个新浪微博爬虫,然而这只是漫漫长征的第一步

Posted by rogerclarkgc on 周五 28 七月 2017

这是我学习python以来写的第三个完整的爬虫,写这个爬虫的目的是为了爬取新浪微博上一些关于环保话题的用户微博,并且顺便爬取用户的个人资料,这个爬虫应该是目前我写的爬取量最大的一个,目前已经爬取了4万条微博和用户个人资料数据,预计数据量将达到十万以上。

当然,仅仅爬取是不够的,爬虫不过是获取数据的一种方式,如今许多社会科学的调查仍然采用问卷调查的方法进行,我认为这种方式虽然是虽然可控性高、数据完整度好但是却存在较低的时效性和较高的主观性。并且问卷调查往往不能广泛进行,所以数据量往往也太小。如今是个信息社会,人们倾向用网络发表看法,群体的舆论倾向往往隐藏在那巨量的微博发言、微信状态中,因此,我认为有必要在社会科学甚至生态经济科学中引入使用网络舆论数据的方法,比如针对某个特定话题,获取新浪微博的用户发言,再对发言进行特定词汇的频率统计和情感分析,就能够得出一种总体的舆论倾向。这种结果也许是一种群体的意见表达,这比几千份问卷的结果更有代表性,并且我们往往无法揣测个体在填问卷时的心理状态,因此问卷调查的准确性也存在疑问,而这种微博数据往往是用户自发产生,心理动机明确,我们研究者也许能更好的估计这种情况下因为心理状态不同产生的误差。

总体来说,使用爬虫是获取舆论数据的好方法,我使用如下技术来写了这个爬虫。

  • 构造请求:requests
  • 模拟登陆:Selenium + Phantomjs和ChromeDriver
  • 数据过滤:BeautifulSoup + 正则表达式
  • 数据存储:mongodb
  • 复用爬虫:自己写了一个task模块驱动爬虫持续运行,采用子线程来运行页面爬取,主线程则持续发放爬取任务

模拟登陆新浪微博

新浪微博和我之前爬取的网站都不同,这是个商业网站,有加密的登录过程,并且搜索和访问用户个人资料都需要在登录状态下进行。因此爬虫需要模拟成一个用户来操作,至少需要模拟一个用户来进行登录。

这下确实难到我了,我尝试解析了新浪微博的登录过程,却怎么也看不出头绪来,无奈只好看看github上有没有人写过类似的项目,果然,有人已经开发出了一个分布式的新浪微博爬虫(ResolveWang),这个项目使用了Celery做任务管理,redis做各个分机之间信号交流,并且他还提供了多个模拟登陆的方法。通过他的工作我才发现,新浪微博还存在一个预登陆的过程,这个过程由于在浏览器时是一闪而过,不好注意到它的URL,在预登陆时还采取了加密的方。这个项目的作者用巨大的耐心使用Fiddle4对登陆的数据进行抓包,并且逐个观察数据包中变化的参数,发现了预登陆时需要自行构造传入服务器的参数,并且还找出了一些参数的加密解密方法,为了模拟这种过程,他用javascript写了加密解密函数,成功通过构造加密URL的方式完成了登录。说实话,他写加密函数的部分我没太看懂,因此觉得应该采用别的方法。好在作者还提供了另外一种解决方案,那就是最直接暴力的Selenium+Phantomjs的方案,这种方案简单直白,一步到位,最适合我这种小白用户。

关于Selenium

Selenium其实是一个自动化测试工具集,这个工具的初衷是为了自动化的测试一些网页,好把程序员从测试这一枯燥的工作中解放出来,它可以模拟浏览时点击、拖动、切换窗口、输入的操作,因此现在也越来越多的用到爬虫中。

关于Phantomjs

Phantomjs是一个用户解析和渲染js的解析器,可以用来解析目前大多数网页中的js内容,因此可以拿它当做一个“无用户界面”的浏览器来使用,用它来加载要预登陆的网页。

有了这两大神器,事情就简单了很多,Selenium管Phantomjs这类解析器叫Webdriver,它支持多种IE、Chrome、FireFox、Phamtomjs多种解析器,因此现在要做的就是用Selenium控制一个Webdriver来进行用户登录名、密码的输入操作,在登录完成后再搞到登录的cookies,就大功告成了!

用requests库构造请求

由于我实际要利用新浪微博的搜索功能来构造一个含有搜索关键词的URL来获取搜索结果,因此我使用requests库来构造请求,构造URL比较简单,直接通过浏览器多尝试几次就可以弄清如何构造一个URL,一个我需要的URL是这样的:

http://s.weibo.com/weibo/%25E5%25A4%25A7%25E7%2586%258A%25E7%258C%25AB&scope=ori&suball=1&timescope=custom:2015-8-01:2015-8-31&page=15

搜索关键词已经转化成了引用的方式,而参数都是用&连接的。

关键的问题在与登录状态的问题,如果直接使用requests的Session对象,那么不会传回我想要的搜索结果:

from requests import Session

s = Session()
result = s.get(url='some url')
page = result.text

因为要使用微博的搜索功能,必须是登录状态,因此,需要搞清楚在登录中使用了哪些cookies,只要搞清楚这一点,可以在使用Session.get方法时传入cookies就能够以登录状态进行搜索了。

为了或得新浪微博登录时的cookies,我逐个测试了webdriver在登录页面时返回的所有cookies,最终发现了一个名为'SUB'的cookies是问题的关键,只要这个cookies存在,就能以登录的状态进行操作。

Session.get方法可以传入一个以字典形式构造的cookies,因此这个请求过程改成这样:

cookie = {'SUB':COOKIES}
s = Session()
result = s.get(url='some url', 
               cookies=cookie,
               headers=header)
page = result.text

这样,获取的页面就是返回的搜索结果,同样,在获取用户个人资料页面时也使用这个cookies也可获得正确的页面。

用BeautifulSoup过滤网页

不得不说,新浪微博似乎做足了反爬虫措施,其实通过requests获得的网页是没有经过js渲染的网页,因此必然会缺少许多内容,而最要命的是,我想要的获得的微博发言、用户评论、用户个人信息都是写在一个动态加载的json里的,我看了看网页源代码,发现微博的工程师专门写了一个用于解析页面内容的函数,每次加载网页,浏览器会使用这个函数定义的规则来解析json,最后才渲染成HTML代码呈现在网页中。

不过好在ResolveWang提供了一种解决思路,先用正则表达式获取json中的内容,再用BeautifulSoup解析json中的HTML代码,这样就能获取想要的内容了

写正则表达式是个辛苦活,我只好死磕乱写,总算是写出了符合自己需求的过滤表达式,随后的soup工作就简单了多。这里也不表。

解析后的内容以列表的形式存储,每条微博都是列表中的一个元素,而元素以字典的形式组织,刚好符合mongodb的bson存储格式。

用mongodb存储内容

这是第二次用mongodb,这个数据库很简单,部署也方便,只需要:

mongod -dbpath YOUR DATABASE PATH

一句就可以完成一个本地数据库的部署,随后的操作都在Python中调用Pymongo的API完成数据的读写操作

复用爬虫

我以前使用过celery来制作定时爬虫,其实celery更为强大的是作为分布式任务发起器,这次我没有使用celery,而是直接采用同时多开进程的办法来爬取。

在爬取时,最为常见的问题是网络连接不稳定导致requests.Session返回不了结果和过于频繁的爬取导致新浪微博封禁的问题,一开始,我没有采用单独用一个线程来执行获取页面的操作,导致一旦某个操作出现以上的两个问题,我的爬虫不是阻塞就是抛出终止异常,我不得不一直在电脑前,盯着屏幕手动重启爬虫进程,这十分枯燥,也很低效。

直到我使用了threading模块中的threading对象后,我的爬虫才开始了真正意义上的自动爬取。

关于threading模块

threading模块是thread模块的一种更上层的包装,主要使用的是thread对象来对线程进行操作。要创建一个threading对象,基本用一下的模式来进行:

import threading

class workThread(threading.Thread):
    # 继承自thread对象
    def __init__(self):
        threading.Thread.__init__(self)
        self.result = None
        self.stop = False

    def run(self):
        # 在run中放入线程要做的活
        if self.stop = False:
            self.result = some_function()

    def stop(self):
        # stop方法用于终止线程
        if self.isAlive():
            self.stop = True

这样就创建了一个继承自的threading.thread的子类,这个子类通过自有的start()方法运行一个进程,通过stop方法来终止一个进程,具体使用是这样的:

thread = workThread()
thread.start()
thread.join(timeout) # 让线程等待一段时间
if thread.isAlive(): # 如果一段时间内没完成则终止线程
    thread.stop()
    raise TimeoutError
else:
    return thread.result

于是,通过使用线程,我写了一个限时函数,这个函数能以一个工作函数作为参数传入,并创建它的线程让它运行。同时对它的运行时间进行计数,超过一定时间后则判断工作函数发生阻塞,此时终止这个函数并开始下一次重试。这样我就能解决大多数的阻塞问题了。

爬虫的结构

经过一段时间的代码工作,我把爬虫主要要做的分在了几个模块里,这个爬虫的模块结果主要如下:

  • sina_scrap_for_gep
    • basic.py:处理账户、判断页面状态
    • cookies.py:处理cookies的获取和存储
    • database.py:数据库操作
    • login.py:用于模拟登陆
    • fetch_page.py:用于获取页面
    • html_screen.py:用于从页面中获取微博数据
    • logger.py:开发中的任务日志模块
    • task.py:任务发起模块
    • test.cookies.py:测试cookies
    • test.person.py:测试Person类

运行效果

我在写爬虫时没有考虑到各个模块之间的消息传递问题,只是把任务消息输出的屏幕上,目前正在开发一个日志模块用来写入消息日志。不过爬虫运行还算稳定,当前运行的效果是这样的。

###获取的博主id2428831331, 这是第10个微博
此次抓取需要登录
获取rogerCOOKIES...
已连接Mongodb!
获取的cookies的时间是2017-07-28 09:20:58, 拥有者为roger
开始获取页面...
抓取http://weibo.com/p/1005052428831331/info?mod=pedit_more页面成功
###等待2...###
###选择towa的账户进行登录###
###获取的博主id5541652987, 这是第11个微博
此次抓取需要登录
获取towaCOOKIES...
已连接Mongodb!
获取的cookies的时间是2017-07-28 09:22:48, 拥有者为towa
开始获取页面...
抓取http://weibo.com/p/1005055541652987/info?mod=pedit_more页面成功
已连接Mongodb, collection:weibo
###一共有11条微博将插入数据库###
已插入微博数据到数据库,集合:weibo,插入时间为:2017-07-28 12:11:55
已插入微博数据到数据库,集合:weibo,插入时间为:2017-07-28 12:11:55
已插入微博数据到数据库,集合:weibo,插入时间为:2017-07-28 12:11:55
已插入微博数据到数据库,集合:weibo,插入时间为:2017-07-28 12:11:55
已插入微博数据到数据库,集合:weibo,插入时间为:2017-07-28 12:11:55
已插入微博数据到数据库,集合:weibo,插入时间为:2017-07-28 12:11:55
出现编码问题,跳过插入此条微博到数据库
出现编码问题,跳过插入此条微博到数据库
已插入微博数据到数据库,集合:weibo,插入时间为:2017-07-28 12:11:55
已插入微博数据到数据库,集合:weibo,插入时间为:2017-07-28 12:11:55
已插入微博数据到数据库,集合:weibo,插入时间为:2017-07-28 12:11:55
###插入操作,成功:9,失败:2###
##############开始获取:第15页搜索结果##############
###等待6...###
此次抓取需要登录
获取rogerCOOKIES...
已连接Mongodb!
获取的cookies的时间是2017-07-28 09:20:58, 拥有者为roger
开始获取页面...

总结

目前我的工作才刚刚开始,爬取的数据中很粗糙,需要进一步重整和清洗,后期需要使用自然语言处理的方法来分析中文文本,也许还要开始学习TensorFlow之类的机器学习技术,这是万里长征的第一步。