ARTICLE DETAIL

资讯详情

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

Python后端爬虫专题23:三条职位能得出什么、不能得出什么——技能、薪资与城市趋势

Python后端爬虫专题23:三条职位能得出什么、不能得出什么——技能、薪资与城市趋势 Python后端爬虫专题23三条职位能得出什么、不能得出什么——技能、薪资与城市趋势上一篇练习完整答案创建成功返回 202任务落库并恰好入队一次seed 不在 allowlist 返回 422不写任务也不入队队列不可用时任务已落库随后标为 dispatch_failed接口返回 503跨租户读取返回 404不能看到任务职位查询也为空。副作用比响应 JSON 更重要。分页完整测试已经落在tests/test_api.py::test_jobs_endpoint_applies_page_and_size_to_real_query它用真实内存 SQLite 写三条不同 source_urlASGITransport 请求/api/jobs?page2size2断言 total3 且只返回“职位 2”。生产租户应由已验证 JWT 的 tenant claim 映射或由受信网关删除外部同名头后注入签名内部头不能接受用户任意声明。先写统计口径后写Counter城市计数每条有效职位按 city 计一次技能计数同一职位的技能已在 Pydantic 阶段去重、大小写用 casefold 合并但展示保留第一次标签年薪估计只有 salary_min 和 salary_max 都存在时取月薪区间中点乘发薪月数平均值只在这些样本之间计算。这不是市场薪资真相。招聘薪资可能是税前、范围夸大、经验要求不同缺失薪资通常也不是随机缺失。接口必须返回salary_sample_size让使用者知道平均值来自几条而不是把无薪资职位当 0 拉低结果。手算三条样本第一条杭州Python/FastAPI10k—20k、13 薪年薪中点 195000第二条上海Python/PostgreSQL20k—30k、12 薪年薪中点 300000第三条杭州、Scrapy、薪资缺失。城市是杭州 2、上海 1Python 2其余技能各 1平均年薪(195000300000)/2247500样本数 2。cd project.\.venv\Scripts\python.exe-m pytest tests\test_analytics.py::test_summary_counts_cities_skills_and_annual_salary_without_inventing_missing_values-q测试期望值是手算常量不调用实现计算 expected因此把salary_months错写为 12 会失败。它还确保缺薪资样本没有作为 0 进入平均。summarize_jobs 的输入为什么是Protocol它不需要整个 JobRecord只要求 city、skills、salary_min/max/months。JobForAnalyticsProtocol 让 JobItem、SQLAlchemy JobRecord 或别的只读对象都能使用只要具有这些属性。这叫结构化类型边界不要求业务类继承一个“为了统计”的基类。输出JobSummary是冻结 dataclass字段含义明确API 再把 top_skills tuple 转为对象列表。统计函数不读取数据库、不感知 FastAPI、不打印结果所以可在命令行、任务和接口复用。排名相同怎么稳定技能先按次数降序再按展示标签 casefold 排序保证同一输入多次运行顺序一致。Counter 的插入顺序不应被当作业务排名规则。城市同样按次数和名称排序。稳定输出对缓存、前端 diff 和测试都重要。top_n小于 1 直接 ValueErrorAPI 用 Query 限制 1—50在边界层返回 422。空输入返回 total0、salary_sample_size0、averageNone不发生除零也不编造 0 元平均薪资。调接口而不看内部函数Compose 运行并完成一次任务后$headers {X-Tenant-IDcourse-demo}Invoke-RestMethod-Urihttp://127.0.0.1:8010/api/analytics?top_n5-Headers$headers|ConvertTo-Json-Depth 5租户隔离已有 API 行为测试tenant-b 的城市与 SecretSkill 不会进入 tenant-a 结果。注意当前实现先读取最多 100000 行再在 Python 聚合适合课程和中小数据。百万级数据应将城市/薪资聚合下推 SQL技能 JSON 可规范化到关联表或使用 PostgreSQL JSONB/GIN并用离线物化视图控制查询成本。为AI/RAG准备的不是统计接口趋势接口给人和仪表盘RAG 更需要逐条、带来源的文档。项目的scripts.export_jobs从某租户读取 JobRecord转换回不含内部 id/tenant 的 JobItem原子写 JSONL每行带schema_versionjobradar.job.v1。它不会把另一个租户或抓取 Cookie 带出去。$env:JOBRADAR_DATABASE_URL postgresqlpsycopg://jobradar:jobradar-dev-password127.0.0.1:5435/jobradar.\.venv\Scripts\python.exe-m scripts.export_jobs--tenant course-demo--outputdata\jobs-for-rag.jsonl下游切分文本时应保留 source_url、published_at 和 schema_version用于引用和兼容性不能把模型生成的总结重新当原始职位事实。本篇完整统计模块按“口径—遍历累积—稳定排序—缺失处理”阅读。短小不代表随意它把容易引发误导的薪资规则集中到一个可测试位置。把经过校验的职位事实汇总为可解释统计。fromcollectionsimportCounterfromdataclassesimportdataclassfromtypingimportProtocol,SequenceclassJobForAnalytics(Protocol):city:strskills:Sequence[str]salary_min:int|Nonesalary_max:int|Nonesalary_months:intdataclass(frozenTrue)classJobSummary:total_jobs:intcity_counts:dict[str,int]top_skills:list[tuple[str,int]]salary_sample_size:intaverage_annual_salary:int|Nonedefsummarize_jobs(jobs:Sequence[JobForAnalytics],*,top_n:int10)-JobSummary:统计城市、技能和有明确区间样本的平均年薪中位估计。iftop_n1:raiseValueError(top_n must be positive)citiesCounter(job.cityforjobinjobs)skill_labels:dict[str,str]{}skill_counts:Counter[str]Counter()annual_salaries:list[int][]forjobinjobs:forskillinjob.skills:keyskill.casefold()skill_labels.setdefault(key,skill)skill_counts[key]1ifjob.salary_minisnotNoneandjob.salary_maxisnotNone:midpoint(job.salary_minjob.salary_max)/2annual_salaries.append(round(midpoint*job.salary_months))ranked_skillssorted(skill_counts.items(),keylambdaitem:(-item[1],skill_labels[item[0]].casefold()))[:top_n]average(round(sum(annual_salaries)/len(annual_salaries))ifannual_salarieselseNone)returnJobSummary(total_jobslen(jobs),city_countsdict(sorted(cities.items(),keylambdaitem:(-item[1],item[0]))),top_skills[(skill_labels[key],count)forkey,countinranked_skills],salary_sample_sizelen(annual_salaries),average_annual_salaryaverage,)本篇课后练习给四条手工样本其中一条无薪资、一条技能大小写不同先手算结果再写测试调用 summarize_jobs 对照。说明“平均薪资 247500”至少要同时展示哪些上下文避免被误解为市场结论。运行一次 JSONL 导出并逐行json.loads完整验证 schema_version、source_url 与职位数。下一篇会让每个任务在日志和指标中可追踪同时确保凭据不会泄露。
返回列表