【问题标题】:Simplest way to count words in a file计算文件中单词的最简单方法
【发布时间】:2013-03-18 21:37:30
【问题描述】:

我正在尝试以最简单的方式编写一个程序来计算 Scala 语言文件中单词出现的次数。到目前为止,我有这些代码:

import scala.io.Codec.string2codec
import scala.io.Source
import scala.reflect.io.File

object WordCounter {
    val SrcDestination: String = ".." + File.separator + "file.txt"
    val Word = "\\b([A-Za-z\\-])+\\b".r

    def main(args: Array[String]): Unit = {

        val counter = Source.fromFile(SrcDestination)("UTF-8")
                .getLines
                .map(l => Word.findAllIn(l.toLowerCase()).toSeq)
                .toStream
                .groupBy(identity)
                .mapValues(_.length)

        println(counter)
    }
}

不要打扰正则表达式。我想知道如何从 在这一行检索到的序列:

map(l => Word.findAllIn(l.toLowerCase()).toSeq)

为了计算每个单词的出现次数。目前我正在获取带有计数单词序列的地图。

【问题讨论】:

    标签: scala


    【解决方案1】:

    您可以通过使用正则表达式"\\W+" 将文件行拆分为单词(flatmap 是惰性的,因此不需要将整个文件加载到内存中)。要计算出现次数,您可以折叠 Map[String, Int] 用每个单词更新它(比使用 groupBy 更节省内存和时间)

    scala.io.Source.fromFile("file.txt")
      .getLines
      .flatMap(_.split("\\W+"))
      .foldLeft(Map.empty[String, Int]){
         (count, word) => count + (word -> (count.getOrElse(word, 0) + 1))
      }
    

    【讨论】:

    • 我真的是 scala 的新手,所以我在理解你传递给 foldLeft 的匿名函数时遇到了一些麻烦。它怎么知道要反转你传入的元组的顺序(count, word)?
    • count实际上是一个Map[String, Int],单词是一个String。
    • 是的我现在明白了,元组的第一个元素是累加器,第二个元素是 foldLeft 正在迭代的元素。
    • 我花了很长时间才明白这一点。我用这种理解回答了关于 SO 的另一个问题,并更详细地解释了正在发生的事情。也许它会对某人有所帮助。 stackoverflow.com/questions/41006847/…
    • foldLeft 函数有一个空的 Map[String, Int],据我了解,对于它找到的每个新单词,它都会创建一个新映射(因为它是不可变的!),它是否存在性能问题?
    【解决方案2】:

    我认为以下内容更容易理解:

    Source.fromFile("file.txt").
      getLines().
      flatMap(_.split("\\W+")).
      toList.
      groupBy((word: String) => word).
      mapValues(_.length)
    

    【讨论】:

    • FWIW,我认为您可以将 (word: String) => word 替换为 identity。
    • 这会将整个文件内容保存在内存中,而接受的答案则不会。
    【解决方案3】:

    我不是 100% 确定你在问什么,但我想我看到了问题所在。尝试使用flatMap 而不是map:

    flatMap(l => Word.findAllIn(l.toLowerCase()).toSeq)
    

    这会将您的所有序列连接在一起,以便 groupBy 对单个单词而不是在行级别完成。


    关于您的正则表达式的说明

    我知道您说过不要担心您的正则表达式,但您可以进行一些更改以使其更具可读性。这是你现在拥有的:

    val Word = "\\b([A-Za-z\\-])+\\b".r
    

    首先,您可以使用 Scala 的三引号字符串,这样您就不必转义反斜杠:

    val Word = """\b([A-Za-z\-])+\b""".r
    

    其次,如果您将- 放在角色类的开头,那么您不需要转义它:

    val Word = """\b([-A-Za-z])+\b""".r
    

    【讨论】:

      【解决方案4】:

      从Scala 2.13 开始,除了用Source 检索单词之外,我们还可以使用groupMapReduce 方法,它(顾名思义)相当于groupBy,后跟mapValues 和一个reduce 步骤:

      import scala.io.Source
      
      Source.fromFile("file.txt")
        .getLines.to(LazyList)
        .flatMap(_.split("\\W+"))
        .groupMapReduce(identity)(_ => 1)(_ + _)
      

      groupMapReduce 阶段,类似于 Hadoop 的 map/reduce 逻辑,

      • groups 自己的话(身份)(groupMapReduce 的组部分)

      • maps 每个分组的单词出现次数为 1(映射组的一部分MapReduce)

      • 将一组单词 (_ + _) 中的reduces 值相加(减少 groupMap 的一部分Reduce)。

      这是one-pass version 可以翻译的内容:

      seq.groupBy(identity).mapValues(_.map(_ => 1).reduce(_ + _))
      

      还要注意从Iterator 到LazyList 的转换,以便使用提供groupMapReduce 的集合(我们不使用Stream,因为从Scala 2.13 开始,建议替换LazyList Streams)。


      根据同样的原则,也可以使用for-comprehension 版本:

      (for {
        line <- Source.fromFile("file.txt").getLines.to(LazyList)
        word <- line.split("\\W+")
      } yield word)
      .groupMapReduce(identity)(_ => 1)(_ + _)
      

      【讨论】:

        【解决方案5】:

        这就是我所做的。这将切断一个文件。 Hashmap 是高性能的好选择,并且会胜过任何类型的排序。 里面还有更简洁的排序和切片功能,你也可以看看。

        import java.io.FileNotFoundException
        
        /**.
         * Cohesive static method object for file handling.
         */
        object WordCountFileHandler {
        
          val FILE_FORMAT = "utf-8"
        
          /**
           * Take input from file. Split on spaces.
           * @param fileLocationAndName string location of file
           * @return option of string iterator
           */
          def apply (fileLocationAndName: String) : Option[Iterator[String]] = {
            apply (fileLocationAndName, " ")
          }
        
          /**
           * Split on separator parameter.
           * Speculative generality :P
           * @param fileLocationAndName string location of file
           * @param wordSeperator split on this string
           * @return
           */
          def apply (fileLocationAndName: String, wordSeperator: String): Option[Iterator[String]] = {
            try{
              val words = scala.io.Source.fromFile(fileLocationAndName).getLines() //scala io.Source is a bit hackey. No need to close file.
        
              //Get rid of anything funky... need the double space removal for files like the README.md...
              val wordList = words.reduceLeft(_ + wordSeperator + _).replaceAll("[^a-zA-Z\\s]", "").replaceAll("  ", "").split(wordSeperator)
              //wordList.foreach(println(_))
              wordList.length match {
                case 0 => return None
                case _ => return Some(wordList.toIterator)
              }
            } catch {
              case _:FileNotFoundException => println("file not found: " + fileLocationAndName); return None
              case e:Exception => println("Unknown exception occurred during file handling: \n\n" + e.getStackTrace); return None
            }
          }
        }
        
        import collection.mutable
        
        /**
         * Static method object.
         * Takes a processed map and spits out the needed info
         * While a small performance hit is made in not doing this during the word list analysis,
         * this does demonstrate cohesion and open/closed much better.
         * author: jason goodwin
         */
        object WordMapAnalyzer {
        
          /**
           * get input size
           * @param input
           * @return
           */
          def getNumberOfWords(input: mutable.Map[String, Int]): Int = {
            input.size
          }
        
          /**
           * Should be fairly logarithmic given merge sort performance is generally about O(6nlog2n + 6n).
           * See below for more performant method.
           * @param input
           * @return
           */
        
          def getTopCWordsDeclarative(input: mutable.HashMap[String, Int], c: Int): Map[String, Int] = {
            val sortedInput = input.toList.sortWith(_._2 > _._2)
            sortedInput.take(c).toMap
          }
        
          /**
           * Imperative style is used here for much better performance relative to the above.
           * Growth can be reasoned at linear growth on random input.
           * Probably upper bounded around O(3n + nc) in worst case (ie a sorted input from small to high).
           * @param input
           * @param c
           * @return
           */
          def getTopCWordsImperative(input: mutable.Map[String, Int], c: Int): mutable.Map[String, Int] = {
            var bottomElement: (String, Int) = ("", 0)
            val topList = mutable.HashMap[String, Int]()
        
            for (x <- input) {
              if (x._2 >= bottomElement._2 && topList.size == c ){
                topList -= (bottomElement._1)
                topList +=((x._1, x._2))
                bottomElement = topList.toList.minBy(_._2)
              } else if (topList.size < c ){
                topList +=((x._1, x._2))
                bottomElement = topList.toList.minBy(_._2)
              }
            }
            //println("Size: " + topList.size)
        
            topList.asInstanceOf[mutable.Map[String, Int]]
          }
        }
        
        object WordMapCountCalculator {
        
          /**
           * Take a list and return a map keyed by words with a count as the value.
           * @param wordList List[String] to be analysed
           * @return HashMap[String, Int] with word as key and count as pair.
           * */
        
           def apply (wordList: Iterator[String]): mutable.Map[String, Int] = {
            wordList.foldLeft(new mutable.HashMap[String, Int])((word, count) => {
              word get(count) match{
                case Some(x) => word += (count -> (x+1))   //if in map already, increment count
                case None => word += (count -> 1)          //otherwise, set to 1
              }
            }).asInstanceOf[mutable.Map[String, Int]] 
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-08-09
          • 1970-01-01
          • 2011-05-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多