Работа с xml

Чем дальше, тем больше AutoCAD и продукты на его основе "завязываются" на xml. Понятно почему: удобный вариант хранения сколь угодно сложной структуры, парсер гарантированно встроен в систему ну и теде.

Достаточно давно я разработал набор функций, которые позволяют нормально работать с xml-документом. Как выяснилось в процессе работы, в основном стоит задача чтения данных (благо заполняю я xml-файлы либо в Notepad++, либо в Microsoft XML Notepad, либо в MS Visual Studio). Но - функции есть, и предоставлю я их целиком.

Предупреждаю сразу: пост получился очень длинный, набор функций, как всегда, в самом конце :)

P.S. Функции не переименовывал. Кому охота - код открыт, используйте наздоровье :)
P.P.S. Аналог всего этого дела был в свое время опубликован у меня на блогспоте, так что не удивляйтесь возможным повторам :).

Сначала "нарисуем" какой-нибудь достаточно простой xml-файлик:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<Acad>
  <Sysvar>
<!-- Принудительно устанавливаемые системные переменные -->
    <Item name="angbase" value="0" />
    <Item name="blipmode" value="0" />
    <Item name="cecolor" value="bylayer" />
    <Item name="celtype" value="bylayer" />
    <Item name="celweight" value="-1" />
    <Item name="clayer" value="0" />
    <Item name="demandload" value="3" />
    <Item name="filedia" value="1"/>
  </Sysvar>
  <SupportPath>
    <!--
   Данные о добавляемых путях поддержки. Вынесено в отдельную ветку, т.к. надо не просто назначать, а проверять наличие путей
   -->
    <Item>\\server\settings\template</Item>
    <Item>\\server\settings\support</Item>
  </SupportPath>
</Acad>

и сохраним его, например, в c:\test.xml (ну так, чтобы было с чем поковыряться ;))

К сожалению, подсветка кода сработала немного некорректно, но это не особо важно. Важно другое:
1. Если такой файл создавать "руками", то надо отследить, чтобы сохранялся он именно в указанной кодировке, т.е. UTF-8. Лично я постоянно использую именно эту кодировку, пока (тьфу-тьфу) проблем не было.
2. Каждый тэг должен быть закрыт.

Теперь начнем работать внутри vlide. Прежде всего надо создать указатель на документ - показать лиспу, с чем мы собираемся работать. А что мы с этим будем делать - вопрос отдельный :)

В процессе работы достаточно широко используются некоторые библиотечные функции:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
(defun _kpblc-error-catch (protected-function
                           on-error-function
                           /
                           catch_error_result
                           )
                          ;|
*** Функция взята из книжной версии ruCAD'a без каких бы то ни было переделок,
*** кроме переименования.
*    Оболочка отлова ошибок.
*    Параметры вызова:
* protected-function  - "защищаемая" функция
* on-error-function - функция, выполняемая в случае ошибки
|;

  (setq catch_error_result (vl-catch-all-apply protected-function))
  (if (and (vl-catch-all-error-p catch_error_result)
           on-error-function
           ) ;_ end of and
    (apply on-error-function
           (list (vl-catch-all-error-message catch_error_result))
           ) ;_ end of apply
    catch_error_result
    ) ;_ end of if
  ) ;_ end of defun

(defun _kpblc-error-print (func-name msg / res)
                          ;|
*    Функция вывода сообщения об ошибке для (_kpblc-error-catch)
*    Параметры вызова:
* func-name имя функции, в которой возникла ошибка
* msg   сообщение об ошибке
|;

  (princ (setq res (strcat "\n ** "
                           (vl-string-trim "][ :\n<>"
                                           (vl-string-subst
                                             ""
                                             "error"
                                             (strcase (_kpblc-conv-value-to-string func-name) t)
                                             ) ;_ end of vl-string-subst
                                           ) ;_ end of vl-string-trim
                           " ERROR #"
                           (if msg
                             (strcat
                               (_kpblc-conv-value-to-string (getvar "errno"))
                               ": "
                               (_kpblc-conv-value-to-string msg)
                               ) ;_ end of strcat
                             ": undefined"
                             ) ;_ end of if
                           ) ;_ end of strcat
               ) ;_ end of setq
         ) ;_ end of princ
  (_kpblc-log res nil)
  (princ)
  ) ;_ end of defun

(defun _kpblc-conv-value-to-string (value /)
                                   ;|
*    конвертация значения в строку.
|;

  (cond
    ((= (type value) 'str) value)
    ((= (type value) 'int) (itoa value))
    ((and (= (type value) 'real) (equal value (_kpblc-eval-value-round value 1.) 1e-6))
     (itoa (fix value))
     )
    ((= (type value) 'real) (rtos value 2 14))
    ((not value) "")
    (t (vl-princ-to-string value))
    ) ;_ end of cond
  ) ;_ end of defun

(defun _kpblc-eval-value-round (value to)
  ;|
;; http://forum.dwg.ru/showthread.php?p=301275
*    Выполняет округление числа до указанной точности
*    Примеры вызова:
(_kpblc-eval-value-round 16.365 0.01) ; 16.37
|;

  (if (zerop to)
    value
    (* (atoi (rtos (/ (float value) to) 2 0)) to)
    ) ;_ end of if
  ) ;_ end of defun

(defun _kpblc-property-get (obj property / res)
                           ;|
*    Получение значения свойства объекта
|;

  (vl-catch-all-apply
    (function
      (lambda ()
        (if (and obj
                 (vlax-property-available-p
                   (setq obj (_kpblc-conv-ent-to-vla obj))
                   property
                   ) ;_ end of vlax-property-available-p
                 ) ;_ end of and
          (setq res (vlax-get-property obj property))
          ) ;_ end of if
        ) ;_ end of lambda
      ) ;_ end of function
    ) ;_ end of vl-catch-all-apply
  res
  ) ;_ end of defun

(defun _kpblc-conv-ent-to-ename (ent_value /)
                                ;|
*    Функция преобразования полученного значения в ename
*    Параметры вызова:
* ent_value значение, которое надо преобразовать в примитив. Может
*     быть именем примитива, vla-указателем или просто
*     списком.
*     Если не принадлежит ни одному из указанных типов,
*     возвращается nil
*    Примеры вызова:
(_kpblc-conv-ent-to-ename (entlast))
(_kpblc-conv-ent-to-ename (vlax-ename->vla-object (entlast)))
|;

  ;; "_kpblc-conv-ent-to-ename")
  (cond
    ((= (type ent_value) 'vla-object)
     (vlax-vla-object->ename ent_value)
     )
    ((= (type ent_value) 'ename) ent_value)
    ;((= (type ent_value) 'str) (handent ent_value))
    ((= (type ent_value) 'list) (cdr (assoc -1 ent_value)))
    (t nil)
    ) ;_ end of cond
  ) ;_ end of defun

(defun _kpblc-conv-ent-to-vla (ent_value / res)
                              ;|
*    Функция преобразования полученного значения в vla-указатель.
*    Параметры вызова:
* ent_value значение, которое надо преобразовать в указатель. Может
*     быть именем примитива, vla-указателем или просто
*     списком.
*     Если не принадлежит ни одному из указанных типов,
*     возвращается nil
*    Примеры вызова:
(_kpblc-conv-ent-to-vla (entlast))
(_kpblc-conv-ent-to-vla (vlax-ename->vla-object (entlast)))
|;

  (cond
    ((= (type ent_value) 'vla-object) ent_value)
    ((= (type ent_value) 'ename) (vlax-ename->vla-object ent_value))
    ((setq res (_kpblc-conv-ent-to-ename ent_value))
     (vlax-ename->vla-object res)
     )
    ) ;_ end of cond
  ) ;_ end of defun

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(defun _kpblc-xml-doc-get (file / doc)
                          ;|
*    Получение указателя на xml-DOMDocument
*    Параметры вызова:
  file  xml-файл. Валидность не проверяется
|;

  (if (findfile file)
    (_kpblc-error-catch
      (function
        (lambda ()
          (setq doc (vlax-get-or-create-object "MSXML2.DOMDocument.3.0"))
          (vlax-put-property doc 'async :vlax-false)
          (vlax-invoke-method doc 'load file)
          ) ;_ end of lambda
        ) ;_ end of function
      '(lambda (x)
         (_kpblc-error-print "_kpblc-xml-doc-get" x)
         (setq doc nil)
         ) ;_ end of lambda
      ) ;_ end of _kpblc-error-catch
    ) ;_ end of if
  doc
  ) ;_ end of defun

Почему используется именно "MSXML2.DOMDocument.3.0"? Это единственное средство, которое я обнаружил в Windows 2000 и выше и в AutoCAD 2008 и выше. На более древних версиях ОС либо AutoCAD, возможно, понадобится другой парсер - не пробовал, не знаю :(

Запустим функцию получения указателя на xml-документ:

1
2
_$ (setq xml_doc (_kpblc-xml-doc-get "c:\\test.xml"))
#<VLA-OBJECT IXMLDOMDocument2 0000000009a27d20>

Если выполнить дамп объекта и подключить MSDN, то выяснится масса интереснейших подробностей. Нас же интересует прежде всего получение "основного" узла (в валидных xml-файлах может быть только один "основной" узел; в противном случае парсер говорит, что файл некорректный и сваливается "в никуда")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(defun _kpblc-xml-node-get-main (obj / res)
                                ;|
*    Получение главного (верхнего) узла xml-дерева. Валидность xml-файла не
* проверяется
*    Параметры вызова:
  obj указатель на объект XML-документа
*    Примеры вызова:
(setq obj (_kpblc-xml-doc-get (findfile (strcat (_kpblc-dir-path-and-splash(_kpblc-dir-get-root-xml))"tables.xml"))))
(_kpblc-xml-node-get-main obj)
|;

  (_kpblc-error-catch
    (function
      (lambda ()
        (setq res (car (_kpblc-xml-conv-nodes-to-list
                         (_kpblc-property-get
                           obj
                           'childnodes
                           ) ;_ end of _kpblc-property-get
                         ) ;_ end of _kpblc-xml-conv-nodes-to-list
                       ) ;_ end of car
              ) ;_ end of setq
        ) ;_ end of lambda
      ) ;_ end of function
    '(lambda (x)
       (_kpblc-error-print "_kpblc-xml-node-get-main" x)
       (setq res nil)
       ) ;_ end of lambda
    ) ;_ end of _kpblc-error-catch
  res
  ) ;_ end of defun

В коде используется еще одна служебная функция: _kpblc-xml-conv-nodes-to-list. Она проста до безобразия:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
(defun _kpblc-xml-conv-nodes-to-list (nodes / i res)
                                     ;|
*    Преобразование указателя на коллекцию Nodes xml-объекта в список.
*    Исключаются описания не узлов (комментарии, DATA-узлы и т.п.)
*    Параметры вызова:
  nodes указатель на коллекцию узлов xml-документа
|;

  (_kpblc-error-catch
    (function
      (lambda ()
        (setq i 0)
        (while (< i (_kpblc-property-get nodes 'length))
          (setq res (cons (vlax-get-property nodes 'item i) res)
                i   (1+ i)
                ) ;_ end of setq
          ) ;_ end of while
        (setq res (vl-remove-if-not
                    (function
                      (lambda (x)
                        (member (_kpblc-property-get x 'nodetype) '(1 2))
                        ) ;_ end of lambda
                      ) ;_ end of function
                    (reverse res)
                    ) ;_ end of vl-remove-if-not
              ) ;_ end of setq
        ) ;_ end of lambda
      ) ;_ end of function
    '(lambda (x)
       (_kpblc-error-print "_kpblc-xml-conv-nodes-to-list" x)
       (setq res nil)
       ) ;_ end of lambda
    ) ;_ end of _kpblc-error-catch
  res
  ) ;_ end of defun

и фактически весьма сильно напоминает функцию преобразования vla-указателя на коллекцию в список ее элементов.

Получим и указатель на основной узел xml-документа:

1
2
_$ (setq main (_kpblc-xml-node-get-main xml_doc))
#<VLA-OBJECT IXMLDOMElement 0000000009a2a300>

Теперь, получив указатель на "основной", или "родительский" узел, можно получить указатели и на его подчиненные узлы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(defun _kpblc-xml-nodes-get-child (parent / node childs res)
                                  ;|
*    Получение подчиненных элементов xml-дерева
*    Параметры вызова
  parent  указатель на узел, для которого получаем Child
    nil недопустим
*    Примеры вызова:
(setq obj (_kpblc-xml-get-document (findfile (strcat (_kpblc-dir-path-and-splash(_kpblc-dir-get-root-xml))"tables.xml"))))
(_kpblc-xml-get-nodes-child (_kpblc-xml-node-get-main obj))
|;

  (if (and parent
           (vlax-method-applicable-p parent 'haschildnodes)
           (equal (vlax-invoke-method parent 'haschildnodes)
                  :vlax-true
                  ) ;_ end of equal
           (setq childs (_kpblc-property-get parent 'childnodes))
           ) ;_ end of and
    (_kpblc-xml-conv-nodes-to-list childs)
    ) ;_ end of if
  ) ;_ end of defun

Дальше начинаем работать с узлами: попробуем получить указатель на узел, имея в распоряжении только его tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(defun _kpblc-xml-nodes-get-child-by-tag (parent tag)
                                         ;|
*    Получение списка подчиненных узлов, у которых тэг совпадает с указанным
*    Параметры вызова:
  parent  указатель на "родительский" узел
  tag маска имени тэга. nil -> "*"
|;

  (setq tag (if tag
              (strcase tag)
              "*"
              ) ;_ end of if
        ) ;_ end of setq
  (vl-remove-if-not
    (function
      (lambda (x)
        (wcmatch
          (strcase (_kpblc-conv-value-to-string (_kpblc-property-get x 'tagname))
                   ) ;_ end of strcase
          tag
          ) ;_ end of wcmatch
        ) ;_ end of lambda
      ) ;_ end of function
    (_kpblc-xml-nodes-get-child parent)
    ) ;_ end of vl-remove-if-not
  ) ;_ end of defun
1
2
3
4
5
6
7
8
9
Теперь, если в коде нам вдруг понадобится получить указатель на все подузлы элемента Sysvar, мы можем сделать нечто наподобие:
[cc lang="cadlisp"](vl-load-com)

(defun test (/ xml_doc main)
  (setq xml_doc      (_kpblc-xml-doc-get "c:\\test.xml")
        main         (_kpblc-xml-node-get-main xml_doc)
        node_sysvars (_kpblc-xml-nodes-get-child-by-tag main "Sysvars")
        ) ;_ end of setq
  ) ;_ end of defun

Дальше, казалось бы, все просто: получаем указатели на подузлы и... И?..

Для узла, оказывается, надо еще уметь получать его атрибуты:

1
2
3
4
5
6
7
8
9
10
11
12
13
(defun _kpblc-xml-attributes-get-by-node (node)
          ;(defun _kpblc-xml-get-attrbitutes (node)
                                         ;|
*    Получение атрибутов узла XML-дерева.
*    Параметры вызова:
  node  проверяемый узел
|;

  (if (vlax-property-available-p node 'attributes)
    (_kpblc-xml-conv-nodes-to-list
      (_kpblc-property-get node 'attributes)
      ) ;_ end of _kpblc-xml-conv-nodes-to-list
    ) ;_ end of if
  ) ;_ end of defun

и их значения, не забывая про сами тэги атрибутов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
(defun _kpblc-xml-attribute-get-name-and-value (xml-attribute)
                                               ;|
*    Получение списка точечной пары имени и значения атрибута
*    Параметры вызова:
  xml-attribute указатель на xml-атрибут документа. Допустимые
    значения:
      vla-object  указатель на 1 атрибут / узел дерева
      list    список атрибутов
      nil   ничего не делается
*    Пример вызова:
|;

  (cond
    ((and xml-attribute
          (= (type xml-attribute) 'vla-object)
          (vlax-property-available-p xml-attribute 'nodename)
          (not (_kpblc-property-get xml-attribute 'attributes))
          ) ;_ end of and
     (cons (strcase (_kpblc-property-get xml-attribute 'nodename) t)
           ((lambda (/ _res)
              (setq _res (vlax-variant-value (_kpblc-property-get xml-attribute 'nodevalue)))
              (foreach item '(("@qute;" . """)
                              ("
&quot;" . """)
                              ("&amp;" . "&")
                              ("&#10;" . "\r")
                              ("&#13;" . "\n")
                              )
                (setq _res (_kpblc-string-replace _res (car item) (cdr item)))
                ) ;_ end of foreach
              _res
              ) ;_ end of lambda
            )
           ) ;_ end of cons
     )
    ((and xml-attribute
          (= (type xml-attribute) 'vla-object)
          (vlax-property-available-p xml-attribute 'nodename)
          (_kpblc-property-get xml-attribute 'attributes)
          ) ;_ end of and
     (mapcar (function _kpblc-xml-attribute-get-name-and-value)
             (_kpblc-xml-attributes-get-by-node xml-attribute)
             ) ;_ end of mapcar
     )
    ((and xml-attribute (listp xml-attribute))
     (mapcar (function _kpblc-xml-attribute-get-name-and-value) xml-attribute)
     )
    ) ;_ end of cond
  ) ;_ end of defun

Теперь, имея в своем распоряжении все это богатство, начинаем уже более серьезную работу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(vl-load-com)

(defun test (/ xml_doc main)
  (setq xml_doc      (_kpblc-xml-doc-get "c:\\test.xml")
        main         (_kpblc-xml-node-get-main xml_doc)
        node_sysvars (mapcar
                       (function _kpblc-xml-attribute-get-name-and-value)
                       (_kpblc-xml-nodes-get-child
                         (car
                           (_kpblc-xml-nodes-get-child-by-tag main "Sysvar")
                           ) ;_ end of car
                         ) ;_ end of _kpblc-xml-nodes-get-child
                       ) ;_ end of mapcar
        ) ;_ end of setq
  ) ;_ end of defun

Если мы посмотрим на значение node_sysvars, мы увидим очень интересную вещь:

1
2
3
4
5
6
7
8
9
'((("name" . "angbase") ("value" . "0"))
  (("name" . "blipmode") ("value" . "0"))
  (("name" . "cecolor") ("value" . "bylayer"))
  (("name" . "celtype") ("value" . "bylayer"))
  (("name" . "celweight") ("value" . "-1"))
  (("name" . "clayer") ("value" . "0"))
  (("name" . "demandload") ("value" . "3"))
  (("name" . "filedia") ("value" . "1"))
  )

Т.е. мало того, что у нас системные переменные получены прямо так, как мы хотели, так еще и устанавливаемые значения в строковом представлении! В принципе, ничего страшного - и не такие задачки решали! :)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(defun _kpblc-sysvar-set (var value)
                         ;|
*    Установка системных переменных. Замена стандартному (setvar) для безошибочной обработки

|;
  (if (getvar var)
    (if (and (= value "") (wcmatch (strcase var t) "dim*"))
      (setvar var ".")
      (vl-catch-all-apply
        (function
          (lambda (/ tmp)
            (setq tmp (getvar var)
                  tmp (cond
                        ((or (= (type value) (type tmp))
                             (and (member value (list 'int 'real))
                                  (member tmp (list 'int 'real))
                                  ) ;_ end of and
                             ) ;_ end of or
                         value
                         )
                        ((= (type tmp) 'int) (_kpblc-conv-value-to-int value))
                        ((= (type tmp) 'real) (_kpblc-conv-value-to-real value))
                        ((= (type tmp) 'str) (_kpblc-conv-value-to-string value))
                        ((= (type tmp) 'list)
                         (mapcar (function atof)
                                 (_kpblc-conv-string-to-list (vl-string-trim "()" (_kpblc-conv-value-to-string value)) ",")
                                 ) ;_ end of mapcar
                         )
                        ) ;_ end of cond
                  ) ;_ end of setq
            (setvar var tmp)
            ) ;_ end of lambda
          ) ;_ end of function
        ) ;_ end of vl-catch-all-apply
      ) ;_ end of if
    ) ;_ end of if
  (getvar var)
  ) ;_ end of defun

Аналогичным образом можно получить и перечень добавляемых путей поддержки (ну или что вы там нафантазируете :))

Прежде чем предоставлять полный архив, хочу предупредить вот о чем:

  1. После окончания работы с xml-документом надо очищать коннекты к нему. Очистка выполняется принудительно, через vlax-release-object
  2. Нередко достаточно организовать коннект только к xml-документу и его основному узлу, а все остальное вычислять по мере надобности
  3. Осторожно работаем с объемными xml-узлами, содержащими внутри несколько сотен, а то и тысям подчиненных узлов. Время получения нужного узла может измеряться уже секундами - в некоторых случаях подобное может оказаться критичным. Тот же assoc в аналогичных условиях может сработать почти моментально
  4. Иногда имеет смысл хранить объект коннекта к xml-документу во внедокументной переменной (см. функции vl-bb-set и vl-bb-ref)

В общем и целом, весь архив функций, необходимых и достаточных (естественно, с моей точки зрения) для полноценной и эффективной работы с xml-файлами, лежит в архиве
Если что-то забыл или не работает - пожалуйста, пишите либо в комментах, либо в почту.
---
Весь набор, объединенный в один лисп: lisru-xml-collection



Комментарии

Есть 4 коммент. к “Работа с xml”
  1. Кулик Алексей aka kpblc пишет:

    Если надо, могу выложить также "все лиспы в одном" и компилированные версии.

  2. Михаил пишет:

    В функции _kpblc-error-print используется функция _kpblc-log, описания которой я что-то никак не найду. Можно опубликовать?
    Заранее благодарен.

  3. Кулик Алексей aka kpblc пишет:

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

  4. Михаил пишет:

    Спасибо. Понял. Исключу.

Поделитесь своим мнением


Я не робот.