В рамках большого проекта, который я пишу по изучению C ++, я пытаюсь прочитать некоторые почти XML-файлы, созданные другой программой. К сожалению, эта программа использует собственную логику экранирования, поэтому некоторые из этих файлов на самом деле не являются допустимым XML. Насколько я могу судить, его логика выхода работает следующим образом:
- Значения атрибутов всегда заключаются в двойные кавычки. Единственная escape-последовательность:
"
, оценивающийся в двойные кавычки. Обратные косые черты в других местах просто интерпретируются буквально; значение не может заканчиваться обратной косой чертой. - Вложенные элементы всегда имеют открывающий внешний тег на одной строке, каждый внутренний тег на отдельной строке и закрывающий внешний тег на другой.
- Теги с текстовым содержимым всегда целиком находятся в одной строке и буквально интерпретируют все символы между открывающим тегом и закрывающим тегом. Копия закрывающего тега не может быть в текстовом содержимом, но другие теги могут быть.
На практике файлы имеют немного большую структуру, чем эта, но я пытался работать только на этих предположениях, чтобы код оставался гибким.
Например, это возможный недопустимый ввод XML:
<outer attr="&value"">
<empty />
<inner><notnested></outer></inner>
</outer>
Что эквивалентно этому допустимому XML:
<outer attr="&amp;value"">
<empty />
<inner><notnested></outer></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 << """; break;
case ''': stream << "'"; break;
case '<': stream << "<"; break;
case '>': stream << ">"; break;
case '&': stream << "&"; 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 ответ
Использовать enum class
Рассмотрите возможность использования enum class
вместо простого enum
для большей безопасности типов.
Я бы также рекомендовал вам использовать заглавные буквы в названиях enum
options, поскольку это обычно делается и является сильным намеком на то, что это константы, а не переменные.
Предпочитаю использовать switch
-Заявления, когда enum
вовлечены
Использовать switch
-выражение вместо цепочки if
—else
-высказывания при проверке значения enum
Переменная. Большинство компиляторов смогут выдавать предупреждения, если вы забудете case
утверждение. Это также делает код более читабельным.
Делать preprocess_file()
брать istream
и ostream
s как параметры
Ваш 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()
получить позицию первого интересного персонажа; стандартная библиотека может сделать это более оптимальным образом.
Множество хороших советов, спасибо. Я понял, что не упомянул об этом в посте, но, как бы то ни было, я смотрел на кормление pugixml из потока, поэтому я оставил вывод строкового потока. Я определенно вижу, как было бы лучше, если бы родительская функция управляла владением.
— яблоко 1417