转载来源:
http://www.tianjun.ml/essays/19
写作缘由
尽管Hadoop的框架是用Java写的,但是基于Hadoop运行的程序并不一定要用Java来写,我们可以选择一些其他的编程语言比如Python或者C++。不过,Hadoop的文档以及Hadoop网站上给出的典型Python例子可能让人觉得必须先将Python的代码用Jython转成一个Java文件。显然,如果你需要使用一些Jython所不能提供的Python特性的话这会很不方便。使用Jython的另外一个问题是使用这种方式与Hadoop交互会带来额外的开销。看一下$HADOOP_HOME/src/examples/python/WordCount.py 这个例子你就会明白了。
到此,本教程的目的也就很明确了:用一种更Pythonic的方式来写Hadoop 上的MapReduce(一种你更为熟悉的方式)
写作目的
本文的目的是用Python写一个简单的运行在Hadoop上的MapReduce程序,该程序不需要使用Jython将Python源码转成Java文件。这个程序将模仿WordCount(读取文本文件并统计单词的词频),输出的文本文件每行包括一个单词和该单词的词频,并用tab分隔。
前提
因为需要实际动手操作,你至少有一个Hadoop集群能正常运行,如果你没有Hadoop集群的话,下面的教程可以帮你建立一个。下面的教程是基于Ubuntu系统建立的,但是你可以将他们应用到其它Linux、Unix系统上。
Running Hadoop On Ubuntu Linux (Multi-Node Cluster)
Running Hadoop On Ubuntu Linux (Single-Node Cluster)
Python MapReduce 源码解析
以下Python代码所使用的技巧是基于Hadoop 数据流的接口实现的。借助该接口,我们可以通过标准输入(stdin)和标准输出(stdout)来传递Map和Reduce过程之间的数据。这里我们简单地使用Python的sys.stdin读取数据并通过sys.stdout输出数据,剩下的事情就全交给Hadoop啦~
Map
将以下代码写入 /home/hduser/mapper.py 它会从stdin读取数据,分割其中的单词然后按行输出单词和其词频到stdout 。不过整个Map处理过程并不会统计每个单词出现总的次数,而是直接输出(<word>, 1)元组。尽管某些单词会出现不止一次,但只要单词出现一次我们就输出一个(<word>, 1)元组。在接下来的Reduce过程我们会统计单词出现的总的次数。当然,你也可以不这么做,但是出于“教程”这一目的,接下来的内容会按照这一思路来写。 :-)
注意:确保该文件是可执行的(chmod +x /home/hduser/mapper.py),否则会出问题。另外第一行记得添加 #!/usr/bin/env python
mapper.py
01
#!/usr/bin/env python
02
03
import
sys
04
05
# input comes from STDIN (standard input)
06
for
line
in
sys
.
stdin
:
07
# remove leading and trailing whitespace
08
line
=
line
.
strip()
09
# split the line into words
10
words
=
line
.
split()
11
# increase counters
12
for
word
in
words
:
13
# write the results to STDOUT (standard output);
14
# what we output here will be the input for the
15
# Reduce step, i.e. the input for reducer.py
16
#
17
# tab-delimited; the trivial word count is 1
18
print
'
%s
\t
%s
'
% (
word
,
1)
Reduce
将以下代码保存到/home/hduser/reducer.py 它会从stdin读取mapper.py的结果(因此mapper.py的输出格式应该与reducer.py的输入格式一致),然后统计每个单词出现的总的次数并输出到stdout
注意:确保该文件是可执行的(chmod +x /home/hduser/mapper.py),否则会出问题。另外第一行记得添加 #!/usr/bin/env python
reducer.py
01
#!/usr/bin/env python
02
03
from
operator
import
itemgetter
04
import
sys
05
06
current_word
=
None
07
current_count
=
0
08
word
=
None
09
10
# input comes from STDIN
11
for
line
in
sys
.
stdin
:
12
# remove leading and trailing whitespace
13
line
=
line
.
strip()
14
15
# parse the input we got from mapper.py
16
word
,
count
=
line
.
split(
'
\t
'
,
1)
17
18
# convert count (currently a string) to int
19
try
:
20
count
=
int(
count)
21
except
ValueError
:
22
# count was not a number, so silently
23
# ignore/discard this line
24
continue
25
26
# this IF-switch only works because Hadoop sorts map output
27
# by key (here: word) before it is passed to the reducer
28
if
current_word
==
word
:
29
current_count
+=
count
30
else
:
31
if
current_word
:
32
# write result to STDOUT
33
print
'
%s
\t
%s
'
% (
current_word
,
current_count)
34
current_count
=
count
35
current_word
=
word
36
37
# do not forget to output the last word if needed!
38
if
current_word
==
word
:
39
print
'
%s
\t
%s
'
% (
current_word
,
current_count)
测试你的代码(cat data | map | sort | reduce)
建议在Hadoop上实际运行mapreduce之前先在本地测试mapper.py和reducer.py否侧可能会出现程序能正常执行但却完全没有输出结果或者输出不是你想要的结果。如果这发生的话,多半是你自己搞砸了......
test
01
# very basic test
02 hduser@ubuntu:~
$
echo
"foo foo quux labs foo bar quux" | /home/hduser/mapper.py
03 foo 1
04 foo 1
05 quux 1
06 labs 1
07 foo 1
08 bar 1
09 quux 1
10
11 hduser@ubuntu:~
$
echo
"foo foo quux labs foo bar quux" | /home/hduser/mapper.py | sort -k1,1 | /home/hduser/reducer.py
12 bar 1
13 foo 3
14 labs 1
15 quux 2
16
17
# using one of the ebooks as example input
18
# (see below on where to get the ebooks)
19 hduser@ubuntu:~
$ cat /tmp/gutenberg/20417-8.txt | /home/hduser/mapper.py
20 The 1
21 Project 1
22 Gutenberg 1
23 EBook 1
24 of 1
25
[...
]
26 (you get the idea)
在Hadoop上运行 Python代码
下载测试用的输入数据
我们使用古腾堡项目中的三本电子书作为测试:
- The Outline of Science, Vol. 1 (of 4) by J. Arthur Thomson
- The Notebooks of Leonardo Da Vinci
- Ulysses by James Joyce
下载这些电子书的 txt格式,然后将这些文件保存到一个临时文件夹比如 /tmp/gutenberg
1 hduser@ubuntu:~
$ ls -l /tmp/gutenberg/
2 total 3604
3 -rw-r--r-- 1 hduser hadoop 674566 Feb 3 10:17 pg20417.txt
4 -rw-r--r-- 1 hduser hadoop 1573112 Feb 3 10:18 pg4300.txt
5 -rw-r--r-- 1 hduser hadoop 1423801 Feb 3 10:18 pg5000.txt
6 hduser@ubuntu:~
$
将本地的数据拷贝到HDFS上
在运行MapReduce任务之前,我们必须先将本地文件拷贝到Hadoop的文件系统上
01 hduser@ubuntu:/usr/local/hadoop
$ bin/hadoop dfs -copyFromLocal /tmp/gutenberg /user/hduser/gutenberg
02 hduser@ubuntu:/usr/local/hadoop
$ bin/hadoop dfs -ls
03 Found 1 items
04 drwxr-xr-x - hduser supergroup 0 2010-05-08 17:40 /user/hduser/gutenberg
05 hduser@ubuntu:/usr/local/hadoop
$ bin/hadoop dfs -ls /user/hduser/gutenberg
06 Found 3 items
07 -rw-r--r-- 3 hduser supergroup 674566 2011-03-10 11:38 /user/hduser/gutenberg/pg20417.txt
08 -rw-r--r-- 3 hduser supergroup 1573112 2011-03-10 11:38 /user/hduser/gutenberg/pg4300.txt
09 -rw-r--r-- 3 hduser supergroup 1423801 2011-03-10 11:38 /user/hduser/gutenberg/pg5000.txt
10 hduser@ubuntu:/usr/local/hadoop
$
运行MapReduce任务
现在一切都准备好了,我们可以通过Hadoop的数据流API来传送Map和Reduce过程中间的数据。
1 hduser@ubuntu:/usr/local/hadoop
$ bin/hadoop jar contrib/streaming/hadoop-*streaming*.jar
\
2 -file /home/hduser/mapper.py -mapper /home/hduser/mapper.py
\
3 -file /home/hduser/reducer.py -reducer /home/hduser/reducer.py
\
4 -input /user/hduser/gutenberg/* -output /user/hduser/gutenberg-output
你可以通过指定 -D参数来更改一些Hadoop 设置,比如增加reducer数量
1 hduser@ubuntu:/usr/local/hadoop
$ bin/hadoop jar contrib/streaming/hadoop-*streaming*.jar -D mapred.reduce.tasks
=16 ...
注意在命令行中可以接受mapred.reducetasks参数来指定reduce的个数,但是不能仅仅通过指定mapred.reduce.tasks来指定map tasks的个数。
整个任务会从HDFS的路径/user/huser/gutenberg 上读取所有的文件,然后处理并输出到HDFS的路径/user/huser/gutnberg-output事实上Hadoop会给每个reducer创建一个输出文件,在我们的例子中只会输出一个文件因为输入的文件很小。上面命令行的输出示例如下:
01 hduser@ubuntu:/usr/local/hadoop
$ bin/hadoop jar contrib/streaming/hadoop-*streaming*.jar -mapper /home/hduser/mapper.py -reducer /home/hduser/reducer.py -input /user/hduser/gutenberg/* -output /user/hduser/gutenberg-output
02 additionalConfSpec_:null
03
null
=@@@userJobConfProps_.get(stream.shipped.hadoopstreaming
04 packageJobJar:
[/app/hadoop/tmp/hadoop-unjar54543/
]
05
[] /tmp/streamjob54544.jar
tmpDir
=null
06
[...
] INFO mapred.FileInputFormat: Total input paths to process : 7
07
[...
] INFO streaming.StreamJob: getLocalDirs():
[/app/hadoop/tmp/mapred/local
]
08
[...
] INFO streaming.StreamJob: Running job: job_200803031615_0021
09
[...
]
10
[...
] INFO streaming.StreamJob: map 0% reduce 0%
11
[...
] INFO streaming.StreamJob: map 43% reduce 0%
12
[...
] INFO streaming.StreamJob: map 86% reduce 0%
13
[...
] INFO streaming.StreamJob: map 100% reduce 0%
14
[...
] INFO streaming.StreamJob: map 100% reduce 33%
15
[...
] INFO streaming.StreamJob: map 100% reduce 70%
16
[...
] INFO streaming.StreamJob: map 100% reduce 77%
17
[...
] INFO streaming.StreamJob: map 100% reduce 100%
18
[...
] INFO streaming.StreamJob: Job
complete: job_200803031615_0021
19
[...
] INFO streaming.StreamJob: Output: /user/hduser/gutenberg-output
20 hduser@ubuntu:/usr/local/hadoop
$
从上面的输出可以看到,Hadoop还为一些统计信息提供了一个基本的网页接口。在Hadoop集群运行时可以打开 http://localhost:50030
查看HDFS路径/user/hduser/gutenberg-output上的文件
你可以通过 dfs -cat命令查看输出文件的内容
01 hduser@ubuntu:/usr/local/hadoop
$ bin/hadoop dfs -cat /user/hduser/gutenberg-output/part-00000
02
"(Lo)cra" 1
03
"1490 1
04
"1498,
" 1
05
"35
" 1
06
"40,
" 1
07
"A 2
08
"AS-IS". 2
09
"A_ 1
10
"Absoluti 1
11
[...
]
12 hduser@ubuntu:/usr/local/hadoop
$
注意上面截图中单词两边的双引号并不是Hadoop自己加上去的,而是python程序将单词分割后生成的,不信的话可以查看完整的输出文件。
使用python的迭代器和生成器改进mapper和reducer代码
上面的例子应该让你明白了怎样构建一个MapReduce应用,不过上面那些代码侧重于易读性,特别是针对Python程序员新手。然而,在真实的应用中,你可能需要使用Python的迭代器和生成器来优化你的代码。
一般来说,迭代器和生成器(产生迭代的函数,比如包含yield输出语句)的优点是只有在你需要使用一个序列的元素时它才会生成该元素。这对于计算量很大或者内存开销很大的任务来说是很有用的。
注意,下面的Map和Reduce脚本只有在Hadoop的环境下才能正常运行,也就是说使用本地的命令“cat Data | ./mapper.py |sort -k1,1 |./reucer.py”并不会正常运行,因为有些函数特性在不能离开Hadoop
mapper.py
01
#!/usr/bin/env python
02
"""A more advanced Mapper, using Python iterators and generators."""
03
04
import
sys
05
06
def
read_input(
file
):
07
for
line
in
file
:
08
# split the line into words
09
yield
line
.
split()
10
11
def
main(
separator
=
'
\t
'
):
12
# input comes from STDIN (standard input)
13
data
=
read_input(
sys
.
stdin)
14
for
words
in
data
:
15
# write the results to STDOUT (standard output);
16
# what we output here will be the input for the
17
# Reduce step, i.e. the input for reducer.py
18
#
19
# tab-delimited; the trivial word count is 1
20
for
word
in
words
:
21
print
'
%s%s%d
'
% (
word
,
separator
,
1)
22
23
if
__name__
==
"__main__"
:
24
main()
reducer.py
01
#!/usr/bin/env python
02
"""A more advanced Reducer, using Python iterators and generators."""
03
04
from
itertools
import
groupby
05
from
operator
import
itemgetter
06
import
sys
07
08
def
read_mapper_output(
file
,
separator
=
'
\t
'
):
09
for
line
in
file
:
10
yield
line
.
rstrip()
.
split(
separator
,
1)
11
12
def
main(
separator
=
'
\t
'
):
13
# input comes from STDIN (standard input)
14
data
=
read_mapper_output(
sys
.
stdin
,
separator
=
separator)
15
# groupby groups multiple word-count pairs by word,
16
# and creates an iterator that returns consecutive keys and their group:
17
# current_word - string containing a word (the key)
18
# group - iterator yielding all ["<current_word>", "<count>"] items
19
for
current_word
,
group
in
groupby(
data
,
itemgetter(
0
)):
20
try
:
21
total_count
=
sum(
int(
count)
for
current_word
,
count
in
group)
22
print
"
%s%s%d
"
% (
current_word
,
separator
,
total_count)
23
except
ValueError
:
24
# count was not a number, so silently discard this item
25
pass
26
27
if
__name__
==
"__main__"
:
28
main()