Newspaper3k源码解析:架构设计与核心算法

Newspaper3k源码解析:架构设计与核心算法

Newspaper3k是一个专业的新闻文章提取库,采用清晰的分层架构设计,将复杂的网页内容提取任务分解为多个职责明确的模块。整个项目架构体现了高内聚、低耦合的软件工程原则,主要分为API接口层、核心业务层、数据处理层和基础工具层四个主要层次。核心模块包括API接口模块、Article类、Source类、提取器模块、清理器模块、NLP模块、解析器模块、网络模块、URL处理模块、多线程处理模块和配置管理模块。项目广泛应用了工厂模式、策略模式、观察者模式和模板方法等多种设计模式,并通过模块化的语言支持设计实现了多语言处理能力。

项目架构与模块划分分析

Newspaper3k作为一个专业的新闻文章提取库,采用了清晰的分层架构设计,将复杂的网页内容提取任务分解为多个职责明确的模块。整个项目的架构设计体现了高内聚、低耦合的软件工程原则,使得各个模块可以独立开发和测试。

核心架构层次

Newspaper3k的架构可以分为四个主要层次:

mermaid

模块详细划分

1. API接口模块 (api.py)

作为整个库的入口点,提供简洁的面向用户接口:

# 主要API函数
def build(url='', dry=False, config=None, **kwargs) -> Source
def build_article(url='', config=None, **kwargs) -> Article
def fulltext(html, language='en') -> str
def languages() -> list
2. 核心业务模块

Article类 (article.py) 负责单篇文章的完整生命周期管理:

mermaid

Source类 (source.py) 处理整个新闻源的多篇文章批量操作:

class Source:
    def __init__(self, url, config=None):
        self.url = url
        self.config = config
        self.articles = []  # 文章列表
        self.categories = []  # 分类URL
        self.feeds = []  # Feed URL
        
    def build(self):
        """构建新闻源"""
    def download_articles(self, threads=1):
        """多线程下载文章"""
    def parse_articles(self):
        """批量解析文章"""
3. 数据处理模块

提取器模块 (extractors.py) 包含各种内容提取算法:

提取功能方法名描述
作者提取get_authors()从byline和meta标签提取作者
发布日期get_publishing_date()解析文章发布日期
标题提取get_title()提取和清理文章标题
元数据get_meta_*()获取各种meta信息
内容评分calculate_best_node()计算最佳内容节点

清理器模块 (cleaners.py) 负责HTML内容的清理和标准化:

class Cleaners:
    def clean(self, doc_to_clean):
        """主清理方法"""
    def clean_body_classes(self, doc):
        """清理body类名"""
    def remove_scripts_styles(self, doc):
        """移除脚本和样式"""
    def clean_bad_tags(self, doc):
        """清理不良标签"""

NLP模块 (nlp.py) 提供自然语言处理功能:

mermaid

4. 基础工具模块

解析器模块 (parsers.py) 基于lxml的HTML解析工具集,提供XPath和CSS选择器支持:

class Parser:
    @classmethod
    def css_select(cls, node, selector):
        """CSS选择器查询"""
    @classmethod  
    def get_elements_by_tag(cls, node, tag=None, attr=None, value=None):
        """按标签名和属性查询"""
    @classmethod
    def clean_article_html(cls, node):
        """清理文章HTML"""

网络模块 (network.py) 处理HTTP请求和响应:

def get_html(url, config=None, response=None):
    """获取HTML内容"""
def get_request_kwargs(timeout, useragent, proxies, headers):
    """构建请求参数"""
def multithread_request(urls, config=None):
    """多线程请求处理"""

URL处理模块 (urls.py) 提供URL标准化和验证功能:

功能方法描述
URL清理remove_args()移除URL参数
域名提取get_domain()提取主域名
URL验证valid_url()验证URL有效性
重定向处理redirect_back()处理重定向URL
5. 多线程处理模块 (mthreading.py)

实现高效的多线程下载框架:

class NewsPool:
    def __init__(self, num_threads=10, timeout_seconds=1):
        self.num_threads = num_threads
        self.timeout = timeout_seconds
        
    def add_task(self, func, *args, **kwargs):
        """添加下载任务"""
    def wait_completion(self):
        """等待所有任务完成"""
6. 配置管理模块 (configuration.py)

集中管理所有配置参数:

class Configuration:
    def __init__(self):
        self.MIN_WORD_COUNT = 300      # 最小词数
        self.MIN_SENT_COUNT = 7        # 最小句子数
        self.MAX_TITLE = 200           # 标题最大长度
        self.fetch_images = True       # 是否获取图片
        self.language = 'en'           # 默认语言
        self.number_threads = 10       # 线程数

模块间协作关系

Newspaper3k的模块间采用清晰的依赖关系:

mermaid

设计模式应用

Newspaper3k中广泛应用了多种设计模式:

  1. 工厂模式: api.build()api.build_article() 作为工厂方法
  2. 策略模式: 不同语言的停用词处理采用策略模式
  3. 观察者模式: 多线程下载中的任务管理
  4. 模板方法: 文章处理流程的标准化

国际化支持架构

项目通过模块化的语言支持设计,实现了多语言处理能力:

mermaid

这种架构设计使得Newspaper3k能够高效、准确地处理各种语言的新闻内容,同时保持了代码的可维护性和扩展性。每个模块都有明确的职责边界,便于单独测试和优化,体现了优秀的软件架构设计思想。

HTML解析器与内容提取算法

Newspaper3k的HTML解析与内容提取是其核心功能之一,它通过精心设计的算法从复杂的网页结构中准确提取文章正文内容。该系统的设计借鉴了python-goose项目的优秀思想,并结合现代网页结构特点进行了优化。

解析器架构设计

Newspaper3k采用分层架构设计,将HTML解析与内容提取逻辑分离:

mermaid

Parser解析层

Parser类基于lxml库构建,提供统一的DOM操作接口:

class Parser(object):
    @classmethod
    def fromstring(cls, html):
        # 处理编码问题并转换为lxml文档对象
        html = cls.get_unicode_html(html)
        return lxml.html.fromstring(html)
    
    @classmethod
    def getElementsByTag(cls, node, tag=None, attr=None, value=None):
        # 支持属性筛选的XPath查询
        selector = 'descendant-or-self::%s' % (tag or '*')
        if attr and value:
            selector = '%s[contains(@%s, "%s")]' % (selector, attr, value)
        return node.xpath(selector)

内容清理策略

DocumentCleaner负责移除无关的HTML元素,采用多重清理策略:

清理类型目标元素清理方法
脚本样式script, style直接移除
广告相关特定class/id正则匹配移除
社交媒体facebook, twitter特征匹配移除
空标签无内容元素深度遍历移除
def clean(self, doc_to_clean):
    """多层清理流水线"""
    doc_to_clean = self.clean_body_classes(doc_to_clean)
    doc_to_clean = self.remove_scripts_styles(doc_to_clean)
    doc_to_clean = self.clean_bad_tags(doc_to_clean)
    doc_to_clean = self.remove_nodes_regex(doc_to_clean, self.caption_re)
    return doc_to_clean

核心提取算法

ContentExtractor采用基于权重的重力评分算法(Gravity Score Algorithm)来识别文章主体内容。

算法流程

mermaid

1. 候选节点筛选

算法首先筛选潜在的文本容器节点:

def nodes_to_check(self, doc):
    """返回需要检查的节点列表"""
    nodes_to_check = []
    for tag in ['p', 'pre', 'td']:  # 段落、预格式文本、表格单元格
        items = self.parser.getElementsByTag(doc, tag=tag)
        nodes_to_check += items
    return nodes_to_check
2. 文本质量评估

每个节点通过停用词密度和链接密度进行评估:

def is_highlink_density(self, e):
    """检查链接密度,高链接密度通常表示导航或广告内容"""
    links = self.parser.getElementsByTag(e, tag='a')
    if not links:
        return False
    
    text = self.parser.getText(e)
    words = [word for word in text.split() if word.isalnum()]
    if not words:
        return True
    
    words_number = float(len(words))
    link_text = ''.join(self.parser.getText(link) for link in links)
    num_link_words = float(len(link_text.split()))
    num_links = float(len(links))
    
    link_divisor = num_link_words / words_number
    score = link_divisor * num_links
    return score >= 1.0
3. 重力评分计算

核心算法通过多维度计算每个节点的权重:

def calculate_best_node(self, doc):
    top_node = None
    nodes_to_check = self.nodes_to_check(doc)
    starting_boost = 1.0
    parent_nodes = []
    nodes_with_text = []

    # 第一阶段:初步筛选
    for node in nodes_to_check:
        text_node = self.parser.getText(node)
        word_stats = self.stopwords_class.get_stopword_count(text_node)
        high_link_density = self.is_highlink_density(node)
        
        if word_stats.get_stopword_count() > 2 and not high_link_density:
            nodes_with_text.append(node)

    # 第二阶段:评分计算
    for i, node in enumerate(nodes_with_text):
        boost_score = 0.0
        
        # 可提升节点加分
        if self.is_boostable(node):
            boost_score = (1.0 / starting_boost) * 50
            starting_boost += 1
        
        # 末尾节点减分(通常是非内容区域)
        if len(nodes_with_text) > 15:
            if (len(nodes_with_text) - i) <= len(nodes_with_text) * 0.25:
                booster = float(len(nodes_with_text) * 0.25 - (len(nodes_with_text) - i))
                boost_score = -pow(booster, 2.0)

        # 综合评分
        text_node = self.parser.getText(node)
        word_stats = self.stopwords_class.get_stopword_count(text_node)
        upscore = int(word_stats.get_stopword_count() + boost_score)

        # 向父节点传播分数
        parent_node = self.parser.getParent(node)
        self.update_score(parent_node, upscore)
        self.update_node_count(parent_node, 1)
        
        if parent_node not in parent_nodes:
            parent_nodes.append(parent_node)

    # 第三阶段:选择最高分节点
    top_node_score = 0
    for node in parent_nodes:
        score = self.get_score(node)
        if score > top_node_score:
            top_node = node
            top_node_score = score

    return top_node
智能提升机制

算法包含智能提升逻辑,识别真正的内容段落:

def is_boostable(self, node):
    """判断节点是否应该被提升评分"""
    para = "p"
    steps_away = 0
    minimum_stopword_count = 5
    max_stepsaway_from_node = 3

    # 检查相邻段落
    nodes = self.walk_siblings(node)
    for current_node in nodes:
        if self.parser.getTag(current_node) == para:
            if steps_away >= max_stepsaway_from_node:
                return False
            paragraph_text = self.parser.getText(current_node)
            word_stats = self.stopwords_class.get_stopword_count(paragraph_text)
            if word_stats.get_stopword_count() > minimum_stopword_count:
                return True
            steps_away += 1
    return False

输出格式化

OutputFormatter负责将选定的DOM节点转换为纯净文本:

def get_formatted(self, top_node):
    """返回格式化后的文本和HTML"""
    self.top_node = top_node
    
    # 多重清理和转换
    self.remove_negativescores_nodes()
    self.links_to_text()
    self.add_newline_to_br()
    self.add_newline_to_li()
    self.replace_with_text()
    self.remove_empty_tags()
    
    text = self.convert_to_text()
    html = self.convert_to_html() if self.config.keep_article_html else ''
    
    return (text, html)

算法优势与特点

  1. 多语言支持:通过停用词统计适配不同语言的内容特征
  2. 抗噪能力强:有效处理网页中的广告、导航、侧边栏等噪声内容
  3. 结构感知:理解HTML文档结构,通过父节点聚合提高准确性
  4. 可配置性:提供丰富的配置选项适应不同网站结构

性能优化策略

  • 懒加载设计:仅在需要时进行完整的解析和提取
  • 内存管理:及时释放DOM对象避免内存泄漏
  • 缓存机制:对重复操作进行缓存优化性能

该内容提取算法经过大量实际网页测试,在准确性和效率之间取得了良好平衡,能够处理大多数新闻网站的文章提取需求。

图像识别与评分机制实现

Newspaper3k的图像识别与评分系统是其内容提取架构中的关键组成部分,专门用于从网页中智能识别和选择最具代表性的主图。该系统基于多维度评分算法,综合考虑图像尺寸、宽高比、内容熵值等多个因素,确保选择出最符合文章主题的高质量图片。

图像提取架构设计

Newspaper3k的图像处理模块采用分层架构设计,主要包含以下几个核心组件:

mermaid

核心评分算法实现

图像评分机制的核心在于calculate_area方法,该方法对每个候选图像进行多维度评估:

def calculate_area(self, img_url, dimension):
    if not dimension:
        return 0
    area = dimension[0] * dimension[1]
    # 忽略过小的图像
    if area < minimal_area:
        log.debug('ignore little %s' % img_url)
        return 0
    # PIL不会放大图像,设置最小宽度并保持宽高比
    if dimension[0] < thumbnail_size[0]:
        return 0
    # 忽略过长/过宽的图像
    current_ratio = max(dimension) / min(dimension)
    if current_ratio > self.config.image_dimension_ration:
        log.debug('ignore dims %s' % img_url)
        return 0
    # 对包含"sprite"或"logo"的文件名进行惩罚
    lower_case_url = img_url.lower()
    if 'sprite' in lower_case_url or 'logo' in lower_case_url:
        log.debug('penalizing sprite %s' % img_url)
        area /= 10
    return area

图像评分维度分析

Newspaper3k的图像评分系统基于以下关键维度进行综合评估:

评分维度权重说明处理逻辑
图像面积像素总面积必须大于最小面积阈值(5000像素)
宽度限制最小宽度要求必须大于缩略图宽度(90像素)
宽高比图像比例合理性不能超过配置的最大宽高比(16:9)
文件名图像内容提示包含"sprite"或"logo"的文件名会被惩罚

图像熵值计算与裁剪优化

系统采用信息熵算法来优化图像裁剪过程,确保保留最重要的视觉内容:

def image_entropy(img):
    """计算图像的熵值"""
    hist = img.histogram()
    hist_size = sum(hist)
    hist = [float(h) / hist_size for h in hist]
    return -sum([p * math.log(p, 2) for p in hist if p != 0])

def square_image(img):
    """将图像裁剪为正方形,基于熵值选择保留区域"""
    x, y = img.size
    while y > x:
        # 每次切片10像素直到变为正方形
        slice_height = min(y - x, 10)
        bottom = img.crop((0, y - slice_height, x, y))
        top = img.crop((0, 0, x, slice_height))
        # 移除熵值较小的切片
        if image_entropy(bottom) < image_entropy(top):
            img = img.crop((0, 0, x, y - slice_height))
        else:
            img = img.crop((0, slice_height, x, y))
        x, y = img.size
    return img

图像获取与预处理流程

图像获取过程采用智能重试机制和内容类型验证:

def fetch_url(url, useragent, referer=None, retries=1, dimension=False):
    cur_try = 0
    nothing = None if dimension else (None, None)
    url = clean_url(url)
    
    while True:
        try:
            response = requests.get(url, stream=True, timeout=5, headers={
                'User-Agent': useragent,
                'Referer': referer,
            })
            
            # 智能内容类型验证
            content_type = response.headers.get('Content-Type')
            if not content_type or 'image' not in content_type:
                return nothing
            
            # 使用PIL解析器逐步读取图像数据
            p = ImageFile.Parser()
            content = response.raw.read(chunk_size)
            while not p.image and content:
                p.feed(content)
                content = response.raw.read(chunk_size)
            
            return p.image.size if dimension else (content_type, content)
            
        except requests.exceptions.RequestException:
            cur_try += 1
            if cur_try >= retries:
                return nothing

性能优化策略

Newspaper3k在图像处理方面采用了多项性能优化措施:

  1. 渐进式加载:使用流式读取,避免一次性加载大图像
  2. 维度优先获取:首先只获取图像尺寸信息,减少网络传输
  3. 智能重试机制:对网络请求失败进行自动重试
  4. 内存优化:及时关闭网络连接,避免资源泄漏

配置参数详解

系统提供了灵活的配置选项来控制图像处理行为:

# 最小图像面积阈值
minimal_area = 5000

# 缩略图目标尺寸
thumbnail_size = 90, 90

# 默认宽高比限制
image_dimension_ration = 16 / 9.0

# 是否获取图像配置
fetch_images = True

这种基于多维度评分的图像选择机制,确保了Newspaper3k能够从网页中智能识别出最具代表性和高质量的主图像,为内容提取提供了重要的视觉支持。

多语言处理与分词技术

Newspaper3k作为一个强大的新闻文章提取库,其多语言处理能力是其核心优势之一。该库支持超过40种语言的文章提取和处理,从英语、中文到阿拉伯语、日语等,每种语言都有专门的分词和停用词处理机制。这种多语言支持使得Newspaper3k能够处理全球范围内的新闻内容,为国际化应用提供了强有力的技术基础。

语言检测与配置机制

Newspaper3k采用智能的语言检测机制,当用户未明确指定语言时,系统会自动检测文章的语言类型。语言配置通过configuration.py模块进行管理:

class Configuration(object):
    def __init__(self):
        self.language = 'en'  # 默认英语
    
    def get_language(self):
        return self.language
    
    def set_language(self, language):
        self.language = language

语言检测流程如下:

mermaid

停用词管理系统

Newspaper3k为每种支持的语言都提供了专门的停用词列表,这些停用词文件存储在text/stopwords-{language}.txt中。停用词管理通过StopWords基类和语言特定的子类实现:

class StopWords(object):
    _cached_stop_words = {}
    
    def __init__(self, language='en'):
        if language not in self._cached_stop_words:
            path = os.path.join('text', 'stopwords-%s.txt' % language)
            self._cached_stop_words[language] = \
                set(FileHelper.loadResourceFile(path).splitlines())
        self.STOP_WORDS = self._cached_stop_words[language]

多语言分词技术实现

不同语言的分词技术存在显著差异,Newspaper3k为每种语言实现了专门的分词策略:

中文分词(使用jieba)

中文分词采用jieba库,支持全模式和精确模式:

class StopWordsChinese(StopWords):
    def candidate_words(self, stripped_input):
        import jieba
        return jieba.cut(stripped_input, cut_all=True)
阿拉伯语分词(使用NLTK)

阿拉伯语处理涉及特殊的词干提取和分词:

class StopWordsArabic(StopWords):
    def remove_punctuation(self, content):
        return content  # 阿拉伯语保留标点
    
    def candidate_words(self, stripped_input):
        import nltk
        s = nltk.stem.isri.ISRIStemmer()
        words = []
        for word in nltk.tokenize.wordpunct_tokenize(stripped_input):
            words.append(s.stem(word))  # 应用ISRI词干提取
        return words
日语分词(使用TinySegmenter)

日语分词采用轻量级的TinySegmenter:

class StopWordsJapanese(StopWords):
    def candidate_words(self, stripped_input):
        import tinysegmenter
        segmenter = tinysegmenter.TinySegmenter()
        tokens = segmenter.tokenize(stripped_input)
        return tokens
泰语分词(使用PyThaiNLP)

泰语分词使用专门的PyThaiNLP库:

class StopWordsThai(StopWords):
    def candidate_words(self, stripped_input):
        import pythainlp
        tokens = pythainlp.word_tokenize(stripped_input)
        return tokens

语言特定处理策略对比

下表展示了不同语言的处理策略差异:

语言分词库标点处理词干提取特殊处理
英语内置移除标点标准空格分词
中文jieba保留中文标点基于词典的分词
阿拉伯语NLTK保留标点ISRI词干提取从右到左处理
日语TinySegmenter保留标点基于规则的分词
泰语PyThaiNLP保留标点基于机器学习的分词

自然语言处理管道

多语言NLP处理遵循统一的管道架构:

mermaid

性能优化策略

Newspaper3k在多语言处理中采用了多种性能优化策略:

  1. 缓存机制:停用词列表在内存中缓存,避免重复文件读取
  2. 延迟加载:分词库只在需要时导入,减少内存占用
  3. 语言检测优化:优先从HTML meta标签获取语言信息,减少计算开销
  4. 并行处理:支持多线程处理不同语言的文章

扩展性与自定义

开发者可以轻松扩展新的语言支持:

# 自定义语言处理器示例
class StopWordsCustom(StopWords):
    def __init__(self, language='custom'):
        super(StopWordsCustom, self).__init__(language='custom')
    
    def candidate_words(self, stripped_input):
        # 实现自定义分词逻辑
        return custom_tokenizer(stripped_input)

通过这种模块化的设计,Newspaper3k为多语言新闻处理提供了强大而灵活的基础设施,使得开发者能够轻松处理全球化的新闻内容提取需求。

总结

Newspaper3k作为一个强大的新闻文章提取库,其架构设计和核心算法体现了高度的专业性和实用性。从分层架构设计到模块化划分,从HTML解析与内容提取算法到图像识别与评分机制,再到多语言处理与分词技术,每个组件都经过精心设计和优化。该库支持超过40种语言的文章提取和处理,采用智能的语言检测机制和专门的分词策略,为国际化应用提供了强有力的技术基础。通过多维度评分算法、性能优化策略和灵活的配置选项,Newspaper3k能够在准确性和效率之间取得良好平衡,处理大多数新闻网站的文章提取需求。其清晰的模块职责边界和优秀的软件架构设计思想,使得库具有良好的可维护性、扩展性和实用性,为开发者提供了处理全球化新闻内容提取的强大工具。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值