Data Science/Side

[KDT] 서울시 다산콜센터(☎120)의 질문 답변 내용 수집 - Python Web Crawling

나는 정은 2022. 10. 6. 00:40

멋쟁이 사자처럼 4주차
2022.10.05
서울특별시 다산콜센터 자주 묻는 질문 & 답변 데이터 수집 (웹 크롤링)

# Pseudo-Code
""" 
    분류 수집하기 
    1) reponse로 요청한 코드를 text로 받아옴
    2) 테이블의 각 칼럼명에 해당하는 0열, 2열에 대해 두 테이블로 나눈 뒤 table Transpose 적용 
    2-1) 세로로 들어가 있는 칼럼명을 일자로 펴줌
    3) 두 테이블의 인덱스를 맞춰준 다음 테이블 columns 기준 병합 (concat)
"""

""" 
    답변 내용과 분류를 수집

    1) url 생성
    2) requests 로 요청 전송
    3) 분류 수집 함수를 만들고 response 값을 넘겨 분류와 제공부서 등이 들어있는 데이터프레임 반환
    4) 내용을 bs를 사용하여 텍스트만 추출
    5) 3)번 내용에 내용, 내용번호를 함께 데이터프레임에 추가
    6) 반복문 대신 map이나 apply를 사용할 것이기 때문에 time.sleep으로 딜레이 발생
    7) 5번까지 수집한 내용을 반환
"""
 

0. 라이브러리 로드

import pandas as pd
import requests 
from bs4 import BeautifulSoup as bs
import time 
  • pandas : 파이썬에서 사용할 수 있는 엑셀과 유사한 데이터분석 도구
  • requests : 매우 작은 브라우저로 웹사이트의 내용과 정보 요청
  • BeautifulSoup : request로 가져온 웹사이트의 html 태그를 탐색하게 도와주는 모듈
  • time : 서버의 부담을 덜어주기 위해 시간 간격을 두고 데이터를 수집

질문 내용 데이터 로드

df = pd.read_csv('seoul-120-list.csv')
 

1. 문서 본문과 문서 정보 데이터 수집

  • 데이터는 먼저 일부만 수집한 뒤 필요에 따라 전체로 수집 범위를 확장하는 것이 좋다.
    • 잘못 수집하거나 코드 동작이 되지 않을 경우의 에러 상황 대비
    • 서버 부담 감소

답변 내용 (문서 본문) 읽어오기

# sample url 
url = "https://opengov.seoul.go.kr/civilappeal/view/?nid=23194045"

response = requests.get(url)
html = bs(response.text)
content = html.select('div > div.view-content.view-content-article > div > div.line-all')[0].get_text()
content
 
  • 내용의 URL을 확인한뒤 requests를 통해 내용에 접근한다.
  • BeautifulSoup의 select를 사용해 답변 내용이 있는 태그를 탐색하고 html 코드로 파싱한다.
    • html 태그에서 "div.line-all"의 0번째 값을 선택(select)하여 text를 확인

문서 정보 Table 데이터 수집 및 변환

table = pd.read_html(response.text)
table
[   0                                                  1
 0  ○  HWPX 파일은 공공 및 민간 분야 문서에 대한 개방성 확보를 위해 ㈜한글과컴퓨터에...
 1  ○  서울정보소통광장에서 결재문서 열람 시 HWPX 파일이 열리지 않는 경우, 한컴오피스...,
           0               1     2           3
 0     원본시스템           다산콜센터  제공부서     서울산업진흥원
 1  작성자(책임자)        120다산콜재단   생산일  2021-06-29
 2      관리번호  D0000042894548    분류          경제]
 
  • 위 Table의 마지막에 저장된 테이블을 가지고 데이터를 원하는 형태로 변환한다.
    • 원본시스템, 작성자, 관리번호, / 제공부서, 생산일, 분류는 columns에 해당하는 데이터이다.
      • 칼럼명 1 / 값 1 / 칼럼명 2 / 값 2 로 구성된 테이블을 2개로 나눠 하나의 행으로 concat해보자.
table_1 = table[[0,1]].set_index(0).T.reset_index(drop=True)
table_1

table_2 = table[[2,3]].set_index(2).T.reset_index(drop=True)
table_2.index = table_1.index
table_2

df_add = pd.concat([table_1, table_2], axis=1)
  • df.T : 인덱스를 분류명으로 지정해준 다음, 전치 행렬로 바꿔주게 되면 새 칼럼으로 사용할 수 있게 된다.
  • concat() : table_1 기준으로 index를 맞춰준 table 2개를 열 기준으로 이어붙인다.
 

 

위 과정을 함수로 만들면 다음과 같다.

def get_desc(response):
    table = pd.read_html(response.text)[-1]
    tb01 = table[[0, 1]].set_index(0).T
    tb02 = table[[2, 3]].set_index(2).T
    tb02.index = tb01.index
    df_desc = pd.concat([tb01, tb02], axis=1)
    return df_desc
 
 

전체 답변 내용 가져오기

전체 데이터를 수집하는 함수 만들기

위에서 수집한 데이터 테이블들을 합쳐 답변내용과 답변 분류를 담은 한 개의 DataFrame을 만들어보자.

def get_view_page(view_no):
    url = f"https://opengov.seoul.go.kr/civilappeal/view/?nid={view_no}"
    response = requests.get(url)
    df_desc = get_desc(response)
    html = bs(response.text)
    content = html.select("div.view-content.view-content-article > div > div.line-all")[0].text
    
    df_desc["내용"] = content
    df_desc["내용번호"] = view_no
    
    time.sleep(0.01)
    return df_desc
  • view_no : 기존에 수집했던 질문에 대한 일련 코드를 입력받는다.
    • '내용 번호' 변수의 값이다.
  • content : requests - bs4 로 불러온 html 코드의 내용 부분의 첫 번째 내용을 문자열로 가져온다.
  • df_desc : html의 마지막부분인 문서 정보에 대한 내용을 전처리하는 함수가 반환한 DataFrame이다.

DataFrame mapping

from tqdm.notebook import tqdm

def find_year(x):
    return x[:4]
df['생산연도'] =  df['생산일'].map(find_year)

tqdm.pandas()
view_detail = df['내용번호'].progress_map(get_view_page)
view_detail
  • tqdm을 판다스에 적용시켜 map , apply와 같은 그룹 함수를 사용할 수 있다.
    • progress_map(_apply) : 진행 상태를 확인하며 데이터를 mapping한다.
      • 불러온 csv 데이터에 저장된 내용 번호 칼럼에 저장된 모든 일력번호를 get_view_page 함수의 인수로 매핑
len(view_detail)
2304

수집한 내용 확인하기

# 수집한 내용을 tolist() 를 통해 리스트로 변환 후 concat 으로 병합합니다.
df_view = pd.concat(view_detail.to_list())
df_view[:5]

  • to_list() : Series 형태로 저장된 데이터세트를 list 자료형으로 바꿔준다.
  • concat() : 하나의 데이터 프레임으로 concat한다.
# 기존 데이터와 병합하여 내용이 함께 수집된 것을 확인합니다.
df_detail = df.merge(df_view, on=["내용번호", "생산일"])
df_detail
  • merge() : 기존 데이터와 병합

사용할 변수만 남기고 데이터 저장하기

df = df_detail[['번호', '분류', '제목', '내용', '내용번호']]
df
 
# 파일명 : "seoul-120-sample.csv" 
file_name = "seoul-120-sample.csv"
df.to_csv(file_name, index=False)
 
 

답변 내용 분류 시각화

df.groupby(['분류']).size().sort_values(ascending=False)
분류
행정        913
경제        767
복지        199
환경        100
주택도시계획     97
교통         78
문화관광       78
안전         50
건강         18
여성가족        3
세금재정        1
dtype: int64
import seaborn as sns
import matplotlib.pyplot as plt
import koreanize_matplotlib

plt.figure(figsize = (20,10))
sns.countplot(data = df, x = '분류')
  • 경제와 행정 관련 질문이 압도적으로 많았다는 것을 알 수 있다.