데린이 재영

서울특별시 다산콜센터(☎120)의 주요 민원 수집하기 - Pandas/Requests/BeautifulSoup/tqdm 본문

멋사 AI school 7기/TIL

서울특별시 다산콜센터(☎120)의 주요 민원 수집하기 - Pandas/Requests/BeautifulSoup/tqdm

재용용 2022. 10. 11. 13:21

목표 설정

- 멋쟁이사자 AI 스쿨 11, 12일차(221004, 221005) 학습 내용 정리하기

- 서울정보소통광장 ▶ 시민소통 ▶ 120 주요 민원 수집하기


데이터 수집 과정

Fig 0. 데이터를 수집할 사이트 - 서울정보소통광장

1. 라이브러리 로드

# 파이썬에서 사용할 수 있는 엑셀과 유사한 데이터분석 도구
import pandas as pd
import numpy as np
# 매우 작은 브라우저로 웹사이트의 내용과 정보를 불러옴
import requests
# request로 가져온 웹사이트의 html 태그를 찾기위해 사용
from bs4 import BeautifulSoup as bs
# 간격을 두고 가져오기 위해 사용
import time
# 진행 상황 확인하기
from tqdm.notebook import tqdm

2. url 가져오기

# 페이지별 url
page_no = 1
url = f"https://opengov.seoul.go.kr/civilappeal/list?page={page_no}"

3. url의 table 정보 가져오기

# url에 대한 데이터 수집
pd.read_html(url)

Fig 1. 한글 지원 하지 않는 경우

 

위와 같이, 한글이 깨지는 현상 발견 ▶ url 의 인코딩 방식 확인하기 ( Network Headers Response Headers Context-Type)

 

Fig 2. 페이지에서 사용된 인코딩 방식 확인

# 한글 깨짐 현상을 방지하고, 수집된 데이터를 df로 지정
df = pd.read_html(url, encoding="utf-8")[0]

위 코드에서 [0]를 사용한 이유 ?

pd.read_html로 데이터를 불러오면 데이터프레임으로 불러올 수 있는데, 수집된 데이터들은 데이터 프레임으로, 리스트로 묶여서 반환된다.

즉 여러 데이터프레임들이 리스트 안에 들어가는데, 이때 필요한 부분을 가져오기 위해 인덱스 번호를 이용했다.

[0] : 필요한 데이터가 0번째 인덱스에 있다는 뜻

# 수집한 민원 정보를 table 이라는 변수로 가져오기
df = pd.read_html(url, encoding="utf-8")[0]
df

Fig 3. df에 저장된 데이터

4. 민원 내용 번호 수집하기

아래 링크는 '하반기 농부의 시장 일정은 어떻게 되나요?' 를 클릭하면 볼 수 있는 url 이다.

 

Fig 4. df[0] 에 대한 링크 url

 

각 질문에 대한 내용번호를 가져오는 이유는, 각 민원에 대한 문서 정보 제공부서와 분류 데이터를 추가하기 위해서이다.

 

Fig 5. 추가할 데이터, 빨간색 박스 부분


(i) 내용번호 태그 찾기

# html을 안전하게 가져오기
response = requests.get(url, headers={'User_agent':'Mozilla/5.0'})
response

# 가져온 html을 text로 변환
response.text # Fig 6의 결과물

# beautifulsoup으로 복잡한 태그들을 보기 좋게 구조화하기
html = bs(response.text)
html # Fig 7의 결과물

** BeautifulSoup 를 사용하는 이유?

복잡한 html 구조를 보기 좋게 바꾸어 원하는 태그를 쉽게 찾기 위해서 사용한다. (아래 사진을 비교해 보면 이유를 쉽게 알 수 있다.)

 

Fig 6. BeautifulSoup 없이, html text 가져왔을 때

 

Fig 7. BeautifulSoup 으로 html text 가져왔을 때

 

Fig 8. html 결과물에서, 필요한 태그 발견


** Copy Selector 를 사용하는 이유?

사이트에서 원하는 태그 위치를 빠르고 쉽게 가져올 수 있음

# content > div > div.view-content > div > table > tbody > tr:nth-child(1) > td.data-title.aLeft > a
# 태그 전체를 select 에 가져올 필요는 없음
a_list = html.select("td.data-title.aLeft > a")

 

Fig 9. Copy Selector 로 태그 위치 가져오는 방법


(ii) 내용번호 DataFrame 에 추가하기

# a 태그에서 내용번호만 가져와 리스트에 넣기
# list comprehension 으로 수집 
a_links = [tag["href"].split("/")[-1] for tag in a_list]

# 가져온 내용번호를 df에 추가하기
df["내용번호"] = a_links

Fig 10. 내용번호 컬럼이 추가된 df

5. 전체 페이지 목록 수집하기

# 지정한 페이지 가져오기    
def get_one_page(page_no):
    # 1) url 가져오기
    url = f"https://opengov.seoul.go.kr/civilappeal/list?&page={page_no}"
    # 2) requests 로 HTTP 요창
    response = requests.get(url)
    # 3) table tag로 둘러쌓인 게시물 가져오기
    df = pd.read_html(response.text, encoding="utf-8")[0]

    # 4) df 행이 0개면 페이지를 찾을 수 없다는 메시지를 반환하기
    if df.shape[0] == 0:
        return f"{page_no} 페이지를 찾을 수 없습니다."

    # 5) 내용 번호 컬럼 추가하기
    # 오류 예외 처리 기법 : try 수행 중 오류 발생하면 except 블록 수행
    try:
        html = bs(response.text)
        a_list = html.select("td.data-title.aLeft > a")
        df["내용번호"] = [a_tag["href"].split("/")[-1] for a_tag in a_list]
    except:
        return f"{page_no} 페이지를 찾을 수 없습니다."
            
    return df

# 모든 페이지 목록 가져오기
page_no = 1
all_page_list = []

while True:
    one_page_df = get_one_page(page_no)
    # 반복문 종료 시험 -> 페이지에서 수집된 df이 df 형태가 아닐 때 멈추기
    if type(one_page_df) != pd.core.frame.DataFrame:
        break
    # 한 페이지에 대한 목차를 리스트에 추가하기
    all_page_list.append(one_page_df)
    # 페이지 수 변경
    page_no += 1
    # 서버 과부하 방지를 위해 시간 텀 두기
    time.sleep(0.01)

# 데이터 합치기
df = pd.concat(all_page_list)

# 데이터 저장하기
file_name = "seoul-120-questions.csv"
df.to_csv(file_name, index=False)

6. 특정 내용 수집하기

# 각 내용번호에 대한 url 가져오기
a_number = 26695536 # 하반기 농부의 시장 일정은 어떻게 되나요? 질문에 대한 내용 번호
a_url = f"https://opengov.seoul.go.kr/civilappeal/{a_number}"

(i) 제공부서, 분류 가져오기

# Response 200 확인하기
a_response = requests.get(a_url, headers={'User_agent':'Mozilla/5.0'})
a_response

# 데이터 프레임으로 가져오기
a_df = pd.read_html(a_response.text)
a_df # Fig 11 결과물

# 특정 부분을 df_1 지정
df_1 = a_df[-1][[2, 3]].set_index(2).T
df_1 # Fig 12 결과물

** BeautifulSoup 없이 바로 pd.read_html 를 사용하는 이유?

필요한 부분이 table 태그로 묶여있었기 때문에 태그 찾는 과정을 생략 하였다.

 

Fig 11. read_html로 가져온 table 태그 속 요소들

 

Fig 12. '제공부서, 생산일, 분류' 만 가져 옴


(ii) 문서 내용 가져오기

# 내용 수집 할 url 가져오기
url = "https://opengov.seoul.go.kr/civilappeal/view/?nid=23194045"
response = requests.get(url)
html = bs(response.text)
# Copy Selector 로 태그 위치 가져오기
# content > div > div.view-content.view-content-article > div:nth-child(2) > div
content = html.select("div.line-all")[0].text # Fig 13 결과물
# \x\n 등이 포함되어 있어 제거를 위해 split 후 join 해 줌
content_list = content.split()
detail = " ".join(content_list) # Fig 14 결과물

** split, join 사용한 이유?

copy selector 로 text를 가져오면, \를 포함한 여러 기호들이 포함됨 

이를 제거하기 위해 split , join 사용 ( 제거를 위해 정규표현식을 이용해보려고 했으나 실패... 그냥 split 해봤는데 됨 )

 

Fig 13. 기호들 포함
Fig 14. split -> join 후 결과


7. 내용 추가하기

# 전체 페이지별 목록이 담긴 데이터 불러오기
df = pd.read_csv("seoul-120-questions.csv")

# 제공부서, 생성일, 분류 추가 함수 
def get_details(response):   
    table = pd.read_html(response.text)[-1]
    # 특정 내용만 가져오고, T로 행 열 전환
    tb = table[[2, 3]].set_index(2).T
    return tb

# 문서 내용 추가하기
def get_one_view(view_no):
    url = f"https://opengov.seoul.go.kr/civilappeal/view/?nid={view_no}"
    response = requests.get(url)
    tb = get_details(response)
    html = bs(response.text)
    # 문서 내용 부분을 content 로 가져옴
    content = html.select("div.line-all")[0].text
    # 문서 내용에 \n\x 등 특수문자가 포함되어서 제거를 위해 split 메서드 사용
    content_list = content.split()
    # 특수문자 제거 후, join으로 string 만들기
    detail = " ".join(content_list)
    tb["내용"] = detail
    tb["내용번호"] = view_no
    # 서버 과부하 방지를 위해 시간 텀 두기
    time.sleep(0.01)
    
    return tb
    
# 반복 작업에서 진행 상황을 확인하기 위해 tqdm, progress_map 사용
tqdm.pandas()
details = df["내용번호"].progress_map(get_one_view)

# 추가한 내용들을 df_detail 에 담아 하나의 DataFrame 으로 합치기
df_detail = pd.concat(details.tolist())

# 데이터 프레임 2개를 "내용번호"를 기준으로 합치기
df = df.merge(df_detail, on=["내용번호"])
# 컬럼 순서 바꾸기
df = df[["번호", "분류", "제목", "내용", "내용번호"]]

Fig 15. 최종 결과물

 

Comments