1. 프로젝트 기간: 2023.06.12 ~

 

2. 프로젝트 개요

  - 지방행정제재·부과금의 징수 등에 관한 법률: 과징금, 이행강제금, 부담금, 변상금, 그 밖의 조세 외의 금전의 체납처분 절차를 규정

  - 이 법의 특징은 다른 개별법의 조문에서 '지방행정제재·부과금의 징수 등에 관한 법률을 준용한다'라고 규정하는 경우에 적용

  - 현재는 이 법률을 준용하는 개별법을 직접 검색하고 재·개정문을 직접 확인하는 중이라 누락 및 번거로움이 발생

  - 국가법령정보센터(현행 법령), 법제처 누리집(입법예고문), 국회 의안정보시스템(의원 발의 법률)의 실시간 관리 자동화 필요

 

3. 프로젝트 진행 과정

import pandas as pd
import numpy as np
from datetime import datetime
from datetime import timedelta
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
import os
from bs4 import BeautifulSoup
import requests
import fitz
from PyQt5 import QtCore, GtGui, QtWidgets

  1) 국회 의안정보시스템 크롤링

    - 국회 의안정보시스템의 의안 검색 메뉴에서 '지방행정제재·부과금의 징수 등에 관한 법률' 검색

    - 검색 결과로 해당 법률을 포함하여 의원이 발의한 의안이 검색됨

    - 위 페이지에서 각 의원 발의 법률의 법률명, 제안일, 의결일, 의결 결과를 크롤링하여 정리

# 검색어를 검색한 페이지의 url
# PageSizeOption=100을 변수로 주어 한 페이지에 100개의 게시글까지 나타나도록 설정
url = 'https://likms.assembly.go.kr/bill/BillSearchProposalResult.do?query=지방행정제재ㆍ부과금의+징수+등에+관한+법률&PageSizeOption=100'

# BeautifulSoup를 사용해 해당 url의 html 문서 가져오기
soup = BeautifulSoup(requests.get(url).text, 'html.parser')

# 크롤링하려는 대상인 각 게시글의 제목의 클래스명을 전부 선택하여 추출
result = soup.select('p.ti a')

# 제목에서 추출하려는 대상을 리스트 형식으로 생성
법령명 = []
제안일 = []
의결일 = []
결과 = []

# 각 제목에서 법령명, 제안일, 의결일, 의결 결과를 분리하여 각 리스트에 추가
for i in range(len(result)):
	# 법령명은 제목 바로 앞의 'ㆍ'부터 '제안일'이라는 글씨가 나오기 바로 전 까지
    법령명.append(str(result[i])[str(result[i]).find('ㆍ') + 1 : str(result[i]).find('제안일')])
    # 제안일은 제안일이라는 글씨가 시작되고 6번째부터 '의결일'이라는 글씨가 시작되기 2번째전까지
    제안일.append(str(result[i])[str(result[i]).find('제안일') + 6 : str(result[i]).find('의결일') - 2])
    # 의결일은 '의결일'이라는 글씨가 시작되고 6번째부터 '의결일'이라는 글씨가 시작되고 12번째까지
    의결일.append(str(result[i])[str(result[i]).find('의결일') + 6 : str(result[i]).find('의결일') + 16])
    # 결과는 '의결일'이라는 글씨가 시작되고 18번째부터 마지막에서 5번째전까지
    결과.append(str(result[i])[str(result[i]).find('의결일') + 18 : -5])

# 각 리스트에 추가해둔 전체 데이터를 데이터프레임으로 결합
의안 = pd.DataFrame({'법령명' : 법령명, '제안일' : 제안일, '의결일' : 의결일, '결과' : 결과})

# 제안일과 의결일을 날짜 형식으로 바꾸지 위해 '.'로 연월일이 나눠져 있던 것을 '-'로 나뉘도록 변경
의안[['제안일', '의결일']] = 의안[['제안일', '의결일']].replace('.', '-')
# 제안일과 의결일을 datetime 형식으로 변경
의안['제안일'] = 의안['제안일'].apply(lambda x: pd.to_datetime(x))
# 의결일 중 의결이 아직 되지 않은 것은 </a>로 되어 있고 이는 '미정'이라는 글자로 변경
의안['의결일'] = 의안['의결일'].apply(lambda x: pd.to_datetime(x) if x != ' </a>' else '미정')

# 제안일과 의결일을 다시 str 형식으로 변경
# datetime 형식으로 두면 00:00:00이라는 시간까지 포함되어 출력되므로 str 형식으로 깔끔하게 정리
의안 = 의안.astype({'제안일':'str', '의결일':'str'})
# 의결일은 '미정'이라는 글씨를 따로 처리('미정'은 그대로 '미정', 미정이 아니면 str로 바꾼 뒤 마지막에서 9번째 자리 글자까지 추출)
의안['의결일'] = 의안['의결일'].apply(lambda x: str(x)[:-9] if x != '미정' else '미정')

# 결과도 결과가 나오지 않았다면 빈 셀로 남아있으므로 이 빈 셀을 '미정'으로 변경
의안['결과'] = 의안['결과'].apply(lambda x: '미정' if x == '' else x)

# 정리한 데이터프레임을 엑셀 파일로 저장
의안.to_excel('의안목록.xlsx', index = False)

 

  2) 법제처 누리집 크롤링

    - 법제처 누리집에서 법이 입법되기 전 예고하는 입법예고문 크롤링

    - 입법예고문은 pdf 형식으로 올라가 있으므로 pdf 형식의 입법예고문을 최신순으로 일정 개수 다운 받고 텍스트를 추출하여 '지방행정제재·부과금의 징수 등에 관한 법률'이라는 텍스트가 있는지 여부를 검색

    - 입법예고문에 키워드가 있다면 해당 입법예고문의 제목을, 그렇지 않으면 '해당없음'을 출력

# 다운받은 입법예고문을 저장할 폴더 생성 후 경로 지정
# '입법예고문'이라는 이름의 폴더가 없다면 새로 생성
if not os.path.exists("입법예고문"):
    os.mkdir("입법예고문")
# '입법예고문'폴더로 경로 지정
os.chdir("입법예고문")

# 1페이지부터 4페이지까지 크롤링
for i in range(1, 5):
	# '입법예고문' 폴더 내에 각 페이지 마다 '입법예고문_1page', '입법예고문_2page' 등 폴더 생성
    if not os.path.exists(f"입법예고문_{i}page"):
        os.mkdir(f"입법예고문_{i}page")
    # 경로는 새로 만든 폴더로 이동
    os.chdir(f"입법예고문_{i}page")
    
    # url의 currentPage 변수에 페이지 번호를 전달하여 각 페이지를 불러오도록 함
    # pageCnt 변수를 10 이상의 수로 조정해도 됨
    url = f'https://www.moleg.go.kr/lawinfo/makingList.mo?mid=a10104010000&currentPage={i}&pageCnt=10&keyField=&keyWord=&stYdFmt=&edYdFmt=&lsClsCd=&cptOfiOrgCd='
	
    # 위 url의 html 문서 불러오기
    soup = BeautifulSoup(requests.get(url).text, 'html.parser')
    
    # 입법예고문의 pdf 파일 다운로드 버튼을 눌러 다운받는 방식
    # pdf 파일 다운로드 버튼이 첫번째인 경우도 있고 두번째인 경우, 그 외 경우 등이 있어 예외처리
    try:
    	# html 문서에서 클래스명이 'wrap title' div를 찾기(각 페이지의 게시글 url)
        for href in soup.find_all('div', 'wrap title'):
        	# 각 게시글 url을 지정하여 html 문서 불러오기
            page_url = "https://www.moleg.go.kr" + href.find('a')['href'].replace("¤", '&curren')
            page_soup = BeautifulSoup(requests.get(page_url).text, 'html.parser')
            # 문서 다운로드 버튼을 찾아 버튼의 이름을 탐색
            title = page_soup.find('div', 'tb_contents').find('ul').find('a').text
            # 버튼 이름에 pdf가 포함되어 있으면 pdf 파일임을 확인하고 클릭
            if 'pdf' in title:
                title = page_soup.find('div', 'tb_contents').find('ul').find('a').text[:title.find('pdf') + 3]
                file_url = page_soup.find('div', 'tb_contents').find('ul').find('a')['href']
                # 해당 버튼을 눌러 pdf 다운로드 페이지로 이동(이동하면 자동 다운로드 됨)
                file = requests.get(file_url)
                # PyMuPDF 라이브러리를 사용하여 pdf파일의 텍스트 추출
                with open(title, 'wb') as f:
                    f.write(file.content)
                    pdf = fitz.open(title)
                    cnt = 0
                    # pdf의 각 페이지에서 키워드를 검색
                    # 없으면 -1, 있으면 개수가 출력됨
                    for page in pdf:
                        text = page.get_text()
                        result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                        # 모든 페이지에 키워드가 없어 모든 결과가 -1이라면 각 결과를 -한 결과는
                        # pdf의 모든 페이지 개수와 동일할 것
                        cnt -= result
                # 모든 페이지에 키워드가 없어 모든 결과가 -1이라면 각 결과를 -한 결과는
                # pdf의 모든 페이지 개수와 동일할 것
                # 따라서 둘을 비교하여 각 pdf에 키워드가 있는지 확인
                # 키워드가 없으면 '해당없음'
                # 키워드가 있으면 입법예고문 pdf의 제목 출력
                if cnt == len(pdf):
                	print('해당없음')
                	# 키워드가 없으면 pdf를 삭제하는 코드이지만 오류 해결 못함
                    # os.unlink(os.getcwd() + title)
                else:
                    print(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
			
            # pdf 파일의 다운로드 버튼 위치가 두번째인 경우 html 문서에서 찾을 버튼을 두번째 버튼으로 변경
            # 이하 동일
            elif 'pdf' in page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text:
                title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text
                title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text[:title.find('pdf') + 3]
                print(title)
                file_url = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1]['href']
                file = requests.get(file_url)
                with open(title, 'wb') as f:
                    f.write(file.content)
                    pdf = fitz.open(title)
                    for page in pdf:
                        text = page.get_text()
                        result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                        cnt -= result
                    if cnt == len(pdf):
                        print("해당없음")
                        self.molegText.append("해당없음")
                        # os.unlink(os.path(os.getcwd() + title))
                    else:
                        print(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                        self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                        self.law_count += 1
            # 버튼의 위치 이외에 pdf 파일이 존재하지 않는 등의 경우에는 생략하는 것으로 예외 처리
            else:
                continue
	# 버튼의 위치 이외에 pdf 파일이 존재하지 않는 등의 경우에는 생략하는 것으로 예외 처리
    except:
        pass
	
    # 한 페이지를 끝낸 뒤 다시 상위 폴더인 '입법예고문' 폴더로 경로 지정
    os.chdir('..')
# 모든 페이지의 입법예고문을 다운받은 후에는 입법예고문의 상위 경로(입법예고문 크롤링 이전 원래의 경로)로 이동
os.chdir('..')

 

  3) 국가법령정보센터 크롤링

    - 국가법령정보센터에는 현행 법령 및 입법예고가 된 시행 예정법령의 모든 조문 검색 가능

    - 법령의 조문에 '지방행정제재·부과금의 징수 등에 관한 법률'이 있는 것을 검색하여 크롤링

    - 크롤링하려는 법령 데이터가 html 문서 내에서 나타나는 것이 아닌 서버에서 실시간으로 받아오는 것으로 BeautifulSoup로 크롤링 불가능

    - 페이지 내에 csv 파일을 다운 받을 수 있는 버튼이 있어 이 버튼을 눌러 csv 파일을 받는 것으로 동적 크롤링

# chromedriver 옵션 설정
chrome_options = webdriver.ChromeOptions()
# chromedriver를 직접 열지 않고 내부적으로 실행시키는 옵션
chrome_options.add_argument('--headless')
# chromedriver에서 다운받은 파일의 경로 지정하는 옵션(현재 경로로 지정)
chrome_options.add_experimental_option('prefs', {'download.default_directory':r'{}'.format(os.getcwd())})

# chromedriver 실행
driver = webdriver.Chrome(options = chrome_options)
# 국가법령정보센터로 접속(검색어 '지방행정제재·부과금의 징수 등에 관한 법률' 적용)
driver.get('https://www.law.go.kr/lsSc.do?menuId=1&subMenuId=15&tabMenuId=81&eventGubun=060114&query=지방행정제재·부과금의+징수+등에+관한+법률')

# 상세검색 클릭
driver.find_element(By.XPATH, r'//*[@id="dtlSch"]/a[1]').click()
# 법률 체크 클릭
driver.find_element(By.XPATH, r'//*[@id="AA1"]').click()
# 검색 클릭
driver.find_element(By.XPATH, r'//*[@id="detailLawCtDiv"]/div[3]/div[2]/a').click()
# 검색 결과 불러오는 동안 대기
time.sleep(1)
# 파일 저장 버튼 클릭
driver.find_element(By.XPATH, r'//*[@id="WideListDIV"]/div[1]/div[1]/div[3]/div[13]/button').click()
# 파일 저장형식 xls로 클릭
driver.find_element(By.XPATH, r'//*[@id="saveForm"]/div[1]/div/div[2]/a[1]').click()
# Alert 창 뜨는 동안 대기
time.sleep(1)
# Alert 창 확인 클릭
result = driver.switch_to.alert
result.accept()
# 다운 받아지는 동안 대기
time.sleep(1)

# chromedriver 종료
driver.quit()

 

  4) 크롤링 과정 통합하여 프로그램화

    - pyqt5 사용

    - 기본 틀 만들기

    - 법안의 발의 과정에 따라 의원발의 법안 → 입법예고문 → 현행 법령 순서로 크롤링 기능 배치

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        self.gridLayout = QtWidgets.QGridLayout(Form)
        self.gridLayout.setObjectName("gridLayout")
		
        # 불러오기 버튼
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")

        self.assemblyLoadBtn = QtWidgets.QPushButton(Form)
        self.assemblyLoadBtn.setObjectName("assemblyLoadBtn")
        self.horizontalLayout.addWidget(self.assemblyLoadBtn)

        self.molegLoadBtn = QtWidgets.QPushButton(Form)
        self.molegLoadBtn.setObjectName("molegLoadBtn")
        self.horizontalLayout.addWidget(self.molegLoadBtn)

        self.lawLoadBtn = QtWidgets.QPushButton(Form)
        self.lawLoadBtn.setObjectName("lawLoadBtn")
        self.horizontalLayout.addWidget(self.lawLoadBtn)

        self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
        
		
        # 불러오기 결과
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")

        self.assemblyLoadResult = QtWidgets.QLineEdit(Form)
        self.assemblyLoadResult.setObjectName("assemblyLoadResult")
        self.horizontalLayout_2.addWidget(self.assemblyLoadResult)

        self.molegLoadResult = QtWidgets.QLineEdit(Form)
        self.molegLoadResult.setObjectName("molegLoadResult")
        self.horizontalLayout_2.addWidget(self.molegLoadResult)

        self.lawLoadResult = QtWidgets.QLineEdit(Form)
        self.lawLoadResult.setObjectName("lawLoadResult")
        self.horizontalLayout_2.addWidget(self.lawLoadResult)

        self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
        
		
        # 결과 불러오기 버튼
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")

        self.assemblyCountBtn = QtWidgets.QPushButton(Form)
        self.assemblyCountBtn.setObjectName("assemblyCountBtn")
        self.horizontalLayout_3.addWidget(self.assemblyCountBtn)

        self.molegCountBtn = QtWidgets.QPushButton(Form)
        self.molegCountBtn.setObjectName("molegCountBtn")
        self.horizontalLayout_3.addWidget(self.molegCountBtn)

        self.lawCountBtn = QtWidgets.QPushButton(Form)
        self.lawCountBtn.setObjectName("lawCountBtn")
        self.horizontalLayout_3.addWidget(self.lawCountBtn)

        self.gridLayout.addLayout(self.horizontalLayout_3, 2, 0, 1, 1)
       
		
        # 결과 불러오기 결과
        self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")

        self.assemblyCountResult = QtWidgets.QTextEdit(Form)
        self.assemblyCountResult.setObjectName("assemblyCountResult")
        self.horizontalLayout_4.addWidget(self.assemblyCountResult)
        self.molegCountResult = QtWidgets.QTextEdit(Form)
        self.molegCountResult.setObjectName("molegCountResult")
        self.horizontalLayout_4.addWidget(self.molegCountResult)
        self.lawCountResult = QtWidgets.QTextEdit(Form)
        self.lawCountResult.setObjectName("lawCountResult")
        self.horizontalLayout_4.addWidget(self.lawCountResult)

        self.gridLayout.addLayout(self.horizontalLayout_4, 3, 0, 1, 1)
        
		
        # 시각화 창
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")

        self.label = QtWidgets.QLabel(Form)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.assemblyTable = QtWidgets.QTableWidget(Form)
        self.assemblyTable.setObjectName("assemblyTable")
        self.assemblyTable.setColumnCount(0)
        self.assemblyTable.setRowCount(0)
        self.verticalLayout.addWidget(self.assemblyTable)
        
        self.label_2 = QtWidgets.QLabel(Form)
        self.label_2.setObjectName("label_2")
        self.verticalLayout.addWidget(self.label_2)
        self.molegText = QtWidgets.QTextBrowser(Form)
        self.molegText.setObjectName("molegTable")
        self.verticalLayout.addWidget(self.molegText)
        
        self.label_3 = QtWidgets.QLabel(Form)
        self.label_3.setObjectName("label-3")
        self.verticalLayout.addWidget(self.label_3)
        self.lawTable = QtWidgets.QTableWidget(Form)
        self.lawTable.setObjectName("lawTable")
        self.lawTable.setColumnCount(0)
        self.lawTable.setRowCount(0)
        self.verticalLayout.addWidget(self.lawTable)

        self.gridLayout.addLayout(self.verticalLayout, 4, 0, 1, 1)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.assemblyLoadBtn.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률 가져오기"))
        self.molegLoadBtn.setText(_translate("Form", "법제처 누리집 입법예고 가져오기"))
        self.lawLoadBtn.setText(_translate("Form", "국가법령정보센터 법령 가져오기"))
        self.assemblyCountBtn.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률 확인"))
        self.molegCountBtn.setText(_translate("Form", "법제처 누리집 입법예고 확인"))
        self.lawCountBtn.setText(_translate("Form", "국가법령정보센터 법령 확인"))
        self.label.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률"))
        self.label_2.setText(_translate("Form", "법제처 누리집 입법예고"))
        self.label_3.setText(_translate("Form", "국가법령정보센터 법령"))

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

 

     - 만들어진 버튼에 각 기능 삽입

# 의안 목록 가져오기('국회 의안정보시스템 의원발의 법률 가져오기'버튼과 연결)
def assemblyLoad(self):
    self.assemblyLoadResult.setText('의안목록 가져오기 진행중')
    url = 'https://likms.assembly.go.kr/bill/BillSearchProposalResult.do?query=지방행정제재ㆍ부과금의+징수+등에+관한+법률&PageSizeOption=100'

    soup = BeautifulSoup(requests.get(url).text, 'html.parser')
    result = soup.select('p.ti a')
    법령명 = []
    제안일 = []
    의결일 = []
    결과 = []
    for i in range(len(result)):
        법령명.append(str(result[i])[str(result[i]).find('ㆍ') + 1 : str(result[i]).find('제안일')])
        제안일.append(str(result[i])[str(result[i]).find('제안일') + 6 : str(result[i]).find('의결일') - 2])
        의결일.append(str(result[i])[str(result[i]).find('의결일') + 6 : str(result[i]).find('의결일') + 16])
        결과.append(str(result[i])[str(result[i]).find('의결일') + 18 : -5])
    의안 = pd.DataFrame({'법령명' : 법령명, '제안일' : 제안일, '의결일' : 의결일, '결과' : 결과})
    의안[['제안일', '의결일']] = 의안[['제안일', '의결일']].replace('.', '-')
    의안['제안일'] = 의안['제안일'].apply(lambda x: pd.to_datetime(x))
    의안['의결일'] = 의안['의결일'].apply(lambda x: pd.to_datetime(x) if x != ' </a>' else '미정')
    의안 = 의안.astype({'제안일':'str', '의결일':'str'})
    의안['의결일'] = 의안['의결일'].apply(lambda x: str(x)[:-9] if x != '미정' else '미정')
    의안['결과'] = 의안['결과'].apply(lambda x: '미정' if x == '' else x)
    의안.to_excel('의안목록.xlsx', index = False)
    self.assemblyLoadResult.setText('의안목록 가져오기 완료')

# 가져온 의안 목록에서 개수 세기('국회 의안정보시스템 의안발의 법률 확인'버튼과 연결)
def assemblyCount(self):
    의안 = pd.read_excel('의안목록.xlsx')
    self.assemblyCountResult.setText('총 의안 개수: {}\n원안가결 의안 개수: {}\n대안반영폐기 개수: {}\n의결 전 의안 개수: {}'.format(len(의안), len(의안[의안['결과']=='원안가결']), len(의안[의안['결과'].isin(['임기만료폐기', '대안반영폐기'])]), len(의안[~의안['결과'].isin(['원안가결', '임기만료폐기', '대안반영폐기'])])))
    self.assemblyTable.setRowCount(len(의안))
    self.assemblyTable.setColumnCount(len(의안.columns))
    self.assemblyTable.setHorizontalHeaderLabels(의안.columns)
    for row_index, row in enumerate(의안.index):
        for col_index, col in enumerate(의안.columns):
            item = QtWidgets.QTableWidgetItem(의안.loc[row][col])
            self.assemblyTable.setItem(row_index, col_index, item)


# 입법예고문 가져오기('법제처 누리집 입법예고문 가져오기'버튼과 연결)
def molegLoad(self):
    if not os.path.exists("입법예고문"):
        os.mkdir("입법예고문")
    os.chdir("입법예고문")
    self.law_count = 0
    for i in range(1, 5):
        if not os.path.exists(f"입법예고문_{i}page"):
            os.mkdir(f"입법예고문_{i}page")
        os.chdir(f"입법예고문_{i}page")
        url = f'https://www.moleg.go.kr/lawinfo/makingList.mo?mid=a10104010000&currentPage={i}&pageCnt=10&keyField=&keyWord=&stYdFmt=&edYdFmt=&lsClsCd=&cptOfiOrgCd='

        soup = BeautifulSoup(requests.get(url).text, 'html.parser')
        try:
            for href in soup.find_all('div', 'wrap title'):
                page_url = "https://www.moleg.go.kr" + href.find('a')['href'].replace("¤", '&curren')
                page_soup = BeautifulSoup(requests.get(page_url).text, 'html.parser')
                title = page_soup.find('div', 'tb_contents').find('ul').find('a').text
                if 'pdf' in title:
                    title = page_soup.find('div', 'tb_contents').find('ul').find('a').text[:title.find('pdf') + 3]
                    file_url = page_soup.find('div', 'tb_contents').find('ul').find('a')['href']
                    file = requests.get(file_url)
                    with open(title, 'wb') as f:
                        f.write(file.content)
                        pdf = fitz.open(title)
                        cnt = 0
                        for page in pdf:
                            text = page.get_text()
                            result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                            cnt -= result
                    if cnt == len(pdf):
                        self.molegText.append("해당없음")
                        # os.unlink(os.getcwd() + title)
                    else:
                        self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                        self.law_count += 1

                elif 'pdf' in page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text:
                    title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text
                    title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text[:title.find('pdf') + 3]
                    print(title)
                    file_url = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1]['href']
                    file = requests.get(file_url)
                    with open(title, 'wb') as f:
                        f.write(file.content)
                        pdf = fitz.open(title)
                        for page in pdf:
                            text = page.get_text()
                            result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                            cnt -= result
                        if cnt == len(pdf):
                            self.molegText.append("해당없음")
                            # os.unlink(os.path(os.getcwd() + title))
                        else:
                            self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                            self.law_count += 1
                else:
                    continue

        except:
            pass

        os.chdir('..')
    os.chdir('..')
    self.molegLoadResult.setText('의안목록 가져오기 완료')

# 입법예고문에서 키워드 포함한 예고문 개수 세기('법제처 누리집 입법예고문 확인'버튼과 연결)
def molegCount(self):
    self.molegCountResult.setText(f'관련 법률: {self.law_count}개')


# 현행 법령 가져오기('국가법령정보센터 법령 가져오기'버튼과 연결)
def lawLoad(self):
    self.lawLoadResult.setText('법령목록 가져오기 진행중')

    # chromedriver 옵션 설정
    chrome_options = webdriver.ChromeOptions()
    # chromedriver를 직접 열지 않고 내부적으로 실행시키는 옵션
    chrome_options.add_argument('--headless')
    # chromedriver에서 다운받은 파일의 경로 지정하는 옵션(현재 경로로 지정)
    chrome_options.add_experimental_option('prefs', {'download.default_directory':r'{}'.format(os.getcwd())})

    # chromedriver 실행
    driver = webdriver.Chrome(options = chrome_options)
    # 국가법령정보센터로 접속(검색어 '지방행정제재·부과금의 징수 등에 관한 법률' 적용)
    driver.get('https://www.law.go.kr/lsSc.do?menuId=1&subMenuId=15&tabMenuId=81&eventGubun=060114&query=지방행정제재·부과금의+징수+등에+관한+법률')

    # 상세검색 클릭
    driver.find_element(By.XPATH, r'//*[@id="dtlSch"]/a[1]').click()
    # 법률 체크 클릭
    driver.find_element(By.XPATH, r'//*[@id="AA1"]').click()
    # 키워드 검색
    driver.find_element(By.XPATH, r'//*[@id="lsBdyDts"]').clear()
    driver.find_element(By.XPATH, r'//*[@id="lsBdyDts"]').send_keys('지방행정제재·부과금의 징수 등에 관한 법률')
    # 검색 클릭
    driver.find_element(By.XPATH, r'//*[@id="detailLawCtDiv"]/div[3]/div[2]/a').click()
    # 검색 결과 불러오는 동안 대기
    time.sleep(1)
    # 파일 저장 버튼 클릭
    driver.find_element(By.XPATH, r'//*[@id="WideListDIV"]/div[1]/div[1]/div[3]/div[13]/button').click()
    # 파일 저장형식 xls로 클릭
    driver.find_element(By.XPATH, r'//*[@id="saveForm"]/div[1]/div/div[2]/a[1]').click()
    # Alert 창 뜨는 동안 대기
    time.sleep(1)
    # Alert 창 확인 클릭
    result = driver.switch_to.alert
    result.accept()
    # 다운 받아지는 동안 대기
    time.sleep(1)

    # chromedriver 종료
    driver.quit()

    self.lawLoadResult.setText('법령목록 가져오기 완료')

# 현행 법령 개수 세기('국가법령정보센터 법령 확인'버튼과 연결)
def lawCount(self):
    # 파일 불러오기
    법령 = pd.read_excel('법령목록.xls')
    # 중복행 제거
    법령 = 법령.drop_duplicates(subset='법령명')
    # 공포일자 및 시행일자 datetime 형식으로 변경
    법령['공포일자'] = pd.to_datetime(법령['공포일자'])
    # 시행일자가 '미정'인 것은 예정된 법령이므로 일단 현재 날짜에서 하루 더 더하기
    법령['시행일자'] = 법령['시행일자'].apply(lambda x: pd.to_datetime(x) if x != '미정' else datetime.today() + timedelta(days = 1))
    # 시행일자와 현재 일자를 비교하여 현재일자 이후에 시행되는 법령을 '예정' 법령으로 표시
    법령['예정'] = np.where((법령['시행일자'] == "미정") | (법령['시행일자'] > datetime.today()), 'o', 'x')
    법령 = 법령[['법령명', '공포일자', '시행일자', '예정']].sort_values(by = ['예정', '공포일자', '시행일자'], ascending = [True, False, False]).reset_index(drop = True)
    법령 = 법령.astype({'공포일자':'str', '시행일자':'str'})
    법령['시행일자'] = 법령['시행일자'].apply(lambda x:str(x)[:10])

    # 전체 법령 개수 출력
    self.lawCountResult.setText('총 법령 개수: {}\n시행 중인 법령 개수: {}\n시행 예정 법령 개수: {}'.format(len(법령), len(법령.loc[법령['예정'] == 'o']), len(법령.loc[법령['예정'] == 'x'])))
    # 예정된 법령을 먼저, 공포일자순으로 정렬
    # 아직 시행되지 않고 예정된 법령 확인 가능
    # 최근 업데이트된 법령 확인 가능
    self.lawTable.setRowCount(len(법령))
    self.lawTable.setColumnCount(len(법령.columns))
    self.lawTable.setHorizontalHeaderLabels(법령.columns)
    for row_index, row in enumerate(법령.index):
        for col_index, col in enumerate(법령.columns):
            item = QtWidgets.QTableWidgetItem(법령.loc[row][col])
            self.lawTable.setItem(row_index, col_index, item)

 

    - 전체 연결 코드

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        self.gridLayout = QtWidgets.QGridLayout(Form)
        self.gridLayout.setObjectName("gridLayout")
		
        # 불러오기 버튼
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")

        self.assemblyLoadBtn = QtWidgets.QPushButton(Form)
        self.assemblyLoadBtn.setObjectName("assemblyLoadBtn")
        self.horizontalLayout.addWidget(self.assemblyLoadBtn)

        self.molegLoadBtn = QtWidgets.QPushButton(Form)
        self.molegLoadBtn.setObjectName("molegLoadBtn")
        self.horizontalLayout.addWidget(self.molegLoadBtn)

        self.lawLoadBtn = QtWidgets.QPushButton(Form)
        self.lawLoadBtn.setObjectName("lawLoadBtn")
        self.horizontalLayout.addWidget(self.lawLoadBtn)

        self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
        
		
        # 불러오기 결과
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")

        self.assemblyLoadResult = QtWidgets.QLineEdit(Form)
        self.assemblyLoadResult.setObjectName("assemblyLoadResult")
        self.horizontalLayout_2.addWidget(self.assemblyLoadResult)

        self.molegLoadResult = QtWidgets.QLineEdit(Form)
        self.molegLoadResult.setObjectName("molegLoadResult")
        self.horizontalLayout_2.addWidget(self.molegLoadResult)

        self.lawLoadResult = QtWidgets.QLineEdit(Form)
        self.lawLoadResult.setObjectName("lawLoadResult")
        self.horizontalLayout_2.addWidget(self.lawLoadResult)

        self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
        
		
        # 결과 불러오기 버튼
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")

        self.assemblyCountBtn = QtWidgets.QPushButton(Form)
        self.assemblyCountBtn.setObjectName("assemblyCountBtn")
        self.horizontalLayout_3.addWidget(self.assemblyCountBtn)

        self.molegCountBtn = QtWidgets.QPushButton(Form)
        self.molegCountBtn.setObjectName("molegCountBtn")
        self.horizontalLayout_3.addWidget(self.molegCountBtn)

        self.lawCountBtn = QtWidgets.QPushButton(Form)
        self.lawCountBtn.setObjectName("lawCountBtn")
        self.horizontalLayout_3.addWidget(self.lawCountBtn)

        self.gridLayout.addLayout(self.horizontalLayout_3, 2, 0, 1, 1)
       
		
        # 결과 불러오기 결과
        self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")

        self.assemblyCountResult = QtWidgets.QTextEdit(Form)
        self.assemblyCountResult.setObjectName("assemblyCountResult")
        self.horizontalLayout_4.addWidget(self.assemblyCountResult)
        self.molegCountResult = QtWidgets.QTextEdit(Form)
        self.molegCountResult.setObjectName("molegCountResult")
        self.horizontalLayout_4.addWidget(self.molegCountResult)
        self.lawCountResult = QtWidgets.QTextEdit(Form)
        self.lawCountResult.setObjectName("lawCountResult")
        self.horizontalLayout_4.addWidget(self.lawCountResult)

        self.gridLayout.addLayout(self.horizontalLayout_4, 3, 0, 1, 1)
        
		
        # 시각화 창
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")

        self.label = QtWidgets.QLabel(Form)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.assemblyTable = QtWidgets.QTableWidget(Form)
        self.assemblyTable.setObjectName("assemblyTable")
        self.assemblyTable.setColumnCount(0)
        self.assemblyTable.setRowCount(0)
        self.verticalLayout.addWidget(self.assemblyTable)
        
        self.label_2 = QtWidgets.QLabel(Form)
        self.label_2.setObjectName("label_2")
        self.verticalLayout.addWidget(self.label_2)
        self.molegText = QtWidgets.QTextBrowser(Form)
        self.molegText.setObjectName("molegTable")
        self.verticalLayout.addWidget(self.molegText)
        
        self.label_3 = QtWidgets.QLabel(Form)
        self.label_3.setObjectName("label-3")
        self.verticalLayout.addWidget(self.label_3)
        self.lawTable = QtWidgets.QTableWidget(Form)
        self.lawTable.setObjectName("lawTable")
        self.lawTable.setColumnCount(0)
        self.lawTable.setRowCount(0)
        self.verticalLayout.addWidget(self.lawTable)

        self.gridLayout.addLayout(self.verticalLayout, 4, 0, 1, 1)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.assemblyLoadBtn.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률 가져오기"))
        self.molegLoadBtn.setText(_translate("Form", "법제처 누리집 입법예고 가져오기"))
        self.lawLoadBtn.setText(_translate("Form", "국가법령정보센터 법령 가져오기"))
        self.assemblyCountBtn.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률 확인"))
        self.molegCountBtn.setText(_translate("Form", "법제처 누리집 입법예고 확인"))
        self.lawCountBtn.setText(_translate("Form", "국가법령정보센터 법령 확인"))
        self.label.setText(_translate("Form", "국회 의안정보시스템 의원발의 법률"))
        self.label_2.setText(_translate("Form", "법제처 누리집 입법예고"))
        self.label_3.setText(_translate("Form", "국가법령정보센터 법령"))
        
    
    # 의안 목록 가져오기('국회 의안정보시스템 의원발의 법률 가져오기'버튼과 연결)
    def assemblyLoad(self):
        self.assemblyLoadResult.setText('의안목록 가져오기 진행중')
        url = 'https://likms.assembly.go.kr/bill/BillSearchProposalResult.do?query=지방행정제재ㆍ부과금의+징수+등에+관한+법률&PageSizeOption=100'

        soup = BeautifulSoup(requests.get(url).text, 'html.parser')
        result = soup.select('p.ti a')
        법령명 = []
        제안일 = []
        의결일 = []
        결과 = []
        for i in range(len(result)):
            법령명.append(str(result[i])[str(result[i]).find('ㆍ') + 1 : str(result[i]).find('제안일')])
            제안일.append(str(result[i])[str(result[i]).find('제안일') + 6 : str(result[i]).find('의결일') - 2])
            의결일.append(str(result[i])[str(result[i]).find('의결일') + 6 : str(result[i]).find('의결일') + 16])
            결과.append(str(result[i])[str(result[i]).find('의결일') + 18 : -5])
        의안 = pd.DataFrame({'법령명' : 법령명, '제안일' : 제안일, '의결일' : 의결일, '결과' : 결과})
        의안[['제안일', '의결일']] = 의안[['제안일', '의결일']].replace('.', '-')
        의안['제안일'] = 의안['제안일'].apply(lambda x: pd.to_datetime(x))
        의안['의결일'] = 의안['의결일'].apply(lambda x: pd.to_datetime(x) if x != ' </a>' else '미정')
        의안 = 의안.astype({'제안일':'str', '의결일':'str'})
        의안['의결일'] = 의안['의결일'].apply(lambda x: str(x)[:-9] if x != '미정' else '미정')
        의안['결과'] = 의안['결과'].apply(lambda x: '미정' if x == '' else x)
        의안.to_excel('의안목록.xlsx', index = False)
        self.assemblyLoadResult.setText('의안목록 가져오기 완료')

    # 가져온 의안 목록에서 개수 세기('국회 의안정보시스템 의안발의 법률 확인'버튼과 연결)
    def assemblyCount(self):
        의안 = pd.read_excel('의안목록.xlsx')
        self.assemblyCountResult.setText('총 의안 개수: {}\n원안가결 의안 개수: {}\n대안반영폐기 개수: {}\n의결 전 의안 개수: {}'.format(len(의안), len(의안[의안['결과']=='원안가결']), len(의안[의안['결과'].isin(['임기만료폐기', '대안반영폐기'])]), len(의안[~의안['결과'].isin(['원안가결', '임기만료폐기', '대안반영폐기'])])))
        self.assemblyTable.setRowCount(len(의안))
        self.assemblyTable.setColumnCount(len(의안.columns))
        self.assemblyTable.setHorizontalHeaderLabels(의안.columns)
        for row_index, row in enumerate(의안.index):
            for col_index, col in enumerate(의안.columns):
                item = QtWidgets.QTableWidgetItem(의안.loc[row][col])
                self.assemblyTable.setItem(row_index, col_index, item)


    # 입법예고문 가져오기('법제처 누리집 입법예고문 가져오기'버튼과 연결)
    def molegLoad(self):
        if not os.path.exists("입법예고문"):
            os.mkdir("입법예고문")
        os.chdir("입법예고문")
        self.law_count = 0
        for i in range(1, 5):
            if not os.path.exists(f"입법예고문_{i}page"):
                os.mkdir(f"입법예고문_{i}page")
            os.chdir(f"입법예고문_{i}page")
            url = f'https://www.moleg.go.kr/lawinfo/makingList.mo?mid=a10104010000&currentPage={i}&pageCnt=10&keyField=&keyWord=&stYdFmt=&edYdFmt=&lsClsCd=&cptOfiOrgCd='

            soup = BeautifulSoup(requests.get(url).text, 'html.parser')
            try:
                for href in soup.find_all('div', 'wrap title'):
                    page_url = "https://www.moleg.go.kr" + href.find('a')['href'].replace("¤", '&curren')
                    page_soup = BeautifulSoup(requests.get(page_url).text, 'html.parser')
                    title = page_soup.find('div', 'tb_contents').find('ul').find('a').text
                    if 'pdf' in title:
                        title = page_soup.find('div', 'tb_contents').find('ul').find('a').text[:title.find('pdf') + 3]
                        file_url = page_soup.find('div', 'tb_contents').find('ul').find('a')['href']
                        file = requests.get(file_url)
                        with open(title, 'wb') as f:
                            f.write(file.content)
                            pdf = fitz.open(title)
                            cnt = 0
                            for page in pdf:
                                text = page.get_text()
                                result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                                cnt -= result
                        if cnt == len(pdf):
                            self.molegText.append("해당없음")
                            # os.unlink(os.getcwd() + title)
                        else:
                            self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                            self.law_count += 1

                    elif 'pdf' in page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text:
                        title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text
                        title = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1].text[:title.find('pdf') + 3]
                        print(title)
                        file_url = page_soup.find('div', 'tb_contents').find('ul').find_all('a')[1]['href']
                        file = requests.get(file_url)
                        with open(title, 'wb') as f:
                            f.write(file.content)
                            pdf = fitz.open(title)
                            for page in pdf:
                                text = page.get_text()
                                result = text.find('지방행정제재ㆍ부과금의 징수 등에 관한 법률')
                                cnt -= result
                            if cnt == len(pdf):
                                self.molegText.append("해당없음")
                                # os.unlink(os.path(os.getcwd() + title))
                            else:
                                self.molegText.append(title + '에서 "지방행정제재ㆍ부과금의 징수 등에 관한 법률"이 개정됨')
                                self.law_count += 1
                    else:
                        continue

            except:
                pass

            os.chdir('..')
        os.chdir('..')
        self.molegLoadResult.setText('의안목록 가져오기 완료')

    # 입법예고문에서 키워드 포함한 예고문 개수 세기('법제처 누리집 입법예고문 확인'버튼과 연결)
    def molegCount(self):
        self.molegCountResult.setText(f'관련 법률: {self.law_count}개')


    # 현행 법령 가져오기('국가법령정보센터 법령 가져오기'버튼과 연결)
    def lawLoad(self):
        self.lawLoadResult.setText('법령목록 가져오기 진행중')

        # chromedriver 옵션 설정
        chrome_options = webdriver.ChromeOptions()
        # chromedriver를 직접 열지 않고 내부적으로 실행시키는 옵션
        chrome_options.add_argument('--headless')
        # chromedriver에서 다운받은 파일의 경로 지정하는 옵션(현재 경로로 지정)
        chrome_options.add_experimental_option('prefs', {'download.default_directory':r'{}'.format(os.getcwd())})

        # chromedriver 실행
        driver = webdriver.Chrome(options = chrome_options)
        # 국가법령정보센터로 접속(검색어 '지방행정제재·부과금의 징수 등에 관한 법률' 적용)
        driver.get('https://www.law.go.kr/lsSc.do?menuId=1&subMenuId=15&tabMenuId=81&eventGubun=060114&query=지방행정제재·부과금의+징수+등에+관한+법률')

        # 상세검색 클릭
        driver.find_element(By.XPATH, r'//*[@id="dtlSch"]/a[1]').click()
        # 법률 체크 클릭
        driver.find_element(By.XPATH, r'//*[@id="AA1"]').click()
        # 키워드 검색
        driver.find_element(By.XPATH, r'//*[@id="lsBdyDts"]').clear()
        driver.find_element(By.XPATH, r'//*[@id="lsBdyDts"]').send_keys('지방행정제재·부과금의 징수 등에 관한 법률')
        # 검색 클릭
        driver.find_element(By.XPATH, r'//*[@id="detailLawCtDiv"]/div[3]/div[2]/a').click()
        # 검색 결과 불러오는 동안 대기
        time.sleep(1)
        # 파일 저장 버튼 클릭
        driver.find_element(By.XPATH, r'//*[@id="WideListDIV"]/div[1]/div[1]/div[3]/div[13]/button').click()
        # 파일 저장형식 xls로 클릭
        driver.find_element(By.XPATH, r'//*[@id="saveForm"]/div[1]/div/div[2]/a[1]').click()
        # Alert 창 뜨는 동안 대기
        time.sleep(1)
        # Alert 창 확인 클릭
        result = driver.switch_to.alert
        result.accept()
        # 다운 받아지는 동안 대기
        time.sleep(1)

        # chromedriver 종료
        driver.quit()

        self.lawLoadResult.setText('법령목록 가져오기 완료')

    # 현행 법령 개수 세기('국가법령정보센터 법령 확인'버튼과 연결)
    def lawCount(self):
        # 파일 불러오기
        법령 = pd.read_excel('법령목록.xls')
        # 중복행 제거
        법령 = 법령.drop_duplicates(subset='법령명')
        # 공포일자 및 시행일자 datetime 형식으로 변경
        법령['공포일자'] = pd.to_datetime(법령['공포일자'])
        # 시행일자가 '미정'인 것은 예정된 법령이므로 일단 현재 날짜에서 하루 더 더하기
        법령['시행일자'] = 법령['시행일자'].apply(lambda x: pd.to_datetime(x) if x != '미정' else datetime.today() + timedelta(days = 1))
        # 시행일자와 현재 일자를 비교하여 현재일자 이후에 시행되는 법령을 '예정' 법령으로 표시
        법령['예정'] = np.where((법령['시행일자'] == "미정") | (법령['시행일자'] > datetime.today()), 'o', 'x')
        법령 = 법령[['법령명', '공포일자', '시행일자', '예정']].sort_values(by = ['예정', '공포일자', '시행일자'], ascending = [True, False, False]).reset_index(drop = True)
        법령 = 법령.astype({'공포일자':'str', '시행일자':'str'})
        법령['시행일자'] = 법령['시행일자'].apply(lambda x:str(x)[:10])

        # 전체 법령 개수 출력
        self.lawCountResult.setText('총 법령 개수: {}\n시행 중인 법령 개수: {}\n시행 예정 법령 개수: {}'.format(len(법령), len(법령.loc[법령['예정'] == 'o']), len(법령.loc[법령['예정'] == 'x'])))
        # 예정된 법령을 먼저, 공포일자순으로 정렬
        # 아직 시행되지 않고 예정된 법령 확인 가능
        # 최근 업데이트된 법령 확인 가능
        self.lawTable.setRowCount(len(법령))
        self.lawTable.setColumnCount(len(법령.columns))
        self.lawTable.setHorizontalHeaderLabels(법령.columns)
        for row_index, row in enumerate(법령.index):
            for col_index, col in enumerate(법령.columns):
                item = QtWidgets.QTableWidgetItem(법령.loc[row][col])
                self.lawTable.setItem(row_index, col_index, item)

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

 

 

4. 프로젝트 결과물

    - 동작 방식

 

+ Recent posts