【问题标题】:Efficiently merge 2 large csv files in java by common labels通过常用标签有效合并java中的2个大型csv文件
【发布时间】:2016-09-22 09:23:07
【问题描述】:

我需要通过用户可以指定的常用行或列标签合并 2 个大型 csv 文件(每个文件中大约有 4000 万个数据元素,因此 ~500mb)。例如,如果 dataset1.csv 包含:

patient_id    x1     x2    x3
pi1           1      2     3
pi3           4      5     6

dataset2.csv 包含:

patient_id    y1    y2    y3
pi0           0     0     0
pi1           11    12    13
pi2           99    98    97
pi3           14    15    16

用户可以通过他们的行标签(患者 ID)指定合并这两个文件,结果 output.csv 将是:

patient_id    x1   x2   x3   y1    y2   y3
pi1           1    2    3    11    12   13
pi3           4    5    6    14    15   16

因为我们仅将两个输入文件共有(交集)的患者 ID 信息结合起来。我解决这个问题的策略是创建一个 HashMap,其中要合并的行或列标签(在这种情况下是行标签,即患者 ID)是键,患者 ID 的数据存储为 ArrayList 作为价值。我为每个输入数据文件创建一个 HashMap,然后根据相似的键合并值。我将数据表示为 ArrayList> 类型的二维 ArrayList,因此合并的数据也具有这种类型。然后我简单地遍历合并的 ArrayList> 对象,我称之为数据类型对象,并将其打印到文件中。代码如下:

下面是依赖于下面Data类文件的DataMerge类。

import java.util.HashMap;
import java.util.ArrayList;

public class DataMerge {


/**Merges two Data objects by a similar label. For example, if two data sets represent
 * different data for the same set of patients, which are represented by their unique patient
 * ID, mergeData will return a data set containing only those patient IDs that are common to both
 * data sets along with the data represented in both data sets. labelInRow1 and labelInRow2 separately 
 * indicate whether the common labels are in separate rows(true) of d1 and d2, respectively, or separate columns otherwise.*/


public static Data mergeData(Data d1, Data d2, boolean labelInRow1, 
        boolean labelInRow2){
    ArrayList<ArrayList<String>> mergedData = new ArrayList<ArrayList<String>>();
    HashMap<String,ArrayList<String>> d1Map = d1.mapFeatureToData(labelInRow1);
    HashMap<String,ArrayList<String>> d2Map = d2.mapFeatureToData(labelInRow2);
    ArrayList<String> d1Features;
    ArrayList<String> d2Features;

    if (labelInRow1){
        d1Features = d1.getColumnLabels();
    } else {
        d1Features = d1.getRowLabels();
    }
    if (labelInRow2){
        d2Features = d2.getColumnLabels();
    } else {
        d2Features = d2.getRowLabels();
    }
    d1Features.trimToSize();
    d2Features.trimToSize();

    ArrayList<String> mergedFeatures = new ArrayList<String>();
    if ((d1.getLabelLabel() != "") && (d1.getLabelLabel() == "")) {
        mergedFeatures.add(d1.getLabelLabel());
    }
    else if ((d1.getLabelLabel() == "") && (d1.getLabelLabel() != "")) {
        mergedFeatures.add(d2.getLabelLabel());
    } else {
        mergedFeatures.add(d1.getLabelLabel());
    }

    mergedFeatures.addAll(d1Features);
    mergedFeatures.addAll(d2Features);
    mergedFeatures.trimToSize();
    mergedData.add(mergedFeatures);

    for (String key : d1Map.keySet()){
        ArrayList<String> curRow = new ArrayList<String>();
        if (d2Map.containsKey(key)){
            curRow.add(key);
            curRow.addAll(d1Map.get(key));
            curRow.addAll(d2Map.get(key));
            curRow.trimToSize();
            mergedData.add(curRow);
        }
    }
    mergedData.trimToSize();
    Data result = new Data(mergedData, true);
    return result;
}

}

以下是数据类型对象及其关联的 HashMap 生成函数以及一些行和列标签提取方法。

import java.util.*;
import java.io.*;

/**Represents an unlabeled or labeled data set as a series of nested     ArrayLists, where each nested 
 * ArrayList represents a line of the input data.*/

public class Data {
private ArrayList<String> colLabels = new ArrayList<String>();  //row labels

private ArrayList<String> rowLabels = new ArrayList<String>();  //column labels

private String labelLabel;

private ArrayList<ArrayList<String>> unlabeledData; //data without row and column labels



/**Returns an ArrayList of ArrayLists, where each nested ArrayList represents a line
*of the input file.*/
@SuppressWarnings("resource")
private static ArrayList<ArrayList<String>> readFile(String filePath, String fileSep){
    ArrayList<ArrayList<String>> result = new ArrayList<ArrayList<String>>();
    try{
        BufferedReader input = new BufferedReader(new FileReader (filePath));
        String line = input.readLine();
        while (line != null){
            String[] splitLine = line.split(fileSep);
            result.add(new ArrayList<String>(Arrays.asList(splitLine)));
            line = input.readLine();
        }
    }
    catch (Exception e){
        System.err.println(e);
    }
    result.trimToSize();;
    return result;
}


/**Returns an ArrayList of ArrayLists, where each nested ArrayList represents a line of the input
 * data but WITHOUT any row or column labels*/


private ArrayList<ArrayList<String>> extractLabelsAndData(String filePath, String fileSep){
    ArrayList<ArrayList<String>> tempData = new ArrayList<ArrayList<String>>();
    tempData.addAll(readFile(filePath, fileSep));
    tempData.trimToSize();
    this.colLabels.addAll(tempData.remove(0));
    this.labelLabel = this.colLabels.remove(0);
    this.colLabels.trimToSize();
    for (ArrayList<String> line : tempData){
        this.rowLabels.add(line.remove(0));
    }
    this.rowLabels.trimToSize();
    return tempData;
}




/**Returns an ArrayList of ArrayLists, where each nested ArrayList represents a line of the input
 * data but WITHOUT any row or column labels. Does mutate the original data*/
private ArrayList<ArrayList<String>> extractLabelsAndData (ArrayList<ArrayList<String>> data){
    ArrayList<ArrayList<String>> result = new ArrayList<ArrayList<String>>();
    for (ArrayList<String> line : data){
        ArrayList<String> temp = new ArrayList<String>();
        for (String element : line){
            temp.add(element);
        }
        temp.trimToSize();
        result.add(temp);
    }
    this.colLabels.addAll(result.remove(0));
    this.labelLabel = this.colLabels.remove(0);
    this.colLabels.trimToSize();
    for (ArrayList<String> line : result){
        this.rowLabels.add(line.remove(0));
    }
    this.rowLabels.trimToSize();
    result.trimToSize();
    return result;
}


/**Returns the labelLabel for the data*/
public String getLabelLabel(){
    return this.labelLabel;
}


/**Returns an ArrayList of the labels while maintaining the order
* in which they appear in the data. Row indicates that the desired
* features are all in the same row. Assumed that the labels are in the
* first row of the data. */
public ArrayList<String> getColumnLabels(){
    return this.colLabels;
}


/**Returns an ArrayList of the labels while maintaining the order
* in which they appear in the data. Column indicates that the desired
* features are all in the same column. Assumed that the labels are in the
* first column of the data.*/
public ArrayList<String> getRowLabels(){
    return this.rowLabels;
}


/**Creates a HashMap where a list of feature labels are mapped to the entire data. For example,
 * if a data set contains patient IDs and test results, this function can be used to create
 * a HashMap where the keys are the patient IDs and the values are an ArrayList of the test
 * results. The boolean input isRow, which, when true, designates that the
 * desired keys are listed in the rows or false if they are in the columns.*/
public HashMap<String, ArrayList<String>> mapFeatureToData(boolean isRow){
    HashMap<String, ArrayList<String>> featureMap = new HashMap<String,ArrayList<String>>();
    if (!isRow){
        for (ArrayList<String> line : this.unlabeledData){
            for (int i = 0; i < this.colLabels.size(); i++){
                if (featureMap.containsKey(this.colLabels.get(i))){
                    featureMap.get(this.colLabels.get(i)).add(line.get(i));
                } else{
                    ArrayList<String> firstValue = new ArrayList<String>();
                    firstValue.add(line.get(i));
                    featureMap.put(this.colLabels.get(i), firstValue);
                }
            }
        }
    } else {
        for (int i = 0; i < this.rowLabels.size(); i++){
            if (!featureMap.containsKey(this.rowLabels.get(i))){
                featureMap.put(this.rowLabels.get(i), this.unlabeledData.get(i));
            } else {
                featureMap.get(this.rowLabels.get(i)).addAll(this.unlabeledData.get(i));
            }
        }
    }
    return featureMap;
} 


/**Writes the data to a file in the specified outputPath. sep indicates the data delimiter.
 * labeledOutput indicates whether or not the user wants the data written to a file to be 
 * labeled or unlabeled. If the data was unlabeled to begin with, then labeledOutput 
 * should not be set to true. */
public void writeDataToFile(String outputPath, String sep){
    try {
        PrintStream writer = new PrintStream(new BufferedOutputStream (new FileOutputStream (outputPath, true)));
        String sol = this.labelLabel + sep;
        for (int n = 0; n < this.colLabels.size(); n++){
            if (n == this.colLabels.size()-1){
                sol += this.colLabels.get(n) + "\n";
            } else {
                sol += this.colLabels.get(n) + sep;
            }
        }
        for (int i = 0; i < this.unlabeledData.size(); i++){
            ArrayList<String> line = this.unlabeledData.get(i);
            sol += this.rowLabels.get(i) + sep;
            for (int j = 0; j < line.size(); j++){
                if (j == line.size()-1){
                    sol += line.get(j);
                } else {
                    sol += line.get(j) + sep;
                }
            }
            sol += "\n";
        }
        sol = sol.trim();
        writer.print(sol);
        writer.close();

    } catch (Exception e){
        System.err.println(e);
    }
}


/**Constructor for Data object. filePath specifies the input file directory,
 * fileSep indicates the file separator used in the input file, and hasLabels
 * designates whether the input data has row and column labels. Note that if 
 * hasLabels is set to true, it is assumed that there are BOTH row and column labels*/
public Data(String filePath, String fileSep, boolean hasLabels){
    if (hasLabels){
        this.unlabeledData = extractLabelsAndData(filePath, fileSep);
        this.unlabeledData.trimToSize();
    } else {
        this.unlabeledData = readFile(filePath, fileSep);
        this.unlabeledData.trimToSize();
    }

}


/**Constructor for Data object that accepts nested ArrayLists as inputs*/
public Data (ArrayList<ArrayList<String>> data, boolean hasLabels){
    if (hasLabels){
        this.unlabeledData = extractLabelsAndData(data);
        this.unlabeledData.trimToSize();
    } else {
        this.unlabeledData = data;
        this.unlabeledData.trimToSize();
    }
}
}

该程序适用于小型数据集,但已经 5 天以上,合并仍未完成。我正在寻找更有效的时间和内存解决方案。有人建议使用字节数组而不是字符串,这可能会使其运行得更快。有人有什么建议吗?

编辑:我在代码中进行了一些挖掘,发现读取输入文件并合并它们几乎不需要时间(比如 20 秒)。编写文件需要 5 天以上的时间

【问题讨论】:

  • 不是你问的,但如果我合并 csv 文件,Java 将是我最后的解决方案。 :) 我会让更多知识渊博的人回答有关 java 效率的问题。
  • 我完全同意。我可以在 R 中轻松做到这一点,但我需要使用 Java
  • 你能使用像 JRI 这样从 Java 调用的 R 库吗?或者通过 Hibernate 和朋友调用的任何类型的 SQL?
  • 总是有 Runtime.getRuntime().exec() 调用 awk 或 perl 或 ruby​​ 脚本... :)
  • 您是否对现有代码进行了概要分析,以了解瓶颈在哪里可以集中您的效率尝试?

标签: java csv arraylist merge large-files


【解决方案1】:

您将所有数百万行数据的所有数据字段连接成一个巨大的字符串,然后写入该单个字符串。当您分配和重新分配非常大的字符串时,内存抖动会导致缓慢的死亡,为您添加到字符串的每个字段和分隔符一遍又一遍地复制它们。在第 3 天或第 4 天左右,每个字符串是......数百万个字符长? ......而你可怜的垃圾收集器正在大汗淋漓地把它拿出来。

不要那样做。

分别构建输出文件的每一行并编写它。然后构建下一行。

此外,使用StringBuilder 类来构建线条,尽管您会在上一步中获得这样的改进,您甚至可能不会为此烦恼。虽然这是这样做的方法,但您应该学习如何做。

【讨论】:

  • 感谢您的评论。我明白你的意思,写每个文件肯定比存储所有内容然后写要好。我会做出改变,让你知道!感谢您的帮助
  • 不完全是。正如您所发现的,您当然可以从两个文件中读取所有数据,甚至可以进行合并。但是一次构造一行输出文件(一个patient_id的数据),然后写入。然后扔掉那条线(当然是隐式发生的),然后再做一次。然后再次。对于每个patient_id。然后,您正在构建的那些字符串每个不超过 100-120 个字符,并且您保留它们的时间不会超过编写它们所需的时间。没有内存压力。
  • 对不起!我的意思是“写每一行”而不是“写每个文件”。我绝对明白你在说什么。但是,为了理解起见,为什么尝试编写连接的输出字符串比编写每一行花费的时间更长?看起来基本上相同数量的数据正在被写入。仅仅是因为发送大量文本会阻塞内存,导致写入过程的内存减少吗?
  • 不是在写字符串,而是在构建字符串。您正在创建一个长度为数百万个字符的单个字符串,而不是创建一百万个字符串,每个字符串只有几个字符长......并在编写后立即丢弃每个这样的短(一行)字符串。在后一种情况下,您的内存中有所有数据的大哈希图 - 显然大约 1gig - + 一个 100 个字符的输出字符串。在前一种情况下,您拥有该 gig 的 hashmap 数据加上一个 multi-gig 字符串加上您正在构建的该字符串的 copy 以在末尾添加一些字符 ...
  • 附言。 user3749778 ...如果这个答案对您有帮助...不要忘记勾选它以告诉其他人这是正确的答案。 (也给我几点。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多