Skip to content

Parsers

Parser functions accept raw EAMS HTML strings and return plain Python dicts. They contain no network I/O and can be used standalone (e.g. in tests with captured fixtures).

tju.parser.parse_schedule

parse_schedule(html)

parse course table

Source code in src/tju/parser/schedule.py
def parse_schedule(html):
    """
    parse course table
    """
    arrangePairArray = []
    courses = []
    arrangeHtmls = re.findall(r"in TaskActivity([\s\S]+?)fillTable", html)[0].split(
        "var teachers"
    )
    for arrangeItem in arrangeHtmls[1:]:
        rawTeachers = re.findall("var actTeachers = ([^;]+);", arrangeItem)[0]
        teacherArray = re.findall('"([^"]+)"', rawTeachers)
        # 这节课的信息
        lineList = arrangeItem.split(";")
        # 课程相关
        courseLine = lineList[14].split(",")
        # classID
        classID = re.findall(r"\((\w+)\)", courseLine[4])[0]
        threePair = re.findall('"([^"]+)","[^"]*","([^"]*)","([01]+)"', arrangeItem)[0]
        location = threePair[1].strip()
        rawWeeks = threePair[2].strip()
        weekArray = []
        for i, b in enumerate(rawWeeks):
            if b == "1":
                weekArray.append(i)
        twoPair = re.findall(r"([0-9]+)\*unitCount\+([0-9]+)", arrangeItem)
        weekday = int(twoPair[0][0]) + 1
        unitArray = list(map(lambda x: int(x[1]) + 1, twoPair))
        arrangePairArray.append(
            (
                classID,
                {
                    "teacher": teacherArray,
                    "week": weekArray,
                    "unit": unitArray,
                    "weekday": weekday,
                    "location": location,
                },
            )
        )
    trs = re.findall(
        r"<tr([\s\S]+?)</tr>", re.findall(r"<tbody([\s\S]+?)</tbody>", html)[0]
    )
    for tr in trs:
        tds = re.findall(r"<td>([\s\S]+?)</td>", tr)
        if len(tds) <= 9:
            continue
        serial = re.findall(r">(\d*)</a>", tds[1])[0]
        no = tds[2]
        name = tds[3]
        if "style" in tds[3]:
            name = (
                re.findall("(.+?)<sup", tds[3])[0].strip()
                + " "
                + re.findall('">(.+?)</s', tds[3])[0].strip()
            )
        credit = float(tds[4])
        teachers = tds[5].split(",")
        weeks = tds[6].strip()
        campus = ""
        if re.findall("(.+?校区)", tds[9]):
            campus = re.findall("(.+?校区)", tds[9])[0].strip()
        courseData = {
            "class_id": serial,
            "course_id": no,
            "name": name,
            "credit": str(credit),
            "teacher": teachers,
            "weeks": weeks,
            "campus": campus,
        }
        courseData["arrange"] = list(
            map(lambda x: x[1], filter(lambda x: x[0] == serial, arrangePairArray))
        )
        courses.append(courseData)
    return courses

tju.parser.parse_profile

parse_profile(html) -> Dict[str, Any]

parse personal profile

Source code in src/tju/parser/profile.py
def parse_profile(html) -> Dict[str, Any]:
    """
    parse personal profile
    """

    matches = re.findall(
        r'<td[ width="25%"]* class="title" style="width:18%">([^<]+)</td>\s*<td[^>]*>([^<]*)</td>',
        html,
    )
    result = {
        key.strip(":") if key.endswith(":") else key: value for key, value in matches
    }

    return result

tju.parser.parse_course

parse_course(html, semester: str)
Source code in src/tju/parser/course.py
def parse_course(html, semester: str):
    lession_id_and_arrange = re.findall(r"contents\['(.*)'\]='(.*)'", html)
    lession_id_to_arrange = dict(lession_id_and_arrange)
    keys_and_content = html.split('<th  class="gridselect-top" >')[1].split("</thead>")
    keys = re.findall(r"<th\s*[class]*.*>(.*)<\/th>", keys_and_content[0])
    keys = ["lession_id"] + keys
    content_and_tail = keys_and_content[1].split("</table>")

    # fix ,\n
    content_and_tail[0] = content_and_tail[0].replace(",\n", ",")
    # fix <sup> tag
    sup_pattern = re.compile(r">(\S+)<\/a>\s*<sup.*>(\S+)<\/sup>\s")
    if sup_pattern.findall(content_and_tail[0]):
        content_and_tail[0] = re.sub(sup_pattern, r">\1 \2</a>", content_and_tail[0])

    # Build row regex dynamically so it works regardless of column count.
    # Old EAMS HTML had 16 columns; newer versions dropped 4 enrollment columns (12 columns).
    # Pattern: checkbox TD (captures lession_id value) + n_data data TDs captured individually.
    n_data = len(keys) - 1  # number of data TDs per row (excludes the lession_id column)
    row_pattern = re.compile(
        r"<tr>\s*<td[^>]*>"
        r'<input[^>]*\bvalue="(\d+)"[^>]*/>'
        r"</td>"
        + (r"\s*<td[^>]*>([\s\S]*?)</td>") * n_data
    )
    content = row_pattern.findall(content_and_tail[0])

    result = []
    for lession in content:
        item = {}
        item["semester"] = semester
        for i, key in enumerate(keys):
            raw = lession[i]
            # Extract text content: prefer anchor text if present, otherwise strip all HTML tags.
            a_match = re.search(r"<a[^>]*>([\s\S]*?)<\/a>", raw)
            if a_match:
                c = a_match.group(1).strip()
            else:
                c = re.sub(r"<[^>]+>", "", raw).strip()
            if key == "教学班":
                if c.startswith("班级:"):
                    c = c.replace("班级:", "").strip()
                c = [_.split(";") for _ in c.split(" ")]
                c = list(itertools.chain(*c))
            elif key == "教师":
                c = c.split(",")
            elif key == "课程类别":
                c = [_.strip() for _ in c.split(",")]
            elif key == "学时/周":
                item["总学时"] = c.split("/")[0]
                item["周学时"] = c.split("/")[1]
                continue
            elif key == "课程名称":
                c = c.replace("\n", "")
            item[key] = c
        # Some courses (e.g. thesis supervision, online-only) have no timetable JS entry.
        arrange_html = lession_id_to_arrange.get(item["lession_id"], "")
        item["arrange"] = _parse_arrange(arrange_html) if arrange_html.strip() else []
        result.append(item)

    numbers = re.findall(r"pageInfo\((\d+),(\d+),(\d+)\)", content_and_tail[1])[0]
    page_from = int(numbers[0])
    page_to = int(numbers[1])
    total = int(numbers[2])
    page_size = page_to - page_from + 1
    page_no = (page_from - 1) // page_size + 1
    return {
        "list": result,
        "page_no": page_no,
        "page_size": page_size,
        "total": total,
    }

tju.parser.parse_course_info

parse_course_info(html)
Source code in src/tju/parser/course.py
def parse_course_info(html):
    semester = re.findall(r"学期:</td>\s+<td.*>(.*)</td>", html)[0]
    faculty = re.findall(r"开课院系:</td>\s+<td.*>(.*)</td>", html)[0]
    result = {
        "semester": semester,
        "faculty": faculty,
    }
    return result

tju.parser.parse_syllabus

parse_syllabus(html)
Source code in src/tju/parser/course.py
def parse_syllabus(html):
    return md(html).replace("\n\n\n", "\n\n").strip()

tju.parser.parse_exam

parse_exam(html)

parse exam

Source code in src/tju/parser/exam.py
def parse_exam(html):
    """
    parse exam
    """
    exams = []

    keys = re.findall(r"<th.*>(.+?)</th.*>", html)
    tbody = re.findall(r"<tbody([\s\S]+?)</tbody>", html)[0]
    courses = re.findall(r"<tr>([\s\S]+?)</tr>", tbody)
    for course in courses:
        arr = re.findall(r"<td>([\s\S]+?)</td>", course)
        if not arr:
            continue
        arr = list(
            map(lambda x: re.findall(r">(.+?)</font", x)[0] if "color" in x else x, arr)
        )
        if len(arr) != len(keys):
            raise HtmlParseError("Failed to parse exam")
        item = {k.strip(): v.strip() for k, v in zip(keys, arr)}
        exams.append(item)
    return exams

tju.parser.parse_exam_batch_id

parse_exam_batch_id(html)

parse exam post

Source code in src/tju/parser/exam.py
def parse_exam_batch_id(html):
    """
    parse exam post
    """
    return re.findall(r"examBatch.id=(\d+)", html)[0]

tju.parser.parse_score

parse_score(html)

parse score

Source code in src/tju/parser/score.py
def parse_score(html):
    """
    parse score
    """
    tables = html.split('<table class="gridtable">')
    if len(tables) < 2:
        raise HtmlParseError("Score data format Error")

    summary = []
    if len(tables) > 2:
        summary_keys_and_values = tables[1].split("</thead>")
        summary_keys = re.findall(r"<th>(.+?)<\/th>", summary_keys_and_values[0])
        summary_values_html = summary_keys_and_values[1].split("</tr>")
        for value_html in summary_values_html:
            values = re.findall(r"<th>(.+?)<\/th>", value_html)
            if not values:
                values = re.findall(r"<td>(.+?)<\/td>", value_html)
            if len(values) != len(summary_keys):
                continue
            summary.append(dict(zip(summary_keys, values)))
        courses_keys_and_values = tables[2].split("</thead>")
    else:
        courses_keys_and_values = tables[1].split("</thead>")

    courses_keys = re.findall(r"<th.*>(.+?)<\/th>", courses_keys_and_values[0])
    courses_values_html = courses_keys_and_values[1].split("</tr>")
    courses = []
    for value_html in courses_values_html:
        if "<td\n" in value_html:
            value_html = value_html.replace("<td\n", "<td>")
        sup_pattern = re.compile(r"(\S+)\t*<sup style=.*>(.*)</sup>")
        if sup_pattern.findall(value_html):
            value_html = re.sub(sup_pattern, r"\1 \2", value_html)
        values = re.findall(r"<td.*?>\s*(.*?)\s*<\/td>", value_html)
        if len(values) != len(courses_keys):
            continue
        courses.append(dict(zip(courses_keys, values)))
    return {"summary": summary, "list": courses}

tju.parser.parse_score_exp

parse_score_exp(html)
Source code in src/tju/parser/score.py
def parse_score_exp(html):
    keys_and_values_html = re.findall(
        r"<table.*class=\"gridtable\">([\s\S]*)</table>", html
    )[0]
    keys_html = re.findall(
        r"<thead class=\"gridhead\">([\s\S]*)</thead>", keys_and_values_html
    )[0]
    result = []
    keys = re.findall(r"<th.*>(.+?)</th>", keys_html)
    values_html = keys_and_values_html.split("</thead>")[1].split("</tr>")
    for value_html in values_html:
        values = re.findall(r"<td.*?>\s*(.*?)\s*</td>", value_html)
        if len(values) != len(keys):
            continue
        result.append(dict(zip(keys, values)))
    return result

tju.parser.parse_free_classroom

parse_free_classroom(html: str) -> list

Parse the EAMS free-classroom search result HTML into a list of dicts.

The result page is an EAMS gridtable fragment (no full wrapper) loaded into the freeRoomList div by the browser. It uses the same

… … structure as the exam and schedule tables.

Raises:

Type Description
DataError

when the EAMS server returns a known error message (e.g. "借用教室小节错误", rate-limit, permission denied).

HtmlParseError

when the response looks like an unexpected HTML structure.

Source code in src/tju/parser/classroom.py
def parse_free_classroom(html: str) -> list:
    """
    Parse the EAMS free-classroom search result HTML into a list of dicts.

    The result page is an EAMS gridtable fragment (no full <html> wrapper) loaded
    into the freeRoomList div by the browser.  It uses the same
    <thead><th>…</thead><tbody><tr><td>… structure as the exam and schedule tables.

    Raises:
        DataError: when the EAMS server returns a known error message
                   (e.g. "借用教室小节错误", rate-limit, permission denied).
        HtmlParseError: when the response looks like an unexpected HTML structure.
    """
    # Detect known server-side error responses before trying to parse
    for marker, message in _SERVER_ERRORS.items():
        if marker in html:
            raise DataError(message)

    keys_raw = re.findall(r"<th[^>]*>([\s\S]*?)</th>", html)
    keys = [re.sub(r"<[^>]+>", "", k).strip() for k in keys_raw]

    tbody_matches = re.findall(r"<tbody[\s\S]*?</tbody>", html)
    if not tbody_matches:
        # No tbody at all → may be an empty result page or unexpected format
        if "gridtable" not in html and "<th" not in html:
            raise HtmlParseError("Unexpected free classroom response: no grid table found.")
        return []

    tbody = tbody_matches[0]
    rows = re.findall(r"<tr[^>]*>([\s\S]*?)</tr>", tbody)

    classrooms = []
    for row in rows:
        cells = re.findall(r"<td[^>]*>([\s\S]*?)</td>", row)
        if not cells:
            continue
        # Strip inner HTML tags and whitespace from each cell
        values = [re.sub(r"<[^>]+>", "", c).strip() for c in cells]
        if not keys:
            classrooms.append({"row": values})
            continue
        if len(values) != len(keys):
            raise HtmlParseError(
                f"Free classroom column count mismatch: got {len(values)}, expected {len(keys)}"
            )
        classrooms.append(dict(zip(keys, values)))

    return classrooms