Files
2026-03-26 18:04:54 +09:00

273 lines
8.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()