灵猴市集Linghou Market

前程无忧职位搜索

前程无忧51job-search-jobs

用途 在前程无忧公开搜索页读取职位搜索结果,适合按关键词、城市和分页获取轻量职位列表。 执行前提 需要先打开 https://we.51job.com/pc/search?keyword=前端&searchType=2 或其他 we.51job.com 页面后执行。 脚本在浏览器页面主世界请求公开搜索接口。若页面被站点风控、验证码或网络策略拦截,可能返回错…

职位搜索招聘
版本1.0.0
扫描passed
更新2026-06-28
超时30000ms

匹配范围

*://we.51job.com/*

排除范围

未声明

能力声明

dom.read

代码

源码已展开
1return 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})();