이번에는 모델한테 전달할 데이터셋을 웹크롤링을 통해 얻어보자
크롤링할 사이트: 위키피디아의 미해결사건 페이지(https://ko.wikipedia.org/wiki/%EB%AF%B8%ED%95%B4%EA%B2%B0_%EC%82%AC%EA%B1%B4 )
전체코드
더보기
def fetch_links_from_page ( url ):
response = requests . get ( url )
soup = BeautifulSoup ( response . text , 'html.parser' )
content_div = soup . find ( 'div' , { 'class' : 'mw-parser-output' })
links = content_div . find_all ( 'a' , href = True )
page_links = []
for link in links :
href = link [ 'href' ]
title = link .text.strip()
# 필터 조건
if href .startswith( '/wiki/' ) and ':' not in href : # 내부 링크만 포함
if len ( title ) < 5 : # 제목이 너무 짧은 경우 제외
continue
if "사건" not in title and "사고" not in title : # 키워드 없는 경우 제외
continue
page_links . append (( title , base_url + href ))
return page_links
def fetch_article_content ( url ):
response = requests . get ( url )
soup = BeautifulSoup ( response . text , 'html.parser' )
title = soup . find ( 'h1' , { 'id' : 'firstHeading' }). text . strip ()
content_div = soup . find ( 'div' , { 'class' : 'mw-parser-output' })
if not content_div :
return { "title" : title , "content" : "" }
content = []
for element in content_div . find_all ([ 'p' , 'ul' , 'ol' , 'table' , 'h2' , 'h3' , 'div' , 'blockquote' ]):
if element .name == 'table' :
content . append ( "표 내용 생략" )
else :
text = element .get_text( strip = True )
if text :
content . append ( text )
full_content = " \n " . join ( content )
return { "title" : title , "content" : full_content }
def collect_articles ( start_url ):
links = fetch_links_from_page ( start_url )
articles = []
for idx , ( title , link ) in enumerate ( links ):
print ( f "[ { idx + 1 } / { len ( links ) } ] Fetching: { title } " )
try :
article = fetch_article_content ( link )
articles . append ( article )
except Exception as e :
print ( f "Failed to fetch { title } : { e } " )
return articles
articles = collect_articles ( start_url )
# 데이터 저장
with open ( "filtered_unsolved_cases.json" , "w" , encoding = "utf-8" ) as f :
json . dump ( articles , f , ensure_ascii = False , indent = 4 )
print ( f "파일 저장 완료!" )
def fetch_links_from_page ( url ):
더보기
페이지에서 링크를 가져오는 함수를 정의한다.
response = requests.get(url)
더보기
requests.get()은 특정 url로 get 요청을 보낸다. 요청이 성공하면 서버는 응답을 반환하고
서버가 반환한 응답을 response에 저장했다.
response에는 http상태코드(ex. 200번대는 성공을 의미), 응답한 본문(response.text; html형태), 헤더정보(response.headers), 쿠키정보 등이 들어있다.
soup = BeautifulSoup(response.text, 'html.parser')
더보기
beautifulsoup에서 웹페이지가 응답한 html내용(=response.text)을 받아서 html 파서로 처리해서 soup 객체로 반환한다. 반환한 객체를 사용하면 특정 태그를 검색하거나, 태그를 모두 찾는등의 작업이 가능하다.
content_div = soup.find('div', {'class': 'mw-parser-output'})
더보기
.find() 는 특정태그를 찾아주는데, 조건에 맞는 첫번째 태그만 반환해준다. 위 코드는 <div>태그를 찾는데, 추가조건으로 클래스 속성이 'mw-parser-output' 인 것을 찾아준다. -> 위키페이지에서 개발자도구로 확인해보면 본문 컨텐츠임
즉 content_div ->위키피디아 본문을 의미
참고)
F12 눌러서 본문이 담긴 태그를 보면
<div class="mw-content-ltr mw-parser-output" lang="ko" dir="ltr"> 본문 내용 </div>
이렇게 되어있다. 이 div 태그는 클래스로 mw-content-ltr, mw-parser-output 두 개를 갖는다. class 속성은 여러 클래스 이름을 공백으로 구분해서 가질 수 있다.
links = content_div . find_all ( 'a' , href = True )
더보기
find_all()은 해당하는 모든 태그를 찾아준다.
이 코드에서는 <a>태그 중에서도 href=True 즉 링크가 있는 <a>태그를 가져온다.
div 클래스 내에서 a태그 중 링크가 있는 부분
page_links = []
for link in links:
href = link['href']
title = link.text.strip()
더보기
links = content_div . find_all ( 'a' , href = True ) 이 코드로 가져온 links에는
링크를 가진 <a>태그들이 들어있고, 그들을 순회하면서 href 속성을 가져온다.
link['href'] 는 마치 딕셔너리에서 값을 가져오는 것처럼 보이는데, 딕셔너리는 아니다. beutifulsoup이 파싱한 객체를 딕셔너리처럼 다룰 수 있게 해주는것 뿐 link.text로 링크 안의 텍스트를 가져오고 strip으로 공백을 제거했다.
ex) <a href="/wiki/%EC%A0%95%EC%9D%B8%EC%88%99_%ED%94%BC%EC%82%B4%EC%82%AC%EA%B1%B4" title="정인숙 피살사건">정인숙 피살사건</a> 이런 <a>태그가 있다면 link['href']는 "/wiki/%EC%A0%95%EC%9D%B8%EC%88%99_%ED%94%BC%EC%82%B4%EC%82%AC%EA%B1%B4" 이 부분을 반환한다.
link.text.strip()은
"정인숙피살사건" 을 반환한다.
# 필터 조건
if href.startswith('/wiki/') and ':' not in href: # 내부 링크만 포함
if len(title) < 5: # 제목이 너무 짧은 경우 제외
continue
if "사건" not in title and "사고" not in title: # 키워드 없는 경우 제외
continue
page_links.append((title, base_url + href))
return page_links
더보기
처음에는 이 코드가 없었는데, 가져오다보니 문제가 생겼다.
일부 링크는 예를 들어 '영어' '공소시효' '목포' '살인' '사건' 이런식으로 미해결사건과 관련이 없거나 관련은 있으나 링크가 제대로 안걸린 경우가 있었다. 또한 위키페이지 외부로 링크가 걸릴 경우는 크롤링에서 제외하고 싶었다.
그래서 필터조건을 추가했다.
':' not in ??
:이 포함된 url은 특수페이지이다. 본문링크가 아닌 특수 정보를 제공하는 페이지라서 제외
ex)
위키백과:대문
최종적으로 모든 필터를 통과한 링크만이 page_links에 저장된다.
def fetch_article_content(url):
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
더보기
마찬가지로 url에서 get 으로 응답 받아서 파싱하고
title = soup.find('h1', {'id': 'firstHeading'}).text.strip()
더보기
제목을 <h1>태그에서 가져온다.
코드를 읽어보면 <h1>태그에서 id가 firstHeading인것을 찾아 text만 뽑아오고 공백을 제거하라고 되어있다.
참고로 페이지 내의 제목은 다음과 같은 태그 안에 들어있다.
<h1 id="firstHeading" class="firstHeading mw-first-heading"><span class="mw-page-title-main">정인숙 피살사건</span></h1>
content_div = soup.find('div', {'class': 'mw-parser-output'})
더보기
내용은 <div>태그에서 class명이 'mw-parser-output'인 것을 찾아 가져온다.
더 세부적으로 나누지 않고 전체 내용을 누락시키지 않기 위해 이렇게 가져왔었다.
그런데 문제가 생겼다.
정확한 이유는 모르겠지만 동작과정에서 content가 누락되거나 일부 내용이 가져와지지 않은것
<div>나 'mw-parser-output' 이 아닌곳에도 내용이 담겨있어서 그것들까지 다 들고오기로 했다.
if not content_div:
return {"title": title, "content": ""}
더보기
우선 빈 컨텐트는 오류가 나지 않게 위와 같이 처리하기로 했고
content = []
for element in content_div.find_all(['p', 'ul', 'ol', 'table', 'h2', 'h3', 'div', 'blockquote']):
더보기
<div>내에 쓸 수 있는 가능한 모든 태그를 다 찾아서 들고오기로 했다.
if element.name == 'table':
content.append("표 내용 생략")
else:
text = element.get_text(strip=True)
if text:
content.append(text)
더보기
.get_text()
beautiful soup 메서드 중 하나로 태그를 다 제거하고 텍스트만 반환한다. strip=True로 공백을 제거할수 있다.
참고) .text는 속성이고 get_text()는 메서드다
코드를 읽어보면 표는 놔두고 텍스트만 공백을 제거해서 content=[] 에 추가하고 있다.
나중에 표도 읽어와야 하면 위 코드를 수정하면 된다. 코드의 유연성을 위해 table을 처리하는 부분을 살려두었다.
full_content = "\n".join(content)
return {"title": title, "content": full_content}
더보기
content를 하나로 이어서(join) 전체 내용을 만들고, 제목과 함께 반환하면 페이지의 내용 추출이 끝난다.
def collect_articles(start_url):
더보기
이제 위의 두 함수를 이용해 최종적으로 링크를 순회하며 본문을 추출하는 함수
이렇게 코드를 분리한 이유는 작업의 모듈화 때문이다.
links = fetch_links_from_page(start_url)
더보기
url을 수집하고 싶은 사이트를 넣어서 url을 수집하고 결과를 links에 저장
articles = []
for idx, (title, link) in enumerate(links):
print(f"[{idx+1}/{len(links)}] Fetching: {title}")
try:
article = fetch_article_content(link)
articles.append(article)
except Exception as e:
print(f"Failed to fetch {title}: {e}")
return articles
더보기
links의 link를 순회하면서, fetch_article_content 함수를 이용해 내용을 추출하고 있다.
원래 try-except문이나 print()문은 없었는데 오류 수정하면서 작동이 잘 되는지 확인하기 위해 나중에 추가했다.
추출한 내용은 모두 articles=[]에 저장
articles = collect_articles(start_url)
더보기
이제 이 두 줄만 쓰면 우리가 원하는 페이지에서 내용을 추출할 수 있다.
만약 페이지 구조가 비슷하다면, 미해결사건이 아닌 다른 페이지의 내용도 start_url만 바꿔서 추출하면 된다.
# 데이터 저장
with open("filtered_unsolved_cases.json", "w", encoding="utf-8") as f:
json.dump(articles, f, ensure_ascii=False, indent=4)
print(f"파일 저장 완료!")
더보기
모델에 데이터를 넘겨줄 때, 메모리에 따로 저장하지 않고 바로 넘겨줄 수 있고 데이터를 저장해서 저장한 파일을 넘겨줄수도 있다. 이번에는 후자를 선택할 것 같아서 데이터 저장코드를 추가했다.
사담) 저장된 json파일을 못찾아서.. 코드가 동작 안하는줄 알고 print() 문을 추가했다.. 사실 json파일은 처음부터 잘 저장되고 있었다