서울특별시 다산콜센터(☎120)의 주요 민원
json은 코드 에러가 날 확률이 적다. 만약 API를 제공할 경우, API를 이용하는 것이 상호이익이다.
간혹 json 파일에 jQuery를 제공하는 경우가 있는데, 이는 지워도 작동하는 경우가 많다.
jQuery를 지웠을 때 null 값이 나오는 건 페이지 설정때문이며, 추후 데이터 수집 시 지워야 한다.
HTTP Status Code는 200 OK 알아도 무방하다
request는 소스코드만 보여주기 때문에 가벼우나 불친절함
서울 120의 url은 'items_per_page'를 통해 페이지당 개수를 조절할 수 있다. 그러나 보통 사이트에서는 서버 과부하를 방지하기 위해 이에 제한을 두는 경우가 많다.
https://opengov.seoul.go.kr/civilappeal/list?items_per_page=50&page=2
라이브러리 호출
# 필요한 도구를 불러온다.
# 파이썬에서 사용할 수 있는 엑셀과 유사한 데이터분석 도구
# 매우 작은 브라우저로 웹사이트의 내용과 정보를 불러옴
# request로 가져온 웹사이트의 html 태그를 찾기위해 사용
# 간격을 두고 가져오기 위해 사용
import pandas as pd
import numpy as np
import requests
from bs4 import BeautifulSoup as bs
import time
utf-8로 encoding.
read_html로 목록 수집은 충분하지만, 상세 정보(링크) 수집을 위해서는 requests가 필연적
cf. requests의 기본 encoding은 utf-8
def get_one_page(page_no):
"""
120 주요질문의 특정 페이지 목록을 수집
1) page_no 마다 url이 변경되게 f-string 을 사용해 만든다.
2) requests 를 사용해서 요청을 보내고 응답을 받는다.
3) 응답에 성공하면 '접속 성공', 실패하면 '실패'를 print 한다.
4) pd.read_html 을 사용해서 table tag로 게시물을 읽어온다.
5) 3번 결과에서 0번 인덱스를 가져와 데이터프레임으로 목록의 내용을 만든다.
6) html tag를 parsing 할 수 있게 bs 형태로 만든다.
7) 목록 안에 있는 a tag를 찾는다.
8) a tag 안에서 string 을 분리해서 내용번호만 리스트 형태로 만든다.
9) 4)의 결과에 "내용번호"라는 컬럼을 만들고 a tag의 리스트를 추가한다.
"""
base_url = f"https://opengov.seoul.go.kr/civilappeal/list?items_per_page=50&page={page_no}"
# request.get()은 기본적으로 encoding = 'utf-8'
response = requests.get(base_url)
if response.status_code in range(200, 300):
print("접속 성공")
else:
print("실패")
# encoding 에러가 난다면 cp949로 변경
# table = pd.read_html(base_url, encoding='utf-8')
table = pd.read_html(response.text)[0]
html = bs(response.text)
a_list = html.select('td.data-title.aLeft > a')
a_link_no = []
for a_tag in a_list:
a_link_no.append(a_tag['href'].split('/')[-1])
table["내용번호"] = a_link_no
return table
def get_one_page(page_no):
# 1) page_no 마다 url이 변경되게 f-string 을 사용해 만든다.
base_url = f"https://opengov.seoul.go.kr/civilappeal/list?items_per_page=50&page={page_no}"
# 2) requests 를 사용해서 요청을 보내고 응답을 받는다.
response = requests.get(base_url)
# 3) pd.read_html 을 사용해서 table tag로 게시물을 읽어온다.
# 4) 3번 결과에서 0번 인덱스를 가져와 데이터프레임으로 목록의 내용을 만든다.
table = pd.read_html(response.text)[0]
# 5) html tag를 parsing할 수 있게 bs 형태로 만든다.
html = bs(response.text)
# 6) 목록 안에 있는 a tag를 찾는다.
a_list = html.select("td.data-title.aLeft > a")
# 7) a tag 안에서 string 을 분리해서 내용번호만 리스트 형태로 만든다.
# 8) 4)의 결과에 "내용번호"라는 컬럼을 만들고 a tag의 리스트를 추가한다.
table["내용번호"] = [a_tag["href"].split("/")[-1] for a_tag in a_list]
return table
또한, 47page까지는 값을 반환하지만 48~200page까지는 빈 데이터프레임을 반환한다.
그럴 경우 try와 except를 이용하여, if문을 이용하여 해결한다.
def get_one_page(page_no):
# 1) page_no 마다 url이 변경되게 f-string 을 사용해 만든다.
base_url = f"https://opengov.seoul.go.kr/civilappeal/list?items_per_page=50&page={page_no}"
# 2) requests 를 사용해서 요청을 보내고 응답을 받는다.
response = requests.get(base_url)
# 3) pd.read_html 을 사용해서 table tag로 게시물을 읽어온다.
# 4) 3번 결과에서 0번 인덱스를 가져와 데이터프레임으로 목록의 내용을 만든다.
table = pd.read_html(response.text)[0]
# 데이터프레임의 행이 0개라면 페이지를 찾을 수 없다는 메시지를 반환합니다.
if table.shape[0] == 0:
return f"{page_no} 페이지를 찾을 수 없습니다."
try:
# 5) html tag를 parsing할 수 있게 bs 형태로 만든다.
html = bs(response.text)
# 6) 목록 안에 있는 a tag를 찾는다.
a_list = html.select("td.data-title.aLeft > a")
# 7) a tag 안에서 string 을 분리해서 내용번호만 리스트 형태로 만든다.
# 8) 4)의 결과에 "내용번호"라는 컬럼을 만들고 a tag의 리스트를 추가한다.
table["내용번호"] = [a_tag["href"].split("/")[-1] for a_tag in a_list]
except:
# 중간에 오류가 발생해도 다음코드를 실행해야 할 때는 예외 메세지만 출력하도록 하는 방법도 있습니다.
# raise Exception(f"{page_no}를 찾을 수 없습니다.")
return f"{page_no} 페이지를 찾을 수 없습니다."
return table
이제 get_one_page 함수를 이용해 게시물이 없을 때까지 페이지를 수집하자.
22-10-04 기준, 47page가 마지막 페이지다.
만약, df_temp.shape[0] == 0를 기준으로 코드를 작성할 경우, 47페이지 이상의 값을 입력할 때 함수가 오류가 발생한다.
get_one_page 함수가 return 값이 무엇인지 잘 생각해보자.
# time.sleep을 통해 일정 간격 쉬었다가 가져옵니다.
# 게시물이 없으면 멈춥니다.
page_no = 50
table_list = []
while True:
print(page_no, end=",")
df_temp = get_one_page(page_no)
if df_temp.shape[0] == 0:
print("수집이 완료되었습니다.")
break
table_list.append(df_temp)
page_no += 1
time.sleep(0.01)
47 page를 초과하거나 에러가 뜰 경우 f"{page_no} 페이지를 찾을 수 없습니다."이 반환된다. 즉 DataFrame이 아니라 string이 반환되므로, type(df_temp) == str 로 조건문을 작성해주자.
# time.sleep을 통해 일정 간격 쉬었다가 가져옵니다.
# 게시물이 없으면 멈춥니다.
page_no = 1
table_list = []
while True:
df_temp = get_one_page(page_no)
if type(df_temp) == str:
print("수집이 완료되었습니다.")
break
print(page_no, end=",")
table_list.append(df_temp)
page_no += 1
time.sleep(0.01)
이후 결과물인 table_list를 csv 파일로 저장하자.
새로운 실습 파일을 열어서 csv파일을 불러오자. Colab의 경우 업로드 해야한다.
우리의 목적은 개별 민원에서 문서 본문과 문서 정보를 수집하는 것이다.
먼저 https://opengov.seoul.go.kr/civilappeal/26695536에서 문서 본문과 문서 정보를 수집해보자.
문서 본문
# 내용 페이지의 주소를 url 변수에 담아줍니다.
# 웹페이지의 결과를 받아옵니다.
# BeautifulSoup을 통해 lxml로 파싱해 올 수 있도록 합니다.
# html 태그에서 "div.line-all"의 0번째 값을 선택(select)하여 text를 확인합니다.
url = "https://opengov.seoul.go.kr/civilappeal/view/?nid=23194045"
# print(url)
response = requests.get(url)
html = bs(response.text)
#content > div > div.view-content.view-content-article > div:nth-child(2) > div
# xa0 : 아스키코드로 전처리 필요
# text와 get_text()는 동일한 기능
content = html.select("div.line-all")[0].text
content
xa0 등은 아스키코드로 추후 전처리가 필요하다.
문서 정보
정말 운이 좋게도 문서정보는 table 태그에 해당한다. 즉, pd.read_html를 이용해 손쉽게 데이터를 얻을 수 있다.
table = pd.read_html(response.text)[-1]
그러나 데이터프라임의 형태가 요상하다.
table의 두 column씩 떼서 transpose를 해줘야 한다.
천천히 살펴보면, column 0이 index고, column 1이 내용이다. 마찬가지로 column 2가 index고 column 3이 내용이다.
이 경우 아래 코드를 작성하여 index 설정을 다시해주고, 데이터프레임을 분리해주자.
table[[0, 1]].set_index(0)
table[[2, 3]].set_index(2)
이제 두 데이터프레임을 transpose 해보자.
tb01 = table[[0, 1]].set_index(0).T
tb02 = table[[2, 3]].set_index(2).T
이제 두 데이터프레임을 가로로 합쳐주자. index를 통일해주고 concat을 axis = 1로 진행하면 된다.
tb02.index = tb01.index
pd.concat([tb01, tb02], axis=1)
위의 내용을 함수로 작성하면 다음과 같다.
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
get_desc(response)
이제 문서 본문과 문서 정보를 하나의 함수로 만들어보자
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.line-all")[0].text
df_desc["내용"] = content
df_desc["내용번호"] = view_no
return df_desc
해당 코드는 에러 처리가 안되어 있다. 다음 시간에는 잘못된 view_no를 입력했을 때의 예외처리를 해보자.
Q & A
Q. 서울 120 사이트는 GET과 POST 중 어느 방식을 사용하여 통신하나요?
A. get 방식을 사용하여 통신
- 근거 1: url에 page가 기재된 경우 높은 확률로 get를 이용함
- 근거 2: Network의 Request Mothod가 GET으로 기재됨
Q. BeautifulSoup은 어떤 도구인가요?
A.
- 파싱 도구
- HTML을 보기 좋게 편집 cf. json 파일을 굳이 bs4로 편집할 이유가 없음
=> 읽어 온 웹사이트의 HTML 문서를 해석하는 용도
Q. 네이버 증권 게시판에 글을 쓰면 그 글의 저작권은 어디에 있나요?
A. 데이터베이스권은 네이버에 있으며 저작권은 글쓴이에게
Q. 페이지별 목록과 상세 내용을 수집하는 pseudo code를 작성해보세요
A. 상세할수록 좋습니다!
1. 테이블 정보만 포함한 url 링크를 찾아 가져온다
2. html 테이블태그를 이용해서 목록(번호, 제목, 생산일, 조회수)를 수집한다
3. 페이지 반복문을 통해 전체 페이지의 목록을 가져온다
4. 목록의 href주소의 끝의 8자리 숫자를 상세 페이지 번호로 가져온다
5. 본문의 문서 정보 테이블도 가져온다.
6. 상세 페이지 번호를 변수로 이용해서 전체 정보를 가져온다
7. 가져온 정보를 목록 + 본문으로 데이터프레임 형태로 연결한다
1. 수집하고자 하는 페이지의 URL 가져오기 2. 소스코드 확인하여 table 태그에 있는지 확인 => pd.read_html(url) 받아올 수 있는지 확인 3. 사이트 > 네트워크 > requests 방식 확인 & response.status_code(200OK) 확인 4. bs로 원하는 데이터가 어떤 태그로 이루어져있는지 확인 5. select로 필요한 정보 다 가져오기 6. 목록 먼저 받고 데이터프레임으로 만들기 7. 클릭 시 연결되는 사이트를 통해 상세 정보를 가져와 데이터프레임에 추가
1) network에서 페이지 링크 형식 복사해 가져오기 2) 첫 번째 페이지의 HTML문서 읽기 3) selector로 페이지 내의 목록 번호, 링크 위치 파악 후 데이터 가져오기 / 리스트에 저장 4) 링크 하나씩 문서 가져오기 반복 (for문) 5) selector로 페이지 내의 분류 정보와 내용 위치 파악 후 데이터 가져오기 6) f-string을 사용하여 페이지를 바꾸는 query string을 수정해서 다른 페이지 불러오기 7) 3~6 과정 반복 / 페이지가 없으면 멈추기(get에서 오류 발생 시)
Q. 서울 120 목록을 수집할 때 tqdm을 사용할 수 없다면?
A. while로 수집할 경우 범위가 정해져 있지 않아서 적절치 않음
Q. transpose 란?
A. 전치행렬을 의미하며, 행과 열을 바꾸는 것을 말합니다.
Q. tb01에서 0이 없는 이유는?
A. 파이썬 자체에서 생략한 것으로 추측됩니다.
'멋쟁이사자처럼 AIS7 > 오늘코드' 카테고리의 다른 글
[1024] ML Tools (0) | 2022.10.24 |
---|---|
[1013] EDA, Data Sampling, 파생변수를 이용한 Data 가공, 시각화(3-7 미작성) (0) | 2022.10.13 |
[1012] EDA, Tidy Data, KOSIS, 정규표현식 (0) | 2022.10.12 |
[1011] EDA (0) | 2022.10.11 |
[1005] EDA (0) | 2022.10.05 |
댓글