Привет Habr!

Дорогой читатель! Если тебя интересует парсинг html и разработка под Android, то эта статья для тебя. Надеюсь ты найдешь в ней много интересного и полезного. В ней я хочу поделиться своим опытом в данной сфере.

Описание проблемы


Немного обо мне. Я студент третьего курса ИТА ЮФУ. Также как и всем студентам, мне нужно каждый день смотреть расписание занятий. Причём мне нужно знать расписание не только на следующий день, но и на одну-две недели вперёд.

Казалось бы, почему нельзя просто сохранить расписание и пользоваться им? К сожалению, есть ряд причин, которые этому препятствуют, а именно:

  • Расписание на одну неделю может сильно отличаться от расписания на другую
  • Расписание не постоянно и может меняться

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

  • Отображение: номера текущей недели, даты, дня недели и расписания на этот день
  • Возможность перелистывать расписание кнопками «назад» и «далее»
  • При отсутствии интернета показывать последнюю загруженную оффлайн версию расписания

Приступим к экзекуции


Итак, закатав рукава, я приступил к работе. Начать необходимо с малого. А именно — с редактирования файла манифеста. Стоит помнить, что наше приложение будет работать с интернетом и нам очень важно получить соответствующее разрешение:

Файл манифестра
Идём в manifests->AndroidManifest.xml. Добавляем permission. В итоге получается что-то типа этого:

<?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>

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

Разметка Activity
<?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 я сразу отмёл, так как я посчитал этот способ крайне неудобным. К тому же мне не очень хотелось использовать лишний виджет, без которого легко можно обойтись.

Подключение Jsoup
Добавляем зависимость в build.gradle:
implementation 'org.jsoup:jsoup:1.11.1'


Не стоит забывать, что работа с web для Android — это тяжёлая задача. Чтобы приложение не висло, нужно чтобы работа с web располагалась вне потока UI. Поэтому будем использовать класс AsyncTask. В него мы и заложим основной функционал, а потом просто передадим данные в UI-поток.

Для тех, кто не знаком с AsyncTask, хочу сказать, что данный класс должен располагаться внутри класса вашего activity. Сам класс приведён ниже.

Код Activity с классом AsyncTask
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, то выводим расписание этого дня в текстовое поле. Однако каждый может написать этот алгоритм по своему. Я прикрепил свою версию его реализации.

Полный код 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;
    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 нужно будет убавлять значение.

Скриншот


Заключение


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

Скриншот приложения

Комментарии (12)


  1. censor2005
    09.01.2020 14:37
    +1

    Чёрт, а я все думал, писать или не писать статью про своё решение проблемы расписаний. Только я сделал помимо андроид приложения Telegram-бота и веб-страничку. Сервер парсит Excel-файл и кидает расписание в базу, а дальше из этой базы бот, приложение и страничка показывают инфу пользователям. Но подумал что никому не открою этой статьёй Америку )


    1. GokenTanmay
      09.01.2020 14:56

      Каждый год в разработку приходят тысячи «неокрепших», каждый месяц рефакторятся или создаются новые библиотеки, фреймворки, обертки…
      Написав статью, как минимум получишь фидбэк, где можно сделать проще/быстрее, а в максимальном случае — сразу покажешь правильный подход.


      1. censor2005
        09.01.2020 15:02

        Ну так то я в разработке около 12 лет. Не скажу что я написал правильный и красивый код, но он максимально удобно решал мою задачу (и задачу 700 студентов и примерно 100 преподавателей), а я сам параллельно познакомился с Android-разработкой.
        Собственно, наверное мне просто стыдно за тот код, написанный в процессе, что и останавливало от написания статьи )
        Кстати, для тех, кто интересуется генерацией расписаний: есть бесплатный кросплатформенный опенсорс проект FET (https://lalescu.ro/liviu/fet/), интерфейс сложный, но сама программа потрясающе функциональна и позволяет покрыть практически все потребности в автоматической генерации расписаний занятий для вузов/школ итд. А при необходимости можно допилить исходники )


  1. Xokare228
    09.01.2020 15:14

    Ха ха, у вас есть сайт. У нас старый добрый стенд с огромными листами


    1. maxzh83
      09.01.2020 15:44

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


      1. Xokare228
        09.01.2020 15:47

        Распознать то не проблема, проблема его получить. Это же надо что бы кто-то сходил, сфотографировал, но даже если этот кто-то найдётся текст будет не в фокусе, с бликами и при ужасном освещении, так что обработка пред распознаванием даст массу новых ощущений в области ниже пояса. И всё равно в самый интересный момент расписание внезапно поменяется. Enjoy


        1. maxzh83
          09.01.2020 18:09

          Это же надо что бы кто-то сходил, сфотографировал

          Недавно на хабре была статься про телеграм-бота для добавления людей в футбольную команду. Тут нужен такой же, который будет решать, кто пойдет фоткать.
          текст будет не в фокусе, с бликами и при ужасном освещении

          А нейронные сети на что?
          На самом деле, это все ирония. Просто показалось, что в статье слишком много всего придумано просто «потому что могу».


          1. Xokare228
            09.01.2020 18:14

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

            Слишком просто, двадцать первый век же. Надо собрать дрона который будет раз в n времени подлетать и фотографировать доску, после чего отправит это всё с помощью лазерного модема
            А нейронные сети на что?

            Да запросто, достроим и через набор программ от топаза прогоним, ну и немного велосипедов, куда же без них
            На самом деле, это все ирония. Просто показалось, что в статье слишком много всего придумано просто «потому что могу».

            Да я тоже местами иронизирую. ИМХО тут надо с источником информации работать, а не костыли городить


  1. Eldhenn
    09.01.2020 16:50

    Расскажите вашему it-отделу про iCalendar.


  1. Alyoshka1976
    09.01.2020 16:59

    Что отобразится, когда при инкрементировании count дата прыгнет на воскресенье?


    1. Spok99 Автор
      09.01.2020 17:11

      Тогда вместо номера недели будет выдан null. В этом случае этот null отслеживается и совершается прыжек еще на один день


  1. 402d
    09.01.2020 18:28

    Вы только свое приложение в Гугл плее от своего имени не выкладывайте. Бан Веронике на 99 процентов