微博话题爬取与存储分析

一步步教你微博话题数据爬取与分析,以上海租房为例

Posted by T.L on October 29, 2016

大数据社会下数据就是黄金,新浪微博作为一个国内网络社交早就意识到这一点,本着资本家和商人的心态给你提供的开放API接口只可以获得少量无关紧要的数据(想要数据,money来换),对比国外Twitte等社交平台会提供一些数据接口供研究人员获取大量研究数据。那我们GEEK的口号是,凡是网上能显示数据的朕兼“可取”(v_v…为什么加个引号呢,因为虽然出于技术角度是都可取得,但出于道德方面考虑也要尊重数据作者的规约)。

本文基于python以新浪微博为数据平台,从数据采集、关键字提取、数据存储三个角度,用最简单的策略来挖掘我们的“黄金”。

有爬虫基础的人可以直接跳过数据采集部分看“上海租房”话题挖掘实战项目,项目地址https://github.com/luzhijun/weiboSA(目前已更新豆瓣小组爬取)。

数据采集

使用python是因为代码简洁,虽然计算比java和c慢很多,但数据采集时间开销大部分是IO部分的,你愿意每次用java或者c写效率也提高不到哪去。

数据采集基本用爬虫机器人,原理谁都会,google就是靠他发家致富走上人生巅峰的。下面介绍常用来做爬虫的几个库。

Urllib

怎样抓网页呢?其实就是根据URL来获取它的网页信息,虽然我们在浏览器中看到的是一幅幅优美的画面,但是其实是由浏览器解释才呈现出来的,实质它是一段HTML代码,加 JS、CSS,如果把网页比作一个人,那么HTML便是他的骨架,JS便是他的肌肉,CSS便是它的衣服。所以最重要的部分是存在于HTML中的,下面我们就写个例子来扒一个网页下来。

1
2
3
import urllib2
response = urllib2.urlopen("http://www.baidu.com")
print response.read()

结果就和在Chrome等浏览器中右键查看源码一样的内容,urllib2是python内置库,简化了httplib的用法(urllib2.urlopen相当于Java中的HttpURLConnection)。有2那肯定有urllib啊,urllib2可以接受一个Request类的实例来设置URL请求的headers,但urllib仅可以接受URL。这意味着,你不可以伪装你的User Agent字符串等。urllib2在python3.x中被改为urllib.request。 接下来用urllib2伪装iphone 6浏览,模拟浏览器发送GET请求。

1
2
3
4
5
req = request.Request('http://www.douban.com/')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
with request.urlopen(req) as f:
    print('Status:', f.status, f.reason)
    print('Data:', f.read().decode('utf-8'))

结果会返回移动版的源码信息

1
2
3
4
...
<link rel="apple-touch-icon-precomposed" href="https://gss0.bdstatic.com/5bd1bjqh_Q23odCf/static/wiseindex/img/screen_icon.png"/>  
<meta name="format-detection" content="telephone=no"/>
...

如果想要以post方式提交,只要在Request中附加data字段就可以,下面附加用户名密码登录新浪博客。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#我们模拟一个微博登录,先读取登录的邮箱和口令,然后按照weibo.cn的登录页的格式以username=xxx&password=xxx的编码传入:
from urllib import parse
print('Login to weibo.cn...')
email = input('Email: ')
passwd = input('Password: ')
login_data = parse.urlencode([
    ('username', email),
    ('password', passwd),
    ('entry', 'weibo'),
    ('client_id', ''),
    ('savestate', '1'),
    ('ec', ''),
    ('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')
])

req = request.Request('https://passport.weibo.cn/sso/login')
req.add_header('Origin', 'https://passport.weibo.cn')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
req.add_header('Referer', 'https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http%3A%2F%2Fm.weibo.cn%2F')

with request.urlopen(req, data=login_data.encode('utf-8')) as f:
    print('Status:', f.status, f.reason)
    for k, v in f.getheaders():
        print('%s: %s' % (k, v))
    print('Data:', f.read().decode('utf-8'))

其中Origin和referer字段是反“反盗链”,就是检查你发送请求的header里面,referer站点是不是他自己。

Cookielib

爬虫被封的一个依据就是重复IP,因此可以为爬虫设置不同代理IP。此外有些网站需要cookie才能查看,所谓Cookie,指某些网站为了辨别用户身份、进行session跟踪而储存在用户本地终端上的数据(通常经过加密)。比如说有些网站需要登录后才能访问某个页面,在登录之前,你想抓取某个页面内容是不允许的。那么我们可以利用Urllib2库保存我们登录的Cookie,然后再抓取其他页面就达到目的了。

cookielib模块的主要作用是提供可存储cookie的对象,以便于与urllib2模块配合使用来访问Internet资源。Cookielib模块非常强大,我们可以利用本模块的CookieJar类的对象来捕获cookie并在后续连接请求时重新发送,比如可以实现模拟登录功能。该模块主要的对象有CookieJar、FileCookieJar、MozillaCookieJar、LWPCookieJar。

它们的关系:CookieJar–派生->FileCookieJar –派生–>MozillaCookieJar和LWPCookieJar

1
2
3
4
5
6
7
8
9
from urllib import request
from http.cookiejar import CookieJar

cookie=CookieJar()
cookie_support= request.HTTPCookieProcessor(cookie)#cookie处理器
opener = request.build_opener(cookie_support)
opener.open('http://www.baidu.com')
for item in cookie:
    print(item.name,':',item.value)

结果:

BAIDUID : E4DECD4AF63915B9AFF5AC28951A3DAA:FG=1
BIDUPSID : E4DECD4AF63915B9AFF5AC28951A3DAA
H_PS_PSSID : 1437_18241_17944_21079_18559_21454_21406_21377_21191_21321
PSTM : 1477631558
BDSVRTM : 0
BD_HOME : 0

这里使用默认的CookieJar 对象,如果要将Cookie保存起来,可以使用FileCookieJar类和其子类中的save方法,加载就用load方法。

写脚本从指定网站抓取数据的时候,免不了会被网站屏蔽IP。所以呢,就需要有一些IP代理。随便在网上找了一个提供免费IP的网站西刺做IP抓取。观察可以发现有我们需要的信息的页面url有下面的规律:www.xicidaili.com/nn/+页码。可是你如果直接通过get方法访问的话你会发现会出现500错误。原因其实出在这个规律下的url虽然都是get方法获得数据,但都有cookie认证,另外还有反外链等,下面例子用来获得西刺的cookie。

1
2
3
4
5
6
7
8
9
10
headers=[('User-Agent','Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25'),
    ('Host','www.xicidaili.com'),
    ('Referer','http://www.xicidaili.com/n')]
def getCookie()
    cookie=CookieJar()
    cookie_support= request.HTTPCookieProcessor(cookie)#cookie处理器
    opener = request.build_opener(cookie_support)
    opener.addheaders=headers
    opener.open('http://www.xicidaili.com/')
    return cookie

有了cookie就可以爬了,爬的内容怎么处理呢,介绍个SB工具—— BeautifulSoup。

BeautifulSoup

BeautifulSoup翻译叫鸡汤,现在版本是4.5.1,简称BS4,倒过来叫4SB,不过抓数据一点都不SB。提供一些简单的、python式的函数用来处理导航、搜索、修改分析树等功能。它是一个工具箱,通过解析文档为用户提供需要抓取的数据,因为简单,所以不需要多少代码就可以写出一个完整的应用程序。Beautiful Soup自动将输入文档转换为Unicode编码,输出文档转换为utf-8编码。你不需要考虑编码方式,除非文档没有指定一个编码方式,这时,Beautiful Soup就不能自动识别编码方式了。然后,你仅仅需要说明一下原始编码方式就可以了。 关于BS的介绍和用法官方文档很详细,下面给几个”Web scraping with python”1中的例子看下BS是否好喝,可以和文档对照看。 首先你得安装了BS,然后爬取http://www.pythonscraping.com/pages/page3.html中的图片来小试牛刀。

1
2
3
4
5
6
7
8
9
import re
from urllib import request
from bs4 import BeautifulSoup

html=request.urlopen("http://www.pythonscraping.com/pages/page3.html")
bs=BeautifulSoup(html,"lxml")
#打印所有图片地址
for pic in bs.find_all('img',{'src':re.compile(".*\.jpg$")}):
    print(pic['src'])

结果:

../img/gifts/logo.jpg
../img/gifts/img1.jpg
../img/gifts/img2.jpg
../img/gifts/img3.jpg
../img/gifts/img4.jpg
../img/gifts/img6.jpg

接上文,我们把西刺的高匿代理ip爬出来放到本地proxy.txt。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cookie=getCookie()
# get the proxy
with open('proxy.txt', 'w') as f:
    for page in range(1,101):
        if page%50==0:#每50页更新下cookie
            cookie=getCookie()

        url = 'http://www.xicidaili.com/nn/%s' %page
        cookie_support= request.HTTPCookieProcessor(cookie)
        opener = request.build_opener(cookie_support)
        request.install_opener(opener)

        req = request.Request(url,headers=dict(headers))
        content = request.urlopen(req)
        soup = BeautifulSoup(content,"lxml")
        trs = soup.find('table',id="ip_list").findAll('tr')
        for tr in trs[1:]:
            tds = tr.findAll('td')
            ip = tds[1].text.strip()
            port = tds[2].text.strip()
            protocol = tds[5].text.strip().
            f.write('%s://%s:%s\n' % (protocol, ip, port))

结果十五秒爬了1万条数据(与电脑环境有关),说明1页正好100条,而总页数超过1000页,也就是记录数超过10w条,如果固定用同一个cookie肯定不安全(谁会有空翻看1000页数据。。。),因此设置每爬50页更新下cookie。 有了代理地址,不一定能保证有效,可能就被封杀了,因此使用思路是把代理地址存入哈希表,验证无效的删除(看状态码),重新在表中取新的记录。 代理地址使用方式如下:

1
2
3
4
...
proxy_handler = request.ProxyHandler({'http': '123.165.121.126:81'}) #http://www.xicidaili.com/nn/2 随便找个
opener = request.build_opener(proxy_handler,cookie_handler ...各种其他handle)
...

另外推荐个神器,crawlera ,基本满足各种需要。

假如真要爬1000页,需要花150秒?好吧,好像也不多,但我要说的是可以多进程或者异步处理。多进程很好做,注意以手动维护一个HttpConnection的池,然后每次抓取时从连接池里面选连接进行连接即可(每秒几百个连接正常的有理智的服务器一定会封禁你的)。python的异步处理用到了Twisted库,却远没有同是异步模式的nodejs火,算是python中的巨型框架了,想想python的巨型框架活的不久,感兴趣的推荐看下《Twisted网络编程必备》2。关于单线程、多线程、异步有张图推荐看下。

写爬虫还要考虑其他很多问题,授权验证、连接池、数据处理、js处理等,这里有个经典爬虫框架:Scrapy,目前支持python3,支持分布式, 使用 Twisted来处理网络通讯,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。

Scrapy与Pyspider

Scrapy的入门学习参见学习Scrapy入门,对应中文文档几小时内可以快速掌握。另外国内某大神开发了个WebUI的Pyspider,具有以下特性:

  1. python 脚本控制,可以用任何你喜欢的html解析包(内置 pyquery)
  2. WEB 界面编写调试脚本,起停脚本,监控执行状态,查看活动历史,获取结果产出
  3. 支持 MySQL, MongoDB, SQLite
  4. 支持抓取 JavaScript 的页面
  5. 组件可替换,支持单机/分布式部署,支持 Docker 部署
  6. 强大的调度控制

从内容上讲,两者具有功能差不多,包括以上3,5,6。不同是Scrapy原生不支持js渲染,需要单独下载scrapy-splash,而PyScrapy内置支持scrapyjs;PySpider内置 pyquery选择器,Scrapy有XPath和CSS选择器,这两个大家可能更熟一点;此外,Scrapy全部命令行操作,Pyscrapy有较好的WebUI;还有,scrapy对千万级URL去重支持很好,采用布隆过滤来做,而Spider用的是数据库来去重?最后,PySpider更加容易调试,scrapy默认的debug模式信息量太大,warn模式信息量太少,由于异步框架出错后是不会停掉其他任务的,也就是出错了还会接着跑。。。从整体上来说,pyspider比scrapy简单,并且pyspider可以在线提供爬虫服务,也就是所说的SaaS,想要做个简单的爬虫推荐使用它,但自定义程度相对scrapy低,社区人数和文档都没有scrapy强,但scrapy要学习的相关知识也较多,故而完成一个爬虫的时间较长。

因为比较喜欢有完整文档的支持,所以后面主要用Scrapy,简要说下Scrapy运行流程。

  • 首先,引擎从调度器中取出一个链接(URL)用于接下来的抓取
  • 引擎把URL封装成一个请求(Request)传给下载器,下载器把资源下载下来,并封装成应答包(Response)
  • 然后,爬虫解析Response
  • 若是解析出实体(Item),则交给实体管道进行进一步的处理。
  • 若是解析出的是链接(URL),则把URL交给Scheduler等待抓取

根据scrapy文档描述,要防止scrapy被禁用,主要有以下几个策略。

  1. 动态设置user agent
  2. 禁用cookies
  3. 设置延迟下载
  4. 使用 Google cache
  5. 使用IP地址池( Tor project 、VPN和代理IP)
  6. 使用 Crawlera

由于Google cache基于你懂的原因不可用,其余都可以利用,Crawlera的分布式下载,我们可以在下次用一篇专门的文章进行讲解。下面主要从动态随机设置user agent、禁用cookies、设置延迟下载和使用代理IP这几个方式入手。

自定义中间件

Scrapy下载器通过中间件控制的,要实现代理IP、user agent切换可以自定义个中间件。 在项目下创建(如何创建项目,使用scrapy start yourProject命令,参考文档)好项目后,在里面找到setting.py文件,先把agents和代理ip放到setting.py中(代理ip较少情况下这样做,较多的话还是放到数据库中去,方便管理),设置中间件名字MyCustomSpiderMiddleware和优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
USER_AGENTS = [
	"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
	"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
	"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
	"Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
	"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
	"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
	"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
	"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
	"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
	"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
	"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
	"Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
	"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
	"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
	"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
	"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
]
PROXIES = [
	{'ip_port': '111.11.228.75:80', 'user_pass': ''},
	{'ip_port': '120.198.243.22:80', 'user_pass': ''},
	{'ip_port': '111.8.60.9:8123', 'user_pass': ''},
	{'ip_port': '101.71.27.120:80', 'user_pass': ''},
	{'ip_port': '122.96.59.104:80', 'user_pass': ''},
	{'ip_port': '122.224.249.122:8088', 'user_pass': ''},
]
# 禁用cookoe (enabled by default)
COOKIES_ENABLED = False

#设置下载延迟
DOWNLOAD_DELAY = 1

# 下载中间件
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
    'weiboZ.middlewares.MyCustomDownloaderMiddleware': 543,
}

middlewares/MyCustomDownloaderMiddleware.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import random
import base64
from settings import PROXIES
class RandomUserAgent(object):
	"""Randomly rotate user agents based on a list of predefined ones"""
	def __init__(self, agents):
		self.agents = agents
	@classmethod
	def from_crawler(cls, crawler):
		return cls(crawler.settings.getlist('USER_AGENTS'))
	def process_request(self, request, spider):
		#随机选个agent
		request.headers.setdefault('User-Agent', random.choice(self.agents))
class ProxyMiddleware(object):
	def process_request(self, request, spider):
		proxy = random.choice(PROXIES)
		if proxy['user_pass'] is not None:
			request.meta['proxy'] = "http://%s" % proxy['ip_port']
			encoded_user_pass = base64.encodestring(proxy['user_pass'])
			request.headers['Proxy-Authorization'] = 'Basic ' + encoded_user_pass
		else:
			request.meta['proxy'] = "http://%s" % proxy['ip_port']

互联网道德和规约

当你准备爬某个网站的时候,首先应该先看下该网站有没有robots.txt。robots.txt是1994年出现的,也称为机器人排除标准(Robots Exclusion Standard),网站管理员不想某些内容被爬到的时候可以再该文件中注明。robots.txt虽然有主流的语法格式,但是各大企业标准不一,没有别人可以阻止你创建自己版本的robots.txt,但这些robots.txt不应该因为不符合主流而不被遵守。一般文件字段包含:User-agent,Allow,Disallow分别代表搜索机器人允许看和不许看的内容。

之前看新闻说今年4月大众点评把百度给告了,请求法院判令两被告停止不正当竞争行为,共同赔偿汉涛公司经济损失9000万元和为制止侵权行为支出的45万余元,并刊登公告、澄清事实消除不良影响。有用百度地图的应该知道这个(最近百度高德开撕,又在黑百度了~~~),定位完毕会显示附近商家和点评信息,来看下大众点评网的robots.txt. 光看

User-agent: *

Disallow: /shop//rank_p

就知道不允许任何企业和个人爬他家的商店评分数据,更何况其他更具有价值的数据呢,数据是黄金,要求赔偿9000万元对百度来说不算多,但百度回应内容大众点评网的robots协议面向百度等搜索引擎开放,百度地图抓取大众点评网的内容正是在robots.txt允许的情况下。通常业内习惯上没有被不允许的就是允许的,也就是说网站的关键信息可以帮助SEO优化的这个不能被禁止哟,不然你就没头条了,看人家竞争对手爱帮网倒是单独被列出来全面封杀了,因为其实力太弱,没有商业合作价值。就算这样我也没看出允许百度抓点评的用户评论数据,难道说点评网之前没robots.txt?人家不傻!百度挖了人家数据还叫嚣着遵守Robots协议,(其实他完全可以偷偷摸摸抓了数据自己私下研究,却要直接在百度地图上显示出来,这是要把数据价值榨干啊,够霸道)好比把人打了顿理直气壮地说你瞅啥一样,太野蛮了。。。

说多了,来看下新浪微博的Robots协议。明确规定了Sitemap: http://weibo.com/sitemap.xml 中列出的内容不允许被百度、360、谷歌、搜狗、微软必应、好搜、神马查看,后面还注明了Disallow: User-agent: * Disallow: /,也就是说前面是单独列出的,理论上这些数据不允许任何机构和个人爬取。这些是啥数据呢,movie和music数据,那你放心好了,微博文本数据可以爬了,但人家也不傻,可以显示的微博信息是有限制的,不可能所有数据库的数据都显示出来。

实战

在58、赶集、链家上找过房子的人都为中介苦恼,所谓的行业规矩令人做呕,这些人不生产社会价值却担当了新世纪的买办角色,好在通过微博也可以找房,而且绝大部分是个人房源。

以上海找房子为例,微博搜索框输入@上海租房 就可以的到如下页面

http://s.weibo.com/weibo/%2540%25E4%25B8%258A%25E6%25B5%25B7%25E7%25A7%259F%25E6%2588%25BF?topnav=1&wvr=6&b=1

还是不错的,然后看下源码发现并没有html数据,显然是AJAX异步了,Scrapy要爬的话还得安装scrapy-splash改下配置用splash解析js内容,而且要看下一页必须登录状态才可以,那要在header里面添加cookie,可以登录后chrome F12 开发工具查看,但你敢保证拿包含自己的账号的cookie去做爬虫发现了不被封?其实这里可以显示的数据最多1000条,按最新的1000条显示,何必大费周章去搞那么复杂呢,可以用移动版的微博搜下嘛,点击

用开发者工具看下网络请求数据状况,搜索包含名字‘page’ 请求消息头,可以发现规律:

左边Name列凡是内容页下拉引起ajax加载新页,新页内容以json格式返回;右边字段末尾page=?部分,代表传递第几页的内容,?最大到100,和电脑版最多看50页一样有数据限制。
json内容如下:

ok,能显示的数据都在里面,而且还是json格式,都不用选择器了,这个要比电脑版简单多了。

数据提取(ETL)

选择需要的数据

并不是所有json字段的数据都有用,这里只选取有用的字段,总的原则是按需抽取。可以看下项目中定义的Items.py

微博内容id 对应字段放数据库中将有唯一约束,防止重复微博。选择mblogid作为唯一id,而千万不是itemid,经测试发现itemid只代表当天微博的槽位,比如限制浏览10条数据,就有1~10个槽位,而itemid就代表这10个槽位标签,并不代表微博内容id。另外mblog字段下还有个id属性,估计和mblogid一样的效果,有兴趣可以试试。
发布时间代表信息的实效,json里面有两个字段表示,一个是时间戳created_timestamp,另一个是显示出来的真实时间数据,这里取真实数据方便直接提取显示,但后期存储的时候需要统一转换为标准时间格式。
评论数、转发数、点赞数和时效结合可以用来综合评估微博信息价值(时间越靠后这三个数字越能评价信息价值)。
用户名、粉丝数、说说数可以用来检验用户是否有价值用户,或者是机器人。
后期处理需要提取求/租信息的关键词,包含价格、几号线、行政区划、信息是求租还是出租。

项目中定义的pipelines.py文件是scrapy管道处理类,也就是主要的后期数据处理类。其中一个是JsonPipeline类,直接将数据打印到json文件中,这个前期可以用来调试爬虫效果。另一个是MongoPipeline类,用来保存后期处理后的数据。在setting文件中ITEM_PIPELINES属性可以设置具体采用哪个管道处理类。

后期处理主要任务是提取关键字,如何从微博信息中爬取地理位置、价格?这里采用双数组Trie树

DAT

Trie树是搜索树的一种,来自英文单词”Retrieval”的简写,可以建立有效的数据检索组织结构,是中文匹配分词算法中词典的一种常见实现。它本质上是一个确定的有限状态自动机(DFA),每个节点代表自动机的一个状态。在词典中这此状态包括“词前缀”,“已成词”等。前面文章讲了下其原理,可以查看

采用Trie树搜索最多经过n次匹配即可完成一次查找(即最坏是0(n)),而与词库中词条的数目无关,缺点是空间空闲率高,它是中文匹配分词算法中词典的一种常见实现。

双数组Trie(doublearrayTrie,DAT)是trie树的一个简单而有效的实现(日本人发明的),由两个整数数组构成,一个是base[],另一个是check[]。双数组Trie树是Trie树的一种变形,是在保证Trie树检索速度的前提下,提高空间利用率而提出的一种数据结构.其本质是一个确定有限状态自动机(DeterministicFiniteAutomaton,DFA),每个节点代表自动机的一个状态,根据变量的不同,进行状态转移,当到达结束状态或者无法转移时完成查询.DAT采用两个线性数组(base和check)对Trie树保存,base和check数组拥有一致的下标,即DFA中的每一个状态,也即Trie树中所说的节点,base数组用于确定状态的转移,check数组用于检验转移的正确性,检验该状态是否存在34

在比较用于正向最大匹配分词的速度方面,DAT分词平均速度为936kB/s5(2006年),项目用到github上一日本人的python版的DAT,其查询速度可以达到 2.755M/s,查询速度和分词速度基本是差不多的,这三倍的差距应该是做了优化的。

词典的收集是比较麻烦,没有现成的,项目中搜集了上海地铁、街道、行政区、乡镇等信息,其中价格信息范围是从600~9000,可识别二千、二千二、两千一等中文价格,后面微博上看到有人用1.2k做价格的,暂时没加入,自己可以加入词条后重新运行下makeData.py文件即可收录。

判断信息是租房还是求房也是根据关键字,当信息中出现[“求租”, “想租”,”求到”,”求从”, “要租”, “寻租”,”寻找”, “找新房子”, “找房子”, “找房”, “寻房”, “求房”, “想找”, “希望房”]信息就标注为求房,否则标注为租房。

此外项目还收集了三千多个楼盘信息,由于有些楼盘信息容易混淆真实语境,比如‘峰会’(真不懂怎么会有这楼盘名)、‘艺品’与信息‘文艺品味’、‘黄兴’、‘金铭’与人名冲突等等。有想根据楼盘查询信息的同学可以把makeData.py中第5、51行注释取消运行下这个文件。

关于时间处理,微博挖到的时间有几种类型:

  • 2016年01月01日 00点00分
  • 1月1日 00:00
  • 今天 00:00
  • 1分钟前/11分钟前
  • 10秒前

需要统一转化,使用DataUtil类处理。其中mongodb使用的是ISO时间,比北京时间早8小时,而pymongo中的datetime.datetime 数据并不会按时区处理,因此手动减少8小时后存储。同样从mongoDB中取出的时间要转化为当地时间。

1
2
3
4
5
> d=new Date()
> d
ISODate("2016-10-29T06:59:49.461Z")
> d.toLocaleDateString()
10/29/2016

数据存储

其实就这点数据放哪个数据库都无所谓,但假如这个数据量很大,就要好好考虑数据存储了。

选择oracle、mysql 还是 nosql

数据库的比较就好比java、c#、python、Go等的骂战一样,没有最好的,只有最适合场景的。oracle、mysql都学过,nosql中学过hbase和mongodb,就我而言单从7个角度比较:

  1. 功能:oracle>mysql> nosql
  2. 写性能:noSql>oracle$\approx$mysql
  3. 简单查询: oracle>mysql>nosql
  4. 复杂查询(含join): oracle>mysql>nosql
  5. 架构扩展: noSql>mysql>oracle
  6. 可维护性: oracle>mysql>nosql
  7. 成本: oracle>mysql$\approx$nosql

对于现在这个场景,爬虫在前端爬数据,管道层在那边处理数据后写数据,而这些数据具有时效性,也就是说只会去读一部分数据,相对来说,这就对写的要求较高。此外,这个场景就一个表,不涉及多表关联、约束等,复杂查询可以说没有,需要功能较少。另外网络数据不能保证一致性和可靠性,只要高可用性(HA)即可,Nosql可以设置副本机制达到高可用性,mysql虽然也可以做到成本稍高,将来可扩展角度也不适合。因此这个场景最适合的是Nosql。

Hbase 还是Mongodb

Cassandra HBase和MongoDb性能比较此文详细比较了三种主流Nosql数据库,最终项目选择Mongodb,就在于MongoDB适合做读写分离场景中的读取场景,并且其用js开发的,对json插入支持特别好。什么时候mongodb是较坏的选择呢,参考WHY MONGODB IS A BAD CHOICE FOR STORING OUR SCRAPED DATA

python的mongodbSDK包叫pymongo,十分钟看个教程就会了,这个业务场景为了加快查询,需要对价格、行政区、发布时间创建索引,其中价格、行政区由于是数组形式所以是多键索引,索引属性是稀疏的,即不允许空值。此外对这条微博的mblog_id加个唯一索引。索引在初始运行时创建,之后除非手动删除数据库后运行,否则不会再创建。

为保证每次插入的数据都是最新的,插入前应比较数据的发布时间与数据库中的最新时间,如果是早的说明已经爬过的,不需要插入。

关于mongodb的使用文档,点这里

运行项目

将项目git到本地后,请先确保以下环境已经安装:

执行下面命令:

mongod
cd weiboSA
scrapy crawl mblogSpider

可选参数:

scrapy crawl mblogSpider -a num= -a new_url=

  • num 代表爬取页面数,默认为100页,目前只支持100页。
  • new_url 默认为搜索移动端‘上海租房’返回的json文件url,如果要添加其他上海租房信息,比如浦东租房,请自行在Chrome中找到请求的json地址,例如:
  • http://m.weibo.cn/page/pageJson?
    containerid=&containerid=100103type%3D1%26q%3D浦东租房
    &type=all
    &queryVal=浦东租房
    &luicode=10000011
    &lfid=100103type%3D%26q%3D上海无中介租房
    &title=浦东租房
    &v_p=11
    &ext=
    &fid=100103type%3D1%26q%3D浦东租房
    &uicode=10000011
    &next_cursor=
    &page=
    如果要数据库收录‘浦东租房’历史记录信息,请将pipelines.py第87、88行注释掉。一般如果有‘上海租房’了就不要去搜索‘浦东租房’,因为基本上有‘浦东租房’的微博都会有@‘上海租房’,所以下面会出现插入重复记录错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  weiboZ git:(master) ✗ scrapy crawl mblogSpider -a num=10 -a new_url="http://m.weibo.cn/page/pageJson\?containerid\=\&containerid\=100103type%3D1%26q%3D%E6%B5%A6%E4%B8%9C%E7%A7%9F%E6%88%BF\&type\=all\&queryVal\=%E6%B5%A6%E4%B8%9C%E7%A7%9F%E6%88%BF\&luicode\=10000011\&lfid\=100103type%3D%26q%3D%E4%B8%8A%E6%B5%B7%E6%97%A0%E4%B8%AD%E4%BB%8B%E7%A7%9F%E6%88%BF\&title\=%E6%B5%A6%E4%B8%9C%E7%A7%9F%E6%88%BF\&v_p\=11\&ext\=\&fid\=100103type%3D1%26q%3D%E6%B5%A6%E4%B8%9C%E7%A7%9F%E6%88%BF\&uicode\=10000011\&next_cursor\=\&page\="
2016-10-29 14:41:11 [root] WARNING: 生成MongoPipeline对象
2016-10-29 14:41:11 [root] WARNING: 开始spider
2016-10-29 14:41:11 [root] WARNING: 允许插入数据的时间大于2016-10-29 14:15:05.875000
2016-10-29 14:41:13 [root] WARNING: do page1.
2016-10-29 14:41:13 [root] WARNING: do other pages.
2016-10-29 14:41:13 [root] ERROR: 编号为:E91f233Ds的数据插入异常
2016-10-29 14:41:13 [root] ERROR: 编号为:Ef4ri5bC6的数据插入异常
2016-10-29 14:41:13 [root] ERROR: 编号为:Ef3UNqMmV的数据插入异常
2016-10-29 14:41:13 [root] ERROR: 编号为:Ef3stkA8a的数据插入异常
2016-10-29 14:41:13 [root] ERROR: 编号为:Ef3pzmJ6i的数据插入异常
2016-10-29 14:41:13 [root] ERROR: 编号为:Ef1OBtvQr的数据插入异常
2016-10-29 14:41:13 [root] ERROR: 编号为:Ef03Lj54z的数据插入异常
2016-10-29 14:41:13 [root] ERROR: 编号为:EeYLU2GQd的数据插入异常
2016-10-29 14:41:13 [root] ERROR: 编号为:EeYlBv7bn的数据插入异常
2016-10-29 14:41:13 [root] ERROR: 编号为:EeXkop2vu的数据插入异常
2016-10-29 14:41:15 [root] WARNING: 结束spider

更改日志显示级别请在setting.py中修改LOG_LEVEL,介意采用项目默认的WARNNING,否则信息会很多。

查询示例

查询当前时区的2016-10-20至今有在9号线附近租房房租不高于2000的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
db.house.find(
{
	created_at:{$gt:new Date('2016-10-20T00:00:00')},
	$or:
		[
			{price:{$lte:2000}},
			{price:[]}
		],
	admin:'9号线',
	tag:true
},
{	
	_id:0,
	text:1,
	created_at:1,
	scheme:1
}
).hint('created_at_-1').pretty()

{
	"text" : "房子在大上海国际花园,漕宝路1555弄,距9号线合川路地铁站步行5分钟,距徐家汇站只有4站,现在转租大床,有独立卫生间,公共厨房,房租2400,平摊下来1200,有一女室友,室友宜家上班,限女生,没有物业费,包网络,水电自理@上海租房无中介 @上海租房无中介 @上海租房 @上海租房无中介联盟",
	"scheme" : "http://m.weibo.cn/1641537045/EetVm3WBV?",
	"created_at" : ISODate("2016-10-25T09:18:00Z")
}
{
	"text" : "#上海租房##上海出租#9号线松江泗泾地铁站金地自在城,12层,步行、公交或小区班车直达地铁站。精装,品牌家具家电,主卧1800RMB/月;公寓门禁出入,房东直租,电话:13816835869,或QQ:36804408。@上海租房 @互助租房 @房天下上海租房 @上海租房无中介   @应届毕业生上海租房",
	"scheme" : "http://m.weibo.cn/1641537045/Een8cAoy8?",
	"created_at" : ISODate("2016-10-24T16:00:00Z")
}
{
	"text" : "#上海租房# 个人离开上海:转租地铁9号线朝南主卧带大阳台,离地铁站两分钟!设备齐全,交通方便,随时入住。具体信息看图片~@上海租房 @上海租房无中介联盟 @魔都租房 帮转谢谢!",
	"scheme" : "http://m.weibo.cn/1641537045/EdRpfuKuH?",
	"created_at" : ISODate("2016-10-21T07:14:00Z")
}
{
	"text" : "9号线桂林路 离地铁站8分钟 招女生室友哦 @上海租房 @上海租房无中介联盟 上海·南京西路",
	"scheme" : "http://m.weibo.cn/1641537045/EdJ2U8Kv3?",
	"created_at" : ISODate("2016-10-20T09:57:00Z")
}

Note

  1. python 的第三方requests库使用起来比自带的urllib更容易,是对urlib的进一步封装,读者可以自己尝试,这里不再举例。
  2. 在spider文件夹目录下可自建爬虫,爬取像豆瓣租房小组类似信息加入数据库。
  3. 数据分析部分比如如何识别微博机器人,如何构建信息评价指标等,每个人实现方案不一样,挖掘信息的程度不同而已,本文不予给出。
  4. 可设置定时任务,比如一般上海租房每天更新两页,就定时运行命令并且num=2。
  5. 技术分享,全篇五千多字欢迎转载,但请注明出处,否则,否则我哭给你看😢。。。

Ref

  1. Web scraping with python, http://shop.oreilly.com/product/0636920034391.do 

  2. Twisted网络编程必备, http://down.51cto.com/data/616351 

  3. THEPPITAKK.Animplementationofdouble-araytrie[z].http:/Ainux.thai.net/–thep/datrie/datrie.html,2006. 

  4. 双数组Tire树简介. http://www.cnblogs.com/ooon/p/4883159.html 

  5. 王思力,张华平,王斌.双数组Trie树算法优化及其应用研究[J].中文信息学报,2006,20(5):24—30