diff --git a/app/src/main/java/agzam/vyatsu/data/remote/FacultyNames.kt b/app/src/main/java/agzam/vyatsu/data/remote/FacultyNames.kt new file mode 100644 index 0000000..a19c666 --- /dev/null +++ b/app/src/main/java/agzam/vyatsu/data/remote/FacultyNames.kt @@ -0,0 +1,36 @@ +package agzam.vyatsu.data.remote + +/** + * Справочник сокращённых названий факультетов ВятГУ. + * Вынесен из GroupsParser, так как TeachersParser использует те же сокращения. + */ +object FacultyNames { + + private val parenthesized = Regex("""\(.*?\)\s?""") + + private val shorthands = mapOf( + "Институт биологии и биотехнологии (факультет)(ОРУ)" to "ИББТ", + "Институт химии и экологии (факультет) (ОРУ)" to "ИнХимЭк", + "Факультет автоматики и вычислительной техники (ОРУ)" to "ФАВТ", + "Факультет истории, политических наук и культурологии (ОРУ)" to "ФИПНК", + "Факультет компьютерных и физико-математических наук (ОРУ)" to "ФКФМН", + "Факультет лингвистики (ОРУ)" to "ФЛ", + "Факультет менеджмента и сервиса (ОРУ)" to "ФМиС", + "Факультет педагогики и психологии (ОРУ)" to "ФПП", + "Факультет строительства и архитектуры (ОРУ)" to "ФСА", + "Факультет технологий, инжиниринга и дизайна (ОРУ)" to "ФТИД", + "Факультет физической культуры и спорта (ОРУ)" to "ФФКС", + "Факультет филологии и медиакоммуникаций (ОРУ)" to "ФФМ", + "Факультет экономики и финансов (ОРУ)" to "ФЭиФ", + "Электротехнический факультет (ОРУ)" to "ЭТФ", + "Юридический институт (факультет) (ОРУ)" to "ЮИ" + ) + + fun shortname(name: String): String { + val stripped = removeParenthesized(name) + val sh = shorthands[name] + return if (sh != null) "$sh ($stripped)" else stripped + } + + fun removeParenthesized(name: String): String = name.replace(parenthesized, "").trim() +} \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/data/remote/GroupsParser.kt b/app/src/main/java/agzam/vyatsu/data/remote/GroupsParser.kt index 10dae46..3ed5fdd 100644 --- a/app/src/main/java/agzam/vyatsu/data/remote/GroupsParser.kt +++ b/app/src/main/java/agzam/vyatsu/data/remote/GroupsParser.kt @@ -14,30 +14,11 @@ object GroupsParser { private val groupPattern = Regex("""/reports/schedule/Group/(\d{4,})_([12])_(\d{8})_(\d{8})\.pdf""") private val grpIdPattern = Regex("""data-grp_period_id="(\d+)"""") - private val parenthesized = Regex("""\(.*?\)\s?""") private const val CHUNK_SIZE = 8192 private const val MAX_SLICE = 1 shl 16 // 64 KiB — защита от аномально длинных единиц private const val MAX_PENDING = 1 shl 20 // 1 MiB — жёсткий потолок буфера - private val shorthands = mapOf( - "Институт биологии и биотехнологии (факультет)(ОРУ)" to "ИББТ", - "Институт химии и экологии (факультет) (ОРУ)" to "ИнХимЭк", - "Факультет автоматики и вычислительной техники (ОРУ)" to "ФАВТ", - "Факультет истории, политических наук и культурологии (ОРУ)" to "ФИПНК", - "Факультет компьютерных и физико-математических наук (ОРУ)" to "ФКФМН", - "Факультет лингвистики (ОРУ)" to "ФЛ", - "Факультет менеджмента и сервиса (ОРУ)" to "ФМиС", - "Факультет педагогики и психологии (ОРУ)" to "ФПП", - "Факультет строительства и архитектуры (ОРУ)" to "ФСА", - "Факультет технологий, инжиниринга и дизайна (ОРУ)" to "ФТИД", - "Факультет физической культуры и спорта (ОРУ)" to "ФФКС", - "Факультет филологии и медиакоммуникаций (ОРУ)" to "ФФМ", - "Факультет экономики и финансов (ОРУ)" to "ФЭиФ", - "Электротехнический факультет (ОРУ)" to "ЭТФ", - "Юридический институт (факультет) (ОРУ)" to "ЮИ" - ) - /** * Стриминговый парсер страницы групп: читает тело ответа по чанкам и * разбирает его на лету, не материализуя весь HTML в память. @@ -80,8 +61,7 @@ object GroupsParser { else -> minOf(divAt, aAt) } if (tagStart < 0) { - // хвостовой текст без div-тегов — обычный текст страницы - pos = pending.length + pos = keepPartialTagTail(pending, pos) break } if (tagStart > pos) { @@ -101,13 +81,13 @@ object GroupsParser { val content = pending.substring(tagEnd + 1, closeDiv) if (isFakName) { val name = content.trim() - if (name.isNotEmpty()) faculty = shortname(name) + if (name.isNotEmpty()) faculty = FacultyNames.shortname(name) } else { val m = grpIdPattern.find(tagText) if (m != null) { groups += Group( id = m.groupValues[1].dropLast(1), - name = removeParenthesized(content), + name = FacultyNames.removeParenthesized(content), faculty = faculty ) } @@ -153,13 +133,22 @@ object GroupsParser { return -1 } - private fun shortname(name: String): String { - val stripped = removeParenthesized(name) - val sh = shorthands[name] - return if (sh != null) "$sh ($stripped)" else stripped + /** + * Хвост буфера может содержать неполное начало тега (]*class="fak_name"""") + private val fakIdInTag = Regex("""data-fak_id="(\d+)"""") + private val kafPeriodTag = Regex("""]*class="kafPeriod"""") + private val kafIdInTag = Regex("""data-kaf_period_id="(\d+)"""") + private val prepodLink = Regex("""href="/reports/schedule/prepod/(\d+)_([12])_(\d{8})_(\d{8})\.html"""") + private val tagStrip = Regex("""<[^>]*>""") + + private const val CHUNK_SIZE = 8192 + private const val MAX_SLICE = 1 shl 16 // 64 KiB — защита от аномально длинных единиц + private const val MAX_PENDING = 1 shl 20 // 1 MiB — жёсткий потолок буфера + + fun parseStream(input: InputStream): ParsedDepartments { + val reader = input.bufferedReader(Charsets.UTF_8) + val buf = CharArray(CHUNK_SIZE) + val pending = StringBuilder(CHUNK_SIZE * 2) + val departments = ArrayList() + val ranges = HashMap>>() + var faculty = "" + + fun addRanges(text: String) { + for (m in prepodLink.findAll(text)) { + val departmentId = m.groupValues[1] + val season = if (m.groupValues[2] == "1") Season.AUTUMN else Season.SPRING + val range = DateRange(m.groupValues[3], m.groupValues[4]) + ranges.getOrPut(departmentId) { + hashMapOf(Season.AUTUMN to ArrayList(), Season.SPRING to ArrayList()) + }.getOrPut(season) { ArrayList() }.add(range) + } + } + + while (true) { + val n = reader.read(buf) + if (n < 0) break + pending.append(buf, 0, n) + + var pos = 0 + while (pos < pending.length) { + val divAt = pending.indexOf(" aAt + aAt < 0 -> divAt + else -> minOf(divAt, aAt) + } + if (tagStart < 0) { + pos = keepPartialTagTail(pending, pos) + break + } + if (tagStart > pos) { + addRanges(pending.substring(pos, tagStart)) + } + val tagEnd = findTagEnd(pending, tagStart) + if (tagEnd < 0) break + val tagText = pending.substring(tagStart, tagEnd + 1) + + val isDiv = tagText.startsWith("", tagEnd + 1) + if (closeDiv < 0) break + addRanges(tagText) + val content = pending.substring(tagEnd + 1, closeDiv) + if (isFakName) { + val stripped = tagStrip.replace(content, "").trim() + if (stripped.isNotEmpty()) faculty = FacultyNames.shortname(stripped) + } else { + val m = kafIdInTag.find(tagText) + if (m != null) { + departments += Department( + departmentId = m.groupValues[1].dropLast(1), + name = tagStrip.replace(content, "").trim(), + faculty = faculty + ) + } + } + pos = closeDiv + 6 + } else { + addRanges(tagText) + pos = tagEnd + 1 + } + + if (pos > MAX_SLICE) { + pending.delete(0, pos) + pos = 0 + } + } + if (pos > 0 && pos <= pending.length) { + pending.delete(0, pos) + } + if (pending.length > MAX_PENDING) { + pending.delete(0, pending.length - MAX_SLICE) + } + } + return ParsedDepartments(departments, ranges) + } + + /** Ищет конец HTML-тега — символ '>' вне кавычек. */ + private fun findTagEnd(s: CharSequence, start: Int): Int { + var i = start + var quote = 0.toChar() + while (i < s.length) { + val c = s[i] + if (quote != 0.toChar()) { + if (c == quote) quote = 0.toChar() + } else { + when (c) { + '"', '\'' -> quote = c + '>' -> return i + } + } + i++ + } + return -1 + } + + /** + * Хвост буфера может содержать неполное начало тега ( 400) + assertTrue(result.groups.any { it.id == "26981" && it.name.contains("ПОДб-1701-08-00") }) + assertTrue(result.dateRanges.size > 400) + } +} \ No newline at end of file diff --git a/app/src/test/java/agzam/vyatsu/TeachersPipelineMemoryTest.kt b/app/src/test/java/agzam/vyatsu/TeachersPipelineMemoryTest.kt new file mode 100644 index 0000000..2bb6c8b --- /dev/null +++ b/app/src/test/java/agzam/vyatsu/TeachersPipelineMemoryTest.kt @@ -0,0 +1,76 @@ +package agzam.vyatsu + +import agzam.vyatsu.data.remote.TeachersParser +import agzam.vyatsu.domain.model.DateRange +import agzam.vyatsu.domain.model.Season +import java.io.InputStream +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class TeachersPipelineMemoryTest { + + private fun fixture(): InputStream = + checkNotNull(javaClass.getResourceAsStream("/teacher.html")) { + "Fixture teacher.html not found in src/test/resources" + } + + @Test + fun parsesDepartmentsAndFaculties() { + val result = fixture().use(TeachersParser::parseStream) + assertTrue("expected >=4 departments", result.departments.size >= 4) + + val ids = result.departments.map { it.departmentId }.toSet() + assertEquals(setOf("11111", "22222", "33333", "44444"), ids) + + val names = result.departments.map { it.name } + assertTrue( + "nested tags must be stripped from kafPeriod names", + names.contains("Кафедра технологий и конструирования машин и аппаратов") + ) + + val avt = result.departments.first { it.departmentId == "11111" } + assertTrue("faculty must be mapped via shortname", avt.faculty.contains("ФАВТ")) + } + + @Test + fun chunkBoundarySafe() { + val full = fixture().use(TeachersParser::parseStream) + val limited = limitReads(fixture(), 64).use(TeachersParser::parseStream) + assertEquals(full, limited) + } + + @Test + fun seasonAndRanges() { + val result = fixture().use(TeachersParser::parseStream) + + val hasAutumn = result.dateRanges.values.any { it[Season.AUTUMN]?.isNotEmpty() == true } + val hasSpring = result.dateRanges.values.any { it[Season.SPRING]?.isNotEmpty() == true } + assertTrue("AUTUMN season must be present", hasAutumn) + assertTrue("SPRING season must be present", hasSpring) + + val autumn = result.dateRanges["11111"]?.get(Season.AUTUMN).orEmpty() + assertTrue( + "DateRange must be parsed from URL groups", + autumn.contains(DateRange("01092026", "13092026")) + ) + assertTrue( + "second AUTUMN range must be present", + autumn.contains(DateRange("14092026", "27092026")) + ) + + val spring = result.dateRanges["44444"]?.get(Season.SPRING).orEmpty() + assertTrue( + "SPRING DateRange must be parsed from URL groups", + spring.contains(DateRange("02022026", "27022026")) + ) + } + + private fun limitReads(input: InputStream, maxBytes: Int): InputStream = + object : InputStream() { + override fun read(): Int = input.read() + + override fun read(b: ByteArray, off: Int, len: Int): Int = + input.read(b, off, minOf(len, maxBytes)) + } +} \ No newline at end of file diff --git a/app/src/test/resources/groups.html b/app/src/test/resources/groups.html new file mode 100644 index 0000000..c84ae2f --- /dev/null +++ b/app/src/test/resources/groups.html @@ -0,0 +1,17699 @@ + + + + + + + Расписание учебных занятий обучающихся очной формы - Официальный сайт ВятГУ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+ + + + + + +
+
+ + +
+
+ + +
+ +
+ + + + + + + +

Студенту

+ + + +
+
+

Расписание учебных занятий обучающихся очной формы

 +


Бакалавр
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Дирекция ОП Пединститута (ОРУ)
+ +
+
Институт биологии и биотехнологии (факультет)(ОРУ)
+ +
+
Институт химии и экологии (факультет) (ОРУ)
+ +
+
Факультет автоматики и вычислительной техники (ОРУ)
+ +
+
Факультет истории, политических наук и культурологии (ОРУ)
+ +
+
Факультет компьютерных и физико-математических наук (ОРУ)
+ +
+
Факультет лингвистики (ОРУ)
+ +
+
Факультет международного образования (ОРУ)
+ +
+
Факультет менеджмента и сервиса (ОРУ)
+ +
+
Факультет педагогики и психологии (ОРУ)
+ +
+
Факультет русского языка, литературы и медиакоммуникаций (ОРУ)
+ +
+
Факультет строительства и архитектуры (ОРУ)
+ +
+
Факультет технологий, инжиниринга и дизайна (ОРУ)
+ +
+
Факультет физической культуры и спорта (ОРУ)
+ +
+
Факультет экономики и финансов (ОРУ)
+ +
+
Электротехнический факультет (ОРУ)
+ +
+
Юридический институт (факультет) (ОРУ)
+ +
+

Специалист
+ + + + + + + + + + + + + + + + + + + + +
+
Факультет автоматики и вычислительной техники (ОРУ)
+ +
+
Факультет технологий, инжиниринга и дизайна (ОРУ)
+ +
+
Факультет экономики и финансов (ОРУ)
+ +
+
Юридический институт (факультет) (ОРУ)
+ +
+

Магистр
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Дирекция ОП Пединститута (ОРУ)
+ +
+
Институт биологии и биотехнологии (факультет)(ОРУ)
+ +
+
Институт химии и экологии (факультет) (ОРУ)
+ +
+
Факультет автоматики и вычислительной техники (ОРУ)
+ +
+
Факультет истории, политических наук и культурологии (ОРУ)
+ +
+
Факультет компьютерных и физико-математических наук (ОРУ)
+ +
+
Факультет лингвистики (ОРУ)
+ +
+
Факультет международного образования (ОРУ)
+ +
+
Факультет менеджмента и сервиса (ОРУ)
+ +
+
Факультет педагогики и психологии (ОРУ)
+ +
+
Факультет русского языка, литературы и медиакоммуникаций (ОРУ)
+ +
+
Факультет строительства и архитектуры (ОРУ)
+ +
+
Факультет технологий, инжиниринга и дизайна (ОРУ)
+ +
+
Факультет экономики и финансов (ОРУ)
+ +
+
Электротехнический факультет (ОРУ)
+ +
+
Юридический институт (факультет) (ОРУ)
+ +
+

Аспирант
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Институт биологии и биотехнологии (факультет)(ОРУ)
+ +
+
Институт химии и экологии (факультет) (ОРУ)
+ +
+
Факультет автоматики и вычислительной техники (ОРУ)
+ +
+
Факультет истории, политических наук и культурологии (ОРУ)
+ +
+
Факультет компьютерных и физико-математических наук (ОРУ)
+ +
+
Факультет менеджмента и сервиса (ОРУ)
+ +
+
Факультет педагогики и психологии (ОРУ)
+ +
+
Факультет русского языка, литературы и медиакоммуникаций (ОРУ)
+ +
+
Факультет строительства и архитектуры (ОРУ)
+ +
+
Факультет технологий, инжиниринга и дизайна (ОРУ)
+ +
+
Факультет физической культуры и спорта (ОРУ)
+ +
+
Факультет экономики и финансов (ОРУ)
+ +
+
Электротехнический факультет (ОРУ)
+ +
+
Юридический институт (факультет) (ОРУ)
+ +
+
+ +
+ + +
+
+

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

+

Шаг 1. Выбираете уровень получаемого образования (бакалавриат, специалитет, магистратура, аспирантура) и свой институт или факультет.

+ +

Шаг 2. Находите свой курс и номер группы. Кликаете на номер своей группы и смотрите расписание учебных занятий на соответствующий период.

+ +

В отношении каждого учебного занятия указывается:

+ +
    +
  • наименование дисциплины;
  • +
  • вид занятий (чтение лекций; проведение практических занятий, семинаров; проведение лабораторных занятий);
  • +
  • Ф.И.О. преподавателя;
  • +
  • аудитория (номер аудитории включает в себя номер корпуса и аудитории).
  • +
+ +

С адресами учебных корпусов можно ознакомиться по ссылке

+ +

ВНИМАНИЕ!!!
+Расписание учебных занятий очной формы обновляется на сайте каждый день после 24.00.

+ +

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

+ +

 

+ +

 

+

+
+
+ +
+
+
+ + + + + + + + + + + + + +
+
+

Мы используем файлы cookies 🍪

+

Мы используем файлы cookies для анализа пользовательской активности с помощью сервиса «Яндекс Метрика» и улучшения работы нашего Сайта. Продолжая использовать Сайт, Вы подтверждаете свое согласие на обработку персональных данных и соглашаетесь с политикой обработки персональных данных.

+
+ +
+ + + diff --git a/app/src/test/resources/teacher.html b/app/src/test/resources/teacher.html new file mode 100644 index 0000000..984e033 --- /dev/null +++ b/app/src/test/resources/teacher.html @@ -0,0 +1,90 @@ + + + + Расписание занятий преподавателей - Официальный сайт ВятГУ + + +
+
+

Планируемое расписание занятий преподавателей

+
Факультет автоматики и вычислительной техники (ОРУ)
+ +
Философский факультет
+ +
+
+ + \ No newline at end of file