etlpy:并⾏爬⾍和数据清洗⼯具(开源)
etlpy是python编写的⽹页数据抓取和清洗⼯具,核⼼⽂件etl.py不超过500⾏,具备如下特点
爬⾍和清洗逻辑基于xml定义,不需⼿⼯编写
基于python⽣成器,流式处理,对内存⽆要求
内置线程池,⽀持串⾏和并⾏处理
内置正则解析,html转义,json转换等数据清洗功能,直接输出可⽤⽂件
插件式设计,能够⾮常⽅便地增加其他⽂件和数据库格式
能够⽀持⼏乎⼀切⽹站,能⾃动填⼊cookie
github地址:,欢迎star!
运⾏需要python3和lxml, 使⽤pip3 install lxml即可安装。内置的⼯程l,包含了链家和⼤众点评两个爬⾍的配置⽰例。
etlpy具有鲜明的函数式风格特征,使⽤了⼤量的动态类型,惰性求值,⽣成器和流式计算。
另外,github上有⼀个项⽬,⾥⾯有各种500⾏左右的代码实现的系统,看了⼏个⾮常赞github/aosabook/500lines
⼆.如何使⽤
当从⽹页和⽂件中抓取和处理数据时,我们总会被复杂的细节,⽐如编码,奇怪的Html和异步ajax请求所困扰。etlpy能够⽅便地处理这些问题。
etlpy的使⽤⾮常简单,先加载⼯程,之后即可返回⼀个⽣成器,返回所需数量即可。下⾯的代码,能够在20分钟内,获取⼤众点评⽹站上海的全部美⾷列表,总共16万
条,30MB.
import etl;
etl.LoadProject('l');
tool = dules['⼤众点评门店'];
datas = tool.QueryDatas()
for r in datas:
print(r)
结果如下:
{'区域': '川沙', '标题': '胖哥俩⾁蟹煲(川沙店)', '区县': '', '地址': '川沙镇川沙路5558弄绿地⼴场三号楼', '环境': '9.0', '介绍': '', '类型': '其他', '总店': '胖哥俩⾁蟹煲', 'ID': '/shop/19815141', '⼝味': '9.1', '星级': '五星商户', '总店id': '19815141', '点评': '2205', '其他{'区域': '⾦杨地区', '标题': '上海⼩南国(⾦桥店)', '区县': '', '地址': '张杨路3611弄⾦桥国际商业⼴场6座2楼', '环境': '8.8', '类型': '本帮江浙菜', 'ID': '/shop/3525763', '⼝味': '8.6', '星级': '准五星商户', '点评': '1973', '其他': '', '均价': 190, '服务': '8.5'
{'区域': '临沂/南码头', '标题': '新弘声酒家(临沂路店)', '区县': '', '地址': '临沂路8弄42号', '环境': '8.7', '介绍': '新弘声酒家!仅售85元!价值100元的午市代⾦券1份,全场通⽤,可叠加使⽤。', '类型': '本帮江浙菜', '总店': '新弘声酒家', 'ID': '/shop/19128637', {'区域': '张江', '标题': '阿拉⼈家上海菜(浦东长泰⼴场店)', '区县': '', '地址': '祖冲之路1239弄1号长泰⼴场10号楼203', '环境': '8.9', '介绍': '仅售42元,价值50元代⾦券', '类型': '本帮江浙菜', '总店': '阿拉⼈家上海菜', 'ID': '/shop/21994899', '⼝味': '8.8', '星级':当然,以上⽅法是串⾏执⾏,你也可以选择并⾏执⾏以获取更快的速度:
tool.mThreadExecute(threadcount=20,execute=False,callback=lambda d:print(d))
可设置线程数,对获取的每个数据的回调⽅法,以及是否执⾏其中的执⾏器(下⽂有解释)。
etlpy的执⾏逻辑基于xml⽂件,不建议⼿⼯编写xml,⽽是使⽤笔者开发的另⼀款图形化爬⾍⼯具,可以通过图形拖拽的⽅式设计并⽣成⼯程⽂件,这套⼯具也即将开源,因为暂时还没想到较好的名字。基于C#/WPF开发,通过这套⼯具,⼗分钟内就能完你可以选择⼿⼯修改xml,或是在代码中直接修改,来采集不同城市,或是输出到不同的⽂件:
tool.AllETLTools[0].arglists=['1']  #修改城市,1为上海,2为北京,参考⼤众点评的⽹页定义
tool.AllETLTools[-1].NewTableName= 'D:\⼤众点评.txt'#修改导出的⽂件
三.原理
我们将每⼀步骤定义为独⽴的模块,将其串成⼀条链条(我们称之为流)。如下图所⽰:
C#版本原理
鉴于博客园不少读者熟悉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版本原理
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的编写思路,就是从函数⽣成类,再最后将类的对象(模块)组合成流。python正则表达式爬虫
⾄于爬⾍获取HTML正⽂的信息,则使⽤了XPath,⽽⾮正则表达式,当然你也可以使⽤正则。XPath也是⾃动⽣成的,具体的原理将在之后的博⽂中讲解。etlpy本质上是重新定义了抓取和清洗的原语,是⼀种新的语⾔(DSL),从⽽⼤⼤降低了编写这类应⽤的成本和复杂度。
(串⾏模式的QueryDatas函数,有⼀个etlcount的可选参数,你可以分别将其值设为从1到n,观察数据是如何被⼀步步地组合出来的)
三.例⼦
采集链家
先以抓取链家地产为例,我们来讲解这种流的强⼤:如何采集所有⼆⼿房数据呢?这涉及到翻页。
翻页时,我们会看到页⾯是这样变换的:
因此,需要构造⼀串上⾯的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。之后,就可以⽤类似于链家的⽅法,抓取所有页⾯了。
四.优化和细节
为了保证讲解的简单,我省略了⼤量实现的细节,其实在其中做了很多的优化。
1. 修改流,获取不同城市的信息
还以⼤众点评为例,我们希望只修改⼀个模块,就能切换北京,上海等美⾷的信息。
北京和上海的美⾷门类和区域列表都不⼀样,所以两个⼦流的队⾸的⽣成器,定义了城市的id。如果想
修改城市,需要修改三个⽣成器。这太⿇烦了,因此,etlpy采⽤了动态替换的⽅法。如果主流中定义了与⼦流中同名的模块,只要修改了主流,主流就可以对⼦流完成修改。
2. 并⾏优化
最简单的并⾏化,应该从流的源头开始:
但如果队⾸只有⼀个元素,那么这种⽅法就⾮常低下了:
⼀种⾮常简单的思路,是将其切成两个流,并⾏在流中完成。
以⼤众点评为例,北京有14个区县,有30种美⾷类型,那么先通过流1,获取420个元素,再以420个元素的基础上,进⾏并⾏,这样速度就快很多了。你也可以在14个区县之后插⼊并⾏化,那么就有14个⼦任务。etlpy通过⼀个ToListTF模块(它什么都不⼲)作为标识,作为流1和流2的分割符。
4.⼀些参数的说明
OneInput=True说明函数只需要字典中的⼀个值,此时传到函数⾥的只有dict[key],否则传递整个dict
OneOutput=True说明函数可能输出多个值,因此函数直接修改dict并返回null,否则返回⼀个value,etlpy在函数外部修改dict.
IsMultiYield=True说明函数会返回⽣成器。
其他参数可具体参考python代码。
五.展望
使⽤xml作为⼯程的配置⽂件有显然的好处,因为能够被各种语⾔⽅便地读取,但是噪⾳太多,不易⼿⼯编写,如果能设计⼀个专⽤的数据清洗语⾔,那么应该会好很多。其实⽤图形化编程,效率会特别⾼。
etlpy的思想,来⾃于讲解Lisp的⼀本书《计算机程序的构造与解释》(SICP),书评在此:
可视化软件会在⼀个⽉内全部开源,解放程序员的⼤脑和双⼿,号称爬⾍的终极武器。敬请期待。有任何问题,欢迎留⾔交流,或在Github中讨论。