etlpy是python编写的网页数据抓取和清洗工具,核心文件etl.py不超过500行,具有以下特色html
github地址: https://github.com/ferventdesert/etlpy, 欢迎star!python
运行须要python3和lxml, 使用pip3 install lxml便可安装。内置的工程project.xml,包含了链家和大众点评两个爬虫的配置示例。git
etlpy具备鲜明的函数式风格特征,使用了大量的动态类型,惰性求值,生成器和流式计算。程序员
另外,github上有一个项目,里面有各类500行左右的代码实现的系统,看了几个很是赞https://github.com/aosabook/500linesgithub
当从网页和文件中抓取和处理数据时,咱们总会被复杂的细节,好比编码,奇怪的Html和异步ajax请求所困扰。etlpy可以方便地处理这些问题。ajax
etlpy的使用很是简单,先加载工程,以后便可返回一个生成器,返回所需数量便可。下面的代码,可以在20分钟内,获取大众点评网站上海的所有美食列表,总共16万条,30MB.正则表达式
import etl; etl.LoadProject('project.xml'); tool = etl.modules['大众点评门店']; datas = tool.QueryDatas() for r in datas: print(r)
结果以下:数据库
{'区域': '川沙', '标题': '胖哥俩肉蟹煲(川沙店)', '区县': '', '地址': '川沙镇川沙路5558弄绿地广场三号楼', '环境': '9.0', '介绍': '', '类型': '其余', '总店': '胖哥俩肉蟹煲', 'ID': '/shop/19815141', '口味': '9.1', '星级': '五星商户', '总店id': '19815141', '点评': '2205', '其余': '订座:本店支持在线订座', '均价': 67, '服务': '8.9'} {'区域': '金杨地区', '标题': '上海小南国(金桥店)', '区县': '', '地址': '张杨路3611弄金桥国际商业广场6座2楼', '环境': '8.8', '类型': '本帮江浙菜', 'ID': '/shop/3525763', '口味': '8.6', '星级': '准五星商户', '点评': '1973', '其余': '', '均价': 190, '服务': '8.5'} {'区域': '临沂/南码头', '标题': '新弘声酒家(临沂路店)', '区县': '', '地址': '临沂路8弄42号', '环境': '8.7', '介绍': '新弘声酒家!仅售85元!价值100元的午市代金券1份,全场通用,可叠加使用。', '类型': '本帮江浙菜', '总店': '新弘声酒家', 'ID': '/shop/19128637', '口味': '9.0', '星级': '五星商户', '总店id': '19128637', '点评': '621', '其余': '团购:新弘声酒家!仅售85元!价值100元的午市代金券1份,全场通用,可叠加使用。', '均价': 87, '服务': '8.8'} {'区域': '张江', '标题': '阿拉人家上海菜(浦东长泰广场店)', '区县': '', '地址': '祖冲之路1239弄1号长泰广场10号楼203', '环境': '8.9', '介绍': '仅售42元,价值50元代金券', '类型': '本帮江浙菜', '总店': '阿拉人家上海菜', 'ID': '/shop/21994899', '口味': '8.8', '星级': '准五星商户', '总店id': '21994899', '点评': '1165', '其余': '团购:仅售42元,价值50元代金券', '均价': 113, '服务': '8.8'}
固然,以上方法是串行执行,你也能够选择并行执行以获取更快的速度:编程
tool.mThreadExecute(threadcount=20,execute=False,callback=lambda d:print(d))
可设置线程数,对获取的每一个数据的回调方法,以及是否执行其中的执行器(下文有解释)。
etlpy的执行逻辑基于xml文件,不建议手工编写xml,而是使用笔者开发的另外一款图形化爬虫工具,能够经过图形拖拽的方式设计并生成工程文件,这套工具也即将开源,由于暂时还没想到较好的名字。基于C#/WPF开发,经过这套工具,十分钟内就能完成大众点评的采集程序的编写,若是手工编码,一个熟练的python程序员可能得写一天。该工具生成的xml,便可被etlpy解析,生成跨平台的多线程爬虫。
你能够选择手工修改xml,或是在代码中直接修改,来采集不一样城市,或是输出到不一样的文件:
tool.AllETLTools[0].arglists=['1'] #修改城市,1为上海,2为北京,参考大众点评的网页定义 tool.AllETLTools[-1].NewTableName= 'D:\大众点评.txt' #修改导出的文件
咱们将每一步骤定义为独立的模块,将其串成一条链条(咱们称之为流)。以下图所示:json
鉴于博客园很多读者熟悉C#,咱们不妨先用C#的例子来说解:
其本质是动态组装Linq, 其数据链为IEnumerable<IFreeDocument>。 IFreeDocument是 IDictionary<string, object>接口的扩展。Linq的Select函数可以对流进行变换,在本例中,就是对字典不一样列的操做(增删改),不一样的模块定义了一个完整的Linq流:
result= source.Take(mount).where(d=>module0.func(d)).select(d=>Module1.func(d)).select(d=>Module2.func(d))….
python的生成器相似于C#的Linq,是一种流式迭代。etlpy对生成器作了扩展,实现了生成器级联,并联和交叉(笛卡尔积)
def Append(a, b): for r in a: yield r; for r in b: yield r; def Cross(a, genefunc, tool): for r1 in a: for r2 in genefunc(tool, r1): for key in r1: r2[key] = r1[key] yield r2;
那么,生成器生成的是什么呢?咱们选用了Python的字典,这种键值对的结构很好用。能够将全部的模块分为四种类型:
生成器(GE):如生成100个字典,键为1-100,值为‘1’到‘100’
转换器(TF):如将地址列中的数字提取到电话列中
过滤器(FT):如过滤全部某一列的值为空的的字典
执行器(GE):如将全部的字典存储到MongoDB中。
咱们如何将这些模块组合成完整链条呢?因为Python没有Linq,咱们经过组合生成器来获取新的生成器,这个函数定义以下:
def __generate__(self, tools, generator=None, execute=False): for tool in tools: if tool.Group == 'Generator': if generator is None: generator = tool.Func(tool, None); else: if tool.MergeType == 'Append': generator = extends.Append(generator, tool.Func(tool, None)); elif tool.MergeType == 'Merge': generator = extends.MergeAll(generator, tool.Func(tool, None)); elif tool.MergeType == 'Cross': generator = extends.Cross(generator, tool.Func, tool) elif tool.Group == 'Transformer': generator = transform(tool, generator); elif tool.Group == 'Filter': generator = filter(tool, generator); elif tool.Group == 'Executor' and execute: generator = tool.Func(tool, generator); return generator;
如何定义模块呢?若是是先定义基类,而后从基类继承,这种方式依然要写大量的代码,并且不够Pythonic(我C#版本的代码就是这样写的)。
以清除字符串中先后空白的字符为例(C#中的trim, Python中的strip),咱们可以定义这样的函数:
def TrimTF(etl, data): return data.strip();
以后,经过读取配置文件,运行时动态地为一个基础对象添加属性和方法,从一个简单的TrimTF函数,生成一个具有一样功能的类。 整个etlpy的编写思路,就是从函数生成类,再最后将类的对象(模块)组合成流。
至于爬虫获取HTML正文的信息,则使用了XPath,而非正则表达式,固然你也可使用正则。XPath也是自动生成的,具体的原理将在以后的博文中讲解。etlpy本质上是从新定义了抓取和清洗的原语,是一种新的语言(DSL),从而大大下降了编写这类应用的成本和复杂度。
(串行模式的QueryDatas函数,有一个etlcount的可选参数,你能够分别将其值设为从1到n,观察数据是如何被一步步地组合出来的)
先以抓取链家地产为例,咱们来说解这种流的强大:如何采集全部二手房数据呢?这涉及到翻页。
翻页时,咱们会看到页面是这样变换的:
http://bj.lianjia.com/ershoufang/pg2/
http://bj.lianjia.com/ershoufang/pg3/
…
所以,须要构造一串上面的url. 聪明的你确定会想到,应当先生成一组序列,从1到100(假设咱们只抓取前100页)。
再经过MergeTF函数,从1-100生成上面的url列表。如今总共是100个url.
再经过爬虫转换器CrawlerTF,每一个页面可以生成30个二手房信息,所以可以生成100*30个页面,但因为是基于流的,因此这3000个信息是不断yield出来的,每生成一个,后续的流程,如去除乱码,提取数字,保存到文件,都会执行。这样咱们就获取了全部的信息。
不一样的流,能够组合为更高级的流。例如,想要获取全部房地产的数据,能够分别定义链家,我爱我家等地产公司的流,再经过流将多个流拼接起来。
大众点评的采集难度更大,每种门类只能翻到第50页,所以想要获取所有数据就必须想办法。
以北京美食为例,若是按不一样美食的门类(咖啡厅,火锅,小吃…)和区域(海淀,西城,东城…)区分,美食页面就没有五十页了。因此,首先生成北京全部区域的流(project中“大众点评区域”,感兴趣的读者能够试着获取这个流看看),再生成全部美食门类的流(大众点评门类)。而后再将这两个流作交叉(m*n),再组合获取了每一个种类的url, 经过url获取页面,再经过XPath获取对应门类的门店数量:
上文中的1238,也就是朝阳区的北京菜总共有1238家。
再经过python脚本计算要翻的页数,由于每页15个,那么有int(1238/15.0)+1页,记做q。 总共要抓取的页面数量,是一个(m,n,q)的异构立方体,不一样的(m,n)都对应不一样的q。 以后,就能够用相似于链家的方法,抓取全部页面了。
为了保证讲解的简单,我省略了大量实现的细节,其实在其中作了不少的优化。
还以大众点评为例,咱们但愿只修改一个模块,就能切换北京,上海等美食的信息。
北京和上海的美食门类和区域列表都不同,因此两个子流的队首的生成器,定义了城市的id。若是想修改城市,须要修改三个生成器。这太麻烦了,所以,etlpy采用了动态替换的方法。 若是主流中定义了与子流中同名的模块,只要修改了主流,主流就能够对子流完成修改。
最简单的并行化,应该从流的源头开始:
但若是队首只有一个元素,那么这种方法就很是低下了:
一种很是简单的思路,是将其切成两个流,并行在流中完成。
以大众点评为例, 北京有14个区县,有30种美食类型,那么先经过流1,获取420个元素,再以420个元素的基础上,进行并行,这样速度就快不少了。你也能够在14个区县以后插入并行化,那么就有14个子任务。etlpy经过一个ToListTF模块(它什么都不干)做为标识,做为流1和流2的分割符。
OneInput=True说明函数只须要字典中的一个值,此时传到函数里的只有dict[key],不然传递整个dict
OneOutput=True说明函数可能输出多个值,所以函数直接修改dict并返回null, 不然返回一个value,etlpy在函数外部修改dict.
IsMultiYield=True说明函数会返回生成器。
其余参数可具体参考python代码。
使用xml做为工程的配置文件有显然的好处,由于可以被各类语言方便地读取,可是噪音太多,不易手工编写,若是能设计一个专用的数据清洗语言,那么应该会好不少。其实用图形化编程,效率会特别高。
etlpy的思想,来自于讲解Lisp的一本书《计算机程序的构造与解释》(SICP),书评在此:Lisp和SICP
可视化软件会在一个月内所有开源,解放程序员的大脑和双手,号称爬虫的终极武器。敬请期待。
有任何问题,欢迎留言交流,或在Github中讨论。