지난번 포스팅에서는 웹에서 JSON 포맷 파일을 읽어와서 pandas DataFrame으로 변환하는 방법에 대해서 소개하였습니다. 


이번 포스팅에서는 JSON과 함께 웹 애플리케이션에서 많이 사용하는 데이터 포맷인 XML (Extensible Markup Language) 을 Python을 사용하여 웹으로 부터 읽어와서 파싱(parsing XML), pandas DataFrame으로 변환하여 분석과 시각화하는 방법을 소개하겠습니다. 


XML (Extensible Markup Language) 인간과 기계가 모두 읽을 수 있는 형태로 문서를 인코딩하는 규칙의 집합을 정의하는 마크업 언어(Markup Language) 입니다. XML의 설계 목적은 단순성, 범용성, 인터넷에서의 활용성을 강조점을 둡니다. XML은 다양한 인간 언어들을 유니코드를 통해 강력하게 지원하는 텍스트 데이터 포맷입니다. 비록 XML의 설계가 문서에 중점을 두고는 있지만, XML은 임의의 데이터 구조를 띠는 웹 서비스와 같은 용도의 재표현을 위한 용도로 광범위하게 사용되고 있습니다. 

- from wikipedia (https://en.wikipedia.org/wiki/XML) -


XML 은 아래와 같이 생겼는데요, HTML, JSON과 왠지 비슷하게 생겼지요? 



[ XML format data 예시 ]



<CATALOG>
<CD>
<TITLE>Empire Burlesque</TITLE>
<ARTIST>Bob Dylan</ARTIST>
<COUNTRY>USA</COUNTRY>
<COMPANY>Columbia</COMPANY>
<PRICE>10.90</PRICE>
<YEAR>1985</YEAR>
</CD>
<CD>
<TITLE>Hide your heart</TITLE>
<ARTIST>Bonnie Tyler</ARTIST>
<COUNTRY>UK</COUNTRY>
<COMPANY>CBS Records</COMPANY>
<PRICE>9.90</PRICE>
<YEAR>1988</YEAR>
</CD>

<CD>

<TITLE>Unchain my heart</TITLE>
<ARTIST>Joe Cocker</ARTIST>
<COUNTRY>USA</COUNTRY>
<COMPANY>EMI</COMPANY>
<PRICE>8.20</PRICE>
<YEAR>1987</YEAR>
</CD>

</CATALOG> 


- source: https://www.w3schools.com/xml/cd_catalog.xml -





[ Python으로 웹에서 XML 데이터를 읽어와서 pandas DataFrame으로 만들기 코드 예제 ]




(1) Import Libraries


먼저 XML 을 파싱하는데 필요한 xml.etree.ElementTree 모듈과 웹 사이트에 접속해서 XML 파일을 읽을 수 있도록 요청하는 urllib 모듈을 불러오겠습니다. XML 데이터를 나무(Tree)에 비유해서 뿌리(root)부터 시작하여 줄기, 가지, 잎파리까지 단계적으로 파싱한다는 의미에서 모듈 이름이 xml.etree.ElementTree 라고 생각하면 됩니다. 



import pandas as pd

import xml.etree.ElementTree as ET


import sys

if sys.version_info[0] == 3:

    from urllib.request import urlopen

else:

    from urllib import urlopen




Python 3.x 버전에서는 'from urllib.request import urlopen'으로 urllib 모듈의 request 메소드를 import해야 하며, 만약 Python 3.x 버전에서 아래처럼 'from urllib import urlopen' 을 사용하면 'ImportError: cannot import name 'urlopen'' 이라는 ImportError가 발생합니다. 



# If you are using Python 3.x version, then ImportError will be raised as below

from urllib import urlopen

---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
<ipython-input-2-dbf1dbb53f94> in <module>()
----> 1 from urllib import urlopen

ImportError: cannot import name 'urlopen'





(2) Open URL and Read XML data from Website URL


이제 "https://www.w3schools.com/xml/cd_catalog.xml" 사이트에서 XML 포맷의 CD catalog 정보를 문자열(string)로 읽어와보겠습니다. 



url = "https://www.w3schools.com/xml/cd_catalog.xml"

response = urlopen(url).read()

xtree = ET.fromstring(response)


xtree

<Element 'CATALOG' at 0x00000219E77DCCC8>

 




(3) Parsing XML data into text by iterating through each node of the tree


다음으로 for loop을 돌면서 나무의 노드들(nodes of tree)에서 필요한 정보를 찾아 파싱(find and parse XML data)하여 텍스트 데이터(text)로 변환하여 사전형(Dictionary)의 키, 값의 쌍으로 차곡차곡 저장(append)을 해보겠습니다



rows = []


# iterate through each node of the tree

for node in xtree:

    n_title = node.find("TITLE").text

    n_artist = node.find("ARTIST").text

    n_country = node.find("COUNTRY").text

    n_company = node.find("COMPANY").text

    n_price = node.find("PRICE").text

    n_year = node.find("YEAR").text

    

    rows.append({"title": n_title, 

                 "artist": n_artist, 

                 "country": n_country, 

                 "company": n_company, 

                 "price": n_price, 

                 "year": n_year})

 




(4) Convert XML text data into pandas DataFrame



# convert XML data to pandas DataFrame

columns = ["title", "artist", "country", "company", "price", "year"]

catalog_cd_df = pd.DataFrame(rows, columns = columns)


catalog_cd_df.head(10)

titleartistcountrycompanypriceyear
0Empire BurlesqueBob DylanUSAColumbia10.901985
1Hide your heartBonnie TylerUKCBS Records9.901988
2Greatest HitsDolly PartonUSARCA9.901982
3Still got the bluesGary MooreUKVirgin records10.201990
4ErosEros RamazzottiEUBMG9.901997
5One night onlyBee GeesUKPolydor10.901998
6Sylvias MotherDr.HookUKCBS8.101973
7Maggie MayRod StewartUKPickwick8.501990
8RomanzaAndrea BocelliEUPolydor10.801996
9When a man loves a womanPercy SledgeUSAAtlantic8.701987





(5) Change data type from string object to float64, int32 for numeric data


아래에 df.dtypes 로 각 칼럼의 데이터 형태를 확인해보니 전부 문자열 객체(string object)입니다. astype()을 이용하여 칼럼 중에서 price는 float64, year는 int32로 변환을 해보겠습니다. 



catalog_cd_df.dtypes

title      object
artist     object
country    object
company    object
price      object
year       object
dtype: object


import numpy as np

catalog_cd_df = catalog_cd_df.astype({'price': np.float

                                      'year': int})


catalog_cd_df.dtypes

title       object
artist      object
country     object
company     object
price      float64
year         int32
dtype: object





(6) Calculate mean value of price by Country and plot bar plot it



country_mean = catalog_cd_df.groupby('country').price.mean()

country_mean

country
EU        9.320000
Norway    7.900000
UK        8.984615
USA       9.385714
Name: price, dtype: float64



country_mean_df = pd.DataFrame(country_mean).reset_index()


import seaborn as sns

sns.barplot(x='country', y='price', data=country_mean_df)

plt.show()


 



이상으로 웹에서 XML 데이터를 Python으로 읽어와서 파싱 후 pandas DataFrame으로 변환하는 방법에 대한 소개를 마치겠습니다. 



Python으로 JSON 파일 읽기, 쓰기는 https://rfriend.tistory.com/474 를 참고하세요. 

Python으로 YAML 파일 읽기, 쓰기는 https://rfriend.tistory.com/540 를 참고하세요. 


많은 도움이 되었기를 바랍니다. 


이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)


Posted by R Friend Rfriend

댓글을 달아 주세요

  1. 공부하자 2019.09.14 00:01  댓글주소  수정/삭제  댓글쓰기

    올려주시는 강좌 잘 보고 있습니다. 덕분에 헤매지 않고 공부에 필요한 도움을 받고있습니다. 감사드립니다.

  2. 감사합니다. 2020.02.08 12:34  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 정말 큰 도움 주셔서 감사합니다.
    그런데 xtree 형식이 response여서 node가 nonetype인데 이럴땐 어떻게 해야 할까요?

  3. 오류가 발생합니다 2020.03.29 22:53  댓글주소  수정/삭제  댓글쓰기

    ParseError: not well-formed (invalid token): line 1, column 0

    다음과 같은 에러가 발생하는데 어떤게 문제인가요?

    • R Friend Rfriend 2020.03.29 22:55 신고  댓글주소  수정/삭제

      이 에러는 저도 겪어보지 못한 에러인데요, 아래 코드 한번 해보실래요?

      from lxml import etree
      parser = etree.XMLParser(recover=True)
      etree.fromstring(xmlstring, parser=parser)

      혹은,
      import xml.etree.ElementTree as ET
      parser = ET.XMLParser(encoding="utf-8")
      tree = ET.fromstring(xmlstring, parser=parser)

    • CEOSW 2020.03.29 23:25  댓글주소  수정/삭제

      아 죄송합니다 데이터 포맷이 XML이 아닌 JSON이였네요. 친절한 답변 감사드립니다.

    • R Friend Rfriend 2020.03.29 23:27 신고  댓글주소  수정/삭제

      해결되었다니 다행입니다. ^^

  4. Ramm 2020.06.16 20:36  댓글주소  수정/삭제  댓글쓰기

    만약 API 호출처럼 xml 페이지의 수가 많다면 반복문으로도 진행이 될까요? 섣불리 판단하지 못하겠네요 ㅠㅠ

    • R Friend Rfriend 2020.06.18 11:34 신고  댓글주소  수정/삭제

      안녕하세요 Ramm님,

      저도 해보지는 않았는데요, 반복문으로 해도 될 거 같습니다. 혹시 해보시게 되면 어떤지 댓글 남겨주시면 고맙겠습니다.

  5. MSI 2020.09.15 10:53  댓글주소  수정/삭제  댓글쓰기

    안녕하십니까. 글 항상 잘 보고 있습니다.

    저는 지금 공공데이터 api를 활용하여 분석을 진행하려고 하고 있습니다.
    저는 xml파일을 따로 불러와서 2번까지는 성공했는데,
    3번에서 'NoneType' object has no attribute 'text'라는 오류가 계속 떠서 어떻게 해결하는 지 궁금해서 이렇게 질문을 남깁니다.

    • R Friend Rfriend 2020.09.15 11:51 신고  댓글주소  수정/삭제

      안녕하세요 MSI님, 반갑습니다.

      작성하신 코드를 봐야지 정확하게 판단이 가능할텐데요,

      일단 에러 메시지가 'NoneType' object has no attribute 'text' 인 것을 보면,

      (1)
      for node in xtree:
      node.find("element_name_here").text

      에서 찾으려고(find) 하는 원소("element_name_here")가 읽어온 XML 파일에 없기 때문에 그런게 아닌가 추측해봅니다.

      부모와 자식의 element 리스트 확인은 아래 코드 참조하세요.

      xtree.findall("*")[:10] # 상위 10개
      [<Element 'CD' at 0x7f7f7d57a680>,
      <Element 'CD' at 0x7f7f7e7d7a90>,
      <Element 'CD' at 0x7f7f7e7d7d10>,
      <Element 'CD' at 0x7f7f7e7e7090>,
      <Element 'CD' at 0x7f7f7e7e7310>,
      <Element 'CD' at 0x7f7f7e7e7540>,
      <Element 'CD' at 0x7f7f7e7a7f90>,
      <Element 'CD' at 0x7f7f7e7a7ae0>,
      <Element 'CD' at 0x7f7f7e7a7770>,
      <Element 'CD' at 0x7f7f7e7a7450>]


      xtree.findall("./CD//")[:10]
      [<Element 'TITLE' at 0x7f7f7e7c69f0>,
      <Element 'ARTIST' at 0x7f7f7e7d7ef0>,
      <Element 'COUNTRY' at 0x7f7f7e7d7f40>,
      <Element 'COMPANY' at 0x7f7f7e7d7f90>,
      <Element 'PRICE' at 0x7f7f7e7d7a40>,
      <Element 'YEAR' at 0x7f7f7e7d7ae0>,
      <Element 'TITLE' at 0x7f7f7e7d7b30>,
      <Element 'ARTIST' at 0x7f7f7e7d7b80>,
      <Element 'COUNTRY' at 0x7f7f7e7d7bd0>,
      <Element 'COMPANY' at 0x7f7f7e7d7c20>]


      (2) 혹은 for loop 을 사용해서 child elements인 node를 하나씩 호출한 것이 아니라, 그냥 xtree 에 바로
      xtree.find("TITLE").text 처럼 하면 에러가 발생합니다.


      (3) XML 파싱하는데 사용하는 메소드가 여러개 있는데요, 아래 링크를 참조하시면 도움이 될거 같습니다.

      https://docs.python.org/2/library/xml.etree.elementtree.html

    • MSI 2020.09.15 13:20  댓글주소  수정/삭제

      감사합니다. 다시 한번 도전해봐야겠습니다!

  6. MSI 2020.09.15 14:22  댓글주소  수정/삭제  댓글쓰기

    rows=[]
    a= xtree.findall("./body//")[2].text
    b= xtree.findall("./body//")[3].text
    c= xtree.findall("./body//")[4].text
    d= xtree.findall("./body//")[5].text
    e= xtree.findall("./body//")[6].text
    f= xtree.findall("./body//")[7].text
    g= xtree.findall("./body//")[8].text
    h= xtree.findall("./body//")[9].text
    n= xtree.findall("./body//")[10].text
    j= xtree.findall("./body//")[11].text
    k= xtree.findall("./body//")[12].text
    l= xtree.findall("./body//")[13].text
    m= xtree.findall("./body//")[14].text
    rows.append({'std_year': a, 'acc_cl_nm' : b,
    'sido_sgg_nm' : c,'acc_cnt' : d,'acc_cnt_cmrt' : e,'dth_dnv_cnt' : f, 'dth_dnv_cnt_cmrt' : g, 'ftlt_rate' : h,
    'injpsn_cnt' : n,'injpsn_cnt_cmrt' : j,'tot_acc_cnt' : k,'tot_dth_dnv_cnt' : l,'tot_injpsn_cnt' : m
    })

    이렇게 할 경우 1개는 나오는데 for문을 어떻게 해야할 지 모르겠습니다...ㅠ 알려주신 첫번째오류로 인해서 생겼습니다. 자식노드가 'body'안에 있었습니다.
    하지만 여기서 for문을 어떻게 써야할 지 모르겠습니다. ㅠㅠ
    번거롭게 해서 죄송합니다 ㅠ

    • R Friend Rfriend 2020.09.15 14:30 신고  댓글주소  수정/삭제

      안녕하세요.

      tree.findall("./body//") 에서 child node elements 들 이름 확인하셨다면

      블로그 포스팅의 (3)번 처럼 해서 파싱해서 key:value 쌍의 dict 를 append 하고,

      (4)번 처럼 pandas DataFrame으로 변환하면 됩니다.

      ('-------' 는 댓글란에서는 들여쓰기가 안먹어서 가독성을 위해서 그냥 넣은 것입니다.)

      rows = []

      for node in tree:
      ----a = node.find("std_year").text
      ----b = node.find("sido_sgg_nm").text
      ... (나머지 써줌)
      ...
      ----rows.append("std_year": a,
      -----------------"sido_sgg_nm": b,
      ----------------- ... }0

      # DataFrame
      df = pd.DataFrame(rows, columns=["std_year", ...])

    • MSI 2020.09.15 14:45  댓글주소  수정/삭제

      답변감사합니다.

      제가 3번같이 할경우에 'NoneType' object has no attribute 'text' 오류가 떠서 맨 처음에 질문했던 것이었습니다....ㅠㅠ 바쁘실텐데 죄송합니다 ㅠ

    • R Friend Rfriend 2020.09.15 14:56 신고  댓글주소  수정/삭제

      xtree.findall() 은 element 이름 확인하기 위해서 말씀드렸던 거예요.

      a = xtree.findall("./body//")[0] 처럼 하면 데이터를 가져오는 것이 아니라 xml.etree.ElementTree.Element 자체를 가져오므로 데이터를 파싱한것이 아닙니다.

      element 이름을 확인하고, for loop 으로 xtree 안의 node 를 순회하면서 node별로 node.find("이름") 를 해서 데이터를 가져와서 append 해보세요.

    • MSI 2020.09.15 15:05  댓글주소  수정/삭제

      네 일단. 저도 그렇게 했는데.. 안되어서 일단 더 공부해보고 완성되면 답글올리겠습니다.
      너무 감사합니다. ㅠㅠㅠ 정말!