Рассмотрим очередной пример использования библиотеки tool1d. Сейчас мы напишем программку, которая облегчила бы мне жизнь пару лет назад.
Сценарий из жизни: розничная сеть, РИБ, кассовые узлы раз в месяц где-нибудь да сломаются, данные по последним продажам не попадают в центральный узел. Как мы решали проблему тогда? Поддержка копирует файл базы из магазина в офис, я с помощью старой доброй Tool1CD выгружаю таблицы с данными о чеках, глазами смотрю, чего не достаёт, руками редактирую выгрузку и запускаю костыль в виде обработки загрузки. Сейчас, имея на руках новый, подключаемый вариант библиотеки, я бы поступил по-другому. Новый сценарий: поддержка копирует файл базы себе на компьютер, запускает в 1С в центральном узле обработку, в обработке выбирает файл базы, нажимает большую волшебную кнопку "Вжух!" и нужные данные появляются в базе. Без моего участия, совсем. Вот таким образом мне нравится делать свою работу! Давайте попробуем сделать такую обработку.
Открываем предыдущую статью, выполняем "Часть 0" и только после этого идём дальше.
Что же должна делать обработка под капотом? Алгоритм следующий:
- Открываем базу
- Определяем принадлежность узла
- Получаем список чеков в базе узла
- Сравниваем с чеками в текущей базе - выделяем список недостающих
- По списку запрашиваем данные из базы узла
В качестве упрощения нам дано то, что все узлы созданы путём копирования базы шаблона, потому мы заранее знаем соответствие метаданных и таблиц. Сведения о моей базе:
- _NODE17 - план обмена По Кассе, поле _FLD254RREF - Касса. Отсюда узнаём, откуда база.
- _DOCUMENT122 - Чек ККМ. Отсюда тащим недостающие данные.
Сведения о вашей базе можете получить через функцию глобального контекста 1С ПолучитьСтруктуруХраненияБазыДанных или через множество обработок на её основе, которые можно найти на Инфостарте.
Работа на стороне 1С останется за кадром, здесь мы будем рассматривать только то , что касается непосредственно функционала tool1cd.
Исходя из алгоритма API нашей компоненты должен выглядеть как-то так:
- ОткрытьБазу / OpenDatabase. Принимает параметр - путь к базе. Возвращает 0, если база открыта, или код ошибки.
- ОпределитьПринадлежностьУзла / GetNodeIdentity. Принимает параметры - имя таблицы плана обмена и имя поля с кассой. Возвращает строку - GUID кассы ККМ.
- ПолучитьСписокДокументов / GetDocumentList. Принимает параметр - имя таблицы. Возвращает строку - список GUID через запятую.
- ПолучитьДанныеПоДокументу / GetDocumentData. Принимает параметры - имя таблицы и GUID документа. Возвращает строку - XML представление документа.
- ЗакрытьБазу / CloseDatabase. Закрывает открытую базу.
Псевдокод на 1С:
Процедура ОбработатьУзел(Знач ПутьКБазе)
ПутьККомпоненте = "C:\...\VNCOMPS\VNCOMP83\example\bin\RelWithDebInfo\AddInNativeWin32.dll";
Если Не ПодключитьВнешнююКомпоненту(ПутьККомпоненте, "CDReader", ТипВнешнейКомпоненты.Native) Тогда
Сообщить("Ошибочка");
Возврат;
КонецЕсли;
Читалка = Новый("AddIn.CDReader.AddInNativeExtension");
Читалка.ОткрытьБазу(ПутьКБазе);
// не зашиваем имена таблиц в компоненту во имя гибкости
КассаИд = Читалка.ОпределитьПринадлежностьУзла("_NODE17", "_FLD254RREF");
Касса = Справочники.Кассы.ПолучитьСсылку(Новый УникальныйИдентификатор(КассаИд));
СписокДокументов = Читалка.ПолучитьСписокДокументов("_DOCUMENT122");
СписокДокументов = СтрРазделить(СписокДокументов, ",");
// преобразуем гуиды в ссылки
Для Инд = 0 По СписокДокументов.Количество() - 1 Цикл
УИД = Новый УникальныйИдентификатор(СписокДокументов[Инд]);
СписокДокументов[Инд] = Документы.ЧекККМ.ПолучитьСсылку(УИД);
КонецЦикла;
СписокНедостающих = ОпределитьСписокНедостающих(Касса, СписокДокументов);
Для Каждого мСсылка Из СписокНедостающих Цикл
ГУИД = Строка(мСсылка.УникальныйИдентификатор());
Сообщить(ГУИД);
Данные = Читалка.ПолучитьДанныеПоДокументу("_DOCUMENT122", ГУИД);
ДобавитьДокументВБазу(Данные);
КонецЦикла;
Читалка.ЗакрытьБазу();
КонецПроцедуры
Начнём с простого
Создадим методы ОткрытьБазу и ЗакрытьБазу. Как в первой статье каждую процедуру и функцию я буду добавлять путём копирования сигнатуры CallAsProc или CallAsFunc и перенаправления вызова, обёрнутого в try/catch, также опуская все остальные необходимые формальности.
Потому как мы работаем с базой не за один заход и нам нужно хранить состояние (открытую базу), то нам необходимо добавить поле T_1CD *db в объявление класса:
// ...
#include <Class_1CD.h> // <<-- вот оно
// ...
class CAddInNative : public IComponentBase
{
public:
enum Props{...};
enum Methods{...};
CAddInNative(void);
virtual ~CAddInNative();
// ...
private:
// ...
T_1CD *db; // <<-- и вот
};
За исключением обработки tVariant код методов крайне прост:
CAddInNative::OpenDatabase
bool ADDIN_API CAddInNative::OpenDatabase(const long lMethodNum, tVariant* pvarRetValue, tVariant* paParams, const long lSizeArray)
{
boost::filesystem::path filepath;
// переделанный кусок кода из eMethLoadPicture
{
if (!lSizeArray || !paParams)
return false;
switch (TV_VT(paParams))
{
case VTYPE_PSTR:
filepath = boost::filesystem::path(std::string(paParams->pstrVal));
break;
case VTYPE_PWSTR:
{
wchar_t *wsTmp = nullptr;
::convFromShortWchar(&wsTmp, TV_WSTR(paParams));
filepath = boost::filesystem::path(std::wstring(wsTmp));
delete[] wsTmp;
break;
}
default:
return false;
}
}
db = new T_1CD(filepath);
TV_VT(pvarRetValue) = VTYPE_I4;
if (db->is_open() && db->is_infobase()) {
TV_I4(pvarRetValue) = 0;
}
else {
TV_I4(pvarRetValue) = 2; // код ошибки
}
return true;
}
CAddInNative::CloseDatabase
bool ADDIN_API CAddInNative::CloseDatabase(const long lMethodNum, tVariant * paParams, const long lSizeArray)
{
delete db;
db = nullptr;
return true;
}
Пробежимся по записям
Итак, базу мы открыли, теперь надо определить её принадлежность. Мы знаем имя таблицы, надо её найти, обойти, найти нужную запись и получить значение поля. Для начала введём вспомогательную функцию, которая будет обрабатывать параметр-строку:
std::string extract_string(const tVariant ¶m)
{
switch (TV_VT(¶m))
{
case VTYPE_PSTR:
return std::string(param.pstrVal, param.strLen);
case VTYPE_PWSTR:
{
uint8_t * data = reinterpret_cast<uint8_t*>(param.pwstrVal);
int size_in_bytes = param.wstrLen * sizeof(WCHAR_T);
std::vector<uint8_t> decode_vector;
decode_vector.assign(data, data + size_in_bytes);
return System::SysUtils::TEncoding::Unicode->toUtf8(decode_vector);
}
default:
throw std::exception("Передали не строку");
}
}
Находим таблицу, находим в ней запись c заполненным _PREDEFINEDID - это ЭтотУзел в плане обмена.
CAddInNative::GetNodeIdentity
bool ADDIN_API CAddInNative::GetNodeIdentity(const long lMethodNum, tVariant * pvarRetValue, tVariant * paParams, const long lSizeArray)
{
std::string table_name = extract_string(paParams[0]);
std::string field_name = extract_string(paParams[1]);
for (int i = 0; i < db->get_numtables(); i++) {
// ищем указанную таблицу
Table *t = db->get_table(i);
if (!System::EqualIC(table_name, t->get_name())) {
continue;
}
// нашли - обрабатываем
// для плана обмена нам нужен узел, у которого заполнен PREDEFINED_ID
TableIterator it(t);
while (!it.eof()) {
BinaryGuid predefined_id = it.current().get<BinaryGuid>("_PREDEFINEDID");
if (!predefined_id.is_empty()) {
// нашли нужный узел!
// берём гуид кассы и возвращаем
BinaryGuid kassa_guid = it.current().get<BinaryGuid>(field_name);
std::string utf8result = kassa_guid.as_1C();
// во имя кроссплатформенности, нужно перевести его в utf-16
std::vector<uint8_t> result = System::SysUtils::TEncoding::Unicode->fromUtf8(utf8result);
m_iMemory->AllocMemory((void**)&pvarRetValue->pwstrVal, result.size());
memcpy((void*)pvarRetValue->pwstrVal, result.data(), result.size());
TV_VT(pvarRetValue) = VTYPE_PWSTR;
pvarRetValue->wstrLen = result.size() / sizeof(WCHAR_T);
return true;
}
it.next();
}
}
// Сюда приходим, если не нашли таблицу или узел
TV_VT(pvarRetValue) = VTYPE_I4;
TV_I4(pvarRetValue) = 3;
return true;
}
В приведённом выше коде глаз должен зацепиться за два момента:
- Мы ищем таблицу по имени циклом - кто первый пришлёт патч, а?
- Конструкция get<BinaryGuid>. BinaryGuid - класс, специально для работы с GUID-ами в файловой базе. Основных задач у него всего две - взять данные из базы и преобразовать их в строку и наоборот - получить строку и преобразовать её в двоичный вид. Напомню очень хорошую статью про GUID - она обязательна к ознакомлению перед просмотром исходников BinaryGuid.
Получим список документов. Ничего нового - ищем таблицу, перебираем записи итератором, формируем строку из GUID-ов.
CAddInNative::GetDocumentList
bool ADDIN_API CAddInNative::GetDocumentList(const long lMethodNum, tVariant * pvarRetValue, tVariant * paParams, const long lSizeArray)
{
std::string table_name = extract_string(paParams[0]);
std::string utf8result;
for (int i = 0; i < db->get_numtables(); i++) {
Table *t = db->get_table(i);
if (!System::EqualIC(table_name, t->get_name())) {
continue;
}
TableIterator it(t);
while (!it.eof()) {
if (!utf8result.empty()) {
utf8result.append(",");
}
BinaryGuid guid = it.current().get<BinaryGuid>("_IDRREF");
utf8result.append(guid.as_1C());
it.next();
}
// во имя кроссплатформенности, нужно перевести результат в utf-16
std::vector<uint8_t> result = System::SysUtils::TEncoding::Unicode->fromUtf8(utf8result);
m_iMemory->AllocMemory((void**)&pvarRetValue->pwstrVal, result.size());
memcpy((void*)pvarRetValue->pwstrVal, result.data(), result.size());
TV_VT(pvarRetValue) = VTYPE_PWSTR;
pvarRetValue->wstrLen = result.size() / sizeof(WCHAR_T);
return true;
}
// Сюда приходим, если не нашли таблицу
TV_VT(pvarRetValue) = VTYPE_I4;
TV_I4(pvarRetValue) = 3;
return true;
}
Получение данных по документу.
Для начала надо составить список таблиц, в которых хранятся данные документа. Это основная таблица документа (_DOCUMENT122) и табличные части (_DOCUMENT122_VT*). Также стоит отметить, что в табличных частях поле Ссылка имеет имя не _IDREF, а _DOCUMENT122_IDREF.
CAddInNative::GetDocumentData
bool ADDIN_API CAddInNative::GetDocumentData(const long lMethodNum, tVariant * pvarRetValue, tVariant * paParams, const long lSizeArray)
{
std::string table_name = extract_string(paParams[0]);
BinaryGuid doc_id = BinaryGuid(extract_string(paParams[1]));
std::stringstream utf8result;
std::string derived_mask = table_name + "_VT";
std::string derived_key_field = table_name + "_IDRREF";
// ищем основную таблицу документа и табличные части
Table *main_table = nullptr;
std::vector<Table *> derived_tables;
for (int i = 0; i < db->get_numtables(); i++) {
Table *t = db->get_table(i);
std::string current_table_name = t->get_name();
if (System::EqualIC(table_name, current_table_name)) {
main_table = t;
}
if (current_table_name.size() < derived_mask.size()) {
continue;
}
if (System::EqualIC(current_table_name.substr(0, derived_mask.size()), derived_mask)) {
// это таблиная часть
derived_tables.push_back(t);
}
}
if (main_table == nullptr) {
TV_VT(pvarRetValue) = VTYPE_I4;
TV_I4(pvarRetValue) = 3;
return true;
}
TableIterator it(main_table);
while (!it.eof()) {
if (it.current().get<BinaryGuid>("_IDRREF") != doc_id) {
it.next();
continue;
}
// нашли нужную запись - выводим
utf8result << "<" << table_name << ">";
store_record_to_stream(utf8result, main_table, it.current());
// теперь ищем записи в подчинённых таблицах
for (Table *derived : derived_tables) {
utf8result << "<" << derived->get_name() << ">";
TableIterator dit(derived);
while (!dit.eof()) {
if (dit.current().get<BinaryGuid>(derived_key_field) == doc_id) {
utf8result << "<record>";
store_record_to_stream(utf8result, derived, dit.current());
utf8result << "</record>";
}
dit.next();
}
utf8result << "</" << derived->get_name() << ">";
}
utf8result << "</" << table_name << ">";
break;
}
// во имя кроссплатформенности, нужно перевести результат в utf-16
std::vector<uint8_t> result = System::SysUtils::TEncoding::Unicode->fromUtf8(utf8result.str());
m_iMemory->AllocMemory((void**)&pvarRetValue->pwstrVal, result.size());
memcpy((void*)pvarRetValue->pwstrVal, result.data(), result.size());
TV_VT(pvarRetValue) = VTYPE_PWSTR;
pvarRetValue->wstrLen = result.size() / sizeof(WCHAR_T);
return true;
}
Вспомогательная процедура по выводу полей:
void store_record_to_stream(std::stringstream &str, Table *t, const TableRecord &record)
{
for (int field_num = 0; field_num < t->get_num_fields(); field_num++) {
Field *f = t->get_field(field_num);
if (record.is_null_value(f)) {
str << "<" << f->get_name() << "/>";
}
else {
str << "<" << f->get_name() << ">";
str << record.get_xml_string(f);
str << "</" << f->get_name() << ">";
}
}
}
В коде процедуры обратим внимание на два момента:
- Проверка is_null_value. Нельзя так просто взять и получить значение поля, которое null - будет выброшено исключение.
- get_xml_string - любезно заранее подготовленная функция, которая возвращает строковое представление значения, которое можно просто взять и запихнуть в XML.
На выходе получаем текст XML, подобный следующему:
Пример получаемого XML-файла
<_DOCUMENT122>
<_IDRREF>dc0a6b43-5503-11e8-9479-cdce35f4f3fd</_IDRREF>
<_VERSION>1:0:10:0</_VERSION>
<_MARKED>false</_MARKED>
<_DATE_TIME>2018-05-11T13:30:55</_DATE_TIME>
<_NUMBERPREFIX>2018-01-01T00:00:00</_NUMBERPREFIX>
<_NUMBER>ALMM0000001</_NUMBER>
<_POSTED>true</_POSTED>
<_FLD1857RREF>4be07a6a-8b6c-4ee4-a4fe-edd07da60d48</_FLD1857RREF>
<_FLD1858_TYPE>01 </_FLD1858_TYPE>
<_FLD1858_RTREF>00000000 </_FLD1858_RTREF>
<_FLD1858_RRREF>00000000-0000-0000-0000-000000000000</_FLD1858_RRREF>
<_FLD1859RREF>00000000-0000-0000-0000-000000000000</_FLD1859RREF>
<_FLD1860RREF>3c0ad32a-4511-11e2-a517-0026554acea0</_FLD1860RREF>
<_FLD1861></_FLD1861>
<_FLD1862>1</_FLD1862>
<_FLD1863>1</_FLD1863>
<_FLD1864RREF>3ba3683e-f136-11e3-9402-d89d671cc0a3</_FLD1864RREF>
<_FLD1865>12460</_FLD1865>
<_FLD1866RREF>355d3a67-f60e-11e3-9407-ac162d728dbf</_FLD1866RREF>
<_FLD1867RREF>00000000-0000-0000-0000-000000000000</_FLD1867RREF>
<_FLD1868RREF>00000000-0000-0000-0000-000000000000</_FLD1868RREF>
<_FLD1869RREF>00000000-0000-0000-0000-000000000000</_FLD1869RREF>
<_FLD1870RREF>d45671c8-4420-48aa-89f5-7af1607d02e3</_FLD1870RREF>
<_FLD1871RREF>00000000-0000-0000-0000-000000000000</_FLD1871RREF>
<_FLD1872RREF>00000000-0000-0000-0000-000000000000</_FLD1872RREF>
<_FLD1873RREF>00000000-0000-0000-0000-000000000000</_FLD1873RREF>
<_FLD1874></_FLD1874>
<_FLD1875RREF>00000000-0000-0000-0000-000000000000</_FLD1875RREF>
<_FLD1876>false</_FLD1876>
<_FLD1877RREF>00000000-0000-0000-0000-000000000000</_FLD1877RREF>
<_FLD1878>0</_FLD1878>
<_FLD1879>0-00-00T00:00:00</_FLD1879>
<_FLD1880>2018-05-11T13:30:55</_FLD1880>
<_FLD1881>0</_FLD1881>
<_FLD1882></_FLD1882>
<_FLD1883>false</_FLD1883>
<_FLD1884>false</_FLD1884>
<_FLD3387RREF>00000000-0000-0000-0000-000000000000</_FLD3387RREF>
<_FLD3388></_FLD3388>
<_FLD3389></_FLD3389>
<_FLD3390></_FLD3390>
<_FLD3391></_FLD3391>
<_FLD3392>0</_FLD3392>
<_FLD3393RREF>00000000-0000-0000-0000-000000000000</_FLD3393RREF>
<_FLD3394>0</_FLD3394>
<_FLD3395></_FLD3395>
<_FLD3396RREF>00000000-0000-0000-0000-000000000000</_FLD3396RREF>
<_FLD3397>0</_FLD3397>
<_FLD3517RREF>00000000-0000-0000-0000-000000000000</_FLD3517RREF>
<_FLD4224>0</_FLD4224>
<_FLD4225RREF>00000000-0000-0000-0000-000000000000</_FLD4225RREF>
<_FLD4226></_FLD4226>
<_FLD4293>456</_FLD4293>
<_FLD4294></_FLD4294>
<_FLD4295>123</_FLD4295>
<_DOCUMENT122_VT1885>
<record>
<_DOCUMENT122_IDRREF>dc0a6b43-5503-11e8-9479-cdce35f4f3fd</_DOCUMENT122_IDRREF>
<_KEYFIELD>00000002 </_KEYFIELD>
<_LINENO1886>2</_LINENO1886>
<_FLD1887RREF>3646e7f8-2c0d-11df-af1f-00215a763825</_FLD1887RREF>
<_FLD1888RREF>00000000-0000-0000-0000-000000000000</_FLD1888RREF>
<_FLD4188RREF>00000000-0000-0000-0000-000000000000</_FLD4188RREF>
<_FLD1889>1</_FLD1889>
<_FLD1890RREF>3646e7f9-2c0d-11df-af1f-00215a763825</_FLD1890RREF>
<_FLD1891>1</_FLD1891>
<_FLD1892>6480</_FLD1892>
<_FLD1893>6480</_FLD1893>
<_FLD1894RREF>8ab98fd8-943a-451a-98e5-973d4b7617e8</_FLD1894RREF>
<_FLD1895>988.47</_FLD1895>
<_FLD1896>0</_FLD1896>
<_FLD1897>0</_FLD1897>
<_FLD1898RREF>00000000-0000-0000-0000-000000000000</_FLD1898RREF>
<_FLD1899_TYPE>01 </_FLD1899_TYPE>
<_FLD1899_N>0</_FLD1899_N>
<_FLD1899_RTREF>00000000 </_FLD1899_RTREF>
<_FLD1899_RRREF>00000000-0000-0000-0000-000000000000</_FLD1899_RRREF>
<_FLD1900>false</_FLD1900>
<_FLD1901>2000000012124</_FLD1901>
<_FLD1902RREF>185600fa-b094-11e1-a511-0026554acea0</_FLD1902RREF>
<_FLD1903>2</_FLD1903>
<_FLD1904RREF>00000000-0000-0000-0000-000000000000</_FLD1904RREF>
<_FLD1905>false</_FLD1905>
<_FLD1906>false</_FLD1906>
<_FLD1907>0-00-00T00:00:00</_FLD1907>
<_FLD2758>false</_FLD2758>
<_FLD2759>0</_FLD2759>
<_FLD2760RREF>00000000-0000-0000-0000-000000000000</_FLD2760RREF>
<_FLD3398></_FLD3398>
<_FLD3399RREF>00000000-0000-0000-0000-000000000000</_FLD3399RREF>
<_FLD3400></_FLD3400>
<_FLD4424RREF>00000000-0000-0000-0000-000000000000</_FLD4424RREF>
</record>
<record>
<_DOCUMENT122_IDRREF>dc0a6b43-5503-11e8-9479-cdce35f4f3fd</_DOCUMENT122_IDRREF>
<_KEYFIELD>00000001 </_KEYFIELD>
<_LINENO1886>1</_LINENO1886>
<_FLD1887RREF>1ff22c5b-84ee-11e3-933d-0026554acea0</_FLD1887RREF>
<_FLD1888RREF>00000000-0000-0000-0000-000000000000</_FLD1888RREF>
<_FLD4188RREF>00000000-0000-0000-0000-000000000000</_FLD4188RREF>
<_FLD1889>1</_FLD1889>
<_FLD1890RREF>1ff22c5c-84ee-11e3-933d-0026554acea0</_FLD1890RREF>
<_FLD1891>1</_FLD1891>
<_FLD1892>5980</_FLD1892>
<_FLD1893>5980</_FLD1893>
<_FLD1894RREF>8ab98fd8-943a-451a-98e5-973d4b7617e8</_FLD1894RREF>
<_FLD1895>912.2</_FLD1895>
<_FLD1896>0</_FLD1896>
<_FLD1897>0</_FLD1897>
<_FLD1898RREF>00000000-0000-0000-0000-000000000000</_FLD1898RREF>
<_FLD1899_TYPE>01 </_FLD1899_TYPE>
<_FLD1899_N>0</_FLD1899_N>
<_FLD1899_RTREF>00000000 </_FLD1899_RTREF>
<_FLD1899_RRREF>00000000-0000-0000-0000-000000000000</_FLD1899_RRREF>
<_FLD1900>false</_FLD1900>
<_FLD1901>2000001197837</_FLD1901>
<_FLD1902RREF>185600fa-b094-11e1-a511-0026554acea0</_FLD1902RREF>
<_FLD1903>1</_FLD1903>
<_FLD1904RREF>00000000-0000-0000-0000-000000000000</_FLD1904RREF>
<_FLD1905>false</_FLD1905>
<_FLD1906>false</_FLD1906>
<_FLD1907>0-00-00T00:00:00</_FLD1907>
<_FLD2758>false</_FLD2758>
<_FLD2759>0</_FLD2759>
<_FLD2760RREF>00000000-0000-0000-0000-000000000000</_FLD2760RREF>
<_FLD3398></_FLD3398>
<_FLD3399RREF>00000000-0000-0000-0000-000000000000</_FLD3399RREF>
<_FLD3400></_FLD3400>
<_FLD4424RREF>00000000-0000-0000-0000-000000000000</_FLD4424RREF>
</record>
</_DOCUMENT122_VT1885>
<_DOCUMENT122_VT1908>
<record>
<_DOCUMENT122_IDRREF>dc0a6b43-5503-11e8-9479-cdce35f4f3fd</_DOCUMENT122_IDRREF>
<_KEYFIELD>00000001 </_KEYFIELD>
<_LINENO1909>1</_LINENO1909>
<_FLD1910RREF>07b72861-e2a5-49f0-bcca-d7e2e0b0c3d5</_FLD1910RREF>
<_FLD1911>12460</_FLD1911>
<_FLD1912>0</_FLD1912>
<_FLD1913>0</_FLD1913>
</record>
</_DOCUMENT122_VT1908>
<_DOCUMENT122_VT1914></_DOCUMENT122_VT1914>
<_DOCUMENT122_VT1923></_DOCUMENT122_VT1923>
<_DOCUMENT122_VT1926></_DOCUMENT122_VT1926>
<_DOCUMENT122_VT1938></_DOCUMENT122_VT1938>
<_DOCUMENT122_VT1943></_DOCUMENT122_VT1943>
<_DOCUMENT122_VT1947></_DOCUMENT122_VT1947>
<_DOCUMENT122_VT3401></_DOCUMENT122_VT3401>
<_DOCUMENT122_VT3407></_DOCUMENT122_VT3407>
<_DOCUMENT122_VT4081></_DOCUMENT122_VT4081>
</_DOCUMENT122>
Уверен, загрузить такого вида XML не составит большого труда, потому расписывать здесь парсер нет никакого смысла.
Что дальше?
Если у вас есть какие-то интересные случаи, которые можно было бы разобрать - пишите в комментариях, обсудим.
Следующей статьёй я намереваюсь обозреть возможности графической утилитки - бегло рассказать про интерфейс, который отличается от оригинального, и затронуть пару низкоуровневых моментов, которые могут быть полезны в домашнем препарировании файлов баз данных.