❗️ Задача:
Напишите программу, которая будет собирать данные из разных источников и записывать два JSON-файла. Парсинг разных данных должен происходить в разных классах. Имена классов и их методов придумайте самостоятельно. По мере реализации проверяйте работу каждого созданного класса. В программе должны быть следующие классы:
- Класс парсинга веб-страницы. В нём должно происходить (реализуйте каждую операцию в отдельных методах):
- ㅤ° получение HTML-кода страницы «Список станций Московского метрополитена» с помощью библиотеки jsoup;
- ㅤ° парсинг полученной страницы и получение из неё следующих данных (создайте для каждого типа данных отдельные классы):
- ㅤㅤㅤ- линии московского метро (имя и номер линии, цвет не нужен);
- ㅤㅤㅤ- станции московского метро (имя станции и номер линии).
- Класс поиска файлов в папках. В методах этого класса необходимо реализовать обход папок, лежащих в архиве. Разархивируйте его и напишите код, который будет обходить все вложенные папки и искать в них файлы форматов JSON и CSV (с расширениями *.json и *.csv). Метод для обхода папок должен принимать путь до папки, в которой надо производить поиск.
- Класс парсинга файлов формата JSON. Изучите структуру JSON-файлов, лежащих в папках, и создайте класс(ы) для хранения данных из этих файлов. Напишите код, который будет принимать JSON-данные и выдавать список соответствующих им объектов.
- Класс парсинга файлов формата CSV. Изучите структуру CSV-файлов, лежащих в папках, и создайте класс(ы) для хранения данных из этих файлов. Напишите код, который будет принимать CSV-данные и выдавать список соответствующих им объектов.
- Класс, в который можно добавлять данные, полученные на предыдущих шагах, и который создаёт и записывает на диск два JSON-файла:
- ㅤ° со списком станций по линиям и списком линий (файл map.json) в следующем формате:
ㅤ° файл stations.json со свойствами станций в следующем формате:
Если каких-то свойств для станции нет, то в файле не должно быть соответствующих ключей.
- Обратите внимание на то, что данные в разных источниках могут пересекаться:
- ㅤ° Одни и те же станции у разных веток при парсинге с сайта. Это могут быть как разные станции (например, в Москве две станции “Арбатская” и две станции “Смоленская”), так и одни и те же, если это станции пересадок.
- ㅤ° Данные о датах открытия для одних и тех же станций в файлах. Если даты отличаются, то это разные станции с одинаковыми названиями.
- ㅤ° Разные значения глубины для одних и тех же станций. Здесь приоритетной считайте значения с наибольшей глубиной.
✒️ Приступим!
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 страннице или вспомнив задание, создадим:
✎ Класс станции московского метро (имя станции, номер линии и информация о наличии пересадки)
↘️
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 файлы создадим:
✎ Класс глубины станции (имя, глубина)
↘️
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 файлы создадим:
✎ Класс даты открытия станции (имя, дата)
↘️
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.
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-код страницы
- Становится понятно, что информация о номере линии находится в классе 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 пришлось воспользоваться вложенными циклами и реализовать данную логику по следующему алгоритму
- Перебираем все элементы списка stationsDepth, используя цикл for, начиная с индекса 0.
- Получаем имя и глубину текущей станции, преобразуя глубину в тип Double.
- Вложенный цикл for перебирает все элементы списка stationsDepth еще раз, начиная с индекса 0.
- Получаем имя и глубину другой станции, преобразуя ее глубину в тип Double.
- Если имена текущей и другой станций равны и они не равны значениям sameName1 и sameName2, то выполняется следующее действие:
- Если глубина текущей станции больше глубины другой станции, то другая станция удаляется из списка stationsDepth. В противном случае удаляется текущая станция из списка.
- В результате в списке 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-объект для карты метро. В этом классе:
❶ Объявим приватные поля:
⭐️Но сначала вспомним структуру требуемого от нас файла (из условия задачи):
✎ 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-файл, содержащию информацию о станциях метро. В этом классе:
❶ Объявим приватные поля:
⭐️Но сначала вспомним структуру требуемого от нас файла (из условия задачи):
✎ 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()'