Hive支持多分隔符与GBK字符集

近期在做将数据从SFTP拷贝的Hive,SFTP中的文件存储的是从关系型数据库抽出来的数据,字段之间用0x7C0x1C两个字符分割,采用GBK字符集,这些都是公司规定的,不可能改动,字符集问题可以通过指定序列化编码方式适配,但是也有问题,下文会介绍,但是分隔符的问题无法通过指定建表参数解决,因为Hive默认只支持一个分隔符,既然不能通过HIVE命令解决多分隔符问题,那只能通过代码解决,从头开始过一遍HIVE的读写流程。

1、Hive的读写流程

Hive底层使用HDFS存储数据,Hive的查询就是从HDFS读取数据并且将数据转换为行对象,对Hive表的增删改会转换成mapreduce作业,计算完之后再写入HDFS,引用官方的文档,读写流程如下:
HDFS files –> InputFileFormat –> <key, value> –> Deserializer –> Row object
Row object –> Serializer –> <key, value> –> OutputFileFormat –> HDFS files

总结一下,Hive面对一个HDFS文档,查询的时候会做如下操作:

  • 调用InputFileFormat(默认是TextInputFormat)将文档中的每一行都转换为Key-Value形式的键值对;
  • 忽略key(key是偏移量,value才是文本内容),使用反序列化接口将value并结合设置的字段分隔符将文本内容切分成各个字段

写入操作和独处操作类似,Hive默认使用的OutputFileFormat 是HiveIgnoreKeyTextOutputFormat,为了让Hive支持多分隔符和GBK编码,需要重新实现某些类。

2、需求

将GBK编码的文件导入到hive,字段之间的分隔符为0x7C0x1C两个ASCII码,要求文件能正确导入,查询不出现乱码,每个字段均能正确显示,能支持删除部分数据(即支持mapreduce运算)

3、大概思路

想要支持多分隔符,有两种方式,一种是自定义序列化和反序列化类,另一种是自定义InputFileFormat和OutputFileFormat,相比第一种方案,第二种方案会简单一些,第二种的主要思想是:
文件从hdfs读出时,在调用InputFileFormat 中的相应方法时将两个分隔符替换成单个分隔符(具体的分隔符在建表的时候指定,最好是控制字符,我选的是0x1c);
文件写入hdfs时,在调用OutputFileFormat 相应方法时将单个分隔符重新转换成两个分隔符。
GBK字符集也需要在建表的时候一并指定,否则,默认按照utf-8的格式存储,建表语句如下:

create table tablename( columns)  
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe' WITH SERDEPROPERTIES("field.delim"='\034',"serialization.encoding"='GBK')
stored as INPUTFORMAT '自定义的InputForamta类'  
OUTPUTFORMAT'自定义的OutputFormat类';

具体的实现见下文

4、自定义inputFileformat

Hive并没有自己实现InputFilFormat,而是默认使用了hadoop下的mapred对应的TextInputFormat类,自定义InputFileFormat主要包括两个类,入口类需要继承TextInputFormat并实现JobConfigurable接口,数据读取类需要实现RecordReader接口

  • 入口类的代码实现
import java.io.IOException;  
import org.apache.hadoop.io.LongWritable;  
import org.apache.hadoop.io.Text;  
import org.apache.hadoop.mapred.FileSplit;  
import org.apache.hadoop.mapred.InputSplit;  
import org.apache.hadoop.mapred.JobConf;  
import org.apache.hadoop.mapred.JobConfigurable;  
import org.apache.hadoop.mapred.RecordReader;  
import org.apache.hadoop.mapred.Reporter;  
import org.apache.hadoop.mapred.TextInputFormat;

public class WareHouseInputFormat extends TextInputFormat implements  JobConfigurable {  

    public RecordReader<LongWritable, Text> getRecordReader(  
        InputSplit genericSplit, JobConf job, Reporter reporter)  
        throws IOException {  

    reporter.setStatus(genericSplit.toString());  
    return new WarehouseStreamRecoder((FileSplit) genericSplit,job);  
    }  
}
  • 数据读取类的代码实现(网络版本)
Package ***;

import java.io.IOException;
import java.io.InputStream;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionCodecFactory;
import org.apache.hadoop.mapred.FileSplit;
import org.apache.hadoop.util.LineReader;


import org.apache.hadoop.mapred.RecordReader;

public class WarehouseStreamRecoder implements RecordReader<LongWritable, Text> {

    private CompressionCodecFactory compressionCodecs = null;
    private long start;
    private long pos;
    private long end;
    private LineReader lineReader;
    int maxLineLength;

    public WarehouseStreamRecoder(FileSplit inputSplit, Configuration job) throws IOException {
        maxLineLength = job.getInt("mapred.ClickstreamRecordReader.maxlength", Integer.MAX_VALUE);
        start = inputSplit.getStart();
        end = start + inputSplit.getLength();
        final Path file = inputSplit.getPath();
        compressionCodecs = new CompressionCodecFactory(job);
        final CompressionCodec codec = compressionCodecs.getCodec(file);

        // Open file and seek to the start of the split
        FileSystem fs = file.getFileSystem(job);
        FSDataInputStream fileIn = fs.open(file);

        boolean skipFirstLine = false;
        if (codec != null) {
            lineReader = new LineReader(codec.createInputStream(fileIn), job);
            end = Long.MAX_VALUE;
        } else {
            if (start != 0) {
                skipFirstLine = true;
                --start;
                fileIn.seek(start);
            }
            lineReader = new LineReader(fileIn, job);
        }
        if (skipFirstLine) {
            start += lineReader.readLine(new Text(), 0, (int) Math.min((long) Integer.MAX_VALUE, end - start));
        }
        this.pos = start;
    }

    public WarehouseStreamRecoder(InputStream in, long offset, long endOffset, int maxLineLength) {
        this.maxLineLength = maxLineLength;
        this.lineReader = new LineReader(in);
        this.start = offset;
        this.pos = offset;
        this.end = endOffset;
    }

    public WarehouseStreamRecoder(InputStream in, long offset, long endOffset, Configuration job) throws IOException {
        this.maxLineLength = job.getInt("mapred.ClickstreamRecordReader.maxlength", Integer.MAX_VALUE);
        this.lineReader = new LineReader(in, job);
        this.start = offset;
        this.pos = offset;
        this.end = endOffset;
    }

    @Override
    public synchronized void close() throws IOException {
        if (lineReader != null)
            lineReader.close();
    }

    @Override
    public LongWritable createKey() {
        return new LongWritable();
    }

    @Override
    public Text createValue() {
        return new Text();
    }

    @Override
    public synchronized long getPos() throws IOException {
        return pos;
    }

    @Override
    public float getProgress() throws IOException {
        if (start == end) {
            return 0.0f;
        } else {
            return Math.min(1.0f, (pos - start) / (float) (end - start));
        }
    }

    @Override
    public synchronized boolean next(LongWritable key, Text value) throws IOException {

        while (pos < end) {
            key.set(pos);
            int newSize = lineReader.readLine(value, maxLineLength,
                    Math.max((int) Math.min(Integer.MAX_VALUE, end - pos),maxLineLength));

            if (newSize == 0)
                return false;


         //从这里开始   
         byte[] bytes = new byte[] {124,28};
            String filedDeli = new String(bytes); 

            String str = value.toString()
                    .replaceAll(filedDeli, "\034");
            value.set(str); 
        //到这里结束


            pos += newSize;

            if (newSize < maxLineLength)
                return true;
        }

        return false;

    }

}

大部分代码都是模板代码,代码中标记为【从这里开始,到这里结束】的代码完成了字符串的替换,其实如果文件时utf-8编码的话,这种方法是没有问题的,但是如果文件时GBK编码,就不行,主要原因就是执行value.toString()这个函数时,默认采用的是utf-8的编码,会导致中文出现乱码,所以直接这么处理对于GBK编码的文件是不行的。

  • 支持GBK编码的解决方案

在网络上找了很久,都是同样的解决方案,不得不惊讶于大家想法的一致性,既然网络上没有,只能自己实现了,发现Text对象可以拿到字节数组,任何编码方式底层都是以二进制的方式存储数据,这就可以通过操作字符数组的方式把0x7c0x1c连续出现的位置替换成0x1c实现这个功能,大部分代码和网络版本的代码是一样的,只是next函数不同,具体代码如下:

public synchronized boolean next(LongWritable key, Text value) throws IOException {

        while (pos < end) {
            key.set(pos);
            int newSize = lineReader.readLine(value, maxLineLength,
                    Math.max((int) Math.min(Integer.MAX_VALUE, end - pos),maxLineLength));

            if (newSize == 0)
                return false;

            //从这里开始
            byte[] value_byte = value.getBytes(); //获取字节数组
            int length = value_byte.length;
            byte[] new_value_byte = new byte[length];
            int index_new = 0;
            for(int index = 0; index < length-1; index++){

                if(value_byte[index] == 124 && value_byte[index+1] == 28){//完成字符替换
                    new_value_byte[index_new] = 28;                
                    index++;
                }else{
                    new_value_byte[index_new] =value_byte[index];
                }
                index_new++;
            }
            value.clear();
            value.set(new_value_byte,0,index_new);//重新设置value
            //到这里结束


            pos += newSize;
            if (newSize < maxLineLength)
                return true;
        }

        return false;
    }

通过以上操作就就可以满足需求中的第一部分,即:查询不出现乱码,每个字段均能正确显示。

5、自定义OutputFileFormat

网络上有很多自定义InputFormat的例子,但是自定义OutputFormat的例子却很少,不知道为啥,可能是没有重新整理数据的需求,从网络上找了一个例子,具体可见https://www.coder4.com/archives/4031, 但是无奈不能满足我的要求,所以只能求助源码,我们知道Hive有自己默认的OutputFormat类,叫HiveIgnoreKeyTextOutputFormat,这个类在单分隔符的情况下可以正常操作GBK字符集的文件,仿照这个类就可以实现我要的功能。
这个类也主要包括两个部分,入口和数据写入hdfs,入口类需要继承TextOutputFormat并实现HiveOutputFormat接口,数据写入需要实现RecordWriter接口,具体的代码实现如下:

  • 入口类的代码
package ***;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Properties;

//import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hive.ql.exec.FileSinkOperator.RecordWriter;
import org.apache.hadoop.hive.ql.io.HiveOutputFormat;
import org.apache.hadoop.hive.ql.exec.Utilities;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.TextOutputFormat;
import org.apache.hadoop.util.Progressable;

@SuppressWarnings({ "rawtypes" })
public class WareHouseOutputFormat<K extends WritableComparable, V extends Writable>
        extends TextOutputFormat<K, V> implements HiveOutputFormat<K, V> {

public RecordWriter getHiveRecordWriter(JobConf job, Path outPath, Class<? extends Writable> valueClass,
            boolean isCompressed, Properties tableProperties, Progressable progress) throws IOException {

    int rowSeparator = 0;
    String rowSeparatorString = tableProperties.getProperty("line.delim", "\n");
        try {
            rowSeparator = Byte.parseByte(rowSeparatorString);
        } catch (NumberFormatException e) {
            rowSeparator = rowSeparatorString.charAt(0);
        }
        final int finalRowSeparator = rowSeparator;

        FileSystem fs = outPath.getFileSystem(job);

        final OutputStream outStream = Utilities.createCompressedStream(job, fs.create(outPath, progress),
                isCompressed);

//        FSDataOutputStream out = fs.create(outPath);

        return new WarehouseRecoderWriter(outStream,finalRowSeparator);

    }

}
  • 数据写入类的代码实现
package ***;

import java.io.IOException;
import java.io.OutputStream;

//import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.hive.ql.exec.FileSinkOperator.RecordWriter;
import org.apache.hadoop.io.Writable;

import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;


public class WarehouseRecoderWriter implements RecordWriter {

    private OutputStream out;
    private int RowSeparator;
    private int byteCount = 0;
    public WarehouseRecoderWriter(OutputStream o, int sp) {
        this.out = o;
        this.RowSeparator = sp;
    }

    @Override
    public void close(boolean arg0) throws IOException {
        out.flush();
        out.close();    
    }

    @Override
    public synchronized void write(Writable wr) throws IOException {
        if (wr instanceof Text){
            Text tr = (Text)wr;
            byte[] value_byte = tr.getBytes();
            byte[] new_byte = fieldDeli(value_byte);
            out.write(new_byte, 0, byteCount);

        }else{
            BytesWritable bw = (BytesWritable)wr;
            byte[] value_byte = bw.getBytes();
            byte[] new_byte = fieldDeli(value_byte);
            out.write(new_byte, 0, byteCount);
        }

    }

    private byte[] fieldDeli(byte[] bt) throwsIOException {
        int len = bt.length;
        byte[] result = new byte[len+500];

        int index_new = 0;

        //将分隔符1c转为7c1c
        for(int index = 0; index < len-1; index++){
            if(bt[index] == 28){
                result[index_new] = 124;
                index_new++;
                result[index_new] = 28;
            }else{
                result[index_new] = bt[index];
            }
            index_new++;
        }
        result[index_new] = 124;
        index_new++;
        result[index_new] = 28;
        index_new++;
        result[index_new] = (byte)RowSeparator;
        byteCount = index_new+1;
        return result;

    }

}

大部分代码是HiveIgnoreKeyTextOutputFormat的实现,从write函数可以看出hive的设计者们也是将流的内容转为字节处理,而不是直接使用字符串处理,这是应该值得学习的地方,当然,如果编码确定,直接进行字符串处理也不会有什么问题,fieldDeli函数实现了字符串替换,这个过程是读出的逆过程。
通过这个类就就可以满足需求中的第二部分,即:能支持删除部分数据(即支持mapreduce运算),处理结果能正常被hive读取。

6、将自定义的类加入到Hive

将自定义类加到Hive的运行环境的方法有很多,首先我们需要将写的自定义输入输出类打成jar包,将第三方jar包加入Hive可以参考文章https://blog.csdn.net/qianshangding0708/article/details/50381966, 因为项目要求要使用jdbc的方式连接hive取数,所以需要使用的是服务端有效的方式,我选择文中介绍的第二种方法,将jar包拷贝到所有Hive节点的${HIVE_HOME}/auxlib目录,重启Hive即可。

7、写在后面的话

  • 本文所实现的方式并不是严格意义上的支持多分隔符,只是进行字符串替换,将多分隔符(对应本文的7c1c)的行对象转为hive认识的单分隔符(对应本文的1c)行对象,所以建表的时候还是要指定为单分隔符(对应本文的1c),并且最好使用控制字符;
  • 现在的类只支持在7c1c和1c之间进行转换,可以对类进行扩展,通过设置job参数指定文件实际分隔符(对应本文的7c1c)和表分隔符(对应本文的1c),在类中读取job参数进行动态适配。
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值