Предварительная обработка недопустимого xml перед подачей его в синтаксический анализатор

В рамках большого проекта, который я пишу по изучению C ++, я пытаюсь прочитать некоторые почти XML-файлы, созданные другой программой. К сожалению, эта программа использует собственную логику экранирования, поэтому некоторые из этих файлов на самом деле не являются допустимым XML. Насколько я могу судить, его логика выхода работает следующим образом:

  • Значения атрибутов всегда заключаются в двойные кавычки. Единственная escape-последовательность: ", оценивающийся в двойные кавычки. Обратные косые черты в других местах просто интерпретируются буквально; значение не может заканчиваться обратной косой чертой.
  • Вложенные элементы всегда имеют открывающий внешний тег на одной строке, каждый внутренний тег на отдельной строке и закрывающий внешний тег на другой.
  • Теги с текстовым содержимым всегда целиком находятся в одной строке и буквально интерпретируют все символы между открывающим тегом и закрывающим тегом. Копия закрывающего тега не может быть в текстовом содержимом, но другие теги могут быть.

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

Например, это возможный недопустимый ввод XML:

<outer attr="&amp;value"">
    <empty />
    <inner><notnested></outer></inner>
</outer>

Что эквивалентно этому допустимому XML:

<outer attr="&amp;amp;value&quot;">
    <empty />
    <inner>&lt;notnested&gt;&lt;/outer&gt;</inner>
</outer>

Мой подход к решению этой проблемы состоит в том, чтобы выполнить некоторый упрощенный синтаксический анализ этих файлов, чтобы преобразовать их в допустимый XML, прежде чем передавать их более традиционному синтаксическому анализатору XML (склоняясь к pugixml, но можно изменить, если это упростит задачу). Вот соответствующие функции в небольшой демонстрационной программе:

#include <cctype>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

static void put_xml_escaped(char c, std::stringstream& stream) {
    switch (c) {
        case '"': stream << "&quot;"; break;
        case ''': stream << "&apos;"; break;
        case '<': stream << "&lt;"; break;
        case '>': stream << "&gt;"; break;
        case '&': stream << "&amp;"; break;
        default: stream << c; break;
    }
}

static std::stringstream preprocess_file(std::string filename) {
    enum tag_parse_state {
                     // "    <tag attr="val">content</tag>    "
        before_tag,  //  ^^^^
        tag_name,    //      ^^^^
        tag_body,    //          ^^^^^^^    ^
        attr,        //                 ^^^^
        content,     //                      ^^^^^^^
        closing_tag, //                             ^^^^^^
        closed       //                                   ^^^^
    };

    std::ifstream file{filename};
    std::stringstream processed{};

    for (std::string line; std::getline(file, line); ) {
        tag_parse_state state = before_tag;

        size_t tag_name_start;
        size_t closing_tag_start, closing_tag_end;

        for (size_t i = 0; i < line.size(); i++) {
            auto c = line[i];

            if (state == before_tag) {
                if (c == '<') {
                    processed << c;
                    state = tag_name;
                    tag_name_start = i + 1;
                } else if (!isspace(c)) {
                    throw std::runtime_error(
                        "Failed to parse line (text before inital tag): " + line
                    );
                }

            } else if (state == tag_name) {
                processed << c;
                if (c == ' ' || c == '>') {
                    // Nice little coincidence: if we're parsing over a closing tag, this will
                    //  double up on the "https://codereview.stackexchange.com/" and thus not find itself
                    auto closing_tag_str = (
                        "</" + line.substr(tag_name_start, i - tag_name_start) + ">"
                    );
                    closing_tag_start = line.rfind(closing_tag_str);
                    closing_tag_end = (
                        closing_tag_start == std::string::npos
                        ? std::string::npos
                        : closing_tag_start + closing_tag_str.size() - 1
                    );

                    if (c == ' ') {
                        state = tag_body;
                    } else {
                        state = closing_tag_start == std::string::npos ? closed : content;
                    }
                }

            } else if (state == tag_body) {
                processed << c;
                if (c == '"') {
                    state = attr;
                } else if (c == '>') {
                    state = closing_tag_start == std::string::npos ? closed : content;
                }

            } else if (state == attr) {
                if (c == '"') {
                    processed << c;
                    state = tag_body;
                } else if (c == '\' && i + 1 < line.size() && line[i + 1] == '"') {
                    put_xml_escaped('"', processed);
                    i++;
                } else {
                    put_xml_escaped(c, processed);
                }

            } else if (state == content) {
                put_xml_escaped(c, processed);
                if (i + 1 >= closing_tag_start) {
                    state = closing_tag;
                }

            } else if (state == closing_tag) {
                processed << c;
                if (i >= closing_tag_end) {
                    state = closed;
                }

            } else if (state == closed) {
                if (!isspace(c)) {
                    throw std::runtime_error(
                        "Failed to parse line (text after closing tag): " + line
                    );
                }
            }
        }

        if (state != closed) {
            throw std::runtime_error("Failed to parse line (missed closing tag): " + line);
        }
    }

    return processed;
}

int main() {
    std::cout << preprocess_file("example.xml").str() << "n";
    return 0;
}

Мне интересны отзывы о:

  • Общие передовые практики (без стиля кода)
  • Крайние случаи, которые я, возможно, пропустил
  • Алгоритмические улучшения — я не думаю, что вы можете добиться большего, чем O (n), при использовании такого конечного автомата, но, возможно, есть совершенно другой подход.

1 ответ
1

Использовать enum class

Рассмотрите возможность использования enum class вместо простого enum для большей безопасности типов.

Я бы также рекомендовал вам использовать заглавные буквы в названиях enum options, поскольку это обычно делается и является сильным намеком на то, что это константы, а не переменные.

Предпочитаю использовать switch-Заявления, когда enumвовлечены

Использовать switch-выражение вместо цепочки ifelse-высказывания при проверке значения enum Переменная. Большинство компиляторов смогут выдавать предупреждения, если вы забудете case утверждение. Это также делает код более читабельным.

Делать preprocess_file() брать istream и ostreams как параметры

Ваш preprocess_file() имеет имя файла в качестве аргумента и возвращает std::stringstream. Однако это означает, что теперь он отвечает за открытие файла и возврат результата в виде std::stringstream неэффективно, если вы просто собираетесь писать его на std::out после. Представьте, что эта функция принимает два параметра: один для входного потока и один для выходного потока, например:

void preprocess(std::istream &file, std::ostream &processed) {
    ...
}

Затем в main() ты можешь написать:

preprocess(std::ifstream("example.xml"), std::cout);
std::cout << "n";

Если вы хотите уметь писать std::cout << preprocess(...), это тоже возможно, но тогда вы должны сделать это class и добавить friend operator<<(std::ostream &, preprocess &) функция, что может оказаться излишним для вашего приложения.

Отсутствует проверка ошибок

Ошибки ввода-вывода могут произойти в любое время как при чтении, так и при записи в файл. Вы можете добавлять проверки после каждой операции ввода-вывода, но это быстро приведет к появлению большого количества беспорядочного кода. К счастью, любое состояние ошибки сохраняется, поэтому вы можете просто добавить чеки в конце preprocess_file(), вот так:

if (file.fail()) {
    throw std::runtime_error("Error while reading input");
}

if (processed.fail()) {
    throw std::runtime_error("Error while writing output");
}

Представление

Вы просматриваете каждый символ индивидуально, большую часть времени просто копируя его на вывод. Для каждого состояния у вас есть только несколько символов, которым вы хотите сопоставить. Рассмотрите возможность использования std::string::find_first_of() получить позицию первого интересного персонажа; стандартная библиотека может сделать это более оптимальным образом.

  • 1

    Множество хороших советов, спасибо. Я понял, что не упомянул об этом в посте, но, как бы то ни было, я смотрел на кормление pugixml из потока, поэтому я оставил вывод строкового потока. Я определенно вижу, как было бы лучше, если бы родительская функция управляла владением.

    — яблоко 1417

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *