文本解析到到大模型应用
一、背景
最近接到一个比较恶心的工作,之前有个同事将很多个小的文档整合到了一个大文档中,同时暴露出一个新的问题,大的文档虽然查找问题比较方便但是维护起来较为麻烦,所以需要将大的文档按照标题拆分成为多个文档。
原始文档为PDF文档,观察得出以下处理思路
-
文档是有目录的文档中的二级标题即为文件名(例如:1.中华人民共和国公司法(2023 年修订))
-
可以获取到文档的页码位置。目录中的页码即为标题的文档范围
-
临界判断,文档页中其实页标题出现的行即为该文档的起始位置,第二个标题出现的行即为上一标题的结束位置
二、获取文档位置
分析目录发现,二级标题的目录字体大小为14到15之间所以以此为依据获取单个文档所在的页码和位置。实现代码如下
python
import fitz
import word_analysis
def get_title_info(pdf_path, start_page, end_page, title_size_min, title_size_max):
"""
从pdf目录获取标题信息
:param pdf_path: 文件路径
:param start_page: 待解析目录开始页码
:param end_page: 待解析目录结束页码
:param title_size_min: 待提取标题最小字号
:param title_size_max: 待提取标题最大字号
:return: [ {'title_name': '标题名称', 'start_page': '该标题开始页码', 'end_flag': '下一标题', 'end_page': '下一标题开始页面'}
......]
"""
pdf_doc = fitz.open(pdf_path)
title_list = []
title_dict = {}
for page_num in range(start_page, end_page):
page = pdf_doc.load_page(page_num)
text_dict = page.get_text("dict")
# 遍历每个文本块
for block in text_dict["blocks"]:
# 遍历每行
for line in block["lines"]:
for span in line["spans"]:
text = span["text"]
size = span["size"]
if size > title_size_min:
title = text.split('..')[0].strip()
start_page = text.split('.')[-1].strip()
if title == '' or title is None:
continue
title_dict['title_name'] = title
title_dict['start_page'] = text.split('.')[-1].strip()
if size < title_size_max:
title_list.append(title_dict)
title_dict = {}
for index in range(len(title_list) -1):
title_list[index]['end_flag'] = title_list[index + 1]['title_name']
title_list[index]['end_page'] = title_list[index + 1]['start_page']
return title_list
if __name__ == '__main__':
pdf_path = r"pdf文件全路径"
title_info = get_title_info(pdf_path, 1, 53, 14, 15)
for t in title_info:
print(t)
输出结果如下
textile
{'title_name': '1.中华人民共和国公司法(2023 年修订)', 'start_page': '62', 'end_flag': '2.中华人民共和国市场主体登记管理条例', 'end_page': '116'}
{'title_name': '2.中华人民共和国市场主体登记管理条例', 'start_page': '116', 'end_flag': '3.中华人民共和国证券法', 'end_page': '137'}
{'title_name': '3.中华人民共和国证券法', 'start_page': '137', 'end_flag': '4.中华人民共和国证券投资基金法', 'end_page': '187'}
{'title_name': '4.中华人民共和国证券投资基金法', 'start_page': '187', 'end_flag': '5.中华人民共和国期货和衍生品法', 'end_page': '213'}
三、保存文档
上一步已经获取了文档的位置,所以尝试按照位置信息保存文档
python
def save_pdf(doc, start_page, end_page, start_y, end_y, heading, output_folder):
"""
:param doc: fitz打开文档后 调用 load_page 方法获取的对象
:param start_page:开始页码
:param end_page:结束页面
:param start_y:开始页码中开始位置坐标
:param end_y:结束页码中结束位置坐标
:param heading:文档名称
:param output_folder:文档位置
:return:
"""
# 创建一个新的 PDF 文档
new_doc = fitz.open()
# 将指定范围的页面复制到新文档中
for page_num in range(start_page, end_page):
page = doc.load_page(page_num)
if page_num == start_page:
# 裁剪页面,从 start_y 开始
rect = page.rect
rect.y0 = start_y
page.set_cropbox(rect)
if page_num == end_page - 1 and end_y > 0:
# 裁剪页面,到 end_y 结束
rect = page.rect
rect.y1 = end_y
page.set_cropbox(rect)
# 将裁剪后的页面添加到新文档中
new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
# 生成输出文件名
output_path = os.path.join(output_folder, f"{heading}.pdf")
# 检查新文档中是否有页面
if len(new_doc) == 0:
print(f"警告:文档 {heading} 没有页面,跳过保存。")
return
# 保存新的 PDF 文件
print(output_path)
new_doc.save(output_path)
new_doc.close()
print(f"保存文件:{output_path}")
四、遇到的难点
虽然确实可以获取到文档,但是存在以下三个致命问题。
-
页眉页脚以及页码会原样保留
-
首页和尾页存在半页的情况(因为是按照位置获取的)
-
pdf重新修改以上两个位置是比较麻烦的
五、大模型应用
新的思路:
通过第二步其实文档的名称已经可以获取到了,还有个简单的办法,直接重新下载一个word版本的,这样一劳永逸了。
尝试利用网页版kimi获取下载链接,存在以下两个问题
-
无法实现自动下载
-
获取的链接不是直接可见的(出于安全性的考虑)
所以尝试利用大模型的api实现这一步骤
5.1 开头难
简单看了通义千问,官方的文档只是一味的介绍功能的强大,对新手极不友好,无法快速上手(中国人的通病,简单的问题喜欢复杂化);请教了身边所谓的会大模型的同事,但人家秘技自珍,压根不愿意听你遇到的业务场景,更不愿分享自己大模型入手的心得(大环境不好导致人与人之间恶性模式滋生)。所以自己花时间琢磨了一下所谓的大模型,写给小白的,那些懂的大神看到这里可以停了。
5.2 官方文档
网页版位置:
url
https://tongyi.aliyun.com/qianwen/
官方文档位置:
url
https://help.aliyun.com/zh/dashscope/developer-reference/
5.3 获取API Key
前往如下链接
url
https://bailian.console.aliyun.com/?tab=model#/efm/model_experience_center/text
可以看到 新用户开通即享每个模型100万免费Tokens立即开通 点击进行注册
注册成功后在页面左下角点击"API-Key"
这里注意未完成认证无法创建API-Key
认证链接如下
url
https://home.console.aliyun.com/home/dashboard/ProductAndService
可以使用支付宝进行认证
认证完成即可创建
这里引出了业务空间的概念,其实简单理解业务空间,就是模型的业务领域,可以在不同的领域创建API-Key,专注于某个领域的模型应用。这里咱们初次使用在默认的业务空间下创建就可以。
5.4 查找应用id
url
https://bailian.console.aliyun.com/?tab=app#/app-center
做好模型配置然后发布
5.5 配置API-Key到环境变量
(1)linux系统
shell
echo "export DASHSCOPE_API_KEY='我们上一步创建的API-Key'" >> ~/.bashrc
shell
source ~/.bashrc
shell
echo $DASHSCOPE_API_KEY
最好新开一个会话确认环境变量是否生效
(2)windows系统
打开环境变量,新建系统环境变量 DASHSCOPE_API_KEY,将API-Key写入其中
在cmd命令窗口执行
bash
echo %DASHSCOPE_API_KEY%
能正常打印就成功了
5.6 模型调用
安装第三方包
bash
pip install dashscope
官方代码样例
python
import os
from http import HTTPStatus
from dashscope import Application
respose = Application.call(
# 若没有配置环境变量,可用百炼API Key将下行替换为:api_key="sk-xxx"。但不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。
api_key=os.getenv("DASHSCOPE_API_KEY"),
app_id='YOUR_APP_ID',# 替换为实际的应用 ID
prompt='你是谁?')
if response.status_code != HTTPStatus.OK:
print(f'request_id={response.request_id}')
print(f'code={response.status_code}')
print(f'message={response.message}')
print(f'请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code')
else:
print(response.output.text)
注意:windows环境需要重启电脑让环境变量生效
请求体参数说明:
参数 | 是否必填 | 说明 |
---|---|---|
app_id | 是 | 应用的标识。可以在应用列表页面创建应用,在应用中可以选择模型类型(通义千问、deepseek等) |
prompt | 是 | 输入当前期望应用执行的指令prompt,用来指导应用生成回复。 |
暂不支持传入文件。如果应用使用的是Qwen-Long模型,应用调用方法与其他模型一致。 | ||
当您通过传入messages自己管理对话历史时,则无需传递prompt。 | ||
session_id | 否 | 历史对话的唯一标识。 传入session_id时,prompt为必传。 |
若同时传入session_id和messages,则优先使用传入的messages。 | ||
目前仅智能体应用和对话型工作流应用支持多轮对话。 | ||
messages | 否 | 由历史对话组成的消息列表。 |
参数说明详见
url
https://help.aliyun.com/zh/model-studio/call-application-through-api?spm=a2c4g.11186623.help-menu-2400256.d_3_0_0.259716danoDEXf#b3ff5b7310yyx
5.7 个人尝试
可能写的多了,思路该跳转回来了。请看本文 一、背景
文档拆分,还要保留格式是比较困难的。因为pdf和word的加密方式并不一致,且pdf拆分的效果并不是太好,所以考虑是不是可以通过大模型找到链接直接下载
python
import os
from http import HTTPStatus
from dashscope import Application
response = Application.call(
# 若没有配置环境变量,可用百炼API Key将下行替换为:api_key="sk-xxx"。但不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。
api_key=os.getenv("DASHSCOPE_API_KEY"),
app_id='应用ID',# 替换为实际的应用 ID
prompt='请提供 中华人民共和国公司法(2023 年修订)的下载链接?')
if response.status_code != HTTPStatus.OK:
print(f'request_id={response.request_id}')
print(f'code={response.status_code}')
print(f'message={response.message}')
else:
print(response.output.text)
得到的结果如下
textile
截至我最后更新的信息(2023年),中华人民共和国公司法的确经过了几次修订,但具体的官方最新版本以及其发布日期可能需要通过官方渠道确认。通常情况下,最新的法律文本会在中国人大网(全国人民代表大会官方网站)上公布。您可以访问该网站来查找《中华人民共和国公司法》的最新版,并直接在线浏览或下载PDF格式文件。
访问步骤如下:
1. 打开浏览器,输入网址:http://www.npc.gov.cn/
2. 在首页中找到"法律法规"或者直接使用搜索功能查找"公司法"。
3. 选择对应的最新年份版本进行查看或下载。
请注意,由于网络环境和政策的变化,请确保从可靠来源获取信息,并且最好核实所下载文件是否为官方发布的最新版本。如果在寻找过程中遇到困难,建议咨询专业法律顾问或相关部门获得帮助。
仅给出了下载网站,并未给出链接,且按照模型给出下载的方式尝试也未成功
5.8 大模型思考
近两年大模型确实冲击了很多的行业,有管理者诉说着大模型的种种神奇之处,制造行业危机感和员工的失业焦虑。在此也说一下笔者个人看法(不喜勿喷,如果不喜欢,那不好意思,因为本身也不是写给你看的)
(1)大模型是将已有的知识归纳总结,是否能完全替代设计能力有待思考
大模型确实是个好东西,它可以快速方便的把已有的知识总结出来供大家参考。但如果说能取代某些行业个人认为目前还是实现不了的。因为他只是在已有的基础上归纳总结,很难做出创新和设计。
珠宝设计行业应用大模型确实造成冲击,不需要那么多的设计师了因为Ai更加快捷准确,但一些新型的设计以及出自高端设计师之手。
程序设计行业,确实不太可能有人记得所有,就像本文pdf处理的方法我不是每个都记得住的,资讯Ai确实能快速的给出案例。但模型不会有人的思维没有办法设计出好的程序。所以我认为模型是"最强辅助"而不是"取代ADC"。它让有设计能力的不再需要那么多的时间做基础的事情效率能有更大提升。
(2)有了大模型是否意味着无脑大模型
前段时间数据行业在设想一个事情,AI自动生成SQL,从而取代ETL。首先把ETL简单看成写SQL已经严重侮辱了数据行业ETL这个方向,在此不做额外的解释。其次自动生成SQL需要依赖于数据中台长期的积累,数据标准长期推进并取得成效(最起码元数据标准和参照数据标准是需要的吧)。所以并不是有了大模型就能不去经历这些步骤,但它确实可以极大缩短某些步骤的时间。
大模型应用是会快速给出知识和答案,但也容易造成部分人知识学习碎片化,不容易生成知识树,学习不够系统。长期推广会导致没有人去关注底层导致缺乏创新。对此我只想说"大佬,你选的嘛"。
六、python的word操作
安装第三方包
bash
pip install docx
创建基础对象
python
from docx import Document
doc = Document()
为文档添加标题
这里level为1是因为设置为0的时候总有一条文本分割线,尝试后没有去掉
python
from docx.shared import Pt
from docx.shared import RGBColor
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
# 添加标题
title = doc.add_heading('标题', level=1)
# 设置标题居中
title.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
# 获取标题的 run 对象
for run in title.runs:
# 设置标题字体大小为 24 磅
run.font.size = Pt(24)
# 设置标题字体颜色为黑色
run.font.color.rgb = RGBColor(0, 0, 0)
添加段落
python
paragraph = doc.add_paragraph()
段落文本加粗
python
run = paragraph.add_run('加粗')
run.bold = True
段落文本带下划线
python
run = paragraph.add_run('下划线')
run.underline = True
段落文本为斜体字
python
run = paragraph.add_run('斜体字')
run.italic = True
段落文本右对齐
python
paragraph = doc.add_paragraph('右对齐')
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT
设置行间距
python
paragraph = doc.add_paragraph('行间距')
paragraph_format = paragraph.paragraph_format
paragraph_format.line_spacing = 1.5
保存文档
python
doc.save('文档路径')
七、代码分享
因为这个工作非长期工作,是一个次要的辅助工作所以代码中并未做异常处理,供参考
python
# !/usr/bin/python
# -*-coding:utf-8 -*-
"""
File : pdf_model.py
Time : 2025-04-25 8:52
Author : Author
Email : no
version : python 3.8.4
Description :
"""
import os
import fitz
class PdfModel(object):
def __init__(self, pdf_path):
self.pdf_doc = fitz.open(pdf_path)
def get_title_info(self, start_page, end_page, title_size_min, title_size_max):
"""
从pdf目录获取标题信息
:param start_page: 待解析目录开始页码
:param end_page: 待解析目录结束页码
:param title_size_min: 待提取标题最小字号
:param title_size_max: 待提取标题最大字号
:return: [ {'title_name': '标题名称', 'start_page': '该标题开始页码', 'end_flag': '下一标题', 'end_page': '下一标题开始页面'}
......]
"""
title_list = []
title_dict = {}
for page_num in range(start_page, end_page):
page = self.pdf_doc.load_page(page_num)
text_dict = page.get_text("dict")
# 遍历每个文本块
for block in text_dict["blocks"]:
# 遍历每行
for line in block["lines"]:
for span in line["spans"]:
text = span["text"]
size = span["size"]
if size > title_size_min:
title = text.split('..')[0].strip()
if title == '' or title is None:
continue
title_dict['title_name'] = title
try:
title_dict['start_page'] = int(text.split('.')[-1].strip())
except ValueError:
title_dict['start_page'] = 9999
if size < title_size_max:
title_list.append(title_dict)
title_dict = {}
for index in range(len(title_list) - 1):
title_list[index]['end_flag'] = title_list[index + 1]['title_name']
try:
title_list[index]['end_page'] = int(title_list[index + 1]['start_page'])
except ValueError:
title_list[index]['end_page'] = 9999
return title_list
def get_text_position(self, page_no, text_name):
"""
查找文本在pdf页中的位置(纵坐标)
:param page_no: 页码
:param text_name: 待查找文本
:return:
"""
position_y = None
page = self.pdf_doc.load_page(page_no - 1)
text_instances = page.get_text("blocks")
# 按 y, x 排序,从上到下,从左向右
text_instances.sort(key=lambda block: (block[1], block[0]))
for block in text_instances:
# 修正解包操作
x1, y1, x2, y2, text = block[:5]
if text_name in text:
position_y = y2
return position_y
def split_pdf_by_headings(self, page_dict, output_folder):
# 如果输出文件夹不存在,创建它
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# 确定文档开始结束位置
page_start = page_dict['start_page']
page_end = page_dict['end_page']
print('============开始页码==============')
start_position_y = self.get_text_position(page_start, page_dict['title_name'])
print(start_position_y)
print('============结束页码==============')
if page_dict['end_flag'] == "":
end_position_y = -1
else:
end_position_y = self.get_text_position(page_end, page_dict['end_flag'])
print(end_position_y)
self.save_pdf(page_start, page_end, start_position_y, end_position_y, page_dict['title_name'], output_folder)
print(f"PDF 文件已按标题拆分到文件夹:{output_folder}")
def save_pdf(self, start_page, end_page, start_y, end_y, heading, output_folder):
"""
按照开始结束页码和页码中的位置截取pdf文档
:param doc: fitz打开文档后 调用 load_page 方法获取的对象
:param start_page:开始页码
:param end_page:结束页面
:param start_y:开始页码中开始位置坐标
:param end_y:结束页码中结束位置坐标
:param heading:文档名称
:param output_folder:文档位置
:return:
"""
position_delta = 15
# 创建一个新的 PDF 文档
new_doc = fitz.open()
# 将指定范围的页面复制到新文档中
for page_num in range(start_page - 1, end_page):
page = self.pdf_doc.load_page(page_num)
if page_num == start_page - 1:
# 裁剪页面,从 start_y 开始
rect = page.rect
rect.y0 = start_y - position_delta
page.set_cropbox(rect)
if page_num == end_page - 1 and end_y > 0:
# 裁剪页面,到 end_y 结束
rect = page.rect
rect.y1 = end_y - position_delta
page.set_cropbox(rect)
# 将裁剪后的页面添加到新文档中
new_doc.insert_pdf(self.pdf_doc, from_page=page_num, to_page=page_num)
# 生成输出文件名
output_path = os.path.join(output_folder, f"{heading}.pdf")
# 检查新文档中是否有页面
if len(new_doc) == 0:
print(f"警告:文档 {heading} 没有页面,跳过保存。")
return
# 保存新的 PDF 文件
new_doc.save(output_path)
new_doc.close()
print(f"保存文件:{output_path}")
def get_pdf_text(self, start_page, start_y, end_page, end_y, output_folder, output_filename):
"""
从指定页面的特定位置开始提取文本内容并保存到文本文件
:param start_page: 开始页码
:param start_y: 开始页码中的起始位置坐标(y 坐标)
:param end_page: 结束页面
:param end_y: 结束页码中的起始位置坐标(y 坐标)
:param output_folder: 输出文件夹路径
:param output_filename: 输出文件名
:return:
"""
position_delta = 15
# 输入参数验证
if start_page < 1 or end_page > len(self.pdf_doc) or start_page > end_page:
raise ValueError(f"Invalid page range: start_page={start_page}, end_page={end_page}. Total pages: {len(self.pdf_doc)}")
if start_y < 0:
raise ValueError(f"Invalid start_y: {start_y}. It must be non-negative.")
# 确保输出目录存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# 生成输出文件路径
output_path = os.path.join(output_folder, output_filename)
# 打开输出文件
with open(output_path, "w", encoding="utf-8") as output_file:
# 遍历指定范围的页面
for page_num in range(start_page - 1, end_page):
page = self.pdf_doc.load_page(page_num)
if page_num == start_page - 1:
# 裁剪页面,从 start_y 开始
rect = page.rect
rect.y0 = start_y - position_delta
page.set_cropbox(rect)
if page_num == end_page - 1:
# 裁剪页面,从 start_y 开始
rect = page.rect
rect.y1 = end_y - position_delta
page.set_cropbox(rect)
text = page.get_text() # 提取页面文本
output_file.write(text) # 将文本写入文件
print(f"文本内容已保存到文件:{output_path}")
def close(self):
self.pdf_doc.close()