乐于分享
好东西不私藏

工厂模式 | 文档解析器工厂

工厂模式 | 文档解析器工厂

import osfrom abc import ABC, abstractmethod """用来定义 “规矩 / 模板” 的工具ABC = Abstract Base Class翻译:抽象基类人话:一个只定规矩、不干活的类@abstractmethod = 抽象方法人话:这是一个必须实现的方法,谁继承我,谁就必须写这个方法!不写 → 直接报错,不让创建对象!强制统一结构""" class DocumentParser(ABC):  #ABC:让类变成抽象类(规矩类)    """文档解析器基类"""    @abstractmethod    def parse(self, filepath):        passclass PDFParser(DocumentParser):    def parse(self, filepath):        return {            'content':f'这是从{filepath}提取的PDF内容。包含10页文本。',            'metadata': {                "type""pdf",                "pages"10,                "file_size""2.5MB"            }            }class DOCXParser(DocumentParser):    def parse(self, filepath):        return {            "content"f"这是从{filepath}提取的Word文档内容。",            "metadata": {                "type""docx",                "paragraphs"50,                "file_size""1.2MB"            }        }class TXTParser(DocumentParser):    def parse(self, filepath):        try:            with open(filepath, 'r', encoding='utf-8')as f:                content = f.read()            return {                "content": content,                "metadata": {                    "type""txt",                    "lines"len(content.split('\n')),                    "file_size"f"{len(content)} bytes"                }            }        except FileNotFoundError:            return{                "content"f"模拟TXT内容来自{filepath}",                "metadata": {"type""txt"}            }class MarkdownParser(DocumentParser):    def parse(self, filepath: str) -> dict:        return {            "content"f"这是从{filepath}解析的Markdown内容。",            "metadata": {                "type""markdown",                "headings"5            }        }class ParserFactory:    """解析器工厂"""    # 注册表模式(可扩展)    _parsers = {        '.pdf': PDFParser,        '.docx': DOCXParser,        '.doc': DOCXParser,        '.txt': TXTParser,        '.md': MarkdownParser,        '.markdown': MarkdownParser,    }    # @staticmethod    @classmethod    def create_parser(cls, filepath: str) -> DocumentParser:        """根据文件扩展名返回对应的解析器"""        ext = os.path.splitext(filepath)[1].lower()        """        splitext = split extension → 按 “后缀名” 拆分路径        比如:            filepath = "test.pdf"            os.path.splitext(filepath)            结果是一个元组:("test", ".pdf") 第 0 位:文件名前面部分 "test",第 1 位:后缀名(带点) .pdf            也可以这样:            _, ext = os.path.splitext(filepath)            ext = ext.lower()        """        parser_class = cls._parsers.get(ext)        if parser_class is None:            raise ValueError(f"不支持的文件格式: {ext}")        return parser_class()    @classmethod    def register_parser(cls, extension, parser_class):        """注册新的解析器(扩展点)"""        # if not extension.startwith('.'):            # extension = '.' + extension        cls._parsers[extension.lower()] = parser_class    @classmethod    def get_supported_formats(cls) -> list:        return list(cls._parsers.keys())# 测试# 测试PDFparser = ParserFactory.create_parser("document.pdf")assert isinstance(parser, PDFParser)result = parser.parse("test.pdf")print(f"PDF解析: {result['metadata']}")# 测试DOCXparser = ParserFactory.create_parser("document.docx")assert isinstance(parser, DOCXParser)result = parser.parse("test.docx")print(f"DOCX解析: {result['metadata']}")# 测试TXTparser = ParserFactory.create_parser("document.txt")assert isinstance(parser, TXTParser)result = parser.parse("test.txt")print(f"TXT解析: {result['metadata']}")# 测试注册新解析器class CSVParser(DocumentParser):    def parse(self, filepath: str) -> dict:        return {            "content"f"CSV数据来自{filepath}",            "metadata": {"type""csv""rows"100}        }ParserFactory.register_parser('.csv', CSVParser)parser = ParserFactory.create_parser("data.csv")assert isinstance(parser, CSVParser)# 测试不支持的格式try:    ParserFactory.create_parser("file.xyz")    assert False"应该抛出异常"except ValueError as e:    print(f"✅ 正确处理不支持格式: {e}")print(f"支持的格式: {ParserFactory.get_supported_formats()}")print("✅ 工厂模式测试通过")

一、场景

根据不同文件后缀,自动创建对应的解析器,不用写一堆 if-elif。

1. 第一段:抽象类(定规矩)

class DocumentParser(ABC):    @abstractmethod    def parse(self, filepath):        pass

作用:强制 PDFParser、DOCXParser 都必须写 parse(),不写就报错,保证统一接口。

2. 第二段:各种解析器(遵守规矩)

它们都继承了 DocumentParser;

它们都老老实实实现了 parse ();

它们接口完全统一。

3. 第三段:工厂(自动造对象)—— 最重要

classParserFactory:    _parsers = {        '.pdf': PDFParser,        '.docx': DOCXParser,        '.txt': TXTParser,    }    @staticmethod    def create_parser(filepath):        ext = os.path.splitext(filepath)[1].lower()  # 拿后缀        parser_class = ParserFactory._parsers.get(ext)        return parser_class()

你给我一个文件名,我自动判断后缀,返回对应解析器。就不用写下面这些了:

if 后缀 == pdf: ...elif 后缀 == docx: ...elif ...

二、个人理解/最初卡在哪里

1、在抽象方法里加代码

@abstractmethoddef parse(self, filepath):    self.filepath = filepath  # 👈 这么写错误

抽象方法只是定规矩,不应该包含任何实际功能代码,上面加了一句实际代码,就破坏了抽象类的意义。

@abstractmethod 的意义:只定义接口,不实现功能,写 self.filepath = filepath 是实现代码,不对。

有的子类可能需要 filepath,有的子类根本不需要,在父类强制存 filepath,会让所有子类被迫接受,非常不灵活。

正确写法:

@abstractmethoddef parse(self, filepath):    pass  # 只有 pass,什么都不写

所以 self.filepath = filepath应该写在子类里。

2、在用@classmethod 地方使用了 @staticmethod

@classmethod(类方法) = 跟【类】绑定,能访问类自己的东西,第一个参数固定是 cls,代表当前类,能访问类变量:cls._parsers,继承后是否自动适配子类

@staticmethod(静态方法) = 跟谁都不绑定,就是个普通工具函数,没有固定参数,不能自动访问类,纯工具,继承不适配,永远用原类,想访问类变量必须写死:ParserFactory._parsers

代码对比:

classParserFactory:    _parsers = {}  # 类变量    # ✅ 类方法:自带 cls,能访问 _parsers    @classmethod    def create_parser(cls, filepath):        return cls._parsers  # 正常访问    # ✅ 静态方法:没有 cls,想访问必须写死类名    @staticmethod    def create_parser2(filepath):        return ParserFactory._parsers  # 写死类名

在本程序中的好处:

不用写死类名 ParserFactory

以后子类继承,自动用子类的 _parsers

代码规范、干净、可扩展

三、关键洞察

抽象方法里写 pass,不能写任何实际代码,实际代码必须写在子类里。