十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 pdfplumber+PyMuPDF 构建气象学 PDF 知识库与刷题系统

用 pdfplumber+PyMuPDF 构建气象学 PDF 知识库与刷题系统 简介这份资料是山东农业大学气象学课程的主要复习内容整理面向该校修读气象学及相关专业课程、需要应对期末或补考的学生。内容以课件为基础、教材为参考梳理了课程的核心考点与思考题涵盖天气与气候概念、太阳辐射与地面辐射、热量交换与土壤温度变化、空气湿度与水汽凝结、气压与风、气团锋面、气旋反气旋、霜冻寒潮台风等模块并列出名词解释、填空、选择、判断、计算与作图等考试题型。资源包共1个PDF文件约146KB轻量便于手机或电脑随时翻阅。已有124人学习下载。读者可据此建立复习框架明确太阳高度角、昼长、积温、气温直减率等计算题以及等压线受力分析、海陆风山谷风等作图题的练习方向减少盲目翻书提高冲刺阶段的复习效率。1. 把《气象学主要复习内容.pdf》当成数据源而不是一份要背的讲义期末前两周农学、植保、园艺几个专业的群里都会出现同一份《气象学主要复习内容.pdf》——几十页辐射、温度、大气水分、气压与风、天气系统、农业气候一路排下来名词解释、公式、简答题混在一起。真正让人难受的不是内容多而是它是一份 PDF想确认「活动积温」和「有效积温」差在哪得靠肉眼翻页想查「饱和水汽压」的系数翻到的那页往往只剩半行公式。换个思路把这份复习资料当数据源处理先抽成干净文本再切成分层的知识块然后把辐射、温度、水分、风这四类考点变成可以查询、可以自动出题、可以回捞错题的本地小系统。山东农业大学气象学这门课的考点分布相对稳定术语体系封闭非常适合做结构化和参数化。适合两类人要考气象学的农科同学能拿到一套按章节检索、按公式刷题的复习工具做数据清洗、文档解析的工程师能拿它练一遍从 PDF 到本地知识库的完整链路场地小、反馈快、不用联网。2. 用 pdfplumber 与 PyMuPDF 抽干气象学 PDF 的正文与公式动手之前先做一件事确认这份 PDF 到底是文本型、扫描型还是混合型。这一步决定后面所有工作量判错了会白干半天。2.1 三行命令判断 PDF 是文本型还是扫描型最省事的办法是先抽三页看字符数。命令行有 poppler 的话直接# 抽第 1~3 页-layout 保留粗略版面 pdftotext -f 1 -l 3 -layout 气象学主要复习内容.pdf - | head -60 # 统计抽出的字符数除以页数 pdftotext -f 1 -l 3 气象学主要复习内容.pdf - | tr -d \n | wc -m三页字符数低于 100基本可以判定是扫描件得走 OCR常见做法是 PaddleOCR 或 tesseract 加中文包几百到几千字符说明有文本层可以直接抽取。注意一个高频情况正文是文本层公式是图片。这种「混合型」最坑因为抽出来的文字里公式位置是空的你会得到「饱和水汽压 es ____」这种残句。判断办法是把某页的图片数量打出来。# probe.py 统计每页文本量与图片量 import fitz # PyMuPDF doc fitz.open(气象学主要复习内容.pdf) for i, page in enumerate(doc, 1): text_len len(page.get_text().strip()) img_cnt len(page.get_images(fullTrue)) print(fp{i:3} chars{text_len:5} images{img_cnt})逻辑很直白get_text()拿文本层长度get_images(fullTrue)拿图片对象数。如果某一页chars很小但images有 2~5 个那一页大概率是整页扫描或者公式图集后续要单独处理。参数上fullTrue会返回 xref 等信息方便后面按 xref 导出原图。2.2 pdfplumber 与 PyMuPDF 怎么选两个库都能抽文本但脾气不一样混用是常态维度pdfplumberPyMuPDFfitz抽取速度慢几十页可以接受快适合批量版面判断强能拿到每个字符的坐标一般需要自己算表格线识别内置extract_table需手写中文紧凑排版x_tolerance可调效果好默认也稳偶有字序错乱图片导出支持但不如 fitz 方便get_imagespixmap很顺典型用法正文 表格图片 全量快速抽取常见做法是正文和表格交给 pdfplumber图片和整页渲染交给 PyMuPDF最后按页码合并。2.3 抽取正文并清理页眉页脚与中文断行复习资料最脏的地方不是公式而是页眉页脚和 PDF 换行。中文 PDF 经常把一个词拆成两行直接拼接会得到「相对湿\n度」这种碎片。# extract_text.py import re, json import pdfplumber # 页眉页脚常见形态校名、章节名重复、纯页码 HEAD_FOOT re.compile(r^(山东农业大学|气象学.*(复习|讲义)|第\s*\d\s*页|[-—]\s*\d\s*[-—])) def clean(text: str) - str: lines [] for ln in text.split(\n): ln ln.replace(\u3000, ).strip() # 全角空格转半角 if not ln or HEAD_FOOT.match(ln): continue lines.append(ln) s \n.join(lines) # 中文行尾被硬回车截断上一行以汉字结尾、下一行以汉字开头 → 合并 s re.sub(r([\u4e00-\u9fa5、])\n([\u4e00-\u9fa5]), r\1\2, s) s re.sub(r[ \t]{2,}, , s) return s out [] with pdfplumber.open(气象学主要复习内容.pdf) as pdf: for i, page in enumerate(pdf.pages, 1): raw page.extract_text(x_tolerance1.5, y_tolerance1.5) or out.append({page: i, text: clean(raw)}) json.dump(out, open(raw_pages.json, w, encodingutf-8), ensure_asciiFalse, indent1) print(pages:, len(out), chars:, sum(len(p[text]) for p in out))参数说明x_tolerance和y_tolerance默认是 3中文排版字距紧调成 1.5 能减少整行被判成两段的情况调太小会把同一行的字符拆散一般 1~2 之间试两次就能定。extract_text默认关闭layout参数输出是逐行文本适合后面按行做正则如果某一页是双栏需要开layoutTrue或者用page.crop()左右各切一半分别抽。抽取完做一次抽查随机挑 3 页把原文 PDF 和raw_pages.json对照读重点看数字和单位有没有错位。6.11 hPa被拆成6.1和1 hPa这种事后面所有公式计算都会跟着错。提示不要在抽取阶段就急着删空行。空行是章节分割的重要线索留到结构化阶段再决定去留。2.4 公式图和示意图怎么落地公式在复习资料里通常有两种形态可选中文本少见和图片常见。图片公式不要指望 OCR 直接还原成 LaTeX上下标和希腊字母错一个公式就废了。更划算的做法是把图导出来当索引用公式本身人工补录。# extract_img.py 把每页图片导出按 页码-序号 命名 import os, fitz os.makedirs(figs, exist_okTrue) doc fitz.open(气象学主要复习内容.pdf) for pno, page in enumerate(doc, 1): for idx, info in enumerate(page.get_images(fullTrue), 1): xref info[0] pix fitz.Pixmap(doc, xref) if pix.n - pix.alpha 4: # CMYK 转 RGB pix fitz.Pixmap(fitz.csRGB, pix) pix.save(ffigs/p{pno:03d}-{idx}.png)导出后按p012-1.png这样的命名建立「页码 → 公式编号」对照表人工填一列latex或symbol。这门课的公式数量有限几十条的量级手工比调 OCR 快而且准。真正需要 OCR 的是整页扫描件那种情况先做整页渲染再识别别只对嵌入图做识别。3. 把辐射、温度、水分、风这四类考点结构化成可查询数据抽出文本只是原料。气象学复习资料的组织逻辑和一般教材不同它按考点类型混排同一页可能既有名词解释又有计算题。要让它可检索得先补一层结构。3.1 用正则识别章节层级别用纯文本切分这类资料的一级标题通常带编号比如「第一章 大气概况」「第二章 太阳辐射」二级标题是「一、」「1.」「1.1」。纯按空行切会切碎必须用行首模式匹配。# outline.py import re H1 re.compile(r^\s*第[一二三四五六七八九十]章\s*[、.]?\s*(.{2,30})$) H2 re.compile(r^\s*([一二三四五六七八九十])\s*[、.]\s*(.{2,30})$) H3 re.compile(r^\s*(\d(?:\.\d){1,2})\s*[、.]?\s*(.{2,40})$) def parse(pages): nodes, cur [], {h1: 未分类, h2: , h3: } for p in pages: for ln in p[text].split(\n): if m : H1.match(ln): cur {h1: m.group(1).strip(), h2: , h3: } elif m : H2.match(ln): cur {**cur, h2: m.group(2).strip(), h3: } elif m : H3.match(ln): cur {**cur, h3: m.group(2).strip()} else: nodes.append({**cur, page: p[page], text: ln}) return nodes逻辑说明每遇到标题行就更新当前路径之后所有正文行都带上这条路径。H3用了{1,2}允许「3.2」和「3.2.1」两种层级正则里的?:是非捕获组避免取错 group。匹配失败的行不丢归到当前路径下宁可多带上下文也不要丢内容。3.2 生成带章节路径的 JSONL 知识块逐行存太碎检索命中后只能看到一句话。按「标题 内容 页范围」聚合成块块长度控制在 200~500 字这个区间既够上下文完整又不至于让关键词命中互相淹没。# to_chunks.py import json from outline import parse MAXLEN 500 pages json.load(open(raw_pages.json, encodingutf-8)) nodes parse(pages) chunks, buf [], {path: , pages: [], text: } for n in nodes: path /.join(x for x in (n[h1], n[h2], n[h3]) if x) or 未分类 if buf[path] path and len(buf[text]) len(n[text]) MAXLEN: buf[text] \n n[text] buf[pages].append(n[page]) else: if buf[path]: chunks.append(buf) buf {path: path, pages: [n[page]], text: n[text]} if buf[path]: chunks.append(buf) for i, c in enumerate(chunks): c[id] fc{i:04d} c[pages] sorted(set(c[pages])) with open(chunks.jsonl, w, encodingutf-8) as f: for c in chunks: f.write(json.dumps(c, ensure_asciiFalse) \n) print(chunks:, len(chunks))pages用列表并去重是为了后面按页码回查原 PDF 截图核对。path字段是关键它让「辐射」这个大词和「太阳高度角」这个小词不会混在同一层。3.3 三类考点怎么打标气象学的题就三类打标之后复习策略完全不同考点类型识别信号复习动作检索关键词示例名词解释「是指」「称为」「定义为」背定义抓限定词相对湿度、露点、锋面计算题含公式、单位、字母符号手算 代码验算饱和水汽压、积温、太阳高度角简答/论述「试述」「简述」「影响…因素」记要点条数和顺序影响气温日变化的因素打标不用模型正则就够句中出现「是指/称为/定义为」归为名词解释出现「」「hPa」「℃·d」「计算」归为计算题出现「简述/试述/说明」归为简答。三类之外的全归「其他」人工再看一遍通常只占一成。3.4 抽样校验别让错误结构一路传下去切块之后必须抽查。做法是从每个一级章节里随机抽两块打印id path pages text回翻原 PDF 对应页比对。重点看三件事章节路径有没有串「大气水分」的内容被挂到「气压与风」下、公式块是不是残句、页眉页脚有没有漏网。抽查花十五分钟能省掉后面所有环节的返工。如果发现某章的路径全错基本是那一章的标题格式和正则不匹配把标题行捞出来单独看一眼就能改。4. 农业气象学公式落地从饱和水汽压到积温的可复现代码气象学复习里最容易失分的是计算题公式记错一个系数整题归零。把这些公式写成函数既能验算手算结果也能随机化参数做无限刷题。4.1 常用符号、单位与取值符号含义单位常见取值 / 范围es饱和水汽压hPa0 ℃ 约 6.11e实际水汽压hPa由 RH 反推RH相对湿度%0~100Td露点温度℃恒 ≤ 气温 Tγd干绝热递减率℃/100 m约 1.0γm湿绝热递减率℃/100 m约 0.4~0.9随温度变化φ当地纬度°泰安一带约 36.2δ太阳赤纬°-23.5~23.5ΣT活动积温℃·d因作物而异查教材附表单位是最容易翻车的地方es 用百帕还是千帕积温用日平均还是日最高最低平均教材之间不统一。写代码时把单位写进函数文档比事后对账省事。4.2 饱和水汽压与露点Magnus 形式的实现与校验农业气象教材普遍采用 Magnus 型经验式系数随版本略有差异实现时把系数抽成参数方便和教材对齐。# meteo.py import math def es_magnus(t_c: float, a: float 7.5, b: float 237.3) - float: 饱和水汽压 es(hPa)t_c 为摄氏温度。 a、b 为 Magnus 系数若教材给的是 17.27/237.7 形式 注意那是 e06.11 的另一套写法别混用。 e0 6.11 # 0 ℃ 时的饱和水汽压hPa return e0 * 10 ** (a * t_c / (b t_c)) def rh_from_td(t_c: float, td_c: float) - float: 由气温和露点求相对湿度(%)RH e(Td)/es(T) return 100.0 * es_magnus(td_c) / es_magnus(t_c) def dew_point(t_c: float, rh: float) - float: 由气温和相对湿度反推露点(℃) e es_magnus(t_c) * rh / 100.0 # es 反解t b*lg(e/e0) / (a - lg(e/e0)) lg math.log10(e / 6.11) return 237.3 * lg / (7.5 - lg) if __name__ __main__: print(round(es_magnus(20), 2)) # 20 ℃ 约 23.4 hPa print(round(rh_from_td(25, 20), 1)) # 25 ℃ / 露点 20 ℃ print(round(dew_point(25, 70), 1)) # 与上式互为镜像结果应接近 19 ℃es_magnus用10 **而不是math.exp是因为教材里的 Magnus 系数是按常用对数拟合的换成 e 指数得换系数。rh_from_td和dew_point互为逆运算这是个天然的自检给一组 (T, RH)先算 Td 再算回 RH误差应当在 0.5% 以内。dew_point里math.log10(e/6.11)当 e 很小时会出问题实际取值都在 0 ℃ 以上不用额外防护但要知道这个边界。注意不同教材的 Magnus 系数有 7.5/237.3、7.45/235、17.27/237.7 几套写法。手算时先看题干给了哪一套代码里的默认值只作验算参照。4.3 积温与太阳高度角把参数写进函数签名积温分两种区别只在一个减法活动积温累加高于界限温度的日平均气温有效积温累加「日平均气温减去界限温度」。def active_accum_temp(temps, t_base10.0): 活动积温只累加 界限温度的日平均气温 return sum(t for t in temps if t t_base) def effective_accum_temp(temps, t_base10.0): 有效积温累加(T - 界限温度)低于界限温度记 0 return sum(max(t - t_base, 0.0) for t in temps) def solar_altitude_sin(lat_deg, decl_deg, hour_angle_deg): 太阳高度角正弦sin(h)sinφsinδcosφcosδcosω ω 为时角正午为 0每小时 15° r math.radians return (math.sin(r(lat_deg)) * math.sin(r(decl_deg)) math.cos(r(lat_deg)) * math.cos(r(decl_deg)) * math.cos(r(hour_angle_deg))) # 泰安 φ≈36.2°夏至 δ≈23.5°正午 ω0 print(round(math.degrees(math.asin(solar_altitude_sin(36.2, 23.5, 0))), 1))三个函数各有坑。积温的界限温度不是随便定的小麦、玉米、水稻常用的界限温度不同数值以教材附表为准代码只负责累加。太阳高度角公式里ω用度、math.cos要弧度忘转就全错所以radians写在函数内部而不是让调用方处理。夏至正午泰安的高度角算出来应在 77° 左右这个数可以直接当回归测试的断言值。4.4 递减率与稳定度判断结果先看符号干绝热递减率 γd≈1.0 ℃/100 m湿绝热递减率 γm 小于 γd气温垂直递减率记为 γ。判断气层稳定度的规则就三条γ γd 为绝对不稳定γ γm 为绝对稳定介于两者之间为条件性不稳定。def stability(gamma, gamma_d1.0, gamma_m0.6): gamma 为实际气温垂直递减率(℃/100m) if gamma gamma_d: return 绝对不稳定 if gamma gamma_m: return 绝对稳定 return 条件性不稳定gamma_m在代码里给了 0.6 这个常见中值真实值随温度和湿度在 0.4~0.9 之间变化做题时以题干给的为准。这个函数的价值在于把「介于两者之间」这种模糊描述固化成可判断的分支刷题时不会因为忘了第三条而丢分。5. 复习闭环关键词检索、自动出题与错题回捞前面几步产出了chunks.jsonl和一组建模函数最后一公里是让它们能被反复用起来。最轻量的方案是 SQLite不需要任何服务端。5.1 用 SQLite FTS5 建索引中文先分词FTS5 默认按空白切词中文整句会被当成一个 token检索等于失效。做法是入库前用 jieba 把text切成空格分隔的形式建表只用于检索原文另存一列。# index.py import sqlite3, json, jieba con sqlite3.connect(meteo.db) con.execute(CREATE VIRTUAL TABLE IF NOT EXISTS chunk USING fts5(path, body, raw UNINDEXED, page UNINDEXED)) for line in open(chunks.jsonl, encodingutf-8): c json.loads(line) body .join(jieba.cut(c[text])) con.execute(INSERT INTO chunk(path, body, raw, page) VALUES(?,?,?,?), (c[path], body, c[text], ,.join(map(str, c[pages])))) con.commit() # 检索示例查饱和水汽压按 BM25 排序取前 5 for row in con.execute( SELECT path, page, snippet(chunk,1,,,…,12), bm25(chunk) FROM chunk WHERE chunk MATCH ? ORDER BY bm25(chunk) LIMIT 5, (饱和水汽压,)): print(row)raw和page加了UNINDEXED避免正文被重复索引导致排名偏差。snippet负责把命中词上下文裁出来bm25(chunk)是 FTS5 内置的相关度函数值越小越相关。查「露点」这种短词容易召回到「露点温度」「露点仪」这时把检索词改成露点 AND 温度就能收窄。5.2 自动出题三类题型三套模板题型生成规则需要的字段名词解释取含「是指/称为」的句子遮住术语path、原句填空句中数值和单位替换为下划线数值正则命中计算调用meteo.py随机化参数函数名 参数区间计算题模板最值得做因为参数可以随机化从meteo.py里挑一个函数按表里的取值范围随机生成 20 组输入输出题目和答案两列答案由函数本身给出。dew_point(25, 70)这种输入换成 18~30 ℃、40%~90% 的随机组合一次就能刷出一页题。生成时把随机种子固定成学号一类的常数同一份 PDF 每次生成的练习卷完全一致方便和同学对答案。5.3 错题回捞一张表加一条 SQL错题本不需要 App一张表就够。CREATE TABLE IF NOT EXISTS wrong ( chunk_id TEXT PRIMARY KEY, path TEXT, body TEXT, wrong_n INTEGER DEFAULT 1, last_ts TEXT ); -- 做错的题存在则次数 1否则插入 INSERT INTO wrong(chunk_id, path, body, wrong_n, last_ts) VALUES (c0031, 第二章 太阳辐射/太阳高度角, sin h sinφsinδ cosφcosδcosω, 1, datetime(now)) ON CONFLICT(chunk_id) DO UPDATE SET wrong_n wrong_n 1, last_ts datetime(now); -- 每次复习先捞这批错 2 次以上、且 3 天没碰过 SELECT path, body FROM wrong WHERE wrong_n 2 AND last_ts datetime(now, -3 days) ORDER BY wrong_n DESC, last_ts ASC;wrong_n和last_ts两列配合复习顺序就自动排出来了错得多的优先久没碰的插队。chunk_id做主键是让同一道题重复做错只累加计数不会撑出重复行。最后一个能立刻用上的技巧把meteo.py里每个公式函数配一条已知答案的断言写成assert abs(dew_point(25, 70) - 19.4) 0.5这样的形式塞进测试文件改系数、改单位的时候跑一遍公式库就不会在你不知情的情况下被改坏。本文还有配套的精品资源点击获取
返回列表