在工作中,会有处理很多数据的场景。
比如,
用户需求:
我有大量数据,
(1)我想要高效查询我想要的数据。
(2)我想要高效计算出我想要的结果数据。
为了存储数据,我们使用的数据存储方式有:
mysql,oracle,表;
csv文件
excel文件
hdfs文件系统
hbase
redis,ehcache
把数据从存储区拿出来作分析,我们使用的数据分析手段有:
java单机计算,分布式计算
hadoop集群
spark集群
redis集群
利用以上的手段,怎么做?
当然用户需求不是这么简单的2句话,本人也不是要写一本书来概括这些技术实现方案,相反,这些不是本文的重点,本文的重点是,以自己的经验,提出一个可实现流程,然后分析每个环节可行性和难点。这是本文将要写的主要内容。
先细化一下需求,目前需求只有一个,就是:
有很多csv文件数据,如何高效查询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