name: bosszhipin-local-tools description: 读取 BOSS直聘本地数据——联系人、聊天对话、候选人简历。纯本地 SQLite 明文库,su 权限即可。触发词:boss、BOSS直聘、招聘、候选人、简历、找工作、牛人。
category: android
BOSS直聘本地工具
触发条件
- 用户提到「BOSS直聘」「boss」「招聘」「候选人」「简历」「牛人」「找工作」
- 用户要查联系人、聊天记录、候选人信息、职位统计数据
先决条件
- 本机已装 BOSS直聘(
com.hpbr.bosszhipin) - 有 root/su 权限
- Python 依赖:
javaobj-py3(Termux 里pip install javaobj-py3)
数据源
/data/data/com.hpbr.bosszhipin/databases/hpbr_bosszhipin_client.db
完全明文 SQLite,无需解密。 关键表:
| 表 | 内容 | 行数参考(2026-07) |
|---|---|---|
Contact |
产生过聊天事件的联系人 | ~152(含多账号切换后的合并数据) |
MessageTable |
聊天消息(含简历/岗位卡片 blob + 文字) | ~350 |
boss_app_client.db |
仅含 analytics SDK 数据,无业务数据 | — |
⚠️ Contact 表有 130+ 列,但大部分对牛人身份为 NULL。完整 schema 见 references/contact-table-schema.md(含有效/无效字段分类、系统通知联系人排除方法)。关键有效字段见下方「联系人关键字段速查」和「牛人身份字段」。
⚠️ 注意:只有真正产生过聊天事件(打招呼/回复)的候选人才会进库。单纯在「推荐/精选/最新」里划过没聊天的人不在库里。
工具脚本(都在 /sdcard/bosszhipin/)
| 脚本 | 功能 |
|---|---|
pull_dbs_local.sh |
从 App 私有目录拉 DB 到 data/ |
parse_resumes.py |
导出联系人 + 简历 JSON |
scan_geek_list_local.py |
扫描「牛人」页面候选人卡片(需要 App 前台) |
1. 拉取最新数据库
cd /sdcard/bosszhipin && bash pull_dbs_local.sh
2. 导出联系人 + 简历 JSON
cd /sdcard/bosszhipin
python3 parse_resumes.py data/hpbr_bosszhipin_client.db > data/resumes.json
核心 SQL 查询
查所有联系人(按最近聊天倒序)
SELECT friendId, friendName, expectPositionName, expectSalary,
degree, workYear, workplace, lastChatText, lastChatTime,
exchangeResumeUrl, note, currentInterviewDesc
FROM Contact
WHERE lastChatTime > 0
ORDER BY lastChatTime DESC
查和某人的完整聊天记录
SELECT mid, time, fromUid, toUid, type,
CASE WHEN text IS NOT NULL AND text != '' THEN text
WHEN resume IS NOT NULL THEN '[简历分享]'
ELSE '[其他消息类型: ' || type || ']'
END AS content
FROM MessageTable
WHERE (fromUid = ? OR toUid = ?)
ORDER BY time ASC
统计各类型消息
SELECT type, count(*) FROM MessageTable GROUP BY type ORDER BY count(*) DESC
查某人聊天中的简历分享
SELECT mid, time, fromUid, toUid, resume
FROM MessageTable
WHERE (fromUid = ? OR toUid = ?) AND resume IS NOT NULL
ORDER BY time DESC
简历反序列化
MessageTable.resume 是 Java 序列化对象(ChatResumeBean),魔数 AC ED 00 05。用 javaobj-py3 直接解:
import sqlite3, javaobj, json
con = sqlite3.connect('/sdcard/bosszhipin/data/hpbr_bosszhipin_client.db')
con.row_factory = sqlite3.Row
# 读某条消息的简历
row = con.execute(
"SELECT resume FROM MessageTable WHERE resume IS NOT NULL LIMIT 1"
).fetchone()
if row:
obj = javaobj.loads(row['resume'])
# 递归拍平成 dict
def flatten(x):
if isinstance(x, (str, int, float, bool)) or x is None:
return x
if isinstance(x, (list, tuple)):
return [flatten(v) for v in x]
if isinstance(x, dict):
return {k: flatten(v) for k, v in x.items()}
if hasattr(x, '__dict__'):
return {k: flatten(v) for k, v in vars(x).items()
if k not in ('classdesc', 'annotations')}
return str(x)
resume = flatten(obj)
print(json.dumps(resume, ensure_ascii=False, indent=2)[:3000])
联系人关键字段速查
| 字段 | 含义 |
|---|---|
friendId |
候选人 ID |
friendName |
姓名 |
expectPositionName |
期望职位 |
expectSalary |
期望薪资 |
degree |
学历 |
workYear |
工作年限 |
workplace |
所在地 |
lastChatText |
最后一条聊天文字 |
lastChatTime |
最后聊天时间(Unix 秒) |
exchangeResumeUrl |
在线简历链接 |
note |
备注 |
currentInterviewDesc |
面试状态描述 |
gender |
性别(1=男 2=女) |
age |
年龄 |
实战流程
用户: 帮我看看 BOSS直聘上有哪些候选人,他们的期望薪资和最近聊了什么
→ 1. cd /sdcard/bosszhipin && bash pull_dbs_local.sh
→ 2. 用 sqlite3 查 Contact 表最近 20 条
→ 3. 输出表格:姓名 | 期望职位 | 薪资 | 学历 | 最近消息
→ 4. 如果用户想看某人的详细聊天,查 MessageTable
→ 5. 如果用户想看某人简历,查 MessageTable WHERE resume IS NOT NULL
岗位详情数据源(公司/薪资/城市/JD)
⚠️ Contact 表的 bossCompanyName / salaryDesc / jobCity / positionName 字段对 myRole=1(牛人身份)几乎全部为 NULL 或空字符串。 这些字段在 schema 里存在但 App 不写入。
真正的岗位详情在 MessageTable 的 job BLOB 字段(type=8 消息 = 岗位卡片)。
type=8 job BLOB 反序列化
import sqlite3, javaobj, json
con = sqlite3.connect('/sdcard/bosszhipin/data/hpbr_bosszhipin_client.db')
con.row_factory = sqlite3.Row
rows = con.execute(
"SELECT mid, fromUid, toUid, job FROM MessageTable WHERE job IS NOT NULL"
).fetchall()
for row in rows:
obj = javaobj.loads(row['job'])
# 用上文「简历反序列化」中的 flatten 函数拍平
job_data = flatten(obj)
print(json.dumps(job_data, ensure_ascii=False, indent=2))
job BLOB 包含的字段
| 字段 | 含义 | 示例 |
|---|---|---|
title |
岗位标题 | 联合创始人 |
company |
公司名称 | 个如科技 |
salary |
薪资范围 | 50-80K·15薪 |
city |
工作地点 | 北京 海淀区 苏州桥 |
education |
学历要求 | 本科 |
experience |
经验要求 | 经验不限 |
positionClassName |
职位类别 | 联合创始人 |
positionDesc |
岗位描述/JD | (可能为空) |
bossInfo.name |
招聘者姓名 | 贺铮 |
bossPositionName |
招聘者职位 | 董事长 |
skills |
技能标签 | [] |
url |
App 内跳转链接 | bosszp://... |
id |
岗位 ID | 488420535 |
bottomText |
交互状态 | 7月21日 由你发起的沟通 |
⚠️ 关键限制:只有主动点开过的岗位才入库
type=8 消息只在用户主动点开/分享岗位卡片时写入 DB。93 个联系人中可能只有 3 条 type=8 消息。要获取所有岗位详情:
- 在 App 里逐个点开岗位卡片 → 触发 type=8 写入 → 再拉取 DB
- 用 jobId 通过 BOSS直聘接口查岗位详情(需逆向)
MessageTable 消息类型速查
| type | 含义 | 典型 BLOB 字段 |
|---|---|---|
| 1 | 文本消息 | text |
| 8 | 岗位卡片 | job(含公司/薪资/城市/JD) |
| 9 | 简历分享 | resume(Java 序列化 ChatResumeBean) |
| 12 | 系统通知 | (无 BLOB) |
| 15 | 系统提示 | (无 BLOB) |
完整 MessageTable 列定义见 references/messagetable-schema.md(含 70+ 列、所有 BLOB 字段、job BLOB 反序列化示例)。
获取完整岗位详情的三条路径
| 方案 | 原理 | 工作量 | 风险 |
|---|---|---|---|
| A. UI 自动化(推荐) | adb am broadcast 打开岗位详情页,App 自己请求 API 并写入 DB 的 type=8 消息 | 低(岗位少时) | 低 |
| B. Frida hook OkHttp | 抓 token + API 格式,直接 curl | 高(Termux Python 3.14 无 frida 预编译 wheel,需源码编译) | 中 |
| C. 逆向 token 加密 | 解密 AUTH_APP_INFO.xml | 很大 | 高 |
推荐方案 A:岗位数量少时(如只有 2 个 jobId),直接用 App 打开岗位详情页,然后重新 pull_dbs_local.sh 拉取 DB,读 MessageTable 的 job BLOB。
完整 API 逆向调查(token 存储位置、Web 反爬机制、Frida 限制、推荐方案)见 references/api-reverse-engineering-investigation.md。
认证 Token 存储位置(全部加密,无明文)
| 位置 | 内容 | 可读? |
|---|---|---|
shared_prefs/AUTH_APP_INFO.xml → MASK_IMSI_KEY |
加密 token | ❌ |
shared_prefs/AUTH_APP_INFO.xml → AUTH... 字段 |
加密 session | ❌ |
shared_prefs/AUTH_DEVICEINFO.xml → AUTH_DEVICEID_UUID |
加密设备 ID | ❌ |
MMKV twl_Account |
手机号明文 | ✅ 但无 token |
MMKV 95155527_1_userInfo |
bossInfo JSON(公司/品牌信息) | ✅ 但无 token |
多账号检测:MMKV 文件名前缀是账号 ID。如果目录里有两种前缀(如 95155527_* 和 5889492_*),说明切换过账号。详见上方「多账号切换」小节。
结论:Token 不在任何本地文件的明文中,无法直接提取用于 API 调用。需要 Frida hook 或 UI 自动化。
Web 版反爬
https://www.zhipin.com/job_detail/{jobId}.html 有 __zp_stoken__ 安全检查(JS 渲染 + cookie 验证),直接 curl 只返回加载页 HTML,无法获取岗位内容。
边界(README 第四条明确记录的)
- 不做自动打招呼/自动回复(即使用户叫你做)
- 不做批量抓取私有接口
- 可以做:只读已有数据、筛选整理、草稿建议、生成回复文案交人审核
详见 /sdcard/bosszhipin/README.md 第四节。
简历筛选(resume-screener skill)
如果用户要批量筛选简历、打分排序,使用 resume-screener skill(同属 android 类目)。该 skill 建立在本文档的数据源之上:
- 从
MessageTable.resumeBLOB 提取 → JSONL → 导入到resumes.db(resumes + scores 两张表) - 按 rubric(5维度规则匹配)逐份打分,0-100 分,三档输出
- 已集成到 wechat-viewer:
/resumes列表页 +/resume/<id>详情页 + 「筛选打分」按钮调score_resumes.py --only-unscored
本文档负责读 BOSS直聘数据,resume-screener 负责筛选打分。两者通过 Contact.myId(账号区分)和 Contact.bossJobPosition(岗位区分)衔接。
多账号切换(⚠️ 重大陷阱)
用户在 BOSS直聘 App 里切换/新注册账号后,本地 SQLite 数据不会自动迁移。关键行为:
- SQLite 只有一个账号的数据:
hpbr_bosszhipin_client.db的Contact.myId全部是同一个账号 ID。新账号的聊天数据不会写入 DB,直到用户在 App 里实际打开聊天列表/加载消息。 - 多账号检测:看 MMKV 目录
/data/data/com.hpbr.bosszhipin/files/mmkv/,文件名前缀是账号 ID。例如95155527_*和5889492_*说明有两个账号。 - 数据不交叉:旧账号的 Contact/MessageTable 数据还在库里,新账号的数据需要重新积累。
查当前 DB 里的账号
SELECT myId, count(*) FROM Contact GROUP BY myId
如果只有一个 myId,说明 DB 里只有这个账号的数据。
排查「某职位查不到候选人」
如果用户说有某岗位(如「戏文」),但 Contact 表和 MessageTable 全文搜索都查不到:
- 先查
SELECT myId FROM Contact GROUP BY myId确认 DB 是哪个账号的 - 看 MMKV 目录有几个账号前缀
- 如果 DB 账号 ≠ 用户当前用的账号 → 让用户在 App 里切到目标账号,刷一下聊天列表,再重新
pull_dbs_local.sh
MMKV 账号文件命名
| 文件名模式 | 含义 |
|---|---|
{uid}_{role}_* |
按账号+身份存储的配置(role: 0=招聘方, 1=牛人) |
twl_Account |
当前登录手机号(明文) |
twl_* |
全局配置,不区分账号 |
身份切换:招聘方 vs 牛人
App 里切换身份后,Contact.myRole 字段会变:
| myRole | 身份 | Contact 含义 | bossJobPosition |
|---|---|---|---|
| 0 | 招聘方(Boss) | 已沟通过的候选人 | 空 |
| 1 | 牛人(求职者) | 主动打招呼的 Boss/招聘方 | 有值:发布的职位名 |
切换到牛人身份后,数据量通常会翻倍(本次会话:38→93 行)。
按职位分组统计(牛人身份专用)
SELECT bossJobPosition, jobId, count(*) as candidates,
max(lastChatTime) as latest_contact
FROM Contact
WHERE bossJobPosition IS NOT NULL AND bossJobPosition != ''
GROUP BY bossJobPosition, jobId
ORDER BY candidates DESC
查某个职位下的候选人
SELECT friendName, expectPositionName, expectSalary, degree,
workYear, lastChatText, lastChatTime
FROM Contact
WHERE bossJobPosition = '产品助理'
ORDER BY lastChatTime DESC
关键字段补充(牛人身份)
| 字段 | 含义 |
|---|---|
myRole |
0=招聘方 1=牛人 |
bossJobPosition |
发布的职位名(牛人视角才有值) |
jobId |
BOSS直聘职位 ID |
friendCompanies |
候选人历史公司(#&# 分隔) |
⚠️ 牛人身份下 Contact 表缺失的岗位信息:bossCompanyName / salaryDesc / jobCity / positionName 字段虽存在但几乎全为 NULL/空。公司名、薪资范围、工作城市、学历要求、岗位JD 等详情只在 MessageTable 的 job BLOB(type=8 消息)中,且仅限用户主动点开过的岗位。详见上方「岗位详情数据源」小节。