大量csv数据的查询和计算的技术实现路径分析(1)

在工作中,会有处理很多数据的场景。

比如,

用户需求:

我有大量数据,

(1)我想要高效查询我想要的数据。

(2)我想要高效计算出我想要的结果数据。

为了存储数据,我们使用的数据存储方式有

mysql,oracle,表;

csv文件

excel文件

hdfs文件系统

hbase

redis,ehcache

把数据从存储区拿出来作分析,我们使用的数据分析手段有

java单机计算,分布式计算

hadoop集群

spark集群

redis集群


利用以上的手段,怎么做?

当然用户需求不是这么简单的2句话,本人也不是要写一本书来概括这些技术实现方案,相反,这些不是本文的重点,本文的重点是,以自己的经验,提出一个可实现流程,然后分析每个环节可行性和难点。这是本文将要写的主要内容。

先细化一下需求,目前需求只有一个,就是:
有很多csv文件数据,如何高效查询csv数据?

那么,为实现上面的需求,需要做一些技术可行性的探讨。
目录如下:

  • 目录
    • Java Split探讨
    • 利用Hdfs存取探讨
    • 利用Hbase存取探讨
    • 利用Spark计算csv数据探讨

目录

Java Split探讨

利用Hdfs存取探讨

利用Hbase存取探讨

利用Spark计算csv数据探讨

1.Java Split探讨
为什么首先要探讨split呢?

首先说到java string,string 对象一旦创建,就是不可变的,类似:

String str = "a";

str = "b";

其实是创建了2个对象。

第二,split() 方法,解析split方法的实现时发现,split切分的生成每个substring(子字符串)实际引用是初始字符串对象的char[],比如:

String str = "a,b,c,d";

String[] strArr = str.split(",");

int strArrLen = strArr.length();

strArrLen=4,就是说会在内存中增加4个str大小的内存消耗。

初始字符串越长,split方法的内存消耗就越大。在实际应用中,会增加内存溢出的风险(java.lang.OutOfMemoryError)。

实际上,我们就遇到了。我们一个csv文件的列有几千个,使用split切分列,随着文件的增多,就导致了以下2个问题:

(1)内存消耗很大

(2)内存不会及时清理

做个计算便知:
假设一个csv文件有2000列,平均每列10个字符,每个字符占2个字节,
eachLineSize:2000×10×2/1024=39kB;
一万行:
39×10000/1024=381M
这个内存消耗大小是比较大的。

可是,使用java处理csv,是少不了字符串处理的。

有同事提出调大jvm内存,虽然短期内有效果,但风险是比较高的。

针对这个问题,笔者查找了一些资料,也对string,split等做了一些研究,发现,这个是有解决方法的(意料之中的,毕竟string,csv处理以前就有这样的需求,那么就会有相应的解决方案)。

方法1

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class CSVUtils {

    private static final char DEFAULT_SEPARATOR = ',';
    private static final char DEFAULT_QUOTE = '"';

    public static void main(String[] args) throws Exception {

        String csvFile = "E:\\helloworld\\JavaTest\\testcsv\\csv2312.csv";

        Scanner scanner = new Scanner(new File(csvFile));
        while (scanner.hasNext()) {
            List line = parseLine(scanner.nextLine());
            System.out.println("[line0= " + line.get(0) + ", line1= " + line.get(1) + " , line2=" + line.get(2) + "]");
        }
        scanner.close();

    }

    public static List parseLine(String cvsLine) {
        return parseLine(cvsLine, DEFAULT_SEPARATOR, DEFAULT_QUOTE);
    }

    public static List parseLine(String cvsLine, char separators) {
        return parseLine(cvsLine, separators, DEFAULT_QUOTE);
    }

    public static List parseLine(String cvsLine, char separators, char customQuote) {

        List result = new ArrayList<>();

        //if empty, return!
        if (cvsLine == null && cvsLine.isEmpty()) {
            return result;
        }

        if (customQuote == ' ') {
            customQuote = DEFAULT_QUOTE;
        }

        if (separators == ' ') {
            separators = DEFAULT_SEPARATOR;
        }

        StringBuffer curVal = new StringBuffer();
        boolean inQuotes = false;
        boolean startCollectChar = false;
        boolean doubleQuotesInColumn = false;

        char[] chars = cvsLine.toCharArray();

        for (char ch : chars) {

            if (inQuotes) {
                startCollectChar = true;
                if (ch == customQuote) {
                    inQuotes = false;
                    doubleQuotesInColumn = false;
                } else {

                    //Fixed : allow "" in custom quote enclosed
                    if (ch == '\"') {
                        if (!doubleQuotesInColumn) {
                            curVal.append(ch);
                            doubleQuotesInColumn = true;
                        }
                    } else {
                        curVal.append(ch);
                    }

                }
            } else {
                if (ch == customQuote) {

                    inQuotes = true;

                    //Fixed : allow "" in empty quote enclosed
                    if (chars[0] != '"' && customQuote == '\"') {
                        curVal.append('"');
                    }

                    //double quotes in column will hit this!
                    if (startCollectChar) {
                        curVal.append('"');
                    }

                } else if (ch == separators) {

                    result.add(curVal.toString());

                    curVal = new StringBuffer();
                    startCollectChar = false;

                } else if (ch == '\r') {
                    //ignore LF characters
                    continue;
                } else if (ch == '\n') {
                    //the end, break!
                    break;
                } else {
                    curVal.append(ch);
                }
            }

        }

        result.add(curVal.toString());

        return result;
    }

}

这里核心思路是用stringbuffer.append() 每2个逗号之间的字符,然后用list.add()。

由于StringBuffer 是可变长度,不会增加其它的StringBuffer 对象,而且切分string 没有沿用String.subString会直接复用初始string的思路,所以整体内存占用是比较小的。

方法2
为使用方便,笔者找到一个工具类,地址:

http://opencsv.sourceforge.net/

依赖:


        com.opencsv
        opencsv
        3.8
    

这个工具类的思路实际跟上面的思路一样,不过在最后一行,会把list转成string[] 数组。比如:

 public class CSVReaderExample {
    /**   
     * @Title: main   
     * @Description: TODO(测试opencsv工具类)   
     * @param: @param args      
     * @return: void      
     * @throws   
     */
    public static void main(String[] args) {
        String csvFile = "E:\\helloworld\\JavaTest\\testcsv\\csv2312.csv";

        CSVReader reader = null;
        try {
            reader = new CSVReader(new FileReader(csvFile));
            String[] line;

            while ((line = reader.readNext()) != null) {
                System.out.println("[line0= " + line[0] + ", line1= " + line[1] + " , line2=" + line[2] + "]");              
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            reader.close();
        }
    }
 }

reader.readNext() 方法代码如下:

/*** Reads the next line from the buffer and converts to a string array.
 *
 * @return A string array with each comma-separated element as a separate
 * entry.
 * @throws IOException If bad things happen during the read
 */
public String[] readNext() throws IOException {

    String[] result = null;
    do {
        String nextLine = getNextLine();
        if (!hasNext) {
            return validateResult(result);
        }
        String[] r = parser.parseLineMulti(nextLine);
        if (r.length > 0) {
            if (result == null) {
                result = r;
            } else {
                result = combineResultsFromMultipleReads(result, r);
            }
        }
    } while (parser.isPending());
    return validateResult(result);
}

parseLineMulti() 方法实现如下:

/** * Parses an incoming String and returns an array of elements.
 *
 * @param nextLine The string to parse
 * @param multi Does it take multiple lines to form a single record.
 * @return The comma-tokenized list of elements, or null if nextLine is null
 * @throws IOException If bad things happen during the read
 */
protected String[] parseLine(String nextLine, boolean multi) throws IOException {

    if (!multi && pending != null) {
        pending = null;
    }

    if (nextLine == null) {
        if (pending != null) {
            String s = pending;
            pending = null;
            return new String[]{s};
        }
        return null;
    }

    List tokensOnThisLine = new ArrayList();
    StringBuilder sb = new StringBuilder(nextLine.length() + READ_BUFFER_SIZE);
    boolean inQuotes = false;
    boolean fromQuotedField = false;
    if (pending != null) {
        sb.append(pending);
        pending = null;
        inQuotes = !this.ignoreQuotations;//true;
    }
    for (int i = 0; i < nextLine.length(); i++) {

        char c = nextLine.charAt(i);
        if (c == this.escape) {
            if (isNextCharacterEscapable(nextLine, inQuotes(inQuotes), i)) {
                i = appendNextCharacterAndAdvanceLoop(nextLine, sb, i);
            }
        } else if (c == quotechar) {
            if (isNextCharacterEscapedQuote(nextLine, inQuotes(inQuotes), i)) {
                i = appendNextCharacterAndAdvanceLoop(nextLine, sb, i);
            } else {

                inQuotes = !inQuotes;
                if (atStartOfField(sb)) {
                    fromQuotedField = true;
                }

                // the tricky case of an embedded quote in the middle: a,bc"d"ef,g
                if (!strictQuotes) {
                    if (i > 2 //not on the beginning of the line
                            && nextLine.charAt(i - 1) != this.separator //not at the beginning of an escape sequence
                            && nextLine.length() > (i + 1) &&
                            nextLine.charAt(i + 1) != this.separator //not at the   end of an escape sequence
                            ) {

                        if (ignoreLeadingWhiteSpace && sb.length() > 0 && StringUtils.isWhitespace(sb)) {
                            sb.setLength(0);
                        } else {
                            sb.append(c);
                        }

                    }
                }
            }
            inField = !inField;
        } else if (c == separator && !(inQuotes && !ignoreQuotations)) {
            tokensOnThisLine.add(convertEmptyToNullIfNeeded(sb.toString(), fromQuotedField));
            fromQuotedField = false;
            sb.setLength(0);
            inField = false;
        } else {
            if (!strictQuotes || (inQuotes && !ignoreQuotations)) {
                sb.append(c);
                inField = true;
                fromQuotedField = true;
            }
        }

    }
    // line is done - check status
    if ((inQuotes && !ignoreQuotations)) {
        if (multi) {
            // continuing a quoted section, re-append newline
            sb.append('\n');
            pending = sb.toString();
            sb = null; // this partial content is not to be added to field list yet
        } else {
            throw new IOException("Un-terminated quoted field at end of CSV line");
        }
        if (inField) {
            fromQuotedField = true;
        }
    } else {
        inField = false;
    }

    if (sb != null) {
        tokensOnThisLine.add(convertEmptyToNullIfNeeded(sb.toString(), fromQuotedField));
    }
    return tokensOnThisLine.toArray(new String[tokensOnThisLine.size()]);

}

opencsv工具类中的实现,相比上一种思路,处理的更为严谨,封装性也比较好。值得推荐。

不过笔者有一个疑问,就是上面parseLineMulti() 方法代码中,

for (int i = 0; i < nextLine.length(); i++) {

    //nextLine 是csv每一行的字符串

}

如果nextLine 是很长的,那是不是要遍历几万甚至更多?比如笔者这里测试的csv有2312个列,第一列(列头)要56000次。虽然之前有测试过cpu的遍历能力,的确很快,可是遍历这么多次,实际还是能感觉到延迟。


未完待续。

参考链接:
https://www.mkyong.com/java/how-to-read-and-parse-csv-file-in-java/
https://blog.csdn.net/jarfield/article/details/5271988

你可能感兴趣的:(hadoop,大数据,spark,redis)