前言
在一次利用sqoop將關係型資料庫Oracle中的資料匯入到hive的測試中,出現了一個分割符的問題。oracle中有欄位中含有\n分行符號,由於hive預設是以’\n’作為換行分割符的,所以用sqoop將oracle中資料匯入到hive中導致hive中的資料條目跟未經處理資料庫不一致,當時的處理方式是資料在匯入到HDFS之前,用sqoop的參數將欄位中的分行符號都替換掉。
Sqoop在將資料從關係型資料庫匯入到HDFS時,支援將\n替換成自訂分行符號(支援單字元自訂分行符號),但是在hive中建表時用語句<row format delimited lines terminated by>指定自訂分行符號會提示如下錯誤:
< linesterminated by>參數目前僅支援’\n’。不能指定自訂分行符號,這樣自訂分行符號的資料就不能匯入到hive中,基於以上考慮,本文簡單說明了如何讓hive實現自訂多個字元的換行和欄位分割符,供大家參考。如有不足請批評指正。
環境
Hadoop:2.2 Hive:0.12(星環inceptor,支援原生hive)
目標
分析hive自訂多字串分行符號; 實現hive自訂多字串欄位分隔符號; 實現hivetextinputformat自訂編碼格式的設定。
1.hive的序列化與還原序列化
Hive中,預設使用的是TextInputFormat,一行表示一條記錄。在每條記錄(一行中),預設使用^A分割各個欄位。
在有些時候,我們往往面對多行,結構化的文檔,並需要將其匯入Hive處理,此時,就需要自訂InputFormat、OutputFormat,以及SerDe了。
首先來理清這三者之間的關係,我們直接引用Hive官方說法:
SerDe is a short name for “Serializer and Deserializer.”
Hive uses SerDe (and !FileFormat) to read and write table rows.
HDFS files –> InputFileFormat –> <key, value> –> Deserializer –> Row object
Row object –> Serializer –> <key, value> –> OutputFileFormat –> HDFS files
總結一下,面對一個HDFS上的檔案,Hive將如下處理(以讀為例):
(1) 調用InputFormat,將檔案切成不同的文檔。每篇文檔即一行(Row)。
(2) 調用SerDe的Deserializer,將一行(Row),切分為各個欄位。
當HIVE執行INSERT操作,將Row寫入檔案時,主要調用OutputFormat、SerDe的Seriliazer,順序與讀取相反。
針對含有自訂分行符號和欄位分隔符號的HDFS檔案,本文僅介紹hive讀取的過程的修改。 2 Hive預設採用的TextInputFormat類
首先建一個簡單的表,然後用<describe extended >命令查看該表的詳細資料。
transwarp> create table test1(id int);OKTime taken: 0.062secondstranswarp>describe extended test1;OKid int None Detailed Table Information Table(tableName:test1, dbName:default, owner:root,createTime:1409300219, lastAccessTime:0, retention:0,sd:StorageDescriptor(cols:[FieldSchema(name:id, type:int,comment:null)],location:hdfs://leezq-vm3:8020/inceptor1/user/hive/warehouse/test1,inputFormat:org.apache.hadoop.mapred.TextInputFormat,outputFormat:org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat,compressed:false,numBuckets:-1,serdeInfo:SerDeInfo(name:null,serializationLib:org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe,parameters:{serialization.format=1}),bucketCols:[], sortCols:[], parameters:{},skewedInfo:SkewedInfo(skewedColNames:[], skewedColValues:[],skewedColValueLocationMaps:{}), storedAsSubDirectories:false),partitionKeys:[],parameters:{transient_lastDdlTime=1409300219},viewOriginalText:null, viewExpandedText:null,tableType:MANAGED_TABLE) Time taken: 0.121 seconds, Fetched: 3 row(s)
從上面可以看出,預設狀態下,hive的輸入和輸出調用的類分別為:
inputFormat:org.apache.hadoop.mapred.TextInputFormat,outputFormat:org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat,
雖然現在現在hadoop現在升級到2.X版本,hive依然採用老版的mapred介面。
我們要改寫的就是類TextInputFormat。 2.1類 TextInputFormat
類TextInputFormat在hadoop-mapreduce-client-core-2.2.0.jar中。
重點看類中getRecordReader方法,該方法返回LineRecordReader對象。並且該方法中已經實現了接收自訂字串作為分行符號的代碼,只要建表前在hive的CLI介面上輸入set textinputformat.record.delimiter=<自訂換行字串>;即可實現自訂多字元分行符號。
2.2類LineRecordReader
為了進一步查看其實現原理,我們進一步看LineRecordReader(package org.apache.hadoop.mapred. LineRecordReader)類。
查看該類的建構函式,該類調用org.apache.hadoop.util.LineReader(在包hadoop-common-2.2.0.jar中)擷取每行的資料,把參數recordDelimiter傳給類對象LineReader,類LineReader中的readLine(Text str, int maxLineLength, intmaxBytesToConsume)方法負責按照使用者自訂分隔字元返回每行的長度,如果使用者不設定 textinputformat.record.delimiter的值,recordDelimiter的值為null,這時readLine方法就會按照預設’\n’分割每行。readLine的代碼如下:
通過讀源碼可以看到,原始的hive可以通過設定參數的方法實現多字元自訂分行符號(textFile的儲存方式),通過上圖中readCustomLine方法獲得使用者自訂分行符號的字串實現自動換行,每行最大可支援2147483648大小。但是要想實現自訂多字元的欄位分隔符號和自訂編碼格式的設定,還需要對源碼進行改寫。下面就講一下改寫的步驟。
3 自訂TextInputFormat 實現自訂多字串的欄位分割符 實現自訂編碼格式的設定
首先建一個空的java工程,添加必須的五個包
然後建立兩個類SQPTextInputFormat和SQPRecordReader,將TextInputFormat和LineRecordReader的代碼分別拷貝過來。
在SQPTextInputFormat中添加對自訂編碼格式的設定。(對分行符號的參數進行了更名,將textinputformat.record.delimiter改成了textinputformat.record.linesep)
//====================================================== String delimiter = job.get("textinputformat.record.linesep"); this.encoding = job.get("textinputformat.record.encoding",defaultEncoding); byte[] recordDelimiterBytes = null; if (null != delimiter) {//Charsets.UTF_8 recordDelimiterBytes = delimiter.getBytes(this.encoding); } return new SQPRecordReader(job, (FileSplit)genericSplit, recordDelimiterBytes);
在SQPRecordReader建構函式中添加對欄位分隔符號和編碼格式的設定。
//====================================================== this.FieldSep = job.get("textinputformat.record.fieldsep",defaultFSep);this.encoding = job.get("textinputformat.record.encoding",defaultEncoding);
在SQPRecordReader的next()方法中添加對欄位分割符的替換和對編碼格式的設定。
//====================================================== if (encoding.compareTo(defaultEncoding) != 0) { String str = new String(value.getBytes(), 0,value.getLength(), encoding); value.set(str); } if (FieldSep.compareTo(defaultFSep) != 0) { String replacedValue = value.toString().replace(FieldSep, defaultFSep); value.set(replacedValue);
詳細的代碼如下:
package com.learn.util.hadoop;//import com.google.common.base.Charsets;import java.io.IOException;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.io.compress.SplittableCompressionCodec;import org.apache.hadoop.mapred.*;public class SQPTextInputFormat extends FileInputFormat<LongWritable, Text>implements JobConfigurable{private CompressionCodecFactory compressionCodecs = null;private final static String defaultEncoding = "UTF-8";//"US-ASCII""ISO-8859-1""UTF-8""UTF-16BE""UTF-16LE""UTF-16"private String encoding = null;public void configure(JobConf conf) { this.compressionCodecs = new CompressionCodecFactory(conf);}protected boolean isSplitable(FileSystem fs, Path file) { CompressionCodec codec = this.compressionCodecs.getCodec(file); if (null == codec) { return true; } return codec instanceof SplittableCompressionCodec;}public RecordReader<LongWritable, Text> getRecordReader(InputSplit genericSplit, JobConf job, Reporter reporter) throws IOException{ reporter.setStatus(genericSplit.toString()); String delimiter = job.get("textinputformat.record.linesep"); this.encoding = job.get("textinputformat.record.encoding",defaultEncoding); byte[] recordDelimiterBytes = null; if (null != delimiter) {//Charsets.UTF_8 recordDelimiterBytes = delimiter.getBytes(this.encoding); } return new SQPRecordReader(job, (FileSplit)genericSplit, recordDelimiterBytes);}}
package com.learn.util.hadoop;import java.io.IOException;import java.io.InputStream;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.apache.hadoop.classification.InterfaceAudience.LimitedPrivate;import org.apache.hadoop.classification.InterfaceStability.Unstable;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.fs.Seekable;import org.apache.hadoop.io.LongWritable;import org.apache.hadoop.io.Text;import org.apache.hadoop.io.compress.CodecPool;import org.apache.hadoop.io.compress.CompressionCodec;import org.apache.hadoop.io.compress.CompressionCodecFactory;import org.apache.hadoop.io.compress.Decompressor;import org.apache.hadoop.io.compress.SplitCompressionInputStream;import org.apache.hadoop.io.compress.SplittableCompressionCodec;import org.apache.hadoop.io.compress.SplittableCompressionCodec.READ_MODE;import org.apache.hadoop.util.LineReader;import org.apache.hadoop.mapred.RecordReader;import org.apache.hadoop.mapred.FileSplit;//@InterfaceAudience.LimitedPrivate({"MapReduce", "Pig"})//@InterfaceStability.Unstablepublic class SQPRecordReader implements RecordReader<LongWritable, Text>{ private static final Log LOG = LogFactory.getLog(SQPRecordReader.class.getName()); private CompressionCodecFactory compressionCodecs = null; private long start; private long pos; private long end; private LineReader in; private FSDataInputStream fileIn; private final Seekable filePosition; int maxLineLength; private CompressionCodec codec; private Decompressor decompressor; private String FieldSep;//field separator private static final String defaultFSep="\001"; private final static String defaultEncoding = "UTF-8";//"US-ASCII""ISO-8859-1""UTF-8""UTF-16BE""UTF-16LE""UTF-16" private String encoding = null; public SQPRecordReader(Configuration job, FileSplit split) throws IOException { this(job, split, null); } public SQPRecordReader(Configuration job, FileSplit split, byte[] recordDelimiter) throws IOException { this.maxLineLength = job.getInt("mapreduce.input.linerecordreader.line.maxlength", 2147483647); this.FieldSep = job.get("textinputformat.record.fieldsep",defaultFSep); this.encoding = job.get("textinputformat.record.encoding",defaultEncoding); this.start = split.getStart(); this.end = (this.start + split.getLength()); Path file = split.getPath(); this.compressionCodecs = new CompressionCodecFactory(job); this.codec = this.compressionCodecs.getCodec(file); FileSystem fs = file.getFileSystem(job); this.fileIn = fs.open(file); if (isCompressedInput()) { this.decompressor = CodecPool.getDecompressor(this.codec); if ((this.codec instanceof SplittableCompressionCodec)) { SplitCompressionInputStream cIn = ((SplittableCompressionCodec)this.codec).createInputStream(this.fileIn, this.decompressor, this.start, this.end, SplittableCompressionCodec.READ_MODE.BYBLOCK); this.in = new LineReader(cIn, job, recordDelimiter); this.start = cIn.getAdjustedStart(); this.end = cIn.getAdjustedEnd(); this.filePosition = cIn; } else { this.in = new LineReader(this.codec.createInputStream(this.fileIn, this.decompressor), job, recordDelimiter); this.filePosition = this.fileIn; } } else { this.fileIn.seek(this.start); this.in = new LineReader(this.fileIn, job, recordDelimiter); this.filePosition = this.fileIn; } if (this.start != 0L) { this.start += this.in.readLine(new Text(), 0, maxBytesToConsume(this.start)); } this.pos = this.start; } public SQPRecordReader(InputStream in, long offset, long endOffset, int maxLineLength) { this(in, offset, endOffset, maxLineLength, null); } public SQPRecordReader(InputStream in, long offset, long endOffset, int maxLineLength, byte[] recordDelimiter) { this.maxLineLength = maxLineLength; this.in = new LineReader(in, recordDelimiter); this.start = offset; this.pos = offset; this.end = endOffset; this.filePosition = null; } public SQPRecordReader(InputStream in, long offset, long endOffset, Configuration job) throws IOException { this(in, offset, endOffset, job, null); } public SQPRecordReader(InputStream in, long offset, long endOffset, Configuration job, byte[] recordDelimiter) throws IOException { this.maxLineLength = job.getInt("mapreduce.input.linerecordreader.line.maxlength", 2147483647); this.in = new LineReader(in, job, recordDelimiter); this.start = offset; this.pos = offset; this.end = endOffset; this.filePosition = null; } public LongWritable createKey() { return new LongWritable(); } public Text createValue() { return new Text(); } private boolean isCompressedInput() { return this.codec != null; } private int maxBytesToConsume(long pos) { return isCompressedInput() ? 2147483647 : (int)Math.min(2147483647L, this.end - pos); } private long getFilePosition() throws IOException { long retVal; if ((isCompressedInput()) && (null != this.filePosition)) retVal = this.filePosition.getPos(); else { retVal = this.pos; } return retVal; } public synchronized boolean next(LongWritable key, Text value) throws IOException { while (getFilePosition() <= this.end) { key.set(this.pos); int newSize = this.in.readLine(value, this.maxLineLength, Math.max(maxBytesToConsume(this.pos), this.maxLineLength)); if (newSize == 0) { return false; } if (encoding.compareTo(defaultEncoding) != 0) {String str = new String(value.getBytes(), 0, value.getLength(), encoding);value.set(str);} if (FieldSep.compareTo(defaultFSep) != 0) {String replacedValue = value.toString().replace(FieldSep, defaultFSep);value.set(replacedValue);} this.pos += newSize; if (newSize < this.maxLineLength) { return true; } LOG.info("Skipped line of size " + newSize + " at pos " + (this.pos - newSize)); } return false; } public synchronized float getProgress() throws IOException { if (this.start == this.end) { return 0.0F; } return Math.min(1.0F, (float)(getFilePosition() - this.start) / (float)(this.end - this.start)); } public synchronized long getPos() throws IOException { return this.pos; } public synchronized void close() throws IOException { try { if (this.in != null) this.in.close(); } finally { if (this.decompressor != null) CodecPool.returnDecompressor(this.decompressor); } }}
4 自訂InputFormat的使用
1. 將程式打成jar包,放在/usr/lib/hive/lib和各個節點的/usr/lib/hadoop-mapreduce目錄下。
在hvie的CLI命令列介面可以設定如下參數,分別修改編碼格式、自訂欄位分隔字元和自訂分行符號。
set textinputformat.record.encoding=UTF-8;//"US-ASCII""ISO-8859-1""UTF-8""UTF-16BE""UTF-16LE""UTF-16"set textinputformat.record.fieldsep=,;set textinputformat.record.linesep=|+|;
2. 建表,標示採用的Inputformat和OutputFormat,其中org.apach…noreKeyTextOutputFormat 是hive預設的OutputFormat分隔字元。
create table test(id string,name string)stored asINPUTFORMAT'com.learn.util.hadoop.SQPTextInputFormat'OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
3. Load 語句載入資料 執行個體
測試資料:
測試資料中有一個欄位中含有分行符號。欄位分隔符號和行分隔字元分別為’,’和“|+|”。
分別設定欄位分隔符號和行分割符,並建表指定Inputformat和outputformat如下圖所示,
Select * 查詢如下:
Select count(*)如下:
結果是3行,正確。
Select id from test1如下:
Select name from test1:
Select count(name) from test1:
結果正確。
Select name,id from test1:
Select id,name from test1;
Id和name兩個欄位單獨查沒問題,但是調用mapreduce一起查的時候帶有‘\n’的欄位顯示上出了問題。
Select id,name from test1 where id=13:
單獨查詢每個欄位時候和查詢總行數的時候都是沒問題的,這說明改寫的InputFormat起作用了,上面的出現的NULL問題應該是hive顯示的問題。