[Python] IO - XML 다루기


Development note/Python  2019. 12. 25. 09:00

안녕하세요. 명월입니다.


이 글은 Python에서 XML를 다루는 방법에 대한 글입니다.


제가 예전에 Python에서 파일을 다루는 방법에 대해 설명한 적이 있습니다.

링크 - [Python] IO (파일 입출력)

csv에 대해서 설명한 적도 있는데 이 글과 관련성은 적으니 링크는 생략하겠습니다. 하단의 카테고리 관련 글로 찾아볼 수 있습니다.


최근에는 xml를 잘 사용하지 않는 데 불과 10년 전만 해도 프로그램 파라미터나 환경설정, 데이터를 기술하기 위한 마크업으로 자주 사용했습니다.

xml이 구조가 항상 태그가 열고 닫는 구조이고 각 태그에 속성(어트리뷰트)를 넣을 수 있는 구조이기 때문에 데이터를 표현하기엔 정말 좋은 구조입니다.

예를 들면 일반 컴파일 언어의 클래스를 데이터로 나타낸다고 하면, 최상단의 태그를 클래스 이름으로 작성하고 파생 태그에 각각의 변수 값을 넣을 수 있습니다. 파생 태그의 속성(어트리뷰트)를 이용해서 변수의 자료형 타입을 지정할 수 있는 등 이런 데이터를 문서로써 표현할 수 있는 방법이 있습니다.


xml의 단점은 아까 이야기했듯이 태그를 열고 닫고를 표현해야 하기 때문에 xml의 사이즈가 커질 수도 있고 속성 타입과 데이터가 많아질수록 가독성이 점점 떨어집니다.

그래서 최근에는 xml과 구조적으로 비슷한 json타입을 많이 사용하고 있습니다.


그렇다고 xml를 전혀 사용하지 않는 건 아니고 아직도 여러 곳에서 많이 사용하고 있습니다.

특히 환경 설정 등에서도 많이 사용하는데 대표적으로 C#에서 App.config와 tomcat의 server.xml등은 아직 xml구조로 많이 사용됩니다.


개인적으로 이런 xml를 로컬에서 관리하기 위해서 python으로 제작해서 사용하곤 합니다. 예를 들면 jenkins같은 CI에서 디플로이할 때, jenkins의 shell command를 작성하는 곳에 python코드를 사용하여 환경 설정 파일 등을 제어하고 수정 관리가 가능하면 꽤 매우 편리해집니다.

#xml를 다루기 위해서는 lxml의 etree를 import 해야 한다.
from lxml import etree;

# 다음의 딕셔너리 데이터를 xml로 만든다.
data = [{'name':'Tom','korean':90,'math':70,'english':80},
        {'name':'Smith','korean':70,'math':70,'english':50},
        {'name':'Wil','korean':80,'math':80,'english':60},
        {'name':'Who','korean':60,'math':40,'english':50}];

#xml의 root는 ClassGroup이라는 태그이다.
root = etree.Element("ClassGroup");
# ClassGroup 태그의 속성(어트리뷰트)의 name을 설정했다.
root.set("name", "Middle class 2");

# members라는 태그를 만들었다.
members = etree.Element("members");
# root의 밑에 태그를 두었다.
root.append(members);
# 딕셔너리를 for문을 통해 루프를 합니다.
for item in data:
  # 이건 SubElement 함수를 사용했는데, 안에는 Element생성과 append가 동시에 있는 것과 같다. 즉, members의 파생태그로 member를 생성한 것이다.
  #member = etree.Element("member");
  #members.append(member);
  member = etree.SubElement(members,"member");
  keys = tuple(item.keys());
  for key in keys:
    # key가 name의 경우는 name을 태그 이름으로 한다.
    if key == 'name':
      sub = etree.SubElement(member, key);
    else :
      # 그 외는 result라는 태그에 type 속성에 key를 넣는다.
      sub = etree.SubElement(member, "result");
      sub.set("type",key);
    # 위 딕셔너리 값을 넣는다.
    sub.text = str(item[key]);

# root를 기반으로 xml 데이터를 만든다. pretty_print파라미터는 사람이 보기 좋기 설정하는 것, 즉 false라면 개행과 format 줄마추기가 되지 않을 것이다.
x_output = etree.tostring(root, pretty_print=True, encoding='UTF-8')
# 파일 IO를 통해 xml를 출력했다.
with open("class.xml", "w") as handle:
  print('',file=handle);
  print(x_output.decode('utf-8'),file=handle);

위 결과를 보니 xml이 제대로 만들어 진 것을 확인할 수 있습니다.


여기서 참고사항은 element.Element를 사용해서 append하는 것과 SubElement를 사용하는 것은 같은 결과를 내보냅니다. 소스를 줄이려면 SubElement를 사용하는게 좋을 듯 싶습니다.

그리고 속성(어트리뷰트)의 경우는 set함수를 사용해서 넣을 수 있습니다.


이번에는 출력한 xml파일을 딕셔너리 형태로 만들어 보겠습니다.

#xml를 다루기 위해서는 lxml의 etree를 import 해야 한다.
from lxml import etree;

# xml를 위한 간단한 탐색 함수를 만들었다.
def explore_sub_node(childrens):
  # return 값은 딕셔너리로 설정했다.
  ret = {};
  # 파생 노드를 읽어온다.
  for child in childrens:
    # 태그가 result의 경우는 type에 딕셔너리 키를 넣었기 때문에 type으로 가져온다.
    if child.tag == 'result':
      keys = tuple(child.attrib.keys());
      for key in keys:
        ret[child.attrib[key]] = child.text;
    # 만약 파생 노드가 존재하면 재귀 함수(※함수에서 자기 자신을 호출하는 것)를 이용해 파생 노드를 딕셔너리 타입으로 넣었다.
    elif len(child.getchildren()) > 0:
      ret[child.tag] = [explore_sub_node(ret) for ret in child.getchildren()];
    # 그렇지 않으면 태그의 값을 넣는다.
    else :
      ret[child.tag] = child.text;
  return ret;

# xml를 읽어오는 것에 대해서는 따로 io를 사용할 필요없이 가져올 수 있다.
tree = etree.ElementTree(file="class.xml");
root = tree.getroot();

# xml 탐색 함수를 이용해 xml를 딕셔너리로 변환했다.
dict = explore_sub_node(root.getchildren());

# root부터 읽어 왔기 때문에 반드시 member 딕셔너리로 설정이 되어 있다.
print(dict);
print();
print(dict["members"]);

첫 번째 결과를 보면 확실히 members의 딕셔너리 안에 리스트 타입으로 되어 있습니다. dict["member"]로 값을 다시 보면 제가 최초로 작성했던 값과 같은 형태로 딕셔너리로 존재하게 되네요.


여기까지 Python에서 xml를 다루는 방법에 대한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.