大数据社会数据就是黄金,新浪微博作为一个国内网络社交早就意识到这一点,本着资本家和商人的心态给你提供的开放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中的,下面我们就写个例子来扒一个网页下来。
import urllib2 response = urllib2.urlopen("http://www.baidu.com") print response.read()结果就和在Chrome等浏览器中右键查看源码一样的内容,urllib2是python内置库,简化了httplib的用法(urllib2.urlopen相当于Java中的HttpURLConnectio)。有2那肯定有urllib啊,urllib2可以接受一个Request类的实例来设置URL请求的headers,但urllib仅可以接受URL。这意味着,你不可以伪装你的User Agent字符串等。urllib2在python3.x中被改为urllib.request。 接下来用urllib2伪装iphone 6浏览,模拟浏览器发送GET请求。
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'))结果会返回移动版的源码信息
... <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字段就可以,下面附加用户名密码登录新浪博客。
#我们模拟一个微博登录,先读取登录的邮箱和口令,然后按照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
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。
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。
BeautifulSoupBeautifulSoup 翻译叫鸡汤,现在版本是4.5.1,简称BS4,倒过来叫4SB,不过抓数据一点都不SB。提供一些简单的、python式的函数用来处理导航、搜索、修改分析树等功能。它是一个工具箱,通过解析文档为用户提供需要抓取的数据,因为简单,所以不需要多少代码就可以写出一个完整的应用程序。Beautiful Soup自动将输入文档转换为Unicode编码,输出文档转换为utf-8编码。你不需要考虑编码方式,除非文档没有指定一个编码方式,这时,Beautiful Soup就不能自动识别编码方式了。然后,你仅仅需要说明一下原始编码方式就可以了。 关于BS的介绍和用法 官方文档 很详细,下面给几个”Web scraping with python”中的例子看下BS是否好喝,可以和文档对照看。 首先你得安装了BS,然后爬取 http://www.pythonscraping.com/pages/page3.html 中的图片来小试牛刀。
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。
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。 有了代理地址,不一定能保证有效,可能就被封杀了,因此使用思路是把代理地址存入哈希表,验证无效的删除(看状态码),重新在表中取新的记录。 代理地址使用方式如下:
... 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网络编程必备》。关于单线程、多线程、异步有张图推荐看下。

写爬虫还要考虑其他很多问题,授权验证、连接池、数据处理、js处理等,这里有个经典爬虫框架:Scrapy,目前支持python3,支持分布式, 使用 Twisted来处理网络通讯,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。
Scrapy与PyspiderScrapy的入门学习参见 学习Scrapy入门 ,对应 中文文档 几小时内可以快速掌握。另外国内某大神开发了个WebUI的 Pyspider ,具有以下特性:
python 脚本控制,可以用任何你喜欢的html解析包(内置 pyquery) WEB 界面编写调试脚本,起停脚本,监控执行状态,查看活动历史,获取结果产出 支持 mysql, MongoDB, SQLite 支持抓取 javascript 的页面 组件可替换,支持单机/分布式部署,支持 Docker 部署 强大的调度控制从内容上讲,两者具有功能差不多,包括以上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被禁用,主要有以下几个策略。
动态设置user agent 禁用cookies 设置延迟下载 使用 Google cache 使用IP地址池( Tor project 、VPN和代理IP) 使用 Crawlera由于Google cache基于你懂的原因不可用,其余都可以利用,Crawlera的分布式下载,我们可以在下次用一篇专门的文章进行讲解。下面主要从动态随机设置user agent、禁用cookies、设置延迟下载和使用代理IP这几个方式入手。
自定义中间件Scrapy下载器通过中间件控制的,要实现代理IP、user agent切换可以自定义个中间件。 在项目下创建(如何创建项目,使用scrapy start yourProject命令,参考文档)好项目后,在里面找到setting.py文件,先把agents和代理ip放到setting.py中(代理ip较少情况下这样做,较多的话还是放到数据库中去,方便管理),设置中间件名字MyCustomSpiderMiddleware和优先级。
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
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树
DATTrie树是搜索树的一种,来自英文单词”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数组用于检验转移的正确性,检验该状态是否存在。在比较用于正向最大匹配分词的速度方面,DAT分词平均速度为936kB/s(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中取出的时间要转化为当地时间。
> 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个角度比较:
功能:oracle>mysql> nosql 写性能:noSql>oracle$\approx$mysql 简单查询: oracle>mysql>nosql 复杂查询(含join): oracle>mysql>nosql 架构扩展: noSql>mysql>oracle 可维护性: oracle>mysql>nosql 成本: oracle>mysql$\approx$nosql对于现在这个场景,爬虫在前端爬数据,管道层在那边处理数据后写数据,而这些数据具有时效性,也就是说只会去读一部分数据,相对来说,这就对写的要求较高。此外,这个场景就一个表,不涉及多表关联、约束等,复杂查询可以说没有,需要功能较少。另外网络数据不能保证一致性和可靠性,只要高可用性(HA)即可,Nosql可以设置副本机制达到高可用性,mysql虽然也可以做到成本稍高,将来可扩展角度也不适合。因此这个场景最适合的是Nosql。
Hbase 还是MongodbCassandra 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到本地后,请先确保以下环境已经安装:
scrapy datrie pymongo mongoDB执行下面命令:
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行注释掉。一般如果有‘上海租房’了就不要去搜索‘浦东租房’,因为基本上有‘浦东租房’的微博都会有@‘上海租房’,所以下面会出现插入重复记录错误。 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 [