将 Data Export API 中的数据输出为 CSV 格式

Alexander Lucas,Google Analytics(分析)API 团队 - 2010 年 8 月


简介

本文介绍了如何从 Google Analytics(分析)Data Export API 执行的任何查询中提取数据,并以常用的 CSV 格式输出结果。这是用户使用从 Data Export API 提取的 Google Analytics(分析)数据执行的最常见任务之一,因此最好定期自动执行此流程,以节省大量时间。此外,在编写了一些可以从查询输出 CSV 文档的代码之后,您可以将这些代码集成到更大的项目中,例如自动报告生成器、邮件程序以及您编写的自定义信息中心的“导出”功能。

开始之前

如果您具备以下条件,就能够充分理解本文内容:

计划概览

本文中所含的代码将执行以下操作:

  1. 使用户能够在运行时选择是将代码输出到控制台还是输出到文件流。
  2. 以参数形式指定 DataFeed 对象,以 CSV 格式输出数据:
    • 输出行标题。
    • 输出数据行,其中每个 DataEntry 在结果输出中组成一行。
    • 通过一种整理方法对 CSV 兼容的输出运行每个值。
  3. 编写一个“Sanitizer”方法,使所有输入都与 CSV 兼容。
  4. 为您提供一个 Java 类,此类可以获取任何 Data Export API 查询并将其转换为 CSV 文件。

返回页首

允许使用可配置的输出流

要执行的第一项操作就是为您要输出的类设置可配置的输出流。这样,使用您的类的任何代码都可以确定输出是应转到标准输出,还是应直接转到某个文件。此时,您只需为 PrintStream 对象设置 getter/setter 方法即可。这将是类已完成的所有输出的目标。

private PrintStream printStream = System.out;

public PrintStream getPrintStream() {
  return printStream;
}

public void setPrintStream(PrintStream printStream) {
  this.printStream = printStream;
}

将输出设置为文件也十分简单。只需提供文件名,以便为该文件创建 PrintStream 对象。

FileOutputStream fstream = new FileOutputStream(filename);
PrintStream stream = new PrintStream(fstream);
csvprinter.setPrintStream(stream);

返回页首

迭代数据

CSV 文件的第一行是列名称行。每一列代表数据 Feed 中的一个维度或指标,因此,为了输出第一行,请执行以下操作。

  1. 获取 Feed 中的第一个条目。
  2. 使用该条目的 getDimensions 方法遍历维度列表。
  3. 使用 Dimension.getName() 方法输出每个维度的名称,后跟英文逗号。
  4. 使用 getMetrics() 方法对指标执行相同操作。在除最后一个指标外的所有指标后面加逗号。

下面是输出行标题的一种方法实现。请注意,此代码不会返回代表整个行的字符串:在处理值时,它会输出为一个输出流。

public void printRowHeaders(DataFeed feed) {
    if(feed.getEntries().size() == 0) {
      return;
    }

    DataEntry firstEntry = feed.getEntries().get(0);

    Iterator<Dimension> dimensions = firstEntry.getDimensions().iterator();
    while (dimensions.hasNext()) {
      printStream.print(sanitizeForCsv(dimensions.next().getName()));
      printStream.print(",");
    }

    Iterator<Metric> metrics = firstEntry.getMetrics().iterator();
    while (metrics.hasNext()) {
      printStream.print(sanitizeForCsv(metrics.next().getName()));
      if (metrics.hasNext()) {
        printStream.print(",");
      }
    }
    printStream.println();
  }

输出 CSV 文件的“正文”(列名称行下面的所有内容)非常类似。只有两个主要差异:第一,不只是评估第一个条目。代码需要遍历 Feed 对象中的所有条目。其次,不要使用 getName() 方法拉取要清理和输出的值,而应改用 getValue()

public void printBody(DataFeed feed) {
    if(feed.getEntries().size() == 0) {
      return;
    }

    for (DataEntry entry : feed.getEntries()) {
      printEntry(entry);
    }
  }

  public void printEntry(DataEntry entry) {
    Iterator<Dimension> dimensions = entry.getDimensions().iterator();
    while (dimensions.hasNext()) {
      printStream.print(sanitizeForCsv(dimensions.next().getValue()));
      printStream.print(",");
    }

    Iterator<Metric> metrics = entry.getMetrics().iterator();
    while (metrics.hasNext()) {
      printStream.print(sanitizeForCsv(metrics.next().getValue()));
      if (metrics.hasNext()) {
        printStream.print(",");
      }
    }
    printStream.println();
  }

此代码将您的 Feed 分为多个条目,又将条目分为要输出的值。但是,如何使这些值兼容 CSV 格式呢?如果“逗号分隔值”文件中的某个值包含英文逗号怎么办?那么必须整理这些值。

返回页首

如何整理数据以与 CSV 兼容

CSV 是一种简单的格式。一个 CSV 文件代表一个数据表,每行都代表该表中的一行。该行中的值采用英文逗号进行分隔。一个新行表示一行新数据。

遗憾的是,这一简单的格式很容易因数据格式不兼容而导致数据丢失。如果您的值中包含英文逗号怎么办?如果您的值中包含换行符怎么办?如果英文逗号和值之间有空格,会导致什么结果?所有这些情况都可以通过使用几个简单的规则来解决。

  • 如果字符串中包含双引号字符,则使用另一个双引号字符对其进行转义。
  • 如果字符串中包含英文逗号,则使用双引号将整个字符串引起来(除非已有双引号)。
  • 如果字符串中包含换行符,则使用双引号将整个字符串引起来(除非已有双引号)。
  • 如果字符串的开头或结尾包含任何种类的空格,则使用双引号将整个字符串引起来(除非已有双引号)。

此时,您可能很难想像您的值应该是什么样子,因此,下面列举了一些示例。请注意,每个示例代表一个值,并按如下所示的方法进行转义。为清楚起见,空格将显示为“_”字符。

整理之前 整理之后
unchanged unchanged
random " doublequote random "" doublequote
comma,separated "comma,separated"

“两
行”
_leading space, and a comma "_leading space, and a comma"
"leading quote, comma """leading quote, comma"
_空格、逗号
第二行和双引号"
"_空格、英文逗号
第二行和英文双引号""

处理所有这些条件的最简单方法是编写一种排错方法。将有问题的数据放进去,即可输出有效的 CSV 值。下面只是此类方法的一种理想的实现示例。

private String sanitizeForCsv(String cellData) {
  StringBuilder resultBuilder = new StringBuilder(cellData);

  // Look for doublequotes, escape as necessary.
  int lastIndex = 0;
  while (resultBuilder.indexOf("\"", lastIndex) >= 0) {
    int quoteIndex = resultBuilder.indexOf("\"", lastIndex);
    resultBuilder.replace(quoteIndex, quoteIndex + 1, "\"\"");
    lastIndex = quoteIndex + 2;
  }

  char firstChar = cellData.charAt(0);
  char lastChar = cellData.charAt(cellData.length() - 1);

  if (cellData.contains(",") || // Check for commas
      cellData.contains("\n") ||  // Check for line breaks
      Character.isWhitespace(firstChar) || // Check for leading whitespace.
      Character.isWhitespace(lastChar)) { // Check for trailing whitespace
      resultBuilder.insert(0, "\"").append("\""); // Wrap in doublequotes.
  }
    return resultBuilder.toString();
}

该方法首先检查是否存在双引号。此操作应在所有其他检查之前完成,因为它们涉及用英文双引号将字符串引起来,确定属于值一部分的双引号与此方法之前添加的双引号之间的区别有些麻烦。这些引号很容易转义,只需再添加一个引号即可。每个 " 变为 "",每个 "" 变为 """",依此类推。

在解决了这种情况之后,方可检查所有其他情况(未整理的空格、英文逗号和换行符)。如果存在这些字符中的任何字符,只需使用双引号将值引起来即可。

请注意,上文中使用的是 StringBuilder 对象,从未直接操控原始字符串。这是因为,StringBuilder 可让您随意操纵字符串,而无需在内存中进行临时副本。由于 Java 中的字符串不可更改,因此您进行的每项细微调整都将创建一个全新的字符串。在处理电子表格数据时,字符串数量会急剧增长。

行数 x 每行的值数 x 对值进行的更改次数 = 所创建的新字符串总数
10000 10 3 300000

返回页首

下一步做什么?

现在,您已经获得了一把金锤,接下来很自然地去寻找钉子。下面这些提示可帮助您快速入门。

  • 查看示例应用源代码,该代码基于示例查询使用此类来输出 CSV 文件。它使用输出文件名作为命令行参数,并默认情况下输出到标准输出。以此为出发点,构建性能卓越的代码!
  • CSV 只是许多常见格式中的一种格式。调整该类以输出为其他格式,如 TSV、YAML、JSON 或 XML。
  • 编写一个生成 CSV 并在完成后通过邮件发送它们的应用。轻松实现每月自动生成报告!
  • 编写一个让您能够以交互方式输入查询的应用,以便生成一个可用于挖掘数据的功能强大的界面。