# BOSS直聘 — Job Search & Extraction Field-tested against zhipin.com on 2026-05-01. Login required for API access; job browsing is accessible without auth. Last browser-verified: 2026-05-01 (all functions re-tested against live site). --- ## Anti-bot / API verdict BOSS直聘 is a Vue SPA. There is no SSR JSON blob (`__NEXT_DATA__`, `__INITIAL_STATE__`, etc.) — all data loads via XHR/fetch to internal `/wapi/` endpoints. **`http_get` does NOT work** — the `/wapi/` endpoints require browser session cookies (not just a CSRF token). All API calls must be made via `js()` + `fetch()` inside the browser session. **However, the API returns real salary numbers** (`"salaryDesc": "18-22K"`) unlike the DOM which renders salary digits via a custom `@font-face` with private-use Unicode characters (U+E000–U+F8FF). **Always prefer the API over DOM extraction.** --- ## Quickstart ```python import json goto_url("https://www.zhipin.com/web/geek/jobs?ka=open_joblist") wait_for_load() wait(3) jobs = json.loads(js(""" (async function() { var r = await fetch('/wapi/zpgeek/pc/recommend/job/list.json?page=1&pageSize=15&city=101020100'); var d = await r.json(); if (d.code !== 0 || !d.zpData) { return JSON.stringify([]); } return JSON.stringify(d.zpData.jobList || []); })() """)) for j in jobs: print(j["salaryDesc"], j["jobName"], "|", j["brandName"]) ``` --- ## URL Patterns | Resource | URL | |----------|-----| | Job search page | `https://www.zhipin.com/web/geek/jobs` | | Job search (with prefs) | `https://www.zhipin.com/web/geek/jobs?ka=open_joblist` | | Job detail page | `https://www.zhipin.com/job_detail/{JOB_ID}.html` | | Company page | `https://www.zhipin.com/gongsi/{COMPANY_ID}~.html` | --- ## API: Job List ``` GET /wapi/zpgeek/pc/recommend/job/list.json ``` ### Query Parameters | Param | Description | Values | |-------|-------------|--------| | `page` | Page number | 1-N | | `pageSize` | Results per page | 15 (default) | | `city` | City code | `101020100` = Shanghai, `101010100` = Beijing, `101280100` = Guangzhou | | `experience` | Experience filter code | `0`=不限, `104`=1-3年, `105`=3-5年, `106`=5-10年 | | `degree` | Education filter code | `0`=不限 | | `salary` | Salary filter code | `0`=不限, `405`=10-20K, `406`=20-50K | | `industry` | Industry filter code | (numeric) | | `scale` | Company size filter code | `0`=不限, `303`=100-499人, `305`=1000-9999人 | | `jobType` | Job type | `0`=不限, `1`=全职, `2`=兼职 | | `encryptExpectId` | Saved preference ID | From user's saved preferences (empty string = default) | | `mixExpectType` | Mixed expectation type | (empty string for default) | | `expectInfo` | Expectation info | (empty string for default) | Filter codes come from `/wapi/zpgeek/pc/all/filter/conditions.json`. Setting `experience`, `salary`, or `degree` to non-zero values without a valid `encryptExpectId` may return no results. The page always sends all filter params (even empty) in its API calls. ### Response (`zpData.jobList[]`) ```python { "securityId": "nbyXZvE4kfp6Y...", # opaque ID for detail API "encryptJobId": "cbcbfca3...", # job detail page ID "encryptBossId": "37a64419...", # recruiter ID "jobName": "Aone/GitHub开发工程师", "salaryDesc": "18-22K", # REAL salary — not font-encoded "jobLabels": ["3-5年", "本科", "Java", "Golang"], "skills": ["Java", "Golang", "Aone", "GitLab"], "jobExperience": "3-5年", "jobDegree": "本科", "cityName": "上海", "areaDistrict": "浦东新区", "businessDistrict": "张江", "brandName": "软通动力", "brandIndustry": "计算机软件", "brandScaleName": "10000人以上", "brandStageName": "未融资", "bossName": "杨女士", "bossTitle": "人事", "bossOnline": false, "bossAvatar": "https://img.bosszhipin.com/...", "brandLogo": "https://img.bosszhipin.com/...", "welfareList": [], "gps": {"longitude": 121.609707, "latitude": 31.185578} } ``` ### Fetch Jobs (browser API) ```python import json def fetch_job_list(page=1, page_size=15, city="101020100", **filters): """Fetch jobs from the BOSS直聘 API. Must be on a zhipin.com page first.""" # Default params (matching what the real page sends) defaults = { "encryptExpectId": "", "mixExpectType": "", "expectInfo": "", "jobType": "", "salary": "", "experience": "", "degree": "", "industry": "", "scale": "" } defaults.update(filters) params = f"page={page}&pageSize={page_size}&city={city}" for k, v in defaults.items(): params += f"&{k}={v}" raw = js(f""" (async function() {{ var r = await fetch('/wapi/zpgeek/pc/recommend/job/list.json?{params}'); var d = await r.json(); if (d.code !== 0 || !d.zpData) {{ return JSON.stringify({{code: d.code, hasMore: false, jobs: [], error: d.msg || 'API error'}}); }} return JSON.stringify({{code: d.code, hasMore: d.zpData.hasMore, jobs: d.zpData.jobList || []}}); }})() """) return json.loads(raw) # Usage result = fetch_job_list(page=1, experience=105) # 3-5年 print(f"Total: {len(result['jobs'])} jobs, hasMore: {result['hasMore']}") for j in result["jobs"]: print(f" {j['salaryDesc']:12s} {j['jobName']:30s} {j['brandName']}") ``` ### Pagination The API uses `hasMore` (boolean), not a total count. **Page-based pagination is unreliable** — `page=2` often returns 0 results even when `page=1` says `hasMore: true`. Use the initial page 1 results and consider widening filters (e.g., smaller `pageSize`, different city) rather than paginating. The actual job search page loads recommendations once at `page=1` and lazy-loads more via scroll, which triggers a different API path. For bulk extraction, vary filters (city, experience, salary) to get different result sets: ```python def fetch_all_jobs(city="101020100", max_pages=10): all_jobs = [] for page in range(1, max_pages + 1): result = fetch_job_list(page=page, city=city) all_jobs.extend(result["jobs"]) if not result["hasMore"] or len(result["jobs"]) == 0: break wait(0.5) # polite delay return all_jobs ``` --- ## API: Job Detail ``` GET /wapi/zpgeek/job/detail.json?securityId={securityId} ``` Use the `securityId` from the job list response (NOT `encryptJobId`). ```python def fetch_job_detail(security_id): raw = js(f""" (async function() {{ var r = await fetch('/wapi/zpgeek/job/detail.json?securityId={security_id}'); var d = await r.json(); if (d.code !== 0 || !d.zpData) {{ return JSON.stringify({{code: d.code, error: d.msg || 'API error'}}); }} var zp = d.zpData; var job = zp.jobInfo; var boss = zp.bossInfo; var brand = zp.brandComInfo; return JSON.stringify({{ code: d.code, title: job.jobName, salary: job.salaryDesc, experience: job.experienceName, degree: job.degreeName, location: job.locationName, address: job.address, gps: {{lng: job.longitude, lat: job.latitude}}, description: job.postDescription, skills: job.showSkills, boss_name: boss.name, boss_title: boss.title, boss_avatar: boss.large, boss_online: boss.online, company_name: brand.brandName, company_logo: brand.logo, company_industry: brand.industryName, company_scale: brand.scaleName, company_stage: brand.stageName }}); }})() """) return json.loads(raw) ``` --- ## API: Filter Conditions ``` GET /wapi/zpgeek/pc/all/filter/conditions.json ``` Returns all available filter options with their numeric codes: ```python def get_filter_conditions(): raw = js(""" (async function() { var r = await fetch('/wapi/zpgeek/pc/all/filter/conditions.json'); var d = await r.json(); if (d.code !== 0 || !d.zpData) { return JSON.stringify({}); } return JSON.stringify(d.zpData); })() """) return json.loads(raw) # Returns: { # experienceList: [{code: 105, name: "3-5年"}, ...], # salaryList: [{code: 406, name: "20-50K", lowSalary: 20, highSalary: 50}, ...], # degreeList: [{code: 203, name: "本科"}, ...], # scaleList: [{code: 305, name: "1000-9999人"}, ...], # stageList: [{code: 807, name: "已上市"}, ...], # industryList: [...], # payTypeList: [...], # partTimeList: [...] # } ``` --- ## City Codes City is identified by numeric code, not name: | City | Code | |------|------| | 上海 | `101020100` | | 北京 | `101010100` | | 深圳 | `101280200` | | 广州 | `101280100` | | 杭州 | `101210100` | | 成都 | `101270100` | To discover a city code, check the `city` param in the job list API call or use the city data API: ``` GET /wapi/zpgeek/common/data/city/site.json ``` --- ## DOM Extraction (fallback) If the API path is blocked, fall back to DOM extraction. Note that salary text uses font-encoded private-use Unicode characters (U+E000–U+F8FF) — DOM `textContent` returns unreadable PUA codepoints like `"-K"` instead of the rendered digits. ### Job Card DOM ``` li.job-card-box div.job-info div.job-title.clearfix a.job-name[href="/job_detail/{ID}.html"] — job title span.job-salary — FONT-ENCODED (see above) ul.tag-list li — experience / education / skill tags div.job-card-footer a.boss-info[href="/gongsi/{ID}~.html"] span.boss-name — company name span.company-location — e.g. "上海·徐汇区·龙华" ``` ```python def extract_job_cards_dom(): raw = js(""" (function() { var cards = document.querySelectorAll('.job-card-box'); var results = []; for (var i = 0; i < cards.length; i++) { var card = cards[i]; function getText(sel) { var el = card.querySelector(sel); return el ? el.textContent.trim() : ''; } function getHref(sel) { var el = card.querySelector(sel); return el ? el.href : ''; } var tags = []; var tagEls = card.querySelectorAll('.tag-list li'); for (var t = 0; t < tagEls.length; t++) { tags.push(tagEls[t].textContent.trim()); } results.push({ title: getText('.job-name'), salary_raw: getText('.job-salary'), tags: tags, company: getText('.boss-name'), location: getText('.company-location'), job_url: getHref('.job-name'), company_url: getHref('.boss-info') }); } return JSON.stringify(results); })() """) return json.loads(raw) ``` 15 cards per page. Scroll to lazy-load more. --- ## Gotchas - **Always prefer the API** — `salaryDesc` returns human-readable salary. DOM salary uses font-encoded PUA chars that require OCR or font-file reversal to decode. - **API needs browser session** — `/wapi/` endpoints require cookies from a real browser page load. Use `js()` + `fetch()` inside the browser, not Python `http_get`. - **securityId vs encryptJobId** — the job detail API uses `securityId` (long opaque string), NOT `encryptJobId`. Both come from the job list response. - **`brandComInfo` not `brandInfo`** — the job detail response uses `zpData.brandComInfo` (not `brandInfo`). Company name is `brandName` (not `companyName`), logo is `logo` (not `brandLogo`). Industry/scale/stage are numeric codes — use `industryName`/`scaleName`/`stageName` for display strings. - **SPA routing** — URL doesn't change when filters are applied via the DOM. With the API, filters are explicit query params. - **Page-based pagination is unreliable** — `page=2` often returns 0 results even when `hasMore` is true on page 1. Vary filters instead of paginating deep. - **Filter params need context** — setting `experience`, `salary`, or `degree` to non-zero values may return `code: 200404` without a valid `encryptExpectId`. Use empty strings for default/no-preference browsing. - **City codes are numeric** — not city names. Use the filter conditions API or city/site.json to look up codes. - **`wait(2-3)` after `goto_url`** — the SPA needs time to establish the session before API calls work. - **Anti-bot detection** — zhipin.com may redirect to about:blank after ~1-2 seconds of page load. Run API calls immediately after navigation in the same execution context. - **City code 101280100 = Guangzhou, NOT Shenzhen** — the code previously documented for Shenzhen is actually Guangzhou.