text/Python

DOM Based Content Extraction via Text Density 구현해보기

hoonzii 2021. 4. 1. 13:00
반응형

결론부터 말하자면 반쪽짜리 구현이다. 참고하고 더 읽을지 말지 결정하기 바란다.

 

사용모듈

- python 3.7

- requests = 뉴스기사 가져오기 위함

- BeautifulSoup = html 파싱을 위함

 

 

이전 두개의 글( 네이버 영화평, 네이버 댓글 수집) 에서 나는 크롤링이라는 말을 쓰지 않았다.

왜냐하면 어떤 velog 글을 보게 되었는데

 

velog.io/@mowinckel/%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%A7%81-I

 

🖨 '웹 크롤러' 좀 그만 만들어라

아무튼 그만 만들어라.

velog.io

해당 글에서 나온 크롤링의 정의를 보고 내가 잘못 알고있었구나 라는걸 깨달았기 때문이다.

 

또한 직접 수집해보며 느낀점으로는 해당 page가 리뉴얼해 html tag나 구조가 변경되면 내가 만든 코드가 무용지물이 된다는 점이 아쉬웠다.

저 글 중간에 그것에 대한 논문이 하나 있다고 링크되어있길래 읽어보았다.

 

DOM Based Content Extraction via Text Density

(google scholar 검색하면 바로 나오니 링크는 생략)

 

내가 평소에 논문을 많이 읽는 것도 아니고...

온통 영어라 잘 안들어와서.. 누군가 설명한게 없을까...(구글링)

 

velog.io/@o-ozogie/%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%B0%80%EB%8F%84%EB%A5%BC-%ED%86%B5%ED%95%9C-DOM-%EA%B8%B0%EB%B0%98-%EC%BB%A8%ED%85%90%EC%B8%A0-%EC%B6%94%EC%B6%9C

 

[번역] 텍스트 밀도를 통한 DOM 기반 컨텐츠 추출

최대한 원문 그대로 번역하고자 했고 애매한 부분들은 다 번역기 돌렸지만 그럼에도 불구하고 제가 이해한대로 번역했기 때문에 의역에 의한 왜곡이 있을 수 있습니다. 오타, 오역 지적해주시

velog.io

 

있었다.

먼저 논문과 번역글을 먼저 정독하기를 바란다.  (이글을 쓰는 나도 사실 이해를 잘한건지)

그렇다면 해당 글을 읽고 잘 만든다면 페이지 구조 변경에도 상관없는 무적의 수집기를 만들 수 있을까??

 

한번 해당 논문이 얘기하는 대로 만들어보자.

 

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>

위 html을 DOM tree로 나타낸 그림

 

먼저 논문은 텍스트 밀도(Text Density) 라는 개념을 설명한다.

html은 DOM tree 구조로 되어있고, 콘텐츠가 포함된 특정 tag의 경우

자식 tag의 숫자는 적고, 문자의 수는 많다는 설명을 통해 시작한다.

 

문자 개수, 태그 개수 정의

계산식

<i> tag의 텍스트 밀도 값 계산

해당 식을 코드로 구현하기 앞서, 논문에서 나온 선행 조건으로

1. html tag 중 <body>만 사용하기

2. <body> tag 중 style, comment, script 등의 본문 추출과 상관없는 태그 제외

 

2가지 조건을 고려해 코드로 작성해보자

오늘 실험을 도와줄 기사 url

news.v.daum.net/v/20210330235246128

 

오세훈 토론 중 폭발 "기가 막혀..박영선 입만 열면 모함"

[서울=뉴시스] 김지은 김성진 한주홍 기자 = 서울시장 자리를 놓고 경쟁하는 박영선 더불어민주당 후보와 오세훈 국민의힘 후보가 30일 중앙선관위원회가 주최한 TV토론회에서 오 후보가 내곡동

news.v.daum.net

import requests
url = 'https://news.v.daum.net/v/20210330235246128'
html = requests.get(url)
soup = BeautifulSoup(html.content, "html.parser")

기사 가져와 soup로 만들기

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))

처음 요소 개수보다 줄어든것을 확인(2번 조건 제외시)

결과를 살펴보자.

텍스트 밀도 점수 가시화

 

논문에 나온 그래프와 유사하게 뽑힌걸 확인할수 있다.

점수가 일정 값( 80 이상 ) 보다 높게 나온 태그를 뽑아 결과를 한번 살펴보자

 

결과 값 예시

오?? 생각보다 기사 본문에 대해서 잘 뽑아준걸 확인했다. 하지만 노이즈 역시 뽑혀 나오는 걸 확인할 수 있다.

 

2. Composite Text Density

논문에서는 노이즈가 <a> tag를 통해 많이 나타나는 경향을 발견했고, 해당 태그 값에 대해 TD 계산이 아닌 다른 계산식을 제안했다.

 

일단 용어가 추가 된다.

a tag에 대한 tag개수, 문자개수 추가

 

그리고 식이 추가되는데 tag<i>가 <a> tag일 경우엔 아래식으로 적용된다고 한다.

아이고 머리야....

식으로 보니까 머리아프다. 기호 하나씩 정리하고 코드로 구현해보자.

 

Ci = <i> tag 아래 모든 문자 개수 합

Ti = <i> tag 아래 모든 태그 개수 합

 

 

 

 

 

식을 쪼개서 생각하기

 

차례대로 구현하면 된다.

CTD 계산식 함수 구현

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번대 태그들에서 점수값이 향상된 것이 보이는데 실제로 유의미한 값이 검출되는지 살펴보자

태그 index >= 300 이고,, 점수가 80이상인 값 살펴보기

기사 제목 링크 값들의 점수가 검출된 것을 볼수 있다.

content 라면 content이긴 하나 원하는 값인지는 사람마다 다르니 pass...

 

게다가 논문과 표를 비교해보면 (반대로!) 가시화 결과 부분에서 값들이 전체적으로 다 증가되는 경향이 보인다.

(잘못 구현한 걸까??)

 

3. Density Sum 및 Threshold

***************이 챕터부터 꼬이기 시작한다. *****************

 

위 값들을 살펴보면, 어느 점수에서 짜를건지 명확하지 않다.

상수값은 안될 말이다. 문서마다 값 나오는게 다를테니...

 

논문에서는 threshold 값 제안하는것을 확인해보자.

 

1번째 threshold 정의

요약하자면 <body> 태그의 Text Density 값을 threshold로 잡겠다!

해당 값보다 큰 TD를 가진 태그 -> content, 아니면 -> noise 로 분류하겠다

이거다

그래서 계산해보았다.

불 필요 태그 값들을 날린뒤, TD 값 계산

 

저 값을 기준으로 상위 값을 살펴보자

매우 별로인 결과가 나온다.

불필요한 태그 값을 날린 뒤 계산했더니 터무니 없는 값이 나온다. 

TD score 값이 9이상인 결과는 위 가시화 결과만 봐도 한가득이다.

 

여기서 부터 ?? 가득인 상태였다.

 

그래.. 그 뒤의 챕터를 보자 하는 심정으로 넘어갔다.

Text Density Sum 이라는 개념을 제시한다. 

번역 링크의 글을 인용한다.

노이즈와 컨텐츠를 구별하기 위해 제시되는 계산이다.

 

density sum 계산

좋다. 계산 식은 위 식보다 어렵지 않고, 이미 위에서 구한 값들을 이용해서 반복해 더해주기만 하면 되니 문제 될것이 없어보인다.

먼저 densitySum 함수를 정의하자

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값을 골라본다.

오지게 높은 density Sum 값이 나온다.

2번에 대해 <body> 태그와 해당 max 태그 사이의 태그를 조사해보자

max 노드의 부모 노드를 따라가다 body 전에 멈추면 되겠지

위 결과를 보면 알겠지만 없다. body 바로 밑에 max 태그가 있는셈이다. 그럼 최소 threshold의 경우 저 40만의 가까운 threshold값을 가지게 된다.

?!

하루정도 더 고민해보았으나, 이해가 안돼 여기까지만 구현하고 마치기로 하였다.

머리속에 넣어두고 어느날 아하! 하는 순간이 오기를 바라며 글을 마친다.

 

잘못된 점이나 궁금한 사항에 대해 댓글 남겨주시면 감사하겠다.

아니면 구현하시고 링크 남겨주셔도 황송할것같다.

( 저자의 git 허브는 들러보았으나, C++이라 그런지 잘모르겠... )

반응형