1.1 文档解析:PDF/Word/HTML的结构化提取

一、文档解析的核心概念与重要性

1.1 什么是文档解析?

文档解析是将非结构化或半结构化的文档内容转换为结构化数据的过程。在RAG系统中,这是知识库构建的第一步,直接影响后续分块、索引和检索的质量。

1.2 为什么需要专门的文档解析?

  • 格式多样性:不同格式(PDF/Word/HTML)有不同的内部结构和编码方式
  • 内容复杂性:包含文本、表格、图片、超链接、元数据等多种元素
  • 应用需求:需要提取纯文本用于向量化,同时保留必要的结构信息

二、主要文档格式解析详解

2.1 PDF文档解析

PDF格式特点
  • 基于页面的格式:固定布局,保持跨平台一致性
  • 内容类型复杂:文本、图像、表单、注释等混合
  • 两种主要类型
    • 文本型PDF:可直接提取文本
    • 图像型PDF(扫描件):需要OCR处理
PDF解析工具对比

|--------------------|----------------------|------------|----------------|
| 工具库 | 优点 | 缺点 | 适用场景 |
| PyMuPDF (fitz) | 速度快,功能全面,支持文本、图像、元数据 | API稍复杂 | 高性能提取,需要精确位置信息 |
| pdfplumber | 表格提取能力强,布局分析好 | 速度中等 | 表格密集型文档 |
| pypdf / PyPDF2 | 简单易用,纯Python实现 | 功能有限,布局分析弱 | 简单文本提取 |
| pdfminer.six | 精确布局分析,支持PDF-1.7 | 速度慢,配置复杂 | 需要精确布局分析的场景 |
| LangChain工具 | 集成化,支持多种解析策略 | 灵活性较低 | 快速原型开发 |

PDF解析实战代码

使用pdfplumber和PyMuPDF库处理PDF文件的模块,提供了两种提取PDF内容的方法:标准文本提取和基于布局的文本提取。

主要函数是process_pdf_file,它根据参数选择不同的提取方法,并返回一个包含提取内容和元数据的字典。

代码结构如下:

  1. 导入必要的库:pdfplumber, logging, Path等。
  2. 定义process_pdf_file函数,该函数是主函数,用于处理PDF文件。
  3. 定义了两个内部函数:_extract_standard和_extract_with_layout,分别对应两种提取方法。
  4. 定义了辅助函数:_process_tables用于处理表格,_table_to_text将表格数据转换为文本,_try_ocr_extraction尝试使用OCR提取文本。

下面详细解释每个部分:

  1. process_pdf_file函数:
    参数:
    pdf_path: PDF文件路径
    extract_tables: 是否提取表格(仅标准提取方法有效)
    extract_layout: 是否使用基于布局的提取方法(如果为True,则使用PyMuPDF提取,否则使用标准方法)
    min_text_length: 最小文本长度,过滤过短的文本块
    combine_pages: 是否将所有页面内容合并为一个文档
    ocr_enabled: 是否启用OCR(仅标准方法中,当文本提取为空时尝试OCR)

    逻辑:
    1. 检查文件是否存在。
    2. 根据extract_layout参数选择提取方法。
    3. 调用相应的提取函数并返回结果。

  2. _extract_standard函数(标准提取方法):
    使用pdfplumber打开PDF,逐页提取文本、表格和图片信息。
    步骤:

    1. 获取文档元数据。
    2. 遍历每一页:
      a. 提取文本,如果文本过短且启用OCR,则尝试OCR提取。
      b. 如果extract_tables为True,提取表格并处理。
      c. 提取图片信息。
      d. 构建页面文档字典,包含文本、表格、图片等。
    3. 如果combine_pages为True,则将所有页面文本合并为一个文档。
    4. 构建返回结果字典,包含成功标志、文件信息、内容和统计信息。
  3. _extract_with_layout函数(基于布局的提取方法):
    使用PyMuPDF(fitz)打开PDF,按文本块提取文本,并保留位置和字体信息。
    步骤:

    1. 遍历每一页,获取文本块(block)。
    2. 对每个文本块,提取文本和字体信息。
    3. 构建块文档字典,包含文本和布局信息(如边界框、字体等)。
    4. 如果combine_pages为True,合并所有文本。
    5. 构建返回结果字典。
  4. 辅助函数:

    1. _process_tables:将pdfplumber提取的表格数据处理成字典,包括表格编号、所在页、行数列数、数据以及文本表示。
    2. _table_to_text:将表格数据转换为格式化的文本字符串,便于阅读。
    3. _try_ocr_extraction:尝试使用pytesseract进行OCR识别,需要将PDF页面转换为图像。
python 复制代码
import pdfplumber
from typing import Dict, List, Any, Optional
import logging
from pathlib import Path

logger = logging.getLogger(__name__)

def process_pdf_file(
    pdf_path: str,
    extract_tables: bool = True,
    extract_layout: bool = False,
    min_text_length: int = 10,
    combine_pages: bool = False,
    ocr_enabled: bool = False
) -> Dict[str, Any]:
    """
    处理PDF文件并提取内容
    
    Args:
        pdf_path: PDF文件路径
        extract_tables: 是否提取表格内容
        extract_layout: 是否基于布局提取(保留位置信息)
        min_text_length: 最小文本长度阈值,过滤过短的文本块
        combine_pages: 是否将所有页面内容合并
        ocr_enabled: 是否启用OCR(需要安装相关依赖)
        
    Returns:
        包含提取内容和元数据的字典
    """
    
    # 设置日志
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(__name__)
    
    # 检查文件是否存在
    pdf_path = Path(pdf_path)
    if not pdf_path.exists():
        raise FileNotFoundError(f"PDF文件不存在: {pdf_path}")
    
    try:
        # 方法1: 基于布局提取(保留位置信息)
        if extract_layout:
            return _extract_with_layout(pdf_path, min_text_length, combine_pages)
        
        # 方法2: 标准文本提取
        else:
            return _extract_standard(pdf_path, extract_tables, min_text_length, combine_pages, ocr_enabled)
            
    except Exception as e:
        logger.error(f"处理PDF失败: {pdf_path}, 错误: {str(e)}")
        raise

def _extract_standard(
    pdf_path: Path,
    extract_tables: bool,
    min_text_length: int,
    combine_pages: bool,
    ocr_enabled: bool
) -> Dict[str, Any]:
    """标准文本提取方法"""
    
    documents = []
    tables_data = []
    images_info = []
    
    try:
        with pdfplumber.open(pdf_path) as pdf:
            # 文档级元数据
            doc_metadata = {
                'source': str(pdf_path),
                'total_pages': len(pdf.pages),
                'author': pdf.metadata.get('Author', ''),
                'title': pdf.metadata.get('Title', ''),
                'creator': pdf.metadata.get('Creator', ''),
                'producer': pdf.metadata.get('Producer', ''),
                'creation_date': pdf.metadata.get('CreationDate', ''),
                'modification_date': pdf.metadata.get('ModDate', ''),
                'file_size': pdf_path.stat().st_size
            }
            
            all_text = ""
            
            for page_num, page in enumerate(pdf.pages, 1):
                # 提取页面文本
                text = page.extract_text()
                
                # 如果文本为空且启用了OCR,尝试OCR处理
                if (not text or len(text.strip()) < min_text_length) and ocr_enabled:
                    text = _try_ocr_extraction(page)
                
                # 跳过过短的文本
                if not text or len(text.strip()) < min_text_length:
                    logger.warning(f"页面 {page_num} 文本过短或为空,已跳过")
                    continue
                
                # 提取表格
                page_tables = []
                if extract_tables:
                    tables = page.extract_tables()
                    if tables:
                        page_tables = _process_tables(tables, page_num)
                        tables_data.extend(page_tables)
                
                # 提取图片信息
                page_images = page.images
                if page_images:
                    images_info.extend([
                        {
                            'page': page_num,
                            'bbox': img.get('bbox', []),
                            'width': img.get('width', 0),
                            'height': img.get('height', 0),
                            'name': img.get('name', '')
                        }
                        for img in page_images
                    ])
                
                # 构建页面文档
                page_doc = {
                    'page_number': page_num,
                    'text': text.strip(),
                    'tables': page_tables,
                    'image_count': len(page_images),
                    'dimensions': {
                        'width': page.width,
                        'height': page.height
                    },
                    'has_text': bool(text and text.strip())
                }
                
                documents.append(page_doc)
                all_text += f"\n\n--- 第 {page_num} 页 ---\n\n{text.strip()}"
            
            # 处理合并选项
            if combine_pages:
                combined_doc = {
                    'page_number': 'all',
                    'text': all_text.strip(),
                    'total_tables': len(tables_data),
                    'total_images': len(images_info),
                    'is_combined': True
                }
                documents = [combined_doc]
            
            # 构建返回结果
            result = {
                'success': True,
                'file_info': doc_metadata,
                'content': {
                    'pages': documents if not combine_pages else documents,
                    'tables': tables_data,
                    'images': images_info,
                    'statistics': {
                        'total_pages': doc_metadata['total_pages'],
                        'pages_with_text': len([d for d in documents if d.get('has_text', False)]),
                        'total_tables': len(tables_data),
                        'total_images': len(images_info),
                        'total_characters': sum(len(d.get('text', '')) for d in documents)
                    }
                }
            }
            
            logger.info(f"PDF处理完成: {pdf_path.name}")
            logger.info(f"提取统计: {result['content']['statistics']}")
            
            return result
            
    except Exception as e:
        logger.error(f"提取过程中出错: {str(e)}")
        return {
            'success': False,
            'error': str(e),
            'file_info': {'source': str(pdf_path)},
            'content': None
        }

def _extract_with_layout(
    pdf_path: Path,
    min_text_length: int,
    combine_pages: bool
) -> Dict[str, Any]:
    """基于布局的提取方法(使用PyMuPDF)"""
    
    try:
        import fitz  # PyMuPDF
    except ImportError:
        raise ImportError("请安装PyMuPDF库: pip install PyMuPDF")
    
    documents = []
    all_text = ""
    
    pdf_document = fitz.open(pdf_path)
    total_pages = len(pdf_document)  # 在关闭之前获取总页数
    
    for page_num in range(total_pages):
        page = pdf_document[page_num]
        
        # 获取页面文本块及其位置
        blocks = page.get_text("dict")["blocks"]
        
        for block_num, block in enumerate(blocks):
            if block["type"] == 0:  # 文本块
                # 合并块内所有行的文本
                block_text = ""
                for line in block.get("lines", []):
                    for span in line.get("spans", []):
                        block_text += span.get("text", "") + " "
                
                block_text = block_text.strip()
                
                # 跳过过短的文本块
                if len(block_text) < min_text_length:
                    continue
                
                # 提取字体信息
                font_info = {}
                if block.get("lines") and block["lines"][0].get("spans"):
                    first_span = block["lines"][0]["spans"][0]
                    font_info = {
                        'font_size': first_span.get("size"),
                        'font_name': first_span.get("font"),
                        'color': first_span.get("color")
                    }
                
                # 构建块文档
                block_doc = {
                    'page_number': page_num + 1,
                    'block_number': block_num,
                    'text': block_text,
                    'layout': {
                        'bbox': block.get("bbox", []),
                        'type': 'text',
                        'font_info': font_info
                    }
                }
                
                documents.append(block_doc)
                all_text += f"\n\n{block_text}"
    
    pdf_document.close()
    
    # 处理合并选项
    if combine_pages:
        combined_doc = {
            'page_number': 'all',
            'text': all_text.strip(),
            'is_combined': True
        }
        documents = [combined_doc]
    
    # 构建返回结果
    result = {
        'success': True,
        'file_info': {
            'source': str(pdf_path),
            'total_pages': total_pages,  # 使用之前保存的变量
            'extraction_method': 'layout_based'
        },
        'content': {
            'blocks': documents if not combine_pages else documents,
            'statistics': {
                'total_blocks': len(documents) if not combine_pages else 1,
                'total_pages': total_pages,  # 使用之前保存的变量
                'total_characters': len(all_text)
            }
        }
    }
    
    return result

def _process_tables(tables: List, page_num: int) -> List[Dict]:
    """处理表格数据"""
    processed_tables = []
    
    for i, table in enumerate(tables):
        if not table:
            continue
            
        table_data = []
        for row in table:
            # 过滤None值,转换为字符串
            processed_row = [str(cell) if cell is not None else "" for cell in row]
            table_data.append(processed_row)
        
        table_info = {
            'table_number': i + 1,
            'page': page_num,
            'row_count': len(table_data),
            'col_count': len(table_data[0]) if table_data else 0,
            'data': table_data,
            'text_representation': _table_to_text(table_data)
        }
        
        processed_tables.append(table_info)
    
    return processed_tables

def _table_to_text(table_data: List[List[str]]) -> str:
    """将表格数据转换为文本格式"""
    if not table_data:
        return ""
    
    # 计算每列的最大宽度
    col_widths = [max(len(str(row[i])) if i < len(row) else 0 
                     for row in table_data) for i in range(len(table_data[0]))]
    
    text_lines = []
    for row in table_data:
        line_parts = []
        for i, cell in enumerate(row):
            if i < len(col_widths):
                line_parts.append(str(cell).ljust(col_widths[i]))
        text_lines.append(" | ".join(line_parts))
    
    return "\n".join(text_lines)

def _try_ocr_extraction(page) -> str:
    """尝试使用OCR提取文本(需要安装相关库)"""
    try:
        from PIL import Image
        import pytesseract
        
        # 将PDF页面转换为图像
        im = page.to_image(resolution=300)
        # 提取文本
        text = pytesseract.image_to_string(im.original)
        return text
    except ImportError:
        logging.warning("OCR功能需要安装pytesseract和Pillow库")
        return ""
    except Exception as e:
        logging.warning(f"OCR提取失败: {str(e)}")
        return ""

# 使用示例
if __name__ == "__main__":

    def main1():
        """基本提取示例"""
        result = process_pdf_file(
            pdf_path="/Users/caotianchen.1/Desktop/pyproject/test2/2506.10380v2.pdf",
            extract_tables=True,
            combine_pages=False
        )
        
        if result['success']:
            # 访问提取的内容
            print(f"文件标题: {result['file_info'].get('title', '无标题')}")
            print(f"总页数: {result['content']['statistics']['total_pages']}")
            
            # 打印每页文本
            for page in result['content']['pages']:
                print(f"\n--- 第 {page['page_number']} 页 ---")
                print(page['text'][:500])  # 只打印前500字符
            
            # 打印表格信息
            if result['content']['tables']:
                print(f"\n找到 {len(result['content']['tables'])} 个表格")


    def main2():        
        """布局提取示例"""
        result = process_pdf_file(
            pdf_path="/Users/caotianchen.1/Desktop/pyproject/test2/2506.10380v2.pdf",
            extract_layout=True,
            combine_pages=True
        )
        if result['success']:
            print(f"文件标题: {result['file_info'].get('title', '无标题')}")
            print(f"总页数: {result['file_info']['total_pages']}")
            print(f"总字符数: {result['content']['statistics']['total_characters']}")
            print(f"\n提取的文本内容:\n{result['content']['blocks'][0]['text'][:1000]}")  # 只打印前1000字符
    
    main1()

2.2 Word文档解析

Word格式特点
  • 基于XML的结构(.docx)
  • 包含样式、格式、超链接等丰富信息
  • 支持表格、图片、注释等元素
Word解析工具

支持两种解析方法:python-docx和langchain的Docx2txtLoader。包括提取文本、表格、页眉页脚、图片信息等。

逻辑解释如下:

  1. 主函数process_word_file是入口,根据参数选择解析方法,并调用相应的解析函数。
  2. 两种解析方法:
    a._extract_with_python_docx:使用python-docx库,可以提取段落、表格、页眉页脚、图片信息等,并且可以按段落分割返回。
    b._extract_with_langchain:使用langchain的Docx2txtLoader,简单提取文本,可以按段落分割(按空行分割)。
  3. 辅助函数:
    • _extract_table_with_structure:提取表格,返回表格的文本表示和结构化数据。
    • _extract_headers_footers:提取页眉页脚。
    • _extract_images_info:通过解压docx文件(zip格式)并解析xml来获取图片信息。
    • _count_words:统计单词数。
  4. 返回结果是一个字典,包含成功与否、文件信息、提取的内容和统计信息。

使用示例中展示了多种用法,包括使用python-docx提取详细信息、使用langchain快速提取、批量处理等。

注意:代码中处理了可能出现的异常,并记录了日志。

下面我们详细看一下每个部分。

1.主函数process_word_file

  • 检查文件是否存在和文件类型。
  • 根据extract_method参数选择解析方法。

2._extract_with_python_docx方法

  • 使用python-docx打开文档,获取文档属性(元数据)。
  • 遍历段落,提取文本,并记录段落样式、字体等信息。
  • 提取表格,并调用_extract_table_with_structure处理表格。
  • 提取页眉页脚(通过访问sections的header和footer)。
  • 提取图片信息(通过解压docx并解析xml)。
  • 构建返回结果,包括完整的文本、段落列表、表格列表、页眉页脚列表、图片信息列表和统计信息。

3._extract_with_langchain方法

  • 使用Docx2txtLoader加载文档,获取文本。
  • 如果设置按段落分割,则按空行分割文本。
  • 构建返回结果,包括完整文本和段落列表(如果分割了的话)。

4.辅助函数

  • _extract_table_with_structure:遍历表格的每一行和每一个单元格,将单元格内的段落文本合并,然后构建每行的文本表示(用"|"连接单元格文本),最后将整个表格用换行符连接。
  • _extract_headers_footers:遍历每个section,提取header和footer中的段落文本。
  • _extract_images_info:将docx文件作为zip打开,读取document.xml和对应的rels文件,解析出图片的嵌入关系和图片名称等信息。
  • _count_words:用正则去掉标点符号,然后按空白分割,计算单词数。

5.使用示例

  • 展示了三种使用场景:详细提取、快速提取和批量处理。
python 复制代码
from docx import Document
from langchain.document_loaders import Docx2txtLoader
import zipfile
import xml.etree.ElementTree as ET
import logging
from typing import List, Dict, Any, Optional
from pathlib import Path
import re

class WordParser:
    def __init__(self):
        self.logger = logging.getLogger(__name__)
    
    def process_word_file(
        self,
        word_path: str,
        extract_method: str = "python-docx",
        include_headers_footers: bool = False,
        include_tables: bool = True,
        include_images_info: bool = False,
        split_by_paragraph: bool = False,
        min_text_length: int = 10
    ) -> Dict[str, Any]:
        """
        处理Word文档并提取内容
        
        Args:
            word_path: Word文件路径
            extract_method: 提取方法,可选 "python-docx" 或 "langchain"
            include_headers_footers: 是否提取页眉页脚
            include_tables: 是否提取表格
            include_images_info: 是否提取图片信息
            split_by_paragraph: 是否按段落分割返回
            min_text_length: 最小文本长度阈值
            
        Returns:
            包含提取内容和元数据的字典
        """
        
        # 检查文件是否存在
        word_path = Path(word_path)
        if not word_path.exists():
            raise FileNotFoundError(f"Word文件不存在: {word_path}")
        
        # 验证文件扩展名
        if word_path.suffix.lower() not in ['.docx', '.doc']:
            raise ValueError(f"不支持的文件类型: {word_path.suffix},仅支持.docx和.doc格式")
        
        try:
            # 选择提取方法
            if extract_method == "langchain":
                return self._extract_with_langchain(word_path, split_by_paragraph, min_text_length)
            else:  # 默认使用python-docx
                return self._extract_with_python_docx(
                    word_path, 
                    include_headers_footers,
                    include_tables,
                    include_images_info,
                    split_by_paragraph,
                    min_text_length
                )
                
        except Exception as e:
            self.logger.error(f"处理Word文件失败: {word_path}, 错误: {str(e)}")
            raise
    
    def _extract_with_python_docx(
        self,
        word_path: Path,
        include_headers_footers: bool,
        include_tables: bool,
        include_images_info: bool,
        split_by_paragraph: bool,
        min_text_length: int
    ) -> Dict[str, Any]:
        """
        使用python-docx库解析Word文档
        """
        
        try:
            doc = Document(word_path)
            
            # 提取文档属性
            core_props = doc.core_properties
            doc_metadata = {
                'source': str(word_path),
                'author': core_props.author or '',
                'title': core_props.title or '',
                'subject': core_props.subject or '',
                'keywords': core_props.keywords or '',
                'created': str(core_props.created) if core_props.created else '',
                'modified': str(core_props.modified) if core_props.modified else '',
                'last_modified_by': core_props.last_modified_by or '',
                'revision': str(core_props.revision) if core_props.revision else '',
                'file_size': word_path.stat().st_size,
                'extraction_method': 'python-docx'
            }
            
            # 初始化数据结构
            all_text = ""
            paragraphs_data = []
            tables_data = []
            headers_data = []
            footers_data = []
            images_info = []
            
            # 提取正文段落
            para_count = 0
            for para_idx, paragraph in enumerate(doc.paragraphs):
                text = paragraph.text.strip()
                if text and len(text) >= min_text_length:
                    para_info = {
                        'text': text,
                        'index': para_idx,
                        'style': paragraph.style.name,
                        'runs_count': len(paragraph.runs),
                        'is_heading': paragraph.style.name.startswith('Heading')
                    }
                    
                    # 检查是否有超链接
                    if paragraph._element.xpath('.//w:hyperlink'):
                        para_info['has_hyperlink'] = True
                    
                    # 检查字体格式
                    if paragraph.runs:
                        first_run = paragraph.runs[0]
                        para_info['font_info'] = {
                            'font_name': first_run.font.name,
                            'font_size': first_run.font.size,
                            'bold': first_run.font.bold,
                            'italic': first_run.font.italic,
                            'underline': first_run.font.underline
                        }
                    
                    paragraphs_data.append(para_info)
                    all_text += text + "\n\n"
                    para_count += 1
            
            # 提取表格
            table_count = 0
            if include_tables:
                for table_idx, table in enumerate(doc.tables):
                    table_text, table_data = self._extract_table_with_structure(table)
                    if table_text.strip():
                        table_info = {
                            'table_index': table_idx,
                            'row_count': len(table.rows),
                            'col_count': len(table.columns),
                            'text_representation': table_text,
                            'data': table_data
                        }
                        tables_data.append(table_info)
                        all_text += f"\n\n--- 表格 {table_idx + 1} ---\n\n{table_text}"
                        table_count += 1
            
            # 提取页眉页脚
            if include_headers_footers:
                try:
                    headers, footers = self._extract_headers_footers(doc)
                    headers_data.extend(headers)
                    footers_data.extend(footers)
                    
                    # 添加到总文本
                    for header in headers:
                        all_text += f"\n[页眉] {header['text']}\n"
                    for footer in footers:
                        all_text += f"\n[页脚] {footer['text']}\n"
                        
                except Exception as e:
                    self.logger.warning(f"提取页眉页脚失败: {str(e)}")
            
            # 提取图片信息
            if include_images_info:
                try:
                    images_info = self._extract_images_info(word_path)
                except Exception as e:
                    self.logger.warning(f"提取图片信息失败: {str(e)}")
            
            # 构建返回结果
            result = {
                'success': True,
                'file_info': doc_metadata,
                'content': {
                    'full_text': all_text.strip(),
                    'statistics': {
                        'paragraphs': para_count,
                        'tables': table_count,
                        'headers': len(headers_data),
                        'footers': len(footers_data),
                        'images': len(images_info),
                        'total_characters': len(all_text.strip()),
                        'word_count': self._count_words(all_text)
                    }
                }
            }
            
            # 根据需要返回详细数据
            if split_by_paragraph:
                result['content']['paragraphs'] = paragraphs_data
            
            if tables_data:
                result['content']['tables'] = tables_data
            
            if headers_data:
                result['content']['headers'] = headers_data
            
            if footers_data:
                result['content']['footers'] = footers_data
            
            if images_info:
                result['content']['images'] = images_info
            
            self.logger.info(f"Word文档处理完成: {word_path.name}")
            self.logger.info(f"提取统计: {result['content']['statistics']}")
            
            return result
            
        except Exception as e:
            self.logger.error(f"python-docx提取失败: {str(e)}")
            return {
                'success': False,
                'error': str(e),
                'file_info': {'source': str(word_path)},
                'content': None
            }
    
    def _extract_with_langchain(
        self,
        word_path: Path,
        split_by_paragraph: bool,
        min_text_length: int
    ) -> Dict[str, Any]:
        """
        使用LangChain的Docx2txtLoader
        """
        
        try:
            loader = Docx2txtLoader(str(word_path))
            langchain_docs = loader.load()
            
            if not langchain_docs:
                return {
                    'success': False,
                    'error': '未提取到任何内容',
                    'file_info': {'source': str(word_path)},
                    'content': None
                }
            
            # 获取第一个文档(Docx2txtLoader通常返回单个文档)
            langchain_doc = langchain_docs[0]
            
            # 构建元数据
            doc_metadata = {
                'source': str(word_path),
                'title': langchain_doc.metadata.get('source', '').split('/')[-1],
                'extraction_method': 'langchain',
                'file_size': word_path.stat().st_size
            }
            
            # 处理内容
            full_text = langchain_doc.page_content
            
            # 如果按段落分割
            paragraphs_data = []
            if split_by_paragraph:
                # 简单的段落分割(按空行)
                raw_paragraphs = [p.strip() for p in full_text.split('\n\n') if p.strip()]
                
                for idx, para in enumerate(raw_paragraphs):
                    if len(para) >= min_text_length:
                        paragraphs_data.append({
                            'text': para,
                            'index': idx,
                            'characters': len(para),
                            'words': len(para.split())
                        })
            
            # 构建返回结果
            result = {
                'success': True,
                'file_info': doc_metadata,
                'content': {
                    'full_text': full_text.strip(),
                    'statistics': {
                        'paragraphs': len(paragraphs_data) if split_by_paragraph else 1,
                        'total_characters': len(full_text.strip()),
                        'word_count': self._count_words(full_text)
                    }
                }
            }
            
            if split_by_paragraph and paragraphs_data:
                result['content']['paragraphs'] = paragraphs_data
            
            self.logger.info(f"LangChain Word文档处理完成: {word_path.name}")
            
            return result
            
        except Exception as e:
            self.logger.error(f"LangChain提取失败: {str(e)}")
            return {
                'success': False,
                'error': str(e),
                'file_info': {'source': str(word_path)},
                'content': None
            }
    
    def _extract_table_with_structure(self, table) -> tuple:
        """提取表格为文本并保留结构"""
        table_data = []
        table_text = []
        
        for row_idx, row in enumerate(table.rows):
            row_data = []
            row_text_parts = []
            
            for cell_idx, cell in enumerate(row.cells):
                # 提取单元格文本(合并所有段落)
                cell_text = ' '.join([para.text.strip() for para in cell.paragraphs if para.text.strip()])
                row_data.append(cell_text)
                row_text_parts.append(cell_text)
            
            # 构建行文本表示
            row_text = " | ".join(row_text_parts)
            table_data.append(row_data)
            table_text.append(row_text)
        
        return "\n".join(table_text), table_data
    
    def _extract_headers_footers(self, doc) -> tuple:
        """提取页眉和页脚"""
        headers = []
        footers = []
        
        # 尝试提取页眉
        try:
            # python-docx中访问页眉
            for section in doc.sections:
                if section.header:
                    header_text = ' '.join([para.text for para in section.header.paragraphs])
                    if header_text.strip():
                        headers.append({
                            'section': 'default',
                            'text': header_text.strip()
                        })
        except Exception as e:
            self.logger.debug(f"提取页眉失败: {str(e)}")
        
        # 尝试提取页脚
        try:
            for section in doc.sections:
                if section.footer:
                    footer_text = ' '.join([para.text for para in section.footer.paragraphs])
                    if footer_text.strip():
                        footers.append({
                            'section': 'default',
                            'text': footer_text.strip()
                        })
        except Exception as e:
            self.logger.debug(f"提取页脚失败: {str(e)}")
        
        return headers, footers
    
    def _extract_images_info(self, word_path: Path) -> List[Dict]:
        """从Word文档中提取图片信息"""
        images_info = []
        
        try:
            # 打开docx文件作为zip包
            with zipfile.ZipFile(word_path, 'r') as docx_zip:
                # 读取document.xml文件
                xml_content = docx_zip.read('word/document.xml')
                root = ET.fromstring(xml_content)
                
                # 定义命名空间
                namespaces = {
                    'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
                    'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
                    'pic': 'http://schemas.openxmlformats.org/drawingml/2006/picture',
                    'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
                }
                
                # 查找所有图片
                drawings = root.findall('.//w:drawing', namespaces)
                
                for i, drawing in enumerate(drawings):
                    # 尝试提取图片信息
                    blip = drawing.find('.//a:blip', namespaces)
                    if blip is not None:
                        embed_id = blip.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
                        
                        if embed_id:
                            # 获取图片关系
                            rels_content = docx_zip.read('word/_rels/document.xml.rels')
                            rels_root = ET.fromstring(rels_content)
                            
                            # 查找对应的图片文件
                            for rel in rels_root.findall('.//{http://schemas.openxmlformats.org/package/2006/relationships}Relationship'):
                                if rel.get('Id') == embed_id:
                                    image_path = rel.get('Target')
                                    
                                    # 提取图片属性
                                    extProps = drawing.find('.//pic:cNvPr', namespaces)
                                    image_name = extProps.get('name', f'image_{i+1}') if extProps is not None else f'image_{i+1}'
                                    
                                    images_info.append({
                                        'id': embed_id,
                                        'name': image_name,
                                        'path_in_doc': image_path,
                                        'index': i + 1
                                    })
                                    break
                                    
        except Exception as e:
            self.logger.warning(f"解析图片信息时出错: {str(e)}")
        
        return images_info
    
    def _count_words(self, text: str) -> int:
        """统计文本中的单词数(简单实现)"""
        # 移除标点符号和多余空格
        cleaned_text = re.sub(r'[^\w\s]', ' ', text)
        words = cleaned_text.split()
        return len(words)

# 使用示例
if __name__ == "__main__":
    # 配置日志
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    
    # 创建解析器实例
    parser = WordParser()
    
    # 示例1: 使用python-docx提取
    print("=== 使用python-docx提取 ===")
    result = parser.process_word_file(
        word_path="/Users/caotianchen.1/Desktop/pyproject/test2/述职讲稿.docx",
        extract_method="python-docx",
        include_headers_footers=True,
        include_tables=True,
        include_images_info=True,
        split_by_paragraph=True,
        min_text_length=20
    )
    
    if result['success']:
        print(f"提取成功!")
        print(f"文件标题: {result['file_info'].get('title', '无标题')}")
        print(f"作者: {result['file_info'].get('author', '未知')}")
        print(f"统计信息: {result['content']['statistics']}")
        
        # 打印前几个段落
        if 'paragraphs' in result['content']:
            print(f"\n--- 前3个段落 ---")
            for para in result['content']['paragraphs'][:3]:
                print(f"段落 {para['index']} (风格: {para.get('style', 'N/A')}):")
                print(f"{para['text'][:200]}...")
        
        # 打印表格信息
        if 'tables' in result['content']:
            print(f"\n--- 找到 {len(result['content']['tables'])} 个表格 ---")
            for table in result['content']['tables'][:2]:  # 只显示前2个表格
                print(f"表格 {table['table_index'] + 1} ({table['row_count']}行×{table['col_count']}列):")
                print(table['text_representation'][:300])
    
    # 示例2: 使用LangChain提取
    print("\n\n=== 使用LangChain提取 ===")
    result2 = parser.process_word_file(
        word_path="/Users/caotianchen.1/Desktop/pyproject/test2/述职讲稿.docx",
        extract_method="langchain",
        split_by_paragraph=True
    )
    
    if result2['success']:
        print(f"提取成功!")
        print(f"总字符数: {result2['content']['statistics']['total_characters']}")
        print(f"总单词数: {result2['content']['statistics']['word_count']}")
    
    # 示例3: 批量处理Word文件
    def process_multiple_word_files(word_files: List[str], **kwargs):
        """批量处理Word文件"""
        parser = WordParser()
        all_results = []
        
        for word_file in word_files:
            try:
                result = parser.process_word_file(word_file, **kwargs)
                all_results.append(result)
                print(f"处理文件 {word_file} 成功")
            except Exception as e:
                print(f"处理文件 {word_file} 失败: {str(e)}")
        
        return all_results
    
    # 批量处理示例
    word_files = ["document1.docx", "document2.docx", "document3.docx"]
    # results = process_multiple_word_files(word_files, extract_method="python-docx")
    
    # 示例4: 提取纯文本(最小配置)
    print("\n\n=== 快速提取纯文本 ===")
    result3 = parser.process_word_file(
        word_path="/Users/caotianchen.1/Desktop/pyproject/test2/述职讲稿.docx",
        extract_method="python-docx",
        include_headers_footers=False,
        include_tables=False,
        split_by_paragraph=False
    )
    
    if result3['success']:
        print("文档全文(前500字符):")
        print(result3['content']['full_text'][:500])

2.3 HTML文档解析

HTML格式特点
  • 标记语言,包含丰富的标签结构
  • 可能包含JavaScript动态内容
  • 需要处理编码问题和CSS样式
HTML解析工具

HTML解析器可以从URL、文件或HTML字符串中提取内容,并转换为结构化的数据或纯文本。

核心类 HTMLParser

  • 使用配置化设计,可以在初始化时设置解析选项(如是否包含链接、图片等)
  • 封装了从不同来源(URL、文件、字符串)解析HTML的功能
  • 返回结构化的数据或纯文本,便于后续处理

主要入口方法 process_html

  • 支持多种来源类型(自动检测或指定)
  • 可配置解析选项(如提取元数据、结构化内容等)
  • 支持返回字典或纯文本两种格式

一、解析流程详解

步骤1:确定来源类型并获取HTML内容

  • 自动检测 :通过_detect_source_type方法,根据输入字符串的特征判断是URL、文件还是HTML字符串。
  • 获取内容
    • URL:使用requests库发送HTTP请求获取HTML,设置超时和请求头以模拟浏览器。
    • 文件:读取本地HTML文件。
    • 字符串:直接使用。

步骤2:解析HTML内容_parse_html_content方法)

  • 使用BeautifulSoup(lxml解析器)解析HTML。
  • 清理HTML:根据初始化参数,移除脚本、样式、导航栏等不需要的标签。
  • 提取元数据:调用_extract_metadata方法,提取标题、描述、关键词、作者、语言和字符集。
  • 提取主要内容:调用_extract_main_content方法,通过多种选择器策略定位网页正文。
  • 提取结构化内容:调用_extract_structured_content方法,提取标题、段落、列表、表格和代码块。
  • 提取纯文本:使用html2text将HTML转换为Markdown风格的纯文本。
  • 提取链接和图片:分别调用_extract_links_extract_images方法。

步骤3:构建返回结果

  • 根据参数return_format返回字典或纯文本。
  • 字典结构包含成功状态、元数据、内容和统计信息。

二、方法说明

清理HTML_clean_html

  • 移除指定标签(如script、style等),这些标签通常不包含有用文本。
  • 移除空标签,简化DOM树。

提取元数据_extract_metadata

  • <title>标签提取标题。
  • 从多个meta标签(包括Open Graph和Twitter卡片)提取描述、关键词、作者等。
  • <html>标签提取语言属性。
  • 从meta标签提取字符集。

提取主要内容_extract_main_content

  • 尝试多种CSS选择器定位主要内容区域(如main、article、.content等)。
  • 如果找不到明确的内容区域,则选择文本最多的div、section或article元素。
  • 最后回退到body或整个soup。

提取结构化内容_extract_structured_content

  • 标题:提取h1到h6,记录级别和文本。
  • 段落:提取所有p标签,记录文本和属性。
  • 列表:提取ul和ol,记录列表项。
  • 表格:如果启用,提取表格数据为二维列表。
  • 代码块:如果启用,提取pre和code标签的文本。

提取纯文本_extract_plain_text

  • 使用html2text库将HTML转换为易读的纯文本(Markdown风格)。
  • 清理多余空行。

提取链接和图片

  • 链接:提取所有a标签的href、文本和title,判断是否为外部链接。
  • 图片:提取所有img标签的src、alt、title、宽高。

三、使用示例

从URL解析

  • 提供URL,自动获取并解析网页。
  • 返回完整的字典结构,包含元数据、纯文本、链接、图片和统计信息。

从文件解析

  • 提供本地HTML文件路径,读取并解析。

从HTML字符串解析

  • 直接提供HTML字符串进行解析。
python 复制代码
from bs4 import BeautifulSoup
import requests
from urllib.parse import urlparse, urljoin
import html2text
import logging
from typing import Dict, List, Any, Optional, Union
from pathlib import Path
import re

class HTMLParser:
    def __init__(
        self, 
        include_links: bool = True, 
        include_images: bool = False,
        include_tables: bool = True,
        include_code: bool = False,
        clean_html: bool = True
    ):
        """
        初始化HTML解析器
        
        Args:
            include_links: 是否包含链接文本
            include_images: 是否包含图片alt文本
            include_tables: 是否提取表格
            include_code: 是否包含代码块
            clean_html: 是否清理HTML(移除script、style等)
        """
        self.include_links = include_links
        self.include_images = include_images
        self.include_tables = include_tables
        self.include_code = include_code
        self.clean_html = clean_html
        
        # 配置html2text转换器
        self.h2t = html2text.HTML2Text()
        self.h2t.ignore_links = not include_links
        self.h2t.ignore_images = not include_images
        self.h2t.body_width = 0  # 不换行
        self.h2t.ignore_emphasis = False  # 保留强调(粗体、斜体)
        
        # 设置日志
        self.logger = logging.getLogger(__name__)
    
    def process_html(
        self,
        source: str,
        source_type: str = "auto",
        timeout: int = 10,
        min_text_length: int = 10,
        extract_metadata: bool = True,
        extract_structure: bool = True,
        return_format: str = "dict"
    ) -> Union[Dict[str, Any], str]:
        """
        处理HTML内容
        
        Args:
            source: HTML来源,可以是URL、文件路径或HTML字符串
            source_type: 来源类型,可选 "auto", "url", "file", "html"
            timeout: 请求超时时间(秒),仅对URL有效
            min_text_length: 最小文本长度阈值
            extract_metadata: 是否提取元数据
            extract_structure: 是否提取结构化内容
            return_format: 返回格式,"dict" 或 "text"
            
        Returns:
            解析后的HTML内容
        """
        
        # 自动检测来源类型
        if source_type == "auto":
            source_type = self._detect_source_type(source)
        
        try:
            # 根据来源类型获取HTML内容
            if source_type == "url":
                html_content, final_url = self._fetch_from_url(source, timeout)
                base_url = final_url
            elif source_type == "file":
                html_content = self._read_from_file(source)
                base_url = f"file://{Path(source).resolve()}"
            else:  # html类型
                html_content = source
                base_url = None
            
            # 解析HTML
            result = self._parse_html_content(
                html_content=html_content,
                base_url=base_url,
                min_text_length=min_text_length,
                extract_metadata=extract_metadata,
                extract_structure=extract_structure
            )
            
            # 设置返回格式
            if return_format == "text":
                return result.get("plain_text", "")
            else:
                return result
            
        except Exception as e:
            self.logger.error(f"HTML处理失败: {source[:50]}..., 错误: {str(e)}")
            if return_format == "text":
                return ""
            else:
                return {
                    'success': False,
                    'error': str(e),
                    'source': source,
                    'content': None
                }
    
    def _detect_source_type(self, source: str) -> str:
        """自动检测来源类型"""
        # 检查是否是URL
        try:
            result = urlparse(source)
            if all([result.scheme, result.netloc]):
                return "url"
        except:
            pass
        
        # 检查是否是文件路径
        if Path(source).exists() and Path(source).suffix.lower() in ['.html', '.htm']:
            return "file"
        
        # 检查是否是HTML字符串
        html_pattern = re.compile(r'<[^>]+>', re.IGNORECASE)
        if html_pattern.search(source) and len(source) > 100:
            return "html"
        
        # 默认为HTML字符串
        return "html"
    
    def _fetch_from_url(self, url: str, timeout: int) -> tuple:
        """从URL获取HTML内容"""
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language': 'en-US,en;q=0.5',
            'Accept-Encoding': 'gzip, deflate',
            'DNT': '1',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1'
        }
        
        response = requests.get(url, headers=headers, timeout=timeout)
        response.raise_for_status()
        
        # 检测编码
        if response.encoding is None:
            response.encoding = 'utf-8'
        
        return response.text, response.url
    
    def _read_from_file(self, file_path: str) -> str:
        """从文件读取HTML内容"""
        with open(file_path, 'r', encoding='utf-8') as f:
            return f.read()
    
    def _parse_html_content(
        self,
        html_content: str,
        base_url: Optional[str] = None,
        min_text_length: int = 10,
        extract_metadata: bool = True,
        extract_structure: bool = True
    ) -> Dict[str, Any]:
        """解析HTML内容"""
        
        # 使用lxml解析器(需要安装lxml库)
        soup = BeautifulSoup(html_content, 'lxml')
        
        # 清理HTML(移除不需要的标签)
        if self.clean_html:
            self._clean_html(soup)
        
        # 提取元数据
        metadata = {}
        if extract_metadata:
            metadata = self._extract_metadata(soup, base_url)
        
        # 提取主要内容
        main_content = self._extract_main_content(soup)
        
        # 提取结构化内容
        structured_content = []
        if extract_structure:
            structured_content = self._extract_structured_content(soup, min_text_length)
        
        # 提取纯文本
        plain_text = self._extract_plain_text(main_content)
        
        # 提取所有链接
        links = self._extract_links(soup, base_url)
        
        # 提取所有图片
        images = self._extract_images(soup, base_url)
        
        # 构建结果
        result = {
            'success': True,
            'metadata': metadata,
            'content': {
                'html': str(main_content)[:5000] + "..." if len(str(main_content)) > 5000 else str(main_content),
                'plain_text': plain_text,
                'links': links,
                'images': images,
                'statistics': {
                    'total_characters': len(plain_text),
                    'total_words': len(plain_text.split()),
                    'link_count': len(links),
                    'image_count': len(images),
                    'structure_count': len(structured_content)
                }
            }
        }
        
        # 添加结构化内容
        if structured_content:
            result['content']['structure'] = structured_content
        
        self.logger.info(f"HTML解析完成,提取字符数: {len(plain_text)}")
        
        return result
    
    def _clean_html(self, soup: BeautifulSoup) -> None:
        """清理HTML,移除不需要的标签"""
        tags_to_remove = ['script', 'style', 'nav', 'footer', 'header', 'iframe', 'noscript']
        
        if not self.include_code:
            tags_to_remove.extend(['code', 'pre'])
        
        for tag in tags_to_remove:
            for element in soup.find_all(tag):
                element.decompose()
        
        # 移除空标签
        for element in soup.find_all():
            if len(element.get_text(strip=True)) == 0 and not element.find_all():
                element.decompose()
    
    def _extract_metadata(self, soup: BeautifulSoup, base_url: Optional[str]) -> Dict[str, str]:
        """提取元数据"""
        metadata = {
            'source': base_url or 'local_html',
            'title': '',
            'description': '',
            'keywords': '',
            'author': '',
            'language': '',
            'charset': ''
        }
        
        # 提取标题
        title_tag = soup.find('title')
        if title_tag:
            metadata['title'] = title_tag.text.strip()
        
        # 提取meta标签
        meta_tags = {
            'description': ['description', 'og:description', 'twitter:description'],
            'keywords': ['keywords'],
            'author': ['author', 'article:author'],
            'charset': ['charset']
        }
        
        for meta_name, meta_keys in meta_tags.items():
            for key in meta_keys:
                if ':' in key:  # Open Graph / Twitter 格式
                    tag = soup.find('meta', attrs={'property': key})
                else:
                    tag = soup.find('meta', attrs={'name': key})
                
                if tag:
                    metadata[meta_name] = tag.get('content', '').strip()
                    break
        
        # 提取语言
        html_tag = soup.find('html')
        if html_tag:
            metadata['language'] = html_tag.get('lang', '')
        
        # 如果没有找到charset,尝试从meta标签中提取
        if not metadata['charset']:
            charset_tag = soup.find('meta', attrs={'charset': True})
            if charset_tag:
                metadata['charset'] = charset_tag.get('charset', '')
        
        return metadata
    
    def _extract_main_content(self, soup: BeautifulSoup) -> BeautifulSoup:
        """提取主要内容区域"""
        # 尝试多种策略定位主要内容
        selectors = [
            'main', 'article', '.content', '#content', 
            '.post-content', '.article-content', '.entry-content',
            '.main-content', '.body-content', '#main', '.main',
            '[role="main"]', '.blog-post', '.story-content'
        ]
        
        for selector in selectors:
            element = soup.select_one(selector)
            if element and len(element.get_text(strip=True)) > 100:
                return element
        
        # 如果没有找到明确的内容区域,尝试使用正文内容最多的元素
        candidates = soup.find_all(['div', 'section', 'article'])
        if candidates:
            # 选择文本最多的候选元素
            best_candidate = max(candidates, key=lambda x: len(x.get_text(strip=True)))
            if len(best_candidate.get_text(strip=True)) > 200:
                return best_candidate
        
        # 最后返回整个body
        return soup.body or soup
    
    def _extract_structured_content(self, soup: BeautifulSoup, min_text_length: int) -> List[Dict]:
        """提取结构化内容(标题、段落等)"""
        structured = []
        
        # 提取标题
        headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
        for heading in headings:
            text = heading.get_text(strip=True)
            if text and len(text) >= min_text_length:
                structured.append({
                    'type': 'heading',
                    'level': int(heading.name[1]),
                    'text': text,
                    'tag': heading.name,
                    'id': heading.get('id', '')
                })
        
        # 提取段落
        paragraphs = soup.find_all('p')
        for idx, para in enumerate(paragraphs):
            text = para.get_text(strip=True)
            if text and len(text) >= min_text_length:
                structured.append({
                    'type': 'paragraph',
                    'text': text,
                    'index': idx,
                    'class': para.get('class', []),
                    'has_links': len(para.find_all('a')) > 0
                })
        
        # 提取列表
        lists = soup.find_all(['ul', 'ol'])
        for list_idx, list_elem in enumerate(lists):
            list_items = list_elem.find_all('li')
            if list_items:
                items_text = [li.get_text(strip=True) for li in list_items if li.get_text(strip=True)]
                if items_text:
                    structured.append({
                        'type': 'list',
                        'list_type': list_elem.name,  # ul 或 ol
                        'item_count': len(items_text),
                        'items': items_text
                    })
        
        # 提取表格
        if self.include_tables:
            tables = soup.find_all('table')
            for table_idx, table in enumerate(tables):
                table_data = self._extract_html_table(table)
                if table_data:
                    structured.append({
                        'type': 'table',
                        'index': table_idx,
                        'row_count': len(table_data),
                        'col_count': len(table_data[0]) if table_data else 0,
                        'data': table_data
                    })
        
        # 提取代码块
        if self.include_code:
            code_blocks = soup.find_all(['pre', 'code'])
            for code_idx, code in enumerate(code_blocks):
                text = code.get_text(strip=True)
                if text and len(text) >= min_text_length:
                    structured.append({
                        'type': 'code',
                        'language': code.get('class', [''])[0] if code.get('class') else '',
                        'text': text
                    })
        
        return structured
    
    def _extract_html_table(self, table) -> List[List[str]]:
        """提取HTML表格数据"""
        rows = []
        for tr in table.find_all('tr'):
            row = []
            for cell in tr.find_all(['td', 'th']):
                # 合并跨行跨列单元格
                colspan = int(cell.get('colspan', 1))
                rowspan = int(cell.get('rowspan', 1))
                
                cell_text = cell.get_text(strip=True)
                
                # 对于合并单元格,添加多个相同内容
                for _ in range(colspan):
                    row.append(cell_text)
            
            if row:
                rows.append(row)
        
        # 确保所有行有相同数量的列
        if rows:
            max_cols = max(len(row) for row in rows)
            for row in rows:
                while len(row) < max_cols:
                    row.append('')
        
        return rows
    
    def _extract_plain_text(self, soup: BeautifulSoup) -> str:
        """提取纯文本"""
        # 使用html2text转换为Markdown风格文本
        text = self.h2t.handle(str(soup))
        
        # 清理多余的空行
        text = re.sub(r'\n\s*\n', '\n\n', text)
        
        return text.strip()
    
    def _extract_links(self, soup: BeautifulSoup, base_url: Optional[str]) -> List[Dict]:
        """提取所有链接"""
        links = []
        seen_hrefs = set()
        
        for a in soup.find_all('a', href=True):
            href = a.get('href', '').strip()
            text = a.get_text(strip=True)
            
            if not href or href in seen_hrefs:
                continue
            
            # 处理相对URL
            if base_url and not urlparse(href).scheme:
                href = urljoin(base_url, href)
            
            link_info = {
                'href': href,
                'text': text,
                'title': a.get('title', ''),
                'is_external': bool(urlparse(href).netloc) and (base_url is None or urlparse(href).netloc != urlparse(base_url).netloc)
            }
            
            links.append(link_info)
            seen_hrefs.add(href)
        
        return links
    
    def _extract_images(self, soup: BeautifulSoup, base_url: Optional[str]) -> List[Dict]:
        """提取所有图片"""
        images = []
        seen_srcs = set()
        
        for img in soup.find_all('img', src=True):
            src = img.get('src', '').strip()
            
            if not src or src in seen_srcs:
                continue
            
            # 处理相对URL
            if base_url and not urlparse(src).scheme:
                src = urljoin(base_url, src)
            
            img_info = {
                'src': src,
                'alt': img.get('alt', ''),
                'title': img.get('title', ''),
                'width': img.get('width', ''),
                'height': img.get('height', '')
            }
            
            images.append(img_info)
            seen_srcs.add(src)
        
        return images

# 使用示例
if __name__ == "__main__":
    # 配置日志
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    
    # 创建HTML解析器
    parser = HTMLParser(
        include_links=True,
        include_images=True,
        include_tables=True,
        include_code=False,
        clean_html=True
    )
    
    # 示例: 从URL解析
    print("=== 从URL解析 ===")
    result = parser.process_html(
        source="https://blog.csdn.net/qq_54708219?type=blog",
        source_type="url",
        min_text_length=20,
        extract_metadata=True,
        extract_structure=True,
        return_format="dict"
    )
    
    if isinstance(result, dict) and result.get('success', False):
        print(f"标题: {result['metadata']['title']}")
        print(f"描述: {result['metadata']['description'][:100]}...")
        print(f"纯文本长度: {len(result['content']['plain_text'])} 字符")
        print(f"链接数: {result['content']['statistics']['link_count']}")
        print(f"图片数: {result['content']['statistics']['image_count']}")
        
        # 打印前200个字符的纯文本
        print(f"\n纯文本预览:")
        print(result['content']['plain_text'][:200] + "...")
    
相关推荐
源代码•宸1 小时前
Golang原理剖析(程序初始化、数据结构string)
开发语言·数据结构·经验分享·后端·golang·string·init
忆锦紫2 小时前
图像增强算法:对比度增强算法以及MATLAB实现
开发语言·图像处理·matlab
m0_748250032 小时前
C++ Web 编程
开发语言·前端·c++
4***17542 小时前
Python酷库之旅-第三方库Pandas(051)
开发语言·python·pandas
码农阿豪2 小时前
远程调试不再难!Remote JVM Debug+cpolar 让内网 Java 程序调试变简单
java·开发语言·jvm
lubiii_2 小时前
MCP应用:cursor+hexstrike-ai的安全实战
开发语言·web安全·ai·php
是罐装可乐2 小时前
前端架构知识体系:深入理解 sessionStorage、opener 与浏览器会话模型
开发语言·前端·javascript·promise·语法糖
cd ~/Homestead2 小时前
PHP 变量、类型、运算符
android·开发语言·php