Kotlin прочитать строку из файла

Обновлено: 04.07.2024

В этом уроке мы рассмотрим, как читать и записывать CSV-файлы в Kotlin, используя библиотеку Apache Commons, с примерами чтения и записи пользовательских объектов.

Вступление

В этой статье мы рассмотрим как читать и записывать CSV-файлы в Kotlin , в частности, с помощью Apache Commons.

Зависимость от Apache Commons

Поскольку мы работаем с внешней библиотекой, давайте продолжим и импортируем ее в наш проект Kotlin. Если вы используете Maven, просто включите commons-csv зависимость:

Или, если вы используете Gradle:

Он будет расположен в разделе /ресурсы/students.csv .

Кроме того, поскольку мы будем считывать эти записи в пользовательские объекты, давайте создадим класс данных:

Чтение CSV-файла в Kotlin

Давайте сначала прочитаем этот файл с помощью BufferedReader , который принимает Путь к ресурсу, который мы хотели бы прочитать:

Затем, как только мы прочитаем файл в буфер, мы сможем использовать сам буфер для инициализации экземпляра CSVParser :

Поскольку мы следуем учебному примеру формата CSV и используем разделитель по умолчанию, запятую ( , ), мы передадим CSVFormat.ПО УМОЛЧАНИЮ в качестве второго аргумента.

Теперь CSVParser является итерируемым , который содержит CSVRecord экземпляры. Каждая строка представляет собой запись CSV. Естественно, затем мы можем выполнить итерацию по экземпляру csvParser и извлечь из него записи:

Для каждой записи CSV вы можете получить соответствующие ячейки с помощью метода get() и ввести индекс ячейки , начиная с 0 . Затем мы можем просто использовать их в конструкторе нашего класса Student data.

Этот код приводит к:

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

Чтение CSV-файла с заголовками в Kotlin

Разумно знать , какие столбцы существуют , но немного меньше знать, в каком порядке они расположены.

Обычно CSV-файлы имеют строку заголовка, в которой указываются имена столбцов, такие как StudentID , Имя и т.д. При создании экземпляра CSVParser , следуя шаблону проектирования Builder , мы можем указать, имеет ли файл, который мы читаем, строку заголовка или нет, в CSVFormat .

По умолчанию формат CSV предполагает, что файл не имеет заголовок. Давайте сначала добавим строку заголовка в наш CSV-файл:

Git Essentials

Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!

Теперь давайте инициализируем экземпляр CSVParser и по пути зададим пару дополнительных параметров в формате CSV :

Таким образом, первая запись (строка) в файле будет рассматриваться как строка заголовка, а значения в этой строке будут использоваться в качестве имен столбцов.

Мы также указали, что регистр заголовка для нас мало что значит, превратив формат в формат без учета регистра.

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

Наконец, как только мы загрузим файл и проанализируем его с помощью этих настроек, вы сможете получить CSVRecord s, как показано ранее:

Это гораздо более снисходительный подход, поскольку нам не нужно знать порядок самих столбцов. Даже если они будут изменены в любой момент времени, CSVParser нас прикроет.

Выполнение этого кода также приводит к:

Написание CSV-файла в Kotlin

Подобно чтению файлов, мы также можем записывать CSV-файлы с помощью Apache Commons. На этот раз мы будем использовать CSVPrinter .

Так же , как CSV-считыватель принимает BufferedReader , CSVPrinter принимает BufferedWriter и формат CSV , который мы хотели бы использовать при записи файла.

Давайте создадим BufferedWriter и создадим экземпляр CSVPrinter :

Метод print Record() экземпляра CSVPrinter используется для записи записей. Он принимает все значения для этой записи и выводит ее в новой строке. Вызов метода снова и снова позволяет нам записывать много записей. Вы можете либо указать каждое значение в списке, либо просто передать список данных.

Нет необходимости использовать метод print Record() для самой строки заголовка, так как мы уже указали его с помощью withHeader() метода CSVFormat . Без указания заголовка там нам пришлось бы распечатать первую строку вручную.

В общем, вы можете использовать csvPrinter вот так:

Не забудьте промыть() и закрыть() принтер после использования.

Поскольку мы работаем здесь со списком студентов и не можем просто распечатать запись таким образом, мы пройдемся по списку студентов, внесем их информацию в новый список и распечатаем этот список данных с помощью метода printRecord() :

В результате получается CSV-файл, содержащий:

Вывод

В этом уроке мы рассмотрели, как читать и записывать CSV-файлы в Kotlin, используя библиотеку Apache Commons.

В этом кратком руководстве мы узнаем о различных способах чтения файла в Kotlin.

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

2. Чтение файла

Давайте сначала создадим входной файл, который будет читать Котлин. Мы создаем файл под названием Kotlin.in и помещаем его в каталог, к которому может получить доступ наш код.

Содержимое файла может быть:

Теперь давайте рассмотрим различные способы чтения этого файла. Мы должны передать полный путь к созданному выше файлу в качестве входных данных для начальных методов и относительный путь в наших ресурсах для последних двух.

2.1. forEachLine

Считывает файл построчно с использованием указанной кодировки (по умолчанию - UTF-8) и вызывает действие для каждой строки:

2.2. useLines

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

После завершения обработки файл закрывается:

2.3. bufferedReader

Возвращает новый BufferedReader для чтения содержимого файла.

Когда у нас есть BufferedReader , мы можем читать все строки в нем:

2.4. readLines

Непосредственно читает содержимое файла в виде списка строк:

Этот метод не рекомендуется использовать для больших файлов.

2.5. inputStream

Создает новый FileInputStream для файла и в результате возвращает его.

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

2.6. readText

Читает все содержимое файла как строку с указанной кодировкой (по умолчанию - UTF-8):

Этот метод не рекомендуется для больших файлов и имеет внутреннее ограничение на размер файла 2 ГБ.

2.7. getResource

Находит ресурс с заданным именем и возвращает объект URL :

Если он найдет ресурс, он вернет URL-адрес , который можно обработать, вызвав метод readText, как показано ранее. Если он не может найти ресурс, он возвращает null . При использовании getResource переданное в fileName не абсолютное имя файла, а имя относительно ресурсов нашего проекта.

2.8. getResourceAsStream

Находит ресурс с заданным именем и возвращает экземпляр InputStream :

Если он найдет ресурс, он вернет InputStream , который можно обработать, как показано ранее, например, получив BufferedReader . Если он не может найти ресурс, он возвращает null . При использовании getResourceAsStream переданное в fileName не абсолютное имя файла, а имя относительно ресурсов нашего проекта.

3. Заключение

В этой статье мы увидели различные способы чтения файла в Kotlin. В зависимости от варианта использования, мы можем выбрать чтение файла построчно или полностью как текст. Мы можем ссылаться на файл абсолютно или найти его среди ресурсов.

Пакет java.io позволяет работать с файлами на трёх разных уровнях:

  1. Уровень отдельных байт. В этом случае файл воспринимается как массив или, точнее, как поток байт. Поток, в отличие от массива, можно только перебирать, с сильно ограниченными возможностями по возвращению назад. Для этой цели имеется тип java.io.InputStream .
  2. Уровень символов. В этом случае файл воспринимается уже как поток символов типа Char , то есть каждые несколько байт файла превращаются в определённый символ — с учётом заданной кодировки файла. Для этой цели имеется тип java.io.InputStreamReader , который внутри себя использует InputStream для чтения байт.
  3. Уровень строк. На этом уровне файл воспринимается как набор строк String , составленных из символов по определённым правилам — чаще всего используется разделение по отдельным строкам файла. Эту роль выполняет тип java.io.BufferedReader , использующий внутри себя InputStreamReader для чтения символов.

При программировании на Java каждый из этих объектов приходится создавать отдельно — вначале InputStream , потом InputStreamReader и, наконец, BufferedReader . Библиотека Котлина позволяет создать любой из этих объектов сразу, используя файл-получатель:

  1. file.inputStream() создаёт байтовый поток.
  2. file.reader() создаёт читатель символов, используя кодировку по умолчанию. file.reader(Charset.forName("CP1251")) создаёт писатель с заданной кодировкой (в данном случае CP1251).
  3. Наконец, file.bufferedReader() создаёт буферизованный читатель строк. Опять-таки, может быть задана нужная кодировка, иначе используется кодировка по умолчанию.

Набор функций у данных трёх объектов различается. У всех у них есть функция close() , закрывающая исходный файл в конце работы с потоком. Также, у них имеется функция высшего порядка use < …​ >, выполняющая описанные в лямбде действия и закрывающая файл в конце своей работы автоматически. Скажем, исходный пример можно было бы переписать с помощью use так:

  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents Loading

Copy raw contents

Copy raw contents

7. Файловые операции

Программы, которые мы пишем, так или иначе должны взаимодействовать с пользователем и внешней средой — операционной системой, устройствами компьютера, сетью Интернет. Простейшим способом взаимодействия является ввод с консоли и вывод на консоль, но сейчас такой способ применяется крайне редко. Более совершенным способом взаимодействия являются файлы — многие программы берут из них настроечную или входную информацию, и используют их для сохранения результатов своей работы или различных настроек. Например, всевозможные редакторы позволяют открывать файлы в определённом формате, просматривать и/или изменять их, сохранять файлы на диске компьютера или в сети Интернет.

В библиотеке Java внутри пакета java.io имеется ряд типов, обеспечивающих возможность работы с файлами, а библиотека Котлина дополняет их некоторыми удобными возможностями. Как обычно, рассмотрим часть этих возможностей на примере.

Эта задача предполагает, что её результат формируется в выходном файле, а не возвращается с помощью оператора return . Тип результата alignFile не указан, в этом случае считается по умолчанию, что в качестве типа результата используется Unit . Это особый тип, придуманный специально для таких случаев (с тем, чтобы все функции в Котлине имели какой-то тип результата). Тип Unit имеет единственное допустимое значение, и оно тоже называется Unit .

Краеугольный тип, используемый для работы с файлами в Котлине — тип java.io.File . В соответствии с названием, он предназначен для различных операций с файлами; объект этого типа соответствует какому-либо реальному файлу, чаще всего находящемуся на жёстком диске. Для создания файла используется специальная функция-конструктор: File(inputName) или File(outputName) в примере. Если в аргументе конструктора указано только имя файла — поиск файла происходит в текущей директории, а если аргумент содержим также путь к файлу — то в директории, указанной этим путём. Специфика конструктора заключается в том, что его имя совпадает с типом объекта, которую он создаёт, и он имеет результат соответствующего типа. Более подробно мы поговорим о конструкторах в следующем уроке.

Обмен данными с файлом может происходить в режиме чтения либо в режиме записи. В режиме чтения информации, заданное имя должно соответствовать уже существующему файлу. Один из способов получения информации из файла — вызов функции file.readLines() . Результат вызова — список строк, из которых состоит файл. Каждый String в этом списке соответствует одной строке файла, строки файла разделяются символом "возврат каретки" и / или "новая строка".

В режиме записи информации, заданное имя может не соответствовать существующему файлу — в этом случае он будет создан. Для записи информации, необходимо создать один из объектов, обеспечивающих такую возможность. В примере, таким объектом является val writer = File(outputName).bufferedWriter() — то есть необходимо вызвать функцию bufferedWriter() на получателе, соответствующем исходному файлу. Как видно из текста примера, writer (писатель) имеет функции writer.newLine() (добавление в файл новой строки), writer.write(string) (добавление в файл заданной строки) и writer.close() (закрытие писателя, выполняется строго ПОСЛЕ выполнения всех остальных действий и фиксирует итоговое состояние файла).

Мы перечислили все файловые операции, присутствующие в исходном примере. Внутри цикла for , каждая из строк файла разбивается по пробелам на слова, с этой целью используется Regex("\\s+") . В currentLineLength накапливается длина текущей строки ВЫХОДНОГО файла. Если в текущей строке достаточно места для очередного слова ВХОДНОГО файла, слово добавляется в текущую строку, в противном случае в файл добавляется перевод строки и слово добавляется в новую строку. Пустые строки входного файла, как и сказано в задании, переносятся в выходной файл без изменений. Добавление в выходной файл нового слова реализовано в локальной функции append .

Отступление: локальные функции

Локальными в Котлине называются функции, определённые внутри другой функции. Локальные функции во многих отношениях подобны обычным функциям, за некоторыми исключениями:

Локальную функцию может вызывать только та функция, в которой она определена (в примере alignFile ), и только после места определения локальной функции

Локальная функция имеет право обращаться к параметрам и переменным внешней функции ( alignFile ). В том числе, она имеет право изменять значения переменных var

За занавесом: чтение из файла

Пакет java.io позволяет работать с файлами на трёх разных уровнях:

Уровень отдельных байтов. В этом случае файл воспринимается как массив или, точнее, как поток байтов. Поток, в отличие от массива, можно только перебирать, с сильно ограниченными возможностями по возвращению назад. Для этой цели имеется тип java.io.InputStream .

Уровень символов. В этом случае файл воспринимается уже как поток символов типа Char , то есть каждые несколько байт файла превращаются в определённый символ — с учётом заданной кодировки файла. Для этой цели имеется тип java.io.InputStreamReader , который внутри себя использует InputStream для чтения байт.

Уровень строк. На этом уровне файл воспринимается как набор строк String , составленных из символов по определённым правилам — чаще всего используется разделение по отдельным строкам файла. Эту роль выполняет тип java.io.BufferedReader , использующий внутри себя InputStreamReader для чтения символов.

При программировании на Java каждый из этих объектов приходится создавать отдельно — вначале InputStream , потом InputStreamReader и, наконец, BufferedReader . Библиотека Котлина позволяет создать любой из этих объектов сразу, используя файл-получатель:

file.inputStream() создаёт байтовый поток.

file.reader() создаёт читатель символов, используя кодировку по умолчанию. file.reader(Charset.forName("CP1251")) создаёт писатель с заданной кодировкой (в данном случае CP1251).

Наконец, file.bufferedReader() создаёт буферизованный читатель строк. Опять-таки, может быть задана нужная кодировка, иначе используется кодировка по умолчанию.

Набор функций у данных трёх объектов различается. У всех у них есть функция close() , закрывающая исходный файл в конце работы с потоком. Для гарантированного вызова функции close() часто применяется конструкция try…​finally…​ :

Ещё лучше с той же целью использовать функцию высшего порядка use < …​ >. Эта функция выполняет описанные в лямбде действия и закрывает файл в конце своей работы автоматически. Скажем, код выше можно было бы переписать с помощью use так:

Код самой функции use выглядит примерно следующим образом

Здесь исходный BufferedWriter в лямбде становится параметром it . Заметим, что при использовании use исходный файл будет закрыт как при корректном завершении функции, так и при возникновении исключения.

Кроме этого, каждый объект обладает своими методами для чтения информации:

inputStream.read() читает из InputStream очередной байт, возвращая его в виде результата типа Int . Если файл закончен, результат этой функции будет -1. inputStream.read(byteArray) читает сразу несколько байт, записывая их в массив байт (число прочитанных байт равно размеру массива). inputStream.read(byteArray, offset, length) записывает в byteArray length байт, начиная с индекса offset .

reader.read() читает из InputStreamReader очередной символ, возвращая его в виде результата типа Int . Здесь используется именно Int , а не Char , так как, во-первых, символ в общем случае может не поместиться в двухбайтовые тип и, во-вторых, чтобы вернуть -1 в случае неудачи. Есть аналогичные методы для чтения символьного массива (НЕ строки) с возможным указанием смещения и числа символов — см. выше про байтовый массив.

bufferedReader.readLine() читает из BufferedReader очередную строку (до перевода строки). bufferedReader.readLines() читает сразу же все строки. Есть ряд других методов для работы со строками по отдельности.

Следует отметить, что все функции чтения информации могут бросить исключение IOException в том случае, если чтение по какой-либо причине невозможно (например, если файл не существует или недоступен).

В примере, мы вообще не создавали bufferedReader , а использовали функцию file.readLines() . Она создаёт bufferedReader внутри себя и обращается к его функции readLines() . После чтения последней строки файл закрывается. Вариант вызова file.readLines(charset) позволяет дополнительно указать кодировку, в которой следует читать файл. Есть и другие варианты высокоуровневых функций чтения файла:

file.forEachLine < line → …​ >. Эта функция высшего порядка предполагает чтение строк файла по одной, и выполнение операции, указанной в лямбде (…​) для каждой из этих строк. Достоинство такого подхода в том, что здесь мы не храним в памяти весь список строк из файла, как делает file.readLines() . Это может быть важно, если размер файла большой. С другой стороны, подобный вариант предполагает обработку строк по очереди, от предыдущей к следующей, и не обеспечивает никакой возможности возврата к уже обработанной строке. Вариант вызова file.forEachLine(charset) < line → …​ >позволяет дополнительно указать кодировку.

За занавесом: запись в файл

Запись в файл использует те же три уровня: байты OutputStream , символы OutputStreamWriter и строки BufferedWriter . Для записи байт либо символов используются функции write , аргументом которых может являться целое число (байт или код символа), или массив (опять-таки байтов или символов). Эти функции не имеют результата и бросают IOException , если файл недоступен для записи. BufferedWriter может использовать функцию write также для записи строк. Как и все три вида потоков чтения, потоки записи необходимо закрывать после использования с помощью close() или use < …​ >.

Сверх этого, для записи часто используется так называемый поток печати PrintStream . В Котлине его можно создать из файла, используя функцию file.printStream() . Поток печати расширяет обычный байтовый поток рядом дополнительных возможностей:

printStream.println(…​) — вывод заданной строки или строкового представления с последующим переходом на новую строку.

printStream.print(…​) — то же, но без перехода на новую строку.

printStream.format(formatString, …​) — форматированный вывод (происходит по принципу, описанном в разделе 6).

Откройте файл src/lesson7/task1/Files.kt в проекте KotlinAsFirst . Он содержит ряд задач, каждая из которых предполагает наличие входного и/или выходного файла. Решите хотя бы одну-две из имеющихся задач, используя описанные в этом разделе приёмы. Обратите внимание на задачи, помеченные как "Сложная" или "Очень сложная", попробуйте решить одну из них. Перед решением задач про HTML-файлы полезно прочитать раздел, посвящённый Kotlin DSL (10.2).

Протестируйте свою реализацию, используя тесты из test/lesson7/task1/Tests.kt . Обратите внимание, что тесты используют готовые входные файлы, расположенные в директории input нашего проекта. Убедитесь, что тесты успешно проходят, обязательно создайте два-три дополнительных теста. Постарайтесь внутри этих тестов проверить какие-либо необычные ситуации, которые могут возникнуть в выбранной вами задаче.

Поздравляем! Выполнив упражнения по этому разделу, вы успешно завершили базовую часть нашего курса. Если вы студент высшей школы INSYS, для получения зачёта вам необходима сумма в 55 баллов за лучшие пять уроков курса (чаще всего это уроки 3-7). Если набранная сумма недостаточна, вы можете как дорешать задачи из этого или предыдущих уроков, так и решить несколько задач из урока 8 или 9.

Если вас интересует получение сертификата Coursera, прочитайте соответствующий параграф ниже.

Читайте также: