결론부터 말하자면 반쪽짜리 구현이다. 참고하고 더 읽을지 말지 결정하기 바란다.
사용모듈
- python 3.7
- requests = 뉴스기사 가져오기 위함
- BeautifulSoup = html 파싱을 위함
이전 두개의 글( 네이버 영화평, 네이버 댓글 수집) 에서 나는 크롤링이라는 말을 쓰지 않았다.
왜냐하면 어떤 velog 글을 보게 되었는데
velog.io/@mowinckel/%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%A7%81-I
해당 글에서 나온 크롤링의 정의를 보고 내가 잘못 알고있었구나 라는걸 깨달았기 때문이다.
또한 직접 수집해보며 느낀점으로는 해당 page가 리뉴얼해 html tag나 구조가 변경되면 내가 만든 코드가 무용지물이 된다는 점이 아쉬웠다.
저 글 중간에 그것에 대한 논문이 하나 있다고 링크되어있길래 읽어보았다.
DOM Based Content Extraction via Text Density
(google scholar 검색하면 바로 나오니 링크는 생략)
내가 평소에 논문을 많이 읽는 것도 아니고...
온통 영어라 잘 안들어와서.. 누군가 설명한게 없을까...(구글링)
있었다.
먼저 논문과 번역글을 먼저 정독하기를 바란다. (이글을 쓰는 나도 사실 이해를 잘한건지)
그렇다면 해당 글을 읽고 잘 만든다면 페이지 구조 변경에도 상관없는 무적의 수집기를 만들 수 있을까??
한번 해당 논문이 얘기하는 대로 만들어보자.
1. Text Density
<div class="main">
<div class="article">
<div class="articleHeadline">
South korea to Hold Artillery Drills on Island</div>
<div class="articleBody">
...The announcement came as ...
<a>Bill Richardson</a>
</div></div></div>
먼저 논문은 텍스트 밀도(Text Density) 라는 개념을 설명한다.
html은 DOM tree 구조로 되어있고, 콘텐츠가 포함된 특정 tag의 경우
자식 tag의 숫자는 적고, 문자의 수는 많다는 설명을 통해 시작한다.
계산식
해당 식을 코드로 구현하기 앞서, 논문에서 나온 선행 조건으로
1. html tag 중 <body>만 사용하기
2. <body> tag 중 style, comment, script 등의 본문 추출과 상관없는 태그 제외
2가지 조건을 고려해 코드로 작성해보자
오늘 실험을 도와줄 기사 url
news.v.daum.net/v/20210330235246128
import requests
url = 'https://news.v.daum.net/v/20210330235246128'
html = requests.get(url)
soup = BeautifulSoup(html.content, "html.parser")
Text Density 계산 코드
all_list = soup.find('body').find_all() # body tag 아래 모든 tag 가져오기
TD_score_list = []
print("요소 개수 : ",len(all_list))
for tag in all_list:
# tag 가 2번 조건에 부합하면 제외
if(tag.name == 'script' or tag.name == 'comment' or tag.name == 'style'):
continue
# 해당 tag 아래 모든 태그 개수 반환
tagNumber = len(tag.find_all())
# 만약 tag 개수값이 0 이라면 1로 치환
if(tagNumber == 0):
tagNumber = 1
# 해당 tag 아래 모든 문자 개수 반환
charNumber = len(tag.text)
# text density 계산
TD = charNumber / tagNumber
# 점수저장
TD_score_list.append(TD)
print("점수 개수", len(TD_score_list))
결과를 살펴보자.
논문에 나온 그래프와 유사하게 뽑힌걸 확인할수 있다.
점수가 일정 값( 80 이상 ) 보다 높게 나온 태그를 뽑아 결과를 한번 살펴보자
오?? 생각보다 기사 본문에 대해서 잘 뽑아준걸 확인했다. 하지만 노이즈 역시 뽑혀 나오는 걸 확인할 수 있다.
2. Composite Text Density
논문에서는 노이즈가 <a> tag를 통해 많이 나타나는 경향을 발견했고, 해당 태그 값에 대해 TD 계산이 아닌 다른 계산식을 제안했다.
일단 용어가 추가 된다.
그리고 식이 추가되는데 tag<i>가 <a> tag일 경우엔 아래식으로 적용된다고 한다.
식으로 보니까 머리아프다. 기호 하나씩 정리하고 코드로 구현해보자.
Ci = <i> tag 아래 모든 문자 개수 합
Ti = <i> tag 아래 모든 태그 개수 합
차례대로 구현하면 된다.
def CTD_return(tag):
C = len(tag.text) #Ci = <i> 이하의 모든 문자의 개수
T = len(tag.find_all()) #Ti = <i> 이하의 모든 태그의 개수
LC = sum([len(a.text) for a in tag.find_all('a')]) #LCi = <i> 이하의 모든 하이퍼링크 문자의 개수
not_LC = sum([len(t.text) for t in tag.find_all() if t.name != 'a']) #not_LCi = <i> 이하의 모든 하이퍼링크 문자가 아닌 문자의 개수
LT = len(tag.find_all('a')) #LT = <i> 이하의 모든 하이퍼링크 태그의 개수
LCb = len(soup.find('body').find_all('a')) # body 태그 이하의 모든 하이퍼 링크의 개수
Cb = len(soup.find('body').text) #body 태그 이하의 모든 문자의 개수
# 1번 식
if T == 0:
T = 1
C_T = C/T
if LC == 0:
LC = 1
C_LT = C/LC
if LT == 0:
LT = 1
T_LT = T/LT
last = C_LT * T_LT
# 2번 식 및 3번 식
if not_LC == 0:
not_LC = 1
if Cb == 0:
Cb = 1
log_num = math.log( ((C/not_LC) * LC) + ((LCb/Cb) * C) + math.exp(1) )
# 4번식 및 최종 결과물
CTD = C_T *math.log(last, log_num)
return CTD
TD 점수도 함수로 구성해보자
def TD_return(tag):
tagNumber = len(tag.find_all())
if(tagNumber == 0):
tagNumber = 1
charNumber = len(tag.text)
TD = charNumber / tagNumber
return TD
그리고 두 함수를 통해 Text Density 를 다시 계산한다.
all_list = soup.find('body').find_all()
score_list = []
print("요소 개수 : ",len(all_list))
for tag in all_list:
if(tag.name == 'script' or tag.name == 'comment' or tag.name == 'style'):
continue
score = 0
if(tag.name == 'a'): # a 태그 일경우 CTD 적용
score = CTD_return(tag)
else: # a 태그가 아닐경우 TD 적용
score = TD_return(tag)
score_list.append(score)
print("점수 개수", len(score_list))
결과를 살펴보자
결과를 살펴보면 300번대 태그들에서 점수값이 향상된 것이 보이는데 실제로 유의미한 값이 검출되는지 살펴보자
기사 제목 링크 값들의 점수가 검출된 것을 볼수 있다.
content 라면 content이긴 하나 원하는 값인지는 사람마다 다르니 pass...
게다가 논문과 표를 비교해보면 (반대로!) 가시화 결과 부분에서 값들이 전체적으로 다 증가되는 경향이 보인다.
(잘못 구현한 걸까??)
3. Density Sum 및 Threshold
***************이 챕터부터 꼬이기 시작한다. *****************
위 값들을 살펴보면, 어느 점수에서 짜를건지 명확하지 않다.
상수값은 안될 말이다. 문서마다 값 나오는게 다를테니...
논문에서는 threshold 값 제안하는것을 확인해보자.
요약하자면 <body> 태그의 Text Density 값을 threshold로 잡겠다!
해당 값보다 큰 TD를 가진 태그 -> content, 아니면 -> noise 로 분류하겠다
이거다
그래서 계산해보았다.
저 값을 기준으로 상위 값을 살펴보자
불필요한 태그 값을 날린 뒤 계산했더니 터무니 없는 값이 나온다.
TD score 값이 9이상인 결과는 위 가시화 결과만 봐도 한가득이다.
여기서 부터 ?? 가득인 상태였다.
그래.. 그 뒤의 챕터를 보자 하는 심정으로 넘어갔다.
Text Density Sum 이라는 개념을 제시한다.
노이즈와 컨텐츠를 구별하기 위해 제시되는 계산이다.
좋다. 계산 식은 위 식보다 어렵지 않고, 이미 위에서 구한 값들을 이용해서 반복해 더해주기만 하면 되니 문제 될것이 없어보인다.
먼저 densitySum 함수를 정의하자
def density_sum(tag):
score = 0
if(len(tag.findChildren()) == 0):
if(tag.name != 'a'):
score = TD_return(tag)
else:
score = CTD_return(tag)
else:
for child_tag in tag.findChildren():
score += density_sum(child_tag)
return score
두번째 난관은 밑에서부터 진행된다.
extract content 하는 수도 코드를 적어놨고 식 위엔 threshold 정의가 되어있다.
위 논문에서 소개된 두번째 threshold 값 계산은 아래와 같다.
1. 전체 page에서 DensitySum 값이 가장 높은 태그를 고른다.
2. 해당 태그와 <body>태그 사이의 DensitySum 값이 가장 작은 태그의 Text Density 값을 threshold값으로 지정한다.
1번에 대해 전체 page라는 건 <body> 인건지 <html>인건지 정확하게 모르겠다.
그래서 진행을 여태 <body>로 해왔으니 <body> 태그 아래 DensitySum값을 골라본다.
2번에 대해 <body> 태그와 해당 max 태그 사이의 태그를 조사해보자
위 결과를 보면 알겠지만 없다. body 바로 밑에 max 태그가 있는셈이다. 그럼 최소 threshold의 경우 저 40만의 가까운 threshold값을 가지게 된다.
하루정도 더 고민해보았으나, 이해가 안돼 여기까지만 구현하고 마치기로 하였다.
머리속에 넣어두고 어느날 아하! 하는 순간이 오기를 바라며 글을 마친다.
잘못된 점이나 궁금한 사항에 대해 댓글 남겨주시면 감사하겠다.
아니면 구현하시고 링크 남겨주셔도 황송할것같다.
( 저자의 git 허브는 들러보았으나, C++이라 그런지 잘모르겠... )
'text > Python' 카테고리의 다른 글
문장 생성 해보기 with. mini-GPT (feat. 네이버 기사 댓글) (0) | 2021.04.21 |
---|---|
문장 생성 해보기 (feat. 네이버 기사 댓글) (0) | 2021.04.02 |
네이버 기사 댓글 가져오기 (5) | 2021.03.29 |
네이버 영화평 가져오기 (0) | 2021.03.29 |
Word2Vec parameter 정리 (0) | 2021.02.18 |
댓글