Найти тему
NM.getJavaPractice();

Files and network

Оглавление
Цель: научиться обходить дерево папок рекурсивно, читать файлы форматов CSV и JSON, работать с содержащимися в них данными, получать данные из HTML-страниц и создавать JSON-файлы
Цель: научиться обходить дерево папок рекурсивно, читать файлы форматов CSV и JSON, работать с содержащимися в них данными, получать данные из HTML-страниц и создавать JSON-файлы

❗️ Задача:

Напишите программу, которая будет собирать данные из разных источников и записывать два JSON-файла. Парсинг разных данных должен происходить в разных классах. Имена классов и их методов придумайте самостоятельно. По мере реализации проверяйте работу каждого созданного класса. В программе должны быть следующие классы:

  • Класс парсинга веб-страницы. В нём должно происходить (реализуйте каждую операцию в отдельных методах):
  • ㅤ° получение HTML-кода страницы «Список станций Московского метрополитена» с помощью библиотеки jsoup;
  • ㅤ° парсинг полученной страницы и получение из неё следующих данных (создайте для каждого типа данных отдельные классы):
  • ㅤㅤㅤ- линии московского метро (имя и номер линии, цвет не нужен);
  • ㅤㅤㅤ- станции московского метро (имя станции и номер линии).
  • Класс поиска файлов в папках. В методах этого класса необходимо реализовать обход папок, лежащих в архиве. Разархивируйте его и напишите код, который будет обходить все вложенные папки и искать в них файлы форматов JSON и CSV (с расширениями *.json и *.csv). Метод для обхода папок должен принимать путь до папки, в которой надо производить поиск.
  • Класс парсинга файлов формата JSON. Изучите структуру JSON-файлов, лежащих в папках, и создайте класс(ы) для хранения данных из этих файлов. Напишите код, который будет принимать JSON-данные и выдавать список соответствующих им объектов.
  • Класс парсинга файлов формата CSV. Изучите структуру CSV-файлов, лежащих в папках, и создайте класс(ы) для хранения данных из этих файлов. Напишите код, который будет принимать CSV-данные и выдавать список соответствующих им объектов.
  • Класс, в который можно добавлять данные, полученные на предыдущих шагах, и который создаёт и записывает на диск два JSON-файла:
  • ㅤ° со списком станций по линиям и списком линий (файл map.json) в следующем формате:
-2

ㅤ° файл stations.json со свойствами станций в следующем формате:

-3

Если каких-то свойств для станции нет, то в файле не должно быть соответствующих ключей.

  • Обратите внимание на то, что данные в разных источниках могут пересекаться:
  • ㅤ° Одни и те же станции у разных веток при парсинге с сайта. Это могут быть как разные станции (например, в Москве две станции “Арбатская” и две станции “Смоленская”), так и одни и те же, если это станции пересадок.
  • ㅤ° Данные о датах открытия для одних и тех же станций в файлах. Если даты отличаются, то это разные станции с одинаковыми названиями.
  • ㅤ° Разные значения глубины для одних и тех же станций. Здесь приоритетной считайте значения с наибольшей глубиной.

✒️ Приступим!

-4

Maven

📌 Добавим зависимости:

↘️

<dependencies>
ㅤ<dependency>
ㅤㅤ<groupId>com.googlecode.json-simple</groupId>
ㅤㅤ<artifactId>json-simple</artifactId>
ㅤㅤ<version>1.1.1</version>
ㅤ</dependency>
ㅤ<dependency>
ㅤㅤ<groupId>org.jsoup</groupId>
ㅤㅤ<artifactId>jsoup</artifactId>
ㅤㅤ<version>1.15.4</version>
ㅤ</dependency>
ㅤ<dependency>
ㅤㅤ<groupId>com.google.code.gson</groupId>
ㅤㅤ<artifactId>gson</artifactId>
ㅤㅤ<version>2.10.1</version>
ㅤ</dependency>
</dependencies>

⭐️ P.S.:

  • jsoup - это библиотека для работы с HTML-документами. Она позволяет извлекать данные из HTML-страниц, а также создавать HTML-документы, основанные на Java. Jsoup может использоваться для извлечения информации из веб-страниц, например, для реализации парсера новостных сайтов.
  • json-simple - это библиотека для работы с JSON (JavaScript Object Notation), форматом данных, который широко используется в веб-приложениях для обмена данными. Эта библиотека обеспечивает простой способ чтения и записи JSON-данных в Java-приложении.
  • gson - это библиотека для сериализации и десериализации объектов Java в JSON и обратно. Она обеспечивает удобный способ преобразования объектов Java в JSON-формат и обратно, что может быть полезным, например, при работе с веб-сервисами, которые передают данные в формате JSON. (она нам понадобится для красивой записи в наши Json файлы)

Pojo классы

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

📌 Изучив данные на html страннице или вспомнив задание, создадим:

-5

✎ Класс станции московского метро (имя станции, номер линии и информация о наличии пересадки)

↘️

public class Station {
ㅤprivate String name;
ㅤprivate String line;
ㅤprivate Boolean hasConnection;
ㅤpublic Station(String name, String line, Boolean hasConnection) {
ㅤㅤthis.name = name;
ㅤㅤthis.line = line;
ㅤㅤthis.hasConnection = hasConnection;
ㅤ}
ㅤ@Override
ㅤpublic String toString() {
ㅤㅤreturn "Station{" + "name='" + name + '\'' + ", line='" + line + '\'' + '}';
ㅤ}
}

✎ Класс линии московского метро (имя и номер линии, цвет по условию задачи нам не нужен)

↘️

public class Line {
ㅤprivate String name;
ㅤprivate String number;
ㅤpublic Line(String name, String number) {
ㅤㅤthis.name = name;
ㅤㅤthis.number = number;
ㅤ}
ㅤ@Override
ㅤpublic String toString() {
ㅤㅤreturn "Line{" + "name='" + name + '\'' + ", number='" + number + '\'' + ㅤ'}';
ㅤ}
}

📌 Изучив JSON файлы создадим:

-6

✎ Класс глубины станции (имя, глубина)

↘️

public class StationDepth {
ㅤprivate String name;
ㅤprivate String depth;
ㅤpublic StationDepth(String name, String depth) {
ㅤㅤthis.name = name;
ㅤㅤthis.depth = depth;
ㅤ}
@Override
ㅤpublic String toString() {
ㅤㅤreturn "StationDepth{" + "name='" + name + '\'' + ", depth='" + depth + '\'' + '}';
ㅤ}
}

📌 Изучив CSV файлы создадим:

-7

✎ Класс даты открытия станции (имя, дата)

↘️

public class StationDate {
ㅤprivate String name;
ㅤprivate String date;
ㅤpublic StationDate (String name, String date) {
ㅤㅤthis.name = name;
ㅤㅤthis.date = date;
ㅤ}
@Override
ㅤpublic String toString() {
ㅤㅤreturn "StationsDates{" + "name='" + name + '\'' + ", date='" + date + '\'' + '}';
ㅤ}
}

ParseHtmlPage

📌 Создадим класс ParseHtmlPage для парсинга веб-страницы, в котором:

❶ Объявим приватные поля:

private String url = "https://skillbox-java.github.io/"; - здесь будем хранить ссылку на необходимую веб-страницу

private String pathToHtml = "data/metro_moscow.html"; - путь, куда мы будем сохранять полученный со страницы html файл

❷ Для хранения информации о линиях московского метро и станциях на каждой линии и с целью дальнейшего получения данной информации объявим поля с модификатором доступа public:

public List<Line> lines; - коллекция List хранящая в себе объекты класса Line

public List<Station> stations; - коллекция List хранящая в себе объекты класса Station

❸ Реализуем необходимые нам методы:

📌 В первую очередь нам понадобится метод, который загрузит HTML-страницу по нашей ссылке url, и сохранит эти данные в файл pathToHtml. Метод getHtmlPage().

private void getHtmlPage() {
}

✎ Подключение к сайту будем осуществлять используя библиотеку Jsoup, получая объект Document, в котором будем хранится HTML-код страницы

Document htmlFile = Jsoup.connect(url).get();

✎ Для записи полученного кода в локальный файл воспользуемся классом PrintWriter, создав его объект, на вход которого передадим путь для сохранения pathToHtml

PrintWriter writer = new PrintWriter(pathToHtml);

⭐️ P.S.: так же необходимо обработать исключение IOException, окружив данный код конструкцией try-catch

✎ Записывать код в локальный файл будем методом write(), на вход которого необходимо передавать строку String. Для преобразования объекта Document в строку используем метод outerHtml()

writer.write(htmlFile.outerHtml());

✎ После успешной запись необходимо закрыть объект PrintWriter с помощью метода close()

writer.close();

↘️ Итого

private void getHtmlPage() {
ㅤtry {
ㅤㅤDocument htmlFile = Jsoup.connect(url).get();
ㅤㅤPrintWriter writer = new PrintWriter(pathToHtml);
ㅤㅤwriter.write(htmlFile.outerHtml());
ㅤㅤwriter.close();
ㅤ} catch (IOException e) {
ㅤㅤe.printStackTrace();
ㅤ}
}

📌Далее реализуем метод, который будет непосредственно парсить, полученную HTML-страницу и сохранять требуемые данные (Информация и о линиях и станциях). Метод parseHtml().

private void parseHtml() {

}

✎ В начале метода инициализируем наши списки lines и stations, в которые будем "класть" полученные данные

lines = new ArrayList<>();
stations = new ArrayList<>();

✎ Для парсинга содержимого HTML файла, необходимо используя статический метод parse() класса Jsoup получить объект класса Document. На вход данного метода необходимо передавать объект класса File и название кодировки (у нас UTF-8), поэтому:

  • Сначала создадим объект класса File, используя путь к нему pathToHtml
File htmlFile = new File(pathToHtml);
  • После чего будем проверять, есть ли у данного файла содержимое и если он пустой, будем вызывать созданный нами метод getHtmlPage()
if (htmlFile.length() == 0) {
ㅤgetHtmlPage();
}
  • Теперь можем создавать объект класса Document:
Document doc = Jsoup.parse(htmlFile, "UTF-8");

⭐️ P.S.: так же необходимо обработать исключение IOException, окружив данный код конструкцией try-catch

✎ Для получения информации о линиях, необходимо внимательно изучить HTML-код странницы, в результате чего становится понятно, что нам требуется получить все элементы (Elements) span с классом js-metro-line.

-8
Elements linesInfo = doc.select("span.js-metro-line");

✎ После чего перебрать их и добавить каждый найденный элемент в список lines в виде объекта класса Line, который содержит информацию о номере линии и ее названии.

for (Element element : linesInfo) {
ㅤlines.add(new Line(element.text(), element.attr("data-line")));
}

✎ Для получения информации о станциях, также изучим HTML-код страницы

-9
-10
  • Становится понятно, что информация о номере линии находится в классе js-metro-stations, а имя и наличие/отсутствие пересадки в классе single-station
  • Исходя из этого понимаем, что нам потребуется найти все элементы с классом js-metro-stations, содержащие информацию о номере линий станций метро в этом классе и для каждого найденного элемента найти все элементы single-station, содержащих информацию о конкретной станции
  • После для каждой станции создадим объект класса Station, содержащий ее название, номер линии и наличие пересадочной станции и добавим этот объект в список stations
Elements stationsPerLine = doc.getElementsByClass("js-metro-stations");
for (Element element : stationsPerLine) {
ㅤElements stationsInfo = element.getElementsByClass("single-station");
ㅤfor (Element station : stationsInfo) {
ㅤㅤString name = station.getElementsByClass("name").text();
ㅤㅤString line = element.attr("data-line");
ㅤㅤBoolean hasConnection = station.select("span.t-icon-metroln").hasAttr("title");
ㅤㅤstations.add(new Station(name, line, hasConnection));
ㅤ}
}

↘️ Итого

private void parseHtml() {
ㅤlines = new ArrayList<>();
ㅤstations = new ArrayList<>();
ㅤFile htmlFile = new File(pathToHtml);
ㅤif (htmlFile.length() == 0) {
ㅤㅤgetHtmlPage();
ㅤ}
ㅤtry {
ㅤㅤDocument doc = Jsoup.parse(htmlFile, "UTF-8");
ㅤㅤElements linesInfo = doc.select("span.js-metro-line");
ㅤㅤfor (Element element : linesInfo) {
ㅤㅤㅤlines.add(new Line(element.text(), element.attr("data-line")));
ㅤㅤ}
ㅤㅤElements stationsPerLine = doc.getElementsByClass("js-metro-stations");
ㅤㅤfor (Element element : stationsPerLine) {
ㅤㅤㅤElements stationsInfo = element.getElementsByClass("single-station");
ㅤㅤㅤfor (Element station : stationsInfo) {
ㅤㅤㅤㅤString name = station.getElementsByClass("name").text();
ㅤㅤㅤㅤString line = element.attr("data-line");
ㅤㅤㅤㅤBoolean hasConnection = station.select("span.t-icon-metroln").hasAttr("title");
ㅤㅤㅤㅤstations.add(new Station(name, line, hasConnection));
ㅤㅤㅤ}
ㅤㅤ}
ㅤ} catch (IOException e) {
ㅤㅤe.printStackTrace();
ㅤ}
}

📌 Для дальнейшего использования создадим:

✎ конструктор нашего класса, который будет вызывать метод parseHtml() для парсинга HTML-страницы

public ParseHtmlPage() {
ㅤparseHtml();
}

✎ и геттеры, которые будут возвращать наши списки линий lines и списки станций stations

public List<Line> getLines() {
ㅤreturn lines;
}
public List<Station> getStations() {
ㅤreturn stations;
}

FilesSearch

📌 Создадим класс FilesSearch для обхода папок и поиск в них файлов формата JSON и CSV, в котором:

❶ Объявим приватные поля:

private final String folderPath = "stations-data/"; - тут будем хранить путь к папкам, которые необходимо обойти

private String JSONFilesAbsolutePath; - сюда будем записывать абсолютные пути ко всем найденным файлам JSON

private String CSVFilesAbsolutePath; - сюда будем записывать абсолютные пути ко всем найденным файлам CSV

❷ Реализуем необходимые нам методы:

📌 Метод осуществляющий рекурсивный поиск всех файлов с расширением ".json", на вход которого будем передавать путь к нашим папкам. Метод getJSONFiles(String folderPath).

✎ Инициализируем переменную JSONFilesAbsolutePath пустой строкой

JSONFilesAbsolutePath = "";

✎ Для обеспечения реализации поставленных задач потребуется объект класса File, создадим его с указанием на папку, переданную в качестве параметра метода.

File folder = new File(folderPath);

✎ После чего созданный объект класса File необходимо проверить на предмет того, является ли он файлом (может быть папкой) и имеет ли он искомое расширение ".json", если да, то абсолютный путь к этому файлу добавляется в строку JSONFilesAbsolutePath

if (folder.isFile() && folder.getName().endsWith(".json")) {
ㅤJSONFilesAbsolutePath += folder.getAbsolutePath() + "\n";
}

✎ На тот случай если наш объект оказался не файлом необходимо реализовать рекурсивный поиск всех файлов в заданной папке и ее подпапках, используя метод listFiles() класса File

File[] files = folder.listFiles();

⭐️ P.S.: метод listFiles() может вернуть null, например при доступе к файлам в защищенных папках, поэтому необходимо обработать исключение NullPointerException, окружив данный код конструкцией try-catch

✎ Если метод listfiles() вернул массив файлов, то для каждого файла из этого массива вызовем метод getJSONfiles(), передавая в качестве параметра абсолютный путь к нему используя метод getAbsolutePath(), результат работы метода для каждого файла будем добавлять в строку JSONFilesAbsolutePath

File[] files = folder.listFiles();
for (File file : files) {
ㅤJSONFilesAbsolutePath += getJSONfiles(file.getAbsolutePath());
}

✎ В конце как результат работы метода будем возвращать строку JSONFilesAbsolutePath со списком абсолютных путей к найденным файлам с расширением ".json".

return JSONFilesAbsolutePath;

↘️ Итого

private String getJSONfiles(String folderPath) {
ㅤJSONFilesAbsolutePath = "";
ㅤFile folder = new File(folderPath);
ㅤif (folder.isFile() && folder.getName().endsWith(".json")) {
ㅤㅤJSONFilesAbsolutePath += folder.getAbsolutePath() + "\n";
ㅤ}
ㅤtry {
ㅤㅤFile[] files = folder.listFiles();
ㅤㅤfor (File file : files) {
ㅤㅤㅤJSONFilesAbsolutePath += getJSONfiles(file.getAbsolutePath());
ㅤㅤ}
ㅤ} catch (NullPointerException e) {
ㅤㅤe.fillInStackTrace();
ㅤ}
ㅤreturn JSONFilesAbsolutePath;
}

📌 Метод осуществляющий рекурсивный поиск всех файлов с расширением ".csv", на вход которого будем передавать путь к нашим папкам. Метод getCSVFiles(String folderPath).

✎ Сделаем его по аналогии предыдущего метода getJSONFiles()

↘️

private String getCSVfiles(String folderPath) {
ㅤCSVFilesAbsolutePath = "";
ㅤFile folder = new File(folderPath);
ㅤif (folder.isFile() && folder.getName().endsWith(".csv")) {
ㅤㅤCSVFilesAbsolutePath += folder.getAbsolutePath() + "\n";
ㅤ}
ㅤtry {
ㅤㅤFile[] files = folder.listFiles();
ㅤㅤfor (File file : files) {
ㅤㅤㅤCSVFilesAbsolutePath += getCSVfiles(file.getAbsolutePath());
ㅤㅤ}
ㅤ} catch (NullPointerException e) {
ㅤㅤe.fillInStackTrace();
ㅤ}
ㅤreturn CSVFilesAbsolutePath;
}

📌 Для дальнейшего использования создадим:

✎ конструктор нашего класса, который будет вызывать созданные методы getJSONfiles() и getCSVfiles()

public FilesSearch() {
ㅤgetJSONfiles(folderPath);
ㅤgetCSVfiles(folderPath);
}

✎ и геттеры, которые будут возвращать полученные абсолютные пути ко всем найденным файлам JSON и CSV

public String getJSONFilesAbsolutePath() {
ㅤreturn JSONFilesAbsolutePath;
}
public String getCSVFilesAbsolutePath() {
ㅤreturn CSVFilesAbsolutePath;
}

ParseJsonFile

📌 Создадим класс ParseJsonFile для парсинга файлов формата JSON, в котором:

❶ Объявим приватные поля:

private List<String> jsonString; - список, в котором, будем хранить содержимое JSON-файлов в виде строк

private List<StationDepth> stationsDepth; - список, для хранения найденной информации о глубине станции, в который будем "класть" объекты класса StationDepth

private String sameName1 = "Смоленская"; - станция, у которой есть одноименное название, понадобится для фильтрации

private String sameName2 = "Арбатская"; - станция, у которой есть одноименное название, понадобится для фильтрации

❷ Реализуем необходимые нам методы:

📌 Для начала нам потребуется получить список JSON-строк из набора найденных файлов с расширением .json, напишем для этого метод getJsonInString(), который будет возвращать список строк List<String>

private List<String> getJsonInString() {

}

✎ Инициализируем наш список в котором, будем хранить содержимое JSON-файлов в виде строк

jsonString = new ArrayList<>();

✎ Далее нам потребуются пути к файлам .json, ранее для этого мы написали код класса FilesSearch, создадим объект этого класса и получим строковый массив путей к файлам json путем вызова метода getJSONFilesAbsolutePath() у объекта класса FilesSearch (при этом разделяя на строки по символу новой строки)

FilesSearch filesSearch = new FilesSearch();
String[] paths = filesSearch.getJSONFilesAbsolutePath().split("\n");

✎ Далее нам необходимо прочесть содержимое каждого из файлов, для этого мы в цикле обойдем все элементы массива paths,получая пути к файлам .json и используя метод Files.readString() "прочтем" эти файлы.

Результат метода readString() сохраняется в виде строки, которую будем добавлять в наш список jsonString с помощью метода add()

for (String path : paths) {
ㅤjsonString.add(Files.readString(Paths.get(path)));
}

⭐️ P.S.: в методе readString() класса Files необходимо обработать исключение IOException, окружив этот код конструкцией try-catch

✎И как результат работы нашего метода вернем список jsonString, содержащий все JSON-строки, прочитанные из файлов

return jsonString;

↘️ Итого

private List<String> getJsonInString() {
ㅤjsonString = new ArrayList<>();
ㅤFilesSearch filesSearch = new FilesSearch();
ㅤString[] paths = filesSearch.getJSONFilesAbsolutePath().split("\n");
ㅤfor (String path : paths) {
ㅤㅤtry {
ㅤㅤㅤjsonString.add(Files.readString(Paths.get(path)));
ㅤㅤ} catch (IOException e) {
ㅤㅤㅤe.printStackTrace();
ㅤㅤ}
ㅤ}
ㅤreturn jsonString;
}

📌 Далее напишем код для метода, который будет осуществлять парсинг данных из JSON-файлов, полученных с помощью метода getJsonInString(). Метод parse()

✎ Инициализируем наш список stationsDepth

stationsDepth = new ArrayList<>();

✎ Для парсинга JSON-строк нам потребуется создать экземпляр класса JSONParser библиотеки json-simple

JSONParser jsonParser = new JSONParser();

✎ Используя ранее созданный метод getJsonInString() в цикле for переберем все полученные строки и каждую из них с помощью jsonParser пропарсим в JSONArray (так как наш самый главный объект - массив это можно наблюдать, изучив структуру наших .json файлов)

for (String string : getJsonInString()) {
ㅤJSONArray jsonData = (JSONArray) jsonParser.parse(string);
.........

✎ Далее для каждого JSON-объекта в JSONArray парсится информация о станции метро, включая название и глубину. Объект StationDepth создается с помощью полученных значений и добавляется в список stationsDepth

for (String string : getJsonInString()) { //повтор
ㅤJSONArray jsonData = (JSONArray) jsonParser.parse(string); //повтор
ㅤfor (Object infoDepth : jsonData) {
ㅤㅤJSONObject stationDepth = (JSONObject) infoDepth;
ㅤㅤString name = (String) stationDepth.get("station_name");
ㅤㅤString depth = (String) stationDepth.get("depth");
ㅤㅤString depth1 = depth.replaceAll(",", ".");
ㅤㅤString depth2 = depth1.replaceAll("\\?", "-0");
ㅤㅤstationsDepth.add(new StationDepth(name, depth2));
}
}

✏️ При этом глубина станции может содержать запятые вместо точек и знаки вопроса, поэтому заменим их соответствующим образом на точки и значение -0 (нам это потребуется, что бы сравнивать повторяющиеся станции по значением глубины, по условию задачи, считаем приоритетным то значение, у которого наибольшая глубина).

⭐️ P.S.: Дополнительно, конструкцией try-catch необходимо обработать все возможные исключения (ClassCastException, ParseException или NullPointerException)

📌 Теперь, из списка stationsDepth необходимо удалить дубликаты станций с одинаковым именем и оставить только ту станцию, у которой наибольшая глубина, для этих целей напишем метод listFormatted().

✎ Из-за выбранной коллекции типа List пришлось воспользоваться вложенными циклами и реализовать данную логику по следующему алгоритму

  1. Перебираем все элементы списка stationsDepth, используя цикл for, начиная с индекса 0.
  2. Получаем имя и глубину текущей станции, преобразуя глубину в тип Double.
  3. Вложенный цикл for перебирает все элементы списка stationsDepth еще раз, начиная с индекса 0.
  4. Получаем имя и глубину другой станции, преобразуя ее глубину в тип Double.
  5. Если имена текущей и другой станций равны и они не равны значениям sameName1 и sameName2, то выполняется следующее действие:
  6. Если глубина текущей станции больше глубины другой станции, то другая станция удаляется из списка stationsDepth. В противном случае удаляется текущая станция из списка.
  7. В результате в списке stationsDepth остаются только станции с уникальными именами, а для каждого имени остается станция с наибольшей глубиной.

↘️

private void listFormatted() {
ㅤfor (int i = 0; i < stationsDepth.size(); i++) {
ㅤㅤString name = stationsDepth.get(i).getName();
ㅤㅤDouble depth = Double.parseDouble(stationsDepth.get(i).getDepth());
ㅤㅤfor (int j = 0; j < stationsDepth.size(); j++) {
ㅤㅤㅤString anotherName = stationsDepth.get(j).getName();
ㅤㅤㅤDouble anotherDepth = Double.parseDouble(stationsDepth.get(j).getDepth());
ㅤㅤㅤif (name.equals(anotherName) && !name.equals(sameName1) && !name.equals(sameName2)) {
ㅤㅤㅤㅤif (depth.compareTo(anotherDepth) > 0) {
ㅤㅤㅤㅤㅤstationsDepth.remove(j);
ㅤㅤㅤㅤ} else {
ㅤㅤㅤㅤㅤstationsDepth.remove(i);
ㅤㅤㅤㅤ}
ㅤㅤㅤ}
ㅤㅤ}
ㅤ}
}

⭐️ P.S.: в принципе данный код можно упростить, используя итераторы вместо индексов для обхода коллекции stationsDepth и метод removeIf() ля удаления элементов, удовлетворяющих определенному условию

📌 Для дальнейшего использования создадим:

✎ конструктор нашего класса, который будет вызывать созданные методы parse() и listFormatted() последовательно

public ParseJsonFile() {
ㅤparse();
ㅤlistFormatted();
}

✎ геттер для получения списка с полученной информацией по глубинам станций

public List<StationDepth> getStationsDepth() {
ㅤreturn stationsDepth;
}

ParseCsvFile

📌 Создадим класс ParseCsvFile для парсинга файлов формата CSV, он сделан по аналогии предыдущего класса ParseJsonFile, поэтому приложу код с небольшим описанием

public class ParseCsvFile {
ㅤprivate List<String> csvLines;
ㅤprivate List<StationDate> stationsDates;
ㅤpublic ParseCsvFile() {
ㅤㅤparse();
ㅤㅤlistFormatted();
ㅤ}
ㅤprivate void parse() {
ㅤㅤstationsDates = new ArrayList<>();
ㅤㅤList<String> lines = getCsvInLines();
ㅤㅤfor (String line : lines) {
ㅤㅤㅤString[] tokens = line.split(",");
ㅤㅤㅤif (tokens.length != 2) {
ㅤㅤㅤㅤSystem.out.println("Wrong line = " + line);
ㅤㅤㅤ}
ㅤㅤㅤstationsDates.add(new StationDate(tokens[0], tokens[1]));
ㅤㅤ}
ㅤ}
ㅤprivate List<String> getCsvInLines() {
ㅤㅤList<String> lines;
ㅤㅤcsvLines = new ArrayList<>();
ㅤㅤFilesSearch filesSearch = new FilesSearch();
ㅤㅤString[] paths = filesSearch.getCSVFilesAbsolutePath().split("\n");
ㅤㅤfor (String path : paths) {
ㅤㅤㅤtry {
ㅤㅤㅤㅤlines = Files.readAllLines(Path.of(path));
ㅤㅤㅤㅤlines.remove(0);
ㅤㅤㅤㅤcsvLines.addAll(lines);
ㅤㅤㅤ} catch (IOException e) {
ㅤㅤㅤㅤe.printStackTrace();
ㅤㅤㅤ}
ㅤㅤ}
ㅤㅤㅤreturn csvLines;
ㅤ}
ㅤprivate void listFormatted() {
ㅤㅤfor (int i = 0; i < stationsDates.size(); i ++) {
ㅤㅤㅤString name = stationsDates.get(i).getName();
ㅤㅤㅤString date = stationsDates.get(i).getDate();
ㅤㅤㅤfor (int j = 0; j <stationsDates.size(); j++) {
ㅤㅤㅤㅤString anotherName = stationsDates.get(j).getName();
ㅤㅤㅤㅤString anotherdate = stationsDates.get(j).getDate();
ㅤㅤㅤㅤif (name.equals(anotherName) && date.equals(anotherdate)) {
ㅤㅤㅤㅤㅤstationsDates.remove(j);
ㅤㅤㅤㅤ}
ㅤㅤㅤ}
ㅤㅤ}
ㅤ}
ㅤpublic List<StationDate> getStationsDates() {
ㅤㅤreturn stationsDates;
ㅤ}
}

Класс имеет два поля: csvLines, хранящий список строк из CSV-файлов, и stationsDates, в котором будем хранить список объектов типа StationDate.

Метод getCsvInLines() создает пустой список lines (нам он нужен потому что в файлах CSV есть заголовки, которые мы в последствии удалим) и вызывает метод getCSVFilesAbsolutePath() созданного нами класса FilesSearch, который возвращает список путей к CSV-файлам. Затем он в цикле перебирает каждый путь, читает все строки из файла, удаляет первую строку (заголовок) и добавляет оставшиеся строки в список csvLines. Метод возвращает список csvLines.

Метод parse() создает пустой список stationsDates и вызывает приватный метод getCsvInLines(), чтобы получить список строк из всех CSV-файлов. Затем он перебирает каждую строку, разбивает ее на части по запятой, и создает новый объект StationDate, который добавляется в список stationsDates. Если длина разбитой строки не равна 2, метод выводит сообщение об ошибке.

Метод listFormatted() перебирает каждый объект StationDate в списке stationsDates. Для каждого объекта он сравнивает его имя и дату с именем и датой каждого другого объекта в списке. Если найдено совпадение, метод удаляет объект с наименьшим индексом (второй объект в случае совпадения).

Конструктор класса вызывает два приватных метода: parse() и listFormatted(), которые выполняют парсинг CSV-файлов и форматирование списка stationsDates, соответственно.

Класс также содержит публичный метод getStationsDates(), который возвращает список stationsDates.

И так, все необходимые данные получены и хранятся в списках List<Line> lines, List<Station> stations, List<StationDepth> stationsDepth и List<StationDate> stationsDates!

✍ Теперь, исходя из условия задачи нам необходимо создать и записать на диск два JSON-файла: 1) map.json со списком станций по линиям и списком линий и 2) stations.json со свойствами станций.

Для этого нам потребуется:

❶ Создать JSON-объект с "картой метро"

❷ Создать JSON-объект со свойствами станций

Реализуем их создание в отдельных классах.

✒️ Приступим!

JsonMoscowMap

📌 Создадим класс JsonMoscowMap, где будем создавать JSON-объект для карты метро. В этом классе:

❶ Объявим приватные поля:

⭐️Но сначала вспомним структуру требуемого от нас файла (из условия задачи):

-11
-12

private JSONObject mainObject; - наш главный объект JSON

private JSONObject stationsObjectJson; - объект, в который будем вносить информацию о станциях метро по линиям

private JSONArray linesArrayJson; - массив объектов, в который будем вносить информацию о линиях метро

private LinkedHashMap<String, String> stationsPerLine; - упорядоченный словарь, содержащий информацию о станциях на каждой линии (далее он нам потребуется)

❷ Объявим поля, в которых получим требуемую информацию, используя ранее созданный класс ParseHtmlPage:

✎ private ParseHtmlPage parseHtmlPage = new ParseHtmlPage();

✎ private List<Station> stations = parseHtmlPage.getStations();

✎ private List<Line> lines = parseHtmlPage.getLines();

❸ Реализуем необходимые нам методы:

📌 В первую очередь нам необходимо получить список станций по каждой линии метро, то есть Линия 1 - список станций, Линия 2 - список станций и т.д., для этих целей напишем метод getStationsPerLine(), который будет возвращать LinkedHashMap<String, String>

✎ Инициализируем наш "словарь" stationsPerLine

stationsPerLine = new LinkedHashMap<>();

✎ Затем, в цикле, пройдемся по списку станций, полученных из объекта ParseHtmlPage, и будем в качестве ключа в нашу "Мапу" класть номера линий и в качестве значений станции, разделяя их двумя пробельными символами

for (int i = 0; i < stations.size(); i++) {
ㅤif (!stationsPerLine.containsKey(stations.get(i).getLine())) {
ㅤㅤstationsPerLine.put(stations.get(i).getLine(), "");
ㅤ}
ㅤstationsPerLine.put(stations.get(i).getLine(),
ㅤㅤstationsPerLine.get(stations.get(i).getLine()) + " " + stations.get(i).getName());
}

✎ После чего возвращаем LinkedHashMap stationsPerLine с заполненными значениями, где каждому ключу линии метро соответствует список станций

return stationsPerLine;

↘️ Итого

private LinkedHashMap<String, String> getStationsPerLine() {
ㅤstationsPerLine = new LinkedHashMap<>();
ㅤfor (int i = 0; i < stations.size(); i++) {
ㅤㅤif (!stationsPerLine.containsKey(stations.get(i).getLine())) {
ㅤㅤㅤstationsPerLine.put(stations.get(i).getLine(), "");
ㅤㅤ}
ㅤㅤstationsPerLine.put(stations.get(i).getLine(),
ㅤㅤㅤstationsPerLine.get(stations.get(i).getLine()) + " " + stations.get(i).getName());
ㅤ}
ㅤreturn stationsPerLine;
}

📌 Метод в котором будем создавать главный JSON-объект. Метод createJsonObject()

У себя в голове "разобьем" этот метод на три логических блока:

  • Создание объекта stationsObjectJson, содержащего информацию о станциях метро по линиям
  • Создание массива объектов linesArrayJson, содержащего информацию о линиях метро
  • Добавление объектов stationsObjectJson и linesArrayJson в главный объект JSON mainObject

✎ Создадим две переменные, которые в дальнейшем будем использовать в качестве ключей в объекте JSON, который мы создадим

String keyStations = "stations";
String keyLines = "lines";

✎ Вызовом метода getStationsPerLine() Заполняем наш LinkedHashMap stationsPerLine с информацией о станциях по линиям.

getStationsPerLine();

✎ Инициализируем главный объект JSON, в который будем добавлять всю информацию.

mainObject = new JSONObject();

✎ Создаем объект JSON для хранения информации о станциях. (далее он будет перемещен в главный объект)

stationsObjectJson = new JSONObject();

✎ Далее в цикле (по линиям) получаем список станций для текущей линии (из нашего LinkedHashMap) и разбиваем его на фрагменты (по двум пробельным символам, мы записывали станции через два пробела, потому что есть станции, состоящие из двух слов, разделенных одним пробелом) после чего пройдясь в цикле по фрагментам добавляем каждый фрагмент в массив JSON-объектов (его необходимо инициализировать) для станций текущей линии и наконец добавляем массив JSON-объектов для станций текущей линии в объект JSON для хранения информации о станциях stationsObjectJson

for (int i = 0; i < lines.size(); i++) {
ㅤString listStations = stationsPerLine.get(lines.get(i).getNumber()).trim();
ㅤString[] fragments = listStations.split("\\s{2}");
ㅤJSONArray stationsArray = new JSONArray();
ㅤfor (String fragment : fragments) {
ㅤㅤstationsArray.add(fragment);
ㅤ}
ㅤstationsObjectJson.put(lines.get(i).getNumber(), stationsArray);
}

Это конец первого логического блока.

Второй логический блок. Инициализируем массив JSON-объектов для хранения информации о линиях.

linesArrayJson = new JSONArray();

✎ Далее в цикле (по линиям) добавляем информацию о каждой линии в массив JSON-объектов для линий, для этого создадим в цикле новый JSON-объект для хранения информации о текущей линии (JSONObject obj), в который будем на каждой итерации цикла добавлять номер и название линии, после чего этот JSON-объект для текущей линии добавляем в массив JSON-объектов для линий linesArrayJson

for (int i = 0; i < lines.size(); i++) {
ㅤJSONObject obj = new JSONObject();
ㅤobj.put("number", lines.get(i).getNumber());
ㅤobj.put("name", lines.get(i).getName());
ㅤlinesArrayJson.add(obj);
}

Третий логический блок.

mainObject.put(keyStations, stationsObjectJson);
mainObject.put(keyLines, linesArrayJson);
return mainObject;

↘️ Итого

private JSONObject createJsonObject() {
ㅤString keyStations = "stations";
ㅤString keyLines = "lines";
ㅤgetStationsPerLine();
ㅤmainObject = new JSONObject();
ㅤstationsObjectJson = new JSONObject();
ㅤfor (int i = 0; i < lines.size(); i++) {
ㅤㅤJSONArray stationsArray = new JSONArray();
ㅤㅤString listStations = stationsPerLine.get(lines.get(i).getNumber()).trim();
ㅤㅤString[] fragments = listStations.split("\\s{2}");
ㅤㅤfor (String fragment : fragments) {
ㅤㅤㅤstationsArray.add(fragment);
ㅤㅤ}
ㅤㅤstationsObjectJson.put(lines.get(i).getNumber(), stationsArray);
ㅤ}
ㅤlinesArrayJson = new JSONArray();
ㅤfor (int i = 0; i < lines.size(); i++) {
ㅤㅤJSONObject obj = new JSONObject();
ㅤㅤobj.put("number", lines.get(i).getNumber());
ㅤㅤobj.put("name", lines.get(i).getName());
ㅤㅤlinesArrayJson.add(obj);
ㅤ}
ㅤmainObject.put(keyStations, stationsObjectJson);
ㅤmainObject.put(keyLines, linesArrayJson);
ㅤreturn mainObject;
}

📌 Для дальнейшего использования создадим:

✎ Конструктор нашего класса, который будет запускать метод createJsonObject()

public JsonMoscowMap() {
ㅤcreateJsonObject();
}

✎ Геттер для получения главного Json объекта

public JSONObject getMainObject() {
ㅤreturn mainObject;
}

JsonStationsInfo

📌 Создадим класс JsonStationsInfo, где будем создавать JSON-файл, содержащию информацию о станциях метро. В этом классе:

❶ Объявим приватные поля:

⭐️Но сначала вспомним структуру требуемого от нас файла (из условия задачи):

-13

private JSONObject mainObject; - основной JSON-объект, в котором будем хранить всю информацию о станциях метро

private JSONArray stationsArray; - массив JSON-объектов, каждый из которых представляет информацию об одной станции метро

private String mainKey = "stations"; - имя ключа, под которым будем хранить массив станций внутри основного JSON-объекта

❷ Объявим поля, в которых получим требуемую информацию, используя ранее созданные классы

private ParseHtmlPage parseHtmlPage = new ParseHtmlPage();

private ParseJsonFile parseJsonFile = new ParseJsonFile();

private ParseCsvFile parseCsvFile = new ParseCsvFile();

private List<Station> stations = parseHtmlPage.getStations();

private List<Line> lines = parseHtmlPage.getLines();

private List<StationDepth> stationsDepth = parseJsonFile.getStationsDepth();

private List<StationDate> stationDates = parseCsvFile.getStationsDates();

❸ Реализуем необходимые нам методы:

📌 createJsonObject(), для создания основного JSON-объекта, так как мы уже это делали, не буду подробно расписывать, оставлю только код и небольшое пояснение

Внутри метода createJsonObject создаем массив stationsArray, который содержит информацию о каждой станции метро.

Каждый элемент массива stationsArray представляет собой JSON-объект, который содержит следующие поля:

  • name: имя станции метро;
  • line: линия, на которой находится станция метро;
  • date: дата открытия станции метро;
  • depth: глубина расположения станции метро;
  • hacConnection: наличие пересадок.

Информация о каждом поле заполняется с помощью данных, полученных из объектов парсеров parseHtmlPage, parseJsonFile и parseCsvFile.

После заполнения всех полей JSON-объекта он добавляется в массив stationsArray. После того, как все JSON-объекты были добавлены в массив, он добавляется в основной JSON-объект под ключом, который содержится в переменной mainKey.

↘️

private JSONObject createJsonObject() {
ㅤmainObject = new JSONObject();
ㅤstationsArray = new JSONArray();
ㅤfor (int stationIndex = 0; stationIndex < stations.size(); stationIndex++) {
ㅤㅤJSONObject obj = new JSONObject();
ㅤㅤString etalonName = stations.get(stationIndex).getName();
ㅤㅤobj.put("name", etalonName);
ㅤㅤfor (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
ㅤㅤㅤif (stations.get(stationIndex).getLine().equals(lines.get(lineIndex).getNumber())) {
ㅤㅤㅤㅤString nameOfLine = lines.get(lineIndex).getName();
ㅤㅤㅤㅤobj.put("line", nameOfLine);
ㅤㅤㅤ}
ㅤㅤ}
ㅤㅤfor (int dateIndex = 0; dateIndex < stationDates.size(); dateIndex++) {
ㅤㅤㅤif (stationDates.get(dateIndex).getName().equals(etalonName)) {
ㅤㅤㅤㅤobj.put("date", stationDates.get(dateIndex).getDate());
ㅤㅤㅤ}
ㅤㅤ}
ㅤㅤfor (int depthIndex = 0; depthIndex < stationsDepth.size(); depthIndex++) {
ㅤㅤㅤif (stationsDepth.get(depthIndex).getName().equals(etalonName)
ㅤㅤㅤ&& stationsDepth.get(depthIndex).getDepth() != "-0") {
ㅤㅤㅤㅤobj.put("depth", stationsDepth.get(depthIndex).getDepth());
ㅤㅤㅤ}
ㅤㅤ}
ㅤㅤobj.put("hacConnection", stations.get(stationIndex).getHasConnection());
ㅤㅤstationsArray.add(obj);
ㅤ}
ㅤmainObject.put(mainKey, stationsArray);
ㅤreturn mainObject;
}

📌 Для дальнейшего использования создадим:

✎ Конструктор нашего класса, который будет запускать метод createJsonObject()

public JsonStationsInfo() {
ㅤcreateJsonObject();
}

✎ Геттер для получения главного Json объекта

public JSONObject getMainObject() {
ㅤreturn mainObject;
}

И так, JSON-объекты получены, теперь мы можем записывать их в файлы. Для записи создадим отдельный класс JsonWriter

JsonWriter

📌 создадим класс, в котором реализуем статический метод writer() на вход которого будем передавать JSON объект и путь, куда необходимо записывать файл

✎ В методе создадим объект FileWriter с указанием пути к файлу. Затем создадим объект Gson, который служит для сериализации Java-объектов в формат JSON. Будем использовать GsonBuilder для создания объекта Gson с параметрами, которые позволяют отформатировать JSON-код в удобочитаемый вид (setPrettyPrinting()).

✎ Для записи объекта в файл используем метод toJson, который принимает два параметра: сериализуемый объект и объект FileWriter.

✎ Так же не забываем использовать метод flush, который осуществляет сброс буфера, и метода close, который закрывает файл. В противном случае файл может не дозаписаться, оставив часть информации в буфере.

↘️

public class JsonWriter {
ㅤpublic static void writer(JSONObject object, String path) {
ㅤㅤtry {
ㅤㅤㅤFileWriter file = new FileWriter(path);
ㅤㅤㅤGson gson = new GsonBuilder().setPrettyPrinting().create();
ㅤㅤㅤgson.toJson(object, file);
ㅤㅤㅤfile.flush();
ㅤㅤㅤfile.close();
ㅤㅤ} catch (IOException e) {
ㅤㅤㅤe.printStackTrace();
ㅤㅤ}
ㅤ}
}

Можно считать задачу выполненной, проверим выполнение используя класс Main метод main

public class Main {
ㅤpublic static void main(String[] args) {
ㅤㅤString pathMapMoscow = "data/map.json";
ㅤㅤString pathStationsInfo = "data/stations.json";
ㅤㅤJsonMoscowMap mapMoscow = new JsonMoscowMap();
ㅤㅤJsonStationsInfo stationsInfo = new JsonStationsInfo();
ㅤㅤJSONObject objMapMoscow = mapMoscow.getMainObject();
ㅤㅤJSONObject objStationsInfo = stationsInfo.getMainObject();
ㅤㅤJsonWriter.writer(objMapMoscow, pathMapMoscow);
ㅤㅤJsonWriter.writer(objStationsInfo, pathStationsInfo);
}
}

▶️ Run 'Main.main()'

✅
✅
✅
-17