ARTICLE DETAIL

资讯详情

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

用Seed-2.1-pro搭建考研择校平台:从官方表格解析到多模态读图完整实践

用Seed-2.1-pro搭建考研择校平台:从官方表格解析到多模态读图完整实践 为什么想到做这个每年考研报名前后研招网和各院校研究生院官网会放出大量表格招生专业目录、历年复试分数线、报录比、推免名单。这些东西不是没有是散。一个考生想对比三所学校同一个专业往往要开十几个页面手动抄到Excel里再算。我最初的想法很简单能不能做一个平台把这些官方文件丢进去自动抽成结构化数据然后按专业、地区、分数线做筛选对比。数据源必须来自官方不能靠爬第三方汇总站因为那些站本身就有错。选模型的时候我用了Seed-2.1-pro。这里说明一下Seed系列模型的版本迭代比较快我写这篇文章时使用的是Seed-2.1-pro这个具体版本它的文本理解和多模态读图能力是我这边实测可用的。如果你后续拿到的版本不同接口参数和表现可能有差异建议以官方最新文档为准。整篇文章讲的是我实际搭出来的东西代码跑通过但涉及具体接口字段的地方我会明确标注哪些是通用逻辑、哪些依赖具体版本。整体架构先定下来平台不复杂分四层采集层负责把官方文件PDF、Excel、图片截图、网页拿到本地解析层用Seed-2.1-pro把非结构化内容转成结构化JSON存储层SQLite存结构化数据够用不用上重型数据库服务层FastAPI提供查询接口前端单独做为什么解析层要单独抽出来因为官方文件的格式差异太大。有的学校给的是标准Excel有的是扫描版PDF有的是网页表格还有的干脆是一张分数线截图。如果解析逻辑和业务逻辑混在一起后面加一所学校就要改一次主流程维护会崩。所以我把解析层设计成每种文件类型一个handler输出统一的中间结构业务层只认中间结构。数据源的真实样子先说清楚数据长什么样这决定了后面所有设计。以招生专业目录为例多数学校给的是这样的Excel学院专业代码专业名称研究方向考试科目拟招生人数计算机学院081200计算机科学与技术01 计算机系统结构①101思想政治理论②201英语一③301数学一④408计算机学科专业基础45计算机学院085400电子信息01 计算机技术①101思想政治理论②204英语二③302数学二④408120看着规整实际上坑很多专业名称有的带空格有的不带考试科目挤在一个单元格里用①②③④分隔但不是所有学校都用这个符号有的用1. 2. 3.拟招生人数里可能写45含推免20“也可能写45”还可能写约45合并单元格满天飞学院名只在第一行出现分数线更麻烦。很多学校不提供Excel直接放一张图[复试分数线截图] 专业代码 专业名称 政治 外语 业务课一 业务课二 总分 081200 计算机科学与技术 50 50 75 75 320 085400 电子信息 50 50 75 75 315这种图传统的OCR方案要配模板换个学校就废了。这正是我决定上多模态模型的原因。用Seed-2.1-pro做结构化抽取文本类文件的处理对于Excel和网页表格其实不需要模型pandas和BeautifulSoup就够了。模型的价值在于处理那些半结构化的部分比如把挤在一起的考试科目拆开。我的做法是先用pandas读Excel拿到原始单元格文本然后把需要拆解的字段交给模型。# requirements: pandas2.0, openai1.30importpandasaspdimportjsonfromopenaiimportOpenAI clientOpenAI(api_key你的api_key,base_url你的base_url# 具体地址以你使用的服务为准)defsplit_exam_subjects(raw_text:str)-list:把挤在一起的考试科目拆成列表promptf把下面的考研考试科目拆成JSON数组每个元素包含 科目代码、科目名称两个字段。只输出JSON不要解释。 原文{raw_text}respclient.chat.completions.create(modelseed-2.1-pro,messages[{role:user,content:prompt}],temperature0)contentresp.choices[0].message.content.strip()# 模型有时会包一层json清掉contentcontent.replace(json,).replace(,).strip()returnjson.loads(content)这里有个细节temperature0。结构化抽取任务不需要创造性温度拉满只会让输出不稳定。我测试下来温度0时同一段文本多次调用结果基本一致。【踩坑提醒】模型返回的JSON偶尔会带markdown代码块标记不清理直接json.loads会抛异常。上面那行replace就是干这个的。更稳的做法是用response_format参数但这个参数是否被当前版本支持需要你按自己的服务文档确认我没在这套环境里验证过。多模态读图分数线截图这是我觉得Seed-2.1-pro真正省事的地方。分数线截图直接丢进去让它输出结构化数据。importbase64defparse_score_image(image_path:str)-list:读取分数线截图返回结构化列表withopen(image_path,rb)asf:img_b64base64.b64encode(f.read()).decode()prompt这是一张考研复试分数线表格的截图。 请把表格内容转成JSON数组每个元素包含字段 major_code专业代码、major_name专业名称、 politics政治、foreign_lang外语、 course1业务课一、course2业务课二、total总分。 数字字段只填数字缺失的填null。只输出JSON。respclient.chat.completions.create(modelseed-2.1-pro,messages[{role:user,content:[{type:text,text:prompt},{type:image_url,image_url:{url:fdata:image/png;base64,{img_b64}}}]}],temperature0)contentresp.choices[0].message.content.strip()contentcontent.replace(json,).replace(,).strip()returnjson.loads(content)【注意】上面image_url这种传图方式是OpenAI兼容接口的通用写法多模态消息的字段名在不同服务上可能不一样。我这边跑通了但如果你换服务商字段结构需要重新对照文档。这一点我不建议照抄先确认接口。我拿几张不同学校的分数线截图测试过。表格线条清晰的字段基本能对齐如果截图分辨率低、有压缩噪点专业名称可能出现个别字错。所以我在解析后加了一层校验专业代码必须是6位数字总分必须在200到500之间不满足的标记为待人工核对。这个校验不是可有可无的。模型读图再强也是概率输出数值类字段必须卡范围。中间结构的设计不管数据来自Excel、PDF还是图片解析后都归一成同一个结构。我用Pydantic定义# requirements: pydantic2.0frompydanticimportBaseModel,FieldfromtypingimportOptionalclassMajorRecord(BaseModel):school:strcollege:strmajor_code:strField(patternr^\d{6}$)major_name:stryear:intenroll_count:Optional[int]Nonescore_total:Optional[int]Field(defaultNone,ge200,le500)score_politics:Optional[int]Nonesource_type:str# excel / pdf / image / webneed_review:boolFalsemajor_code用正则卡死6位数字score_total卡在200到500。Pydantic校验失败就自动标记need_reviewTrue不阻断流程但会进人工核对队列。这样做的好处是解析层可以放心激进反正有校验兜底业务层拿到的数据一定是干净的。存储和查询SQLite足够。建一张主表importsqlite3definit_db(pathkaoyan.db):connsqlite3.connect(path)conn.execute( CREATE TABLE IF NOT EXISTS majors ( id INTEGER PRIMARY KEY AUTOINCREMENT, school TEXT NOT NULL, college TEXT, major_code TEXT NOT NULL, major_name TEXT NOT NULL, year INTEGER NOT NULL, enroll_count INTEGER, score_total INTEGER, source_type TEXT, need_review INTEGER DEFAULT 0, UNIQUE(school, major_code, year) ) )conn.execute(CREATE INDEX IF NOT EXISTS idx_code ON majors(major_code))conn.commit()returnconnUNIQUE(school, major_code, year)这个约束很重要。同一所学校同一个专业同一年的数据只能有一条重复导入时用INSERT OR REPLACE覆盖避免脏数据堆积。查询接口用FastAPI# requirements: fastapi0.110, uvicorn0.29fromfastapiimportFastAPI,Queryimportsqlite3 appFastAPI()app.get(/search)defsearch(major_code:strQuery(None),school:strQuery(None),min_score:intQuery(None)):connsqlite3.connect(kaoyan.db)conn.row_factorysqlite3.Row sqlSELECT * FROM majors WHERE need_review 0params[]ifmajor_code:sql AND major_code ?params.append(major_code)ifschool:sql AND school LIKE ?params.append(f%{school}%)ifmin_score:sql AND score_total ?params.append(min_score)rowsconn.execute(sql,params).fetchall()conn.close()return[dict(r)forrinrows]注意查询里带了need_review 0也就是默认只返回通过校验的数据。需要核对的数据单独走另一个接口避免污染正常查询结果。实际跑起来的效果我用几所学校的公开数据做了测试两所学校的招生目录Excel一所学校的分数线截图一份网页表格。结果大致是Excel和网页表格字段抽取基本准确考试科目拆分没有出错分数线截图清晰截图下数值全部正确有一张手机拍的、略有反光的图专业名称错了一个字被正则校验拦下来了这里我要说清楚这只是一次小样本测试不代表模型在所有学校数据上的准确率。不同学校的表格样式差异极大样本量也小不能拿这个结果去推断整体表现。真实上线前必须针对目标数据源做足够规模的抽样验证。【关键结论】模型在这套流程里扮演的是把非结构化变结构化的角色不是保证正确。数值和代码类字段一定要加规则校验把模型的概率输出和确定性校验结合起来产品才敢用。几个设计上的取舍为什么不用纯OCR传统OCR比如PaddleOCR识别文字没问题但表格结构还原要额外做版面分析而且每种表格样式都要调。多模态模型直接输出JSON省掉了版面分析这一层。代价是每次调用有成本且结果是概率性的。我的判断是数据量不大的场景几千条以内多模态模型更省事数据量巨大且格式固定传统OCR加模板反而更可控。为什么解析和业务分离前面提过核心原因是数据源会不断新增。分离之后加一所学校只是加一个handler不动主流程。关于模型版本Seed-2.1-pro的处理能力是我这边实测的但模型版本更新快接口字段、支持的图片格式、上下文长度这些都可能变。文章里的代码是逻辑框架具体参数请对照你使用时的官方文档。我不建议把文章里的接口细节当成固定契约。后续可以做的现在这个平台能跑通导入→解析→校验→存储→查询的闭环但还有几个方向没做增量更新官方数据每年变需要设计版本机制保留历史年份对比专业代码的标准化不同学校的专业代码体系不完全一致跨校对比需要映射表人工核对界面need_review的数据目前只能看数据库做一个简单的标注页面会更实用如果你也在做类似的数据抽取类应用我的建议是先把校验规则想清楚再谈模型能力。模型是拿来提效的不是拿来兜底的。把边界卡住产品才站得住。
返回列表