-
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=1092. 엘리먼트 트리로 xml 불러오기, root 확인하기
XML의 트리구조를 구조를 분석해서 컴퓨터로 가져오는 과정을 XML 파싱이라고 합니다.
파이썬에서 xml을 파싱 하는 표준 라이브러리는 여러 가지가 있습니다.
https://wiki.python.org/moin/PythonXml
구글링 해보니 엘리먼트 트리를 많이 사용하는 것 같습니다.엘리먼트 트리의 공식 문서입니다. 3.8부터는 한글화 되었네요.
불행히도 영문입니다만, 코드만 봐도 대략 파악하실 수 있을 겁니다.
https://docs.python.org/ko/3/library/xml.etree.elementtree.htmlimport 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-syntaxhtml문서를 크롬에서 열면 개발자 도구를 이용해 XPath를 '바로' 뽑을 수 있는 반면..
xml문서를 크롬에서 열면 (이쁘게 보여주기 위해?) html로 변환을 하기 때문에 XPath를 '바로' 뽑을 수 없습니다.
바로 뽑으면 참 편한데....------------------------------------
지금까지 urllib와 EnementTree를 이용해 기상청 RSS를 파싱해 보았습니다.
다음 회에는 Beautiful Soup 과 selenium을 이용한 크롤링을 설명드리도록 하겠습니다.반응형