Scala 数据统计之 Hello World

最近要做一个报表,可是其中有一项 CDN 相关的数据就是不对,差了好几个数量级。于是我从最原始的数据源头开始分析。下面的几行是我们的数据的样本,我们需要统计其中数字部分的总和。
需求有了,那我们应该用什么工具来进行统计呢? 我们有各种的脚本语言可以帮我们完成这个统计任务,甚至我们还可以写 Java 程序来统计。在这篇文章中,我想演示的是如何用 Scala 完成任务。Scala 丰富的 Collection 的类型,及其相应的一些方法,使得 Scala 非常的适合于这类型的数据统计的任务。

下面便是我们要统计的数据的一个片段,每一行都类似于 JSON 的一个 Object,Key 是 CDN 的名称,Value 是对应的 CDN 下载的流量(当然了,这里的 CDN 名称是做过处理的啦~)。我们统计所有的 Value 的总和。这里我用正则表达式的方式去获取 Value。对于每个匹配:(\d+)的 Match,我们取其的 group(1),再toLong就可以得到 CDN 的下载量。

{"unknownCdn0":26899482,"cdn1":0}
{}
{"cdnwnl.cdn":57916969,"cdncdn":32546565}
{"cdnl.cdn":102774524,"smb.cdnak.cdn":72105320,"vod.cdnak.dal.cdn":4880812,"cdnal.cdn":891754311,"cdncdn":56142652,"smb.cdnak.dal.cdn":6457988,"video.fw.com":21128604,"cdnnl.dal.cdn":4356524}
{"cdnon.cdn":1266090912,"cdnn.cdn":307729776,"cdnn.cdn":669043872,"cdnn.cdn":1638160,"cdncdn":122998720,"cdncdn":51214848}

第一种方法

我们定义一个可变变量 result,并遍历每一行进行累加。

import scala.util.matching._
import scala.io.Source

val file = Source.fromFile("bytesloadeddelta")

var result = 0l
val lines = file.getLines;
while (lines.hasNext) {
  val matches = """:(\d+)""".r findAllMatchIn lines.next
  matches foreach {m => result = result + m.group(1).toLong}
}

println(result)
file.close
第二种方法

在第一种方法中,虽然很直观,很符合一个 C/Java 程序员的思维,但个人认为这并不是 Scala 的思维。所以我们再做稍微的修改,去掉可变变量var result

import scala.util.matching._
import scala.io.Source

val file = Source.fromFile("bytesloadeddelta")

println(sumAll(file.getLines.toList))

def sumAll(lines: List[String]): Long = {
  if (lines == Nil) {
    return 0
  } else {
    val line = lines.head
    val sums = for(m <- """:(\d+)""".r findAllMatchIn lines.head) 
      yield m.group(1).toLong
    
    return sums.sum + sumAll(lines.tail)
  }
}
file.close
第三种方法

第二种方法用递归的方式去掉了可变变量。但是有个最大的问题是file.getLines.toList将输入文件的所有内容都读到了内存中,当我们的输入足够大的时候,上面的方法就会内存溢出,对于 1.5G 的输入文件,即使使用了-J-Xmx8G,依然是内存溢出。所以我们再次对代码修改。

import scala.util.matching._
import scala.io.Source

val file = Source.fromFile("bytesloadeddelta")

val result = file.getLines.map({line =>
  val sums = for(m <- """:(\d+)""".r findAllMatchIn line) 
    yield m.group(1).toLong
  sums.sum
}).sum

println(result)
file.close
第四种方式

睡觉前想到的第四种更加简单的方式。相比第三种,我们就是把map换成了flatMap。他们之间的区别就是前者返回一个 element,而后者返回一个 List。

import scala.util.matching._
import scala.io.Source

val file = Source.fromFile("cdnbytes")

val result = file.getLines.flatMap({line =>
  for(m <- """:(\d+)""".r findAllMatchIn line) 
    yield m.group(1).toLong
}).sum

println(result)
file.close

嗯,好了,这就是最后我们使用的代码啦,简洁,明了。O(∩_∩)O哈哈~
最后要说明的一点是file.close这个语句一定不能忘了!网上的很多例子,甚至包括官网的例子,在讲解 Source 的时候,都没有关闭打开的文件描述符。而且官网使用的例子Source.fromFile("filename").getLines这种方式根本就没有机会关闭文件描述符,因为 BufferedSource 这个引用根本就没有保留下来。然后开始的时候,我就认为这个文件描述符,会被隐式的关闭。结!果!它必须要手动的 close,然后我的代码在运行两天后,就抛了 IOException: too many open files。

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注