In [1]:
from glob import glob
import pandas as pd
import numpy as np
import nltk
import matplotlib.pyplot as plt
from bs4 import BeautifulSoup
from difflib import SequenceMatcher
import webnectar as wbn
from goose import Goose
%matplotlib inline
plt.rcParams['figure.figsize'] = (10,3)
%load_ext autoreload
%autoreload 2
A popularização da World Wide Web trouxe um volume crescente de publicações. Simultaneamente, cresceu a necessidade e o poder computacional para processar esse fluxo contínuo de informação. Infelizmente a primeira etapa na mineração de dados neste tipo de documento já é criticamente complicada: extração do conteúdo textual principal de documentos em HTML.
Desde a criação do HTML, os documentos publicados na Web tem sofrido acréscimos de design e estrutura. Além de inserir formatação, os provedores de conteúdo frequentemente apresentam conteúdos adicionais relacionados, permitem comentários de visitantes e possuem conteúdo dinâmico. Quanto maior a complexidade de apesentação do documento, maior a dificuldade de identificar o conteúdo principal do documento em meio ao código HTML.
Atualmente é frequente o emprego de 80% a 99% dos caracteres do documento dedicados a elementos não textuais ou conteúdo de interesse secundário como links para outras notícias. Além disso, cada provedor é responsável por embutir no documento suas formatações específicas que podem variar com o tempo. Portanto, ajustar a extração a um provedor ou a um design específico é uma solução temporária e pouco relevante no contexto do Big Data.
In [2]:
## Para validação, usar o L3S-GN1
dataset = glob('datasets/L3S-GN1/extracts/*')
dataset = [doc.replace("datasets/L3S-GN1/extracts/","") for doc in dataset]
def getHTML(doc):
with open('datasets/L3S-GN1/original/'+doc+'.html', "r") as myfile:
return myfile.read().decode('UTF-8')
def getTEXT(doc):
with open('datasets/L3S-GN1/extracts/'+doc, "r") as myfile:
return myfile.read().decode('UTF-8')
In [3]:
def measure_docs_composition(doc):
html = getHTML(doc)
txt = getTEXT(doc)
rlen = len(html)
clen = len(txt)
return rlen,clen
docs_composition = { doc : measure_docs_composition(doc) for doc in dataset }
docs_composition = pd.DataFrame(docs_composition).transpose()
docs_composition.columns = ["documento html",u"conteúdo"]
In [4]:
fig, (ax,bx) = plt.subplots(ncols=2,figsize=(8,3))
ax.set_title(u"Número de caracteres")
bx.set_title(u"conteúdo/html")
docs_composition[["documento html",u"conteúdo"]].boxplot(ax=ax,vert=False)
docs_composition['%'] = 100.0*docs_composition[u"conteúdo"]/docs_composition["documento html"]
docs_composition[['%']].boxplot(ax=bx,return_type='dict')
ax.set_position([0,0.3,0.8,0.8])
bx.set_position([0.85,0.3,0.2,0.8])
#ax.set_xscale('log')
Para quem está familiarizado com a natureza padrão de um documento HTML, encontrar o conteúdo principal não requer a leitura completa do documento. Inadvertidamente, ao ler usamos um algoritmo visual e intuitivo porém altamente eficiente:
Começamos do início do texto procurando por uma região densa em linguagem natural. Ao encontrar, lemos seu conteúdo e decidimos se trata do assunto a qual o documento é dedicado. Caso positivo, este provavelmente faz parte do conteúdo principal da página.
O bom funcionamento deste método é embasado nas seguintes hipóteses:
O que será apresentado neste trabalho é uma formalização computacional deste método, formalmente dividido em três algoritmos:
A etapa mais trivial do procedimento é extrair apenas o conteúdo em linguagem natural da página, ignorando HTML, scripts e mídia. Diversas ferramentas existentes realizam esta tarefa, mas é de interesse das próximos etapas de processamento que seja mantido o espaçamento de informação presente no HTML.
Em outras palavras, o objetivo é extrair o texto do documento mantendo a correspondência com as respectivas linhas de código no HTML. Assim, espera-se que o texto principal da página esteja aglutinado enquanto demais resíduos estejam esparsos.
Entretanto, por causa do inerente impacto da estrutura textual do documento HTML, é necessário uma etapa de pré-processamento para normalizar o código HTML. Assim protegemos as futuras análises da influência da indentação e estilo de organização estrutural do autor da página.
Nesta etapa de normalização, utilizaremos o método prettify()
da biblioteca BeautifulSoup, que reconstrói o documento HTML após a análise de sua árvore DOM. Assim, após remover a indentação teremos uma correspondência entre comprimento da linha e número caracteres de código HTML.
In [5]:
def measure_text_extraction_composition(doc):
try:
raw = getHTML(doc)
raw_lines = raw.split('\n')
rl = np.array([len(line) for line in raw_lines])
article = wbn.extract(raw_html=raw)
norm = article.normdoc
norm_lines = norm.split('\n')
nl = np.array([len(line) for line in norm_lines])
text = article.rawtext
text_lines = text.split('\n')
tl = np.array([len(line) for line in text_lines])
return rl.mean(),nl.mean(),tl.mean()
except Exception as e:
print doc+" deu merda: "+repr(e)
return 0,0,0
text_composition = { doc: measure_text_extraction_composition(doc) for doc in dataset}
text_composition = pd.DataFrame(text_composition)
text_composition = text_composition.transpose()
text_composition.columns = ["html original", "html normalizado",u"texto extraído"]
In [6]:
fig, ax = plt.subplots(figsize=(10,3))
text_composition[text_composition<300].boxplot(ax=ax,vert=False)
#ax.set_xscale('log')
pass
Como essa extração é totalmente indiscriminada, i.e. obtém todo o conteúdo textual, esperamos um alto recall e uma precisão baixa.
O recall teve uma média de 0,988568 e desvio padrão 0,039959. Apenas um documento apresentou recall fora dos 25%, mas ao averiguar a estrutura deste documento em particular foi possível observar um erro de digitação. Por não fechar aspas em um script, o parser html5lib corrigiu o documento colocando todo o conteúdo consequente dentro do script. Naturalmente, o extrator de texto ignorou o conteúdo considerando ser script.
In [7]:
def measure_text_extraction_scores(doc):
try:
mytry = wbn.extract(raw_html=getHTML(doc)).rawtext
except:
return 0,0
truth = getTEXT(doc)
extract_tokens = nltk.tokenize.word_tokenize(mytry)
true_tokens = nltk.tokenize.word_tokenize(truth)
Nretrieved = len(extract_tokens)
Nrelevant = len(true_tokens)
matches = SequenceMatcher(None,extract_tokens,true_tokens,autojunk=False).get_matching_blocks()
Ncommon = 0
for match in matches:
Ncommon += match[2]
(p,r) = (0,0)
try:
p = 1.0*Ncommon/Nretrieved
except:
pass
try:
r = 1.0*Ncommon/Nrelevant
except:
pass
return p,r
text_scores = { doc: measure_text_extraction_scores(doc) for doc in dataset}
text_scores = pd.DataFrame(text_scores)
text_scores = text_scores.transpose()
text_scores.columns = ["precision","recall"]
In [8]:
fig, ax = plt.subplots(figsize=(10,3))
text_scores.boxplot(ax=ax,vert=False)
text_scores.describe()
Out[8]:
Dado que conseguimos extrar apenas o conteúdo textual de um documento HTML preservando o espaçamento original dos campos de texto, como estimar um subconjunto representativo do contexto do documento? É nesta etapa que a normalização e indentação do documento original se evidenciam críticas, pois será necessário uma análise estrutural do texto.
In [9]:
def measure_context_scores(doc):
try:
mytry = wbn.extract(raw_html=getHTML(doc)).context
except:
return 0,0
truth = getTEXT(doc)
extract_tokens = nltk.tokenize.word_tokenize(mytry)
true_tokens = nltk.tokenize.word_tokenize(truth)
Nretrieved = len(extract_tokens)
Nrelevant = len(true_tokens)
matches = SequenceMatcher(None,extract_tokens,true_tokens,autojunk=False).get_matching_blocks()
Ncommon = 0
for match in matches:
Ncommon += match[2]
(p,r) = (0,0)
try:
p = 1.0*Ncommon/Nretrieved
except:
pass
try:
r = 1.0*Ncommon/Nrelevant
except:
pass
return p,r
context_scores = { doc: measure_context_scores(doc) for doc in dataset}
context_scores = pd.DataFrame(context_scores)
context_scores = context_scores.transpose()
context_scores.columns = ["precision","recall"]
In [10]:
fig, ax = plt.subplots(figsize=(10,3))
ax.set_title("L3S-GN1")
ax.set_xlabel("My Scores")
context_scores.boxplot(ax=ax,vert=False)
context_scores.describe()
Out[10]:
In [11]:
fdp = context_scores[context_scores.precision<0.1].index[0]
context_scores.loc[fdp]
Out[11]:
In [12]:
def measure_goose_scores(doc):
try:
mytry = Goose({'enable_image_fetching':False}).extract(raw_html=getHTML(doc)).cleaned_text
except:
return 0,0
truth = getTEXT(doc)
extract_tokens = nltk.tokenize.word_tokenize(mytry)
true_tokens = nltk.tokenize.word_tokenize(truth)
Nretrieved = len(extract_tokens)
Nrelevant = len(true_tokens)
matches = SequenceMatcher(None,extract_tokens,true_tokens,autojunk=False).get_matching_blocks()
Ncommon = 0
for match in matches:
Ncommon += match[2]
(p,r) = (0,0)
try:
p = 1.0*Ncommon/Nretrieved
except:
pass
try:
r = 1.0*Ncommon/Nrelevant
except:
pass
return p,r
goose_scores = { doc: measure_goose_scores(doc) for doc in dataset}
goose_scores = pd.DataFrame(goose_scores)
goose_scores = goose_scores.transpose()
goose_scores.columns = ["precision","recall"]
In [13]:
fig, ax = plt.subplots(figsize=(10,3))
ax.set_title("L3S-GN1")
ax.set_xlabel("Goose Scores")
goose_scores.boxplot(ax=ax,vert=False)
goose_scores.describe()
Out[13]:
RASCUNHO
RASCUNHO
RASCUNHO
RASCUNHO
RASCUNHO
RASCUNHO
[1] http://www.w3.org/People/Raggett/tidy/
[2] http://tidy.sourceforge.net/docs/tidy_man.html
Christian Kohlschuetter, Peter Fankhauser, Wolfgang Nejdl Boilerplate Detection using Shallow Text Features WSDM 2010: Third ACM International Conference on Web Search and Data Mining New York City, NY USA. Disponível em <http://www.l3s.de/~kohlschuetter/publications/wsdm187-kohlschuetter.pdf > acessado em 13/10/2014.
In [29]:
html = getHTML(dataset[0])
article = wbn.extract(raw_html=html)
lens = [len(line) for line in article.rawtext.split('\n')]
pds = pd.Series(lens)
pd.rolling_mean(pds,100).shift(-50).plot()
npa = np.convolve(lens, np.ones((100,))/100, mode='same')
plt.plot(npa)
npa
Out[29]: