Дорогой читатель! Если тебя интересует парсинг html и разработка под Android, то эта статья для тебя. Надеюсь ты найдешь в ней много интересного и полезного. В ней я хочу поделиться своим опытом в данной сфере.
Описание проблемы
Немного обо мне. Я студент третьего курса ИТА ЮФУ. Также как и всем студентам, мне нужно каждый день смотреть расписание занятий. Причём мне нужно знать расписание не только на следующий день, но и на одну-две недели вперёд.
Казалось бы, почему нельзя просто сохранить расписание и пользоваться им? К сожалению, есть ряд причин, которые этому препятствуют, а именно:
- Расписание на одну неделю может сильно отличаться от расписания на другую
- Расписание не постоянно и может меняться
Конечно, есть сайт с расписанием, но он не очень удобен, так как на нём выводится просто сырая таблица с расписанием на 20 недель. Студенту приходится листать большую страницу, в поисках расписания на нужный день. Кроме того, в оффлайн режиме расписание становится недоступным.
Я решил сделать небольшое приложение, которое могло бы парсить сайт с расписанием моего института, и обладало бы следующим набором плюшек:
- Отображение: номера текущей недели, даты, дня недели и расписания на этот день
- Возможность перелистывать расписание кнопками «назад» и «далее»
- При отсутствии интернета показывать последнюю загруженную оффлайн версию расписания
Приступим к экзекуции
Итак, закатав рукава, я приступил к работе. Начать необходимо с малого. А именно — с редактирования файла манифеста. Стоит помнить, что наше приложение будет работать с интернетом и нам очень важно получить соответствующее разрешение:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapplication"> <uses-permission android:name="android.permission.INTERNET" /> ... </manifest>
Теперь перейдём к интерфейсу. Пока сделаем акцент на функционал и не будем злоупотреблять виджетами. Поэтому я разместил всего четыре виджета: Заголовок, текстовое поле и кнопки: назад и далее.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/WeekNumber" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Номер недели" app:layout_constraintTop_toTopOf="parent" /> <EditText android:id="@+id/timetable" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="100dp" android:ems="10" android:inputType="textMultiLine" android:text="Расписание" app:layout_constraintTop_toBottomOf="@+id/WeekNumber" tools:layout_editor_absoluteX="0dp" /> <Button android:id="@+id/next" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Далее" app:layout_constraintBottom_toBottomOf="parent" tools:layout_editor_absoluteX="0dp"></Button> <Button android:id="@+id/down" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Назад" app:layout_constraintBottom_toTopOf="@+id/next" tools:layout_editor_absoluteX="0dp"></Button> </androidx.constraintlayout.widget.ConstraintLayout>
Теперь начнём писать парсинг. Тут нам поможет замечательный парсер с открытым исходным кодом Jsoup. Вариант с использованием WebView я сразу отмёл, так как я посчитал этот способ крайне неудобным. К тому же мне не очень хотелось использовать лишний виджет, без которого легко можно обойтись.
implementation 'org.jsoup:jsoup:1.11.1'
Не стоит забывать, что работа с web для Android — это тяжёлая задача. Чтобы приложение не висло, нужно чтобы работа с web располагалась вне потока UI. Поэтому будем использовать класс AsyncTask. В него мы и заложим основной функционал, а потом просто передадим данные в UI-поток.
Для тех, кто не знаком с AsyncTask, хочу сказать, что данный класс должен располагаться внутри класса вашего activity. Сам класс приведён ниже.
package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public class MainActivity extends AppCompatActivity { public boolean offline; public String request; public String WeekNumber; public int count; //Виджеты public TextView weeknumber; public EditText timetable; public Button next; public Button down; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); offline = false;// Работаем а онлайн режиме count = 0;//Описание этой переменной будет дано ниже weeknumber = findViewById(R.id.WeekNumber); timetable = findViewById(R.id.timetable); next = findViewById(R.id.next); down = findViewById(R.id.down); getting AsyncTask = new getting(); AsyncTask.execute(); } class getting extends AsyncTask<String, String, String> { @Override protected void onPreExecute() { super.onPreExecute(); //В этом методе код перед началом выполнения фонового процесса } @Override protected String doInBackground(String... params) { /*Этот метод выполняется в фоне Тут мы обращаемся к сайту и вытаскиваем его html код */ String answer = "";// В эту переменную мы будем класть ответ от сайта. Пока что она пустая String url = "https://ictis.sfedu.ru/rasp/HTML/82.htm";// Адрес сайта с расписанием Document document = null; try { document = Jsoup.connect(url).get();// Коннектимся и получаем страницу answer = document.body().html();// Получаем код из тега body страницы } catch (IOException e) { // Если произошла ошибка, значит вероятнее всего, отсутствует соединение с интернетом // Загружаем в переменную answer оффлайн версию из txt файла try { BufferedReader read = new BufferedReader(new InputStreamReader(openFileInput("timetable.txt"))); String str = ""; while ((str = read.readLine())!=null){ answer +=str; } read.close(); offline = true;//работаем в оффлайн режиме } catch (FileNotFoundException ex) { //Если файла с сохранённым расписанием нет, то записываем в answer пустоту answer = ""; ex.printStackTrace(); } catch (IOException ex) { ex.printStackTrace(); } } //Убираю лишний текст из html //Заменяю html код отсутствия пары на запись nolessone //Убираю двойные пробелы answer = answer.replace("Пары","") .replace("Время","") .replace("<br>","br") .replace("<font face=\"Arial\" size=\"1\"></font><p align=\"CENTER\"><font face=\"Arial\" size=\"1\"></font>","nolessone")//Заменяем "сигнатуру" пустой пары на nolessone .replace(" ",""); return Jsoup.parse(answer).text();//Вытаскиваем текст из кода в переменной answer и передаём в UI-поток } @Override protected void onPostExecute(String result) { super.onPostExecute(result); /*Этот метод выполняется при завершении фонового кода Сюда возвращаются данные из потока */ request = "";//Начинаем формировать ответ String temp = result.toString();//Делаём временную строку // Записываем содержимое, в файл timetable.txt, в котором будем хранить оффлайн версию расписания try { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(openFileOutput("timetable.txt",MODE_PRIVATE))); writer.write(temp); writer.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } boolean start = false; for(String str:temp.split("Неделя: ")){ if(start) { //В начало каждой недели добавляем слово newweek и добавляем в request request += "newweek"+str.split("Расписание")[0] + "\n"; } start = true; } // Добавляем к дням недели приставку newday, для дальнейшей разбивки строки request = request.replace("Пнд","newdayПнд").replace("Втр","newdayВтр") .replace("Срд","newdayСрд").replace("Чтв","newdayЧтв") .replace("Птн","newdayПтн").replace("Сбт","newdayСбт"); /*Получаем дату дня Если count = 0, то вернётся дата сегодняшнего дня Если count = -1, то вчерашнего Если count = 1, то завтрашнего и т.д */ Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR,count); Date dayformat = calendar.getTime(); SimpleDateFormat format = new SimpleDateFormat("dd MMMM"); //Выводим результат timetable.setText(request); if(offline && !temp.equals("")){ //Уведомляем пользователя, что загружена оффлайн версия расписания Toast.makeText(getApplicationContext(),"Загружена оффлайн версия расписания!",Toast.LENGTH_LONG).show(); } //Если наш ответ равен пустоте, значит произошла ошибка if(temp.equals("")){ Toast.makeText(getApplicationContext(),"Произошла ошибка!",Toast.LENGTH_LONG).show(); } } } }
В итоге мы получим данные вот в таком виде:
Разберём методы, которые мы использовали:
Создаём элемент типа Document
Document document = null;
Получаем страницу
document = Jsoup.connect(url).get();
Теперь достаём содержимое тега body
answer = document.body().html();
В Jsoup также можно получить содержимое других основных тегов. Например можно получить заголовок страницы, используя метод title() и т.д. Метод html() Возвращает html код, а text() — обычный текст без html тегов.
Получив html код, можно преобразовать его в обычный текст, убрав все теги. Это можно сделать с помощью parse(htmlcode).text():
return Jsoup.parse(answer).text();
Хотелось бы поделиться ещё полезными методами Jsoup, которые не были использованы:
Element link = document.select("tag");//выберет элемент с тегом String url = link.attr("attribute"); // выдаст атрибут тега
На картинке в спойлере выше приведён пример расписания на одну неделю. В действительности нам будет возвращено 20 таких недель. Теперь наша задача найти в этом наборе данных сегодняшний день и вывести его.
Доведение до ума
Итак, что мы имеем? Мы научились приводить html код страницы в строку, которую можно легко распарсить. Это легко можно сделать используя строковые методы .split() и .replace().
В общем случае алгоритм будет выглядеть так.
Сначала получаем нужную дату от Android. Потом делаем два цикла, один вложенный в другой. Первый цикл проходит по неделям, второй, который внутри него, пробегает по дням недели. Если дата данного дня совпадает с датой, полученной от Android, то выводим расписание этого дня в текстовое поле. Однако каждый может написать этот алгоритм по своему. Я прикрепил свою версию его реализации.
package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public class MainActivity extends AppCompatActivity { public boolean offline; public String request; public String WeekNumber; public int count; //Виджеты public TextView weeknumber; public EditText timetable; public Button next; public Button down; public void formating(String day){ String DayTimetable = ""; String[] weeks = request.split("newweek"); String DayData = "";//Тут будет день недели и дата /* Переменные ниже будут содержать информацию о каждой паре Всего в день может быть семь пар массив less содержит названия каждого предмета массив tich содержит ФИО преподавателя каждого предмета массив aud содержит аудиторию, в которой будет проходить предмет (например Д-230) */ String less[] = new String[7]; String tich[] = new String[7]; String aud[] = new String[7]; for(String thisweek:weeks){//пробегаемся по неделям if(thisweek.indexOf(day) != -1) {//Если нужный нам день найден в этой неделе то... WeekNumber = thisweek.split(" ")[0];//Достаём номер недели for(String thisday:thisweek.split("newday")){//Теперь пробегаемся по дням этой недели if(thisday.indexOf(day) != -1) {//Если данный день совпадает с нужным нам днём, то... //Делаем так, тобы перед каждой парой была приставка newless //пара всегда начинается с соответствующей приставки пр. лек. лаб. и пр. thisday = thisday.replace("no","newless") .replace("пр.","newlessпр.") .replace("лек.","newlessлек.") .replace("лаб.","newlessлаб."); int i = 0; for(String thislessone:thisday.split("newless")) {//Теперь пробегаемся по предметам данного дня if(i != 0) { String[] ScienceInformation = thislessone.replace("br ","").split("br"); String science = ScienceInformation[0]; science = science.replace("lessone","Окно"); String ticher = ""; if(ScienceInformation.length > 1) ticher = ScienceInformation[1]; DayTimetable += i + "-ая: Предмет - " + science+"\n"+ticher+"\n\n"; ticher = ticher.replace("А-","@А-").replace("А-","@Б-") .replace("В-","@В-").replace("Г-","@Г-") .replace("Д-","@Д-").replace("Е-","@Е-") .replace("И-","@И-").replace("K-","@K-"); String Auditory; if(ticher.split("@").length == 2){ Auditory = "Аудитория: "+ticher.split("@")[1]; }else Auditory = "Аудитория: Дома";//На случай если пары нет ticher = ticher.split("@")[0]; if(ticher.length() >0){ ticher = "Преподаватель: "+ticher; }else{ ticher = "Самоподготовка"; } if(i==1){ less[i-1] = "1-ая (8:00-9:35) "+science; } if(i==2){ less[i-1] = "2-ая (9:50-11:25) "+science; } if(i==3){ less[i-1] = "3-ая (11:55-13:30) "+science; } if(i==4){ less[i-1] = "4-ая (13:45-15:20) "+science; } if(i==5){ less[i-1] = "5-ая (15:50-17:25) "+science; } if(i==6){ less[i-1] = "6-ая (17:40-19:15) "+science; } if(i==7){ less[i-1] = "7-ая (19:30-21:05) "+science; } tich[i-1] = ticher; aud[i-1] = Auditory; }else DayData = thislessone;//При i=0 в thislessone будет дата текущего дня i++; } } } } } timetable.setText(DayData);//Выводим дату for(int i = 0; i <=6; i++){ timetable.setText(timetable.getText()+"\n"+less[i]+tich[i]+aud[i]);//Вывод пары, препода и аудитории каждой пары (от нулевой до шестой) } weeknumber.setText("Сейчас "+ WeekNumber + " неделя");//Выводим номер неддели } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); offline = false;// Работаем в онлайн режиме count = 0; weeknumber = findViewById(R.id.WeekNumber); timetable = findViewById(R.id.timetable); next = findViewById(R.id.next); down = findViewById(R.id.down); getting getting = new getting(); getting.execute(); //События для кнопок назад и вперёд next.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { count++; Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR,count); Date dayformat = calendar.getTime(); SimpleDateFormat format = new SimpleDateFormat("dd MMMM"); formating(format.format(dayformat)); } }); down.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { count--; Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR,count); Date dayformat = calendar.getTime(); SimpleDateFormat format = new SimpleDateFormat("dd MMMM"); formating(format.format(dayformat)); } }); } class getting extends AsyncTask<String, String, String> { @Override protected void onPreExecute() { super.onPreExecute(); //В этом методе код перед началом выполнения фонового процесса getSupportActionBar().setTitle("Загрузка..."); } @Override protected String doInBackground(String... params) { /*Этот метод выполняется в фоне Тут мы обращаемся к сайту и вытаскиваем его html код */ String answer = "";// В эту переменную мы будем класть ответ от сайта. Пока что она пустая String url = "https://ictis.sfedu.ru/rasp/HTML/82.htm";// Адрес сайта с расписанием Document document = null; try { document = Jsoup.connect(url).get();// Коннектимся и получаем страницу answer = document.body().html();// Получаем код из тега body страницы } catch (IOException e) { // Если произошла ошибка, значит вероятнее всего, отсутствует соединение с интернетом // Загружаем в переменную answer офлайн версию из txt файла try { BufferedReader read = new BufferedReader(new InputStreamReader(openFileInput("timetable.txt"))); String str = ""; while ((str = read.readLine())!=null){ answer +=str; } read.close(); offline = true;//работаем в оффлайн режиме } catch (FileNotFoundException ex) { //Если файла с сохранённым расписанием нет, то записываем в answer пустоту answer = ""; ex.printStackTrace(); } catch (IOException ex) { ex.printStackTrace(); } } //Убираю лишний текст из html //Заменяю html код отсутствия пары на запись nolessone //Убираю двойные пробелы answer = answer.replace("Пары","") .replace("Время","") .replace("<br>","br") .replace("<font face=\"Arial\" size=\"1\"></font><p align=\"CENTER\"><font face=\"Arial\" size=\"1\"></font>","nolessone") .replace(" ",""); return Jsoup.parse(answer).text();//Вытаскиваем текст из кода в переменной answer и передаём в UI-поток } @Override protected void onPostExecute(String result) { super.onPostExecute(result); /*Этот метод выполняется при завершении фонового кода Сюда возвращаются данные из потока */ request = "";//Начинаем формировать ответ String temp = result.toString();//Делаём временную строку // Записываем содержимое, в файл timetable.txt, в котором будем хранить оффлайн версию расписания try { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(openFileOutput("timetable.txt",MODE_PRIVATE))); writer.write(temp); writer.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } boolean start = false; for(String str:temp.split("Неделя: ")){ if(start) { //В начало каждой недели добавляем слово newweek и добавляем в request request += "newweek"+str.split("Расписание")[0] + "\n"; } start = true; } // Добавляем к дням недели приставку newday, для дальнейшей разбивки строки request = request.replace("Пнд","newdayПнд").replace("Втр","newdayВтр") .replace("Срд","newdayСрд").replace("Чтв","newdayЧтв") .replace("Птн","newdayПтн").replace("Сбт","newdayСбт"); /*Получаем дату дня Если count = 0, то вернётся дата сегодняшнего дня Если count = -1, то вчерашнего Если count = 1, то завтрашнего и т.д */ Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR,count); Date dayformat = calendar.getTime(); SimpleDateFormat format = new SimpleDateFormat("dd MMMM"); //Вызываем функцию, которая будет заниматься представлением данных formating(format.format(dayformat)); if(offline && !temp.equals("")){ //Уведомляем пользователя, что загружена оффлайн версия расписания Toast.makeText(getApplicationContext(),"Загружена оффлайн версия расписания!",Toast.LENGTH_LONG).show(); } //Если наш ответ равен пустоте, значит произошла ошибка if(temp.equals("")){ Toast.makeText(getApplicationContext(),"Произошла ошибка!",Toast.LENGTH_LONG).show(); } getSupportActionBar().setTitle("Готово"); } } }
выборка расписания происходит в методе formating(). Подав на вход методу дату, мы получим расписание на данный день. Так мы легко можем реализовать код для кнопок «назад» и «далее»
Код кнопки «Далее»:
count++; Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR,count); Date dayformat = calendar.getTime(); SimpleDateFormat format = new SimpleDateFormat("dd MMMM"); formating(format.format(dayformat));// Вызываем formating
С помощью Calendar мы получаем сегодняшнюю дату. С помощью метода add мы прибавляем к сегодняшней дате количество дней, записанных в count. Код кнопки «назад» будет аналогичен, только из count нужно будет убавлять значение.
Заключение
Конечно, можно поработать над дизайном, но это уже другая тема. Я лишь хотел поделиться основными технологиями. В спойлерах ниже я прикрепил скриншоты с улучшенным дизайном. Также я добавил несколько функций, например: настройки, возможность выбрать учебную группу и т.д. Само приложение можно будет посмотреть чуть позже, как только доведу его до ума.
ссылка на оригинал статьи https://habr.com/ru/post/483284/
Добавить комментарий