> Вопрос в следующем: как лучше решить такую задачу?> Меня интересует методика и, если можно, пример.Обычно (когда вещей вроде ручных subseq или регулярок уже не хватает) используют какую-либо библиотеку генераторов парсеров - на основе LALR, PEG и тому подобных грамматик. Например есть
cl-yacc (и
пример там же), с его использованием разбор таких строк будет выглядеть примерно так:
(defpackage #:obj-format
(:use #:common-lisp
#:yacc)
(:export #:lexer
#:*parser*
#:obj-repl))(in-package #:obj-format);;;; The lexer.
;;; lexer conditions
(define-condition obj-lexer-error (yacc-runtime-error)
((character :initarg :character :reader lexer-error-character))
(:report (lambda (e stream)
(format stream "OBJ lexing failed~@[: unexpected character ~S~]"
(lexer-error-character e)))))(defun obj-lexer-error (char)
(error (make-condition 'obj-lexer-error :character char)))(defun maybe-unread (char stream)
(when char
(unread-char char stream)))(defun read-integer (stream)
(let (result)
(loop
(let ((char (read-char stream nil nil)))
(when (or (null char) (not (digit-char-p char)))
(maybe-unread char stream)
(when (null result)
(obj-lexer-error char))
(return-from read-integer result))
(setf result (+ (* (or result 0) 10)
(- (char-code char) (char-code #\0))))))))(defun skip-commentaries (stream)
(loop
(let ((char (read-char stream nil nil)))
(when (or (char= char #\Newline) (null char))
(return-from skip-commentaries)))))(defun lexer (&optional (stream *standard-input*))
(loop
(let ((char (read-char stream nil nil)))
(cond ((member char '(#\Space #\Tab)))
((member char '(nil #\Newline))
(return-from lexer (values nil nil)))
((char= char #\#)
(skip-commentaries stream)
(return-from lexer (values nil nil)))
((member char '(#\/))
(let ((symbol (intern (string char) '#.*package*)))
(return-from lexer (values symbol symbol))))
((char= char #\f)
(return-from lexer (values 'f #\f)))
((digit-char-p char)
(unread-char char stream)
(return-from lexer (values 'number (read-integer stream))))
(t
(obj-lexer-error char))))));;; semantic rules
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun expression (expression)
`(:expression ,expression))
(defun face-definition (f tokens)
(declare (ignore f))
(labels ((flatten-this (list)
(if (atom (first list))
(when list
(list list))
(if (atom (first (first list)))
(cons (first list) (flatten-this (rest list)))
(flatten-this (first list))))))
`(:face-definition ,@(flatten-this tokens))))
(defun vertex (vertex)
`(:vertex ,vertex))
(defun vertex/texture-coordinate (vertex /1 texture-coordinate)
(declare (ignore /1))
`(:vertex/texture-coordinate ,vertex ,texture-coordinate))
(defun vertex/texture-coordinate/normal (vertex /1 texture-coordinate /2 normal)
(declare (ignore /1 /2))
`(:vertex/texture-coordinate/normal ,vertex ,texture-coordinate ,normal))
(defun vertex/normal (vertex /1 /2 normal)
(declare (ignore /1 /2))
`(:vertex/normal ,vertex ,normal))
) ;;; semantic rules
;;; define parser
(define-parser *parser*
(:start-symbol expression)
(:terminals (number / f))
(expression
(face-definition #'expression)
)
(face-definition
(f face-definition-1 #'face-definition)
(f face-definition-2 #'face-definition)
(f face-definition-3 #'face-definition)
(f face-definition-4 #'face-definition))
(face-definition-1
(vertex face-definition-1)
vertex)
(face-definition-2
(vertex/texture-coordinate face-definition-2)
vertex/texture-coordinate)
(face-definition-3
(vertex/texture-coordinate/normal face-definition-3)
vertex/texture-coordinate/normal)
(face-definition-4
(vertex/normal face-definition-4)
vertex/normal)
(vertex
(number #'vertex))
(vertex/texture-coordinate
(number / number #'vertex/texture-coordinate))
(vertex/texture-coordinate/normal
(number / number / number #'vertex/texture-coordinate/normal))
(vertex/normal
(number / / number #'vertex/normal)))
(defun obj-repl (&optional (stream *standard-input*))
(let ((*standard-input* stream))
(format t "Type an obj-expression.~%~%")
(loop
(with-simple-restart (abort "Return to OBJ-REPL toplevel.")
(format t "> ")
(let ((obj-expr (parse-with-lexer #'lexer *parser*)))
(when (null obj-expr)
(return-from obj-repl))
(format t "< ~A~%" obj-expr))))));;;; Examples.
#|
OBJ-FORMAT> (obj-repl)
Type an obj-expression.
> q
OBJ lexing failed: unexpected character #\q
[Condition of type OBJ-LEXER-ERROR]
Unexpected terminal NIL (value NIL). Expected one of: (F)
[Condition of type YACC-PARSE-ERROR]
> f
Unexpected terminal NIL (value NIL). Expected one of: (NUMBER)
[Condition of type YACC-PARSE-ERROR]
> f 1
< (EXPRESSION (FACE-DEFINITION (VERTEX 1)))
> f 1 2 3
< (EXPRESSION (FACE-DEFINITION (VERTEX 1) (VERTEX 2) (VERTEX 3)))
> f 1/2 3/4
< (EXPRESSION
(FACE-DEFINITION (VERTEX/TEXTURE-COORDINATE 1 2)
(VERTEX/TEXTURE-COORDINATE 3 4)))
> f 1 1/2
Unexpected terminal / (value /). Expected one of: (NUMBER NIL)
[Condition of type YACC-PARSE-ERROR]
Unexpected terminal NUMBER (value 2). Expected one of: (F)
[Condition of type YACC-PARSE-ERROR]
Unexpected terminal NIL (value NIL). Expected one of: (F)
[Condition of type YACC-PARSE-ERROR]
> f 1/2/3 4/5/6 # comments...
< (EXPRESSION
(FACE-DEFINITION (VERTEX/TEXTURE-COORDINATE/NORMAL 1 2 3)
(VERTEX/TEXTURE-COORDINATE/NORMAL 4 5 6)))
|#
и дальше расширять до полного парсера этих OBJ файлов тоже довольно легко - просто добавляются правила в грамматику и/или новые условия к лексеру. И таким способом (LALR(1) грамматики + лексер) можно разбирать довольно большой класс языков (уровня Си, например).