Apache Pig中文教程(进阶)

本文基于以下环境:
pig 0.8.1

(1)如何编写及使用自定义函数(UDF)
首先给出一个链接:Pig 0.8.1 API,还有Pig UDF Manual。这两个文档能提供很多有用的参考。
自定义函数有何用?这里以一个极其简单的例子来说明一下。
假设你有如下数据:

[root@localhost pig]$ cat a.txt 
uidk	12	3
hfd	132	99
bbN	463	231
UFD	13	10

现在你要将第二列的值先+500,再-300,然后再÷2.6,那么我们可以这样写:

grunt> A = LOAD 'a.txt' AS(col1:chararray, col2:double, col3:int);
grunt> B = FOREACH A GENERATE col1, (col2 + 500 - 300)/2.6, col3;
grunt> DUMP B;
(uidk,81.53846153846153,3)
(hfd,127.6923076923077,99)
(bbN,255.0,231)
(UFD,81.92307692307692,10)

我们看到,对第二列进行了 (col2 + 500 - 300)/2.6 这样的计算。麻烦不?或许这点小意思没什么。但是,如果有比这复杂得多的处理,每次你需要输入多少pig代码呢?我们希望有这样一个函数,可以让第二行pig代码简化如下:

grunt> B = FOREACH A GENERATE col1, com.codelast.MyUDF(col2), col3;

这样的话,对于我们经常使用的操作,岂不是很方便?
pig的UDF(user-defined function)就是拿来做这个的。
文章来源:http://www.codelast.com/
下面,就以IntelliJ这个IDE为例(其实用什么IDE倒无所谓,大同小异吧),说明我们如何实现这样一个功能。
新建一个新工程,在 工程下创建“lib”目录,然后把pig安装包中的“pig-0.8.1-core.jar”文件放置到此lib目录下,然后在“Project Structure→Libraries”下添加(点击“+”号)一个库,就命名为“lib”,然后点击右侧的“Attach Classes”按钮,选择pig-0.8.1-core.jar文件,再点击下方的“Apply”按钮应用此更改。这样做之后,你就可以在IDE的编辑 器中实现输入代码时看到智能提示了。
此外,你还需要用同样的方法,将一堆Hadoop的jar包添加到工程中,包括以下文件:

hadoop-XXX-ant.jar
hadoop-XXX-core.jar
hadoop-XXX-examples.jar
hadoop-XXX-test.jar
hadoop-XXX-tools.jar

其中,XXX是版本号。
如果没有这些文件,你在编译jar包的时候会报错。
文章来源:http://www.codelast.com/
跟我一起,在工程目录下的 src/com/coldelast/ 目录下创建Java源代码文件 MyUDF.java,其内容如下:

package com.codelast;

import java.io.IOException;

import org.apache.pig.EvalFunc;
import org.apache.pig.data.Tuple;

/**
 * Author: Darran Zhang @ codelast.com
 * Date: 2011-09-29
 */

public class MyUDF extends EvalFunc<Double> {

  @Override
  public Double exec(Tuple input) throws IOException {
    if (input == null || input.size() == 0) {
      return null;
    }

    try {
      Double val = (Double) input.get(0);
      val = (val + 500 - 300) / 2.6;
      return val;
    } catch (Exception e) {
      throw new IOException(e.getMessage());
    }
  }
}

在上面的代码中,input.get(0)是获取UDF的第一个参数(可以向UDF传入多个参数);同理,如果你的UDF接受两个参数(例如一个求和的UDF),那么input.get(1)可以取到第二个参数。
然后编写build.xml(相当于C++里面的Makefile),用ant来编译、打包此工程——这里就不把冗长的build.xml写上来了,而且这也不是关键,没有太多意义。
文章来源:http://www.codelast.com/
假定编译、打包得到的jar包名为cl.jar,我们到这里几乎已经完成了大部分工作。下面就看看如何在pig中调用我们刚编写的自定义函数了。

grunt> REGISTER cl.jar;
grunt> A = LOAD 'a.txt' AS(col1:chararray, col2:double, col3:int);    
grunt> B = FOREACH A GENERATE col1, com.codelast.MyUDF(col2), col3;
grunt> DUMP B;
(uidk,81.53846153846153,3)
(hfd,127.6923076923077,99)
(bbN,255.0,231)
(UFD,81.92307692307692,10)

注:第一句是注册你编写的UDF,使用前必须先注册。
从结果可见,我们实现了预定的效果。
UDF大有用途!
注意:对如果你的UDF返回一个标量类型(类似于我上面的例子),那么pig就可以使用反射(reflection)来识别出返回类型。如果你的UDF返 回的是一个包(bag)或一个元组(tuple),并且你希望pig能理解包(bag)或元组(tuple)的内容的话,那么你就要实现 outputSchema方法,否则后果很不好。具体可看这个链接的说明。

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

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

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

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

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

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

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

(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 接口,这个接口类似于下面的样子:

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是怎么写出来的,这里还是用这个功能来说明。
代码如下:

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不得不说的一些话题
如果你要加载一个数据文件,例如:

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

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

public class MyLoadFunc extends LoadFunc implements LoadMetadata {

  public ResourceSchema getSchema(String filename, Job job) throws IOException {
    //TODO:
  }
}

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

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

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


转载于:https://my.oschina.net/u/1170537/blog/384432

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值