와이파이 실습 때와 같이 의료기관 공공데이터를 활용하여 현황을 분석하고 지도 시각화를 진행했다. 유사한 내용은 제외하고 한 번 더 짚어봐야 할 내용과 처음 배운 내용을 기록하고자 한다.
참고. groupby를 하면, NaN값은 제외하고 계산하기 때문에 원시자료의 데이터 수와 다른 값이 나올 수 있다. 이처럼 분석할 때는 차이가 왜 발생하는지 이유를 파악하고 있어야 한다.
워드클라우드를 그리기에 앞서 의료기관별 진료과목명을 모두 끌어와 하나의 text로 만들었다. 그리고 진료과목명의 워드클라우드를 그리게 되는 진료과목명이 몇개나 들어있는지 확인했다.
먼저 단순히 text의 갯수를 구하면 그 text를 이루고 있는 문자의 갯수를 가져온다. 따라서 우리가 특수문자를 공백으로 치환한 후 각 의료기관별 진료과목을 공백으로 구분하여 join했기 때문에 공백을 기준으로 스플릿한다면, 진료과목의 개수를 구할 수 있다.
이 과정에서 우리는 저 {} 중괄호 안에 len(text)를 넣기 위해 format 함수를 활용한다. format함수는 중괄호로 인덱스를 만들고 format함수 인자에 그 값을 넣어주는 것이다.
참고. 중괄호의 갯수는 1개 이상일 수 있다. 여러개의 값을 format함수로 넣을 때 첫 인덱스 값은 0이며 1, 2값을 주면 인덱스 순서에 맞게 값을 대입시켜 준다.
(1) print('총 {} 개 데이터가 있습니다.'.format(len(text)))
(2) print('총 {0} 개 데이터가 있습니다{1}.'.format(len(text)),'!')
#3. 데이터 전처리
#3-1. 유효한 데이터 수집(NaN 값은 '-'로 바꾸고 좌표값이 없는 의료기관은 삭제)
mdf = df.replace(np.nan, '-', regex=True)
d = mdf[mdf['위도'] == '-'].index
mdf.drop(d, axis='index', inplace=True)
# 기타 정보가 없어도 영업중인 의료기관은 모두 표시되도록 처리
#3-2. 영업 중인 의료기관만 수집
op_df = mdf.loc[mdf['상세영업상태명']=='영업중']
op_df.reset_index(drop=True, inplace=True)
NaN데이터를 모두 삭제하면, 수백 건 이상의 의료기관 데이터가 없어지기 때문에 필수정보를 포함하고 있지 않은 데이터만 삭제하기로 결정했다.
그 다음 우리가 원하는 지도 시각화 대상은 영업중인 의료기관임에 따라 상세영업상태명이 영업중인 의료기관만 추출해 새로운 op_df에 담았다.
#4. 주소를 좌표로 변환할 함수 준비
def geocoding(address):
geolocoder = Nominatim(user_agent = 'South Korea', timeout=None)
geo = geolocoder.geocode(address)
crd = {"lat": float(geo.latitude), "lng": float(geo.longitude)}
return crd
#5. 사용자에게 주소 입력받기
address = input('도로명주소를 입력하세요.')
start = time.time() # 사용자 입력 시각 측정(Shout out to 세종)
#6. 사용자 주소를 좌표로 변환 후, 튜플 형태로 변수에 담기
crd = geocoding(address)
IMHERE = (crd['lat'], crd['lng'])
#7. 데이터프레임 새로 생성 후, 필요한 컬럼 및 입력 주소와의 거리 계산하여 담기
c_hosp = pd.DataFrame(columns=['사업장명', '의료기관종명', '진료과목',
'전화번호', '영업상태','위도', '경도', '거리'])
for n in op_df.index:
h_loc = (op_df.loc[n, '위도'], op_df.loc[n, '경도'])
c_hosp.loc[n] = [op_df.loc[n, '사업장명'], op_df.loc[n, '의료기관종별명'],
op_df.loc[n, '진료과목내용명'], op_df.loc[n, '소재지전화'],
op_df.loc[n, '상세영업상태명'], op_df.loc[n, '위도'],
op_df.loc[n, '경도'], geodesic(IMHERE, h_loc).kilometers]
#8. 입력 주소와 가장 가까운 영업중인 의료기관 상위 5곳 구하기
c_hosp = c_hosp.sort_values(by=['거리']).head(5)
#9. 의료기관 자료의 평균 위치를 중심좌표로 지도 준비
h_map = folium.Map(location=[op_df['위도'].mean(), op_df['경도'].mean()], zoom_start=10)
#10. 입력 주소와 상위 5곳의 의료기관 마커 올리기
folium.Marker([crd['lat'], crd['lng']], icon=folium.Icon(color='red', icon='glyphicon
glyphicon-home')).add_to(h_map)
for n in c_hosp.index:
folium.Marker([c_hosp.loc[n, '위도'], c_hosp.loc[n, '경도']],
popup='<pre>'+'병원명: '+c_hosp.loc[n, '사업장명']+', 진료과목: '
+c_hosp.loc[n, '진료과목']+', 전화번호: '+c_hosp.loc[n, '전화번호']+
'</pre>', icon=folium.Icon(color='cadetblue',icon='fa-hospital-o'
,prefix='fa')).add_to(h_map)
#11. 실행에 소요된 시간 표시 및 지도 시각화
print('실행시간:', (round(time.time()-start)),'초')
h_map
네이버 공감별 연예랭킹 뉴스가 있다😊 좋아요부터 슬퍼요까지 여섯가지의 공감별 높은 공감을 받은 뉴스들을 순서대로 랭킹을 매겨놓은 뉴스이다.
아이디어는 이 각각의 공감별 뉴스를 크롤링하고 사용자가 공감값을 입력하면 그 공감 랭킹 뉴스 제목으로 워드클라우드를 만드는 것이다.
그런데 어떻게 만들지...🤔라는 고민이 이어졌고 기존에 배웠던 뉴스 크롤링과 다른 점을 구별해보았다.
미리 구조를 설계하고 들어가면 좋으련만 방법을 모르니 일단 실습했었던 크롤링 과정에서 하나씩 확장해야겠다고 생각했다. 공감별 뉴스는 총 6개 부문이지만, 합치는 건 나중에 정 어려우면 머지를 하면 되니 차후 문제라 판단했고, 한 공감을 기준으로 공감종류와 공감수를 크롤링해오고자 했다.
sym_df1 = pd.DataFrame(columns=['공감종류', '순위', '기사제목',
'기사링크', '기사내용', '공감수', '수집일자'])
driver.get("https://entertain.naver.com/ranking/sympathy")
driver.implicitly_wait(3) # 바로 움직이면 컴퓨터로 인식하니까 잠시 멈춤
time.sleep(1.5)
driver.execute_script('window.scrollTo(0,800)')
time.sleep(3)
html_source = driver.page_source
soup = BeautifulSoup(html_source, 'html.parser')
li_list= soup.select('li._inc_news_lst3_rank_reply')
1) 좋아요 공감 많은 기사 리스트가 있는 웹페이지 소스
2) li_list로 담은 후 출력되는 값:
좋아요 공감이 많은 순으로 총 30개의 기사 리스트의 HTML 소스가 담겨있다.
li_list에 담긴 li 태그 수만큼 for문을 돌리면서 각 기사별 '공감종류와 순위, 기사제목, 기사링크, 기사 내용, 공감 수'를 추출하고 처음에 만든 sym_df1에 append한다.
가장 고민했던 컬럼은 공감 종류였다. 다른 컬럼들처럼 태그 안에서 값을 추출하고 싶다는 생각에 공감별로 페이지 소스를 확인해봤다. 살펴보니 a 태그 중 'likeitnews_item_likeit like'라는 클래스명을 가진 태그가 있었고, 다른 공감 페이지도 'likeitnews_item_likeit cheer'와 같이 마지막 단어만 다르게 클래스 명을 가지고 있어 이를 가져와 스플릿한 후 활용했다.
for li in range(0,len(li_list)):
try:
#공감종류
a = li_list[li].find('a',{'class', 'likeitnews_item_likeit like'}).attrs['class']
kind = str(a).split("'")[3]
#순위
rank = li_list[li].find('em',{'class', 'blind'}).text.replace('\n','')
.replace('\t','').strip()
#기사 제목
title = li_list[li].find('a',{'class', 'tit'}).text.replace('\n','')
.replace('\t','').strip()
#기사 링크
link = li_list[li].find('a').attrs['href']
#기사 내용
summary = li_list[li].find('p',{'class', 'summary'}).text.replace('\n','')
.replace('\t','').strip()
#공감 수
num = li_list[li].find('a',{'class', 'likeitnews_item_likeit like'})
.text.replace('\n','').replace('\t','').replace(',', '').strip()
symnum = str(num)[3:]
#dataframe 저장(append)
sym_df1 = sym_df1.append({'공감종류': kind,
'순위': rank,
'기사제목': title,
'기사링크': 'https://entertain.naver.com'+link,
'기사내용': summary,
'공감수': symnum,
'수집일자' : datetime.datetime.now(timezone('Asia/Seoul'))
.strftime('%Y-%m-%d %H:%M:%S')}, ignore_index=True)
except:
pass
sym_df = pd.concat([sym_df1,sym_df2,sym_df3,sym_df4,sym_df5,sym_df6])
sym_df.reset_index(drop=True, inplace=True)
이 코드는 우리 팀 PL의 아이디어이다😍 사용자로부터 먼저 input을 받은 후 해당하는 주소에만 접근해 선택적으로 크롤링한다. 감정별로 변수를 선언하고 크롤링 프로그램이 접근할 주소에 붙여서 if문에 넣고 input값과 일치하는 기사 리스트만 추출한다.
sym_df = pd.DataFrame(columns=['공감종류', '순위', '기사제목', '기사링크',
'기사내용', '공감수', '수집일자'])
driver = webdriver.Chrome('chromedriver', options=options)
input_sympathy = input('보고싶은 공감 랭킹뉴스를 입력하세요
[like, cheer, congrats, expect, surprise, sad] ')
#5. 입력값에 따라 해당하는 공감 기사만 파싱해오기
site = 'https://entertain.naver.com/ranking/sympathy'
emo1 = '/like'
emo2 = '/cheer'
emo3 = '/congrats'
emo4 = '/expect'
emo5 = '/surprise'
emo6 = '/sad'
if input_sympathy == 'like':
driver.get(site)
elif input_sympathy == 'cheer':
driver.get(site+emo2)
elif input_sympathy == 'congrats':
driver.get(site++emo3)
elif input_sympathy == 'expect':
driver.get(site+emo4)
elif input_sympathy == 'surprise':
driver.get(site+emo5)
else:
driver.get(site+emo6)
장점은 한 개의 공감 리스트만 크롤링하기 때문에 소요되는 시간이 대폭 줄어든다는 것이다.
(ps. 정말 창의적이라고 생각했다...😲 난 왜 구조를 변경할 생각을 못했을까😭)
sym_df = pd.DataFrame(columns=['공감종류', '순위', '기사제목',
'기사링크', '기사내용', '공감수', '수집일자'])
site = 'https://entertain.naver.com/ranking/sympathy'
Emotions = ["like","cheer","congrats","expect","surprise","sad"]
#3. 감정별 데이터 파싱 위한 for문 설정
for n in range(0, len(Emotions)): # 좋아요와 그밖의 공감 간 다른 주소형식을 감안
if Emotions[n] == 'like':
driver.get(site)
else:
driver.get(site+'/'+ str(Emotions[n]))
driver.implicitly_wait(3)
time.sleep(1.5)
driver.execute_script('window.scrollTo(0,800)')
time.sleep(3)
html_source = driver.page_source
# print(html_source)
soup = BeautifulSoup(html_source, 'html.parser')
li_list= soup.select('li._inc_news_lst3_rank_reply')
Emotions = ["like","cheer","congrats","expect","surprise","sad"]
Emo_df = pd.DataFrame(Emotions, columns = ['감정'])
#3. 감정별 데이터 파싱 위한 for문 설정
e_name = Emo_df['감정']
for n in range(0, len(e_name)): # 좋아요와 그밖의 공감 간 다른 주소형식을 감안
if Emo_df['감정'][n] == 'like':
driver.get(site)
else:
driver.get(site+'/'+ str(Emo_df['감정'][n]))
driver.implicitly_wait(3)
time.sleep(1.5)
#5. 사용자로부터 공감 입력값 받기
input_sympathy = input('보고싶은 공감 랭킹뉴스를 입력하세요
[like, cheer, congrats, expect, surprise, sad] ')
#6. 입력값에 해당하는 공감 기사제목만 join
text = " ".join(wc for wc in sym_df[sym_df.공감종류 == input_sympathy].기사제목.astype(str))
def clean_text(inputString): # 특수문자 삭제
text_rmv = re.sub('[-=+,#/\?:^.@*\"※~ㆍ!』‘|\(\)\[\]`\'…》\”\“\’·]', ' ', inputString)
return text_rmv
#2. 데이터 프레임 생성 및 데이터 수집 위한 변수 설정
sym_df = pd.DataFrame(columns=['공감종류', '순위', '기사제목',
'기사링크', '기사내용', '공감수', '수집일자'])
url_list = ['','/cheer','/congrats','/expect','/surprise','/sad']
#url = 'https://entertain.naver.com/ranking/sympathy'
sympathy = 'like' # 초기값을 줘버림
#3. 감정별 데이터 파싱 위한 for문 설정
for n in range(0, len(url_list)): # 좋아요와 그밖의 공감 간 다른 주소형식을 감안
url = 'https://entertain.naver.com/ranking/sympathy' #진짜 감동: url 리셋
url += url_list[n] # 자기자신에게 더하는 것
if url_list[n] != '':
sympathy = url_list[n].replace('/','')
driver.get(url)
... ... (중략)
print(' 수집 중... '+url+' sympathy:' + sympathy)
for index_1 in range(0, len(li_list)):
try:
#공감종류
kind = sympathy
... ... (중략)
#공감 수
temp_cnt = li_list[index_1].find('a',{'class', 'likeitnews_item_likeit '+str(sympathy)})
.text.replace('\n','').replace('\t','').strip()
cnt = re.sub(r'[^0-9]','', temp_cnt)
#dataframe 저장(append)
sym_df = sym_df.append({'공감종류': kind,
'순위': rank,
'기사제목': title,
'기사링크': 'https://entertain.naver.com'+link,
'기사내용': summary,
'공감수': cnt,
'수집일자' : datetime.datetime.now(timezone('Asia/Seoul'))
.strftime('%Y-%m-%d %H:%M:%S')}, ignore_index=True)
except:
pass
앞으로 코드 작성 시 닥치는 대로 문제를 해결하려 애를 쓰기 전에 먼저 체계적으로 로직을 구상하고, 시도한 코드는 오류가 나도 기록해놔야겠다는 생각이 들었다.
이번 과제 초반 코드 중 그 이유가 있다. 데이터프레임이 아닌 리스트로 len 함수를 활용했을 때 분명 오류가 났는데 교수님 코드가 동일한 방식으로 작성되어 있어 다시 시도해보니 코드가 정상적으로 실행되었다. 물론 다른 부분에서 오류가 났던 것이겠지만 기록이 없으니 무작정 리스트 형태로는 len함수가 돌아가지 않는 것이라고 오판을 했으니 말이다.