Статьи

Scala Tutorial — scala.io.Source, доступ к файлам, flatMap, изменяемые Карты

Предисловие

Это 8 часть руководств для начинающих программистов, попадающих в Scala. Другие посты находятся в этом блоге, и вы можете получить ссылки на эти и другие ресурсы на странице ссылок курса по компьютерной лингвистике, для которого я их создаю. Кроме того, вы можете найти эту и другие серии учебников на странице JCG Java Tutorials .

Это руководство о доступе к файловой системе для работы с текстовыми файлами. Предыдущий учебник показал, как построить карту, которая содержит количество слов каждого типа в данном тексте. Однако предполагалось, что текст был доступен в переменной String, и, как правило, нам интересно знать о файлах, которые существуют в файловой системе или в Интернете. Из этого туториала вы узнаете, как прочитать содержимое файла в Scala для обработки, либо создав одну строку для файла, либо используя его построчно в потоковом режиме. Попутно неизменные Карты представлены как способ включить подсчет слов без чтения всего файла в память.

Подсчет слов по содержимому файла

В качестве примера мы будем использовать полный Шерлок Холмс из проекта Гутенберг . Загрузите его, поместите в каталог, а затем запустите Scala REPL в этом каталоге. Для доступа к файлам мы будем использовать класс Source , поэтому для начала вам нужно его импортировать.

1
2
scala> import scala.io.Source
import scala.io.Source

Source предоставляет несколько способов взаимодействия с файлами и делает их доступными для вас в вашей программе Scala. Вероятно, вам понадобится метод fromFile .

1
2
scala> Source.fromFile("pg1661.txt")
res3: scala.io.BufferedSource = non-empty iterator

Это создает BufferedSource , из которого вы можете легко получить все содержимое файла в виде строки.

1
2
3
4
5
6
7
8
9
scala> val holmes = Source.fromFile("pg1661.txt").mkString
holmes: String =
"Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle
 
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever.  You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.net
<...many more lines...>

При этом вы можете сделать то же самое, что показано в уроке 7, чтобы получить количество слов (за исключением того, что здесь мы будем разбивать последовательности пробелов, а не только один пробел).

1
2
3
4
5
6
7
8
scala> val counts = holmes.split("\\s+").groupBy(x=>x).mapValues(x=>x.length)
counts: scala.collection.immutable.Map[java.lang.String,Int] = Map(wood-work, -> 1, "Pray, -> 1, herself. -> 2, stern-post -> 1, "Should -> 1, incident -> 8, serious -> 14, earth--" -> 2, sinister -> 10, comply -> 7, breaks -> 1, forgotten -> 3, precious -> 10, 'It -> 3, compliment -> 2, suite, -> 1, "DEAR -> 1, summarise. -> 1, "Done -> 1, fine.' -> 1, lover -> 5, of. -> 2, lead. -> 1, plentiful -> 1, 'Lone -> 4, malignant -> 1, terrible -> 14, rate -> 1, mole -> 1, assert -> 1, lights -> 2, Stevenson, -> 1, submitted -> 4, tap. -> 1, beard, -> 1, band--a -> 1, force! -> 1, snow -> 7, Produced -> 2, ask, -> 1, purchasing -> 1, Hall, -> 1, wall. -> 5, remarked -> 32, laughing -> 4, member." -> 1, 30,000 -> 2, Redistributing -> 1, coat, -> 6, "'One -> 2, 'band,' -> 1, relapsed -> 1, apol...
 
scala> counts("Holmes")
res2: Int = 197
 
scala> counts("Watson")
res3: Int = 4

Чтобы вам не показалось странным, что Уотсон появляется только четыре раза, имейте в виду, что мы разделяемся на пробелы, и это означает, что в предложении, подобном следующему, знак интереса — Уотсон », а не Уотсон .

«Вы, возможно, не могли прийти в лучшее время, мой дорогой Ватсон», сердечно сказал он.

Поиск этого и других показывает больше токенов, содержащих Ватсона в истории.

1
2
3
4
5
6
7
8
scala> counts("Watson,\"")
res4: Int = 19
 
scala> counts("Watson,")
res5: Int = 40
 
scala> counts("Watson.")
res6: Int = 10

Конечно, реальная проблема заключается в том, что токенизация на пустом месте слишком грубая. Чтобы сделать это должным образом, обычно требуется хороший ручной токенизатор (который может хранить токены, такие как, например, Mr. и Yahoo!, в то же время разделяя знаки препинания на большинство слов), или компьютерный обученный, который обучается на данных, помеченных вручную для токенов. Пример последнего см. В лексемах инструментария Apache OpenNLP , которые включают предварительно обученные модели для английского языка.

Рабочая строка за строкой

Довольно часто вам нужно построчно работать с файлом, а не читать все как одну строку, как мы делали выше. Например, вам может потребоваться обрабатывать каждую строку по-разному, поэтому просто иметь ее как одну строку не очень удобно. Или вы можете работать с большим файлом, который не может легко поместиться в память (что происходит, когда вы читаете всю строку). Вы можете получить строки в файле как Iterator [String] , в котором каждый элемент является отдельной строкой из файла, используя метод getLines .

1
2
scala> Source.fromFile("pg1661.txt").getLines
res4: Iterator[String] = non-empty iterator

Этот итератор готов для вас использовать строки, но он не сразу считывает весь файл в память — вместо этого он буферизует его так, что каждая строка будет доступна для вас, когда вы об этом просите, по существу, считывая данные с диска, когда вы требовать больше строк. Вы можете думать об этом как о потоке файла в вашу программу Scala, так же как современный аудио и видео контент передается на ваш компьютер: он никогда не сохраняется, а просто передается по частям туда, где он нужен, когда это необходимо.

Конечно, итераторы имеют много общего со структурами данных последовательности, такими как списки: если у нас есть итератор, мы можем использовать foreach , for , map и т. Д. Таким образом, чтобы распечатать все строки в файле, мы можем сделать следующее.

01
02
03
04
05
06
07
08
09
10
11
12
scala> Source.fromFile("pg1661.txt").getLines.foreach(println)
Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle
 
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever.  You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.net
 
Title: The Adventures of Sherlock Holmes
 
Author: Arthur Conan Doyle
<...many more lines...>

Это создает много выходных данных, но показывает, как вы можете легко создать собственную реализацию Scala для программы Unix cat : просто сохраните следующую строку в файле с именем cat.scala :

1
scala.io.Source.fromFile(args(0)).getLines.foreach(println)

И затем вызовите это с именем файла, чтобы перечислить его содержимое.

1
$ scala cat.scala pg1661.txt

Вернемся к REPL, это не совсем идеально, чтобы увидеть весь файл. Если вы просто хотите увидеть начало файла, используйте метод take в Итераторе перед foreach .

1
2
3
4
5
6
scala> Source.fromFile("pg1661.txt").getLines.take(5).foreach(println)
Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle
 
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever.  You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included

Метод take в целом весьма полезен для любой последовательности и обеспечивает дополнение метода drop, как показано в следующих примерах на простом List [Int] .

01
02
03
04
05
06
07
08
09
10
11
scala> val numbers = 1 to 10 toList
numbers: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
 
scala> numbers.take(3)
res12: List[Int] = List(1, 2, 3)
 
scala> numbers.drop(3)
res13: List[Int] = List(4, 5, 6, 7, 8, 9, 10)
 
scala> numbers.take(3) ::: numbers.drop(3)
res14: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Подсчет слов построчно, сначала попробуйте

Теперь, когда мы увидели, как читать файл и начинать работать с ним построчно, как мы подсчитываем количество вхождений каждого слова? Вспомните из урока 7 и выше, что отправной точкой должна была быть последовательность (Array, List и т. Д.) Строк, в которой каждый элемент представляет собой слово-токен. Чтобы начать двигаться к этому, мы можем просто использовать метод toList на Iterator [String], полученный из getLines .

1
2
scala> val holmes = Source.fromFile("pg1661.txt").getLines.toList
holmes: List[String] = List(The Project Gutenberg EBook of The Adventures of Sherlock Holmes, by Sir Arthur Conan Doyle, (#15 in our series by Sir Arthur Conan Doyle), "", Copyright laws are changing all over the world. Be sure to check the, copyright laws for your country before downloading or redistributing, this or any other Project Gutenberg eBook., "", This header should be the first thing seen when viewing this Project, Gutenberg file.  Please do not remove it.  Do not change or edit the, header without written permission., "", Please read the "legal small print," and other information about the, eBook and Project Gutenberg at the bottom of this file.  Included is, important information about your specific rights and restrictions in, how the file may be used.  You can also find ou...

Теперь у нас есть содержимое файла в виде List [String] , и мы можем продолжать делать с ним полезные вещи. Например, мы могли бы отобразить каждую строку (строки) как последовательности строк, разделенных пробелами.

1
2
scala> val listOfListOfWords = Source.fromFile("pg1661.txt").getLines.toList.map(x => x.split(" ").toList)
listOfListOfWords: List[List[java.lang.String]] = List(List(Project, Gutenberg's, The, Adventures, of, Sherlock, Holmes,, by, Arthur, Conan, Doyle), List(""), List(This, eBook, is, for, the, use, of, anyone, anywhere, at, no, cost, and, with), List(almost, no, restrictions, whatsoever., "", You, may, copy, it,, give, it, away, or), List(re-use, it, under, the, terms, of, the, Project, Gutenberg, License, included), List(with, this, eBook, or, online, at, www.gutenberg.net), List(""), List(""), List(Title:, The, Adventures, of, Sherlock, Holmes), List(""), List(Author:, Arthur, Conan, Doyle), List(""), List(Posting, Date:, April, 18,, 2011, [EBook, #1661]), List(First, Posted:, November, 29,, 2002), List(""), List(Language:, English), List(""), List(""), List(***, START, OF, THIS, PRO...

И, как мы видели в уроке 7 , когда у нас есть список списков, мы можем использовать flatten для создания одного большого списка.

1
2
scala> val listOfWords = listOfListOfWords.flatten
listOfWords: List[java.lang.String] = List(Project, Gutenberg's, The, Adventures, of, Sherlock, Holmes,, by, Arthur, Conan, Doyle, "", This, eBook, is, for, the, use, of, anyone, anywhere, at, no, cost, and, with, almost, no, restrictions, whatsoever., "", You, may, copy, it,, give, it, away, or, re-use, it, under, the, terms, of, the, Project, Gutenberg, License, included, with, this, eBook, or, online, at, www.gutenberg.net, "", "", Title:, The, Adventures, of, Sherlock, Holmes, "", Author:, Arthur, Conan, Doyle, "", Posting, Date:, April, 18,, 2011, [EBook, #1661], First, Posted:, November, 29,, 2002, "", Language:, English, "", "", ***, START, OF, THIS, PROJECT, GUTENBERG, EBOOK, THE, ADVENTURES, OF, SHERLOCK, HOLMES, ***, "", "", "", "", Produced, by, an, anonymous, Project, Gut...

Но теперь вы можете распознать, что это шаблон « карта-затем-сглаживание» , который мы видели ранее, что означает, что мы можем вместо этого использовать FlatMap .

1
2
scala> val flatMappedWords = Source.fromFile("pg1661.txt").getLines.toList.flatMap(x => x.split(" "))
flatMappedWords: List[java.lang.String] = List(Project, Gutenberg's, The, Adventures, of, Sherlock, Holmes,, by, Arthur, Conan, Doyle, "", This, eBook, is, for, the, use, of, anyone, anywhere, at, no, cost, and, with, almost, no, restrictions, whatsoever., "", You, may, copy, it,, give, it, away, or, re-use, it, under, the, terms, of, the, Project, Gutenberg, License, included, with, this, eBook, or, online, at, www.gutenberg.net, "", "", Title:, The, Adventures, of, Sherlock, Holmes, "", Author:, Arthur, Conan, Doyle, "", Posting, Date:, April, 18,, 2011, [EBook, #1661], First, Posted:, November, 29,, 2002, "", Language:, English, "", "", ***, START, OF, THIS, PROJECT, GUTENBERG, EBOOK, THE, ADVENTURES, OF, SHERLOCK, HOLMES, ***, "", "", "", "", Produced, by, an, anonymous, Project,...

Но вас должно немного беспокоить все это: не была ли здесь идея (частично) не читать все строки сразу? Действительно, с тем, что мы сделали выше, как только мы сказали toList на Итераторе, весь файл был прочитан в память. Тем не менее, мы можем обойтись без шага toList и просто напрямую добавить итератор в FlatMap и получить новый итератор по токенам, а не по строкам.

1
2
scala> val flatMappedWords = Source.fromFile("pg1661.txt").getLines.flatMap(x => x.split(" "))
flatMappedWords: Iterator[java.lang.String] = non-empty iterator

Теперь, если мы хотим посчитать слова, мы можем преобразовать это в список и выполнить groupBy трюк mapValues, который мы уже видели (вывод опущен).

1
scala> val counts = Source.fromFile("pg1661.txt").getLines.flatMap(x => x.split(" ")).toList.groupBy(x=>x).mapValues(x=>x.length)

Упс — это сработало, но мы снова перенесли весь файл в память, потому что список, созданный из toList, содержит все строки для файла. Далее мы увидим, как использовать изменяемую карту, чтобы обойти это.

Подсчет слов путем потоковой передачи с помощью итератора и использования изменяемых карт

До сих пор во всех руководствах я придерживался неизменных структур данных, за исключением случаев, когда изменяемые структуры появляются из-за контекста (например, массивы, выходящие из метода toString ). Хорошо по возможности использовать неизменяемые структуры данных, но бывают случаи, когда изменяемые структуры более удобны и, возможно, более уместны.

С неизменными картами, которые мы видели в предыдущем уроке, вы не могли изменить назначение для ключа, а также не могли добавить новый ключ.

01
02
03
04
05
06
07
08
09
10
lettersToNumbers: scala.collection.immutable.Map[java.lang.String,Int] = Map(A -> 1, B -> 2, C -> 3)
 
1
scala> lettersToNumbers("A") = 4
<console>:9: error: value update is not a member of scala.collection.immutable.Map[java.lang.String,Int]
lettersToNumbers("A") = 4
 
scala> lettersToNumbers("D") = 5
<console>:9: error: value update is not a member of scala.collection.immutable.Map[java.lang.String,Int]
lettersToNumbers("D") = 5

Существует еще один вид Map, scala.collection.mutable.Map , который допускает такого рода поведение.

01
02
03
04
05
06
07
08
09
10
11
12
scala> import scala.collection.mutable
import scala.collection.mutable
 
scala> val mutableLettersToNumbers = mutable.Map("A"->1, "B"->2, "C"->3)
mutableLettersToNumbers: scala.collection.mutable.Map[java.lang.String,Int] = Map(C -> 3, B -> 2, A -> 1)
 
scala> mutableLettersToNumbers("A") = 4
 
scala> mutableLettersToNumbers("D") = 5
 
scala> mutableLettersToNumbers
res4: scala.collection.mutable.Map[java.lang.String,Int] = Map(C -> 3, D -> 5, B -> 2, A -> 4)

Он также имеет удобный способ увеличить число, связанное с ключом, используя метод + = .

1
2
3
4
scala> mutableLettersToNumbers("D") += 5
 
scala> mutableLettersToNumbers
res6: scala.collection.mutable.Map[java.lang.String,Int] = Map(C -> 3, D -> 10, B -> 2, A -> 4)

Однако мы не можем использовать этот метод с ключом, который не существует.

1
2
3
scala> mutableLettersToNumbers("E") += 1
java.util.NoSuchElementException: key not found: E
<...stacktrace...>

К счастью, мы можем предоставить дефолт. Вот пример запуска новой карты со значением по умолчанию 0.

01
02
03
04
05
06
07
08
09
10
11
scala> val counts = mutable.Map[String,Int]().withDefault(x=>0)
counts: scala.collection.mutable.Map[String,Int] = Map()
 
scala> counts("Z") += 1
 
scala> counts("Y") += 1
 
scala> counts("Z") += 1
 
scala> counts
res11: scala.collection.mutable.Map[String,Int] = Map(Z -> 2, Y -> 1)

Примечание : когда вы начинаете с некоторыми значениями, уже имеющимися на карте, Scala может определить типы ключей и значений, но при инициализации пустой карты необходимо явно объявить типы ключей и значений.

Имея это в виду , вот как мы можем использовать flatMap и изменяемую карту для подсчета слов в тексте, не считывая весь текст в память.

1
2
3
4
import scala.collection.mutable
val counts = mutable.Map[String, Int]().withDefault(x=>0)
for (token <- scala.io.Source.fromFile("pg1661.txt").getLines.flatMap(x =>x.split("\\s+")))
counts(token) += 1

Создав таким образом Карту счетчиков, мы можем преобразовать ее в неизменяемую Карту с помощью метода toMap, как только мы закончим добавление элементов.

1
2
3
scala> val fixedCounts = counts.toMap
fixedCounts: scala.collection.immutable.Map[String,Int] = Map(wood-work, -> 1,
<...output truncated...>

Теперь мы не можем изменять значения на fixedCounts , что имеет преимущества во многих контекстах, например, мы не можем случайно уничтожить значения или добавить нежелательные ключи, и есть (положительные) последствия для параллельной обработки.

1
2
3
4
scala> fixedCounts("Holmes") = 0
<console>:13: error: value update is not a member of scala.collection.immutable.Map[String,Int]
fixedCounts("Holmes") = 0
^

Чтение файла с URL

Оказывается, scala.io.Source может делать гораздо больше, чем просто читать из файла. Другой пример — чтение URL-адреса для доступа к файлу в Интернете с использованием метода fromURL .

1
2
3
for (line <- Source.fromURL(holmesUrl).getLines)
println(line)

Если вы просто собираетесь снова и снова анализировать один и тот же файл, это, вероятно, не то, что вам нужно — просто скачайте файл и используйте его локально. Однако это может быть весьма полезно в тех случаях, когда вы изучаете ссылки на страницах (например, при обработке данных Википедии или Твиттера) и вам нужно читать содержимое с URL-адресов на лету.

Используйте (вверх) источник

Последнее замечание об итераторах, которые вы получаете с Source.fromFile и Source.fromURL: вы можете проходить их только один раз! Это часть того, что делает их более эффективными — они не хранят весь этот текст в памяти. Так что не удивляйтесь, если вы получите следующее поведение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
scala> val holmesIterator = Source.fromFile("pg1661.txt").getLines
 holmesIterator: Iterator[String] = non-empty iterator
 
scala> holmesIterator.foreach(println)
 
Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle
 
This eBook is for the use of anyone anywhere at no cost and with
 almost no restrictions whatsoever.  You may copy it, give it away or
 re-use it under the terms of the Project Gutenberg License included
 with this eBook or online at www.gutenberg.net
 
<...many lines of output...>
 
This Web site includes information about Project Gutenberg-tm,
 including how to make donations to the Project Gutenberg Literary
 Archive Foundation, how to help produce our new eBooks, and how to
 subscribe to our email newsletter to hear about new eBooks.
 
scala> holmesIterator.foreach(println)
 
<...nothing output!...>

Итак, Итератор исчерпан! Если вы хотите снова просмотреть файл, вам нужно будет запустить новый итератор, как вы это делали в первый раз. Неплохая вещь в том, чтобы оставаться с Итераторами и не преобразовывать их в списки (и, следовательно, переносить все в память), заключается в том, что каждая операция отображения, которую мы выполняем в Итераторе, применяется только к текущему элементу, который мы просматриваем, поэтому нам никогда не нужно читать все файл в память.

Конечно, если у вас есть достаточно небольшой файл для работы, вы можете абсолютно свободно вносить его в список и работать с ним таким образом, если хотите, — это часто будет более удобным, поскольку вы можете создавать шаблоны groupBy и mapValue .

Ссылка: Первые шаги в Scala для начинающих программистов, часть 8 от нашего партнера JCG Джейсона Болдриджа в блоге Bcomposes .

Статьи по Теме :