最近参加了招商银行总行的fintench精英技术训练营的选拔赛,在通过笔试后,进入了课题研究的环节。因为前段时间学习了一段时间Python,所以选择了《基于微博爬虫的舆情分析》这一课题。该课题的具体要求如下:
课题背景: 请设计微博爬虫,获取微博上最近N天(N<=10)内与招商银行相关的热点新闻与用户意见,代码可根据输入的天数返回最新的微博信息。在已收集的数据中对提及的重点内容(招行相关产品、服务和重点事件等)进行抽取并进行一定程度上的情绪判定。最终以浅显易懂的方式呈现该段时间范围内微博上与招行相关的舆情信息,其中呈现的具体内容和方式可由考生自行设计。
请提交解决方案:课题需将爬虫构建相关PPT和代码作为研究的最终成果,并在PPT中简要展示以下几点内容:
(1)构建爬虫和舆情分析的主要流程与模块;
(2)项目过程中使用到的工具,遇到的困难和问题,以及解决的方式;
(3)简要评价爬虫的效率与性能;
(4)最终基于微博信息进行舆情分析的结果展示;
本课题主要对新浪微博进行爬虫,但是遗憾的是新浪微博并没有提供以“关键字+时间+区域”方式获取官方API。但是庆幸的是,新浪提供了高级搜索功能。
点击搜索微博后,我们看地址栏:
http://s.weibo.com/weibo/%E6%8B%9B%E5%95%86%E9%93%B6%E8%A1%8C&typeall=1&suball=1×cope=custom:2017-05-02:2017-05-02&Refer=g
解析如下固定地址部分: http://s.weibo.com/wb/
关键字(2次URLEncode编码): %E6%8B%9B%E5%95%86%E9%93%B6%E8%A1%8C
搜索时间范围: timescope=custom:2017-05-02:2017-05-02
可忽略项: Refer=g
某次请求的页数(未出现): page=1(某页请求页数)
既然是这么回事,我们接下来就可以使用网页爬虫的方式获取“关键字+时间”的微博了……
本课题所采用的爬虫语言是Python。在对新浪微博进行爬虫之前,首先需要模拟登陆,这里所采用的办法是:使用rsa加密模块进行模拟登陆。接下来要构造URL,爬取网页,然后解析网页中的微博信息,如图所示。
另外,高级搜索最多返回 50 页微博。时间范围( timescope )可设置为 1 天,如 2017-05-02:201-05-02 。 爬虫代码如下: 爬虫新浪微博需要先模拟登陆,这里采用rsa加密模块进行模拟登陆。 #coding=utf8 import urllib import urllib2 import cookielib import base64 import re import json import hashlib import rsa import binascii cj = cookielib.LWPCookieJar() cookie_support = urllib2.HTTPCookieProcessor(cj) opener = urllib2.build_opener(cookie_support, urllib2.HTTPHandler) urllib2.install_opener(opener) postdata = { 'entry': 'weibo', 'gateway': '1', 'from': '', 'savestate': '7', 'userticket': '1', 'ssosimplelogin': '1', 'vsnf': '1', 'vsnval': '', 'su': '', 'service': 'miniblog', 'servertime': '', 'nonce': '', 'pwencode': 'rsa2', #加密算法 'sp': '', 'encoding': 'UTF-8', 'prelt': '401', 'rsakv': '', 'url': 'http://weibo.com/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack', 'returntype': 'META' } class WeiboLogin: def __init__(self, username, password): self.username = username self.password = password def __get_spwd(self): rsaPublickey = int(self.pubkey, 16) key = rsa.PublicKey(rsaPublickey, 65537) #创建公钥 message = self.servertime + '\t' + self.nonce + '\n' + self.password #拼接明文js加密文件中得到 passwd = rsa.encrypt(message, key) #加密 passwd = binascii.b2a_hex(passwd) #将加密信息转换为16进制。 return passwd def __get_suser(self): username_ = urllib.quote(self.username) username = base64.encodestring(username_)[:-1] return username def __prelogin(self): prelogin_url = 'http://login.sina.com.cn/sso/prelogin.php?entry=sso&callback=sinaSSOController.preloginCallBack&su=%s&rsakt=mod&client=ssologin.js(v1.4.4)' % self.username response = urllib2.urlopen(prelogin_url) p = re.compile(r'\((.*?)\)') strurl = p.search(response.read()).group(1) dic = dict(eval(strurl)) #json格式的response self.pubkey = str(dic.get('pubkey')) self.servertime = str(dic.get('servertime')) self.nonce = str(dic.get('nonce')) self.rsakv = str(dic.get('rsakv')) def login(self): url = 'http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.18)' try: self.__prelogin() #预登录 except: print 'Prelogin Error' return global postdata postdata['servertime'] = self.servertime postdata['nonce'] = self.nonce postdata['su'] = self.__get_suser() postdata['sp'] = self.__get_spwd() postdata['rsakv'] = self.rsakv postdata = urllib.urlencode(postdata) headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:37.0) Gecko/20100101 Firefox/37.0'} req = urllib2.Request( url = url, data = postdata, headers = headers ) result = urllib2.urlopen(req) text = result.read() p = re.compile('location\.replace\(\'(.*?)\'\)') try: login_url = p.search(text).group(1) urllib2.urlopen(login_url) print "Login Succeed!" except: print 'Login Error!' 爬虫模块代码,注意这里把爬虫得到的代码写入了EXCEL中,保存。 # coding: utf-8 ''' 以关键词收集新浪微博 ''' import wx import sys import urllib import urllib2 import re import json import hashlib import os import time from datetime import datetime from datetime import timedelta import random from lxml import etree import logging import xlwt import xlrd from xlutils.copy import copy from datetime import datetime class CollectData(): """数据收集类 利用微博高级搜索功能,按关键字搜集一定时间范围内的微博。 """ def __init__(self, keyword, startTime, interval='50', flag=True, begin_url_per = "http://s.weibo.com/weibo/"): self.begin_url_per = begin_url_per #设置固定地址部分,默认为"http://s.weibo.com/weibo/" self.setKeyword(keyword) #设置关键字 self.setStartTimescope(startTime) #设置搜索的开始时间 #self.setRegion(region) #设置搜索区域 self.setInterval(interval) #设置邻近网页请求之间的基础时间间隔(注意:过于频繁会被认为是机器人) self.setFlag(flag) #设置 self.logger = logging.getLogger('main.CollectData') #初始化日志 ##设置关键字 ##关键字需解码 def setKeyword(self, keyword): self.keyword = keyword.decode('GBK').encode("utf-8") #先将其GBK解码,然后再UTF-8编码,然后再输出: print 'twice encode:',self.getKeyWord() ##设置起始范围,间隔为1天 ##格式为:yyyy-mm-dd def setStartTimescope(self, startTime): if not (startTime == '-'): self.timescope = startTime + ":" + startTime else: self.timescope = '-' ##设置搜索地区 #def setRegion(self, region): # self.region = region ##设置邻近网页请求之间的基础时间间隔 def setInterval(self, interval): self.interval = int(interval) ##设置是否被认为机器人的标志。若为False,需要进入页面,手动输入验证码 def setFlag(self, flag): self.flag = flag ##构建URL def getURL(self): return self.begin_url_per+self.getKeyWord()+"&typeall=1&suball=1×cope=custom:"+self.timescope+"&page=" ##固定地址+关键字二次UTF-8编码+ ##http://s.weibo.com/weibo/%E8%83%96%E7%BA%B8%E5%92%8C%E7%98%A6%E7%BA%B8%E7 ##%9A%84%E5%8C%BA%E5%88%AB&typeall=1&suball=1×cope=custom:2017-05-01-0:2017-05-02-0&Refer=g ##关键字需要进行两次urlencode def getKeyWord(self): once = urllib.urlencode({"kw":self.keyword})[3:] #首先把中文字符转换为十六进制,然后在每个字符前面加一个标识符%。 return urllib.urlencode({"kw":once})[3:] ##爬取一次请求中的所有网页,最多返回50页 def download(self, url, maxTryNum=4): hasMore = True #某次请求可能少于50页,设置标记,判断是否还有下一页 isCaught = False #某次请求被认为是机器人,设置标记,判断是否被抓住。抓住后,需要复制log中的文件,进入页面,输入验证码 name_filter = set([]) #过滤重复的微博ID set 一个无序不重复元素集 i = 1 #记录本次请求所返回的页数 while hasMore and i < 51 and (not isCaught): #最多返回50页,对每页进行解析,并写入结果文件 source_url = url + str(i) #构建某页的URL 在原来的基础上加上page后面的页码 data = '' #存储该页的网页数据 goon = True #网络中断标记 ##网络不好的情况,试着尝试请求三次 for tryNum in range(maxTryNum): ##0-3 try: html = urllib2.urlopen(source_url, timeout=12) data = html.read() break except: if tryNum < (maxTryNum-1): time.sleep(10) else: print 'Internet Connect Error!' self.logger.error('Internet Connect Error!') self.logger.info('url: ' + source_url) self.logger.info('fileNum: ' + str(fileNum)) self.logger.info('page: ' + str(i)) self.flag = False goon = False break if goon: lines = data.splitlines() ##按照行分隔,返回一个包含各行作为元素的列表 isCaught = True for line in lines: ## 判断是否有微博内容,出现这一行,则说明没有被认为是机器人 if line.startswith('<script>STK && STK.pageletM && STK.pageletM.view({"pid":"pl_weibo_direct"'): ##判断字符串以 开头,此处是微博页面代码 isCaught = False n = line.find('html":"') ##返回html在该行的索引值 if n > 0: j = line[n + 7: -11].encode("utf-8").decode('unicode_escape').encode("utf-8").replace("\\", "") ## 没有更多结果页面 if (j.find('<div class="search_noresult">') > 0): hasMore = False ## 有结果的页面 else: #此处j要decode! page = etree.HTML(j.decode('utf-8')) ps1 = page.xpath("//p[@node-type='feed_list_content']") #使用xpath解析得到微博内容 as2 = page.xpath("//a[@class='W_texta W_fb']") #使用xpath解析得到博主地址 ai = 0 #获取昵称和微博内容 for p1 in ps1: name = p1.attrib.get('nick-name') txt = p1.xpath('string(.)') addr1 = as2[ai].attrib.get('href') u = addr1.find('u/') addr = '' if u > 0: addr = addr1.replace("u/","p/100505") + '/info?mod=pedit_more' else: addr = addr1 + '/info?mod=pedit_more' #获得博主个人信息地址 locate = '' sex = '' edu = '' ai += 1 data2 = '' #存储该页的网页数据 html2 = urllib2.urlopen(addr, timeout=12) data2 = html2.read() lines2 = data2.splitlines() for line2 in lines2: if line2.startswith('<script>FM.view({"ns":"","domid":"Pl_Official_PersonalInfo__59","css":["style/css/module/pagecard/PCD_text_b.css?version=97033aed3c17bc3f"]'): n2 = line2.find('html":"') if n2 > 0: j2 = line2[n2 + 7: -12].replace("\\", "") page2 = etree.HTML(j2.decode('utf-8')) infotype1 = u'所在地:' infotype2 = u'性别:' infotype3 = u'小学:' infotype4 = u'初中:' infotype5 = u'高中:' infotype6 = u'大学:' infotype = '' info = '' ps2 = page2.xpath("//span[@class = 'pt_title S_txt2']") info = page2.xpath("//span[@class = 'pt_detail']") infoIndex = 0 for p2 in ps2: infotype = p2.xpath('string(.)') if (infotype == infotype1): locate = info[infoIndex].xpath('string(.)') if (infotype == infotype2): sex = info[infoIndex].xpath('string(.)') if (infotype == infotype3 or infotype == infotype4 or infotype == infotype5 or infotype == infotype6): edu = infotype.replace(":","") break infoIndex += 1 if(name != 'None' and str(txt) != 'None' and name not in name_filter): name_filter.add(name) oldWb = xlrd.open_workbook('weiboData.xls', formatting_info=True) oldWs = oldWb.sheet_by_index(0) rows = int(oldWs.cell(0,0).value) newWb = copy(oldWb) newWs = newWb.get_sheet(0) newWs.write(rows, 0, str(rows)) newWs.write(rows, 1, name) newWs.write(rows, 2, locate) newWs.write(rows, 3, sex) newWs.write(rows, 4, edu) newWs.write(rows, 5, self.timescope) newWs.write(rows, 6, addr1) newWs.write(rows, 7, txt) newWs.write(0, 0, str(rows+1)) newWb.save('weiboData.xls') print "save with same name ok" break lines = None ## 处理被认为是机器人的情况 if isCaught: print 'Be Caught!' self.logger.error('Be Caught Error!') self.logger.info('url: ' + source_url) self.logger.info('fileNum: ' + str(fileNum)) self.logger.info('page:' + str(i)) data = None self.flag = False break ## 没有更多结果,结束该次请求,跳到下一个请求 if not hasMore: print 'No More Results!' if i == 1: time.sleep(random.randint(3,8)) else: time.sleep(10) data = None break i += 1 ## 设置两个邻近URL请求之间的随机休眠时间,防止Be Caught。目前没有模拟登陆 sleeptime_one = random.randint(self.interval-25,self.interval-15) sleeptime_two = random.randint(self.interval-15,self.interval) if i%2 == 0: sleeptime = sleeptime_two else: sleeptime = sleeptime_one print 'sleeping ' + str(sleeptime) + ' seconds...' time.sleep(sleeptime) else: break #content.close() #content = None ##改变搜索的时间范围,有利于获取最多的数据 def getTimescope(self, perTimescope): if not (perTimescope=='-'): times_list = perTimescope.split(':') start_date = datetime(int(times_list[-1][0:4]), int(times_list[-1][5:7]), int(times_list[-1][8:10]) ) #start_date = datetime.date.fromtimestamp(time.mktime(time.strptime(times_list[-1],"%Y-%m-%d"))) start_new_date = start_date - timedelta(days = 1) ##从当前时间开始减1天 start_str = start_new_date.strftime("%Y-%m-%d") ##接收以时间元组,并返回以可读字符串表示的当地时间 return start_str + ":" + start_str else: return '-' def main(): logger = logging.getLogger('main') #获得日志系统的 对象,即创建一个logger logFile = './collect.log' logger.setLevel(logging.DEBUG) #设置日志级别 NOTSET < DEBUG < INFO < WARNING < ERROR < CRITICAL filehandler = logging.FileHandler(logFile) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s') filehandler.setFormatter(formatter) logger.addHandler(filehandler) while True: ## 接受键盘输入 keyword = raw_input('Enter the keyword(type \'quit\' to exit ):') if keyword == 'quit': sys.exit() startTime = raw_input('Enter the start time(Format:YYYY-mm-dd):') Ndays = int(raw_input('Enter the recent N days messages:')) #region = raw_input('Enter the region([BJ]11:1000,[SH]31:1000,[GZ]44:1,[CD]51:1):') #interval = raw_input('Enter the time interval( >30 and deafult:50):') mmm = 0 #计数天数 ##实例化收集类,收集指定关键字和起始时间的微博 cd = CollectData(keyword, startTime) while cd.flag: if mmm == Ndays: print "Have finished all messages!" sys.exit() print cd.timescope logger.info(cd.timescope) ##打印日志信息 url = cd.getURL() ##获取URL符合新浪高级搜索的URL结构 cd.download(url) cd.timescope = cd.getTimescope(cd.timescope) #改变搜索的时间,到下一天 mmm += 1 else: cd = None print '-----------------------------------------------------' print '-----------------------------------------------------' else: logger.removeHandler(filehandler) logger = None ##if __name__ == '__main__': ## main() 爬虫模块主程序 # -*- coding: utf-8 -*- import login import collectWeiboDataByKeyword import urllib import urllib2 import sys uid = 'XXXXXXX' ###这里写你自己的微博账号 psw = 'XXXXXXX' ###这里写你自己的微博密码 reload(sys) sys.setdefaultencoding('utf8') #这两句话用来修改系统默认编码 simLogin = login.WeiboLogin(uid, psw) simLogin.login() collectWeiboDataByKeyword.main();
爬虫结果:
运行mymain.py文件后,按照提示依次输入:关键词、爬虫起始时间、最近N天信息的N,如下:
爬虫获得的微博信息写入weiboData.xls文件中,一共获得691条微博:
舆情分析请看下一章。 我的全部代码: https://github.com/Yuzhen-Li/Analysis-of-Public-Opinion-Based-on-Microblogging-Reptile 参考
1.https://sanwen8.cn/p/415Cgz9.html
2.http://dataunion.org/24057.html
3.http://blog.csdn.net/amyque/article/details/50933143
4.http://blog.csdn.net/gatieme/article/details/43235791
5.https://www.qcloud.com/document/product/271/2072
6.https://www.qcloud.com/document/developer-resource/494/7244
7.https://www.anotherhome.net/2920
8.http://m.blog.csdn.net/article/details?id=38149451