Коллеги, просветите.
Как реализуется парсинг строк, для сценариев, модов и пр. Что-то посложнее Key=Value?
Вот в майнкрафте есть консоль, есть модуль управления, всё это вопринимает кучу команд, написаных от руки.
В том же X@COM система модов хавает вот такую красоту
Как это делается? У меня устойчивое впечатление, что все вокруг знают этот секрет и только я, как лох, пурхаюсь с pos и уже тот же парсинг BBCode превращается в Ад и Ужас.
Я знаю, что на свете существуют регулярные выражения, скажем, но всё явно не так просто.
Как парсят несложные скрипты? Как распарсить что-то вроде форумного BBCоde? Как реализуется распозначание сколько-нибудь сложных команд из игровой консоли, например? Как это сделать из Делфи (6)? Подскажите направление, в котором копать/искать.
К сожалению не могу претендовать на профессионализм, так как я писал только парсеры для XML, JSON, арифметических и условных выражений и нескольких форматов 3д моделей. Еще были шаблоны для web страниц на php.
Я думаю все обстоит именно так плохо как ты думаешь.
Суть парсинга - лишь преобразование в удобный программе формат из удобного для чтения/хранения формата.
Практически всегда парсинг - это просто разбиение на составные элементы(списком либо деревом если есть иерархия) на основе спец символов и анализ кусочков - поиск токенов, команд, тэгов и т.д.
Делфи не очень к этому приспособлен в силу не самой крутой работы со строками(впрочем сторонние компоненты решают, да и в последних версиях все гораздо круче), но меня в конце концов все сводится к посимвольному перебору строки.
Регулярные выражения - очень удобная штука, хотя в парсинге имхо это скорее вспомогательный инструмент для поиска и замены. Основной принцип остается таким же. В php регулярки позволяли решать проблемы целиком в силу специфики - там и входные данные и результат - текст с тегами, свой формат представления просто не нужен.
Вот хороший пример, шаблоны я делал очень похоже http://myrusakov.ru/php-parsing-bb.html
По поводу конкретно BBCode и других частных случаев есть много статей.
Самая крутая теория, которую я знаю, находится в книжке «Компиляторы:принципы, технологии и инструменты» - к сожалению я только начал ее осиливать.
В кратце в теории - код разбирается анализатором на токены, где каждое слово или цифра разделяется и записывается в последовательный список (как пример) с определением типа токена (команда, переменная, число, вызов функции). Далее уже по разобранному анализатором коду идет выполнение, которое определить может только тот, кто придумал этот язык, разумеется.
Лучше всего из всех здесь присуствующих объяснит тему Дож, так как написал и свой язык программирования и даже компилятор (если не ошибаюсь. Ну написал же уже надеюсь? :) )
В целом анализатор писал и я, когда мне требовалась умная подсветка и автодополнения кода, когда писал свой quadshade.
procedure TG2Parser.SkipSpaces;
begin
while (_Position < Len)
and (
(_Text[_Position] = ' ')
or (_Text[_Position] = #$D)
or (_Text[_Position] = #$A)
) do
Inc(_Position);
end;
function TG2Parser.Read(const Count: Integer): AnsiString;
var c: Integer;
begin
if Count + _Position > Len then
c := Len - _Position
else
c := Count;
SetLength(Result, c);
Move(_Text[_Position], Result[1], c);
Inc(_Position, c);
end;
function TG2Parser.Read(const Pos: Integer; const Count: Integer): AnsiString;
var c: Integer;
begin
if Count + Pos > Len then
c := Len - Pos
else
c := Count;
if c <= 0 then
begin
Result := '';
Exit;
end;
SetLength(Result, c);
Move(_Text[Pos], Result[1], c);
end;
function TG2Parser.IsAtSymbol: Integer;
var i, j: Integer;
var Match: Boolean;
begin
for i := 0 to High(_Symbols) do
begin
Match := True;
for j := 0 to Length(_Symbols[i]) - 1 do
if (_CaseSensitive and (_Text[_Position + j] <> _Symbols[i][j + 1]))
or (not _CaseSensitive and (LowerCase(_Text[_Position + j]) <> LowerCase(_Symbols[i][j + 1]))) then
begin
Match := False;
Break;
end;
if Match then
begin
Result := i;
Exit;
end;
end;
Result := -1;
end;
function TG2Parser.IsAtKeyword: Integer;
var i, j: Integer;
var Match: Boolean;
begin
for i := 0 to High(_KeyWords) do
begin
Match := True;
for j := 0 to Length(_KeyWords[i]) - 1 do
if (_CaseSensitive and (_Text[_Position + j] <> _KeyWords[i][j + 1]))
or (not _CaseSensitive and (LowerCase(_Text[_Position + j]) <> LowerCase(_KeyWords[i][j + 1]))) then
begin
Match := False;
Break;
end;
if Match then
begin
Result := i;
Exit;
end;
end;
Result := -1;
end;
function TG2Parser.IsAtCommentLine: Integer;
var i, j: Integer;
var Match: Boolean;
begin
for i := 0 to High(_CommentLine) do
begin
Match := True;
for j := 0 to Length(_CommentLine[i]) - 1 do
if (_CaseSensitive and (_Text[_Position + j] <> _CommentLine[i][j + 1]))
or (not _CaseSensitive and (LowerCase(_Text[_Position + j]) <> LowerCase(_CommentLine[i][j + 1]))) then
begin
Match := False;
Break;
end;
if Match then
begin
Result := i;
Exit;
end;
end;
Result := -1;
end;
function TG2Parser.IsAtCommentStart: Integer;
var i, j: Integer;
var Match: Boolean;
begin
for i := 0 to High(_Comment) do
begin
Match := True;
for j := 0 to Length(_Comment[i][0]) - 1 do
if (_CaseSensitive and (_Text[_Position + j] <> _Comment[i][0][j + 1]))
or (not _CaseSensitive and (LowerCase(_Text[_Position + j]) <> LowerCase(_Comment[i][0][j + 1]))) then
begin
Match := False;
Break;
end;
if Match then
begin
Result := i;
Exit;
end;
end;
Result := -1;
end;
function TG2Parser.IsAtCommentEnd: Integer;
var i, j: Integer;
var Match: Boolean;
begin
for i := 0 to High(_Comment) do
begin
Match := True;
for j := 0 to Length(_Comment[i][1]) - 1 do
if (_CaseSensitive and (_Text[_Position + j] <> _Comment[i][1][j + 1]))
or (not _CaseSensitive and (LowerCase(_Text[_Position + j]) <> LowerCase(_Comment[i][1][j + 1]))) then
begin
Match := False;
Break;
end;
if Match then
begin
Result := i;
Exit;
end;
end;
Result := -1;
end;
function TG2Parser.IsAtString: Integer;
var i, j: Integer;
var Match: Boolean;
begin
for i := 0 to High(_String) do
begin
Match := True;
for j := 0 to Length(_String[i]) - 1 do
if (_CaseSensitive and (_Text[_Position + j] <> _String[i][j + 1]))
or (not _CaseSensitive and (LowerCase(_Text[_Position + j]) <> LowerCase(_String[i][j + 1]))) then
begin
Match := False;
Break;
end;
if Match then
begin
Result := i;
Exit;
end;
end;
Result := -1;
end;
function TG2Parser.IsAtEOF: Boolean;
begin
Result := _Position >= Len;
end;
function TG2Parser.NextToken(var TokenType: TG2TokenType): AnsiString;
var i: Integer;
var b: Boolean;
var Str: AnsiString;
begin
Result := '';
TokenType := ttEOF;
SkipSpaces;
if _Position >= Len then
Exit;
i := IsAtCommentStart;
while i > -1 do
begin
Inc(_Position, Length(_Comment[i][0]));
while (_Position < Len - Length(_Comment[i][1]))
and (IsAtCommentEnd <> i) do
Inc(_Position);
Inc(_Position, Length(_Comment[i][1]));
SkipSpaces;
i := IsAtCommentStart;
end;
i := IsAtCommentLine;
while i > -1 do
begin
Inc(_Position, Length(_CommentLine[i]));
while (_Position < Len)
and (_Text[_Position] <> #$D)
and (_Text[_Position] <> #$A) do
Inc(_Position);
SkipSpaces;
i := IsAtCommentLine;
end;
i := IsAtString;
if i > -1 then
begin
TokenType := ttString;
Inc(_Position, Length(_String[i]));
while (_Position <= Len - Length(_String[i]))
and (IsAtString <> i) do
begin
Result := Result + _Text[_Position];
Inc(_Position);
end;
if _Position <= Len - Length(_String[i]) then
Inc(_Position, Length(_String[i]));
Exit;
end;
i := IsAtSymbol;
if i > -1 then
begin
TokenType := ttSymbol;
Result := _Symbols[i];
Inc(_Position, Length(_Symbols[i]));
Exit;
end;
b := True;
while b do
begin
Result := Result + _Text[_Position];
Inc(_Position);
if _Position >= Length(_Text) then
b := False;
if b and (
(_Text[_Position] = ' ')
or (_Text[_Position] = #$D)
or (_Text[_Position] = #$A)
) then
b := False;
if b then
begin
i := IsAtSymbol;
if i > -1 then
b := False;
end;
end;
if Length(Result) > 0 then
begin
if StrToIntDef(Result, 0) = StrToIntDef(Result, 1) then
begin
TokenType := ttNumber;
Exit;
end;
for i := 0 to High(_KeyWords) do
if LowerCase(_KeyWords[i]) = LowerCase(Result) then
begin
TokenType := ttKeyword;
Result := _KeyWords[i];
Exit;
end;
TokenType := ttWord;
end;
end;
//TG2Parser END
Пасиб, буду копаться.
Я, правда, свой разметочный парсер как раз допилил (узнав, что нет магического слова и всё надо делать руками, что сподвигло :) ) за одно и с твоим варианто разберусь
Писать парсеры ручками дело муторное, можно фгуглить yacc или bison. Однако они не настолько удобные как более модерновые тулзы.
Раз: http://goldparser.org/ Я сам пользовался, сделал интерпретатор си-подобного скрипотового языка, имеет готовые генераторы и парсеры под дельфу.
Два: http://www.antlr.org/ Этим бользуются те кто е&*(шат по хардкору.
Zer0 дело говорит, один раз опробуешь мощь генераторов LALR или LL(k), и больше нет проблемы с парсингом, будете парсить всё на свете силой мысли. Чтобы понять как оно устроено внутри, можно почитать Книгу дракона, но на практике это вовсе не обязательно.
В совсем простых случаях свои парсеры писать легко. У меня с самого начала геймдевелоперской деятельности была парочка функций, которыми я орудовал при разборе простых штук и их мне хватало для большого спектра конфигов
// Убивает все пробелы слева и справа
Trim(S): String;
// Разбивает строку S на две части первой встретившейся подстрокой Separator
// Заносит то, что левее, в Left, а то, что правее, в Right
// Вернёт False, если Separator не найден в строке; в Left окажется S, а в Right пустота
ParseBinary(S, Separator, out Left, out Right): Boolean;
// Разбивает строку на части разделителем Separator (легко реализуется через ParseBinary)
ParseList(S, Separator, out List): Boolean;
В конкурсе depict даже микроскриптовик был, вот начало описания первого уровня
Дешёвый, но универсальный метод своего красивого и удобного конфига — это использовать XML. Готовых парсеров дофига, свой написать нетрудно: последовательно поглощаем символы слева направо и применяем в нужные моменты рекурсию, на выходе получаем дерево, которое уже легко обходить программно.
Doj написал: Дешёвый, но универсальный метод своего красивого и удобного конфига — это использовать XML. Готовых парсеров дофига, свой написать нетрудно: последовательно поглощаем символы слева направо и применяем в нужные моменты рекурсию, на выходе получаем дерево, которое уже легко обходить программно.
Добавлю что XML в силу своей брутальности не совсем приспособлен для лекого редактирования и часто заменяется на JSON, а ценители прекрасного сразу вспомнят YAML. Они оба достаточно просто парсятся.
Однако использовать в релизе plain-text, который еще и парсить надо не рекомендуется, страдает и время загрузки и появляется дырка для желающих своими ручками что-то подправить. Заменой становится BSON файлик который генерится из JSON простеньким парсером-конвертором.
Если говорить о скорости, то ещё можно вспомнить про protobuf, который тоже умеет из plain-text превращаться в бинарный формат, да ещё и генерировать код для его программного чтения.
Но всё же конфиги обычно набираются вручную, парсинг маленького XML-файла трудно назвать брутальным.
А по теме, парсер на конечных автоматах (что бы искал отдельные слова, символы и строки (последовательности в кавычках) и выдавал в виде списка, написать несложно, или лучше использовать готовый.
Dj_smart написал:
Мне больше всего понравился формат файлов как в питоне с отступами по ТАБ-ам (а еще это реализовано в игре Cortex Command). Формат такой:
ParentElement
ChildElement = LOL
Name = Вася
TypeOf = Circle
OtherChild = WUT
Name = Петя
TypeOf
Фишка в простоте, фунциональности и удобочитаемости. И парсер написать нетрудно.
Как реализуется парсинг строк, для сценариев, модов и пр. Что-то посложнее Key=Value
:)
Ты привёл как раз самое простое - ключ=значение. Для него и парсинг, как таковой, не нужен и табуляция не более чем свистелка-перделка (иначе, завязка функциональности на форматирование и звиздец).
Это у меня уже есть, я с этого стартовал.
Doj написал:
// Убивает все пробелы слева и справа
Trim(S): String;
// Разбивает строку S на две части первой встретившейся подстрокой Separator
// Заносит то, что левее, в Left, а то, что правее, в Right
// Вернёт False, если Separator не найден в строке; в Left окажется S, а в Right пустота
ParseBinary(S, Separator, out Left, out Right): Boolean;
// Разбивает строку на части разделителем Separator (легко реализуется через ParseBinary)
ParseList(S, Separator, out List): Boolean;
Примерно суть понятна, спасибо, буду думать дальше.
RichDad написал:
Я сейчас пересел на iOS разработку интерфейса. Там такие конструкции везде. конфиг - скорее всего вот такой.
Понимаешь, вопрос стоит не "где так делается", вопрос стоит "КАК это реализовано" :)
Совет - не стоит ожидать чудес, обычно первый проход делается что называется "в лоб", брутфорсом.
Что оптимизируется:
* identifiers после того как выделены в тексте, через hash map преобразуются в айдишники чтоб в свичах/кейсах было удобнее с ними работать;
* обычно в парсинге исходный текст не разбивается буквально на подстроки которые хранятся в виде копий в памяти, а преобразуется пары (адрес, длинна) в буффере, это позволяет экономить память и немного ускоряет процесс из-за отсуствия необходимости в копировании и строковых аллолкаций (обычно правильно реализованные вектора делают преаллокацию и там выделений получается в сумме меньше);
Что советую заценить: https://github.com/Kirill/simplexml эта либа пропитана очень неплохим стилем и в целом полезна для общего развития. В свое время многому меня научила :)
Не понимаю зачем выдумывать свой формат? На работе пользуемся YAML для описания всех обьектов на сцене, выглядит как-то так: http://pastebin.com/raw.php?i=9syhQiSd