?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄 첨부
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄 첨부
Extra Form
라이선스 기타(따로 작성)

***개선된 버전을 SIR에 올렸습니다.

https://sir.kr/g5_tip/15526

 

안녕하세요? 저녁식사는 맛있게 드셨는지요?

 

여러모로 부족한 점이 ...

 

 

 

안녕하세요?

 

대한약사회 측에서 운영하는 '휴일지킴이 약국' 사이트를 크롤링하여 도로명 주소 등 데이터를 획득한 후에 

 

위 주소를 국토교통부의 Geocoder API 2.0를 이용하여 경위도 좌표계의 좌표로 변환하여

 

Folium 및 PyQt5을 이용하여 마커로 표시하는 윈도우 앱입니다!

 

 

저는 윈도우 기준으로 작성을 하였지만 Folium에서 HTML로 export할 수 있기 때문에,

 

홈페이지를 운영하시는 분들도 참고하시면 유용하게 활용하실 것 같네요 ^-^

 

 

우선 휴일지킴약국 홈페이지(https://www.pharm114.or.kr/main.asp)에서의 검색 결과를 살펴보면,

 

각 약국마다 위치정보 및 특이사항의 입력 여부가 다른 등 데이터가 상이하기 때문에 tr 태그의 수가 일정하지 않습니다 ㅠㅠ

(하나의 약국 당 tr 태그 2~4개를 사용합니다.)

 

이런 점을 감안하여 누락되는 데이터 없이 모든 정보를 크롤링을 할 수 있도록 작성하였습니다~!

 

img 20210313 192653.png.jpg

 

 

그리고 휴일지킴이 약국 사이트는 대한약사회 측에서 API를 제공하지 않고,

 

봇에 의한 크롤링을 방지하기 위한 약간의 장치들이 있더군요 ㅎㄷㄷ

 

예컨대 POST 전송시 formdata에 ss_key 값을 입력하여야 합니다.

 

다행히 이 장치들을 어렵지 않게 우회할 수 있었어요~ ^-^

 

 

사실 위 장치들을 우회하는 것보다 인코딩이 EUC-KR이었던 점이 더 귀찮았네요 ㅜㅜㅜㅜㅜ

 

크롬 개발자도구에서 POST 전송의 Formdata를 확인하면 (unable to decode value)라고 뜨더군요!

 

사이트 소스를 확인해보면 사이트 자체는 EUC-KR이지만, 일부 처리 과정 있어 UTF-8로 인코딩하는 로직이 있어요.

 

 

다음 소스에서 br 태그를 직접 입력하면, 게시글을 볼 때 태그가 출력되는 것이 아니라 개행이 되기 때문에 부득이 'BR태그'라고 적었네요 ㅠㅠ

 

from PyQt5 import QtWidgets, QtWebEngineWidgets
from requests_html import HTMLSession
from bs4 import BeautifulSoup
import numpy as np
from datetime import datetime, timedelta
import folium, json, sys, io
from folium.plugins import MarkerCluster


def crawling_pharm114(): # 휴일지킴이 약국 사이트를 크롤링하는 함수입니다.
    s = HTMLSession()
    html = s.get('https://www.pharm114.or.kr/common_files/sub1_page1.asp').content
    soup = BeautifulSoup(html, 'html5lib')
    ss_key = soup.find('input', {'name' : 'ss_key'})['value'] # ss_key 값을 구합니다.
    now = datetime.now()
    # 다음 세 줄은 30분 간격으로 시간을 나누는 스크립트이나, 현재 시간을 직접 formdata에 대입하여도 무방합니다.
    delta = timedelta(minutes = 30)
    start_time = (now - (now - datetime.min) % delta).strftime('%H:%M')
    end_time = (now + (datetime.min - now) % delta).strftime('%H:%M')

    formdata = {
    'search_first': 'T',
    'ss_key': ss_key,
    'm_year': now.year,
    'm_month': '{:02d}'.format(now.month), # 반드시 이렇게 두 자리로 처리하지 않아도 무방합니다.
    'm_day': '{:02d}'.format(now.day), # 반드시 이렇게 두 자리로 처리하지 않아도 무방합니다.
    'time_s1': start_time,
    'time_e1': end_time,
    'addr1': '서울특별시'.encode('euc-kr'), # 인코딩을 euc-kr로 변경합니다.
    'addr2': '서초구'.encode('euc-kr'), # 인코딩을 euc-kr로 변경합니다.
    'image1.x': '22', # 마우스 위치를 감지하여 대입하는 부분이며, 적절한 값을 입력하면 무방합니다.
    'image1.y': '11', # 마우스 위치를 감지하여 대입하는 부분이며, 적절한 값을 입력하면 무방합니다.
    #'addr3': '검색할 키워드'.encode('euc-kr') 결과 내 재검색(도로명,건물번호,건물명)을 할 경우에 입력하면 됩니다.
    }
    html = s.post('https://www.pharm114.or.kr/search/search_result.asp', data = formdata).content
    soup = BeautifulSoup(html, 'html5lib')
    try: 
        trs = soup.find('div', {'id' : 'printZone'}).find('table', {'style' : 'TABLE-LAYOUT: fixed'}).tbody.find_all('tr', recursive = False)
    except: # tr 태그를 검색하지 못한 경우를 처리합니다(다만 크롤링상의 에러일 가능성을 배제할 수 없습니다.)
        print('현재 해당 지역에 영업 중인 약국이 없습니다!')
        sys.exit()

    # HTML에서 원하는 데이터를 추출합니다.
    result = []
    for t in trs:
        if t.td.has_attr('width'):
            name = t.find('td', {'height' : '30'}).text
            script = t.find('script').text
            adr = script.split("');")[0].split("'")[-1]
            w86 = t.find_all('td', {'width' : '86'})
            phone = w86[0].text
            time = w86[1].text.strip()
            next = t.find_next_sibling()
            if next.find('strong'):
                loc = next.find('strong').text.split(': ')[-1][:-1]
                if next.find_next_sibling().find('strong'):
                    remark = next.find_next_sibling().find('strong').text.split(': ')[-1][:-1]
                    result.append([name, adr, phone, time, loc, remark])
                else:
                    result.append([name, adr, phone, time, loc])
            else:
                result.append([name, adr, phone, time])
    print(result)
    return result

def geocoding(address): # 국토교통부 Geocoder API 2.0를 이용하여 도로명주소를 좌표로 변경하는 함수입니다.
    s = HTMLSession()
    address = address.replace('지하 ', '') # 도로명주소에 '지하'가 포함되는 경우 API에서 에러가 발생합니다.
    apikey = 'API키를 입력하세요!'
    r = s.get('http://apis.vworld.kr/new2coord.do?q=' + address + '&apiKey=' + apikey + '&domain=http://map.vworld.kr/&output=json').text
    x, y = list(json.loads(r).values())[1], list(json.loads(r).values())[0]
    return (x, y)

def get_center(result): # 좌표들의 센터를 반환하는 함수입니다.
    temp_x, temp_y = [], []
    for r in result:
        x, y = geocoding(r[1])
        temp_x.append(float(x))
        temp_y.append(float(y))
    avg_x = np.mean(temp_x)
    avg_y = np.mean(temp_y)
    return (avg_x, avg_y)

def main():
    result = crawling_pharm114()
    center = get_center(result)
    m = folium.Map(location = center, zoom_start = 17)
    marker_cluster = MarkerCluster().add_to(m)
    for r in result:
        loc = geocoding(r[1])
        folium.Marker(
            location = loc,
            tooltip = '<strong style="color:blue">' + r[0] + '</strong>BR태그' + 'BR태그'.join(r[2:]),
            icon=folium.Icon(color='red', icon='ok')
        ).add_to(marker_cluster)
    data = io.BytesIO()
    m.save(data, close_file=False)
    app = QtWidgets.QApplication(sys.argv)
    w = QtWebEngineWidgets.QWebEngineView()
    w.setWindowTitle('휴일지킴이 약국')
    w.setHtml(data.getvalue().decode())
    w.resize(800, 600)
    w.show()
    sys.exit(app.exec_())
    return


if __name__ == "__main__":
    main()

 

 

실행시켜보면 결과는 다음과 같습니다!

 

MarkerCluster로 처리하였기 때문에 인근에 위치한 마커들을 군집시켜 클러스터로 표현하며,

 

특정 클러스터를 클릭하면 해당 위치를 확대하여 보여줍니다 ^^

 

img 20210313 222101.png.jpg

 

 

각 마커에 마우스오버하면 다음과 같이 약국 정보를 표시합니다 ^^

 

img 20210313 230000.png.jpg

 

 

위 소스에서 조금 더 보완해야 될 점에 대하여 살펴보면요 ^^

 

 

1. Folium에서 줌 설정

 

Folium에서의 줌과 관련하여 (i) zoom_start를 어떻게 설정할지, (ii) fit_bounds를 사용할지 등 문제가 있는데요~

 

이 부분에 대해서는 일단 제가 거주하는 지역에 최적화된 값을 넣기는 하였는데,

 

보다 universal한 목적으로 사용하려면 보완이 필요할 것 같네요 ^^

 

테스트해보니 타 지역(약국 간의 거리가 멀리 떨어진 지역)에서는 앱을 실행시킨 후에 줌 아웃을 해야 되는 경우가 발생합니다.

 

 

2. 비효율적인 부분

 

Folium에서 center를 맞추기 위하여 Geocoder API로 각 좌표들을 구하고 그 평균을 계산하는 과정이 들어가기 때문에,

 

불필요하게 Geocoder API를 두 번 호출하는 방식으로 작성되어 있습니다 ㅠㅠ

 

 

부디 위 소스가 주말에 급히 약국을 찾으시는 분들께 조금이나마 도움이 되시기를 기원합니다!

 

그럼 남은 주말 뜻깊게 보내시고, 일교차가 큰데 감기 조심하세요~ ^-^

 

제 허접한 소스를 읽어주셔서 감사합니다!!

 

  • profile
    이니스프리 2021.03.14 14:21
    생각해보니 두 개 이상의 페이지에 걸쳐 약국 정보가 올라온 경우를 제가 고려하지 않았네요 ㅠㅠ
    이 부분을 보완하도록 하겠습니다!!
  • ?
    title: 은메달도다 2021.03.14 17:07
    휴일지킴이 약국이란 것도 있군요. 365일 오픈하는 약국도 존재하네요 ㄷㄷ...
  • profile
    이니스프리 2021.03.15 18:27
    옙! 우리나라는 의약분업 제도를 시행 중이기 때문에

    365일 운영하는 의료기관이 있으면 그에 상응하여 365일 운영하는 약국도 있어야 해요~ ㅎㄷㄷ

    (물론 관련 법령상 응급의료시설은 의약분업의 예외에 해당하기는 합니다.)

    그럼 도다 님께서도 저녁식사 맛있게 드세요 ^^
  • ?
    title: 은메달도다 2021.03.15 19:35
    아하 그렇군요!
    즐거운 저녁 되세요~
  • ?
    해롱 2021.03.15 11:01
    와...
  • profile
    이니스프리 2021.03.15 18:30
    오오~ 허접한 소스인데 감사합니다! ^-^
  • profile
    이니스프리 2021.03.15 23:48
    SIR에 개선된 버전을 올렸습니다!
    https://sir.kr/g5_tip/15526

List of Articles
번호 분류 제목 글쓴이 날짜 조회 수
78 코드 폰트를 자동 설치하는 코드 1 네모 2018.07.16 1020
77 코드 파이썬을 이용한 텔레그램 새 글 알림 (허접합니다) 4 이니스프리 2017.11.19 2561
76 코드 파이선 셸에서 실행하면...? 3 제르엘 2018.07.22 595
75 코드 클라이언트단에서 이미지 리사이징 6 file 네모 2018.05.06 1174
74 코드 컴퓨터의 uuid 얻기 5 title: 황금 서버 (30일)humit 2018.01.28 1292
73 코드 잘못 쓰면 컴퓨터가 날아가는 코드 29 제르엘 2018.07.08 1047
72 자료 이게 팔릴까 - Xe/라이믹스 에러페이지 [2017-10-04] 3 file title: 열려라 맛스타의 자물쇠TVJ 2017.10.04 773
71 코드 유튜브에 약간의 기능을 추가 해주는 크롬 확장 프로그램. 11 file Hanam09 2018.01.26 1101
70 코드 엑셀파일 불러서 히스토그램 그려주는 함수 국내산라이츄 2017.08.03 887
69 코드 엑셀 읽어서 그래프 그려주는 함수 1 국내산라이츄 2017.08.03 1576
68 코드 아주 간단한 기초 C++ 6 제르엘 2018.04.21 598
67 자료 소셜XE / 기존 통합 로그인 스킨 V2.2 2 file NoYeah 2017.06.28 1096
66 코드 세린서버에서 시도중인 백업 스크립트 입니다. 4 NoYeah 2017.06.27 856
65 코드 새 글 자동 댓글 스크립트 (AutoHotkey) 9 이니스프리 2017.11.26 3590
64 코드 사이트 서버 이전 (또는 미러링 사이트 구축) 쉽게하는 스크립트 1 NoYeah 2018.01.14 1124
63 코드 브라우저 언어에 따라 다른 폴더를 사용하는 PHP 코드 4 file 네모 2017.10.10 936
62 코드 미완성 받아쓰기 (C) title: 대한민국 국기gimmepoint 2018.04.20 587
61 코드 매우 특이한 버그 9 title: 대한민국 국기gimmepoint 2018.06.05 777
60 자료 링크 파싱 애드온용 스킨 (트위터 스타일) 3 file SNAX 2017.10.03 634
59 코드 도박 중독자를 위한 광고 차단 규칙 file 제르엘 2020.08.21 471
Board Pagination Prev 1 2 3 4 Next
/ 4