Python 爬虫实战:爬取 58 同城招聘信息
本文详细介绍了基于Python爬虫技术抓取58同城招聘信息的全流程实现方案。通过requests库构建HTTP请求,结合BeautifulSoup解析HTML页面,提取岗位名称、薪资范围、企业信息等核心字段,并采用CSV格式进行结构化存储。针对58同城的反爬机制,重点讲解了随机User-Agent、请求间隔控制等规避策略,同时强调了爬取过程中的合规性问题。文章还提供了进阶优化方向,包括动态页面处理

前言
就业市场的信息获取效率直接影响求职者的决策与企业的招聘效果,58 同城作为综合性生活服务平台,汇聚了海量的全职、兼职招聘信息,涵盖行业、薪资、工作地点、岗位职责等核心维度。通过 Python 爬虫技术系统化抓取这些招聘数据,既能为求职者提供多维度的岗位对比依据,也可用于就业市场的行业薪资分析、岗位需求趋势研究等专业场景。本文将从技术原理、实战开发、数据处理等维度,完整讲解如何合规、高效地爬取 58 同城招聘信息,兼顾代码的稳定性与数据的结构化呈现。
摘要
本文聚焦 58 同城招聘信息的爬虫实战开发,详细阐述了基于requests库的 HTTP 请求构建、BeautifulSoup库的 HTML 解析、数据清洗与 CSV 结构化存储的全流程。通过实战案例,以58 同城北京招聘页面为爬取目标,实现岗位名称、薪资范围、招聘企业、工作地点、岗位类型、发布时间、岗位职责、岗位链接等核心字段的抓取,并通过表格可视化展示爬取结果,同时讲解 58 同城反爬策略规避、数据合法性等关键问题。最终输出标准化的招聘数据集,为后续的就业市场分析奠定基础。
一、技术原理与环境准备
1.1 核心技术栈说明
本次爬虫开发依赖的 Python 库及核心作用如下表所示,确保版本适配以避免兼容性问题:
| 库名称 | 版本要求 | 核心作用 |
|---|---|---|
| requests | ≥2.31.0 | 发送 HTTP/HTTPS 请求,获取网页源代码 |
| BeautifulSoup4 | ≥4.12.0 | 解析 HTML 文档,定位并提取目标数据字段 |
| csv | 内置库 | 将爬取的非结构化数据存储为标准化 CSV 文件 |
| time | 内置库 | 设置请求间隔,降低反爬触发概率 |
| fake-useragent | ≥1.4.0 | 生成随机 User-Agent,模拟真实浏览器请求 |
| lxml | ≥4.9.3 | 作为 BeautifulSoup 解析器,提升 HTML 解析效率 |
1.2 环境搭建
执行以下命令安装第三方依赖库,建议在虚拟环境中操作以避免环境冲突:
bash
运行
pip install requests beautifulsoup4 fake-useragent lxml
1.3 反爬机制与规避思路
58 同城针对爬虫设置了多维度的反爬策略,核心及对应规避方案如下:
| 反爬策略 | 具体表现 | 规避思路 |
|---|---|---|
| User-Agent/Referer 检测 | 非浏览器请求头返回空数据或 403 错误 | 使用 fake-useragent 生成随机合法 User-Agent,Referer 设置为 58 同城首页 |
| 高频请求限制 | 短时间多次请求触发 IP 封禁 / 滑块验证码 | 设置 3-8 秒随机请求间隔,单 IP 请求频率≤3 次 / 分钟 |
| Cookie 验证 | 部分页面需有效 Cookie 才能加载完整招聘信息 | 采用 Session 维持 Cookie 状态,或从浏览器手动提取有效 Cookie 补充 |
| 页面结构动态混淆 | 核心数据类名 / 标签不定期随机变化 | 增加多维度定位逻辑(如标签 + 文本特征),降低页面更新影响 |
| 虚假数据干扰 | 页面混入无效占位数据,干扰爬虫提取 | 增加数据有效性校验(如薪资格式、企业名称非空) |
二、实战开发:爬取 58 同城招聘信息
2.1 目标分析
本次爬取目标为58 同城北京招聘列表页面,需提取的核心字段如下:
- 岗位名称
- 薪资范围(如 8k-15k / 月)
- 招聘企业名称
- 工作地点(含行政区 + 具体地址)
- 岗位类型(全职 / 兼职 / 实习)
- 发布时间(如 1 小时前 / 昨天)
- 岗位职责(核心任职要求)
- 岗位详情链接
2.2 完整代码实现
python
运行
import requests
from bs4 import BeautifulSoup
import csv
import time
import random
from fake_useragent import UserAgent
# 初始化UserAgent生成器
ua = UserAgent()
# 创建Session维持Cookie状态
session = requests.Session()
# 目标URL(北京招聘列表页)
base_url = "https://bj.58.com/job/"
# 存储爬取数据的列表
job_data = []
# 请求头配置(可补充浏览器真实Cookie提升稳定性)
headers = {
"User-Agent": ua.random,
"Referer": "https://www.58.com/",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
# 可选:补充浏览器Cookie,格式示例
# "Cookie": "58home=bj; id58=xxx; city=bj;"
}
def clean_text(text):
"""
清洗文本数据,去除冗余空格、换行符等
:param text: 原始文本
:return: 清洗后的文本
"""
if not text:
return "未知"
# 去除多余空格、换行、制表符
return " ".join(text.strip().split())
def get_job_detail(detail_url):
"""
爬取岗位详情页的岗位职责信息
:param detail_url: 岗位详情链接
:return: 岗位职责文本
"""
try:
# 发送详情页请求,设置更长超时时间
response = session.get(detail_url, headers=headers, timeout=20)
response.raise_for_status()
response.encoding = "utf-8"
soup = BeautifulSoup(response.text, "lxml")
# 定位岗位职责(多维度匹配,适配页面结构变化)
duty_tags = [
soup.find("div", class_="job_intro"),
soup.find("div", attrs={"data-name": "jobDesc"}),
soup.find("div", class_="desc_con")
]
duty_text = "未知"
for tag in duty_tags:
if tag:
duty_text = clean_text(tag.get_text())
break
# 详情页爬取后休眠,降低频率
time.sleep(random.uniform(1, 3))
return duty_text
except Exception as e:
print(f"详情页爬取异常 {detail_url}:{e}")
return "未知"
def get_job_data(page_num):
"""
爬取指定页码的招聘数据
:param page_num: 页码数
"""
# 构造分页URL(58同城招聘分页参数为pn)
page_url = f"{base_url}pn{page_num}/" if page_num > 1 else base_url
try:
# 发送GET请求,设置超时时间15秒
response = session.get(page_url, headers=headers, timeout=15)
# 验证请求状态码
response.raise_for_status()
# 统一编码格式
response.encoding = "utf-8"
# 解析HTML文档
soup = BeautifulSoup(response.text, "lxml")
# 定位招聘列表容器(多类名匹配,适配页面更新)
job_list = soup.find_all("div", class_=["job_item", "job_con_item"])
if not job_list:
print(f"第{page_num}页未找到招聘数据,可能触发反爬或页面结构更新")
return
# 遍历提取每条招聘数据
for item in job_list:
# 岗位名称
job_title = clean_text(item.find("a", class_=["job_name", "title"]).get_text()) if item.find("a", class_=["job_name", "title"]) else "未知"
# 薪资范围
salary = clean_text(item.find("span", class_=["salary", "job_salary"]).get_text()) if item.find("span", class_=["salary", "job_salary"]) else "未知"
# 招聘企业
company = clean_text(item.find("a", class_=["comp_name", "company_name"]).get_text()) if item.find("a", class_=["comp_name", "company_name"]) else "未知"
# 工作地点
location = clean_text(item.find("span", class_=["work_addr", "job_address"]).get_text()) if item.find("span", class_=["work_addr", "job_address"]) else "未知"
# 岗位类型
job_type = clean_text(item.find("span", class_=["job_type", "type"]).get_text()) if item.find("span", class_=["job_type", "type"]) else "全职"
# 发布时间
publish_time = clean_text(item.find("span", class_=["publish_time", "time"]).get_text()) if item.find("span", class_=["publish_time", "time"]) else "未知"
# 岗位链接
link_tag = item.find("a", class_=["job_name", "title"])
job_link = link_tag["href"] if (link_tag and "href" in link_tag.attrs) else "未知"
# 补全链接(相对路径转绝对路径)
if job_link.startswith("/"):
job_link = f"https://bj.58.com{job_link}"
# 爬取岗位职责(详情页)
duty = get_job_detail(job_link) if job_link != "未知" else "未知"
# 封装数据为字典
job_item = {
"岗位名称": job_title,
"薪资范围": salary,
"招聘企业": company,
"工作地点": location,
"岗位类型": job_type,
"发布时间": publish_time,
"岗位职责": duty,
"岗位链接": job_link
}
job_data.append(job_item)
# 打印单条数据,便于实时监控
print(f"第{page_num}页已爬取:{job_title} - {salary} - {company}")
# 随机休眠3-8秒,规避反爬
time.sleep(random.uniform(3, 8))
except requests.exceptions.RequestException as e:
print(f"第{page_num}页请求异常:{e}")
except Exception as e:
print(f"第{page_num}页数据解析异常:{e}")
def save_to_csv(data, filename="58同城北京招聘信息.csv"):
"""
将爬取的招聘数据保存为CSV文件
:param data: 爬取的岗位数据列表
:param filename: 保存的文件名
"""
if not data:
print("无有效数据可保存")
return
# 定义CSV表头(与字典键对应)
csv_headers = ["岗位名称", "薪资范围", "招聘企业", "工作地点", "岗位类型", "发布时间", "岗位职责", "岗位链接"]
# 写入CSV文件,utf-8-sig确保中文正常显示
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=csv_headers)
writer.writeheader()
writer.writerows(data)
print(f"\n数据保存完成!共爬取{len(data)}条招聘数据,文件路径:{filename}")
if __name__ == "__main__":
print("开始爬取58同城招聘信息...")
# 爬取1-2页数据(58反爬严格,建议少量测试)
for page in range(1, 3):
print(f"\n========= 开始爬取第{page}页数据 =========")
get_job_data(page)
# 保存数据到CSV
save_to_csv(job_data)
print("\n爬取任务全部完成!")
2.3 代码核心原理解析
(1)Session 与请求头强化
使用requests.Session()维持 Cookie 状态,解决 58 同城的 Cookie 验证机制;请求头中除随机 User-Agent 外,补充Referer为 58 首页,模拟真实用户访问路径,同时设置Accept-Encoding和Accept-Language,进一步降低反爬识别概率。
(2)多维度数据提取逻辑
- 类名容错:58 同城页面类名易更新,采用多类名匹配(如
["job_name", "title"]),避免单一类名失效导致数据提取失败; - 文本清洗:自定义
clean_text函数,去除文本中的冗余空格、换行符、制表符,保证数据整洁; - 详情页爬取:通过
get_job_detail函数爬取岗位职责,采用多标签匹配定位核心内容,适配不同的详情页结构; - 链接补全:将相对路径的岗位链接补全为绝对路径,确保可直接访问。
(3)分页爬取与频率控制
58 同城招聘分页参数为pn(第 2 页为pn2),通过条件判断构造分页 URL;每页爬取后设置 3-8 秒随机休眠,详情页额外设置 1-3 秒休眠,严格控制请求频率,避免触发 IP 封禁或验证码。
(4)异常处理机制
- 网络异常:捕获
requests.exceptions.RequestException处理超时、连接失败、403/404 等请求异常; - 解析异常:捕获通用
Exception处理数据解析过程中的格式错误、索引越界等问题; - 详情页异常:单独捕获详情页爬取异常,避免单条详情页失败影响整页数据提取;
- 数据空值:增加 “未找到招聘数据” 判断,及时反馈反爬触发或页面结构更新。
(5)数据存储设计
将数据封装为字典列表,通过csv.DictWriter写入 CSV 文件,utf-8-sig编码确保中文在 Excel/WPS 中正常显示;表头覆盖核心招聘维度,便于后续的薪资分析、岗位需求统计等场景。
三、输出结果展示
3.1 控制台输出示例
plaintext
开始爬取58同城招聘信息...
========= 开始爬取第1页数据 =========
第1页已爬取:Python开发工程师 - 10k-20k/月 - 北京XX科技有限公司
第1页已爬取:新媒体运营 - 6k-12k/月 - 北京XX文化传媒有限公司
第1页已爬取:销售代表 - 8k-15k/月 - 北京XX商贸有限公司
========= 开始爬取第2页数据 =========
第2页已爬取:UI设计师 - 9k-18k/月 - 北京XX设计有限公司
第2页已爬取:行政助理 - 5k-8k/月 - 北京XX企业管理有限公司
数据保存完成!共爬取5条招聘数据,文件路径:58同城北京招聘信息.csv
爬取任务全部完成!
3.2 CSV 文件输出示例
| 岗位名称 | 薪资范围 | 招聘企业 | 工作地点 | 岗位类型 | 发布时间 | 岗位职责 | 岗位链接 |
|---|---|---|---|---|---|---|---|
| Python 开发工程师 | 10k-20k / 月 | 北京 XX 科技有限公司 | 朝阳区望京 SOHO | 全职 | 1 小时前 | 1. 负责后端接口开发;2. 参与数据库设计;3. 配合前端联调。要求:熟练掌握 Django/Flask 框架,有 2 年以上开发经验。 | https://bj.58.com/job/123456.html |
| 新媒体运营 | 6k-12k / 月 | 北京 XX 文化传媒有限公司 | 海淀区中关村 | 全职 | 昨天 | 1. 负责公众号、小红书内容编辑;2. 策划线上营销活动;3. 统计运营数据。要求:有 1 年以上新媒体运营经验,会 PS 优先。 | https://bj.58.com/job/789012.html |
| 销售代表 | 8k-15k / 月 | 北京 XX 商贸有限公司 | 丰台区马家堡 | 全职 | 3 天前 | 1. 开发新客户,维护老客户;2. 完成月度销售目标;3. 跟进客户订单。要求:沟通能力强,能接受出差。 | https://bj.58.com/job/345678.html |
| UI 设计师 | 9k-18k / 月 | 北京 XX 设计有限公司 | 东城区东直门 | 全职 | 1 天前 | 1. 负责产品界面设计;2. 制作设计原型;3. 配合开发还原设计。要求:熟练使用 Figma/PS,有电商设计经验优先。 | https://bj.58.com/job/901234.html |
四、进阶优化方向
4.1 动态页面适配
58 同城部分招聘数据通过 AJAX 动态加载,单纯requests无法获取完整数据,可通过以下方案优化:
- 接口抓包:F12 开发者工具→Network→XHR,定位招聘数据接口(JSON 格式),直接请求接口获取数据,示例:
python
运行
# 示例:58同城招聘AJAX接口请求
ajax_url = "https://bj.58.com/job/ajax/get_job_list/?pn=1&city=bj"
response = session.get(ajax_url, headers=headers)
data = response.json()
# 解析JSON数据...
- 浏览器模拟:使用
Playwright模拟浏览器渲染,自动处理动态加载、Cookie 验证等问题,示例:
python
运行
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto(base_url)
# 等待数据加载
page.wait_for_selector(".job_item")
# 提取页面源码
html = page.content()
soup = BeautifulSoup(html, "lxml")
# 解析数据...
browser.close()
4.2 数据清洗与标准化
- 薪资标准化:从薪资范围中提取最低 / 最高薪资(如 “10k-20k / 月”→最低 10000,最高 20000),便于数值分析;
- 地点标准化:拆分工作地点为 “行政区” 和 “具体地址”,便于区域统计;
- 去重处理:基于岗位名称 + 企业名称 + 工作地点组合去重,避免重复爬取同一岗位;
- 无效数据过滤:过滤薪资为 “面议”、企业名称为空的无效数据。
4.3 多线程 / 协程优化
针对少量数据爬取场景,引入threading多线程提升效率(58 反爬严格,不建议大规模并发):
python
运行
import threading
from queue import Queue
# 定义任务队列
q = Queue()
# 填充页码队列
for page in range(1, 3):
q.put(page)
def crawl_worker():
while not q.empty():
page = q.get()
get_job_data(page)
q.task_done()
# 启动2个线程
for _ in range(2):
t = threading.Thread(target=crawl_worker)
t.start()
q.join()
4.4 验证码自动处理
若触发滑块验证码,可引入ddddocr库识别验证码,结合requests模拟滑动:
python
运行
import ddddocr
import json
ocr = ddddocr.DdddOcr()
# 抓取验证码图片
captcha_url = "https://cdn.58.com/captcha/xxx.jpg"
captcha_img = session.get(captcha_url).content
# 识别验证码缺口位置
res = ocr.slide_match(captcha_img, target_img, simple_target=True)
# 模拟滑动请求
slide_data = {"distance": res["target"][0], "track": []}
session.post("https://bj.58.com/captcha/slide", data=json.dumps(slide_data))
4.5 可视化分析
基于爬取的数据,使用pandas+pyecharts实现薪资分布可视化:
python
运行
import pandas as pd
from pyecharts import options as opts
from pyecharts.charts import Histogram
# 读取数据
df = pd.read_csv("58同城北京招聘信息.csv")
# 提取薪资下限
df["薪资下限"] = df["薪资范围"].str.extract(r"(\d+)k").astype(float) * 1000
# 绘制薪资分布直方图
hist = (
Histogram()
.add_xaxis(["0-5k", "5k-10k", "10k-15k", "15k-20k", "20k+"])
.add_yaxis("岗位数量", [
len(df[df["薪资下限"] < 5000]),
len(df[(df["薪资下限"] >= 5000) & (df["薪资下限"] < 10000)]),
len(df[(df["薪资下限"] >= 10000) & (df["薪资下限"] < 15000)]),
len(df[(df["薪资下限"] >= 15000) & (df["薪资下限"] < 20000)]),
len(df[df["薪资下限"] >= 20000])
])
.set_global_opts(title_opts=opts.TitleOpts(title="北京招聘岗位薪资分布"))
)
hist.render("薪资分布.html")
五、合规性说明
- 本次爬取仅针对 58 同城公开的招聘信息,未涉及企业商业机密、求职者隐私等敏感数据;
- 爬取频率严格控制在合理范围(单页休眠 3-8 秒),未对 58 同城服务器造成流量压力,符合 “善意爬取” 原则;
- 爬取的数据仅用于学习、研究目的,禁止用于商业用途;
- 遵守 58 同城用户协议及 robots.txt 规则(58 同城 robots.txt:https://www.58.com/robots.txt),如需批量爬取请提前联系平台获取授权;
- 若爬取过程中触发验证码、IP 封禁等反爬机制,应立即停止爬取,待解除限制后再操作,避免违规;
- 不得将爬取的招聘信息用于虚假招聘、恶意竞争等违法违规行为。
六、总结
本文通过完整的实战案例,讲解了 58 同城招聘信息的爬虫开发全流程,涵盖环境搭建、请求构建、多维度数据提取、详情页爬取、分页控制、结构化存储等核心环节。代码具备良好的容错性与适配性,可通过修改目标 URL 中的城市缩写(如上海为sh、广州为gz)适配不同城市的招聘数据爬取。同时,强调了爬虫开发的合规性与道德准则,避免触碰法律红线。后续可基于爬取的数据开展行业薪资分析、岗位需求趋势研究、区域就业市场对比等深度应用,进一步挖掘数据价值。

更多推荐




所有评论(0)