Introduction
파이썬에서 XML 파일을 자동으로 생성한 후, xmltodict 나 ElementTree 를 사용해 다시 XML 파일을 파싱하려고 할 때 다음과 같은 에러가 발생하는 경우가 있다.
Error parsing XML: not well-formed (invalid token)
사용하는 도구에 따라 XML 파일의 어느 위치가 잘못 되었는지 알려 주기도 하기 때문에 vim과 같은 에디터를 열어 해당 위치 내용을 직접 수정해도 되지만, 파일이 크고 수정해야 하는 지점이 많으면 이 방법을 사용하기엔 여의치 않다.
이번 글에서는 파이썬을 이용해 해당 문제를 편하게 해결하는 방법을 소개한다.
Problem
위 문제의 원인은 크게 두 가지로 나눌 수 있다. XML 파일 내부에 XML 문서에서 처음부터 허용하지 않는 유니코드 글자 가 포함되어 있는 경우와, XML 컨트롤 파라미터와 혼동 되어 문서 내의 텍스트 부분에서 사용을 지양해야 하는 글자가 텍스트에 포함된 경우이다.
Case 1
먼저 XML 문서에서 허용하고 있는 글자의 종류는 다음과 같이 정의되고 있다.
Legal characters are tab, carriage return, line feed, and the legal characters of Unicode and ISO/IEC 10646
이를 유니코드 범위로 표현하면 다음과 같다.
Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
/* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */
이 범위에 해당하지 않는 글자는 XML 문서에서 제거해 놓아야 한다.
Case 2
이와는 다르게, 문서 내부에서 사용은 허용되지만 이미 특정한 용도를 가지고 있어 텍스트 부분에다 적어야 할 때는 다른 대체 스트링으로 바꾸어 주어야 하는 글자들이 있다 (이를 escaping이라고 표현하는 듯 하다).
이런 글자들로는 < & > ' " 이 있는데, 이들을 XML predefined entities 라고 표현하고 있다.
> 와 < 는 XML 태그의 끝과 시작을 나타내기 위한 글자이고, &는 entity reference의 시작을 알리는 글자이기 때문에 텍스트 부분에서 사용하는 것은 금지된다 (<는 맥락에 따라 실제로는 문제가 발생하지 않을 수도 있다).
' (single quote) 와 " (double quote) 는 각각 single quote와 double quote로 둘러싸인 어트리뷰트를 표현할 때에만 사용하는 것이 금지되지만, 아예 모든 텍스트 부분에서 사용을 금지하는 것이 권장되고 있다.
Name | Character | Unicode code point (decimal) | Standard |
quot | " | U+0022 (34) | XML 1.0 |
amp | & | U+0026 (38) | XML 1.0 |
apos | ' | U+0027 (39) | XML 1.0 |
lt | < | U+003C (60) | XML 1.0 |
gt | > | U+003E (62) | XML 1.0 |
위 표는 XML 1.0 의 predefined entities를 보여주고 있다. Character 열에 표현된 글자들은 XML 문서의 컨트롤 파라미터가 아닌 텍스트 부분에서 지양해야 할 글자들이며, 각각은 &{Name}; 혹은 &#{decimal}; 로 대체해서 표현하는 것이 가능하다.
예를 들어 글자 &을 XML의 텍스트 부분에 사용하고 싶다면, & 혹은 & 로 해당 글자를 대체해 표현할 수 있다.
Solution
결국 위 조건을 모두 만족하는 텍스트만이 XML 문서의 텍스트 부분을 구성하는 데 사용되어야 한다. Case 1은 파이썬의 내장 정규 표현식 모듈 (re; regex) 을 사용해 해결할 수 있고, Case 2는 해당 글자들을 모두 찾아 escape string으로 대체해 주는 것으로 해결할 수 있다.
import re
_illegal_xml_chars_RE = re.compile(u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
XML_PREDEFINED_ENTITIES = {
"<": "<",
"&": "&",
">": ">",
"'": "'",
'"': """,
}
def escape_xml_illegal_chars(val, replacement='?'):
"""Filter out characters that are illegal in XML.
Looks for any character in val that is not allowed in XML
and replaces it with replacement ('?' by default).
>>> escape_illegal_chars("foo \x0c bar")
'foo ? bar'
>>> escape_illegal_chars("foo \x0c\x0c bar")
'foo ?? bar'
>>> escape_illegal_chars("foo \x1b bar")
'foo ? bar'
>>> escape_illegal_chars(u"foo \uFFFF bar")
u'foo ? bar'
>>> escape_illegal_chars(u"foo \uFFFE bar")
u'foo ? bar'
>>> escape_illegal_chars(u"foo bar")
u'foo bar'
>>> escape_illegal_chars(u"foo bar", "")
u'foo bar'
>>> escape_illegal_chars(u"foo \uFFFE bar", "BLAH")
u'foo BLAH bar'
>>> escape_illegal_chars(u"foo \uFFFE bar", " ")
u'foo bar'
>>> escape_illegal_chars(u"foo \uFFFE bar", "\x0c")
u'foo \x0c bar'
>>> escape_illegal_chars(u"foo \uFFFE bar", replacement=" ")
u'foo bar'
"""
return _illegal_xml_chars_RE.sub(replacement, val)
>>> text = u"foo \uFFFF bar <>&"
foo bar <>&
>>> text = escape_xml_illegal_chars(text)
foo ? bar <>&
>>> for char, escape_char in XML_PREDEFINED_ENTITIES.items():
... text = text.replace(char, escape_char)
foo ? bar &#60;>&
위와 같은 방식으로 XML 파싱 시 invalid 하다고 판정되는 텍스트를 valid한 텍스트로 변환하는 것이 가능하다. escape_xml_illegal_chars 함수는 여기서 참고했다.
아래는 참고할 만한 페이지들이다.
References
stripping illegal characters out of xml in python
https://leosimons.com/2011/03/17/stripping-illegal-characters-out-of-xml-in-python/
Validate an XML file
https://www.xmlvalidation.com/
XML recommendation 1.1, §2.2 Characters
https://www.w3.org/TR/xml/#charsets
List of predefined entities in XML
https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
What are invalid characters in XML
https://stackoverflow.com/questions/730133/what-are-invalid-characters-in-xml