?

단축키

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

  1. [Python] 싸이월드 미니홈피 백업 스크립트

  2. [Python] 유튜브 영상을 다운받아 일정 간격으로 캡쳐하여 10장씩 merge하기

  3. [Python] 텔레그램을 이용한 게시판 새 글 알림봇

  4. [Python] 휴일지킴이 약국을 크롤링하여 Folium 지도에 마커로 표시하는 PyQt 윈도우 앱

  5. [XE / Rhymix] Bootstrap 패널 위젯 스타일

  6. [아미나] Dropbox API를 이용한 이미지 호스팅 보드스킨

  7. [아미나] 게시글을 작성하면 ID와 IP로 필터링하여 자동으로 랜덤 댓글을 남기기 (+랜덤 포인트)

  8. [아미나] 네이트 실시간 검색어 순위 위젯 (아미나 캐시 적용)

  9. [아미나] 출석 여부를 나타내는 메인화면 위젯

  10. [오토핫키] 구글 드라이브의 공유링크를 이미지 호스팅을 위한 다이렉트 링크로 바꿔주는 스크립트

  11. [오토핫키] 브라우저를 열어 지난번과 동일한 폴더에 MZK를 다운받고 압축을 네이티브로 해제하는 스크립트

  12. [오토핫키] 특정 사이트에 대한 ping 테스트 결과를 실행시간과 함께 로그 파일로 저장하는 스크립트

  13. [파이썬] Requests를 사용한 네이버 카페 크롤링 - 일정수 이상의 리플이 달린 게시글만 텔레그램 알림

  14. 경험치 현황 위젯

  15. 내가 만든 merge sort

  16. 내가 만든 사칙연산 계산기

  17. 내가 만든 함수 모음집

  18. 내가 만든 함수 모음집 2

  19. 도박 중독자를 위한 광고 차단 규칙

  20. 링크 파싱 애드온용 스킨 (트위터 스타일)

Board Pagination Prev 1 2 3 4 Next
/ 4