
简介面向备考山东选调生的应届毕业生和基层岗位意向考生这份2007—2011年山东选调生考试真题集收录五年行测与申论回忆整理试题。行测部分涵盖常识判断、数学运算、图形推理、逻辑推理、定义判断、言语理解与资料分析常识题侧重乡镇政府职能定位、企业社会责任、微博问政、经济发展方式转变等贴近基层的热点申论记录了2011年首次分设A、B卷的命题变化A卷讨论互联网时代社会服务与远程教育平台在基层的应用B卷以《让青春在基层闪光》考查对基层工作的认识与态度。压缩包共1个PDF文件约517KB体积小巧便于打印、标注和移动端翻阅。现已有108人学习。通过真题可直观了解题量分布、命题侧重与答题节奏结合解析复盘错题、补齐知识盲区为选调生笔试复习和基层工作认知提供实用参考。1. 一份 2007-2011 山东选调生考试真题.pdf 最难的地方不是看是查手上拿到 2007-2011 山东选调生考试真题.pdf 这类资料多数人的第一反应是直接 CtrlF 搜数量关系常识判断然后发现搜不到任何东西——页面明明有字检索框却一片空白。这不是阅读器的问题是这份 PDF 很可能根本没有文本层它是复印机或扫描仪产出的图像页每个字在文件里其实是一堆像素点。真正的痛点在于跨五个年度的合集里想找 2009 年某道工程问题、想统计五年里重复考过的常识点、想把错题按题型归档靠肉眼翻页是不现实的。这篇讲的是把这份真题集从能看不能搜变成能按年份、题型、关键词秒查的完整链路先给 PDF 做文本层体检再决定走 PyMuPDF 直抽还是走 OCR识别完按题号、选项切分成结构化的题目记录入 SQLite 建 FTS5 全文索引最后用 SimHash 处理跨年份重复题和 OCR 噪声自检。适合手上有老真题 PDF、想做自用题库或考点统计的人也适合正在踩 PDF 解析坑的后端和数据处理同学。2. 先给 2007-2011 山东选调生考试真题.pdf 做文本层体检再决定用 PyMuPDF 还是走 OCR2.1 老真题 PDF 的三种形态与对应提取路线山东选调生考试真题这五年的合集来源往往很杂有的是当年排版文件导出的原生素文本 PDF有的是后来扫描重制的图像 PDF还有一种是两者混在一份文件里——前几页目录是文本正文全是图。三条路线的时间成本差一个数量级所以第一步永远是分型而不是直接上 OCR 或直接上 pdfplumber。PDF 形态快速判断特征提取路线单本耗时量级原生文本层pdffonts有嵌入字体输出单页字符数 800PyMuPDF / pdfplumber 直接抽取分钟级纯扫描图像pdffonts无数据行单页字符数 ≈ 0300dpi 渲染 OCR小时级混合卷部分页有字符、部分页为 0逐页分流两套流程各跑一遍视扫描页占比伪文本层OCR 过一遍的有字符但全是乱码或全角符号错位按扫描处理重做 OCR小时级第四种最容易被误判。有些文件被低质量 OCR 工具处理过看着有文本层实际识别结果里选调生变成了选诟生这种文本层比没有还危险因为流程会跳过 OCR 直接入库错误被永久固化。2.2 用 pdfinfo 和 pdffonts 十秒判定有没有文本层poppler-utils 里的这两个命令是最快的分型工具比写脚本还快# 1) 基本信息页数、页面尺寸、是否带加密标志 pdfinfo 2007-2011山东选调生考试真题.pdf # 2) 关键一步看有没有嵌入字体 pdffonts 2007-2011山东选调生考试真题.pdf | head -30pdffonts的输出列里emb为 yes 表示字体已嵌入uni为 yes 表示带 Unicode 映射。只要这行命令连一条数据行都不输出就可以直接判定整本是图像页后面所有工作按扫描件处理。反过来如果输出了十几行字体还要留意uni列——uni为 no 的老字体编码抽出来的文本可能是乱码这种情况需要按第 3 章的 OCR 流程重做或者用 PyMuPDF 的get_text配flags参数尝试修复。注意pdfinfo报告页数后顺手看一眼页面尺寸是否一致。五年真题合订本常见 A4 与 B5 混排后面渲染成图时如果不统一 dpiOCR 的行高阈值会失效。2.3 PyMuPDF 逐页探测文本密度产出待 OCR 页清单单看整本的pdffonts只能判断大致混合卷必须逐页看。下面这段脚本是每次开工都会先跑一遍的探针import fitz # PyMuPDF PDF 2007-2011山东选调生考试真题.pdf doc fitz.open(PDF) text_pages, scan_pages [], [] for page in doc: # sortTrue 按版面阅读顺序输出双栏排版不至于整列串行 raw page.get_text(text, sortTrue) n len(raw.strip()) if n 50: # 阈值单页有效字符数 text_pages.append(page.number) else: scan_pages.append(page.number) print(有文本层页数:, len(text_pages)) print(需 OCR 页数 :, len(scan_pages), scan_pages[:20])逻辑说明阈值取 50 而不是 0是因为扫描页里经常残留页码、页眉这类被单独识别过的文本对象字符数在个位数到二十几之间浮动真正有正文的一页字符数通常在 800 以上。参数上get_text的 flavor 有四个取值text是纯文本流blocks带矩形坐标适合做版面分析words到词粒度dict是完整结构。做密度探测用text就够需要判断哪一行是通栏标题时换blocks。2.4 双栏真题用 pdfplumber 裁切分栏避免左右串行纸质真题卷基本是双栏排版PyMuPDF 的sortTrue能处理大部分情况但对齐不齐的扫描重排本仍会串行。这时用 pdfplumber 按坐标裁两半更稳import pdfplumber with pdfplumber.open(PDF) as pdf: page pdf.pages[0] w, h page.width, page.height # 0.48~0.52 之间留重叠带防止边界字符被切掉 left page.crop((0, 0, w * 0.52, h)).extract_text(x_tolerance2, y_tolerance3) right page.crop((w * 0.48, 0, w, h)).extract_text(x_tolerance2, y_tolerance3) print(left or ) print( * 20) print(right or )参数说明x_tolerance控制同一行内字符被判定为相邻的最大水平间距默认 3调大容易把两栏内容粘成一行调小会把一行断成多行y_tolerance控制换行判定默认 3中文行距紧的扫描件可以降到 2。裁切比例不要用精确的 0.5真题卷的栏间距常常偏向一侧0.48 / 0.52 的重叠带是实践里最省事的做法重复的字符在后续切分阶段按行去重即可。3. 扫描页走 OCR把 2007-2011 山东选调生考试真题.pdf 渲染成 300dpi 灰度图再识别3.1 渲染参数怎么定dpi、色彩空间与预处理取舍OCR 的上限由输入图像决定渲染这一步便宜且可控值得调好。200dpi 对小五号中文题干的识别率明显下滑400dpi 索引体积和耗时又翻倍300dpi 是通用甜点import fitz doc fitz.open(PDF) for pno in scan_pages: page doc[pno] # dpi300 是中文小字号的通用起点灰度比彩色省内存也不影响识别 pix page.get_pixmap(dpi300, colorspacefitz.csGRAY) pix.save(fimg/{pno:04d}.png)关于二值化实践中的反直觉结论是老真题卷底灰重、有装订线阴影时直接喂灰度图给 OCR 引擎效果通常好于先做阈值二值化。原因是二值化会把笔画细的汉字比如已/己/巳这类的连通性破坏掉OCR 的检测模型反而找不到文本框。只有在扫描噪点极其严重盐椒噪声时才用 OpenCV 的fastNlMeansDenoising先降噪再识别不要用全局固定阈值。另一种常见的预处理是去水印和去装订线。若真题集每页带半透明内部资料水印它会被识别成散落的单字混进题干。处理办法是在渲染后对水印所在的固定区域做page.add_redact_annot后再渲染或者直接在切分阶段用正则把这些噪声词过滤掉——后者更安全因为红action区域一旦框错就把题干擦掉了。3.2 PaddleOCR 批量识别并把版面坐标一起留下只拿文本行、丢掉坐标是后面所有版面还原失败的根本原因。识别函数要连cx / cy一起返回from paddleocr import PaddleOCR # use_angle_cls 处理横竖混排langch 用于简体中文 ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) def page_lines(img_path): res ocr.ocr(img_path, clsTrue)[0] or [] items [] for box, (txt, score) in res: xs [p[0] for p in box] ys [p[1] for p in box] items.append({ cx: sum(xs) / 4, # 文本框中心 x用于分栏 cy: sum(ys) / 4, # 文本框中心 y用于同行排序 w: max(xs) - min(xs), # 框宽用于判断通栏标题 text: txt.strip(), score: score, }) return items参数说明use_angle_clsTrue会额外跑一次方向分类代价是耗时增加约两成但真题卷里有竖排的答题说明不开会识别成乱码。score是置信度低于 0.6 的行要打标记后面写进raw_text供人工回看原图。w这个宽度字段容易被忽略但它决定了下一步能不能识别出通栏标题。3.3 用坐标把打乱的双栏还原成单栏阅读顺序OCR 引擎返回的顺序是按检测框位置排的双栏页里会左右横跳。还原逻辑是先分栏、再按 y 排序def reflow(items, page_w): mid page_w / 2 # 宽度超过页宽 60% 的框视为跨栏标题单独提到最前 banners [i for i in items if i[w] page_w * 0.6] body [i for i in items if i[w] page_w * 0.6] left sorted([i for i in body if i[cx] mid], keylambda i: i[cy]) right sorted([i for i in body if i[cx] mid], keylambda i: i[cy]) return banners left right这里的关键判断是通栏标题。如果只做左右分栏第一部分 言语理解与表达共 30 题这类标题会被判进左栏导致它出现在左栏第一题之后切分时被当成题干的一部分。加上宽度过滤后标题回到正确位置也顺便成了卷面模块的天然分隔符后面识别言语理解 / 数量关系模块归属时可以复用它。3.4 题号与选项的切分正则以及三个误判陷阱拿到有序的行列表后切题靠正则import re # 覆盖 1. 1、 1 三种真题卷常见编号 Q_RE re.compile(r^\s*(\d{1,3})\s*[\.、]\s*) # 选项标记A. A、 A OPT_RE re.compile(r^\s*([A-Da-d])\s*[\.、]\s*) # 页眉页脚噪声 NOISE re.compile(r(第\s*\d\s*页|共\s*\d\s*页|准考证|绝密|内部资料)) def split_questions(lines): questions, cur [], None for ln in lines: ln NOISE.sub(, ln[text]).strip() if not ln: continue m Q_RE.match(ln) # 题号后必须跟足够长的正文排除 2007. 这种年份干扰 if m and len(ln) 6 and not OPT_RE.match(ln): cur {qno: int(m.group(1)), body: [Q_RE.sub(, ln)], opt: []} questions.append(cur) continue if cur is None: # 卷首说明、考试须知直接丢弃 continue if OPT_RE.match(ln): cur[opt].append(ln) else: cur[body].append(ln) return questions逻辑说明与三个坑第一题干里出现年份2007.1.1或小数3.5 倍时Q_RE会误判所以加了两道闸——长度必须大于 6 个字符且不能同时匹配选项正则。第二题号必须单调递增校验切完一卷后检查qno序列如果出现 17 之后回到 3说明这是新模块重新编号或者页眉残留被当成了题号。第三选项标记只在题干已开启的情况下生效否则卷首的A 类职位会被切成选项。4. 结构化入库SQLite 表设计加 FTS5让2009 年数量关系一句话查出来4.1 四张表之外的取舍为什么答案要和题目分表考虑真题集里答案往往单独排在卷末或者根本没收。设计上把answer、analysis直接挂在question表里但要允许为空同时保留raw_text字段存原始 OCR 文本。原因是一旦切分逻辑出错raw_text是唯一能回溯的证据重建成本远低于重跑 OCR。CREATE TABLE paper ( id INTEGER PRIMARY KEY, year INTEGER NOT NULL, -- 2007..2011 subject TEXT NOT NULL, -- 行测 / 申论 / 综合知识 module TEXT, -- 言语理解、数量关系、判断推理... src_file TEXT, page_from INTEGER, page_to INTEGER, UNIQUE(year, subject) ); CREATE TABLE question ( id INTEGER PRIMARY KEY, paper_id INTEGER REFERENCES paper(id), qno INTEGER NOT NULL, -- 卷内题号 qtype TEXT, -- 单选 / 多选 / 判断 / 简答 stem TEXT NOT NULL, -- 题干已合并多行 options TEXT, -- JSON 数组[A ...,B ...,C ...,D ...] answer TEXT, analysis TEXT, raw_text TEXT, -- 未清洗原文含 OCR 置信度标记 UNIQUE(paper_id, qno) ); CREATE INDEX idx_q_paper ON question(paper_id); CREATE INDEX idx_q_type ON question(qtype);UNIQUE(paper_id, qno)这个约束是有意加的重跑入库脚本时它能直接暴露切分重复的问题比事后写 SQL 去查重复题号早发现得多。4.2 中文全文检索的三种方案选 FTS5 的哪一种分词器SQLite 的 FTS5 内置分词器对中文并不友好三种可行路线差别明显方案建表关键写法适用场景明显局限unicode61tokenizeunicode61英文混排、纯前缀匹配中文整句被当成一个 token只能前缀命中trigramtokenizetrigram想支持任意子串模糊查询索引体积约为原文 3 倍两字词检索弱预分词 unicode61写入前用 jieba 切词加空格词粒度精确、可接同义词表需要维护分词器新词与专有名词易切错真题检索的真实需求是找包含工程问题的题和找提到行政法的题都是词粒度所以选第三条原文照旧存在question.stem另建一张 FTS 表存 jieba 切好的索引文本。这样原文不乱检索质量也可控。CREATE VIRTUAL TABLE question_fts USING fts5( stem_idx, options_idx, tokenizeunicode61 );rowid不单独建列直接复用question.id查询时用JOIN question q ON q.id f.rowid关联回去。4.3 入库脚本切分结果到 SQLite 的完整写入import sqlite3, json, jieba def seg(text): 搜索引擎模式切词比精确模式召回更高 return .join(t for t in jieba.cut_for_search(text) if t.strip()) con sqlite3.connect(sd_xd_zt.db) cur con.cursor() cur.execute(INSERT OR IGNORE INTO paper(year, subject, module, src_file, page_from, page_to) VALUES (?,?,?,?,?,?), (2009, 行测, 数量关系, PDF, page_from, page_to)) paper_id cur.lastrowid or cur.execute( SELECT id FROM paper WHERE year? AND subject?, (2009, 行测)).fetchone()[0] q_rows, f_rows [], [] for q in questions: stem .join(q[body]) opts json.dumps(q[opt], ensure_asciiFalse) q_rows.append((paper_id, q[qno], 单选, stem, opts, None, None, raw)) f_rows.append((stem, opts)) cur.executemany(INSERT OR REPLACE INTO question (paper_id, qno, qtype, stem, options, answer, analysis, raw_text) VALUES (?,?,?,?,?,?,?,?), q_rows) # FTS 表用 rowid 对齐 question.id last_id cur.execute(SELECT MAX(id) FROM question).fetchone()[0] start last_id - len(f_rows) 1 cur.executemany(INSERT INTO question_fts(rowid, stem_idx, options_idx) VALUES (?,?,?), [(start i, seg(s), seg(o)) for i, (s, o) in enumerate(f_rows)]) con.commit() con.close() print(写入题目数:, len(q_rows))逻辑说明INSERT OR IGNORE用于 paper 表的幂等重复跑同一年的卷子不会插两行question 表用INSERT OR REPLACE配合唯一约束重跑时按题号覆盖旧记录而不是堆积。FTS 表的 rowid 必须与 question.id 严格对齐所以用最大 id 倒推起始值的方式回填这是最容易写错的一处——如果中间有题目被OR IGNORE丢掉rowid 就会整体错位检索结果会张冠李戴。稳妥做法是插入后读回每行的真实 id再逐条写 FTS。4.4 三个能立刻用上的查询以及 MATCH 语法的坑-- 1) 2009 年数量关系里同时含工程和效率的题 SELECT p.year, p.module, q.qno, substr(q.stem, 1, 60) FROM question_fts f JOIN question q ON q.id f.rowid JOIN paper p ON p.id q.paper_id WHERE question_fts MATCH 工程 AND 效率 AND p.year 2009 ORDER BY q.qno; -- 2) 五年里所有提到行政处罚的选择题 SELECT p.year, q.qno, q.answer FROM question_fts f JOIN question q ON q.id f.rowid JOIN paper p ON p.id q.paper_id WHERE question_fts MATCH 行政处罚 ORDER BY p.year, q.qno; -- 3) 前缀检索查所有以行政开头的词 SELECT rowid FROM question_fts WHERE question_fts MATCH 行政*;提示FTS5 的MATCH里AND/OR/NOT必须大写小写会被当作普通检索词连字符、引号要转义否则语法报错。验证索引有没有生效用EXPLAIN QUERY PLAN看执行计划里出现的是VIRTUAL TABLE INDEX还是全表扫描EXPLAIN QUERY PLAN SELECT rowid FROM question_fts WHERE question_fts MATCH 言语理解;5. 进阶跨年份重复题检测与 OCR 结果自检5.1 用 SimHash 找出五年里重复出现的题真题卷的重复率比想象中高尤其是常识判断和言语理解模块。逐字比对不管用因为 OCR 噪声会让同一道题在两年里长得不一样。SimHash 对局部改动不敏感正合适import hashlib, re def _h(tok): return int(hashlib.md5(tok.encode()).hexdigest()[:16], 16) def simhash(text, bits64): norm re.sub(r\s, , text) v [0] * bits # 3-gram 滑窗避免下列正确这类高频词主导指纹 for i in range(len(norm) - 2): hv _h(norm[i:i 3]) for b in range(bits): v[b] 1 if (hv b) 1 else -1 out 0 for b in range(bits): if v[b] 0: out | 1 b return out def hamming(a, b): return bin(a ^ b).count(1)汉明距离阈值取 3 适合文本干净的场景这份真题集有 OCR 噪声实践里放宽到 3~6宁可多召回几条人工确认。用 3-gram 而不是单字是因为单字会把下列各句中没有语病的一项是这类模板句算成高度相似导致误报率飙升。比对时按年份分组只比对不同年份之间的题同年内部的重复更可能是切分 bug 而不是真题重复。5.2 入库后必跑的两条校验 SQL-- 1) 选项少于 3 个的选择题切分失败的重灾区 SELECT p.year, q.qno, length(q.options) FROM question q JOIN paper p ON p.id q.paper_id WHERE q.qtype 单选 AND (q.options IS NULL OR json_array_length(q.options) 3); -- 2) 题号断档检测数量远小于最大题号说明有题目漏切 SELECT p.year, p.module, COUNT(*) AS cnt, MAX(q.qno) AS maxno FROM question q JOIN paper p ON p.id q.paper_id GROUP BY p.year, p.module HAVING COUNT(*) MAX(q.qno) - 3;第二条的思路是拿实际题数和最大题号做差。一卷 30 道题全切到两者应该接近如果差出 5 以上说明中间整段丢失通常对应的就是某个跨页的题干——OCR 把跨页题切成了两条前一条只有题号没正文。5.3 一个具体技巧把 OCR 置信度写回 raw_text 做定点复核低置信行不该直接丢也不该无差别人工看。入库时把它们写成行内标记复核时一条 SQL 就能捞出来raw \n.join(f{{}s:.2f{}}|{t} if s 0.6 else t for t, s in lines)复核查询直接定位到具体年份和页码SELECT p.year, p.src_file, q.qno, q.raw_text FROM question q JOIN paper p ON p.id q.paper_id WHERE q.raw_text LIKE %0.5%|% OR q.raw_text LIKE %0.4%|%;拿到结果后按src_file加题号回到第 3 章渲染出的img/{{}pno{}}.png原图上核对改完只更新stem字段不必重跑整本 OCR。这一步做完五年的题量分布、模块占比、跨年重复率才真正可信。本文还有配套的精品资源点击获取