MikroTik. Скрипт для транслита Windows-1251 комментариев

Добавлено: 10/09/2026 11:38 |  Обновлено: 10/09/2026 11:41 |  Добавил: nick |  Просмотры: 27 Комментарии: 0
Вводная часть
Сейчас RouterOS экспортирует не-ASCII текст, обычно кириллицу, в виде последовательности байтов в кодировке UTF-8. Предыдущие версии RouterOS делали это в кодировке Windows-1251/CP1251. Этот скрипт предназначен для транслитерации (перевод в латиницу) байтов в кодировке Windows-1251/CP1251..
Сейчас RouterOS экспортирует не-ASCII текст, обычно кириллицу, в виде последовательности байтов в кодировке UTF-8. Предыдущие версии RouterOS делали это в кодировке Windows-1251/CP1251. Поэтому после обновления комментарии в кириллице будут выглядеть вот так: Предлагаемый скрипт предназначен для транслитерации (перевод в латиницу) байтов в кодировке Windows-1251/CP1251. Но если вы хотите вместо транслита перевести CP1251 в UTF-8, то можете взять данный скрипт за основу.

Суть работы скрипта

Скрипт сканирует экспортированную конфигурацию RouterOS (MikroTik) (.rsc) на наличие значений comment=, содержащих последовательности экранирования байтов \\XX, декодирует их, транслитерирует кириллический текст в латиницу и создаёт новый .rsc-скрипт, содержащий команды set [ find comment="<исходный комментарий с экранированием>" ] comment="<латиница>" — по одной команде для каждого пути меню — чтобы его можно было напрямую запустить на роутере для перезаписи этих комментариев.
#!/usr/bin/env python3
"""
translit_comments.py

Scans a RouterOS (MikroTik) exported config (.rsc) for `comment=` values
that contain \\XX byte-escape sequences (RouterOS' way of exporting
non-ASCII text, usually Windows-1251/CP1251 Cyrillic), decodes them,
transliterates the Cyrillic text to Latin, and writes a new .rsc script
containing `set [ find comment="<original escaped comment>" ] comment="<latin>"`
commands -- one per menu path -- so it can be run directly on the router
to rewrite those comments in place.

Usage:
    python3 translit_comments.py input.rsc output.rsc
"""

import re
import sys

# --- Cyrillic -> Latin transliteration table -------------------------------

_TRANSLIT = {
    'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'e',
    'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
    'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
    'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch',
    'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya',
}
# Add uppercase variants (capitalize the first letter of the mapped value)
for _cyr, _lat in list(_TRANSLIT.items()):
    _TRANSLIT[_cyr.upper()] = _lat.capitalize() if _lat else ''


def transliterate(text: str) -> str:
    return ''.join(_TRANSLIT.get(ch, ch) for ch in text)


# --- RouterOS string byte-escape decoding -----------------------------------

_HEX_RE = re.compile(r'^[0-9A-Fa-f]{2}$')


def decode_ros_escapes(raw: str, encoding: str = 'cp1251') -> str:
    """
    Decode a RouterOS-exported string that may contain \\XX hex byte
    escapes (non-ASCII characters) as well as \\" and \\\\ escapes,
    back into a normal unicode string using the given single-byte
    encoding (RouterOS' console default is Windows-1251 for Cyrillic).
    """
    out = bytearray()
    i = 0
    n = len(raw)
    while i < n:
        c = raw[i]
        if c == '\\' and i + 1 < n:
            nxt = raw[i + 1]
            if nxt in ('"', '\\'):
                out.append(ord(nxt))
                i += 2
                continue
            two = raw[i + 1:i + 3]
            if _HEX_RE.match(two):
                out.append(int(two, 16))
                i += 3
                continue
            # unknown escape, keep literally
            out.append(ord(nxt))
            i += 2
            continue
        out.append(ord(c) if ord(c) < 256 else 63)  # '?' fallback
        i += 1
    return bytes(out).decode(encoding, errors='replace')


def ros_escape(text: str) -> str:
    """Escape a plain-ASCII string for safe placement inside a RouterOS
    double-quoted string literal (escape backslashes and quotes)."""
    return text.replace('\\', '\\\\').replace('"', '\\"')


# --- Config parsing -----------------------------------------------------

def join_continuations(text: str):
    """
    RouterOS export wraps long lines with a trailing backslash; the
    continuation line starts with leading whitespace and is glued
    directly onto the previous line (no extra space inserted).
    Returns a list of logical (fully joined) lines.
    """
    raw_lines = text.splitlines()
    logical = []
    buf = ''
    for line in raw_lines:
        if buf:
            buf += line.lstrip()
        else:
            buf = line
        if buf.endswith('\\'):
            buf = buf[:-1]
        else:
            logical.append(buf)
            buf = ''
    if buf:
        logical.append(buf)
    return logical


# Matches comment="....." allowing escaped quotes/backslashes inside
_COMMENT_RE = re.compile(r'comment=(".*?(?<!\\)")')

# Detects at least one \XX hex-byte escape (i.e. actual bytecode present)
_HAS_BYTE_ESCAPE_RE = re.compile(r'\\[0-9A-Fa-f]{2}')


def find_commented_lines(logical_lines):
    """
    Walk the joined config lines, tracking the current menu path
    (lines starting with '/'), and yield (path, original_line,
    quoted_comment_literal) for every add/set line whose comment
    contains raw byte escapes.
    """
    path = None
    results = []
    for line in logical_lines:
        stripped = line.strip()
        if not stripped or stripped.startswith('#'):
            continue
        if stripped.startswith('/'):
            path = stripped
            continue
        if not (stripped.startswith('add ') or stripped.startswith('set ')):
            continue

        m = _COMMENT_RE.search(stripped)
        if not m:
            continue

        quoted = m.group(1)          # includes the surrounding quotes
        inner = quoted[1:-1]         # strip the quotes
        if not _HAS_BYTE_ESCAPE_RE.search(inner):
            continue                 # comment has no bytecode -> skip

        results.append((path, stripped, quoted, inner))
    return results


def build_output(entries, encoding='cp1251'):
    out_lines = [
        '# Auto-generated by translit_comments.py',
        '# Rewrites comments containing CP1251 byte-escapes to Latin transliteration',
        '# Rules are located via their original (bytecode) comment value',
        '',
    ]
    for path, orig_line, quoted_orig, inner_escaped in entries:
        decoded = decode_ros_escapes(inner_escaped, encoding=encoding)
        latin = transliterate(decoded)
        latin_escaped = ros_escape(latin)
        out_lines.append(path)
        out_lines.append(
            f'set [ find comment={quoted_orig} ] comment="{latin_escaped}"'
        )
        out_lines.append(f'# was: {decoded}')
        out_lines.append('')
    return '\n'.join(out_lines)


def main():
    if len(sys.argv) != 3:
        print(f'Usage: {sys.argv[0]} input.rsc output.rsc', file=sys.stderr)
        sys.exit(1)

    in_path, out_path = sys.argv[1], sys.argv[2]

    with open(in_path, 'r', encoding='utf-8', errors='replace') as f:
        text = f.read()

    logical_lines = join_continuations(text)
    entries = find_commented_lines(logical_lines)

    if not entries:
        print('No comments with byte-escape sequences found.')
        return

    output = build_output(entries)

    with open(out_path, 'w', encoding='utf-8') as f:
        f.write(output)

    print(f'Found {len(entries)} comment(s) with bytecode. Wrote {out_path}')
    for path, _, _, inner in entries:
        print(f'  {path}: {inner[:40]!r}...' if len(inner) > 40 else f'  {path}: {inner!r}')


if __name__ == '__main__':
    main()

Посмотреть скрипт на GitHub Gist

Оставьте свой комментарий

Комментариев нет