匹配范围
*://we.51job.com/*51job-search-jobs用途 在前程无忧公开搜索页读取职位搜索结果,适合按关键词、城市和分页获取轻量职位列表。 执行前提 需要先打开 https://we.51job.com/pc/search?keyword=前端&searchType=2 或其他 we.51job.com 页面后执行。 脚本在浏览器页面主世界请求公开搜索接口。若页面被站点风控、验证码或网络策略拦截,可能返回错…
*://we.51job.com/*dom.read1return await (async () => {2 const input = readParams();3 const keyword = toText(input.keyword == null ? "前端" : input.keyword);4 if (!keyword) {5 throw new Error("keyword is required.");6 }7 8 const page = readInteger(input.page, 1, 1, 100, "page");9 const limit = readInteger(input.limit, 10, 1, 50, "limit");10 const sortType = readSortType(input.sortType == null ? input.sort : input.sortType);11 const areaInfo = resolveArea(input);12 13 const data = await fetchJobSearch({14 keyword,15 areaCode: areaInfo.areaCode,16 page,17 limit,18 sortType,19 });20 21 const body = data.resultbody || data.resultBody || data.data || {};22 const jobBlock = body.job || body.jobs || body.searchResult || {};23 const items = firstArray([24 jobBlock.items,25 jobBlock.jobList,26 jobBlock.list,27 body.items,28 body.jobList,29 body.list,30 ]);31 const total = readTotal(jobBlock, body, items.length);32 const jobs = items.slice(0, limit).map(normalizeJob);33 34 return {35 keyword,36 area: areaInfo.area,37 areaCode: areaInfo.areaCode,38 page,39 limit,40 sortType,41 total,42 count: jobs.length,43 jobs,44 };45 46 function readParams() {47 const value = typeof params === "undefined" ? {} : params;48 if (value == null) {49 return {};50 }51 if (typeof value !== "object" || Array.isArray(value)) {52 throw new Error("params must be an object.");53 }54 return value;55 }56 57 function fetchJobSearch(options) {58 const query = new URLSearchParams({59 api_key: "51job",60 timestamp: String(Date.now()),61 keyword: options.keyword,62 searchType: "2",63 function: "",64 industry: "",65 jobArea: options.areaCode,66 jobArea2: "",67 landmark: "",68 metro: "",69 salary: "",70 workYear: "",71 degree: "",72 companyType: "",73 companySize: "",74 jobType: "",75 issueDate: "",76 sortType: options.sortType,77 pageNum: String(options.page),78 pageSize: String(options.limit),79 source: "1",80 accountId: "",81 pageCode: "sou|sou|soulb",82 });83 84 const url = "https://we.51job.com/api/job/search-pc?" + query.toString();85 return fetch(url, {86 method: "GET",87 credentials: "include",88 headers: {89 accept: "application/json, text/plain, */*",90 },91 }).then(async (response) => {92 const text = await response.text();93 if (!response.ok) {94 throw new Error("Request failed with HTTP " + response.status + ".");95 }96 97 let json;98 try {99 json = JSON.parse(text);100 } catch (error) {101 throw new Error("The site returned a non-JSON response. Open a normal search page and try again.");102 }103 104 if (json && json.status != null && String(json.status) !== "1") {105 const message = toText(json.message || json.msg || json.error) || "The search API returned an error.";106 throw new Error(message);107 }108 return json;109 });110 }111 112 function resolveArea(value) {113 const area = toText(value.area == null ? "全国" : value.area) || "全国";114 const explicitCode = toText(value.areaCode == null ? value.jobArea : value.areaCode);115 if (isAreaCode(explicitCode)) {116 return { area, areaCode: explicitCode };117 }118 if (isAreaCode(area)) {119 return { area, areaCode: area };120 }121 122 const areaCodes = {123 全国: "000000",124 北京: "010000",125 上海: "020000",126 广州: "030200",127 深圳: "040000",128 天津: "050000",129 重庆: "060000",130 南京: "070200",131 苏州: "070300",132 杭州: "080200",133 宁波: "080300",134 成都: "090200",135 厦门: "110300",136 青岛: "120300",137 合肥: "150200",138 郑州: "170200",139 武汉: "180200",140 长沙: "190200",141 西安: "200200",142 };143 if (!areaCodes[area]) {144 throw new Error("Unsupported area. Use areaCode with the site's six-digit area code.");145 }146 return { area, areaCode: areaCodes[area] };147 }148 149 function isAreaCode(value) {150 return /^\d{6}(,\d{6})*$/.test(value);151 }152 153 function readSortType(value) {154 const text = toText(value == null ? "0" : value).toLowerCase();155 const map = {156 "0": "0",157 default: "0",158 relevance: "0",159 related: "0",160 "1": "1",161 latest: "1",162 newest: "1",163 date: "1",164 };165 if (!map[text]) {166 throw new Error("sortType must be 0/default/relevance or 1/latest/date.");167 }168 return map[text];169 }170 171 function normalizeJob(item) {172 const jobId = firstText(item, ["jobid", "jobId", "jobID", "job_id", "id"]);173 const detailUrl = firstText(item, ["jobHref", "jobUrl", "jobURL", "url", "href"]);174 return {175 jobId,176 jobName: firstText(item, ["jobName", "jobname", "name", "title"]),177 companyName: firstText(item, ["companyName", "companyname", "coname", "company"]),178 city: firstText(item, ["jobAreaString", "jobArea", "workArea", "workarea_text", "cityString", "city"]),179 salary: firstText(item, ["provideSalaryString", "providesalary_text", "salary", "salaryText"]),180 workYear: firstText(item, ["workYearString", "workyearString", "workyear", "workYear"]),181 degree: firstText(item, ["degreeString", "degreefrom", "degree", "education"]),182 companyType: firstText(item, ["companyTypeString", "companytype_text", "companyType"]),183 companySize: firstText(item, ["companySizeString", "companysize_text", "companySize"]),184 industry: firstText(item, ["companyIndustryType1Str", "industryType1Str", "industry", "companyIndustry"]),185 welfareTags: firstArray([item.jobTags, item.attribute_text, item.welfareTags, item.welfare]).map(toText).filter(Boolean),186 publishTime: firstText(item, ["issuedate", "issueDate", "publishTime", "releaseTime"]),187 updateTime: firstText(item, ["updateDateTime", "updatedate", "updateTime", "updatetime"]),188 detailUrl,189 };190 }191 192 function firstArray(values) {193 for (const value of values) {194 if (Array.isArray(value)) {195 return value;196 }197 }198 return [];199 }200 201 function firstText(item, keys) {202 for (const key of keys) {203 if (item && item[key] != null && item[key] !== "") {204 return toText(item[key]);205 }206 }207 return "";208 }209 210 function readTotal(jobBlock, body, fallback) {211 const candidates = [212 jobBlock.totalcount,213 jobBlock.totalCount,214 jobBlock.total,215 body.totalcount,216 body.totalCount,217 body.total,218 ];219 for (const value of candidates) {220 const number = Number(value);221 if (Number.isFinite(number) && number >= 0) {222 return number;223 }224 }225 return fallback;226 }227 228 function readInteger(value, defaultValue, min, max, name) {229 const number = value == null || value === "" ? defaultValue : Number(value);230 if (!Number.isInteger(number) || number < min || number > max) {231 throw new Error(name + " must be an integer between " + min + " and " + max + ".");232 }233 return number;234 }235 236 function toText(value) {237 if (value == null) {238 return "";239 }240 if (Array.isArray(value)) {241 return value.map(toText).filter(Boolean).join(" ");242 }243 return String(value).replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();244 }245})();