LINUX.ORG.RU

Нужен совет как сделать ML

 ,


1

3

Стало у меня больше свободного времени. В такие моменты я как обычно возвращаюсь к идее парсера одного языка разметки. Язык почти как xml, практически можно сказать что спецсимволы ‘<’, ‘>’, ‘/’ заменены на другие. Тексты на языке очень похожи на тексты исходников для вёрски в LaTeX.

Задуманный парсер ограниченее LaTeX - вся разметка языка на конструкциях вида \markA, \markB{argument}, \markC[optional argument]{arg}. Всё остальное употребляется как обычный текст.

Вот нужен совет как определить чтение этих конструкций, хотя бы в виде regexp’а вроде «\[a-zA-Z]+». Как и TeX парсер должен быть ориентирован на человеческие тексты, а значит знаки препинания из тела конструкции вылетают. Также парсер должен работать с исходниками для TeX/LaTeX, который расчитан на математические выражения - плюсы, минусы + спецсимволы для TeX ‘_’, ‘^’, ‘$’ и прочее под ограничением. Забавно, но эксперименты с pdflatex наталкивают на ещё большую ограниченость парсинга по сравнению с TeX - его \def вполне может включать в тело конструкций практически все выше перечисленное. Ладно мне такой парсер гораздо труднее реализовать, главное - нужно ли это? Получается парсер не рассчитан на подобные TeX-хаки.

Как посоветуйте определять конструкции? Хочется ведь прилично именовать разметку без всяких \MyNewSuperPuperGiperMegaTag.

PS. По сути проблема - как делать многословные идентификаторы в языке типа «go-forward» или «go_forward» когда практически все символы с клавиатуры под ограничением, кроме алфавита и символа «@» (привет от команды \makeatletter)? К проблеме можно подойти философски - имеется диалектическое противоречие 2-х процессов: с одной стороны у нас обработка текстов материальными машинами с ПО подобным TeX и ограничениями как у TeX, с другой - процесс работы с текстами используя материальную клавиатуру со всеми известным ограничением на набор символов (почему бы не дать языку возможность быть массово используемым?). И, вроде бы, \def TeX-а позволяет обходить принятый в сообществе LaTeX стиль разметки - как показывают эксперименты с pdflatex, подобные хаки работают весьма нестабильно. Да и вопрос - зачем они нужны при использовании самого LaTeX. Из этого всего выводится очевидное - использовать смену регистра вроде \goForward.

★★★★★

Последнее исправление: ados (всего исправлений: 3)

главное - нужно ли это?

Смотря для чего:-)

Человечеству - не нужно. Вам - м.б. и да, хотя бы для набора опыта, получения удовольствия от решения задачи и т.д.

ЩЫ. Если хочется сделать что то нужное, пойдёмте вьюверы для научных данных писать или статфизикой заниматься:-)

AntonI ★★★★★
()
Последнее исправление: AntonI (всего исправлений: 1)
@dataclass
class Token:
  type: TokenType
  value: str

@dataclass
class Parser:
  stream: typing.TextIO
  curch: str | None = None
  nextch: str | None = None
  def readch(self) -> str: ...
  def advance(self):
    self.curch, self.nextch = self.nextch, self.readch() 
  def peekch(self, charset: str) -> bool:
    if self.nextch in charset:
       self.advance()
       return True
    return False
  def parse(self) -> Iterable[Token]:
    self.advance()
    while True:
      if self.peekch('\\'):
        val = ''  # Если эта косая палка не нужна, а она не нужна в принципе
        # Тут еще один цикл
        while self.peekch('abcdef...A..Z'):
          val += self.curch
        yield Token(TokenType.Tag, val)
      if self.peekch(''):
        return Token(TokenType.EOF, self.curch)
rtxtxtrx ★★
()
Последнее исправление: rtxtxtrx (всего исправлений: 6)
Ответ на: комментарий от rtxtxtrx

Я не знаю какие там сейчас языки для псевдокода, думаю питон понятен и такой парсер можно реализовать на любом языке, где есть классы и генераторы. parse можно обозвать как next_token, либо выделить из парсера лексер/токенайзер, токены в дерево можно превратить с помощью рекурсивного спуска либо сразу транспилировать в хтмл какой. Не знаю в чем замысел глубинный. Но на высокоуровневых языках на токены можно и с помощью регулярок разбить - так еще проще (и побыстрее). Нужны сущности: Token, TokenType (Enum), Tokenizer (просто разбивает на токены) и Parser/Compiler (проверяет токены и что-то на выходе выдает). Довольно тривиальная задача. Не знаю что там годами обдумывать

peekch можно обозвать как match и ввести еще обратную ему операцию not_match чтобы advance выполнялся пока текущий символ не входит в заданный диапазон

rtxtxtrx ★★
()
Последнее исправление: rtxtxtrx (всего исправлений: 2)
Ответ на: комментарий от rtxtxtrx

Еще можно такой финт сделать:

input: str | None = None # разбираемая строка
def match(self, pat: re.Pattern) -> str | Literal[False]:
  if m := pat.match(self.input):
    rv = m.group(0)
    # Все время обрезаем строку
    self.input = self.input[len(rv):]
    return rv
  return False

NAME_RE = re.complile('[a-z]\w*')
if val := self.match(NAME_RE):
   yield Token('NAME', val)

Много чего придумать можно в зависимости от возможностей языках. На джавах, растах, go, java и c++ будет не так весело

rtxtxtrx ★★
()
Ответ на: комментарий от rtxtxtrx

Довольно элегантно, но не оптимально:

import re
from dataclasses import dataclass


@dataclass
class Token:
    name: str
    value: str


@dataclass
class Tokenizer:
    token_types = {
        "ID": re.compile(r"[a-z]\w*", re.I),
        "NUMBER": re.compile(r"\d+(\.\d*)?"),
        "SPACE": re.compile(r"\s+"),
        "OPERATOR": re.compile(r"[-+*/()=]"),
    }

    def next_token(self) -> Token:
        for token_name, token_pat in self.token_types.items():
            if m := token_pat.match(self.input):
                val = m.group(0)
                self.input = self.input[len(val) :]
                return Token(token_name, val)
        raise ValueError()

    def tokenize(self, input_: str) -> list[Token]:
        self.input = input_
        rv = []
        while self.input:
            tok = self.next_token()
            rv.append(tok)
        return rv


assert Tokenizer().tokenize("res = 2 + 2 * 2") == [
    Token(name="ID", value="res"),
    Token(name="SPACE", value=" "),
    Token(name="OPERATOR", value="="),
    Token(name="SPACE", value=" "),
    Token(name="NUMBER", value="2"),
    Token(name="SPACE", value=" "),
    Token(name="OPERATOR", value="+"),
    Token(name="SPACE", value=" "),
    Token(name="NUMBER", value="2"),
    Token(name="SPACE", value=" "),
    Token(name="OPERATOR", value="*"),
    Token(name="SPACE", value=" "),
    Token(name="NUMBER", value="2"),
]
rtxtxtrx ★★
()

Бесполезная мысль вслух: а потыкай ка палочкой lua, тот который в luatex. pdflatex — это почти прошлое тысячелетие.

Хотя конечно «python как обёртка» всё решает.

Evgueni ★★★★★
()
Последнее исправление: Evgueni (всего исправлений: 1)

как делать многословные идентификаторы … спользуя материальную клавиатуру

Но ведь у нас же есть XCompose и кроме go-forward может быть go—forward, go–forward и go−forward, например. Ну или вообще любое извращение вида go·forward просто потому что можем.

ugoday ★★★★★
()
Ответ на: комментарий от rtxtxtrx

На Пердле будет ещё интереснее, питонх можно будет выкинуть. И это я не люблю Пердл, но то для чего он сделан, он делает преотлично.

Virtuos86 ★★★★★
()
Последнее исправление: Virtuos86 (всего исправлений: 1)
Ответ на: комментарий от Virtuos86

Забавное утверждение!!! Я свой коды выше немного изменил, добавил 100 строчек и получил полноценный интерпретатор:

from __future__ import annotations

import operator
import re
import typing
from dataclasses import dataclass


class Token(typing.NamedTuple):
    type: str
    value: str


class TokenizerError(Exception):
    pass


class Tokenizer:
    TOKEN_TYPES = {
        # "ID": re.compile(r"[a-z]\w*", re.I),
        "NUMBER": re.compile(r"\d+(\.\d*)?"),
        "SPACE": re.compile(r"\s+"),
        "ADDSUB": re.compile(r"[-+]"),
        "MULDIV": re.compile(r"[*/]"),
        "LPAREN": re.compile(r"\("),
        "RPAREN": re.compile(r"\)"),
        "EOF": re.compile(r"$"),
    }

    def get_token(self) -> Token:
        for typ, pat in self.TOKEN_TYPES.items():
            if m := pat.match(self.input, self.pos):
                self.pos = m.end()
                return Token(typ, m.group(0))
        raise TokenizerError(f"unpexpected character at position: {self.pos}")

    def tokenize(self, input_: str) -> list[Token]:
        self.input = input_
        self.pos = 0
        rv = []
        while 1:
            t = self.get_token()
            rv.append(t)
            if t.type == "EOF":
                break
        return rv


class Expr:
    def eval(self) -> int:
        raise NotImplemented


@dataclass
class Const(Expr):
    val: int

    def eval(self) -> int:
        return self.val


@dataclass
class BinOp(Expr):
    op: str
    left: Expr
    right: Expr

    def eval(self) -> int:
        return {
            "+": operator.add,
            "-": operator.sub,
            "*": operator.mul,
            "/": operator.floordiv,
        }[self.op](
            self.left.eval(),
            self.right.eval(),
        )


@dataclass
class UnaryOp(Expr):
    op: str
    right: Expr

    def eval(self) -> int:
        return {
            "+": operator.pos,
            "-": operator.neg,
        }[self.op](
            self.right.eval(),
        )


class ParseError(Exception):
    pass


class Parser:
    def advance(self) -> None:
        self.cur_tok, self.next_tok = self.next_tok, next(self.token_it, None)

    def match(self, token_type: str) -> bool:
        if self.next_tok is not None and self.next_tok.type == token_type:
            self.advance()
            return True
        return False

    def expect(self, token_type: str) -> None:
        if not self.match(token_type):
            raise ParseError(
                f"Unexpected token: expected {token_type!r}, got {self.next_tok.type!r}"
            )

    def factor(self) -> Expr:
        if self.match("LPAREN"):
            rv = self.expr()
            self.expect("RPAREN")
            return rv
        if self.match("ADDSUB"):
            return UnaryOp(op=self.cur_tok.value, right=self.factor())
        self.expect("NUMBER")
        return Const(val=float(self.cur_tok.value))

    def muldiv(self) -> Expr:
        rv = self.factor()
        while self.match("MULDIV"):
            rv = BinOp(left=rv, op=self.cur_tok.value, right=self.factor())
        return rv

    def addsub(self) -> Expr:
        rv = self.muldiv()
        while self.match("ADDSUB"):
            rv = BinOp(left=rv, op=self.cur_tok.value, right=self.muldiv())
        return rv

    expr = addsub

    def parse(self, tokens: list[Token]) -> Expr:
        # Всякий мусор типа пробелов и комментариев фильтруют
        self.token_it = filter(lambda t: t.type != "SPACE", tokens)
        self.next_tok = None
        self.advance()
        rv = self.expr()
        self.expect("EOF")
        print(rv)
        return rv


import sys

tokens = Tokenizer().tokenize(sys.argv[1])
print(tokens)
print(Parser().parse(tokens).eval())

Да лишп - это чистая логика:

BinOp(op='+', left=Const(val=2.0), right=BinOp(op='*', left=Const(val=2.0), right=Const(val=2.0)))
(+ 2.0 (* 2.0 2.0))

Но факт в том, что на питоне любые сложные вещи - выглядят как типичное приложение хипстора в сто строк…

И, да, тайпхинты тут видны только потому что VSCode довольно убог и часто не может определить возвращаемый тип. Осознай это и страдай

rtxtxtrx ★★
()
Последнее исправление: rtxtxtrx (всего исправлений: 4)
Ответ на: комментарий от rtxtxtrx

И токенайзер еще красивее можно сделать:

class Token(typing.NamedTuple):
    type: str
    value: str


class Tokenizer:
    TOKEN_TYPES = {
        "NUMBER": r"\d+(\.\d*)?",
        "SPACE": r"\s+",
        "ADDSUB": r"[-+]",
        "MULDIV": r"[*/]",
        "LPAREN": r"\(",
        "RPAREN": r"\)",
        "EOF": "$",
        # must by last
        "UNEXPECTED": ".",
    }

    @cached_property
    def re_token(self) -> re.Pattern:
        return re.compile(
            "|".join(f"(?P<{k}>{v})" for k, v in self.TOKEN_TYPES.items())
        )

    def tokenize(self, s: str) -> typing.Iterable[Token]:
        def make_token(m: re.Match) -> Token:
            if m.lastgroup == "UNEXPECTED":
                raise ValueError(f"Unexpected character at offset {m.start()}")
            return Token(m.lastgroup, m.group(m.lastgroup))

        return map(make_token, self.re_token.finditer(s))
rtxtxtrx ★★
()

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

from __future__ import annotations

import string
import typing
from dataclasses import dataclass, field
from io import StringIO


class Node:
    pass


@dataclass
class Element(Node):
    name: str
    attrs: dict[str, str] = field(default_factory=dict)
    children: list[Node] = field(default_factory=list)


@dataclass
class Text(Node):
    value: str


class SyntaxError(Exception):
    pass


class MLParser:
    def readch(self) -> str:
        return self.fp.read(1)

    def advance(self) -> None:
        self.curch, self.nextch = self.nextch, self.readch()

    def match(self, charset: str) -> bool:
        if self.nextch and self.nextch in charset:
            self.advance()
            return True
        return False

    def expect(self, charset: str) -> None:
        if not self.match(charset):
            raise SyntaxError(f"syntax error at offset {self.fp.tell()}")

    def parse_name(self) -> str:
        rv = ""
        while self.match(string.ascii_letters):
            rv += self.curch
        if not rv:
            raise SyntaxError("required name")
        return rv

    def skip_spaces(self) -> None:
        while self.match(string.whitespace):
            pass

    def parse_quouted(self) -> str:
        self.expect('"')
        rv = ""
        while True:
            if not self.nextch:
                raise SyntaxError("unexpected end")
            if self.match('"'):
                break
            self.advance()
            rv += self.curch
        return rv

    def handle_close_tag(self) -> None:
        name = self.parse_name()
        self.expect(">")
        if not self.open_tags or self.open_tags.pop() != name:
            raise SyntaxError(f"unexpected close tag {name}")
        # self.skip_spaces()

    def collect_children(self, node: Element) -> None:
        text = ""
        while self.nextch:
            if self.match("<"):
                if text:
                    node.children.append(Text(text))
                    text = ""
                if self.match("/"):
                    self.handle_close_tag()
                    break
                child = Element(self.parse_name())
                self.open_tags.append(child.name)
                while True:
                    if self.match(">"):
                        break
                    self.expect(string.whitespace)
                    self.skip_spaces()
                    attr = self.parse_name()
                    self.skip_spaces()
                    self.expect("=")
                    self.skip_spaces()
                    child.attrs[attr] = self.parse_quouted()
                self.collect_children(child)
                node.children.append(child)
            else:
                self.advance()
                text += self.curch
        if text:
            node.children.append(Text(text))

    def parse(self, fp: typing.TextIO) -> Element:
        self.fp = fp
        self.curch = self.nextch = None
        self.open_tags = []
        self.advance()
        root = Element("root")
        self.collect_children(root)
        if self.open_tags:
            raise SyntaxError(f"unclosed tags: {', '.join(self.open_tags)}")
        return root


import sys

print(MLParser().parse(StringIO(sys.argv[1])))

Вроде нормально все парсит:

python ml.py 'Hello, <span color="red"><i><b>world</b></i></span>!'
Element(name='root', attrs={}, children=[Text(value='Hello, '), Element(name='span', attrs={'color': 'red'}, children=[Element(name='i', attrs={}, children=[Element(name='b', attrs={}, children=[Text(value='world')])])]), Text(value='!')])

Для твоего диалекта текса изменения нужны минимальные. Или че ты там придумываешь. Простая задача, но малоприменимая или не знаю, что там придумать: текс в хтмл превращать? Зато показательно тотальное превосходство питона над статически-типизированными недоязыками. В нем НЕ НУЖНЫ DDD, паттерны и пр чушь, хотя я уверен, что найдутся аметисты, которые даже тут их обнаружат (считаю Java абсолютным злом как и заучивание [ее и применимых только в ней] паттернов)

rtxtxtrx ★★
()
Последнее исправление: rtxtxtrx (всего исправлений: 3)
Ответ на: комментарий от AntonI

Любят же люди себе приключения на пятую точку находить. Глазами миллион точек не увидеть, на монитор не вывести, а значит и рисовать их не надо. Совсем. Отрисовывать нужно только то, что человек способен увидеть и различить, т.е. в даннмом случае грубую аппроксимацию исходных данных.

ugoday ★★★★★
()
Ответ на: комментарий от ugoday

Любят же люди ванговать о том в чем нифига не понимают.

Миллион это всего лишь картинка 1000х1000, все прекрасно видно на обычном fullHD, не говоря уже о 4К мониторе.

1000^3 это уже миллиард (для тех кто прогуливал советсткую школу - на ЕГЭ об этом могут уже и не рассказывать), их действительно не увидишь все - но есть специальные способы отображения позволяющие увидеть самые интересные места.

AntonI ★★★★★
()
Ответ на: комментарий от AntonI

Миллион это всего лишь картинка 1000х1000,

И вправду, тут я дал маху.

их действительно не увидишь все

Но в главном-то я прав! И собственно о «есть специальные способы отображения» и говорил.

ugoday ★★★★★
()
Ответ на: комментарий от ugoday

Ну што бы эти способы задействовать, миллиард точек таки придётся так или иначе обойти. Или значимую их часть. Ну что бы понять что рисовать и как, а что нет.

А ещё таких файлов может быть сильно больше одного,и хотелось бы между ними переключаться/крутить изображение/играть настройками без адищенских лагов.

Современные типовые решения вроде паравью такое не тянут.

AntonI ★★★★★
()
Последнее исправление: AntonI (всего исправлений: 1)
Ответ на: комментарий от Virtuos86

ЛУЧШЕ бы опу пример накинул на Rust или Go. На C ты даже split не осилишь сделать. Доказал бы превосходство тупизации над моим write-only кодом на коленке c первыми, пришедшими в голову решениями. Да, это как эпичное Гамаза: «Сделай мне обзор на дендиевскую игрушку МАРИО ОДИН»

rtxtxtrx ★★
()
Последнее исправление: rtxtxtrx (всего исправлений: 1)
Ответ на: комментарий от AntonI

Ты такой кубик нормально не посмотришь глазками всё равно. К визуализации настолько сложных и больших данных требуются специальные подходы, универсального нет, например можно зону максимумов каких-то по каким-то критериям рисовать и смотреть, скажем на 1000 точек или размерность понижать до 2 мерной, если важно на какие-то отношения точек в пространстве посмотреть, например какие с какими точками рядом, а какие нет. Линейный график то проблемно смотреть, мне норм было только потому что там пики широкие и заметные были.

peregrine ★★★★★
()
Ответ на: комментарий от ugoday

Не надо апроксимацию грубую. В ней толку нет обычно. Надо смотреть всякие крайние, предельные и аномальные случаи, там всё интересно, но сначала их аналитически или как-то иначе надо определять уметь.

peregrine ★★★★★
()
Ответ на: комментарий от peregrine

Если у вас есть 10млн точек и вы хотите отобразить их на экране с 2млн пикселей, то вы гарантированно обламаетесь. Я говорю только об этом. Соответвтенно имеет смысл как-то заморочиться и сократить количество данных для отображения.

ugoday ★★★★★
()
Ответ на: комментарий от rtxtxtrx

Мысли вслух: а выглядел бы гипотетический язык разметки как:

(. Hello, (red (i (b world)))!)

— здесь первый элемент списка модифицирует представление остальных элементов. Начальная точка означает отсутствие изменений.

То и разбор этой строки был бы вообще элементарен.

ugoday ★★★★★
()
Ответ на: комментарий от ugoday

Ну я помню статью 2009-го года от одного из директоров Google про написание интерпретатора LISP. Я тогда про обратную польскую нотацию узнал (да пхпмакакой можно быть и не зная про битовые флаги, ни то что про польские нотации).

rtxtxtrx ★★
()
Ответ на: комментарий от peregrine

Успехи успешные. Наши вьюверы отлично позволяют смотреть кубики 1000^3 и даже больше.

Ты путаешь 1D, 2D и 3D визуализацию - а это вообще то совершенно разные задачи.

AntonI ★★★★★
()

Я, кстати, чтобы код не пропадал без дела, осуществил свою мечту. Я всегда хотел хотел bbcodes в терминале, чтобы еще можно было HEX для задания цвета использовать.

pip install terml
from terml import format

print(format("[b][color=#f70000 background=yellow] WARNING [/color]:[/b] [magenta]Life leads to [i blue underline]Death[/i].[/magenta]"))

Результат: https://ibb.co/M78y2z8

rtxtxtrx ★★
()