feat: metagpt-Python-PDF-Feedback-UI-20260326
This commit is contained in:
272
pdf_opener/main.py
Normal file
272
pdf_opener/main.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""PDF文件读取和文本提取工具。
|
||||
|
||||
使用pypdf库实现PDF文件的读取、基本信息展示和文本内容提取。
|
||||
支持通过命令行参数指定PDF文件路径和可选的页码范围。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import pypdf
|
||||
from pypdf import PdfReader
|
||||
|
||||
|
||||
class PDFOpener:
|
||||
"""PDF文件打开和内容提取类。
|
||||
|
||||
封装了PDF读取、信息提取和文本提取的全部功能。
|
||||
|
||||
Attributes:
|
||||
file_path: PDF文件的路径。
|
||||
start_page: 提取文本的起始页码(从0开始,包含)。
|
||||
end_page: 提取文本的结束页码(从0开始,包含),None表示最后一页。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: str,
|
||||
start_page: int = 0,
|
||||
end_page: Optional[int] = None,
|
||||
) -> None:
|
||||
"""初始化PDFOpener实例。
|
||||
|
||||
Args:
|
||||
file_path: PDF文件的路径。
|
||||
start_page: 提取文本的起始页码(从0开始),默认为0。
|
||||
end_page: 提取文本的结束页码(从0开始),默认为None(最后一页)。
|
||||
"""
|
||||
self._file_path: str = file_path
|
||||
self._start_page: int = start_page
|
||||
self._end_page: Optional[int] = end_page
|
||||
|
||||
def open(self) -> PdfReader:
|
||||
"""打开PDF文件并返回PdfReader对象。
|
||||
|
||||
处理文件不存在、格式错误、权限不足等常见异常,
|
||||
遇到异常时输出友好错误信息并退出程序。
|
||||
|
||||
Returns:
|
||||
pypdf.PdfReader对象。
|
||||
|
||||
Raises:
|
||||
SystemExit: 当文件不存在、格式错误或权限不足时退出程序。
|
||||
"""
|
||||
try:
|
||||
reader = PdfReader(self._file_path)
|
||||
return reader
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
f"错误:文件未找到,请检查路径是否正确:'{self._file_path}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
except pypdf.errors.PdfReadError as e:
|
||||
print(
|
||||
f"错误:无法读取PDF文件,文件可能已损坏或格式不正确:{e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
except PermissionError:
|
||||
print(
|
||||
f"错误:没有权限读取文件:'{self._file_path}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
def get_info(self, reader: PdfReader) -> dict:
|
||||
"""获取PDF文件的基本信息。
|
||||
|
||||
Args:
|
||||
reader: pypdf.PdfReader对象。
|
||||
|
||||
Returns:
|
||||
包含PDF基本信息的字典,包括总页数和元数据。
|
||||
"""
|
||||
num_pages: int = len(reader.pages)
|
||||
metadata: Optional[pypdf.DocumentInformation] = reader.metadata
|
||||
|
||||
info: dict = {
|
||||
"num_pages": num_pages,
|
||||
"title": metadata.title if metadata and metadata.title else "未知",
|
||||
"author": metadata.author if metadata and metadata.author else "未知",
|
||||
"subject": metadata.subject if metadata and metadata.subject else "未知",
|
||||
"creator": metadata.creator if metadata and metadata.creator else "未知",
|
||||
"producer": metadata.producer if metadata and metadata.producer else "未知",
|
||||
"creation_date": (
|
||||
metadata.creation_date if metadata and metadata.creation_date else "未知"
|
||||
),
|
||||
}
|
||||
return info
|
||||
|
||||
def display_info(self, info: dict) -> None:
|
||||
"""将PDF基本信息格式化输出到控制台。
|
||||
|
||||
Args:
|
||||
info: 包含PDF基本信息的字典。
|
||||
"""
|
||||
separator: str = "=" * 50
|
||||
print(separator)
|
||||
print("PDF 文件基本信息")
|
||||
print(separator)
|
||||
print(f" 总页数 : {info.get('num_pages', '未知')}")
|
||||
print(f" 标题 : {info.get('title', '未知')}")
|
||||
print(f" 作者 : {info.get('author', '未知')}")
|
||||
print(f" 主题 : {info.get('subject', '未知')}")
|
||||
print(f" 创建工具 : {info.get('creator', '未知')}")
|
||||
print(f" 生成工具 : {info.get('producer', '未知')}")
|
||||
print(f" 创建日期 : {info.get('creation_date', '未知')}")
|
||||
print(separator)
|
||||
print()
|
||||
|
||||
def extract_text(self, reader: PdfReader) -> list[str]:
|
||||
"""按页码范围逐页提取PDF文本内容。
|
||||
|
||||
Args:
|
||||
reader: pypdf.PdfReader对象。
|
||||
|
||||
Returns:
|
||||
每页文本内容组成的列表,列表索引对应页码偏移。
|
||||
"""
|
||||
num_pages: int = len(reader.pages)
|
||||
|
||||
# 确定实际的起始和结束页码(基于0的索引)
|
||||
actual_start: int = max(0, self._start_page)
|
||||
actual_end: int = (
|
||||
num_pages - 1 if self._end_page is None else min(self._end_page, num_pages - 1)
|
||||
)
|
||||
|
||||
if actual_start > actual_end:
|
||||
print(
|
||||
f"警告:起始页码 ({actual_start + 1}) 大于结束页码 ({actual_end + 1}),"
|
||||
f"将不提取任何文本。",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return []
|
||||
|
||||
texts: list[str] = []
|
||||
for page_index in range(actual_start, actual_end + 1):
|
||||
page = reader.pages[page_index]
|
||||
page_text: str = page.extract_text() or ""
|
||||
texts.append(page_text)
|
||||
|
||||
return texts
|
||||
|
||||
def display_text(self, texts: list[str]) -> None:
|
||||
"""将提取的文本内容格式化输出到控制台。
|
||||
|
||||
Args:
|
||||
texts: 每页文本内容组成的列表。
|
||||
"""
|
||||
if not texts:
|
||||
print("未提取到任何文本内容。")
|
||||
return
|
||||
|
||||
separator: str = "-" * 50
|
||||
actual_start_display: int = self._start_page + 1 # 转换为1-based显示
|
||||
|
||||
for i, text in enumerate(texts):
|
||||
page_number: int = actual_start_display + i
|
||||
print(f"【第 {page_number} 页】")
|
||||
print(separator)
|
||||
if text.strip():
|
||||
print(text)
|
||||
else:
|
||||
print("(本页无可提取的文本内容)")
|
||||
print(separator)
|
||||
print()
|
||||
|
||||
def run(self) -> None:
|
||||
"""主流程入口,依次执行PDF读取、信息展示和文本提取。
|
||||
|
||||
按顺序调用 open、get_info、display_info、extract_text、display_text。
|
||||
"""
|
||||
reader: PdfReader = self.open()
|
||||
info: dict = self.get_info(reader)
|
||||
self.display_info(info)
|
||||
texts: list[str] = self.extract_text(reader)
|
||||
self.display_text(texts)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""解析命令行参数。
|
||||
|
||||
Returns:
|
||||
包含解析后参数的Namespace对象:
|
||||
- file_path: PDF文件路径(必填)
|
||||
- start_page: 起始页码,1-based(可选,默认为1)
|
||||
- end_page: 结束页码,1-based(可选,默认为None表示最后一页)
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="PDF文件读取和文本提取工具",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=(
|
||||
"示例用法:\n"
|
||||
" python main.py document.pdf\n"
|
||||
" python main.py document.pdf --start-page 2 --end-page 5\n"
|
||||
" python main.py document.pdf --start-page 3\n"
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"file_path",
|
||||
type=str,
|
||||
help="PDF文件的路径(必填)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--start-page",
|
||||
type=int,
|
||||
default=1,
|
||||
dest="start_page",
|
||||
metavar="N",
|
||||
help="提取文本的起始页码(从1开始,默认为1)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--end-page",
|
||||
type=int,
|
||||
default=None,
|
||||
dest="end_page",
|
||||
metavar="N",
|
||||
help="提取文本的结束页码(从1开始,默认为最后一页)",
|
||||
)
|
||||
|
||||
args: argparse.Namespace = parser.parse_args()
|
||||
|
||||
# 验证页码参数合法性
|
||||
if args.start_page < 1:
|
||||
parser.error("--start-page 必须大于等于1")
|
||||
|
||||
if args.end_page is not None and args.end_page < 1:
|
||||
parser.error("--end-page 必须大于等于1")
|
||||
|
||||
if args.end_page is not None and args.start_page > args.end_page:
|
||||
parser.error("--start-page 不能大于 --end-page")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""程序主入口函数。
|
||||
|
||||
解析命令行参数,创建PDFOpener实例并执行主流程。
|
||||
"""
|
||||
args: argparse.Namespace = parse_args()
|
||||
|
||||
# 将1-based的用户输入页码转换为0-based的内部索引
|
||||
start_page_index: int = args.start_page - 1
|
||||
end_page_index: Optional[int] = (
|
||||
args.end_page - 1 if args.end_page is not None else None
|
||||
)
|
||||
|
||||
pdf_opener = PDFOpener(
|
||||
file_path=args.file_path,
|
||||
start_page=start_page_index,
|
||||
end_page=end_page_index,
|
||||
)
|
||||
pdf_opener.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user