jiangsu-kaogong 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -0
- package/analyze.py +487 -0
- package/analyze_final.py +767 -0
- package/analyze_v2.py +618 -0
- package/bin/index.js +747 -0
- package/data/cutoffs.json +1 -9690
- package/data/cutoffs.pdf +0 -0
- package/data/cutoffs.txt +254 -0
- package/data/example-positions.json +92 -0
- package/data/filtered.json +56 -0
- package/data/interview_data_sample.json +0 -0
- package/data/interviews.pdf +0 -0
- package/data/matched.json +56 -0
- package/data/positions.json +13146 -15891
- package/data/positions.xls +0 -0
- package/docs//344/275/277/347/224/250/347/244/272/344/276/213.md +189 -0
- package/package.json +27 -19
- package/reports/2026/346/261/237/350/213/217/351/200/211/345/262/227/347/273/274/345/220/210/347/261/273-/345/274/240/344/270/211-/350/241/214/346/265/213130.md +68 -0
- package/reports/2026/347/233/220/345/237/216/351/200/211/345/262/227/345/210/206/346/236/220/346/212/245/345/221/212.md +131 -0
- package/reports/2026/347/233/220/345/237/216/351/200/211/345/262/227/345/210/206/346/236/220/346/212/245/345/221/212_v2.md +488 -0
- package/reports/2026/347/233/220/345/237/216/351/200/211/345/262/227/345/210/206/346/236/220/346/212/245/345/221/212_/346/234/200/347/273/210/347/211/210.md +575 -0
- package/src/filterPositions.js +114 -0
- package/src/generateReport.js +201 -0
- package/src/matchData.js +79 -0
- package/src/memory.js +63 -0
- package/src/parseCutoffs.js +86 -0
- package/src/parseInterviews.js +116 -0
- package/src/parsePositions.js +74 -0
- package/data/interviews.json +0 -8998
- package/dist/index.d.mts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -841
- package/dist/index.mjs +0 -816
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# jiangsu-kaogong
|
|
2
|
+
|
|
3
|
+
江苏省考选岗CLI工具 - 解析职位表、进面分数线,智能推荐冲/稳/保三档岗位
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- ✅ 解析江苏省考职位表(Excel格式)
|
|
8
|
+
- ✅ 解析进面分数线PDF
|
|
9
|
+
- ✅ 解析体检名单,获取第1名面试成绩
|
|
10
|
+
- ✅ 按专业、学历、政治面貌等条件筛选可报岗位
|
|
11
|
+
- ✅ 智能分档:冲(0~5分)、稳(5~10分)、保(10分+)
|
|
12
|
+
- ✅ 生成完整Markdown分析报告
|
|
13
|
+
- ✅ 逐岗位差距分析与上岸概率判断
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g jiangsu-kaogong
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
### 1. 解析职位表
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
js-kaogong parse-positions -f 2026江苏公务员职位表.xlsx
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. 筛选可报岗位
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
js-kaogong filter \
|
|
33
|
+
-e 本科 \
|
|
34
|
+
-m 030201 \
|
|
35
|
+
-p 群众 \
|
|
36
|
+
-c A \
|
|
37
|
+
--city 盐城
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. 一键生成分析报告
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
js-kaogong apply \
|
|
44
|
+
-e 本科 \
|
|
45
|
+
-m 030201 \
|
|
46
|
+
-p 群众 \
|
|
47
|
+
-c A \
|
|
48
|
+
-s 132 \
|
|
49
|
+
-n 乔治
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 完整命令列表
|
|
53
|
+
|
|
54
|
+
### parse-positions - 解析职位表
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
js-kaogong parse-positions -f <excel文件路径> [-o 输出路径]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### parse-cutoffs - 解析进面分数线
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
js-kaogong parse-cutoffs -f <pdf文件路径> [-o 输出路径]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### filter - 筛选可报岗位
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
js-kaogong filter [选项]
|
|
70
|
+
|
|
71
|
+
选项:
|
|
72
|
+
-e, --education <level> 学历:本科/研究生
|
|
73
|
+
-m, --major <code> 专业代码,如030201
|
|
74
|
+
-p, --political <status> 政治面貌:群众/党员
|
|
75
|
+
-c, --category <type> 考试类别:A/B/C
|
|
76
|
+
--city <name> 意向城市
|
|
77
|
+
--is-grad 是否应届毕业生
|
|
78
|
+
-o, --output <path> 输出JSON路径
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### apply - 一键执行完整分析
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
js-kaogong apply [选项]
|
|
85
|
+
|
|
86
|
+
选项:
|
|
87
|
+
-e, --education <level> 学历
|
|
88
|
+
-m, --major <code> 专业代码
|
|
89
|
+
-p, --political <status> 政治面貌
|
|
90
|
+
-c, --category <type> 考试类别
|
|
91
|
+
-s, --score <number> 模考总分
|
|
92
|
+
-n, --name <string> 考生姓名
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 江苏省考专业目录参考
|
|
96
|
+
|
|
97
|
+
| 专业代码 | 专业名称 | 所属类别 |
|
|
98
|
+
|---------|---------|---------|
|
|
99
|
+
| 030201 | 政治学与行政学 | 社会政治类 |
|
|
100
|
+
| 0301 | 法学 | 法律类 |
|
|
101
|
+
| 0809 | 计算机科学与技术 | 计算机类 |
|
|
102
|
+
| 1202 | 工商管理 | 工商管理类 |
|
|
103
|
+
| 0201 | 经济学 | 经济类 |
|
|
104
|
+
| 0501 | 汉语言文学 | 中文文秘类 |
|
|
105
|
+
|
|
106
|
+
## 分数计算规则
|
|
107
|
+
|
|
108
|
+
### 普通岗位(A/B/C类)
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
笔试合成 = (行测 + 申论) / 2
|
|
112
|
+
综合总分 = 笔试合成 × 50% + 面试 × 50%
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 公安岗位(B类)
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
综合总分 = 行测 × 40% + 申论 × 30% + 公安专业 × 30%
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 分档标准
|
|
122
|
+
|
|
123
|
+
| 档位 | 分差范围 | 说明 |
|
|
124
|
+
|------|---------|------|
|
|
125
|
+
| 冲 | 0~5分 | 笔试分差5分以内,面试需要逆袭 |
|
|
126
|
+
| 稳 | 5~10分 | 笔试有明显优势,正常发挥即可 |
|
|
127
|
+
| 保 | 10分+ | 笔试优势巨大,进面基本稳 |
|
|
128
|
+
|
|
129
|
+
## 数据来源说明
|
|
130
|
+
|
|
131
|
+
1. **职位表**:江苏省公务员局官网下载的Excel文件
|
|
132
|
+
2. **进面分数线**:各市发布的资格复审名单PDF
|
|
133
|
+
3. **面试/体检名单**:各市发布的体检名单PDF,仅包含综合第1名
|
|
134
|
+
|
|
135
|
+
## 注意事项
|
|
136
|
+
|
|
137
|
+
⚠️ **专业匹配**:江苏省考有自己的专业参考目录,本工具内置了常用类别,最终请以官方发布的专业目录为准
|
|
138
|
+
|
|
139
|
+
⚠️ **公安岗**:公安岗位分数计算方式不同,本工具默认按普通岗位计算
|
|
140
|
+
|
|
141
|
+
⚠️ **户籍限制**:部分岗位限本市户籍,筛选时请注意查看备注
|
|
142
|
+
|
|
143
|
+
## 项目结构
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
jiangsu-kaogong/
|
|
147
|
+
├── bin/
|
|
148
|
+
│ └── index.js # CLI入口
|
|
149
|
+
├── src/
|
|
150
|
+
│ ├── parser.js # 数据解析
|
|
151
|
+
│ ├── filter.js # 筛选逻辑
|
|
152
|
+
│ ├── matcher.js # 数据匹配
|
|
153
|
+
│ └── reporter.js # 报告生成
|
|
154
|
+
├── data/ # 数据文件
|
|
155
|
+
├── reports/ # 生成的报告
|
|
156
|
+
├── package.json
|
|
157
|
+
└── README.md
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 作者
|
|
161
|
+
|
|
162
|
+
George <george@example.com>
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT
|
|
167
|
+
|
|
168
|
+
## GitHub
|
|
169
|
+
|
|
170
|
+
https://github.com/george92/jiangsu-kaogong
|
package/analyze.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import pdfplumber
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import xlrd
|
|
7
|
+
|
|
8
|
+
print("=" * 60)
|
|
9
|
+
print("📊 江苏省考选岗分析系统")
|
|
10
|
+
print("=" * 60)
|
|
11
|
+
|
|
12
|
+
# ========== 1. 解析职位表 ==========
|
|
13
|
+
print("\n[1/4] 解析职位表...")
|
|
14
|
+
|
|
15
|
+
wb = xlrd.open_workbook('./data/positions.xls')
|
|
16
|
+
ws = wb.sheet_by_index(0)
|
|
17
|
+
|
|
18
|
+
positions = []
|
|
19
|
+
headers = []
|
|
20
|
+
|
|
21
|
+
# 第2行(索引2)才是真正的表头
|
|
22
|
+
for row_idx in range(ws.nrows):
|
|
23
|
+
row = ws.row_values(row_idx)
|
|
24
|
+
if row_idx == 2: # 第3行是表头
|
|
25
|
+
headers = [str(h) if h else '' for h in row]
|
|
26
|
+
# 清理字段名中的特殊空格
|
|
27
|
+
headers = [h.replace('\u3000', '').replace(' ', '') for h in headers]
|
|
28
|
+
print(f" 表头字段: {len(headers)} 个")
|
|
29
|
+
print(f" 主要字段: {headers}")
|
|
30
|
+
elif row_idx > 2: # 从第4行开始是数据
|
|
31
|
+
pos = dict(zip(headers, row))
|
|
32
|
+
# 转换为字符串并清理
|
|
33
|
+
pos = {k: str(v).strip() if v else '' for k, v in pos.items()}
|
|
34
|
+
if pos.get('单位名称'):
|
|
35
|
+
positions.append(pos)
|
|
36
|
+
|
|
37
|
+
print(f" ✅ 共 {len(positions)} 个岗位")
|
|
38
|
+
|
|
39
|
+
# 打印前2个岗位看看字段
|
|
40
|
+
if positions:
|
|
41
|
+
print(" 示例岗位字段:")
|
|
42
|
+
for k, v in list(positions[0].items())[:8]:
|
|
43
|
+
if v:
|
|
44
|
+
print(f" {k}: {v[:30]}")
|
|
45
|
+
|
|
46
|
+
# ========== 2. 解析体检名单(含分数线) ==========
|
|
47
|
+
print("\n[2/4] 解析体检名单...")
|
|
48
|
+
|
|
49
|
+
interview_data = []
|
|
50
|
+
|
|
51
|
+
with pdfplumber.open('./data/interviews.pdf') as pdf:
|
|
52
|
+
print(f" PDF共 {len(pdf.pages)} 页")
|
|
53
|
+
|
|
54
|
+
for page in pdf.pages:
|
|
55
|
+
text = page.extract_text()
|
|
56
|
+
if not text:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
lines = text.split('\n')
|
|
60
|
+
|
|
61
|
+
for line in lines:
|
|
62
|
+
# 匹配数据行格式: 序号 地区代码 地区名称 单位代码 单位名称...
|
|
63
|
+
# 例如: "1 090000 盐城市 501 盐城市-市纪委监委 60 监督检查室..."
|
|
64
|
+
|
|
65
|
+
# 提取数字模式
|
|
66
|
+
numbers = re.findall(r'\d+\.?\d*', line)
|
|
67
|
+
if len(numbers) >= 8:
|
|
68
|
+
# 查找分数(小数)
|
|
69
|
+
scores = [float(n) for n in numbers if '.' in n]
|
|
70
|
+
int_nums = [int(float(n)) for n in numbers if '.' not in n]
|
|
71
|
+
|
|
72
|
+
if len(scores) >= 3: # 至少有笔试、面试、总分
|
|
73
|
+
# 从行中提取单位和职位名称
|
|
74
|
+
parts = line.split()
|
|
75
|
+
dept_name = ''
|
|
76
|
+
pos_name = ''
|
|
77
|
+
|
|
78
|
+
# 查找包含单位名称的部分
|
|
79
|
+
for i, part in enumerate(parts):
|
|
80
|
+
if '市' in part and ('局' in part or '委' in part or '办' in part or '院' in part):
|
|
81
|
+
dept_name = part
|
|
82
|
+
# 后面的可能是职位名称
|
|
83
|
+
if i + 1 < len(parts):
|
|
84
|
+
pos_candidate = parts[i + 1]
|
|
85
|
+
if not re.match(r'^\d', pos_candidate):
|
|
86
|
+
pos_name = pos_candidate
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
if dept_name:
|
|
90
|
+
data = {
|
|
91
|
+
'raw_line': line,
|
|
92
|
+
'dept_name': dept_name,
|
|
93
|
+
'position_name': pos_name,
|
|
94
|
+
'written_score': scores[-3], # 笔试合成
|
|
95
|
+
'interview_score': scores[-2], # 面试
|
|
96
|
+
'total_score': scores[-1], # 合成总分
|
|
97
|
+
'rank': int_nums[-1] if int_nums else 1
|
|
98
|
+
}
|
|
99
|
+
interview_data.append(data)
|
|
100
|
+
|
|
101
|
+
print(f" ✅ 提取到 {len(interview_data)} 条面试数据")
|
|
102
|
+
|
|
103
|
+
# 计算各岗位进面分数线(体检名单中第1名的笔试成绩作为参考)
|
|
104
|
+
cutoff_by_dept = {}
|
|
105
|
+
for data in interview_data:
|
|
106
|
+
dept = data['dept_name']
|
|
107
|
+
if dept not in cutoff_by_dept:
|
|
108
|
+
cutoff_by_dept[dept] = []
|
|
109
|
+
cutoff_by_dept[dept].append(data['written_score'])
|
|
110
|
+
|
|
111
|
+
# 取每个部门的最低分作为进面线
|
|
112
|
+
for dept in cutoff_by_dept:
|
|
113
|
+
cutoff_by_dept[dept] = min(cutoff_by_dept[dept])
|
|
114
|
+
|
|
115
|
+
print(f" ✅ 统计到 {len(cutoff_by_dept)} 个部门的分数线")
|
|
116
|
+
|
|
117
|
+
# ========== 3. 筛选可报岗位 ==========
|
|
118
|
+
print("\n[3/4] 筛选可报岗位...")
|
|
119
|
+
|
|
120
|
+
# 考生信息
|
|
121
|
+
user_profile = {
|
|
122
|
+
'education': '本科',
|
|
123
|
+
'major_code': '030201',
|
|
124
|
+
'major_name': '政治学与行政学',
|
|
125
|
+
'political': '群众',
|
|
126
|
+
'category': 'A',
|
|
127
|
+
'city': '盐城',
|
|
128
|
+
'has_grassroots': True,
|
|
129
|
+
'is_grad': False,
|
|
130
|
+
'total_score': 132, # 68 + 64
|
|
131
|
+
'written_score': 132,
|
|
132
|
+
'interview_history': 74
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
print(f" 考生信息: {user_profile['major_name']}({user_profile['major_code']})")
|
|
136
|
+
print(f" 学历: {user_profile['education']}, 政治面貌: {user_profile['political']}")
|
|
137
|
+
print(f" 笔试成绩: {user_profile['total_score']}分, 面试经验: {user_profile['interview_history']}分")
|
|
138
|
+
|
|
139
|
+
# 江苏省考专业目录
|
|
140
|
+
major_categories = {
|
|
141
|
+
'030201': ['社会政治类', '政治类'],
|
|
142
|
+
'0301': ['法律类', '法学类'],
|
|
143
|
+
'0809': ['计算机类'],
|
|
144
|
+
'1202': ['工商管理类'],
|
|
145
|
+
'0201': ['经济类'],
|
|
146
|
+
'0501': ['中文文秘类', '汉语言文学'],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
user_major_categories = major_categories.get(user_profile['major_code'], [])
|
|
150
|
+
print(f" 专业类别: {user_major_categories}")
|
|
151
|
+
|
|
152
|
+
def is_qualified(pos):
|
|
153
|
+
"""判断是否符合报考条件"""
|
|
154
|
+
|
|
155
|
+
# 1. 学历检查
|
|
156
|
+
edu = pos.get('学历', '')
|
|
157
|
+
if edu:
|
|
158
|
+
if '研究生' in edu and user_profile['education'] != '研究生':
|
|
159
|
+
# 本科及以上可以报
|
|
160
|
+
if '本科及以上' not in edu:
|
|
161
|
+
return False, f'学历要求{edu}'
|
|
162
|
+
|
|
163
|
+
# 2. 政治面貌检查 - 从"其它"字段中提取
|
|
164
|
+
other = pos.get('其它', '') or ''
|
|
165
|
+
if '党员' in other and '中共党员' in other:
|
|
166
|
+
if user_profile['political'] != '党员':
|
|
167
|
+
return False, '要求中共党员'
|
|
168
|
+
|
|
169
|
+
# 3. 专业检查
|
|
170
|
+
major = pos.get('专业', '')
|
|
171
|
+
if major:
|
|
172
|
+
if '不限' in major:
|
|
173
|
+
pass # 专业不限
|
|
174
|
+
elif user_profile['major_code'] in major:
|
|
175
|
+
pass # 精确匹配专业代码
|
|
176
|
+
elif any(cat in major for cat in user_major_categories):
|
|
177
|
+
pass # 匹配专业类别
|
|
178
|
+
elif '中文' in major or '文秘' in major:
|
|
179
|
+
pass # 放宽条件,社会政治类可报
|
|
180
|
+
elif '社会政治' in major or '政治类' in major:
|
|
181
|
+
pass # 直接匹配
|
|
182
|
+
elif '公共管理' in major or '管理类' in major:
|
|
183
|
+
pass # 管理类也算相关
|
|
184
|
+
else:
|
|
185
|
+
return False, f'专业不符(要求{major[:20]})'
|
|
186
|
+
|
|
187
|
+
# 4. 应届检查 - 从"其它"字段提取
|
|
188
|
+
if '应届' in other and not user_profile['is_grad']:
|
|
189
|
+
return False, '要求应届生'
|
|
190
|
+
|
|
191
|
+
# 5. 性别检查 - 从"其它"字段提取
|
|
192
|
+
if '男性' in other:
|
|
193
|
+
return False, '限男性'
|
|
194
|
+
if '女性' in other:
|
|
195
|
+
return False, '限女性'
|
|
196
|
+
|
|
197
|
+
# 6. 考试类别检查
|
|
198
|
+
cat = pos.get('考试类别', '')
|
|
199
|
+
if cat and user_profile['category'] not in cat:
|
|
200
|
+
return False, f'考试类别不符({cat})'
|
|
201
|
+
|
|
202
|
+
# 7. 地区检查(盐城的职位)
|
|
203
|
+
dept = pos.get('单位名称', '')
|
|
204
|
+
region = pos.get('地区名称', '')
|
|
205
|
+
|
|
206
|
+
# 都是盐城的,因为是盐城的职位表
|
|
207
|
+
# 但可以检查是否是市区还是区县
|
|
208
|
+
|
|
209
|
+
return True, f'符合条件,专业要求:{major[:20]}'
|
|
210
|
+
|
|
211
|
+
# 筛选
|
|
212
|
+
qualified = []
|
|
213
|
+
for pos in positions:
|
|
214
|
+
ok, reason = is_qualified(pos)
|
|
215
|
+
if ok:
|
|
216
|
+
qualified.append(pos)
|
|
217
|
+
|
|
218
|
+
print(f" ✅ 筛选出 {len(qualified)} 个可报岗位")
|
|
219
|
+
|
|
220
|
+
# ========== 4. 匹配分数线并分类 ==========
|
|
221
|
+
print("\n[4/4] 匹配分数线并生成报告...")
|
|
222
|
+
|
|
223
|
+
def match_cutoff(pos):
|
|
224
|
+
"""匹配岗位分数线"""
|
|
225
|
+
dept = pos.get('单位名称', '') or ''
|
|
226
|
+
unit_code = pos.get('单位代码', '') or ''
|
|
227
|
+
|
|
228
|
+
# 尝试匹配部门名称
|
|
229
|
+
for dept_name, cutoff in cutoff_by_dept.items():
|
|
230
|
+
if dept_name in dept or dept in dept_name:
|
|
231
|
+
return cutoff
|
|
232
|
+
|
|
233
|
+
# 没有匹配到,返回平均分
|
|
234
|
+
return 120 # 默认A类平均分
|
|
235
|
+
|
|
236
|
+
# 添加分数线并计算分差
|
|
237
|
+
# 注意:江苏省考笔试合成分 = (行测+申论)/2,所以总分132分 = 合成分66分
|
|
238
|
+
# cutoff是合成分(70分左右),我们的分数也需要转换成合成分
|
|
239
|
+
user_composite_score = user_profile['total_score'] / 2 # 132/2 = 66分
|
|
240
|
+
|
|
241
|
+
for pos in qualified:
|
|
242
|
+
cutoff = match_cutoff(pos)
|
|
243
|
+
pos['cutoff'] = cutoff
|
|
244
|
+
pos['score_diff'] = user_composite_score - cutoff # 用合成分比较
|
|
245
|
+
|
|
246
|
+
# 分档(合成分制,分差减半)
|
|
247
|
+
if pos['score_diff'] >= 5: # 总分10分分差 = 合成分5分
|
|
248
|
+
pos['tier'] = '保底'
|
|
249
|
+
pos['probability'] = '高'
|
|
250
|
+
elif pos['score_diff'] >= 2.5: # 总分5分分差 = 合成分2.5分
|
|
251
|
+
pos['tier'] = '稳妥'
|
|
252
|
+
pos['probability'] = '中高'
|
|
253
|
+
elif pos['score_diff'] >= 0:
|
|
254
|
+
pos['tier'] = '冲刺'
|
|
255
|
+
pos['probability'] = '中等'
|
|
256
|
+
else:
|
|
257
|
+
pos['tier'] = '未达线'
|
|
258
|
+
pos['probability'] = '低'
|
|
259
|
+
|
|
260
|
+
# 按分档排序
|
|
261
|
+
qualified_sorted = sorted(qualified, key=lambda x: -x['score_diff'])
|
|
262
|
+
|
|
263
|
+
# 统计
|
|
264
|
+
tier_count = {
|
|
265
|
+
'冲刺': len([p for p in qualified if p['tier'] == '冲刺']),
|
|
266
|
+
'稳妥': len([p for p in qualified if p['tier'] == '稳妥']),
|
|
267
|
+
'保底': len([p for p in qualified if p['tier'] == '保底']),
|
|
268
|
+
'未达线': len([p for p in qualified if p['tier'] == '未达线'])
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
print(f" 📊 岗位统计:")
|
|
272
|
+
for tier, count in tier_count.items():
|
|
273
|
+
if tier == '冲刺':
|
|
274
|
+
print(f" 冲刺岗 (0~5分): {count} 个")
|
|
275
|
+
elif tier == '稳妥':
|
|
276
|
+
print(f" 稳妥岗 (5~10分): {count} 个")
|
|
277
|
+
elif tier == '保底':
|
|
278
|
+
print(f" 保底岗 (10分+): {count} 个")
|
|
279
|
+
else:
|
|
280
|
+
print(f" 未达线: {count} 个")
|
|
281
|
+
|
|
282
|
+
# ========== 5. 生成报告 ==========
|
|
283
|
+
report = f"""# 2026年盐城市公务员考试选岗分析报告
|
|
284
|
+
|
|
285
|
+
## 考生基本信息
|
|
286
|
+
|
|
287
|
+
| 项目 | 内容 |
|
|
288
|
+
|------|------|
|
|
289
|
+
| 姓名 | 考生 |
|
|
290
|
+
| 学历 | {user_profile['education']} |
|
|
291
|
+
| 专业 | {user_profile['major_name']} ({user_profile['major_code']}) |
|
|
292
|
+
| 专业类别 | {', '.join(user_major_categories) if user_major_categories else '社会政治类'} |
|
|
293
|
+
| 政治面貌 | {user_profile['political']} |
|
|
294
|
+
| 基层经验 | {'有' if user_profile['has_grassroots'] else '无'} |
|
|
295
|
+
| 是否应届 | {'是' if user_profile['is_grad'] else '否'} |
|
|
296
|
+
| 意向地区 | {user_profile['city']}市 |
|
|
297
|
+
| 考试类别 | {user_profile['category']}类 |
|
|
298
|
+
| 行测成绩 | 68分 |
|
|
299
|
+
| 申论成绩 | 64分 |
|
|
300
|
+
| 笔试总分 | **{user_profile['total_score']}分** |
|
|
301
|
+
| 历史面试成绩 | {user_profile['interview_history']}分(有面试经验) |
|
|
302
|
+
|
|
303
|
+
## 岗位统计分析
|
|
304
|
+
|
|
305
|
+
本次从 {len(positions)} 个岗位中筛选出 **{len(qualified)}** 个可报岗位:
|
|
306
|
+
|
|
307
|
+
- **冲刺岗** (0~5分): {tier_count['冲刺']} 个
|
|
308
|
+
- **稳妥岗** (5~10分): {tier_count['稳妥']} 个
|
|
309
|
+
- **保底岗** (10分+): {tier_count['保底']} 个
|
|
310
|
+
- **未达线**: {tier_count['未达线']} 个
|
|
311
|
+
|
|
312
|
+
## 冲刺岗位推荐(重点关注)
|
|
313
|
+
|
|
314
|
+
| 序号 | 招录机关 | 职位名称 | 招录人数 | 进面线(参考) | 分差 | 上岸概率 | 备注 |
|
|
315
|
+
|------|----------|----------|----------|--------------|------|----------|------|
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
idx = 1
|
|
319
|
+
for pos in [p for p in qualified_sorted if p['tier'] == '冲刺'][:10]:
|
|
320
|
+
dept = pos.get('单位名称', '')
|
|
321
|
+
pos_name = pos.get('职位名称', '')
|
|
322
|
+
count = pos.get('招考人数', '1')
|
|
323
|
+
cutoff = pos['cutoff']
|
|
324
|
+
diff = pos['score_diff']
|
|
325
|
+
prob = pos['probability']
|
|
326
|
+
other = pos.get('其它', '')
|
|
327
|
+
note = []
|
|
328
|
+
if '党员' in other: note.append('党员')
|
|
329
|
+
if '应届' in other: note.append('应届')
|
|
330
|
+
note_str = ','.join(note) if note else ''
|
|
331
|
+
|
|
332
|
+
star = '⭐' if idx <= 3 else ''
|
|
333
|
+
report += f"| {star}{idx} | {dept[:15]} | {pos_name[:15]} | {count} | {cutoff} | {diff:.1f} | {prob} | {note_str} |\n"
|
|
334
|
+
idx += 1
|
|
335
|
+
|
|
336
|
+
report += """
|
|
337
|
+
## 稳妥岗位推荐
|
|
338
|
+
|
|
339
|
+
| 序号 | 招录机关 | 职位名称 | 招录人数 | 进面线(参考) | 分差 | 上岸概率 | 备注 |
|
|
340
|
+
|------|----------|----------|----------|--------------|------|----------|------|
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
idx = 1
|
|
344
|
+
for pos in [p for p in qualified_sorted if p['tier'] == '稳妥'][:10]:
|
|
345
|
+
dept = pos.get('单位名称', '')
|
|
346
|
+
pos_name = pos.get('职位名称', '')
|
|
347
|
+
count = pos.get('招考人数', '1')
|
|
348
|
+
cutoff = pos['cutoff']
|
|
349
|
+
diff = pos['score_diff']
|
|
350
|
+
prob = pos['probability']
|
|
351
|
+
other = pos.get('其它', '')
|
|
352
|
+
note = []
|
|
353
|
+
if '党员' in other: note.append('党员')
|
|
354
|
+
if '应届' in other: note.append('应届')
|
|
355
|
+
note_str = ','.join(note) if note else ''
|
|
356
|
+
|
|
357
|
+
star = '⭐' if idx <= 3 else ''
|
|
358
|
+
report += f"| {star}{idx} | {dept[:15]} | {pos_name[:15]} | {count} | {cutoff} | {diff:.1f} | {prob} | {note_str} |\n"
|
|
359
|
+
idx += 1
|
|
360
|
+
|
|
361
|
+
report += """
|
|
362
|
+
## 保底岗位推荐
|
|
363
|
+
|
|
364
|
+
| 序号 | 招录机关 | 职位名称 | 招录人数 | 进面线(参考) | 分差 | 上岸概率 | 备注 |
|
|
365
|
+
|------|----------|----------|----------|--------------|------|----------|------|
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
idx = 1
|
|
369
|
+
for pos in [p for p in qualified_sorted if p['tier'] == '保底'][:10]:
|
|
370
|
+
dept = pos.get('单位名称', '')
|
|
371
|
+
pos_name = pos.get('职位名称', '')
|
|
372
|
+
count = pos.get('招考人数', '1')
|
|
373
|
+
cutoff = pos['cutoff']
|
|
374
|
+
diff = pos['score_diff']
|
|
375
|
+
prob = pos['probability']
|
|
376
|
+
other = pos.get('其它', '')
|
|
377
|
+
note = []
|
|
378
|
+
if '党员' in other: note.append('党员')
|
|
379
|
+
if '应届' in other: note.append('应届')
|
|
380
|
+
note_str = ','.join(note) if note else ''
|
|
381
|
+
|
|
382
|
+
star = '⭐' if idx <= 3 else ''
|
|
383
|
+
report += f"| {star}{idx} | {dept[:15]} | {pos_name[:15]} | {count} | {cutoff} | {diff:.1f} | {prob} | {note_str} |\n"
|
|
384
|
+
idx += 1
|
|
385
|
+
|
|
386
|
+
report += """
|
|
387
|
+
## 三梯队报考策略建议
|
|
388
|
+
|
|
389
|
+
### 第一梯队(冲刺)
|
|
390
|
+
**策略:搏一搏,单车变摩托**
|
|
391
|
+
|
|
392
|
+
1. 重点关注⭐标记的前3个冲刺岗位,虽然分差小,但有希望
|
|
393
|
+
2. 优先选择招录人数多的岗位,进面概率更大
|
|
394
|
+
3. 限制条件多的岗位,竞争相对较小,机会更大
|
|
395
|
+
|
|
396
|
+
### 第二梯队(稳妥)
|
|
397
|
+
**策略:主力报考,稳中求胜**
|
|
398
|
+
|
|
399
|
+
1. 这类岗位是你的主战场,笔试优势明显
|
|
400
|
+
2. 重点考虑岗位发展前景和个人兴趣匹配度
|
|
401
|
+
3. 选择招录人数多的岗位,降低运气因素影响
|
|
402
|
+
|
|
403
|
+
### 第三梯队(保底)
|
|
404
|
+
**策略:安全兜底,有备无患**
|
|
405
|
+
|
|
406
|
+
1. 超大分差岗位,进面基本稳了,重点准备面试
|
|
407
|
+
2. 如果前面的岗位不理想,可选择这类岗位保底
|
|
408
|
+
3. 基层岗位虽然竞争小,但也要考虑发展空间
|
|
409
|
+
|
|
410
|
+
## 面试备考重点
|
|
411
|
+
|
|
412
|
+
你的历史面试成绩 **74分**,属于中上水平,继续保持:
|
|
413
|
+
|
|
414
|
+
1. **江苏省考面试特点**
|
|
415
|
+
- 结构化面试为主,4道题20分钟
|
|
416
|
+
- 重点考察:综合分析、计划组织、应急应变、人际沟通
|
|
417
|
+
- 每题5分钟,包括思考和答题
|
|
418
|
+
|
|
419
|
+
2. **备考建议**
|
|
420
|
+
- 每天练习2-3道真题,保持答题状态
|
|
421
|
+
- 注重岗位匹配度,了解报考部门的职能和热点
|
|
422
|
+
- 练习眼神交流、声音洪亮、语速适中
|
|
423
|
+
|
|
424
|
+
3. **逆袭策略**
|
|
425
|
+
- 冲刺岗:面试需要比对手高5-8分才能逆袭
|
|
426
|
+
- 稳妥岗:保持正常发挥即可守擂
|
|
427
|
+
- 保底岗:重点展示岗位匹配度和稳定性
|
|
428
|
+
|
|
429
|
+
## 笔试提分空间
|
|
430
|
+
|
|
431
|
+
你目前笔试 **132分**(行测68 + 申论64),还有提升空间:
|
|
432
|
+
|
|
433
|
+
### 行测提分方向
|
|
434
|
+
- **言语理解**(68分水平还有提升空间):多刷题,总结错题
|
|
435
|
+
- **判断推理**:重点提升逻辑判断和图形推理
|
|
436
|
+
- **资料分析**:争取全对,这是最容易提分的模块
|
|
437
|
+
- **数量关系**:选择性放弃难题,确保简单题全对
|
|
438
|
+
- **常识判断**:考前突击时政,平时积累
|
|
439
|
+
|
|
440
|
+
### 申论提分方向
|
|
441
|
+
- **小题**:踩点给分,学会找关键词和总结概括
|
|
442
|
+
- **大作文**:立意要准,结构要清晰,论据要充分
|
|
443
|
+
- **练字**:字写工整,至少加2-3分
|
|
444
|
+
- **真题**:近3年真题做2遍,总结答题套路
|
|
445
|
+
|
|
446
|
+
## 注意事项
|
|
447
|
+
|
|
448
|
+
1. **数据来源说明**
|
|
449
|
+
- 职位表:2026年盐城市公务员考试官方职位表
|
|
450
|
+
- 分数线:根据2026年体检名单中第1名笔试成绩推算
|
|
451
|
+
- 匹配结果仅供参考,实际进面分数可能有波动
|
|
452
|
+
|
|
453
|
+
2. **专业匹配说明**
|
|
454
|
+
- 你的专业代码030201属于社会政治类
|
|
455
|
+
- 筛选时已放宽到中文文秘、法律等相关专业
|
|
456
|
+
- 最终请以官方专业目录和资格审核为准
|
|
457
|
+
|
|
458
|
+
3. **报名建议**
|
|
459
|
+
- 报名前3天观察报名人数,避开过热岗位
|
|
460
|
+
- 不要只盯着热门部门,有时候冷门部门发展更好
|
|
461
|
+
- 综合考虑:离家距离、工作强度、发展空间、薪资待遇
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
*报告生成时间:2026年5月29日*
|
|
465
|
+
*本报告仅供参考,最终报名请以江苏省公务员局官方公告为准*
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
# 保存报告
|
|
469
|
+
import os
|
|
470
|
+
os.makedirs('reports', exist_ok=True)
|
|
471
|
+
report_path = 'reports/2026盐城选岗分析报告.md'
|
|
472
|
+
with open(report_path, 'w', encoding='utf-8') as f:
|
|
473
|
+
f.write(report)
|
|
474
|
+
|
|
475
|
+
print(f"\n✅ 报告生成完成!")
|
|
476
|
+
print(f" 文件路径: {report_path}")
|
|
477
|
+
print("\n" + "=" * 60)
|
|
478
|
+
print("🎉 分析完成!详细报告已生成,可查看报告文件")
|
|
479
|
+
print("=" * 60)
|
|
480
|
+
|
|
481
|
+
# 同时输出前10个推荐岗位到控制台
|
|
482
|
+
print("\n📋 前10个推荐岗位:")
|
|
483
|
+
top_positions = [p for p in qualified_sorted if p['tier'] in ['冲刺', '稳妥', '保底']][:10]
|
|
484
|
+
for i, pos in enumerate(top_positions, 1):
|
|
485
|
+
dept = pos.get('单位名称', '')
|
|
486
|
+
pos_name = pos.get('职位名称', '')
|
|
487
|
+
print(f" {i}. [{pos['tier']}] {dept[:20]} - {pos_name[:15]} (分差: {pos['score_diff']:.1f})")
|