ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 4. 기상청 RSS를 파싱해보자. (python, ElementTree)
    Python/파이썬 웹 크롤러 2019. 5. 16. 19:25
    반응형

    예제 주소: 

    https://github.com/pycrawling/crawling-tutorial/blob/master/weather.ipynb

     

    1. XML과 트리구조

    xml은 Tree 구조를 가집니다. 

    구글에서 검색해보세요. xml 트리 구조

    위 그림에서 아래의 트리구조를 확인할 수 있습니다.

    link와 item 은 아래에 자식 요소(엘리먼트)가 더 있지만 생략했습니다. 

    rss -+- channel -+- title 
                                   +- link -+ 
                                   +- description  
                                   +- language  
                                   +- generator  
                                   +- pubDate  
                                   +- item -+ 

    브라우저에서 다음 링크를 연 후에 삼각형을 클릭해보면서 트리 구조를 확인합시다.
    http://www.kma.go.kr/weather/forecast/mid-term-rss3.jsp?stnId=109

     

    2. 엘리먼트 트리로 xml 불러오기, root 확인하기

    XML의 트리구조를 구조를 분석해서 컴퓨터로 가져오는 과정을 XML 파싱이라고 합니다.
    파이썬에서 xml을 파싱 하는 표준 라이브러리는 여러 가지가 있습니다.
    https://wiki.python.org/moin/PythonXml

    구글링 해보니 엘리먼트 트리를 많이 사용하는 것 같습니다.

     

    엘리먼트 트리의 공식 문서입니다. 3.8부터는 한글화 되었네요. 
    불행히도 영문입니다만, 코드만 봐도 대략 파악하실 수 있을 겁니다. 
    https://docs.python.org/ko/3/library/xml.etree.elementtree.html

    import xml.etree.ElementTree as ET
    # 라이브러리를 불러들입니다. 매번 타이핑하기엔 너무 길죠? ET로 약칭하기로 합니다.  
    
    root = ET.fromstring(xml_data)
    # xml_data 라는 문자열이 담긴 변수를, fromstring() 메소드(함수)에 인자로 넘겨주고, 
    # 함수에서 파싱을 거친 뒤, root에 넘겨줍니다. 

    * 만약 파일을 직접 파싱한다면 ET.fromstring 대신 ET.parse 와 ET.getroot()를 사용하면 됩니다. 

     

    root가 뭔지 확인해봅니다. 

    root  # <Element 'rss' at 0x000001E557432B88>

    rss라는 엘리먼트를 객체로 돌려주네요. 

     

    root.tag 와 root.attrib 을 확인해 봅니다. 

    root.tag  # 'rss'
    root.attrib  # {'version': '2.0'}

     

    3. xml 엘리먼트의 구성

    이제 태그와 attrib가 뭔지 알아볼까요?

     

    xml에서 하나의 엘리먼트는 tag, attrib, text 값을 가집니다. 

    <rss version="2.0">

    tag는 위의 예에서 rss 즉 태그의 이름을 의미합니다. 

    attrib는 version="2.0"처럼 태그의 속성을 의미합니다. 
    파이썬에서는 {키: 값}의 딕셔너리 구조입니다. 

     

    text를 설명하기 위해 title 태그를 보겠습니다. 

    <title>기상청 육상 중기예보</title>

    text는 태그 사이의 text 값을 말합니다. 

    위의 예에서는 '기상청 육상 중기예보'입니다. 

    여기서 xml에서 태그는 <태그명></태그명>으로 열고 닫혀야 된다는 것을 확인할 수 있네요. 
    위에 '<rss>태그도 '</rss>'로 닫혀 있습니다. 찾아보세요~!

     

    참고로 '<br>'처럼 혼자인 태그는 <br />로 쓰는 게 원칙입니다.

    하지만 예전 사이트들의  html 소스를 보시면 '<br>'로 되어 있는 곳도 많을 겁니다. 

     

    4. find을 이용한 자식 노드 찾기

    rss -+- channel -+- title 
                                   +- link -+ 
                                   +- description  
                                   +- language  
                                   +- generator  
                                   +- pubDate  
                                   +- item -+ 

    이 구조에서 title에 접근해 봅시다. 

     

    노드 찾기는 뿌리(root)에서 시작합니다. 

    for child in root: 
        print(child.tag)
        
    # channel

    root는 반복하면서 자신의 자식을 하나씩 꺼내 줍니다. 

    root는 자식이 하나밖에 없어서.. (위 트리 구조 참고) 
    그 자식인 channel이 나왔습니다. 

     

    엘리먼트 트리에서 'root'의 자식인 'channel'은 root.find('channel')으로 표현합니다. 

    channel의 자식들을 찾아보겠습니다. 

    for child in root.find('channel'):
         print(child.tag)
    title
    link
    language
    generator
    pubDate
    item

    이제 우리가 목표로 했던 title는 다음과 같은 형식으로 표현할 수 있음을 알 수 있습니다. 

    root.find('channel').find('title')  # <Element 'title' at 0x000001E55743D228> 

    각각 확인해 봅시다. 

    root.find('channel').find('title').tag  # 'title' 
    root.find('channel').find('title').attrib  # {} 
    root.find('channel').find('title').text  # '기상청 육상 중기예보' 

     

    find를 사용하지 않고 [] 를 이용하는 방법도 있습니다.

    root[0][0].tag  # 'title'
    root[0][2].tag  # 'description'
    

    숫자로 하드코딩을 하는 방법은 가독성에도 문제가 있고, 서버에서 제공하는 데이터의 순서가 바뀌기만 하더라도 뒤죽박죽이 되어버리기 때문에 추천하진 않습니다. 

    --------------------------------------------------------

    전체 xml을 확인해보면 우리가 실제로 이용해야할 날씨 데이터들은 대부분 item 항목에 있습니다. 
    item의 description의 header의 자식 노드들을 뽑아 봅니다. 

    (헥헥헥 힘들다 ㅠ,.ㅠ) 

    for child in root.find('channel').find('item').find('description').find('header'): 
        print(child.tag, ':', child.text)
    title : 서울,경기도 육상중기예보 
    tm : 201904280600 
    wf : 기압골의 영향으로 5월 1일에 비가 오겠고, 그 밖의 날은 고기압의 영향으로 맑은 날이 많겠습니다.   
    기온은 평년(최저기온: 9~12℃, 최고기온: 19~22℃)과 비슷하겠습니다.
    강수량은 평년(2~5mm)과 비슷하겠습니다.
    서해중부해상의 물결은 0.5~2.0m로 일겠습니다.

    item의 description의 body의 location의 자식 노드들을 뽑아 봅니다. (헥헥헥 더 힘들다 ㅠ,.ㅠ)

    for child in root.find('channel').find('item').find('description').find('body').find('location'): 
        print(child.tag, ':', child.text)
    province : 서울ㆍ인천ㆍ경기도 
    city : 서울 
    data :  
    data :  
    data :  
    data :  
    data :  
    data :  
    data :  
    data :  
    data :  
    data :  
    data :  
    data :  
    data : 

    아 이제 하나 하나씩 자식 노드들을 검색하는 것도 한계에 달했습니다. 
    data 항목은 제대로 보이지도 않네요. 이 data 항목은 어떻게 처리하는 것이 좋을까요?

     

    이럴 때 사용하기 위해서 findall() 이라는 함수가 있습니다. 

     

    5. findall을 이용한 자식 노드 찾기

    ElementTree에서 findall은 자식 노드들 중에 인수에 지정된 엘리먼트들을 모두 모아오는 함수입니다.

    (리스트로 모으죠?)

    root.find('channel').find('item').find('description').find('body').find('location').findall('data')
    [<Element 'data' at 0x000001E557481688>,
     <Element 'data' at 0x000001E5574818B8>,
     <Element 'data' at 0x000001E557481AE8>,
     <Element 'data' at 0x000001E557481D18>,
     <Element 'data' at 0x000001E557481F48>,
     <Element 'data' at 0x000001E5574841D8>,
     <Element 'data' at 0x000001E557484408>,
     <Element 'data' at 0x000001E55743DEF8>,
     <Element 'data' at 0x000001E557450098>,
     <Element 'data' at 0x000001E557484728>,
     <Element 'data' at 0x000001E557484958>,
     <Element 'data' at 0x000001E557484B88>,
     <Element 'data' at 0x000001E557484DB8>]

     

    각각의 내용을 확인하기 위해서는 또 for 를 돌려야 겠죠?

    for data in root.find('channel').find('item').find('description').find('body').find('location').findall('data'):
         for child in data:
             print(child.tag, ':', child.text)
    mode : A02
    tmEf : 2019-05-01 00:00
    wf : 구름많음
    tmn : 13
    tmx : 21
    
    ... 생략 ...
    
    tmEf : 2019-05-08 00:00
    wf : 맑음
    tmn : 12
    tmx : 22
    reliability : 보통

    드디어 우리가 원하는 데이터들을 모두 파싱했습니다. 

    이젠 우리는 이 데이터를 DB에 저장할 수도 있을 것이고,
    www로 세상에 공개할 수도 있습니다만.. 

     

    이렇게 끝일까요? 
    너무 번거롭지 않던가요?

    실전에서 사용할 2가지 방법을 알려드리겠습니다. 

     

    6. iter를 이용한 자식 노드 찾기

    for data in root.iter('data'):
        for child in data:
            print(child.tag, ':', child.text)

    findall은 직계 자식만 검색하는 반면 iter는 하위 트리 전부를 검색합니다. ^^

    [참고] iter는 이터레이터라는 개념에서 나온 것입니다. 
    이터레이터에 대해 궁금하신 분들은 https://dojang.io/mod/page/view.php?id=2405 참고.

     

    root 이하 모든 태그를 순환할 수도 있습니다.

    for child in root.iter():
        print(child.tag)

     

    7. XPath를 이용한 자식 노드 찾기

    root 부터 시작합니다. 

    root.find('.')  # <Element 'rss' at 0x000001B298F0E9F8>

    하위 노드로 내려가 볼까요?

    root.find('./channel') # <Element 'channel' at 0x000001B298F0E8B8>

    이젠 많이 내려가 보겠습니다. 다음과 같은 건 어떻게 표현할까요?

    root.find('channel').find('item').find('description').find('body').find('location').findall('data')
    root.find('./channel/item/description/body/location').findall('data')

    간결합니다. @,.@

     

    중간 단계의 생략이 가능합니다. 

    root.find('.//item/description/body/location').findall('data')
    root.find('.//location').findall('data')
    root.findall('.//data')

    더 간결합니다. @,.@

     

    최종 코드

    우리는 필요한 정보을 꺼내 올 수 있게 되었습니다. 

    from urllib import request  # urllib 라이브러리를 불러옵니다.
    import xml.etree.ElementTree as ET
    
    r = request.urlopen('https://www.kma.go.kr/wid/queryDFSRSS.jsp?zone=2726058000')
    xml_data = r.read().decode('utf-8')
    # print(xml_data)
    
    root = ET.fromstring(xml_data)
    data = root.findall('.//data')
    # print(data)
    for child in data:
        for each in child:
            print(each.tag, ':', each.text)
    
    # for each in data[0]:
    #     print(each.tag, ':', each.text)

     

    더 자세히 알고 싶으시다면, 엘리먼트 트리 공식 문서의 XPath 파트를 읽어보셔도 좋습니다. 
    https://docs.python.org/ko/3/library/xml.etree.elementtree.html#supported-xpath-syntax

     

    html문서를 크롬에서 열면 개발자 도구를 이용해 XPath를 '바로' 뽑을 수 있는 반면..
    xml문서를 크롬에서 열면 (이쁘게 보여주기 위해?) html로 변환을 하기 때문에 XPath를 '바로' 뽑을 수 없습니다. 
    바로 뽑으면 참 편한데....

     

    ------------------------------------

     

    지금까지 urllib와 EnementTree를 이용해 기상청 RSS를 파싱해 보았습니다.
    다음 회에는 Beautiful Soup 과 selenium을 이용한 크롤링을 설명드리도록 하겠습니다. 

    반응형
Designed by Tistory.