저번에 elastic search에 데이터를 저장했다.

텍스트 데이터를 저장하는데 유용하다고 들었기 때문이다.

근데, elastic search가 필요 없어졌다.

 

1. 나는 실시간으로 데이터를 처리할 생각이 없고 하루에 한번만 데이터를 분석해 저장할 것이다.

2. 검색엔진이 필요없다.

3. elastic search는 비슷한 단어가 들어있다면 검색해서 가져온다.

예를 들어, 교육위원장 김영호를 검색하면 통일위원장 김영호도 같이 가져왔다.

 

그래서 다른 데이터베이스를 사용해야 하는데 nosql인 MongoDB와 rdbms인 postgres 중 고민을 하는데,

nosql인 MongoDB가 나을 거 같다는 생각이 들었다.

 

1. 텍스트를 elastic search와 비슷한 형식으로 저장하고 싶음

2. join이나 관계성 필요 없이 발언자와 발언 내용만을 저장하고 싶음

3. 텍스트 양이 많야 nosql의 속도가 더 빠를 거 같음

4. 특정 단어를 확인하는 NLP분석을 진행할 것임

5. 나중에 필요하다면 elastic search와 연결할 수 있음

 

그리고 최종으로 데이터 분석을 맞춘 데이터들만 postgres에 저장하면 될 거 같다.

 


MongoDB에 연결하기

먼저 MongoDB를 설치해야 한다.

https://www.mongodb.com/ko-kr/docs/manual/tutorial/install-mongodb-on-os-x/

 

macOS에 MongoDB Community Edition 설치 - MongoDB 매뉴얼 v8.0

설치 후 macOS가 mongod 실행을 차단할 수 있습니다. mongod을 시작할 때 개발자를 식별하거나 확인할 수 없다는 보안 오류를 수신한 경우 다음을 실행하여 mongod에 액세스 권한을 부여합니다. 시스템

www.mongodb.com

아래와 같이 하면 맥에서는 몽고 db가 실행된다.

macOS 서비스를 통해서 기본적인 몽고db를 실행할 수 있다.

백그라운드 프로세서로서 수동으로 진행하려면 다른 기능을 추가해야한다.

brew tap mongodb/brew

brew update

brew install mongodb-community@8.0

brew services start mongodb-community@8.0

mongsh

mongodb 연결

로컬 ip에서만 진행하려 한다. 

어차피 로컬에서 분석을 돌리고 저장을 sql에다가 하면 되기 때문에 지금은 필요 없다고 판단 했다.

그리고 기존에 로컬에 있던 json,csv 파일은 전부 mongodb에 저장하기로 했다.

 

그리고 Connecting to로 Vscode에 MongoDB Extension을 추가해 연결했다.

vscode에 연결하기

그래서 이제부터는 지금까지 local에 파일로 저장되거나 elastic에 저장되도록 되어있는 코드들을 전부
MongoDB에 저장되도록 변경해야 한다.

 

db_utils라는 파일에 함수를 만들었다.

 

from pymongo import MongoClient
from dotenv import load_dotenv
import os

def connect_to_mongo(db_name, collection_name):
    """MongoDB 연결"""
    load_dotenv()
    pwd = os.getenv("MONGODB_PWD") # 나중에 암호를 추가해야할 수 있음
    client = MongoClient(f"mongodb://localhost:27017/")
    db = client[db_name]
    collection = db[collection_name]
    return client, db, collection

def save_to_mongo(data, db_name, collection_name, mongo_uri="mongodb://localhost:27017/"):
    """
    데이터를 MongoDB에 저장하는 함수.

    :param data: 저장할 데이터 (리스트 또는 딕셔너리)
    :param db_name: 사용할 데이터베이스 이름
    :param collection_name: 사용할 컬렉션 이름
    :param mongo_uri: MongoDB 연결 URI (기본값: 로컬 MongoDB)
    """
    try:
        client, collection = connect_to_mongo(db_name,collection_name,mongo_uri)

        # 데이터 저장
        if isinstance(data, list):  # 리스트 형태면 여러 개 저장
            collection.insert_many(data)
        else:  # 단일 딕셔너리면 하나만 저장
            collection.insert_one(data)

        print(f"✅ 데이터 저장 완료! (DB: {db_name}, Collection: {collection_name})")
    except Exception as e:
        print(f"❌ 데이터 저장 실패: {e}")
    finally:
        client.close()

 

그 이후 데이터를 mongodb에 추가했다.

from common.api_utils import load_api_key, fetch_data
from common.db_utils import save_to_mongo

if __name__ == "__main__":
    url = "https://open.assembly.go.kr/portal/openapi/nekcaiymatialqlxr"
    key = load_api_key()

    if key is None:
        raise ValueError("❌ OPEN_GOVERMETN_API_KEY 환경 변수가 설정되지 않았습니다.")

    all_data = fetch_data(url, key)
    save_to_mongo(all_data, "government","congress_scedhule")

 

아래와 같이 얻은 Mongodb 데이터를 확인할 수 있다.

result = collection.find({},{"MEETTING_DATE":1,"_id":0}).to_list()

sql 문으로 치면 아래와 같다.

SELECT MEETTING_DATE

FROM CONGRESS_SCEDHULE;

앞에 {}에는 where절이고 뒤에 {}에는 select절이고 0과 1로 들어감 여부를 확인할 수 있다.

from pymongo import MongoClient

# MongoDB 서버에 연결
client = MongoClient("mongodb://localhost:27017/")
db = client["government"]  
collection = db["congress_scedhule"]

# 데이터베이스 목록 출력
db_list = client.list_database_names()
print("✅ 현재 MongoDB 데이터베이스 목록:", db_list)

# 데이터 조회
result = collection.find({},{"MEETTING_DATE":1,"_id":0}).to_list()
print("✅ MongoDB 데이터 확인:", result)

# 연결 종료
client.close()

 


RDS와 연결하기

brew install postgresql
psql postgres

1. postgresql을 설치하고 연결한다.

2. 그 다음에 CREATE DATABASE {원하는 데이터베이스 이름}을 추가해 db에 추가한다.

3. vscode에 sqltools라는 extension에 값을 추가한다.

4. database 아키텍쳐를 설계한다.

5. 테이블을 만들어준다. 

6. psycopg2-binary를 통해 MongoDB에서 값을 가져와 테이블에 insert 한다.

지금 만들 테이블

CREATE TABLE CONGRESS_SCHEDULE(
    meeting_date DATE PRIMARY KEY,
    get_pdf BOOLEAN NOT NULL DEFAULT FALSE
);

CREATE TABLE COMITTEE_SCHEDULE(
    meeting_date DATE PRIMARY KEY,
    get_pdf BOOLEAN NOT NULL DEFAULT FALSE
);
def get_schedule(db_name, collection_name):
    """위원회 및, 국회의원회의 날짜를 가져오는 함수"""
    try:
        client, db, collection = connect_to_mongo(db_name,collection_name)
        
        result = collection.find({},{"MEETTING_DATE":1,"_id":0}).to_list()
        print("✅ MongoDB 날짜 데이터 가져옴:")
        
        return result

    except Exception as e:
        print(f"❌ mongodb 날짜 데이터 저장 실패: {e}")
    finally:
        client.close()
        
def connect_to_postgresql():
    """postgresql에 연결하는 함수"""
    load_dotenv()
    conn = psycopg2.connect(
        host = os.getenv("SQL_HOST"),
        dbname = os.getenv("SQL_DBNAME"),
        user = os.getenv("SQL_USER"),
        password = os.getenv("SQL_PWD"),
        port = os.getenv("SQL_PORT")
    )
    return conn

def schedule_to_postgresql(schedule_list,collection_name):
    try:
        conn = connect_to_postgresql()
        cur = conn.cursor()
        for meetting_date in schedule_list:
            date = meetting_date["MEETTING_DATE"]
            cur.execute(f"""
                        INSERT INTO {collection_name} (meeting_date)
                        VALUES (%s)
                        ON CONFLICT (meeting_date) DO NOTHING
                        """, (date,))
        conn.commit()
        print("✅ postgres 데이터 저장 성공")
    except Exception as e:
        conn.rollback()
        print(f"❌ postgres 데이터 저장 실패 : {e}")
    finally:
        conn.close()

mongo db에서 값을 가져와 데이터를 읽어내서 sql에 저장하면 된다.

저장 모습을 확인할 수 있다.

python에서 상위 경로에 있는 모듈 참조하는 법

 

1. python으로 상위 디렉토리를 설정해주는 방법이다.

import sys
import os

# 현재 스크립트 파일(get_meeting_schedule.py)의 디렉터리에서 상위 두 개 폴더를 경로에 추가
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))

 

📌 설명

os.path.dirname(__file__) → 현재 파일(get_meeting_schedule.py)이 있는 디렉터리를 가져옴.

os.path.join(..., "../..") → 현재 폴더에서 두 단계 위로 이동 (extract_data 기준).

sys.path.append(...) → Python이 extract_data/common/ 경로를 인식하게 함.

 

단점

모든 파일마다 코드를 추가해야한다.


 

2. command로 상위 디렉토리를 설정해주는 방법이다.

 export PYTHONPATH="{코드가 있는 디렉토리}"

 

📌 설명

빠르게 경로를 설정할 수 있다.

단점

터미널이 종료될때 마다 해줘야한다.

 


3. setup.py 설정해서 패키지로 만들기

pip install setuptools

 

from setuptools import setup, find_packages

setup(
    name="{원하는 패키지 이름}",
    version="1.0",
    packages=find_packages(),
)

📌 설명

 find_packages() __init__.py가 있는 모든 디렉터리를 자동으로 패키지로 인식한다.

 name="{원하는 패키지 이름}" 원하는 이름으로 변경 가능.

• 모듈이 들은 디렉토리에 __init__.py 파일을 추가해야함.

깔끔하게 설치가 가능함, 내용이 바뀌어도 자동 반영 됨.

cd /Users/song-giung/Documents/coding/goverment_project/
pip install -e .

단점

새로운 모듈을 추가할 때는 다시 install 해야함

• 추가 라이브러리 설치해야함

 

 

https://github.com/woongCat/goverment_check_project

 

GitHub - woongCat/goverment_check_project: 국회의원의 국회의원 회의 참석율과 공약이행률 확인하는 airflow

국회의원의 국회의원 회의 참석율과 공약이행률 확인하는 airflow. Contribute to woongCat/goverment_check_project development by creating an account on GitHub.

github.com

이제는 api를 통해서 pdf링크를 전부 가져왔다.

그리고 pdfplumber로 pdf에 텍스트를 추출하고

pdf 그 자체는 저장하지 않고 io.BytesIO를 이용해 메모리로만 읽어오기로 했다.

 

굳이 pdf 그 자체를 저장하지 않는 이유는 텍스트를 전부 추출하고 링크를 갖고 있기 때문에 pdf가 차지하는 용량은 안 가져오기로 했다.

 

그래서 아래와 같은 pdf 파일을 가져왔다. 이제 이 추출된 pdf를 어떻게 저장할지 고민해야할 차례다.

회의록의 기본적인 모양

[STEP 1 : 발언자와 발언으로 구분 짓기]

    speaker_pattern = re.compile(r"◯([\w]+ [\w]+)\s*\n*([\s\S]+?)(?=\n◯|\Z)", re.MULTILINE)

1. 발언자 이름 앞에는 ◯으로 시작한다. =

2. 발언자는 직책과 이름 2단어로 구성되어 있고 그 사이에 공백이 있다. = ([\w]+ [\w]+)

3. 발언자와 발언 사이에는 공백이 여러개가 있다. = \s*\n*

4. 그 다음에 발언이 온다. +?를 사용한 이유는 ◯을 만나면 멈추기 위함이다. = ([\s\S]+?)

5. 다음 발언자 또는 문서 끝까지 매칭한다. = (?=\n◯|\Z)

 

[STEP 2 : 발언자, 발언 내용을 이용해 엘라스틱 서치에 들어갈 내용을 추가한다.]

def get_speaker_data(id,title,date,text):
    """ 텍스트에서 발언자와 발언 내용을 추출하는 함수 """
    speaker_pattern = re.compile(r"◯([\w]+ [\w]+)\s*\n*([\s\S]+?)(?=\n◯|\Z)", re.MULTILINE)
    speech_list = []
    
    for match in speaker_pattern.finditer(text):
        speaker = match.group(1).strip()
        speech = match.group(2).strip()

        speech_list.append({
            "document_id": id,
            "title": title,
            "date": date,
            "speaker": speaker, # 발언자
            "text": speech, # 발언
            "summary": None,  # 요약은 나중에 추가
            'timestamp': datetime.now(), # 편집날짜
        })

    return speech_list

 

근데, elastic search는 어떻게 사용하나요?

아하, 저도 잘 몰라서 요것저것 알아봤습니다!

생각보다 딱 정리된 블로그가 없어서 애 좀 먹었습니다.

 

먼저 local로 설치하는 방법과 docker로 사용하는 방법이 있는데 손쉽게 하기 위해서 docker를 사용하기로 했습니다!

[STEP 3 : Docker를 이용해 elasticSearch를 가져온다.]

 

docker run -d --name elasticsearch -p 9200:9200 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:8.6.0

이렇게 설정해주면 된다.

그리고 .env에 넣어주면 된다.

근데, password라고 환경변수를 설정하지 말자. 자꾸 오류가 난다. 

나는 결국, ELASTIC_SEARCH_PW라고 변수명을 바꾸고 나서야 됐다.

만약, 이걸 통해 비번을 바꿔주지 않는다면 로그를 확인하면 http traffic on an https channel을 볼 수 있고

elastic_transport.ConnectionError: 가 나오는 것을 확인할 수 있다. 

docker logs elasticsearch

elastic_transport.ConnectionError: Connection error caused by: ConnectionError(Connection error caused by: ProtocolError(('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))))

[STEP 4 : elasticSearch에 pdf data를 추가한다.]

def connect_to_elasticsearch():
    """Elasticsearch에 연결하는 함수"""
    load_dotenv()

    Elasticsearch_key = os.getenv("ELASTIC_SEARCH_PW")
    # Elasticsearch 연결
    es = Elasticsearch(
        "https://localhost:9200",
        basic_auth=("elastic", Elasticsearch_key),  # 인증 추가
        verify_certs=False  # 자체 서명된 인증서 문제 방지
        )

    # 인덱스 설정 (인덱스가 없으면 생성)
    index_name = "congress_meetings"

    if not es.indices.exists(index=index_name):
        es.indices.create(index=index_name)
        
    return es,index_name

def save_to_elasticsearch(data):
    """ 추출된 국회의원 발언 데이터를 Elasticsearch에 저장하는 함수 """
    es, index_name = connect_to_elasticsearch()
    for entry in data:
        es.index(index=index_name, body=entry)
    print("Elasticsearch 저장 완료!")

 

[STEP 5: elasticSearch에 들어간 내용 확인하기.]

from elasticsearch import Elasticsearch
import os

# .env에서 환경변수 불러오기
from dotenv import load_dotenv
load_dotenv()

# 환경변수에서 비밀번호 가져오기
ELASTIC_PASSWORD = os.getenv("ELASTIC_SEARCH_PW")

# Elasticsearch 클라이언트 설정
es = Elasticsearch(
    "https://localhost:9200",
    basic_auth=("elastic", ELASTIC_PASSWORD),  # 인증 추가
    verify_certs=False  # 자체 서명된 인증서 문제 방지
)

# 인덱스명 설정 (저장된 데이터가 들어 있는 인덱스)
index_name = "congress_meetings"

# 모든 데이터 조회
def get_all_documents():
    query = {
        "query": {
            "match_all": {}
        }
    }
    
    res = es.search(index=index_name, body=query, size=10)  # 최대 10개 문서 조회
    return res['hits']['hits']

# 데이터 가져오기
documents = get_all_documents()
for doc in documents:
    print(f"문서 ID: {doc['_id']}, 내용: {doc['_source']}")

원하는 모습으로 나온 것을 확인할 수 있다~

https://open.assembly.go.kr/portal/data/service/selectServicePage.do?infId=OO1X9P001017YF13038&infSeq=1&isInfsPop=Y

 

열린국회정보

국회를 열다, 정보를 나누다.

open.assembly.go.kr

 

여기부터 필수인자를 어디서 구해오는지 알아야 한다.

국회의원 본 회의록을 API를 통해서 가져오려고 합니다.

그럼 API를 어떻게 가져오는 지 인자를 확인합니다.

 

기본인자와 요청인자의 타입 안에 형식과 필수 여부가 있는데 필수라고 적힌 데이터를 가져와야 합니다.

근데, 대수와 회의날짜는 어디에 있는 걸까요?

 

다른 api에서 찾아오면 됩니다.

 

https://open.assembly.go.kr/portal/data/service/selectServicePage.do?infId=ORDPSW001070QH19059&infSeq=1&isInfsPop=Y

 

열린국회정보

국회를 열다, 정보를 나누다.

open.assembly.go.kr

열린 국회는 본회의 일정이라는 명시적인 이름으로 데이터를 제공합니다.

여기서 회의날짜를 가져오면 될 거 같습니다.

여기도 또 대수가 나왔다.

여기도 마찬가지로 대수가 나왔는데,
대수는 국회의원 대수고 예시에 맞춰서 '100022'면 22대 국회의원을 가져옵니다.

어디 나와있는 건 아닌데, 직관적으로 값을 넣어야 할 때도 있는 거 같습니다.

 

 

[STEP 1] 회의록 일정 가져오기

그럼 일정부터 json으로 저장하겠습니다.

 

import requests
from dotenv import load_dotenv
import json
import os

# 나중에 airflow에 추가되야 하는 코드

load_dotenv()
key = os.getenv("OPEN_GOVERMETN_API_KEY")
url = "https://open.assembly.go.kr/portal/openapi/nekcaiymatialqlxr"

params = {
    "KEY" : key,
    "Type" : 'json',
    "pIndex" : '1',
    "pSize" : "100",
    "UNIT_CD" : "100022",
}

# 요청 받기
response = requests.get(url=url, params=params)

# json으로 저장 받기
file_path = 'congress_meeting/congress_schedule.json'
with open(file_path, 'w') as f:
    json.dump(response.json(), f)
    
print(response.json())

 

 

 

[STEP 2] 본회의 기록 가져오기

 

이제 json을 저장했으니 다시 본 회의 회의록을 구하러 가봅시다~

 

받아온 json 안에서 국회 일정 리스트로 필수인자들을 가져오고
반복문을 통해서 다시 회의록이 담긴 json을 가져오면 됩니다~

 

import requests
from dotenv import load_dotenv
import os
import json 

# 추후 dag로 변경될 필요 있음

load_dotenv()
key = os.getenv("OPEN_GOVERMETN_API_KEY")

def get_conf_dates():
    # 국회 일정 리스트 가져오기
    conf_date_list = []
    with open("congress_meeting/congress_schedule.json" ,'r') as file:
        schedule_file = json.load(file)
        data_count = schedule_file['nekcaiymatialqlxr'][0]['head'][0]['list_total_count'] # 국회 일정 개수
        for i in range(data_count):
            conf_date_list.append(schedule_file['nekcaiymatialqlxr'][1]['row'][i]['MEETTING_DATE'])
        return conf_date_list
        
def get_responses():
    # 국회 회의록 가져오기
    url = "https://open.assembly.go.kr/portal/openapi/nzbyfwhwaoanttzje"
    conf_date_list = get_conf_dates()
    for conf_date in conf_date_list:
        print(f"데이터를 저장중입니다.{conf_date}")
        params = {
            "KEY" : key,
            "Type" : 'json',
            "pIndex" : '1',
            "pSize" : "100",
            "DAE_NUM" : "22",
            "CONF_DATE" : conf_date
        }

        response = requests.get(url=url, params=params)
        if response.status_code == 200:
            with open (f"congress_meeting/meetings/{conf_date}.json",'w') as f:
                json.dump(response.json(), f)
        else:
            print(response.text)
            response.raise_for_status()
        
if __name__ == "__main__":
    get_responses()

 

디렉토리를 하나 만들어서 json 파일들을 따르륵 저장해줍니다. 

 

[STEP 3] pdfplumber로 URL에서 pdf 글자로 추출

이후에는 안에 pdf 파일들을 가져와서 pdfplumber를 사용해 글자만 가져옵니다.

pdf를 저장하지 않고 일단은 메모리를 통해 내용을 추출해옵니다.

import requests
import pdfplumber
import io

# PDF 다운로드 URL
url = "http://likms.assembly.go.kr/record/mhs-10-040-0040.do?conferNum=054706&fileId=0000124454&deviceGubun=P&imsiYn=P"

# HTTP 요청하여 PDF 다운로드
response = requests.get(url)
response.raise_for_status()  # 오류 발생 시 예외 처리

# PDF 내용을 메모리에서 직접 읽기
with pdfplumber.open(io.BytesIO(response.content)) as pdf: # 반면, io.BytesIO를 사용하면 메모리에서 직접 처리할 수 있어 더 효율적입니다.
    for page in pdf.pages:
        print(page.extract_text())  # 페이지별 텍스트 출력
        
# 이후에는 이 내용을 어떻게 저장할지 데이터베이스로 만들어야 함

 

이렇게 하면 저장된 데이터에 pdf를 읽어올 수 있고 이를 반복문으로 json 파일에서 읽게 해오면 됩니다.

 

[STEP 4] URL에서 PDF 추출 후 데이터베이스에 적재

근데 어떻게 해야 데이터를 효율적으로 저장할 수 있을 지 고민이 됩니다.
1. full text

2. csv or json

3. elastic search

 

가장 사용해보고 싶은 방식은 elastich search로 가져오고 싶습니다. 

 

왜냐하면 국회의원 별로 어떤 발언을 했는 지 요약을 하기 위함입니다.

 

elastic search에 역색인을 통해서 국회의원 발언을 검색해서 국회의원 별 발언을 정리해보고 싶습니다.

 

그래서 데이터베이스를 어떤 모양으로 만들어야 할지가 관건입니다. 

 

국회의원만 있으면 데이터가 처리되는지 국회의 진행방식과 pdf를 눈으로 확인하고 데이터를 어떻게 저장할지 결정해야 할 거 같습니다.

    params = {
        'serviceKey': serviceKey,
        'pageNo': '1',        # 페이지 번호
        'numOfRows': '10',    # 한 번에 가져올 데이터 개수
        'resultType': 'json', # 응답 형식 (JSON)
        'sgId': '20200415',   # 선거 ID
        'sgTypecode': '2',    # 선거 구분 코드
        'cnddtId': '100135069', # 후보자 ID
    }
 

값은 당선인정보api에서 가져와 csv에 제대로 넣은 거 같은데 무엇이 문제일까?

11,20200415,2,100135069,성북구갑,서울특별시,성북구,1,,더불어민주당,김영배,金永培,남,19670308,53,서울특별시 성북구 고려대로2길,75,정당인,68,고려대학교 정치외교학과 졸업,"(전) 민선5기, 6기 성북구청장",(전) 문재인대통령 청와대비서관(정책조정/민정),82954,60.9

 



국회의원은 공약서가 있지 않았다....

이게 요청 메세지가 아닌 응답 메세지에 있어서 확인에 시간이 걸렸다 국회의원은 api로 확인할 수 없음을 확인하였다. 

어쩔 수 없이 공약 크롤링이 필요한 시점이 되었다.

https://policy.nec.go.kr

 

title

 

policy.nec.go.kr

 

우리위원회는 정당·후보자에게 ‘문자인식이 되는 PDF 파일’을 제출하여 시각장애인의 접근성·편의성을 확보토록 안내하고 있습니다

 

다행히 pdf에서 글자를 추출할 수 있다. 이제는 이름과 명칭을 연결해서 데이터를 만들어야 한다,,,

 

매우 귀찮아지긴 했지만, 그래도 할 수 있는 방향이 생겼다. 

https://do-one-more.tistory.com/7

 

공공 api로 국회의원 코드 읽어오기 - python

[STEP 1 ]우선 공공 api에 회원가입 후 선거 공약 정보를 활용 신청한다. 그다음 스크롤을 내리면 필수적으로 얻어야 하는 정보들을 볼 수 있다.추가로 얻어야 하는 params 정보는 선거ID, 선거 종류

do-one-more.tistory.com

 

 

저번에는 공공 api로 코드를 읽어 json 파일로 확인까지 했다

이번에는 json파일을 pandas를 이용해 df 형식으로 바꾼 뒤 CSV로 저장해줬다.

 

[STEP : 1] JSON파일을 df로 전환한 뒤, csv파일에 합치고 csv 파일로 저장하기

import requests
from dotenv import load_dotenv
import json
import os
import pandas as pd
        
def read_api_to_df(url,params):
    response = requests.get(url, params=params)
    
    if response.status_code == 200:
        print(f"API READ CORRECTLY {response.status_code}")
    else:
        print(f"Error Code :{response.status_code}")
    
    response_json = json.loads(response.content)
    json_to_df = pd.json_normalize(response_json)
    
    return json_to_df

def api_to_csv(url):
    
    df = pd.DataFrame()
    file_path = 'public_vote_code'
    
    for i in range(1,1001):
        params = {'serviceKey' : serviceKey, 'resultType' : 'json', 'pageNo' : f'{i}', 'numOfRows' : '100'}
        new_df = read_api_to_df(url,params=params)
        df = pd.concat([df, new_df])
        
    df.to_csv("public_vote_code.csv")
        
if __name__ == "__main__":    
    # api key
    load_dotenv()
    serviceKey = os.getenv("GONGGONG_API_KEY")
    
    # 공공데이터 국회의원 선거공약정보확인을 위한 코드 정보
    govcode_url = "http://apis.data.go.kr/9760000/CommonCodeService/getCommonSgCodeList"
    
    # api code csv로 저장
    api_to_csv(govcode_url)

완전히 이상한 결과가 나왔다.

json파일 안에 json 파일이 겹겹이 쌓인 형식이어서 값이 원하는 것과 다르게 나왔다.
그리고 데이터가 그렇게 많지도 않았다. response.body.items.item에 있는 값만 가져오도록 다시 만들어야 할 거 같다.

 

[STEP 2] json안에 있는 response.body.items.item만으로 csv파일 만들도록 하기

import requests
from dotenv import load_dotenv
import json
import os
import pandas as pd
        
def read_api_to_df(url,params):
    try:
        response = requests.get(url, params=params)
        response.raise_for_status()  # HTTP 에러가 있으면 예외 발생
    except requests.exceptions.RequestException as e:
        print(f"Error during API request: {e}")
        return pd.DataFrame()

	# 이 부분에서 items까지 가져오도록 만듦
    try: 
        response_json = response.json()
        items = response_json.get('response', {}).get('body', {}).get('items', {}).get('item', [])
        print(items)

        if items:
            json_to_df = pd.json_normalize(items)
        else:
            print("No items found in response")
            return pd.DataFrame()
            
    except (KeyError, ValueError, TypeError) as e:
        print(f"Error during JSON processing: {e}")
        return pd.DataFrame()
    
    return json_to_df

def api_to_csv(url):
    
    df = pd.DataFrame()
    file_path = 'public_vote_code'
    
    for i in range(1,10):
        params = {'serviceKey' : serviceKey, 'resultType' : 'json', 'pageNo' : f'{i}', 'numOfRows' : '100'}
        new_df = read_api_to_df(url,params=params)
        df = pd.concat([df, new_df])
        
    df.to_csv("public_vote_code.csv")
        
if __name__ == "__main__":    
    # api key
    load_dotenv()
    serviceKey = os.getenv("GONGGONG_API_KEY")
    
    # 공공데이터 국회의원 선거공약정보확인을 위한 코드 정보
    govcode_url = "http://apis.data.go.kr/9760000/CommonCodeService/getCommonSgCodeList"
    
    # api code csv로 저장
    api_to_csv(govcode_url)

잘 들어온 모습을 확인할 수 있다.

[STEP 3] CSV에 있는 국회의원 선거 코드를 가져와서  국회의원 코드 가져오기

https://www.data.go.kr/tcs/dss/selectApiDataDetailView.do?publicDataPk=15000864

 

중앙선거관리위원회 당선인정보

당선인 정보를 조회 할 수 있는 서비스 이다. (선거종류, 선거구명, 시도명, 구시군명, 기호, 정당, 성명, 성별, 연령, 경력, 득표수, 득표율 등을 조회 가능)

www.data.go.kr

 

 

위에서 만든 csv에서 sgId를 list로 가져온 뒤, 당선인 정보를 가져온다.

import requests
from dotenv import load_dotenv
import json
import os
import pandas as pd

def get_votecode():
    try:
        csv_file = pd.read_csv('public_vote_code.csv')
        congress_vote_id_list = csv_file[csv_file['sgTypecode'] == 2]['sgId'].to_list()
        return congress_vote_id_list
    except FileNotFoundError:
        print("Error: 'public_vote_code.csv' not found.")
        return []

def read_api_to_df(url, params):
    try:
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        print(f"Response status code: {response.status_code}")
    except requests.exceptions.RequestException as e:
        print(f"Error during API request: {e}")
        return pd.DataFrame()

    try:
        response_json = response.json()
        items = response_json.get('response', {}).get('body', {}).get('items', {}).get('item', [])
        if items:
            return pd.json_normalize(items)
        else:
            print("No items found in response")
            return pd.DataFrame()
    except json.JSONDecodeError as e:
        print(f"Error decoding JSON: {e}")
        return pd.DataFrame()
    except (KeyError, ValueError, TypeError) as e:
        print(f"Error during JSON processing: {e}")
        return pd.DataFrame()

def api_to_csv(url):
    serviceKey = load_api_key()
    
    df = pd.DataFrame()
    vote_code = get_votecode()
    if not vote_code:
        print("Error: No vote codes found.")
        return
    
    file_path = "vote_erection.csv"
    
    for id in vote_code:
        for i in range(1, 10):
            params = {
                'serviceKey': f'{serviceKey}',
                'pageNo': f'{i}',
                'resultType': 'json',
                'numOfRows': '100',
                'sgId': f'{id}',
                'sgTypecode': '2',
                'sdName': '',
                'sggName': ''
            }
            new_df = read_api_to_df(url, params=params)
            df = pd.concat([df, new_df], ignore_index=True)
    
    df.drop_duplicates(inplace=True)
    df.to_csv(file_path, index=False, encoding='utf-8-sig')
    print(f"Data saved to {file_path}")

def load_api_key():
    load_dotenv()
    serviceKey = os.getenv("GONGGONG_API_KEY")
    if not serviceKey:
        raise ValueError("API key not found in environment variables.")
    return serviceKey

if __name__ == "__main__":
    erection_url = 'http://apis.data.go.kr/9760000/WinnerInfoInqireService2/getWinnerInfoInqire'
    api_to_csv(erection_url)

 

[step 5] 국회의원 당선인 정보를 가지고 국회의원의 공약을 확인한다.

2개의 컬럼값을 가져오려면 DataFrame 형태로 가져오기에 to_list()가 되지 않고 to_dict로 가져와야한다.

그리고 각 레코드를 읽어서 저장하기 위해 to_dict('records')로 추가로 선정한다.

import requests
from dotenv import load_dotenv
import json
import os
import pandas as pd

def get_votecode():
    try:
        csv_file = pd.read_csv('vote_erection.csv')
        pledge_code_list = [(row['huboid'], row['sgId']) for row in csv_file[['huboid', 'sgId']].to_dict('records')]
        return pledge_code_list
    except FileNotFoundError:
        print("Error: 'vote_erection.csv' not found.")
        return []

def read_api_to_df(url, params):
    try:
        response = requests.get(url, params=params)
        response.raise_for_status()
        print(f"Response status code: {response.status_code}")
    except requests.exceptions.RequestException as e:
        print(f"Error during API request: {e}")
        return pd.DataFrame()

    try:
        response_json = response.json()
        items = response_json.get('response', {}).get('body', {}).get('items', {}).get('item', [])
        if items:
            return pd.json_normalize(items)
        else:
            print("No items found in response")
            return pd.DataFrame()
    except json.JSONDecodeError as e:
        print(f"Error decoding JSON: {e}")
        return pd.DataFrame()
    except (KeyError, ValueError, TypeError) as e:
        print(f"Error during JSON processing: {e}")
        return pd.DataFrame()

def api_to_csv(url):
    serviceKey = load_api_key()
    
    df = pd.DataFrame()
    pledge_code_list = get_votecode()
    if not pledge_code_list:
        print("Error: No vote codes found.")
        return
    
    file_path = "vote_pledge.csv"
    
    for huboid,sgId in pledge_code_list:
        for i in range(1, 10):
            params = {
                'serviceKey': f'{serviceKey}',
                'pageNo': f'{i}',
                'resultType': 'json',
                'numOfRows': '100',
                'cnddtId' : f'{huboid}',
                'sgId': f'{sgId}',
                'sgTypecode': '2',
                'sdName': '',
                'sggName': ''
            }
            new_df = read_api_to_df(url, params=params)
            df = pd.concat([df, new_df], ignore_index=True)
    
    df.drop_duplicates(inplace=True)
    df.to_csv(file_path, index=False, encoding='utf-8-sig')
    print(f"Data saved to {file_path}")

def load_api_key():
    load_dotenv()
    serviceKey = os.getenv("GONGGONG_API_KEY")
    if not serviceKey:
        raise ValueError("API key not found in environment variables.")
    return serviceKey

if __name__ == "__main__":
    pledge_url = "http://apis.data.go.kr/9760000/ElecPrmsInfoInqireService/getCnddtElecPrmsInfoInqire"    
    api_to_csv(pledge_url)


트래픽 1000개가 생각보다 적기 때문에 날짜를 확인해서 특정날짜로 제한해야할 수도 있겠다

[STEP 1 ]우선 공공 api에 회원가입 후 선거 공약 정보를 활용 신청한다. 

그다음 스크롤을 내리면 필수적으로 얻어야 하는 정보들을 볼 수 있다.

추가로 얻어야 하는 params 정보는 선거ID, 선거 종류 코드 후보자 ID

왜 필수 데이터를 어디서 받아와야 하는지 항목 설명에 적지 않았지?

 

[STEP : 2] 중앙선거 관리위원회 코드정보 데이터를 받아온다.

 

 

[STEP : 3]api를 통해 내용을 읽어오는 코드를 확인한다.

 

나는 dotenv를 통해서 .env에 인증키를 넣고 load_dotenv()로 읽어오는 방식을 사용해서 데이터를 숨겨서 가져왔다.

params에 들어가는 데이터는 decoding된 암호키를 읽어와야 request를 받을 수 있다.

 

이를 통해서 json 키를 읽어올 수 있다.

import requests
from dotenv import load_dotenv
import os

def api_test(url,params):
response = requests.get(url, params=params)

if response.status_code == 200:
print(response.content)
else:
print(f"Error Code :{response.status_code}")

if __name__ == "__main__":
 
load_dotenv()
serviceKey = os.getenv("GONGGONG_API_KEY")
 
# 공공데이터 국회의원 선거공약정보
pledge_params ={'serviceKey' : serviceKey, 'resultType' : 'json', 'pageNo' : '1', 'numOfRows' : '10', 'sgId' : '20231011', 'sgTypecode' : '4', 'cnddtId' : '1000000000' }
 
# 공공데이터 국회의원 선거공약정보확인을 위한 코드 정보
govcode_params = {'serviceKey' : serviceKey, 'resultType' : 'json'}
 
api_test(govcode_url,params=govcode_params)


 

이걸 통해 print를 확인하면 

 

b'{"response":{"header":{"resultCode":"INFO-00","resultMsg":"NORMAL SERVICE"},"body":{"items":{"item":[{"num":"1","sgId":"19920324","sgName":"\xec\xa0\x9c14\xeb\x8c\x80 \xea\xb5\xad\xed\x9a\x8c\xec\x9d\x98\xec\x9b\x90\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"0","sgVotedate":"19920324"},{"num":"2","sgId":"19920324","sgName":"\xea\xb5\xad\xed\x9a\x8c\xec\x9d\x98\xec\x9b\x90\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"2","sgVotedate":"19920324"},{"num":"3","sgId":"19921218","sgName":"\xec\xa0\x9c14\xeb\x8c\x80 \xeb\x8c\x80\xed\x86\xb5\xeb\xa0\xb9\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"0","sgVotedate":"19921218"},{"num":"4","sgId":"19921218","sgName":"\xeb\x8c\x80\xed\x86\xb5\xeb\xa0\xb9\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"1","sgVotedate":"19921218"},{"num":"5","sgId":"19960411","sgName":"\xec\xa0\x9c15\xeb\x8c\x80 \xea\xb5\xad\xed\x9a\x8c\xec\x9d\x98\xec\x9b\x90\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"0","sgVotedate":"19960411"},{"num":"6","sgId":"19960411","sgName":"\xea\xb5\xad\xed\x9a\x8c\xec\x9d\x98\xec\x9b\x90\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"2","sgVotedate":"19960411"},{"num":"7","sgId":"19971218","sgName":"\xec\xa0\x9c15\xeb\x8c\x80 \xeb\x8c\x80\xed\x86\xb5\xeb\xa0\xb9\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"0","sgVotedate":"19971218"},{"num":"8","sgId":"19971218","sgName":"\xeb\x8c\x80\xed\x86\xb5\xeb\xa0\xb9\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"1","sgVotedate":"19971218"},{"num":"9","sgId":"20000413","sgName":"\xec\xa0\x9c16\xeb\x8c\x80 \xea\xb5\xad\xed\x9a\x8c\xec\x9d\x98\xec\x9b\x90\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"0","sgVotedate":"20000413"},{"num":"10","sgId":"20000413","sgName":"\xea\xb5\xad\xed\x9a\x8c\xec\x9d\x98\xec\x9b\x90\xec\x84\xa0\xea\xb1\xb0","sgTypecode":"2","sgVotedate":"20000413"}]},"numOfRows":10,"pageNo":1,"totalCount":179}}}'

 

위와 같은 읽을 수 없는 b로 시작하는 바이너리 코드를 만날 수 있다. 이를 UTF-8로 디코딩 하거나 JSON으로 파싱하면 확인할 수 있다.

[STEP : 4] request한 데이터를 json 형태의 양식으로 확인하게 하는 코드를 작성한다.

import requests
from dotenv import load_dotenv
import json
import os

def api_test(url,params):
response = requests.get(url, params=params)

if response.status_code == 200:
print(json.loads(response.content))
else:
print(f"Error Code :{response.status_code}")

if __name__ == "__main__":
 
load_dotenv()
serviceKey = os.getenv("GONGGONG_API_KEY")
 
# 공공데이터 국회의원 선거공약정보
pledge_params ={'serviceKey' : serviceKey, 'resultType' : 'json', 'pageNo' : '1', 'numOfRows' : '10', 'sgId' : '20231011', 'sgTypecode' : '4', 'cnddtId' : '1000000000' }
 
# 공공데이터 국회의원 선거공약정보확인을 위한 코드 정보
govcode_params = {'serviceKey' : serviceKey, 'resultType' : 'json'}
 
api_test(govcode_url,params=govcode_params)

 

{'response': {'header': {'resultCode': 'INFO-00', 'resultMsg': 'NORMAL SERVICE'}, 'body': {'items': {'item': [{'num': '1', 'sgId': '19920324', 'sgName': '제14대 국회의원선거', 'sgTypecode': '0', 'sgVotedate': '19920324'}, {'num': '2', 'sgId': '19920324', 'sgName': '국회의원선거', 'sgTypecode': '2', 'sgVotedate': '19920324'}, {'num': '3', 'sgId': '19921218', 'sgName': '제14대 대통령선거', 'sgTypecode': '0', 'sgVotedate': '19921218'}, {'num': '4', 'sgId': '19921218', 'sgName': '대통령선거', 'sgTypecode': '1', 'sgVotedate': '19921218'}, {'num': '5', 'sgId': '19960411', 'sgName': '제15대 국회의원선거', 'sgTypecode': '0', 'sgVotedate': '19960411'}, {'num': '6', 'sgId': '19960411', 'sgName': '국회의원선거', 'sgTypecode': '2', 'sgVotedate': '19960411'}, {'num': '7', 'sgId': '19971218', 'sgName': '제15대 대통령선거', 'sgTypecode': '0', 'sgVotedate': '19971218'}, {'num': '8', 'sgId': '19971218', 'sgName': '대통령선거', 'sgTypecode': '1', 'sgVotedate': '19971218'}, {'num': '9', 'sgId': '20000413', 'sgName': '제16대 국회의원선거', 'sgTypecode': '0', 'sgVotedate': '20000413'}, {'num': '10', 'sgId': '20000413', 'sgName': '국회의원선거', 'sgTypecode': '2', 'sgVotedate': '20000413'}]}, 'numOfRows': 10, 'pageNo': 1, 'totalCount': 179}}}

 

원하는 결과가 나왔다.

 

이제 이 코드를 확인해서 데이터를 전처리 하고 당선인 정보를 확인한 뒤 선거 공약 정보를 추가할 수 있게 되었다.

 


결론 

공공 api를 request 하기 위해서는 params의 값을 추가해줘야 한다.

 

그 때 공공키는 디코딩된 암호를 추가해야 하고 받은 request는 바이너리 형태로 오기 때문에 json.load를 하거나 utf-8로 디코딩 해야 되는데 json 형식으로 저장하기 위해 json.load로 데이터를 저장하면 된다.

 

추가로 json으로 저장하는 코드를 작성하고 데이터를 확인해서 다른 api에 어떤 param으로 들어가야 하는지 추가 작업을 하면 될 거 같다.

깃허브 주소

https://github.com/son-kino/goverment_check_project.git

 

GitHub - son-kino/goverment_check_project: 국회의원의 국회의원 회의 참석율과 공약이행률 확인하는 airflow

국회의원의 국회의원 회의 참석율과 공약이행률 확인하는 airflow. Contribute to son-kino/goverment_check_project development by creating an account on GitHub.

github.com

 

우선 네이버 개발자 api를 통해서 애플리케이션 api 받아오기

application을 등록 후 발급 받은 모습, 웬만하면 잘 해주는 모양이다.

 

그 다음에 가져온 api를 테스트 해본다.

.env 파일에 넣어서 외부 사용자가 보지 못하도록 보안을 처리한 다음 환경 변수로 값들을 읽어온다.

 

뉴스에 넣기 원하는 검색어를 가져와서 query={}부분에 넣어준다.

 

헤더에 넣어서 api를 요청하면 거기에 맞는 값이 나올 수 있도록 설정했다.

 

이후 검색어를 다양화하고 Elastic Search를 통해서 원하는 값들을 가져올 예정이다.

import os
import requests
from dotenv import load_dotenv

# .env 파일 로드
load_dotenv()

# 환경 변수 읽기
client_id = os.getenv("NAVER_API_ID")
client_secret = os.getenv("NAVER_API_PW")

# 검색어와 URL 설정
enc_text = "국회의원"

# 헤더 추가
headers = {
"X-Naver-Client-Id": client_id,
"X-Naver-Client-Secret": client_secret
}

# API 요청
response = requests.get(url, headers=headers)

# 응답 확인 및 출력
if response.status_code == 200:
print(response.json()) # JSON 형태로 출력
else:
print(f"Error Code: {response.status_code}")

+ Recent posts