파이썬으로 만든 부산 버스앱

개요

안녕하세요. 😁 

오늘은 일상에서 많이 사용하는 버스(Bus)앱을 파이썬으로 만들어 보려 합니다.

제 고향인 부산의 버스정보를 표시하도록 구성하였으며 파이썬 언어, PyQt6를 이용해 제작되었습니다.

먼저 결과물을 살펴보면,

 

앱에서 버스정류장 이름, 버스번호 등을 이용해 검색하고 관련정보가 출력되는 모습을 볼 수 있는데 저런 정보들은 어디서 얻어 올까요.

바로 공공데이터 포털이라는 대한민국전자정부 누리집 사이트입니다.

공공데이터포털 홈페이지

정부에서 수집된 공공정보(날씨, 교통, 의료, 부동산, 금융 등) 가 총 망라되어 있으며, 제공되는 데이터 종류는 정적인 파일데이터(Excel, csv 등)실시간 API데이터(Request)로 크게 분류해 볼 수 있습니다.

부산버스 검색

버스 정보는 실시간으로 위치 등이 요구되므로 실시간 API 데이터이고, 회원가입 후 활용신청하면 누구나 정보를 이용할 수 있습니다.

다른 지방자치단체나 광역시도 마찬가지로 검색 후 활용신청하면 됩니다.

검색 후 활용신청 클릭

대부분의 공공기반 앱 (날씨, 교통, 의료, 교육) 등이 이 곳을 통해 정보를 가져옵니다.

예를 들면 기상청의 날씨정보를 받아와 날씨앱이 정보를 제공하는 것 처럼요.

회원가입과 정보이용은 모두 무료이므로 여러분도 얼른 가입하고 활용신청 후 아래 예시를 진행하며 공부해 보시기 바랍니다.

마이페이지 활용신청확인

대략적인 개념이 전달되었다면 이제 예제 코드에 대해 설명해 보겠습니다.


개발환경

  • Windows 11 64bit, MS Visual Studio 2022
  • Python 3.11, PyQt6, requests


개발 순서

1. 공공데이터포털 회원가입

2. 부산광역시 부산버스정보시스템 활용신청

3. 신청 후 "OpenAPI활용가이드_부산버스정보시스템_v2.0" 문서 참조

4. 문서에서 서버 URL 및 요청, 응답 프로토콜 파악

5. REST API를 이용한 Data Query, Response 처리

6. Response Data(XML) 시각화(UI)

가장 중요한 부분은 "OpenAPI활용가이드_부산버스정보시스템_v2.0" 문서를 참조해 Rest Server의 주요 정보(URL, XML Tag, query, response)에 대한 이해가 선행되어야만 요청 후 버스정보에 대한 응답처리가 가능합니다.

아래는 해당 문서에서 제공되는 목차입니다.

부산버스정보시스템_v2.0 목차

참고로 예제 앱은 목차의 상세기능 중 1, 2, 3번에 대한 기능을 제공합니다.

 

소스코드

총 6개의 파이썬 파일(*.py) 로 구성

아래 6개 파이썬 파일을 같은 경로에 두고 main.py (시작파일)를 실행.

(모든 소스코드는 직접 작성, 코드설명은 chatGPT 도움 작성)

 

1. main.py

메인 위젯, 전체 코드의 시작점, HOME 역할

(3개의 탭 위젯으로 구성)

main.py

from PyQt6.QtWidgets import QApplication, QTabWidget
from PyQt6.QtCore import Qt
import sys

from bus_stop_list import BusStopList
from bus_node_info import BusNodeInfo
from bus_nodestop_info import BusNodeStopInfo

class Form(QTabWidget):

    def __init__(self):
        super().__init__()
        self.setWindowTitle('부산 버스 정보')
        self.resize(800,600)

        self.tab = []
        self.tab.append(BusStopList())
        self.tab.append(BusNodeInfo(self))
        self.tab.append(BusNodeStopInfo())

        for t in self.tab:
            self.addTab(t, t.windowTitle())

    def OnTabChange(self, tabId, txt):
        self.setCurrentIndex(tabId)
        self.tab[tabId].OnSearch(txt)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Form()
    w.show()
    sys.exit(app.exec())


이 코드는 PyQt6를 사용하여 부산 버스 정보를 보여주는 간단한 탭형 애플리케이션을 구성하는 Main함수 (Main Activity)에 해당하는 부분. QTabWidget을 상속받은 Form 클래스에서 각기 다른 기능을 수행하는 3개의 탭을 정의.

BusStopList, BusNodeInfo, BusNodeStopInfo라는 세 개의 클래스를 각각 탭에 추가하여, 탭을 클릭할 때마다 해당 기능을 제공하도록 설정.

self.tab 리스트에 각 탭 인스턴스를 저장하고 addTab() 메서드를 통해 QTabWidget에 추가.

OnTabChange 메서드는 특정 탭 인덱스로 전환하고, 해당 탭에서 검색 기능을 수행하도록 하는 함수이며 tabId와 검색어 txt를 받아 OnSearch() 메서드를 호출.

 

2. bus_stop_list.py

지역정보로 정류소 정보 조회 (Ex. 명지국제신도시 검색)

(정류소 ID, 명칭, 번호, 위,경도 좌표 등)

"부산버스정보시스템_v2.0 문서" 상세기능내역 중 1번에 해당.

bus_stop_list.py

from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QTreeWidget,
                             QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit,QTreeWidgetItem,
                             QHeaderView)
from PyQt6.QtCore import Qt, pyqtSignal, QEvent
import sys

from RestAPI import sendRequest
from threading import Thread

class BusStopList(QWidget):

    # user defined signal
    update_signal = pyqtSignal(str, list)

    def __init__(self, parent=None):
        super().__init__()
        self.parent = parent
        self.setWindowTitle('정류소정보 조회')
        label = QLabel('정류소 지역 검색')
        self.le = QLineEdit()
        btn = QPushButton('검색')

        hbox = QHBoxLayout()
        hbox.addWidget(label)
        hbox.addWidget(self.le)
        hbox.addWidget(btn)

        vbox = QVBoxLayout()
        vbox.addLayout(hbox)
        self.tree = QTreeWidget()
        vbox.addWidget(self.tree)

        self.setLayout(vbox)

        # signal
        btn.clicked.connect(self.OnSearch)
        self.update_signal.connect(self.updateTree)
        QApplication.instance().installEventFilter(self)
        self.tree.itemDoubleClicked.connect(self.OnDbClick)

    def eventFilter(self, obj, e):
        if e.type() == QEvent.Type.KeyPress and (e.key() == Qt.Key.Key_Return or e.key() == Qt.Key.Key_Enter):        
            if obj==self.le:
                self.OnSearch()
                return True
        return super(QWidget, self).eventFilter(obj, e)

    def OnSearch(self):
        place = self.le.text()

        URL = 'http://apis.data.go.kr/6260000/BusanBIMS/busStopList?'
        POS = f'bstopnm={place}'        
        KEY = '&serviceKey=공공데이터포털키입력'

        MSG = URL + POS + KEY

        self.t = Thread(target=self.threadFunc, args=(place, MSG))
        self.t.start()

    def updateTree(self, place, result):
        self.le.clear()
        label = {'bstopid':'정류소 ID', 'bstopnm':'정류소명','arsno':'정류소번호', 
                 'gpsx':'GPS 경도', 'gpsy':'GPS 위도', 'stoptype':'구분'}
        row = len(result)
        col = len(label)

        self.tree.clear()
        self.tree.setColumnCount(col)
        self.tree.setHeaderLabels(label.values())

        root = QTreeWidgetItem(self.tree)
        root.setText(0, place)

        for r in range(row):
            # 두 딕셔너리 비교후 key가 빠진 딕셔너리 생성
            diff = {k:'-' for k in label if not k in result[r]}
            # 빠진 딕셔너리와 기준 딕셔너리 병합 (**은 unpack, 딕셔너리 풀기)
            merged = {**result[r], **diff}
            # 병합된 딕셔너리 순서 정렬
            sortedDict = {k:merged[k] for k in label if k in merged}
            item = QTreeWidgetItem(root, sortedDict.values())


        root.setExpanded(True)
        self.tree.header().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)

    def OnDbClick(self, item, col):
        # check child item
        if item.childCount()==0:
            x = item.text(3)
            y = item.text(4)
            from Google_Map import Dialog
            dlg = Dialog(x, y)
            dlg.exec()


    def threadFunc(self, place, msg):
        result = sendRequest(msg)
        #print(result)
        self.update_signal.emit(place, result)

이 코드는 PyQt6로 작성된 BusStopList 클래스이며, 특정 지역의 버스 정류장 정보를 조회하여 결과를 트리 형태로 표시하는 위젯.

앞서 설명한 Main Activity의 탭 중 첫번째에 해당.

GUI 구성

텍스트 입력 필드(QLineEdit), 검색 버튼(QPushButton), 트리 위젯(QTreeWidget)을 배치하여 사용자가 지역을 입력하고 검색할 수 있는 인터페이스를 제공.

트리 위젯은 검색된 정류장 데이터를 트리 구조로 표시.

검색 기능

사용자가 검색 버튼을 클릭하거나 엔터 키를 누르면 OnSearch 메서드가 호출. 이 메서드는 특정 URL로 검색 요청을 보내기 위해 threadFunc 메서드를 새로운 스레드로 실행.

Request 후 Response 까지 지연이 발생되므로 별도의 실행흐름(쓰레드)을 생성해 비동기 처리.

스레드 사용

OnSearch 메서드에서 생성된 스레드는 sendRequest 함수(외부 API 요청 함수)를 통해 데이터를 비동기적으로 수신. 요청이 완료되면 사용자 정의 시그널 update_signal을 통해 updateTree 메서드로 데이터전달.

트리 업데이트

updateTree 메서드는 트리 위젯을 초기화하고, API에서 반환된 데이터를 정렬하여 트리에 추가. 정류장 정보가 부족할 경우, 빠진 정보는 '-'로 대체.

더블 클릭 이벤트

트리 항목을 더블 클릭하면 OnDbClick 메서드가 실행되며, 선택된 정류장의 GPS 좌표를 이용해 Google Map 지도를 다이얼로그(Dialog 클래스)에서 표시.


3. bus_node_info.py

버스번호로 정보 조회 (Ex. 171번)

(노선 ID, 버스종류, 기점명, 종점명, 첫차,막차시간, 배차간격등)

"부산버스정보시스템_v2.0 문서" 상세기능내역 중 2번에 해당.

노선정보 조회탭

from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit, QVBoxLayout, QHBoxLayout,
                             QPushButton, QTreeWidget, QTreeWidgetItem, QHeaderView, QMessageBox,
                             QMenu)
from PyQt6.QtCore import Qt, QEvent, pyqtSignal, QRegularExpression, QSignalMapper
from PyQt6.QtGui import QRegularExpressionValidator, QAction
import sys

from RestAPI import sendRequest
from threading import Thread

class BusNodeInfo(QWidget):

    update_signal   = pyqtSignal(str, str, list)
    error_signal    = pyqtSignal(str, str)
    tab_signal      = pyqtSignal(int, str)

    def __init__(self, parent=None):
        super().__init__()
        self.parent = parent
        self.setWindowTitle('노선정보 조회')
        label1 = QLabel('버스번호')
        self.nodeNo = QLineEdit()
        rx = QRegularExpression("-?\\d{1,4}");
        self.nodeNo.setValidator(QRegularExpressionValidator(rx))
        label2 = QLabel('노선ID')
        self.nodeId = QLineEdit()
        btn = QPushButton('노선검색')

        hbox = QHBoxLayout()
        hbox.addWidget(label1)
        hbox.addWidget(self.nodeNo)
        hbox.addWidget(label2)
        hbox.addWidget(self.nodeId)
        hbox.addWidget(btn)

        vbox = QVBoxLayout()
        vbox.addLayout(hbox)
        self.tree = QTreeWidget()
        vbox.addWidget(self.tree)

        self.setLayout(vbox)
        # context menu
        label = ('노선정류소 조회', '예약 1', '예약 2')
        mapper = QSignalMapper(self)
        actions = []
        for k, v in enumerate(label):
            act = QAction(v, self)
            mapper.setMapping(act, k)
            act.triggered.connect(mapper.map)
            actions.append(act)

        self.menu = QMenu()
        self.menu.addActions(actions)

        # signal
        btn.clicked.connect(self.OnSearch)        
        self.update_signal.connect(self.updateTree)
        self.error_signal.connect(self.displayError)
        self.tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.tree.customContextMenuRequested.connect(self.OnContextMenu)
        self.tab_signal.connect(self.parent.OnTabChange)
        mapper.mappedInt.connect(self.OnClickContextMenu)
        QApplication.instance().installEventFilter(self)

    def eventFilter(self, obj, e):
        if e.type() == QEvent.Type.KeyPress and (e.key() == Qt.Key.Key_Return or e.key() == Qt.Key.Key_Enter):
            if obj==self.nodeId or obj==self.nodeNo:
                self.OnSearch()
                return True
        return super(QWidget, self).eventFilter(obj, e)

    def OnSearch(self):
        busNum = self.nodeNo.text()
        nodeId = self.nodeId.text()

        URL = 'http://apis.data.go.kr/6260000/BusanBIMS/busInfo?'
        NO  = f'lineno={busNum}'
        ID  = f'&lineid={nodeId}'
        KEY = '&serviceKey=공공데이터포털키입력'

        MSG = URL + NO + ID + KEY

        self.t = Thread(target=self.threadFunc, args=(busNum, nodeId, MSG))
        self.t.start()

    def updateTree(self, no, id, result):
        self.nodeNo.clear()
        self.nodeId.clear()
        label = {'lineid':'노선ID', 'buslinenum':'버스번호', 'bustype':'종류', 'startpoint':'기점명', 'endpoint':'종점명',
                 'companyid':'회사명', 'firsttime':'첫차시간', 'endtime':'막차시간', 
                 'headwaypeak':'배차간격(출퇴근)', 'headwaynorm':'배차간격(일반)', 'headwayholi':'배차간격(휴일)'}

        row = len(result)
        col = len(label)

        self.tree.clear()
        self.tree.setColumnCount(col)
        self.tree.setHeaderLabels(label.values())

        root = QTreeWidgetItem(self.tree, type=0)
        root.setText(0, f'Bus No:{no}')

        for r in range(row):
            # 두 딕셔너리 비교후 key가 빠진 딕셔너리 생성
            diff = {k: '-' for k in label if not k in result[r]}
            # 빠진 딕셔너리와 기준 딕셔너리 병합
            merged = {**result[r], **diff}
            # 병합된 딕셔너리 순서 정렬
            sortedDict = {k: merged[k] for k in label if k in merged}
            item = QTreeWidgetItem(root, sortedDict.values(), type=1)

        root.setExpanded(True)
        self.tree.header().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)

    def displayError(self, title, content):
        QMessageBox.information(self, title, content, QMessageBox.StandardButton.Ok)

    def OnContextMenu(self, pt):
        item = self.tree.itemAt(pt)
        if item.type() == 1:
            # get node id
            self.id = item.text(7)
            self.menu.exec(self.tree.mapToGlobal(pt))

    def OnClickContextMenu(self, idx):
        self.tab_signal.emit(2, self.id)


    def threadFunc(self, no, id, msg):
        result = sendRequest(msg)
        #print(result)
        if result:
            self.update_signal.emit(no, id, result)
        else:
            self.error_signal.emit('오류', f'{no}번 버스는 존재하지 않는 번호입니다')

이전코드와 유사성이 많음.

왜냐하면 정보를 검색하고 QTreeWidget에 쓰는 동일한 패턴이기 때문.

이 코드는 BusNodeInfo Class PyQt6 위젯으로, 부산 지역 특정 버스 노선의 상세 정보를 검색하여 트리 형태로 표시.

GUI 구성

버스번호와 노선ID 입력 필드, 그리고 노선검색 버튼을 배치해 사용자가 특정 버스 노선의 정보를 검색할 수 있는 인터페이스를 제공. 검색 결과는 트리 위젯(QTreeWidget)에 표시.

검색 기능

사용자가 노선검색 버튼을 클릭하거나 입력 필드에서 엔터 키를 누르면 OnSearch 메서드가 호출. 이 메서드는 API 요청 메시지를 구성하여 threadFunc 스레드 함수로 전달.

스레드

OnSearch 메서드에서 생성된 스레드는 외부 API 호출을 비동기적으로 처리하여 UI의 응답성을 유지. 요청 결과가 있으면 update_signal을 통해 updateTree 메서드에 데이터를 전달하고, 실패 시 error_signal을 통해 에러 메시지를 출력.

트리 업데이트

updateTree 메서드는 트리 위젯을 초기화하고, API 결과를 순서에 맞게 트리에 추가. 필요한 데이터가 없는 경우는 '-'로 대체하여, 트리 항목에 표시.

컨텍스트 메뉴

트리 항목을 오른쪽 클릭하면 컨텍스트 메뉴가 열리며, 노선정류소 조회 등의 옵션을 선택할 수 있음. 메뉴 옵션을 클릭하면 해당 노선 ID가 부모 클래스의 특정 탭(tab_signal)으로 전달.

오류 처리

API 요청 결과가 없을 때 displayError 메서드를 통해 에러 메시지를 팝업 창으로 표시하여 사용자에게 통보.


4. bus_nodestop_info.py

노선 ID로 세부 정보 조회 (Ex. 노선ID 5200171000)

(노선 전체 정류소명, 평균시간, 버스차량번호, 버스위치 등)

"부산버스정보시스템_v2.0 문서" 상세기능내역 중 3번에 해당.

노선 정류소 조회탭

from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit, QVBoxLayout, QHBoxLayout,
                             QPushButton, QTreeWidget, QTreeWidgetItem, QHeaderView, QMessageBox)
from PyQt6.QtCore import Qt, QEvent, pyqtSignal, QRegularExpression
from PyQt6.QtGui import QRegularExpressionValidator
import sys

from RestAPI import sendRequest
from threading import Thread

class BusNodeStopInfo(QWidget):

    update_signal = pyqtSignal(str, list)
    error_signal = pyqtSignal(str, str)

    def __init__(self, parent=None):
        super().__init__()
        self.parent = parent
        self.setWindowTitle('노선정류소 조회')
        rx = QRegularExpression("-?\\d{1,10}");
        label = QLabel('노선ID')
        self.nodeId = QLineEdit()
        self.nodeId.setValidator(QRegularExpressionValidator(rx))
        btn = QPushButton('노선정류소 검색')

        hbox = QHBoxLayout()
        hbox.addWidget(label)
        hbox.addWidget(self.nodeId)
        hbox.addWidget(btn)

        vbox = QVBoxLayout()
        vbox.addLayout(hbox)
        self.tree = QTreeWidget()
        vbox.addWidget(self.tree)

        self.setLayout(vbox)
        # signal
        btn.clicked.connect(self.OnSearch)
        QApplication.instance().installEventFilter(self)
        self.update_signal.connect(self.updateTree)
        self.error_signal.connect(self.displayError)

    def eventFilter(self, obj, e):
        if e.type() == QEvent.Type.KeyPress and (e.key() == Qt.Key.Key_Return or e.key() == Qt.Key.Key_Enter):
            if obj==self.nodeId:
                self.OnSearch()
                return True
        return super(QWidget, self).eventFilter(obj, e)

    def OnSearch(self, id=None):
        if id:
            self.nodeId.setText(id)

        nodeId = self.nodeId.text()

        URL = 'http://apis.data.go.kr/6260000/BusanBIMS/busInfoByRouteId?'
        ID  = f'lineid={nodeId}'
        KEY = '&serviceKey=공공데이터포털키입력'

        MSG = URL + ID + KEY

        self.t = Thread(target=self.threadFunc, args=(nodeId, MSG))
        self.t.start()

    def updateTree(self, id, result):
        self.nodeId.clear()
        label = {'bstopidx':'순번', 'bstopnm':'정류소명', 'avgym':'평균시간', 'nodeid':'노드ID',
                'lineno':'노선번호', 'direction':'방향', 'gpsym':'GPS 연결시간', 'carno':'차량번호',
                'lat':'(위도)GPS Y', 'lin':'(경도)GPS X', 'nodekn':'정류장타입', 'arsno':'정류소번호',
                'rpoint':'회차점', 'lowplate':'일반(0),저상(1)'}

        row = len(result)
        col = len(label)

        self.tree.clear()
        self.tree.setColumnCount(col)
        self.tree.setHeaderLabels(label.values())

        root = QTreeWidgetItem(self.tree)
        root.setText(0, f'노선 ID:{id}')

        for r in range(row):
            # 두 딕셔너리 비교후 key가 빠진 딕셔너리 생성
            diff = {k: '-' for k in label if not k in result[r]}
            # 빠진 딕셔너리와 기준 딕셔너리 병합
            merged = {**result[r], **diff}
            # 병합된 딕셔너리 순서 정렬
            sortedDict = {k: merged[k] for k in label if k in merged}
            item = QTreeWidgetItem(root, sortedDict.values())

        root.setExpanded(True)
        self.tree.header().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)

    def displayError(self, title, content):
        QMessageBox.information(self, title, content, QMessageBox.StandardButton.Ok)

    def threadFunc(self, id, msg):
        result = sendRequest(msg)
        #print(result)
        if result:
            self.update_signal.emit(id, result)
        else:
            self.error_signal.emit('오류', f'{id}번 노선 ID는 존재하지 않는 번호입니다')

이전 코드들과 유사하므로 일반적인 설명(데이터 요청, 수신, 업데이트 과정)은 생략하고 수신된 버스 정보 데이터를 어떻게 파싱하는지 설명해 보겠습니다.

아래는 수신된 XML 데이터를 파이썬 딕셔너리로 변환한 모습입니다.

응답 XML 데이터

모든 정류장 정보가 다 포함되어 있어 양이 많아 복잡해 보이지만 자세히 살펴보면 단순한 딕셔너리 구조입니다.

그런데 주어진 result 데이터는 버스 노선의 모든 정류소 정보를 담고 있으며, 각 정류소의 정보는 일부 필드만 포함하는 경우가 있습니다.

(서버에서 일부 XML 필드가 필요없다고 판단한 경우)

예를 들어, 모든 정류소가 carno(차량번호), avgym(평균시간) 또는 gpsym(GPS 연결시간) 필드를 가지고 있지는 않습니다.

왜냐하면 수십군데의 정류소 중 실제 버스가 위치하는 정류장의 경우만 차량번호가 제공됩니다.

(버스 노선 중 투입된 버스 수량은 매 정류소마다 버스가 존재할 정도로 촘촘하지 않기 때문)

이 불완전한 데이터에서 누락된 필드는 '-'로 채우고, 원하는 순서대로 정렬해야만 트리에 표시할 수 있습니다.

이 과정이 코드 81~88번 라인에 구현되어 있습니다.

diff 딕셔너리 생성

label 딕셔너리의 키들('bstopidx', 'bstopnm', 등)을 기준으로 반복하면서, 현재 정류소 정보(result[r])에 없는 키들을 찾습니다.

찾은 키들은 '-' 값으로 초기화한 새로운 딕셔너리인 diff에 추가합니다.

예를 들어, 만약 result[r]에 'lineno' 키가 없다면, diff 딕셔너리에는 {'lineno': '-'}가 추가됩니다. 이로써 result[r]에 없는 모든 필드는 '-' 값으로 채워진 상태가 됩니다.

merged 딕셔너리 생성

result[r](현재 정류소 정보)와 diff(누락된 필드를 '-'로 채운 딕셔너리)를 병합합니다. 이로써 merged 딕셔너리는 모든 필드를 포함하며, 누락된 필드는 '-' 값으로 채워집니다. 

(**은 파이썬에서 딕셔너리같은 hashable 자료를 unpacking 하는데 사용)

sortedDict 딕셔너리 생성

label의 키 순서에 맞춰 merged 딕셔너리를 정렬하여 sortedDict를 만듭니다. 이는 label에 정의된 순서대로 데이터를 보여주기 위해 필요한 과정입니다.

이로 인해, 트리 위젯에 표시되는 필드 순서가 label 딕셔너리에 정의된 순서와 동일하게 정렬됩니다.

트리 아이템 생성 및 추가

sortedDict.values()를 사용해 QTreeWidgetItem을 생성하여 트리 위젯의 root 아이템에 추가합니다. 여기서 sortedDict.values()는 label에 맞춰 정렬된 값들만을 반환하므로, 화면에 보기 좋게 정렬된 상태로 데이터를 표시할 수 있습니다.

결과적으로, 이 코드는 데이터가 누락되었을 때 '-'로 표시하고, 필드를 원하는 순서대로 정렬하여 QTreeWidget에 아이템으로 추가합니다.


5. RestAPI.py

REST API 송수신 처리 모듈, XML Parser

import requests
import xml.etree.ElementTree as et

def sendRequest(msg):
    response = requests.get(msg)
    txt = response.content.decode('utf-8')
    result = xmlParser(txt, 'item')
    return result

def xmlParser(txt, target=''):
    lst = []
    root = et.fromstring(txt)
    for tag in root.iter(tag=target):
        temp = {}
        for k, subtag in enumerate(tag.iter()):
            if tag != subtag:
                temp[subtag.tag]=subtag.text
        lst.append(temp)
    return lst

이 코드는 sendRequestxmlParser라는 두 개의 함수로 구성.

sendRequest(msg)

주어진 URL msg로 Http GET 요청을 보내 XML 데이터를 수신하고, 수신한 데이터를 xmlParser 함수로 파싱합니다.

xmlParser의 결과값(리스트 형태의 파싱된 데이터)을 반환합니다.

xmlParser(txt, target='')

XML 문자열 txt에서 target으로 지정된 태그(item)를 찾고, 각 item 태그 내의 데이터를 딕셔너리 형태로 변환해 리스트로 만듭니다.

최종적으로 lst에 각 item의 정보를 담아 반환.


6. Google_Map.py 

정류소 위치정보를 지도에 표시

(Google Maps Static API 사용, Subscription Key 필요)

Google_Map.py

from PyQt6.QtWidgets import QDialog, QComboBox, QLabel, QVBoxLayout, QFrame
from PyQt6.QtGui import QPixmap
from PyQt6.QtCore import Qt
import requests

class Dialog(QDialog):

    def __init__(self, x, y):
        super().__init__()
        self.setWindowTitle('Google Static Map')
        self.x = float(x)
        self.y = float(y)

        self.cmb = QComboBox()
        zoom = [str(i) for i in range(1, 21)]
        self.cmb.addItems( zoom )
        self.cmb.setCurrentIndex(18)
        self.label = QLabel()
        self.label.setFrameShape(QFrame.Shape.Box)
        self.label.resize(600,600)

        vbox = QVBoxLayout()
        vbox.addWidget(self.cmb)
        vbox.addWidget(self.label)

        self.setLayout(vbox)

        self.googleMap(self.x, self.y)

        #signal
        self.cmb.currentTextChanged.connect(self.OnZoomChanged)

    def OnZoomChanged(self, txt):
        self.googleMap(self.x, self.y, txt)


    def googleMap(self, x, y, z=19):
        URL = 'https://maps.googleapis.com/maps/api/staticmap?'
        CEN = f'center={y:0.6f},{x:0.6f}'
        SZ  = '&size=640x640&scale=2'
        ZM  = f'&zoom={z}'
        MK  = f'&markers=color:0xFF0000|label:B|{y:0.6f},{x:0.6f}'
        MT  = '&maptype=hybrid'
        KEY = '&key=구글 Maps Static API Key 입력'

        MSG = URL + CEN + SZ + ZM + MK + MT + KEY

        response = requests.get(MSG)

        img = QPixmap()
        img.loadFromData(response.content)

        w = self.label.width()
        h = self.label.height()
        img = img.scaled(w, h, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
        self.label.setPixmap(img)

구글맵 API 사용법은 이전 게시물 or Google Map 개발자 가이드 참조 바랍니다. 

이상으로 모든 설명을 마칩니다. 감사합니다.

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기