본문 바로가기

HOW/Python

[Python] 나이스 학교코드 크롤링하는 방법

본 글은 제가 나이스 학교코드를 크롤링하는 로직을 찾아낸 방법에 대해 작성한 글입니다.

따라서 타 강좌 글보다 내용이 부실하거나 제대로 이해가 되지 않을 수 있습니다.

크롤링 하는 로직을 찾는 방법은 이렇구나 하는 정도로만 이해해 주시기 바랍니다. (__)

 

나이스에서 급식 정보나 학사일정과 같은 학교 관련 정보를 받아올려면 나이스에서 각 학교별로 부여하는 '학교코드'라는 것이 필요합니다. 문제는 이 학교코드라는 것을 쉽게 얻을 수 없습니다. 나이스 측에서 공식적으로 제공하는 api도 존재하지 않기 때문이죠. 

 

지금은 막혀버린 수입축산물 이력관리시스템

그래서 원래는 수입축산물 이력관리시스템에서 코드를 받아왔었습니다. 꽤 오랫동안 정상적으로 받아왔기에 괜찮을 줄 알았더니만 대한민국 정부 서비스 아니랄까봐 '공인인증서'를 도입하고 말았습니다.

 

그래서 새로운 방법을 찾아내기 시작하였습니다. 이것저것 검색하다 보니 학교알리미라고 해서 학교 공시 정보 등을 공개하는 사이트를 알게 되었습니다. 그리고 학교알리미에서 학교 정보를 조회하는 과정에서 사용자가 학교 이름을 검색하면 학교 이름을 서버로 보내고, 서버에서 코드가 포함된 학교 내역을 받아오면 받은 내역을 사용자에게 보여준다는 점도 알게 되었습니다. 그래서 바로 코드를 받아오는 방식을 뜯어보기 시작하였습니다.


1. 학교알리미 로직 파악하기

우선 우리가 요리를 할 페이지를 찾아보겠습니다. 제가 학교알리미 사이트를 이것저것 찾아본 결과 공시자료 검색이라는 페이지에서 학교 검색 결과를 불러오는 것을 확인하였습니다.

 

학교알리미의 공시자료 검색 사이트

해당 페이지에서 보면 여러가지 폼과 버튼을 확인할 수 있습니다만 학교 조회에서 필요한 폼들을 생각해 보면 '시/도' 셀렉트와 '시/군/구' 셀렉트 그리고 '학교명' 텍스트 인풋로 좁혀질 수 있습니다. 해당 폼에 작성된 내용이 무슨 이름으로 넘어가는지 확인하기 위해 개발자 도구를 실행시켜 봅시다.

 

개발자 도구를 실행해서 각 폼을 확인해 본 결과 '시/도' 셀렉트는 'SIDO_CODE'로 '시/군/구' 셀렉트는 'GUGUN_CODE'로 '학교이름' 텍스트 인풋을 'SRC_HG_NM'으로 넘어간다는 것을 알 수 있었습니다. 이 외에도 조회 사이트로 넘어가는 모든 폼들을 확인해본 결과 다음과 같은 정보를 알 수 있었습니다.

 

HG_CO 학교코드? 쯤 되는 것 같음
SEARCH_KIND 검색 방식으로 위에서 설정하는 것?
HG_JONGRYU_GB 학교 종류 (초, 중, 고등학교)
GS_HANGMOK_CD 공시 항목 코드
GU_GUN_CODE 구, 군 코드
GUGUN_CODE 구, 군 코드 (왜 두 개나 있는지는 의문)
SIDO_CODE 시, 도 코드
SRC_HG_NM 학교 이름

 

이러한 여러 정보들 중 우리가 원하는, 우리가 넘길 폼의 이름인 'SRC_HG_NM'에 대해 잘 기억해 두고 다음 단계로 넘어갑시다.

 

웹 프로그래밍에 대해 잘 모르시는 분들을 위해 설명드리자면 웹서버에서 서버간 통신하는 방법은 여러가지가 있습니다만 사용자에게 정보를 받아야는 상황 즉 폼을 넘겨야 하는 상황에서는 form이라는 코드를 사용합니다. 이 form 코드는 아이디나 네임을 가지지만 가장 중요한건 메소드와 액션입니다. 액션은 폼의 내용을 넘길 주소를 의미하고 메소드는 넘기는 방식을 의미합니다. 방식에는 get와 post 두 가지가 있는데 get은 주소 뒤에 폼의 내용을 붙여서 넘기는 걸 의미하고 post는 숨겨서 넘기는 걸 의미합니다.

 

학교알리미의 학교 검색 페이지에서 액션과 메소드를 확인해 보면 POST 방식으로 넘기며 '/ei/ss/Pneiss_a01_l0.do'라는 페이지로 정보를 넘기는 것을 확인할 수 있습니다. 이러한 내용을 그림으로 다시 정리해 보자면

 

나의 사랑 그림판

대충 이런 느낌이 된다는 걸 알 수 있습니다. 제대로 이해가 되지 않는다고요? 맞습니다. 저도 그렇습니다. (응?) 대충 로직만 파악하면 별로 상관없으니 다음 단계로 넘어갑시다.


2. 학교알리미에 파라미터 보내기

이제 해야할 것은 학교알리미에 파라미터를 보내는 작업입니다. 파라미터를 설명하기 위해선 http 통신 방법에 대해 설명해야 합니다. http에서 통신을 진행할 때 값을 전달하는 방법은 파라미터를 보내는 것입니다. 파라미터는 파이썬의 딕셔너리와 비슷하게 전달됩니다. 즉 'key': 'value' 형식으로 전달이 된다는 것입니다. 예를 들어 네이버에 파이썬이라고 검색을 하게 되면 query=파이썬이라고 서버에 보내져 서버에서 파이썬이라는 값을 가진 페이지를 검색합니다. 대충 개념은 이해가 되셨죠?

 

그럼 왜 학교알리미에 파라미터를 보내야하냐? 바로 학교알리미에서 어떠한 형식으로 값을 전달하는지 알아야 하기 때문입니다. api를 사용해 보신 분들을 아시겠지만 어떠한 키에 어떠한 밸류가 담겨져서 보낸다는 항목이 있습니다. 이 항목을 통해 개발자들이 파싱을 하고 값을 활용하는 것입니다. 백문이 불여일견, 직접 해보겠습니다.

import requests

우선 requests 모듈을 호출해 주도록 하겠습니다. 만약 requests 모듈이 존재하지 않는다면 pip를 활용해서 설치해 주시기 바랍니다. requests 모듈을 간단하게 설명해 보자면 웹서버와 통신을 할 수 있도록 돕는 모듈입니다. 대부분의 웹 관련 파이썬 프로젝트에 활용되니 참고해 두시기 바랍니다.

url = "https://www.schoolinfo.go.kr/ei/ss/Pneiss_a01_l0.do"

이제 url 값을 선언해 줄 차례입니다. url은 당연히 아까 알아본 학교알리미의 학교정보 조회 페이지입니다.

para = {
	"HG_CO": "",
	"SEARCH_KIND": "",
	"HG_JONGRYU_GB": "",
	"GS_HANGMOK_CD": "",
	"GU_GUN_CODE": "",
	"GUGUN_CODE": "",
	"SIDO_CODE": "",
	"SRC_HG_NM": "한가람"
}

그리고 이제 파라미터 값을 선언해 줄 차례입니다. 제가 아까 파이썬의 딕셔너리처럼 파라미터를 전달한다고 말씀드렸었죠? 그래서 파라미터 값을 지정할 때는 딕셔너리를 이용하여 선언해 줍니다. 참고로 아까 제가 파라미터 하나 기억해 두셔야 한다고 말씀드렸습니다. 잘 기억나시나요? 바로 SRC_HG_NM입니다. 이 파라미터는 학교 이름이 들어가는 파라미터이기에 기억해 두셨다가 본인이 검색할 학교의 이름을 입력해 주시기 바랍니다.

headers = {
	"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
}

이제 마지막으로 헤더 값을 지정을 해 줍니다. 헤더 값을 http 통신시 겉 페이지? 정도 된다고 보시면 됩니다. 어떠한 형식으로 전달할 것이며 어떠한 형식으로 받길 원한다 같은 느낌으로 말이죠. 위의 헤더 내용은 통신 내용을 UTF-8 형식으로 인코딩 하겠다는 의미입니다. 이 과정을 해 주지 않으면 경우에 따라 인코딩이 되지 않은 상태로 전달되기에 해주는 것이 좋습니다.

re = requests.post(url, data=para, headers=headers)

이제 서버단과 통신을 할 차례입니다. 아까전에 제가 통신 방법이 post라고 말씀드렸죠? 그래서 requests 모듈에서 post 형식의 통신 방법을 선언해 줄겁니다. post 함수에서는 url, data, headers 파라미터를 넘겨주어야 합니다. 차례대로 저희가 아까 지정한 변수를 대입해 줍니다. 그럼 학교알리미 서버와의 통신 준비는 모두 되었습니다. 그럼 이제 통신을 해 볼까요?

print(re.text)

이 코드를 입력하시고 실행을 해 보시면 아마 학교 목록이 주르륵 표시가 될겁니다. 이 코드의 의미는 해당 페이지의 내용을 출력하라는 뜻입니다.

{"schoolList02":[{"DTLAD_BRKDN":"6872-3","USER_TELNO_GA":"031-996-5345","HMPG_ADRES":"http:\/\/www.gimpohangaram.es.kr","PERC_FAXNO":"031-996-5346","FOND_SC_CODE":"2","ADRES_BRKDN":"경기도 김포시 구래동","LCTN_NM":"경기","SCHUL_CODE":"J100005869","SCHUL_KND_SC_CODE":"02","SCHUL_RDNMA":"경기도 김포시 김포한강9로 115번길 57","SCHUL_NM":"김포한가람초등학교","JU_DSTRT_OFCDC_CODE":"J100000158","USER_TELNO":"031-996-5345","JU_ATPT_OFCDC_CODE":"J100000001","ZIP_CODE":"415080","USER_TELNO_SW":"031-996-5342","ADRCD_ID":"4157010900"},{"DTLAD_BRKDN":"운정택지지구내","USER_TELNO_GA":"031-8071-5282","HMPG_ADRES":"http:\/\/han.es.kr","PERC_FAXNO":"031-8071-5286","FOND_SC_CODE":"2","ADRES_BRKDN":"경기도 파주시 와동동","LCTN_NM":"경기","SCHUL_CODE":"J100006454","SCHUL_KND_SC_CODE":"02","SCHUL_RDNMA":"경기도 파주시 금바위로 87","SCHUL_NM":"한가 람초등학교","JU_DSTRT_OFCDC_CODE":"J100000400","USER_TELNO":"031-8071-5280","JU_ATPT_OFCDC_CODE":"J100000001","ZIP_CODE":"10895","USER_TELNO_SW":"031-8071-5280","ADRCD_ID":"4148012200"}],"schoolList03":[{"DTLAD_BRKDN":"6873-6 ","HMPG_ADRES":"http:\/\/www.kphangaram.ms.kr","PERC_FAXNO":"031-996-7398","FOND_SC_CODE":"2","ADRES_BRKDN":"경기도 김포시 구래동","LCTN_NM":"경기","SCHUL_CODE":"J100006783","SCHUL_KND_SC_CODE":"03","SCHUL_RDNMA":"경기도 김포시 김포한강9로 140","SCHUL_NM":"김포한가람중학교","JU_DSTRT_OFCDC_CODE":"J100000158","USER_TELNO":"031-996-7391","JU_ATPT_OFCDC_CODE":"J100000001","ZIP_CODE":"415080","ADRCD_ID":"4157010900"},{"DTLAD_BRKDN":"514-1","HMPG_ADRES":"http:\/\/www.hangaram.ms.kr","PERC_FAXNO":"031-950-7904","FOND_SC_CODE":"2","ADRES_BRKDN":"경기도 파주시 와동동","LCTN_NM":"경기","SCHUL_CODE":"J100005877","SCHUL_KND_SC_CODE":"03","SCHUL_RDNMA":"경기도 파주시 금바위로 77","SCHUL_NM":"한가람중학교","JU_DSTRT_OFCDC_CODE":"J100000400","USER_TELNO":"031-950-7900","JU_ATPT_OFCDC_CODE":"J100000001","ZIP_CODE":"413190","ADRCD_ID":"4148012200"}],"schoolList04":[{"DTLAD_BRKDN":"909","HMPG_ADRES":"http:\/\/www.hangaram.hs.kr","PERC_FAXNO":"02-2642-3865","FOND_SC_CODE":"3","ADRES_BRKDN":"서울특별시 양천구 목동 ","LCTN_NM":"서울","SCHUL_CODE":"B100000549","SCHUL_KND_SC_CODE":"04","SCHUL_RDNMA":"서울특별시 양천구 목동서로 15","SCHUL_NM":"한가람고등학교","JU_DSTRT_OFCDC_CODE":"B100000001","USER_TELNO":"02-2642-3862","JU_ATPT_OFCDC_CODE":"B100000001","ZIP_CODE":"07984","HS_KND_SC_CODE":"04","ADRCD_ID":"1147010200"}],"schoolList05":[]}

정말 좀 보기 힘들게 반환이 됩니다... 이 상태로 눈 아프게 하나하나 읽으면서 파헤치기는 어려울겁니다. 조금 보기 좋게 해보겠습니다.

관심법이니라

짐..이 아니라 저의 관심법으로 깔끔하게 정리하였습니다.

{"schoolList02":
	[{
		"DTLAD_BRKDN":"6872-3",
		"USER_TELNO_GA":"031-996-5345",
		"HMPG_ADRES":"http:\/\/www.gimpohangaram.es.kr",
		"PERC_FAXNO":"031-996-5346",
		"FOND_SC_CODE":"2",
		"ADRES_BRKDN":"경기도 김포시 구래동",
		"LCTN_NM":"경기",
		"SCHUL_CODE":"J100005869",
		"SCHUL_KND_SC_CODE":"02",
		"SCHUL_RDNMA":"경기도 김포시 김포한강9로 115번길 57",
		"SCHUL_NM":"김포한가람초등학교",
		"JU_DSTRT_OFCDC_CODE":"J100000158",
		"USER_TELNO":"031-996-5345",
		"JU_ATPT_OFCDC_CODE":"J100000001",
		"ZIP_CODE":"415080",
		"USER_TELNO_SW":"031-996-5342",
		"ADRCD_ID":"4157010900"
	}]
}

대충 요런 느낌이 됩니다. 보기 좋쥬? 그럼 하나하나 뜯어보도록 하겠습니다.

 

우선 제일 먼저 어떠한 형식으로 반환되는지 알아보겠습니다. 반환된 값은 딕셔너리 -> 리스트 -> 딕셔너리 순 이었습니다. schoolList02, 03, 04, 05 형식의 키와 리스트를 가진 딕셔너리가 가장 바깥에 감싸져 있었습니다. 그리고 schoolList02, 03, 04, 05 형식의 키의 각각의 밸류들에는 학교가 리스트로 구분되어 저장되어 있었습니다. 즉 ['학교 1의 정보', '학교 2의 정보', '학교 3의 정보', '학교 x의 정보'] 의 형식이었습니다. 그 뒤에 각 리스트 값에는 학교의 정보가 또다시 딕셔너리로 저장되어 있었습니다. 해당 정보는 아래에서 알아보겠습니다.

 

약간 이런 느낌? 어째 더 복잡해진 듯한...

 

첫 번째로 가장 바깥에 있었던 딕셔너리를 알아보겠습니다. 딕셔너리의 키 중 하나였던 schoolList02의 경우에는 교급을 구분하는 명칭이라고 볼 수 있겠습니다. 반환받은 내용을 정리해 본 결과 schoolList02는 초등학교, 03은 중학교, 04는 고등학교, 05는 특수학교입니다. 어찌된 이유에서인지 01은 존재하지 않았습니다. 따라서 본인이 반환받은 내용에서 중학교를 접근하고 싶다면 schoolList03이라는 키를 통해 접근하면 됩니다.

 

두 번째로 중간에 있었던 리스트를 알아보겠습니다. 리스트의 순서는 가나다 순으로 추청됩니다만 정확히는 모르겠습니다. 여튼 반환된 학교의 정보가 리스트에 순서대로 쌓여 있었습니다.

 

세 번째로 마지막에 있었던 딕셔너리를 알아보겠습니다. 딕셔너리의 키는 총 17개 였습니다. 전화번호, 팩스번호, 홈페이지 주소, 우편번호 등이 실려있었으며 가장 중요한 학교 이름과 학교 코드도 있었습니다. (그런데 어째선지 시행 몇 년이 지난 신 우편번호가 아닌 구 우편번호가...) 학교 이름과 코드 부분만 떼오겠습니다.

"SCHUL_CODE":"J100005869",
"SCHUL_NM":"김포한가람초등학교",

이렇게 떼왔습니다. SCHUL_CODE 라는 키에 학교코드가 SCHUL_NM 이라는 키에 학교 이름이 실렸습니다. 그럼 결과적으로 SCHUL_CODE와 SCHUL_NM의 밸류를 반환하면 되겠죠? 그럼 이제 본격적으로 파싱을 해 보겠습니다.

3. 데이터 파싱하기

대충 구성 방법에 대해 알겠으니 파싱을 진행하도록 하겠습니다.

print(re.text['schoolList02'])

 

반환 받은 값에서 schoolList02라는 키를 가진 밸류를 가지고 오겠습니다.

Traceback (most recent call last):
  File "c:/Users/joong/Desktop/py.py", line 21, in <module>
    print(re.text['schoolList02'])
TypeError: string indices must be integers

했더니 어째 에러가 납니다. 타입 에러인걸 보이 요청할 수 없는 형식에다가 뭘 요청했다 봅니다. 도대체 무슨 자료형이길래 문제를 일으키는지 알아봅시다.

print(type(re.text))
# <class "str">

아하... str으로 받아온 것 이었습니다. 사실 그렇습니다. 웹에서 딕셔너리를 바로 받아올 수 없습니다. 사실 위의 형식은 json이라고 하는 api에서 흔히 사용되는 형식입니다. 즉 가장 밖에 있는 형식은 딕셔너리가 아닌 json 형식이었던 것이었죠. 그럼 우린 어떻게 받아오냐고요? 바로 모듈을 활용하면 됩니다.

import json
rejs = json.loads(re.text)

위의 코드는 json 모듈은 받아온 텍스트를 json 형태로 이해할 수 있도록 도와줍니다. 정확히는 dict 형식으로 이해할 수 있도록 도와줍니다. 즉 우리가 앞에서 이해한 것으로 바꿔준다는 것입니다. 가장 밖에 있는 텍스트의 자료형을 dict로 바꾸어 주었기에 앞에서 했던 명령을 오류 없이 진행할 수 있습니다.

print(rejs['schoolList02'])

입력해 보시면 오류없이 출력되는 걸 확인하실 수 있습니다. 그럼 이제 해당 정보에서 학교의 정보를 빼오도록 하겠습니다.

schoolnfo = rejs['schoolList02']
print(schoolnfo[0])

이 코드는 아까 받아온 데이터에서 초등학교 부분을 빼오고, 초등학교 데이터에서 0번째(1번째) 데이터를 뽑아오도록 합니다. 그럼 앞에서 본 데이터가 표시가 됩니다. 이제 코드와 이름을 빼오겠습니다.

si = schoolnfo[0]
print("학교이름 : " + si['SCHUL_NM'])
print("학교코드 : " + si['SCHUL_CODE'])

이 코드는 해당 학교 정보에서 학교 이름과 코드를 출력하도록 합니다. 한 번 실행해 볼까요?

학교이름 : 김포한가람초등학교
학교코드 : J100005869

축하합니다! 잘 표시가 됩니다. 공인인증서로 막혔던 접근 방법이 해결되었습니다~

사실 제가 보여 드렸던 데이터 접근 방법을 실제 상황에서 사용하기에는 어려움이 존재합니다. 몇 번째에 본인이 원하는 학교가 있을지 모르기 대문입니다. 따라서 for 문이나 while 문을 통해 하나하나 접근하는 방법이 필요한데 해당 방법은 여러분들이 직접 구현해 보시기 바랍니다.