package agzam.vyatsu.data.remote import agzam.vyatsu.domain.model.DateRange import agzam.vyatsu.domain.model.Department import agzam.vyatsu.domain.model.ParsedDepartments import agzam.vyatsu.domain.model.Season import java.io.InputStream /** * Стриминговый парсер страницы преподавателей: читает тело ответа по чанкам и * разбирает его на лету, не материализуя весь HTML в память. * Страница teacher.html весит ~2.4MB, поэтому весь документ грузить нельзя. */ object TeachersParser { private val fakNameTag = Regex("""]*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 } /** * Хвост буфера может содержать неполное начало тега (