Apache Pig的一些基础概念及用法总结3(转)

(18)LOAD数据时,如何一次LOAD多个目录下的数据
例如,我要LOAD两个HDFS目录下的数据:/abc/2010 和 /abc/2011,则我们可以这样写LOAD语句:

1
A = LOAD '/abc/201{0,1}' ;

(19)怎样自己写一个UDF中的加载函数(load function)
加载函数(load function)是干什么的?
先举一个很简单的例子,来说明load function的作用。
假设有如下数据:

1
2
3
4
[root@localhost pig] # cat a.txt
1,2,3
a,b,c
9,5,7

我们知道,pig默认是以tab作为分隔符来加载数据的,所以,如果你没有指定分隔符的话,将使得每一行都被认为只有一个字段:

1
2
3
4
5
grunt> B = FOREACH A GENERATE $0;
grunt> DUMP B;
(1,2,3)
(a,b,c)
(9,5,7)

而我们想要以逗号作为分隔符,则应该使用pig内置函数PigStorage

1
A = LOAD 'a.txt' using PigStorage( ',' );

这样的话,我们再用上面的方法DUMP B,得到的结果就是:

1
2
3
(1)
(a)
(9)

这个例子实在太简单了,在这里,PigStorage这个函数就是一个加载函数(load function)。
定义:

Load/Store Functions
 
These user-defined functions control how data goes into Pig and comes out of Pig. Often, the same function handles both input and output but that does not have to be the case.

即:加载函数定义了数据如何流入和流出pig。一般来说,同一函数即处理输入数据,又处理输出数据,但并不是必须要这样。
有了这个定义,就很好理解加载函数的作用了。再举个例子:你在磁盘上保存了只有你自己知道怎么读取其格式的数据(例如,数据是按一定规则加密过的,只有你知道如何解密成明文),那么,你想用pig来处理这些数据,把它们转换成一个个字段的明文时,你就必须要有这样一个加载函数(load function),来进行LOAD数据时的转换工作。这就是加载函数(load function)的作用。
文章来源:http://www.codelast.com/
知道了load function是干嘛的,现在怎么写一个load function?如果你看的是这个链接的UDF手册:Pig Wiki UDF Manual中,会发现它是这样说的——
加载函数必须要实现 LoadFunc 接口,这个接口类似于下面的样子:

01
02
03
04
05
06
07
08
09
10
public interface LoadFunc {
     public void bindTo(String fileName, BufferedPositionedInputStream is, long offset, long end) throws IOException;
     public Tuple getNext() throws IOException;
     // conversion functions
     public Integer bytesToInteger( byte [] b) throws IOException;
     public Long bytesToLong( byte [] b) throws IOException;
     ......
     public void fieldsToRead(Schema schema);
     public Schema determineSchema(String fileName, ExecType execType, DataStorage storage) throws IOException;
}

其中:

  • bindTo函数在pig任务开始处理数据之前被调用一次,它试图将函数与输入数据关联起来。
  • getNext函数读取输入的数据流并构造下一个元组(tuple)。当完成数据处理时该函数会返回null,当该函数无法处理输入的元组(tuple)时它会抛出一个IOException异常。
  • 接下来就是一批转换函数,例如bytesToInteger,bytesToLong等。这些函数的作用是将数据从bytearray转换成要求的类型。
  • fieldsToRead函数被保留作未来使用,应被留空。
  • determineSchema函数对不同的loader应有不同的实现:对返回真实数据类型(而不是返回bytearray字段)的loader,必须要实现该函数;其他类型的loader只要将determineSchema函数返回null就可以了。

但是,如果你在IDE中import了pig 0.8.1的jar包“pig-0.8.1-core.jar”,会发现 LoadFunc 根本不是一个接口(interface),而是一个抽象类(abstract class),并且要实现的函数也与该文档中所说的不一致。因此,只能说是文档过时了。
所以,要看文档的话,还是要看这个Pig UDF Manual,这里面的内容才是对的。
同时,我也推荐另外一个关于Load/Store Function的链接:《Programming Pig》Chapter 11. Writing Load and Store Functions。这本书很好很强大。

开始写一个loader。我们现在写一个①中所描述的、可以按逗号分隔符加载数据文件的loader——PigStorage已经有这个功能了,不过为了演示loader是怎么写出来的,这里还是用这个功能来说明。
代码如下:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.codelast.udf.pig;
 
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.*;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.pig.*;
import org.apache.pig.backend.executionengine.ExecException;
import org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.*;
import org.apache.pig.data.*;
 
import java.io.IOException;
import java.util.*;
 
/**
  * A loader class of pig.
  *
  * @author Darran Zhang (codelast.com)
  * @version 11-10-11
  * @declaration These codes are only for non-commercial use, and are distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.
  * You must not remove this declaration at any time.
  */
 
public class MyLoader extends LoadFunc {
   protected RecordReader recordReader = null ;
 
   @Override
   public void setLocation(String s, Job job) throws IOException {
     FileInputFormat.setInputPaths(job, s);
   }
 
   @Override
   public InputFormat getInputFormat() throws IOException {
     return new PigTextInputFormat();
   }
 
   @Override
   public void prepareToRead(RecordReader recordReader, PigSplit pigSplit) throws IOException {
     this .recordReader = recordReader;
   }
 
   @Override
   public Tuple getNext() throws IOException {
     try {
       boolean flag = recordReader.nextKeyValue();
       if (!flag) {
         return null ;
       }
       Text value = (Text) recordReader.getCurrentValue();
       String[] strArray = value.toString().split( "," );
       List lst = new ArrayList<String>();
       int i = 0 ;
       for (String singleItem : strArray) {
         lst.add(i++, singleItem);
       }
       return TupleFactory.getInstance().newTuple(lst);
     } catch (InterruptedException e) {
       throw new ExecException( "Read data error" , PigException.REMOTE_ENVIRONMENT, e);
     }
   }
}

如上,你的loader类要继承自LoadFunc虚类,并且需要重写它的4个方法。其中,getNext方法是读取数据的方法,它做了读取出一行数据、按逗号分割字符串、构造一个元组(tuple)并返回的事情。这样我们就实现了按逗号分隔符加载数据的loader。
文章来源:http://www.codelast.com/
关于load function不得不说的一些话题
如果你要加载一个数据文件,例如:

1
A = LOAD 'myfile' AS (col1:chararray, col2: int );

假设此文件的结构不复杂,你可以手工写 AS 语句,但如果此文件结构特别复杂,你总不可能每次都手工写上几十个甚至上百个字段名及类型定义吧?
这个时候,如果我们可以让pig从哪里读出来要加载的数据的schema(模式),就显得特别重要了。
在实现load function的时候,我们是通过实现 LoadMetadata 这个接口中的 getSchema 方法来做到这一点的。例如:

1
2
3
4
5
6
public class MyLoadFunc extends LoadFunc implements LoadMetadata {
 
   public ResourceSchema getSchema(String filename, Job job) throws IOException {
     //TODO:
   }
}

实现了 getSchema 方法之后,在pig脚本中加载数据的时候,就可以无需编写 AS 语句,就可以使用你在 getSchema 方法中指定的模式了。例如:

1
2
3
4
REGISTER 'myUDF.jar' ;
A = LOAD 'myfile' USING com.codelast.MyLoadFunc();
B = foreach A generate col1;
SOTRE B INTO 'output' ;

看清楚了,在 LOAD 的时候,我们并没有写 AS 语句来指定字段名,但是在后面的 FOREACH 中,我们却可以使用 col1 这样的字段名,这正是因为 getSchema 方法的实现为我们做到了这一点。在数据文件的结构特别复杂的时候,这个功能几乎是不可或缺的,否则难以想像会给分析数据的人带来多大的不便。

(20)重载(overloading)一个UDF
类似于C++的函数重载,pig中也可以重载UDF,例如一个函数ADD可以对两个int进行操作,也可以对两个double进行操作,那么我们可以为该函数实现 getArgToFuncMapping 方法,该函数返回一个 List<FuncSpec> 对象,这个对象中包含了参数的类型信息。具体怎么实现,可以看这个链接(搜索“Overloading UDFs”定位到所需章节)。

(21)pig运行不起来,提示“org.apache.hadoop.ipc.Client – Retrying connect to server: localhost/127.0.0.1:9000. Already tried 1 time(s)”错误的解决办法
发生这个错误时,请先检查Hadoop的各个进程是否都运行起来了,例如,在我的一次操作中,遇到这个错误时,我发现Hadoop namenode进程没有启动起来:

1
ps -ef | grep java | grep NameNode

应该有两个进程启动起来了:

org.apache.hadoop.hdfs.server.namenode.NameNode
org.apache.hadoop.hdfs.server.namenode.SecondaryNameNode

如果没有,那么你要到Hadoop安装目录下的“logs”目录下,查看NameNode的日志记录文件(视用户不同,日志文件的名字也会有不同),例如,我的NameNone日志文件 hadoop–namenode-root-XXX.log 的末尾,显示出有如下错误:

ERROR org.apache.hadoop.hdfs.server.namenode.NameNode: org.apache.hadoop.hdfs.server.common.InconsistentFSStateException: Directory /tmp/hadoop-root/dfs/name is in an inconsistent state: storage directory does not exist or is not accessible.

文章来源:http://www.codelast.com/
我到它提示的地方一看,果然不存在最后一级目录(我是伪分布式运行的Hadoop,不要觉得奇怪),于是手工创建了这个目录,然后停掉Hadoop:

1
stop-all.sh

稍候一会儿再重新启动Hadoop:

1
start-all.sh

然后再去看一下NameNode的日志,又发现了新的错误信息:

ERROR org.apache.hadoop.hdfs.server.namenode.NameNode: java.io.IOException: NameNode is not formatted.

这表明NameNode没有被格式化。于是将其格式化:

1
[root@localhost bin] # hadoop namenode -format

命令行问你是否要格式化的时候,选择YES即可。格式化完后会提示:

common.Storage: Storage directory /tmp/hadoop-root/dfs/name has been successfully formatted.

说明成功了。这个时候,再像前面一样重启Hadoop进程,再去看NameNode的日志文件的最后若干行,应该会发现前面的那些错误提示没了。这个时候,再检查Hadoop各进程是否都成功地启动了,如果是的话,那么这个时候你就可以在Hadoop的伪分布式模式下启动pig:

1
[root@localhost home] # pig

而不用以本地模式来运行pig了(pig -x local)。
总之,配置一个伪分布式的Hadoop来调试pig在某些情况下是很有用的,但是比较麻烦,因为还牵涉到Hadoop的正确配置,但是最好搞定它,以后大有用处啊。

(22)用含有null的字段来GROUP,结果会如何
假设有数据文件 a.txt 内容为:

1
2
3
4
1 2 5
1   3
1 3
6 9 8

其中,每两列数据之间是用tab分割的,第二行的第2列、第三行的第3列没有内容(也就是说,加载到Pig里之后,对应的数据会变成null),如果把这些数据按第1、第2列来GROUP的话,第1、2列中含有null的行会被忽略吗?
来做一下试验:

1
2
3
A = LOAD 'a.txt' AS (col1: int , col2: int , col3: int );
B = GROUP A BY (col1, col2);
DUMP B;

输出结果为:

1
2
3
4
((1,2),{(1,2,5)})
((1,3),{(1,3,)})
((1,),{(1,,3)})
((6,9),{(6,9,8)})

从上面的结果(第三行)可见,原数据中第1、2列里含有null的行也被计入在内了,也就是说,GROUP操作是不会忽略null的,这与COUNT有所不同(见本文前面的部分)。

(23)如何统计数据中某些字段的组合有多少种
假设有如下数据:

1
2
3
4
5
[root@localhost] # cat a.txt
1 3 4 7
1 3 5 4
2 7 0 5
9 8 6 6

现在我们要统计第1、2列的不同组合有多少种,对本例来说,组合有三种:

1
2
3
1 3
2 7
9 8

也就是说我们要的答案是3。
用Pig怎么计算?
文章来源:http://www.codelast.com/
先写出全部的Pig代码:

1
2
3
4
5
A = LOAD 'a.txt' AS (col1: int , col2: int , col3: int , col4: int );
B = GROUP A BY (col1, col2);
C = GROUP B ALL ;
D = FOREACH C GENERATE COUNT (B);
DUMP D;

然后再来看看这些代码是如何计算出上面的结果的:
第一行代码加载数据,没什么好说的。
第二行代码,得到第1、2列数据的所有组合。B的数据结构为:

1
2
grunt> DESCRIBE B;
B: { group : (col1: int ,col2: int ),A: {col1: int ,col2: int ,col3: int ,col4: int }}

把B DUMP出来,得到:

1
2
3
((1,3),{(1,3,4,7),(1,3,5,4)})
((2,7),{(2,7,0,5)})
((9,8),{(9,8,6,6)})

非常明显,(1,3),(2,7),(9,8)的所有组合已经被排列出来了,这里得到了若干行数据。下一步我们要做的就是统计这样的数据一共有多少行,也就得到了第1、2列的组合有多少组。
第三和第四行代码,就实现了统计数据行数的功能。参考本文前面部分的“怎样统计数据行数”一节。就明白这两句代码是什么意思了。
这里需要特别说明的是:
a)为什么倒数第二句代码中是COUNT(B),而不是COUNT(group)?
我们是对C进行FOREACH,所以要先看看C的数据结构:

1
2
grunt> DESCRIBE C;
C: { group : chararray,B: { group : (col1: int ,col2: int ),A: {col1: int ,col2: int ,col3: int ,col4: int }}}

可见,你可以把C想像成一个map的结构,key是一个group,value是一个包(bag),它的名字是B,这个包中有N个元素,每一个元素都对应到②中所说的一行。根据②的分析,我们就是要统计B中元素的个数,因此,这里当然就是COUNT(B)了。
b)COUNT函数的作用是统计一个包(bag)中的元素的个数:

COUNT
Computes the number of elements in a bag.
从C的数据结构看,B是一个bag,所以COUNT函数是可以用于它的。
如果你试图把COUNT应用于一个非bag的数据结构上,会发生错误,例如:
1
java.lang.ClassCastException: org.apache.pig.data.BinSedesTuple cannot be cast to org.apache.pig.data.DataBag

这是把Tuple传给COUNT函数时发生的错误。

(24)两个整型数相除,如何转换为浮点型,从而得到正确的结果
这个问题其实很傻,或许不用说你也知道了:假设有int a = 3 和 int b = 2两个数,在大多数编程语言里,a/b得到的是1,想得到正确结果1.5的话,需要转换为float再计算。在Pig中其实和这种情况一样,下面就拿几行数据来做个实验:

1
2
3
[root@localhost ~]# cat a.txt
3 2
4 5

在Pig中:

1
2
3
4
5
grunt> A = LOAD 'a.txt' AS (col1: int , col2: int );
grunt> B = FOREACH A GENERATE col1/col2;
grunt> DUMP B;
(1)
(0)

可见,不加类型转换的计算结果是取整之后的值。
那么,转换一下试试:

1
2
3
4
5
grunt> A = LOAD 'a.txt' AS (col1: int , col2: int );
grunt> B = FOREACH A GENERATE ( float )(col1/col2);
grunt> DUMP B;
(1.0)
(0.0)

这样转换还是不行的,这与大多数编程语言的结果一致——它只是把取整之后的数再转换为浮点数,因此当然是不行的。
文章来源:http://www.codelast.com/
正确的做法应该是:

1
2
3
4
5
grunt> A = LOAD 'a.txt' AS (col1: int , col2: int );
grunt> B = FOREACH A GENERATE ( float )col1/col2;
grunt> DUMP B;
(1.5)
(0.8)

或者这样也行:

1
2
3
4
5
grunt> A = LOAD 'a.txt' AS (col1: int , col2: int );
grunt> B = FOREACH A GENERATE col1/( float )col2;
grunt> DUMP B;
(1.5)
(0.8)

这与我们的通常做法是一致的,因此,你要做除法运算的时候,需要注意这一点。

(25)UNION的一个例子
假设有两个数据文件为:

1
2
3
4
5
6
7
8
[root@localhost ~] # cat 1.txt
0 3
1 5
0 8
 
[root@localhost ~] # cat 2.txt
1 6
0 9

现在要求出:在第一列相同的情况下,第二列的和分别为多少?
例如,第一列为 1 的时候,第二列有5和6两个值,和为11。同理,第一列为0的时候,第二列的和为 3+8+9=20。
计算此问题的Pig代码如下:

1
2
3
4
5
6
A = LOAD '1.txt' AS (a: int , b: int );
B = LOAD '2.txt' AS (c: int , d: int );
C = UNION A, B;
D = GROUP C BY $0;
E = FOREACH D GENERATE FLATTEN( group ), SUM (C.$1);
DUMP E;

输出为:

1
2
(0,20)
(1,11)

文章来源:http://www.codelast.com/
我们来看看每一步分别做了什么:
第1行、第2行代码分别加载数据到关系A、B中,没什么好说的。
第3行代码,将关系A、B合并起来了。合并后的数据结构为:

1
2
grunt> DESCRIBE C;
C: {a: int ,b: int }

其数据为:

1
2
3
4
5
6
grunt> DUMP C;
(0,3)
(1,5)
(0,8)
(1,6)
(0,9)

第4行代码按第1列(即$0)进行分组,分组后的数据结构为:

1
2
grunt> DESCRIBE D;
D: { group : int ,C: {a: int ,b: int }}

其数据为:

1
2
3
grunt> DUMP D;
(0,{(0,9),(0,3),(0,8)})
(1,{(1,5),(1,6)})

最后一行代码,遍历D,将D中每一行里的所有bag(即C)的第2列(即$1)进行累加,就得到了我们要的结果。

转载:http://www.codelast.com/


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值