├── .gitignore ├── 基于行块分布函数的通用网页正文抽取算法.pdf ├── README.md └── html-extractor.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | *.pyc -------------------------------------------------------------------------------- /基于行块分布函数的通用网页正文抽取算法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnyangkui/html-extractor/HEAD/基于行块分布函数的通用网页正文抽取算法.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html-extractor 2 | 3 | #### 《基于行块分布函数的通用网页正文抽取算法》-稍许改进,Python实现 4 | 5 | 在第六届中国软件杯大赛分布式爬虫赛题中,实现了该算法,意图实现新闻、博客类网站正文的自动结构化。比赛提供的测试要求提取的正文一字不差,不能包含多余的不属于正文的内容,也不能少了正文内容。《基于行块分布函数的通用网页正文抽取算法》提取的正文基本正确,但要做到一字不差,却有点困难,于是提出了以下改进。 6 | 7 | 该正文抽取算法在基于行块分布函数的网页正文抽取方法上做了稍许改进,提高了准确率,使提取的正文更加“一字不差”。在比赛给出的测试包下进行测试,准确率达到90以上。 8 | 9 | ## 算法实现描述 10 | 11 | 对于新闻博客类网站,一般文字内容最集中的区域就是正文。但是也不尽然,有些新闻正文很短,而导航栏内容信息很多。首先,过滤噪声标签等的影响。采用正则过滤掉ul、script、style、注释等内容,标记该内容为A,然后过滤所有标签,再标记该内容为B。然后定义k行为一个行块,去掉空格的长度为行块长度。将过滤掉标签的内容B进行行块长度统计,根据行块分布找出最密集的区域则为初步得到的正文内容Text。 12 | 13 | 该正文内容已经基本正确,但是如果该正文区域后方或前方不远处出现小部分无关内容,也会计入正文内容,导致有稍许误差。为了提高准确率,在去掉噪声的网页A中全文搜索初步正文Text中的块信息,一般为“p“标签,再计算得到该标签的父节点。将所有获得的父节点存储下来,出现次数最多的父节点标签记为包含整个正文区域的标签。直接从该标签提取文字即为正文。该过程失败则使用之前提取的内容Text作为正文,成功则使用该过程提取的文字作为正文。 14 | 15 | --- 16 | 17 | 附:《基于行块分布函数的通用网页正文抽取》是哈尔滨工业大学信息检索研究中心陈 鑫 (Xin Chen) 的研究成果,详情在这:[https://code.google.com/archive/p/cx-extractor/](https://code.google.com/archive/p/cx-extractor/) 18 | -------------------------------------------------------------------------------- /html-extractor.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | import re 3 | import requests 4 | from collections import Counter 5 | from bs4 import BeautifulSoup 6 | 7 | 8 | def get_html(url): 9 | """ 获取html """ 10 | # obj = requests.get(url) 11 | # return obj.text 12 | 13 | try: 14 | obj = requests.get(url) 15 | code = obj.status_code 16 | if code == 200: 17 | # 防止中文正文乱码 18 | html = obj.content 19 | html_doc = str(html, 'utf-8') 20 | return html_doc 21 | return None 22 | except: 23 | return None 24 | 25 | 26 | def filter_tags(html_str, flag): 27 | """ 过滤各类标签 28 | :param html_str: html字符串 29 | :param flag: 是否移除所有标签 30 | :return: 过滤标签后的html字符串 31 | """ 32 | html_str = re.sub('(?is)', '', html_str) 33 | html_str = re.sub('(?is)', '', html_str) # remove html comment 34 | html_str = re.sub('(?is).*?', '', html_str) # remove javascript 35 | html_str = re.sub('(?is).*?', '', html_str) # remove css 36 | html_str = re.sub('(?is).*?', '', html_str) # remove a 37 | html_str = re.sub('(?is).*?', '', html_str) # remove li 38 | # html_str = re.sub('&.{2,5};|&#.{2,5};', '', html_str) #remove special char 39 | if flag: 40 | html_str = re.sub('(?is)<.*?>', '', html_str) # remove tag 41 | return html_str 42 | 43 | 44 | def extract_text_by_block(html_str): 45 | """ 根据文本块密度获取正文 46 | :param html_str: 网页源代码 47 | :return: 正文文本 48 | """ 49 | html = filter_tags(html_str, True) 50 | lines = html.split('\n') 51 | blockwidth = 3 52 | threshold = 86 53 | indexDistribution = [] 54 | for i in range(0, len(lines) - blockwidth): 55 | wordnum = 0 56 | for j in range(i, i + blockwidth): 57 | line = re.sub("\\s+", '', lines[j]) 58 | wordnum += len(line) 59 | indexDistribution.append(wordnum) 60 | startindex = -1 61 | endindex = -1 62 | boolstart = False 63 | boolend = False 64 | arcticle_content = [] 65 | for i in range(0, len(indexDistribution) - blockwidth): 66 | if (indexDistribution[i] > threshold and boolstart is False): 67 | if indexDistribution[i + 1] != 0 or indexDistribution[i + 2] != 0 or indexDistribution[i + 3] != 0: 68 | boolstart = True 69 | startindex = i 70 | continue 71 | if boolstart is True: 72 | if indexDistribution[i] == 0 or indexDistribution[i + 1] == 0: 73 | endindex = i 74 | boolend = True 75 | tmp = [] 76 | if boolend is True: 77 | for index in range(startindex, endindex + 1): 78 | line = lines[index] 79 | if len(line.strip()) < 5: 80 | continue 81 | tmp.append(line.strip() + '\n') 82 | tmp_str = ''.join(tmp) 83 | if u"Copyright" in tmp_str or u"版权所有" in tmp_str: 84 | continue 85 | arcticle_content.append(tmp_str) 86 | boolstart = False 87 | boolend = False 88 | return ''.join(arcticle_content) 89 | 90 | 91 | def extract_text_by_tag(html_str, article): 92 | """ 全网页查找根据文本块密度获取的正文的位置,获取文本父级标签内的正文,目的是提高正文准确率 93 | :param html: 网页html 94 | :param article: 根据文本块密度获取的正文 95 | :return: 正文文本 96 | """ 97 | lines = filter_tags(html_str, False) 98 | soup = BeautifulSoup(lines, 'lxml') 99 | p_list = soup.find_all('p') 100 | p_in_article = [] 101 | for p in p_list: 102 | if p.text.strip() in article: 103 | p_in_article.append(p.parent) 104 | tuple = Counter(p_in_article).most_common(1)[0] 105 | article_soup = BeautifulSoup(str(tuple[0]), 'xml') 106 | return remove_space(article_soup.text) 107 | 108 | 109 | def remove_space(text): 110 | """ 移除字符串中的空白字符 """ 111 | text = re.sub("[\t\r\n\f]", '', text) 112 | return text 113 | 114 | 115 | def extract(url): 116 | """ 抽取正文 117 | :param url: 网页链接 118 | :return:正文文本 119 | """ 120 | html_str = get_html(url) 121 | if html_str == None: 122 | return None 123 | article_temp = extract_text_by_block(html_str) 124 | try: 125 | article = extract_text_by_tag(html_str, article_temp) 126 | except: 127 | article = article_temp 128 | return article 129 | 130 | 131 | if __name__ == '__main__': 132 | url = 'http://www.eeo.com.cn/2020/0215/376405.shtml' 133 | text = extract(url) 134 | print(text) 135 | --------------------------------------------------------------------------------