文本解析到到大模型應用
一、背景
最近接到一個比較惡心的工作,之前有個同事將很多個小的文檔整合到了一個大文檔中,同時暴露出一個新的問題,大的文檔雖然查找問題比較方便但是維護起來較為麻煩,所以需要將大的文檔按照標題拆分成為多個文檔。
原始文檔為PDF文檔,觀察得出以下處理思路
-
文檔是有目錄的文檔中的二級標題即為文件名(例如:1.中華人民共和國公司法(2023 年修訂))
-
可以獲取到文檔的頁碼位置。目錄中的頁碼即為標題的文檔范圍
-
臨界判斷,文檔頁中其實頁標題出現的行即為該文檔的起始位置,第二個標題出現的行即為上一標題的結束位置
二、獲取文檔位置
分析目錄發現,二級標題的目錄字體大小為14到15之間所以以此為依據獲取單個文檔所在的頁碼和位置。實現代碼如下
import fitz
import word_analysisdef 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:continuetitle_dict['title_name'] = titletitle_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_listif __name__ == '__main__':pdf_path = r"pdf文件全路徑"title_info = get_title_info(pdf_path, 1, 53, 14, 15)for t in title_info:print(t)
輸出結果如下
{'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'}
三、保存文檔
上一步已經獲取了文檔的位置,所以嘗試按照位置信息保存文檔
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.rectrect.y0 = start_ypage.set_cropbox(rect)if page_num == end_page - 1 and end_y > 0:# 裁剪頁面,到 end_y 結束rect = page.rectrect.y1 = end_ypage.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 官方文檔
網頁版位置:
https://tongyi.aliyun.com/qianwen/
官方文檔位置:
https://help.aliyun.com/zh/dashscope/developer-reference/
5.3 獲取API Key
前往如下鏈接
https://bailian.console.aliyun.com/?tab=model#/efm/model_experience_center/text
可以看到 新用戶開通即享每個模型100萬免費Tokens立即開通 點擊進行注冊
注冊成功后在頁面左下角點擊“API-Key”
這里注意未完成認證無法創建API-Key
認證鏈接如下
https://home.console.aliyun.com/home/dashboard/ProductAndService
可以使用支付寶進行認證
認證完成即可創建
這里引出了業務空間的概念,其實簡單理解業務空間,就是模型的業務領域,可以在不同的領域創建API-Key,專注于某個領域的模型應用。這里咱們初次使用在默認的業務空間下創建就可以。
5.4 查找應用id
https://bailian.console.aliyun.com/?tab=app#/app-center
做好模型配置然后發布
5.5 配置API-Key到環境變量
(1)linux系統
echo "export DASHSCOPE_API_KEY='我們上一步創建的API-Key'" >> ~/.bashrc
source ~/.bashrc
echo $DASHSCOPE_API_KEY
最好新開一個會話確認環境變量是否生效
(2)windows系統
打開環境變量,新建系統環境變量 DASHSCOPE_API_KEY,將API-Key寫入其中
在cmd命令窗口執行
echo %DASHSCOPE_API_KEY%
能正常打印就成功了
5.6 模型調用
安裝第三方包
pip install dashscope
官方代碼樣例
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',# 替換為實際的應用 IDprompt='你是誰?')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 | 否 | 由歷史對話組成的消息列表。 |
參數說明詳見
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拆分的效果并不是太好,所以考慮是不是可以通過大模型找到鏈接直接下載
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',# 替換為實際的應用 IDprompt='請提供 中華人民共和國公司法(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)
得到的結果如下
截至我最后更新的信息(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操作
安裝第三方包
pip install docx
創建基礎對象
from docx import Document
doc = Document()
為文檔添加標題
這里level為1是因為設置為0的時候總有一條文本分割線,嘗試后沒有去掉
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)
添加段落
paragraph = doc.add_paragraph()
段落文本加粗
run = paragraph.add_run('加粗')
run.bold = True
段落文本帶下劃線
run = paragraph.add_run('下劃線')
run.underline = True
段落文本為斜體字
run = paragraph.add_run('斜體字')
run.italic = True
段落文本右對齊
paragraph = doc.add_paragraph('右對齊')
paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT
設置行間距
paragraph = doc.add_paragraph('行間距')
paragraph_format = paragraph.paragraph_format
paragraph_format.line_spacing = 1.5
保存文檔
doc.save('文檔路徑')
七、代碼分享
因為這個工作非長期工作,是一個次要的輔助工作所以代碼中并未做異常處理,供參考
# !/usr/bin/python
# -*-coding:utf-8 -*-
"""File : pdf_model.pyTime : 2025-04-25 8:52Author : AuthorEmail : noversion : python 3.8.4Description :
"""
import os
import fitzclass 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:continuetitle_dict['title_name'] = titletry:title_dict['start_page'] = int(text.split('.')[-1].strip())except ValueError:title_dict['start_page'] = 9999if 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'] = 9999return title_listdef get_text_position(self, page_no, text_name):"""查找文本在pdf頁中的位置(縱坐標):param page_no: 頁碼:param text_name: 待查找文本:return:"""position_y = Nonepage = 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 = y2return position_ydef 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 = -1else: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.rectrect.y0 = start_y - position_deltapage.set_cropbox(rect)if page_num == end_page - 1 and end_y > 0:# 裁剪頁面,到 end_y 結束rect = page.rectrect.y1 = end_y - position_deltapage.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.rectrect.y0 = start_y - position_deltapage.set_cropbox(rect)if page_num == end_page - 1:# 裁剪頁面,從 start_y 開始rect = page.rectrect.y1 = end_y - position_deltapage.set_cropbox(rect)text = page.get_text() # 提取頁面文本output_file.write(text) # 將文本寫入文件print(f"文本內容已保存到文件:{output_path}")def close(self):self.pdf_doc.close()