在很多人的眼里,Map-Reduce等于Hadoop,没有Hadoop谈Map-Reduce犹如自上谈兵,实则不然,Map-Reduce是一种计算模型,只是非常适合在并行的环境下运行,Hadoop是Map-Reduce的一种实现,没有Hadoop照样可以跑Map-Reduce程序。python就内置有map()和reduce方法(虽然与hadoop的map-reduce有区别)。
这篇文章主要介绍如何用python在linux的管道进行map-reduce编程,本文写的所有map-reduce程序都可以原封不动的放在Hadoop下运行,关于用Hadoop Streaming调用python的map-reduce程序,可以参考这篇文章(非常详细,推荐看看),本文主要通过几个实例来讲解如何用map-reduce来处理具体的问题。
一、python的内置函数map reduce
首先来看看python的两个内置函数map()和reduce()
1、map
map(function, iterable, ...)
map对迭代器的每个元素调用function函数,在Python2中,将得到的结果组成一个list,而在python3中结果是一个迭代器。function的参数只有一个,即为迭代器的元素,看一个实例:
>>> d = [1,2,3,4,5] >>> m = map(lambda x:x*2,d) >>> print m [2, 4, 6, 8, 10]
这里的map可以理解为一种映射,将迭代器中的元素经过function映射到另一个值。
2、reduce
reduce(function, iterable[, initializer])
reduce可以理解为规约,它对迭代器从左到右累计调用function,function有两个参数,第一个是之前的累积值,第二个当前规约的元素,实例:
>>> d = [1,2,3,4,5] >>> r = reduce(lambda x,y:x+y,d) >>> r 15
利用reduce定义一个求前n个整数之和的函数
>>> def accuPlus(n): return reduce(lambda x,y:x+y,range(1,n+1)) >>> print accuPlus(10) 55 >>> print accuPlus(500) 125250 >>>
上面介绍的python的两个内置函数与我接下来要介绍的map-reduce还是有一些区别,适用于分布式下的map-redece主要是以键值对的形式处理数据,map阶段发射很多键值对出去,然后按键排序,而reduce则对键相同的键值对进行处理,也可认为是一种规约。
二、用map-reduce做词频统计
词频统计可以说是map-reduce的Hello World程序,它简单明了,却描述了map-reduce的基本原理,统计中文的词频得先要分词,目前有一些免费的分词软件,中科院和哈工大的都还不错,现在我们有下面这样一篇已经分好词的文章(data.txt)需要进行词频统计:
双子座 的 你 是 这样 的 吗 1 手机 不 离 身 睡觉 不 关机 2 对待 不同 的 人 有 不同 的 性格 3 从 小 懂得 很多 道理 但 知 行 往往 难以 合 一 4 有 时候 很 神经 有时候 很 镇静 5 会 因为 别人 一 句 话 伤心 但 不 会 被 发现 6 很 会 安慰 别人 却 不 会 安慰 自己 7 会 经常 怀念 从 前
定义map的处理(mapper.py)如下:
1 #!encoding=utf-8 2 3 from sys import stdin 4 5 def mapprint(word): 6 '''定义map函数对元素的处理方式,这里直接打印''' 7 print word 8 9 #对每行进行统计 10 for line in stdin: 11 words = line.strip().split(' ') 12 map(mapprint,words)
mapper.py从标准输入流中对每一行进行处理,然后将每个单词打印到标准输出,输出之后需要对词进行排序,才能被reduce处理,利用管道我们来看看mapper.py对data.txt进行处理:
$ cat data.txt |python mapper.py |sort
输出的部分结果如下:
-
安慰 安慰 被 别人 别人 不 不 不 不 不同 不同 从 从 但 但 道理
接下来我们看看reducer.py,mapper的输出排序后,相同的元素会出现在一起,我们可以从上往下扫描所有的元素,并维持一个计数器,遇到相同的词就把计数器加1,遇到不相同的词,就说明上一个词已经统计完毕了,可以将这个词及其词频输出到标准输出:
1 #!encoding=utf-8 2 from sys import stdin 3 4 last = None 5 count = 0 6 for line in stdin: 7 word = line.strip() 8 if word != last:#遇到不同的词 9 if last: 10 print '%s %d' % (last,count) 11 last = word 12 count = 0 13 count += 1 14 #输出最后一个词 15 if last: 16 print '%s %d' % (last,count) 17
结合map和reduce,整个词频统计执行如下:
$ cat data.txt |python mapper.py | sort |python reducer.py
输出的词频结果:
1 1 2 1 3 1 4 1 5 1 6 1 7 1 安慰 2 被 1 别人 2 不 4 不同 2 从 2 但 2 道理 1 的 4 懂得 1 对待 1 发现 1 关机 1 合 1 很 3 很多 1 话 1 怀念 1 会 5 经常 1 句 1 离 1 吗 1 难以 1 你 1 前 1 却 1 人 1 伤心 1 身 1 神经 1 时候 1 是 1 手机 1 双子座 1 睡觉 1 往往 1 小 1 行 1 性格 1 一 2 因为 1 有 2 有时候 1 这样 1 镇静 1 知 1 自己 1
为什么要把这个问题复杂化,完全可以用一个程序来处理统计词频这事,看下面的count.py
1 from collections import defaultdict 2 from sys import stdin 3 4 m = defaultdict(int) 5 for line in stdin: 6 words = line.strip().split(' ') 7 for word in words: 8 m[word] += 1 9 10 for word,count in m.items(): 11 print '%s %s' % (word,count)
有两个理由让我偏向于使用map-reduce作数据处理:
- 首先map-reduce的思路清晰,它将一个问题分成两步,map阶段我们只需要把词打印到标准输出,再经过排序进入reduce阶段,reduce对相同的词进行累加;
- map-reduce的程序可以原封不动的在Hadoop上跑起来,大规模的数据可以完爆count.py,现在的编程应该倾向于人的易理解性与扩展性,而不是在小数据上提升那么一丁点性能。
二、用map-reduce做多表等值连接
假设我们有这样一份某app的日志文件app.log,包含appId、IMEI、userInfo这三个字段(这里的讲述表的等值连接不牵涉到userInfo字段,所以该字段都置为U):
appId IMEI userInfo 8111111 I86733062 U1 8111112 I86733010 U2 8111113 I86733048 U3 8111114 I86733012 U4 8111115 I86733020 U5 8111116 I86733063 U6 8111117 I86733042 U7 8111118 I86733022 U8 8111119 I86733016 U9 8111120 I86733027 U10
现在我们想挖掘更多的用户信息,得到了一份微博的数据,微博的数据weibo.log包含weiboId、IMEI、weiboInfo(同意我们将weibInfo置为W):
weiboId IMEI weiboInfo 1287680 I86733017 W1 1287681 I86733048 W2 1287682 I86733015 W3 1287683 I86733047 W4 1287684 I86733020 W5 1287685 I86733051 W6 1287686 I86733022 W7 1287687 I86733036 W8
两个表都有一个共同的字段,即手机的IMEI序列号,通过微博的IMEI与我们app日志文件IMEI合并,就可以找出一些app用户的微博信息,进行进一步的分析,现在我们需要的是将既在app.log又在weibo.log中的用户找出来。
合并app.log和weibo.log到aw.log,合并了之后是方便于map的处理,但我们需要标记每条记录来自于哪个表,我们用A标准来自于app.log,用W标志weibo.log:
$ awk 'NR > 1 {printf "A %s ",$0}' app.log > aw.log
$ awk 'NR > 1 {printf "W %s ",$0}' weibo.log >> aw.log
这样合并之后的aw.log如下:
A 8111111 I86733062 U1 A 8111112 I86733010 U2 A 8111113 I86733048 U3 A 8111114 I86733012 U4 A 8111115 I86733020 U5 A 8111116 I86733063 U6 A 8111117 I86733042 U7 A 8111118 I86733022 U8 A 8111119 I86733016 U9 A 8111120 I86733027 U10 W 1287680 I86733017 W1 W 1287681 I86733048 W2 W 1287682 I86733015 W3 W 1287683 I86733047 W4 W 1287684 I86733020 W5 W 1287685 I86733051 W6 W 1287686 I86733022 W7 W 1287687 I86733036 W8
现在我们来写map的mapper.py,map的输出应该以IMEI作为key,其他字段作为value,这样经过排序后,才能保证在reduce阶段,IMEI相同的app.log记录和weibo.log记录能够连续出现,mapper.py实现如下:
1 #!encoding=utf-8 2 from sys import stdin 3 4 for line in stdin: 5 data = line.strip().split(' ') 6 if len(data) != 4:#过滤掉错误行 7 continue 8 #把IMEI放在最前面,以便以IMEI排序 9 print '%s %s %s %s' % (data[2],data[0],data[1],data[3])
mapper输出并排序下:
$ cat aw.log |python mapper.py |sort -k1 I86733010 A 8111112 U2 I86733012 A 8111114 U4 I86733015 W 1287682 W3 I86733016 A 8111119 U9 I86733017 W 1287680 W1 I86733020 A 8111115 U5 I86733020 W 1287684 W5 I86733022 A 8111118 U8 I86733022 W 1287686 W7 I86733027 A 8111120 U10 I86733036 W 1287687 W8 I86733042 A 8111117 U7 I86733047 W 1287683 W4 I86733048 A 8111113 U3 I86733048 W 1287681 W2 I86733051 W 1287685 W6 I86733062 A 8111111 U1 I86733063 A 8111116 U6
接下来处理reduce了,reduce从流中不断扫描行,当遇到相同的IMEI,并且一个来自weibo.log一个来自app.log,就把这两条记录拼接起来,reducer.py:
1 #!encoding=utf-8 2 from sys import stdin 3 4 wIMEI = None#记录来自为微博的IMEI 5 weibo = None 6 7 aIMEI = None#记录来自app的IMEI 8 app = None 9 10 for line in stdin: 11 data = line.strip().split(' ',2) 12 if len(data) != 3:#过滤错误的数据行 13 continue 14 if data[1] == 'A': 15 aIMEI = data[0] 16 app = data[2] 17 elif data[1] == 'W': 18 wIMEI = data[0] 19 weibo = data[2] 20 if wIMEI == aIMEI and wIMEI is not None:#两个IMEI相等时连接两行 21 print '%s %s %s' % (wIMEI,app,weibo) 22 aIMEI = wIMEI = None#重置 23 24 if wIMEI == aIMEI and wIMEI is not None:#最后的记录不要忘记输出 25 print '%s %s %s' % (wIMEI,app,weibo)
连接map-reduce,整个等值连接结果如下:
$ cat aw.log |python mapper.py |sort -k1 | python reducer.py I86733020 8111115 U5 1287684 W5 I86733022 8111118 U8 1287686 W7 I86733048 8111113 U3 1287681 W2
上述的map-reduce阶段依然可以用一个程序跑,但日志文件往往比较大,几GB到几十GB也很正常,但我们的map-reduce总是能伸缩自如,用Hadoop不怕数据量大。
三、用map-reduce做矩阵的迭代运算
我们来看最后一个例子,一个矩阵的一下轮的元素等于其周围四面八方所有元素以及自己的之和的平均值,比如下面矩阵:
1 2 3 4 5 6 7 8 9
下一轮矩阵为:
(1+2+4+5)/4 (1+2+3+4+5+6)/6 (2+3+5+6)/4
(1+2+4+5+7+8)/6 (1+2+3+4+5+6+7+8+9)/9 (2+3+5+6+8+9)/6
(4+5+7+8)/5 (4+5+6+7+8+9)/6 (5+6+8+9)/4
当矩阵非常大的时候,我们需要把矩阵转为三元组的形式<x,y,value>,这样方便map-reduce的处理,否则,矩阵非常大,矩阵的一行就能爆内存。比如上面的矩阵转化为三元组后变为:
0,0,1 0,1,2 0,2,3 1,0,4 1,1,5 1,2,6 2,0,7 2,1,8 2,2,9
现在我们处理map,先理清map阶段需要发射的key和value是什么,map扫描每一行得到的是矩阵的一个元素,而这个元素会参与周围所有元素的均值计算,因此我们对每一个<x,y,value>输出的键值对是(<xi,yj,>,<x,y,value>),<xi,yj,>是<x,y>周围的坐标,map输出后,按xi,yj顺序排序,对所有key值相同的元素累加并求平均值,mapper.py:
1 #!encoding=utf-8 2 from sys import stdin 3 4 M = 3#矩阵的行数 5 N = 3#矩阵的列数 6 7 for line in stdin: 8 data = line.strip().split(',') 9 x,y = map(int,data[0:2]) 10 value = float(data[2]) 11 for i in xrange(-1,2): 12 if x + i < 0 or x + i >= M:#超出上下边界 13 continue 14 for j in xrange(-1,2): 15 if y + j < 0 or y + j >= N:#超出左右边界 16 continue 17 print '%d,%d %f' % (x+i,y+j,value)#为周围的每个值计算提供贡献
执行mapper.py,排序时注意一下,字段分割符是制表符' ',实际上hadoop的默认分割符就是制表符:
$ cat data.txt |python mapper.py |sort -t$' '
mapper.py输出的中间过程比较长,我们看一下部分结果:
0,0 1.000000 0,0 2.000000 0,0 4.000000 0,0 5.000000 0,1 1.000000 0,1 2.000000 0,1 3.000000 0,1 4.000000 0,1 5.000000 0,1 6.000000 0,2 2.000000 0,2 3.000000 0,2 5.000000 0,2 6.000000
接下来写reducer.py,在reduce阶段依旧是扫描每一行,并判断是否与上一行的key相同,如果相同就累加value,如果不同,则计算平均值,并输出到标准输出:
1 from sys import stdin 2 last = None 3 count = 0 4 s = 0 5 for line in stdin: 6 p,v = line.strip().split(' ') 7 value = float(v) 8 if last != p: 9 if last: 10 print '%5s,%10f' % (last,s/count) 11 last = p 12 s = 0 13 count = 0 14 s += value 15 count += 1 16 if last: 17 print '%5s,%10f' % (last,s/count)
连接map-reduce执行:
$ cat data.txt |python mapper.py |sort -t$' '|python reducer.py
输出如下:
0,0, 3.000000 0,1, 3.500000 0,2, 4.000000 1,0, 4.500000 1,1, 5.000000 1,2, 5.500000 2,0, 6.000000 2,1, 6.500000 2,2, 7.000000
其实这里,矩阵连续迭代到max(M,N)+3次之后,矩阵的每个元素都将趋向于整个矩阵的平均值,因为经过max(M,N)+3次迭代后,最左边的元素已经传递到了最右边,最上面的元素也已经传递到了最下边,作为一个单机版的测试,我写了迭代测试:
1 from os import system 2 3 lastMatrix = 'data.txt' 4 matrix = None 5 for i in xrange(1,4+3): 6 if matrix != None: 7 lastMatrix = matrix 8 matrix = 'data.'+str(i) 9 print i 10 cmd = 'cat '+lastMatrix+"|python mapper.py |sort -t' '|python reducer.py >"+matrix 11 system(cmd) 12 13 system('cat '+matrix)
迭代6次后输出:
0,0, 4.937500 0,1, 4.953125 0,2, 4.968750 1,0, 4.984375 1,1, 5.000000 1,2, 5.015625 2,0, 5.031250 2,1, 5.046875 2,2, 5.062500
可以看到已经基本接近矩阵均值5了。
四、总结
上面介绍了三个实例,用map-reduce来处理数据,可以看到,按照这种思路写代码,不仅思路清晰,而且扩展性强,因为背后有hadoop在支撑。需要注意的是,上面的map-reduce代码在hadoop中运行时,应该对每一行的处理加上一个try...except,过滤掉那些异常数据,因为海量数据里总是有一些噪声数据,如果没有try-except,hadoop虽然在失败时会重试,但重试多次后任然失败,那整个任务就会失败了。
最后,完整的代码在github上可以查看。