“ 从 Full JD 补全、技能提取到德国软件测试Skill Map。”
本系列前三篇文章分别解决了三个问题:怎样把一个现实问题拆成数据项目,怎样通过 Jobs API 批量获取真实职位,以及怎样把本地项目整理并上传到 GitHub。
第一篇最终从 2,671 个去重职位中筛出了 568 个与软件测试相关的岗位。但这些岗位究竟需要什么技能?
今天我们继续扩展这个 Python 项目:从 568 个相关职位出发,寻找原始招聘页面,补全 Full JD,再用 BeautifulSoup,JSON-LD 和正则表达式提取技能。
过程中遇到的数据问题:为什么 HTTP 的需求率一度高达 6.62%?为什么抓到整个网页反而可能让数据变差?为什么 272 份可用 JD,最终只用其中 201 份作为核心分析样本?
最终我们得到不是一个简单的“热门技能排行榜”,而是一张可以真正用于制定学习计划的德国软件测试 Skill Map。
1. Adzuna获取的职位描述不全
上一篇数据分析中,我们已经完成了一轮比较完整的职位采集和分类。整个流程大致是:
Adzuna API → 2,671 个去重职位 → 职位分类 → 568 个 Relevant Jobs
这 568 个职位已经经过分类器和人工抽查,相比招聘网站直接返回的搜索结果干净很多。看起来,数据似乎已经足够了。
如果想知道德国软件测试岗位最需要哪些技能,直接在这 568 条数据里搜索 Python、Selenium、ISTQB 不就可以了吗?
但是Adzuna API 返回的数据虽然包含 description 字段,但它只是职位描述片段,而不是完整 JD。
一个技能没有出现在 snippet 中,不代表招聘方没有要求它。所以项目需要进入第二阶段:补全 Full Job Description。
2. 补全职位描述(JD)
到这里我有一个疑问:“为什么不一开始就在抓取有完整 JD 的网站的信息?” 既然最后还是需要完整 JD,为什么前面还要通过 Jobs API 获取几千个职位,再分类、清洗,然后重新寻找招聘页面?
如果一开始就从各种招聘网站直接抓Full JD,会马上面对不同网站、不同公司官网的页面结构差异,而且大量职位后来会被判断为 UNCERTAIN 或IRRELEVANT,从而导致产生大量无效数据。
所以我们分两个阶段解决问题:第一阶段解决的是“哪些职位值得分析?”,第二阶段解决的是“这些职位具体要求什么?”
API 很适合第一件事。它可以通过统一的数据结构批量返回职位 ID、标题、公司、地点、坐标和描述片段。我们可以先快速建立一个相对完整的职位池,再通过规则和人工验证筛出真正属于软件测试的岗位。
最后采用一种分层策略:API → 建立结构化职位池 → 分类与清洗 → Relevant Jobs → 只对 Relevant Jobs 补 Full JD。
先用成本较低的方法缩小范围,再把成本更高的方法用在真正需要分析的数据上。
3. 找到原始职位
接下来要做的,是根据职位标题和公司名称寻找 Original Job Page。程序会为每个职位生成搜索词,再通过搜索服务寻找可能的原始招聘页面。
如果第一次搜索没有得到可信结果,还可以使用 fallback search:调整搜索词或放宽条件,再尝试一次。
这一阶段可以概括为:Relevant Job → Search Query → Candidate URL → URL 验证 → 抓取网页 → 提取 Full JD。
这也解释了我当时提的另一个问题:“前面几百条数据的搜索到底是在干什么?” 它不是重新搜索“有没有这些职位”。前面的 API 已经完成了职位发现。这里搜索的真正目标,是给已经确定要分析的职位找到信息更完整的原始页面。
这是典型的 Data Enrichment:不是重新建立数据集,而是在已有数据上补充新的字段。
4. BeautifulSoup
抓到网页以后,需要用到新工具:BeautifulSoup。
我们可以把网页理解成一棵由 HTML 标签组成的结构。requests 的工作是把 HTML 从服务器拿回来;BeautifulSoup 的工作,则是解析这份 HTML,让 Python 能够理解和查找里面的结构。
例如可以使用 BeautifulSoup(html, 'html.parser') 创建解析对象,再通过get_text() 提取页面中的可见文字。
所以两者的关系很简单:requests 负责拿网页,BeautifulSoup 负责解析网页,Python再对内容进行分析。
但很快我们就发现:能把网页文字全部拿出来,并不等于已经拿到了干净的 JD。
5. JSON-LD
很多招聘页面除了给人看的 HTML,还会提供一份给搜索引擎和程序读取的结构化数据,其中一种常见格式就是 JSON-LD。
在招聘页面中,它可能包含 @type: JobPosting,以及 title、description、hiringOrganization 等字段。
那验证的时候,把 JSON-LD 和 Description 合并,再search 吗?并不是。
实际search job的流程如下图:
6. Data Cleaning
问: 我们这个过程哪里涉及到了 Data Cleaning?
第一阶段已经发生过多关键词职位合并、Job ID 去重、职位分类和 False Positive 排除。进入 Full JD阶段后,又增加了搜索结果筛选、URL 验证、页面验证、Full JD 提取、HTML 噪声处理和技能误判清理。
所以 Data Cleaning 并不等于执行一次 df.drop_duplicates(),也不是一个做完就结束的步骤。
更准确地说:只要原始数据与真正想分析的对象之间还有差距,就仍然存在 Data Cleaning。
7. 第一次结果
拿到 Full JD 后,就可以开始做 Skill Extraction。先建立技能词表,例如 Python、Java、Selenium、Playwright、Cypress、CI/CD、ISTQB、Linux、Docker 等,再用 Regex 在 JD 中寻找这些技能。
第一版很快就跑出了结果。但检查结果时,一个数字显得有些奇怪:HTTP 出现在 18 个岗位中,占 6.62%。
HTTP 当然是测试工程师可能接触的知识。问题是,它真的被这么多招聘信息明确列为技能要求吗?
于是我们没有直接相信统计表,而是回到原始 Evidence。结果发现,一些命中来自 https://company.com/jobs/... 这样的 URL。
Regex 看到了 http,于是判断这个岗位要求 HTTP。但这里的 HTTP 根本不是技能,它只是URL 的一部分。
程序没有报错,CSV 正常生成,统计代码也完全运行成功,但结果是错的。
这也是这个项目里非常重要的一次提醒:代码成功运行,不等于数据分析结果正确。
8.升版
从“程序执行”的角度看,第一版已经完成了;但从“数据分析”的角度看,它并没有完成。
第二版做了几件事情:清除 URL、收紧 Regex、增加 Negative Context,并保存 Evidence Snippet,然后重新提取。
Evidence Snippet 特别重要。以前程序只告诉我HTTP=True,现在还会告诉我为什么它认为 HTTP=True。这样人工抽查时,就可以直接查看关键词附近的文本。
修正后,HTTP 从 18 个岗位 / 6.62%,下降到了 1 个岗位 / 0.37%。
这说明问题并不是市场突然发生了变化。变化的是:我们的测量方法变准确了。
9. 网页污染数据
HTTP 问题解决后,很快又发现了第二个问题。Visible HTML 中可能出现“查看更多相关职位”之类的内容。
这意味着一个招聘网页并不只有当前职位。它可能同时包含当前职位 JD、相关推荐职位、导航栏、Footer 和公司其他岗位。
假设当前职位没有要求 Java,但页面底部推荐了 Senior Java Test Automation Engineer。如果直接对整页文字搜索 Java,程序可能会判断当前职位要求 Java。
于是产生另一类 False Positive。
这时候出现了一个有点反直觉的结论:抓到的文字更多,不一定意味着数据更完整。有时候,它只是意味着噪声更多。
10.排除污染数据
比较两种 Full JD Extraction Method。最终得到 272 份 usable Full JD,其中 201 份来自 JSON-LD JobPosting,71 份来自 Visible HTML。
接下来没有直接把 272 份全部混在一起,而是比较两组数据中的技能频率。结果发现,一些技能在两种 Extraction Method 中存在明显差异。
这意味着:数据提取方式本身可能正在影响分析结果。
因此最终采用 201 份 JSON-LD 作为 Primary Dataset,71 份 Visible HTML 作为 Robustness Dataset。
如果增加的数据同时带来了更大的 Measurement Error,那么 200 条来源更清晰的数据,可能比 300 条混有推荐职位、导航和页面噪声的数据更适合作为核心样本。
数据不一定需要百分之百完整,关键是清楚知道哪些数据可靠,以及结论能支持到什么程度。
最终,我们以 201 份 JSON-LD Full JD 作为核心样本。得到的部分技能频率如下:
技能 | 岗位数 | 占比 |
Test Automation | 123 | 61.19% |
Agile | 93 | 46.27% |
ISTQB | 66 | 32.84% |
Python | 56 | 27.86% |
CI/CD | 54 | 26.87% |
Selenium | 47 | 23.38% |
Jira | 38 | 18.91% |
Playwright | 35 | 17.41% |
Scrum | 34 | 16.92% |
C++ | 32 | 15.92% |
Java | 31 | 15.42% |
到这里,好像已经可以制定学习计划了。但出现频率不等于学习顺序。ISTQB、Python、CI/CD 和 Selenium 的学习成本不同,在招聘中的作用也完全不同。另外:技能不是孤立出现的。
于是下一步不再只统计“Python 出现了多少次”,而是问:“Python 通常和什么一起出现?”这就是 Skill Co-occurrence。
例如数据中有一个非常明显的关系:94.44% 出现 CI/CD 的岗位,同时出现 Test Automation。
类似地,Java 与 Selenium、Python 与 Automation、Selenium 与 Playwright 之间,也存在不同程度的共现关系。
这时候我们分析的就不再是一张关键词排行榜,而是德国企业正在招聘什么样的技能组合。
再加入 Role Segmentation——区分 General Software Testing、Test Automation、Embedded / Automotive 等岗位——最终才能得到真正的 Skill Map。
Skill Frequency + Skill Co-occurrence + Role Segmentation → German Software Testing Skill Map
12.市场需求与学习计划
到这里,我以为问题已经解决了。既然市场数据已经告诉我们德国公司需要什么,那么让 AI 根据 Skill Map 排一份学习路线就可以了。
第一版Skill Map:Testing Fundamentals → ISTQB Foundation → Python → Selenium → API Testing → CI/CD……
接下来排除个人已有的测试经验和技能, 删掉 Testing Fundamentals 和 ISTQB , 从而得到个人 Learning Map 。
最终逻辑变成:Market Demand − Existing Skills + Skill Dependencies → Personal Learning Plan。
同一份德国招聘数据,最后对工作经验不同的人会生成不同的学习路线。
13. 从数据到决策
这个项目最初的问题:如果准备找德国的软件测试工作,我应该学什么?
这个问题完全可以直接问 AI。几秒钟后就能得到 Python、Java、Selenium、Playwright、API Testing、CI/CD、Docker 等答案。这些答案未必错。
但真正的问题是:为什么是这些?它们在目标市场中到底有多重要?哪些是独立技能,哪些其实属于同一套技术栈?哪些属于通用 QA,哪些主要集中在 Embedded / Automotive?更重要的是——其中哪些我已经会了?
于是一个原本很模糊的问题,最后变成了一条完整的数据链:职位采集 → 职位分类 → Full JD Enrichment → HTML / JSON-LD Extraction → Data Cleaning → Skill Extraction → False Positive Audit → Robustness Check → Skill Frequency → Skill Co-occurrence → Role Segmentation → Skill Map → Personal Skill Gap → Learning Plan。
这个过程中,Python 的价值并不只是帮我自动处理了很多数据。更重要的是,它让我可以把“我感觉应该学这些”,逐渐变成“根据我定义的样本和分析方法,数据支持我优先关注这些技能”。
当然,这仍然不是一份对整个德国劳动力市场的完整普查。职位数据来自特定数据源,因此结果应该理解为基于该数据源和分析口径得到的样本,而不是整个市场的绝对真相。
但对实际决策来说,它已经比一张来源不明的“软件测试学习路线图”多了一层很重要的东西:证据。
工具不是从问题中消除不确定性,而是帮助我们把不确定性看得更清楚。
(完)