ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python爬虫实战:51job招聘数据抓取与清洗全攻略

Python爬虫实战:51job招聘数据抓取与清洗全攻略 简介这是一份面向Python初学者与爬虫实践者的多场景实战代码合集聚焦招聘、影视、电商、婚恋及社交平台等真实数据采集需求解决岗位分析、市场调研、内容聚合等典型问题。压缩包共6个Python源文件总大小仅9KB轻量易读涵盖51job岗位信息与技能要求爬取、猫眼电影TOP100评分与票房数据抓取、什么值得买商品评价采集、我主良缘用户画像信息获取以及百度贴吧图片批量下载等核心案例。每个脚本均基于requestsBeautifulSoup或类似解析库实现结构清晰、注释友好便于理解HTTP请求构造、HTML解析、反爬应对与数据清洗等关键环节。已有591人学习下载适合用于课堂演示、课设参考或自学练手可直接运行调试快速掌握多类型网站的爬虫编写逻辑与工程化思路。 我一直在收集各城市的Python岗位薪资数据但手动在51job上一页页翻实在太折磨人复制粘贴到Excel里还容易出错。后来干脆花了一个下午写了个Python爬虫把搜索结果全量拉下来自动清洗成结构化表格。这篇文章就是这次实操的完整记录从页面分析、反爬应对到完整代码全都摊开讲。这个项目适合两类人一是刚学完Python基础、想找真实网站练手的爬虫新手二是需要做招聘数据分析、又不想被重复劳动拖垮的职场人。文章里的思路换到智联、BOSS直聘等其他招聘网站上一样能复用。1. 项目整体设计与技术选型1.1 为什么选51job作为爬取目标选网站这件事直接决定了你的爬虫项目是半小时搞定还是一星期都搞不定。我最后锁定51job是权衡过好几轮的。第一51job的招聘信息覆盖范围广、数据量大搜一个Python关键词能出来几千条结果样本量够分析用。第二它的页面结构虽然也改版过几次但相比BOSS直聘那种重异步加载、接口加密的网站51job的数据获取门槛低很多尤其老版搜索页search.51job.comHTML里直接带着职位数据用requests加BeautifulSoup就能解析出来不需要逆向JS。第三职位信息包含薪资、地点、经验要求、学历要求、发布时间这些字段做数据分析时维度足够丰富。我当时的需求也很明确搜Python相关岗位抓取职位名称、公司名称、薪资范围、工作地点、经验要求、学历要求、发布时间、职位链接这八个字段存到Excel里做后续分析。需求清楚之后技术方案就是水到渠成的事了。1.2 这属于哪一类爬虫技术栈怎么选爬虫圈里常把爬虫分成三类批量型爬虫、增量型爬虫、垂直型爬虫。这个项目严格来说是垂直型爬虫和批量型爬虫的结合体。垂直型体现在我只针对搜索关键词“Python”这一个维度不碰其他行业批量型体现在我需要把搜索结果的所有页数全部抓下来而不是只抓第一页。判断清楚项目类型很重要因为不同类型的爬虫你的代码结构和后续维护思路是完全不同的。技术栈选型上我走的是一条相对保守的路线核心就五个库requests发HTTP请求拿到网页HTMLBeautifulSoup4解析HTML提取职位节点re正则表达式处理页码、薪资等不规则文本pandas把数据整理成DataFrame方便去重和导出openpyxl配合pandas把数据写成Excel文件还有一个容易被新手忽略的库——time它承担了请求限速功能是反爬环节里最关键的一环。至于有些人一上来就推荐Scrapy、Selenium、Playwright我不建议第一版就上。Scrapy虽然框架化、效率高但调试成本和学习曲线对新人不友好Selenium能解决动态渲染问题但会额外启动浏览器爬取速度慢很多等requests方案走不通了再上Selenium也不迟。1.3 环境准备Python、pip、PyCharm写代码之前先把环境备好。我用的是Python 3.10版本Pycharm作为IDE。如果你刚装Python有几点可以留意一下。一是pip安装第三方库时如果速度很慢可以临时换国内镜像源。比如装requestspip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple二是装pandas和openpyxl这两个库时建议一起装pip install pandas openpyxl requests beautifulsoup4 lxml这里lxml也要装它是BeautifulSoup的解析器比Python内置的html.parser快很多在后面解析大量HTML节点时体感差距非常明显。装好之后可以用一行代码验证环境是否正常import requests, bs4, pandas print(requests.__version__, bs4.__version__, pandas.__version__)能正常打印出版本号说明环境没有问题。实测中很多时候爬虫代码本身没写错反而是环境没配置好导致各种莫名其妙的报错所以这步别跳过。2. 核心细节解析与反爬应对2.1 看懂51job的页面结构数据到底藏在哪写爬虫最忌讳的事是拿到一个网站就开始盲写代码。正确做法是先打开浏览器按F12进开发者工具把页面结构摸清楚。我在2024年实操时51job的搜索页面分两套老版search.51job.com搜索结果HTML直接渲染在网页源码里适合初学者解析新版we.51job.com是前后端分离架构搜索结果通过异步接口加载直接请求页面HTML拿到的是空壳框架。我第一版代码选择走老版页面因为数据获取更直接但第二版面对改版时也切换到了接口方式思路后面会讲。老版搜索页的URL长这样https://search.51job.com/list/000000,000000,0000,00,9,99,Python,2,1.html这个URL里藏着不少信息拆开看是这样的。000000,000000表示工作地点范围一个是城市、一个是区域全部是000000代表全国后面的0000和00是职能分类9是行业类型99表示按关键字搜索Python是搜索关键词2是搜索类型最后一个1是页码。想搜具体城市把第一段改成对应的城市代码就行。打开开发者工具的Element面板可以看到每条职位记录都包在一个div.el节点里里面有几个关键子节点a.jname是职位名称和链接a.cname是公司名称span.area是工作地点span.sal是薪资span.time是发布时间。学历和经验要求通常藏在span.tags里。这套选择器在实操过程中不一定永远不变网站改版是常态。我当时就遇到过class从div.el变成div.joblist-item的情况导致第一天写的代码第二天就跑不通了。所以解析时我给自己留了一手BeautifulSoup选择器失效时用正则表达式兜底。两种方案搭配使用能扛住很多突发情况。2.2 请求头伪装与Cookie策略爬虫请求发出去服务器怎么判断你是真人还是程序主要看你请求头里的信息最核心的就是User-Agent。一个Python脚本的默认UA通常是python-requests/2.31.0服务器一看就知道是爬虫。解决方案是伪装成浏览器。我当时用的是Chrome浏览器的UAheaders { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://search.51job.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }User-Agent告诉服务器“我是一个浏览器”Referer告诉服务器“我是从51job首页跳转进来的”。这两个字段是大多数情况下最有效的伪装手段。关于Cookie这里分两种情况。如果你只是访问搜索结果页不登录账号一般不需要带Cookie。但如果你爬了几十页之后触发了风控返回结果变成验证页就需要带上Cookie再试。做法是浏览器打开51job按F12进Network面板刷新页面找到任意一个请求在Request Headers里找到Cookie整串复制出来加进headers里headers[Cookie] 你的Cookie字符串值得注意的是Cookie是有时效性的可能几个小时后就失效了。如果发现带上Cookie后请求依然返回验证页重新去浏览器复制一次就行。这个操作不需要登录账号也能实现除非你的访问已经触发了强制登录。2.3 请求频率控制数据能拿多快取决于你多克制新手最容易犯的错误是“想一口气吃成胖子”for循环里不加任何停顿一次性把上百页全部请求完。结果通常是请求发到十几页时页面开始返回验证码甚至IP被临时封禁。爬虫的本质是高频请求但服务器不是你家硬盘你得尊重它的承受能力。我给自己定的节奏是每页请求之间随机停顿1到3秒import time import random time.sleep(random.uniform(1, 3))1到3秒的随机延时既保证整体耗时还能接受又把请求节奏模拟成了真人浏览的间隔。这里一定要用random.uniform产生随机间隔如果每页间隔固定值服务器同样能通过规律识别出你是程序。顺带说一句反爬不是洪水猛兽大多数普通网站的反爬策略都是针对明显异常的请求模式的。只要你的请求频率合理、UA伪装到位目标网站通常不会为难你。而像验证码、登录墙这类强反爬属于更高层级的防御遇到时优先考虑降低频率和调整策略而不是硬闯。3. 实操过程与核心代码实现3.1 构造搜索URL先把首页请求通现在开始写代码。第一步是构造URL并发起请求验证能不能正常拿到数据。老版51job的页面编码是GBK所以拿到response之后要设置编码否则中文会乱码。import requests def get_html(url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://search.51job.com/, } try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding gbk return resp.text except Exception as e: print(f请求失败: {e}) return None if __name__ __main__: url https://search.51job.com/list/000000,000000,0000,00,9,99,Python,2,1.html html get_html(url) print(html[:500])这段代码里有两个细节值得注意。timeout10是请求超时时间不设置的话遇到网络波动时程序可能卡住很久。resp.encoding gbk是强制指定编码requests默认会根据响应头猜测编码但有时候猜得并不准手动指定更稳妥。打印出来前500个字符如果你能看到HTML标签说明请求通了如果看到的是验证码页面或者空白页说明被拦截了回头检查请求头。3.2 解析职位数据BeautifulSoup与正则双保险拿到HTML之后第二步是解析。我用BeautifulSoup为主、正则兜底的策略。from bs4 import BeautifulSoup import re def parse_page(html): rows [] soup BeautifulSoup(html, lxml) items soup.select(div.el) if not items: # 选择器失效时用正则兜底 pattern re.findall( ra[^]*classjname[^]*href([^]*)[^]*(.*?)/a.*? ra[^]*classcname[^]*(.*?)/a.*? rspan[^]*classarea[^]*(.*?)/span.*? rspan[^]*classsal[^]*(.*?)/span, html, re.S ) for item in pattern: rows.append({ 职位链接: item[0], 职位名称: re.sub(r.*?, , item[1]).strip(), 公司名称: item[2].strip(), 工作地点: item[3].strip(), 薪资: item[4].strip(), }) else: for item in items: try: title_tag item.select_one(a.jname) company_tag item.select_one(a.cname) area_tag item.select_one(span.area) sal_tag item.select_one(span.sal) time_tag item.select_one(span.time) title title_tag.get_text(stripTrue) if title_tag else company company_tag.get_text(stripTrue) if company_tag else area area_tag.get_text(stripTrue) if area_tag else salary sal_tag.get_text(stripTrue) if sal_tag else publish_time time_tag.get_text(stripTrue) if time_tag else link title_tag.get(href) if title_tag else rows.append({ 职位名称: title, 公司名称: company, 工作地点: area, 薪资: salary, 发布时间: publish_time, 职位链接: link, }) except Exception: continue return rows这里要重点讲讲为什么需要“双保险”。我第一次跑这套代码的时候BeautifulSoup选择器解析得很顺利。但过了两个星期再跑发现select(div.el)返回空列表。打开浏览器一看页面改版了职位节点class换成了别的名字。因为正则是对HTML原文做匹配受class命名影响相对小所以能兜住一部分场景。当然正则也不是万能钥匙HTML属性顺序一变、标签层级一改正则也会失效。遇到那种情况最可靠的方式是重新打开F12看最新的页面结构更新选择器。解析时还有一个容易翻车的地方有些职位是“急聘”或“置顶”标签这些标签也会以span的形式出现在节点里如果不做过滤可能把标签文本误当成字段内容。所以我统一用get_text(stripTrue)方法提取纯文本并用try/except把单条解析失败的情况隔离掉。一条数据解析失败不应该影响整页数据。3.3 薪资字段清洗把字符串变成能分析的数值原始数据里薪资字段长这样“1.5-2.5万/月”“6-8千/月”“面议”“30-50万/年”。这种字符串直接存进Excel人眼能看懂但没法做统计、排序、画图。所以我写了一个薪资清洗函数把范围字符串拆成最低薪资和最高薪资两个数值字段统一换算成元/月。import re def parse_salary(salary_str): if not salary_str or salary_str.strip() 面议: return None, None unit_map {千: 1000, 万: 10000} match re.search(r(\d(?:\.\d)?)-(\d(?:\.\d)?)([千万]), salary_str) if not match: return None, None low_str, high_str, unit match.groups() low float(low_str) * unit_map[unit] high float(high_str) * unit_map[unit] if 年 in salary_str: low / 12 high / 12 return int(round(low)), int(round(high))这个函数的逻辑是先用正则从字符串里提取两个数字和单位再根据单位换算成具体数值。如果是年薪除以12转成月薪。最终返回两个整数方便后续做区间分析和平均值计算。这里有个细节需要注意薪资单位“千”和“万”的换算新手最容易搞混。6-8千/月如果不换算直接当成数值会以为是6到8元1.5-2.5万/月如果不换算会以为是1.5到2.5元。换算成元之后6千变60001.5万变15000数据才有分析价值。至于“面议”这类缺失薪资的情况我选择保留为None在后续分析时单独统计而不是直接删掉。因为面议岗位的数量本身也是市场行情的一个重要参考维度。3.4 翻页、去重、保存Excel完整代码整合上面几个函数准备好之后主流程就很简单了构造第一页URL解析出总页数循环遍历所有页面把数据汇总到列表最后用pandas去重并保存。总页数可以从页面里用正则提取常见格式是“共XX页”或者totalPage XX。我用正则找这一句def get_total_pages(html): match re.search(r共(\d)页, html) if match: return int(match.group(1)) match re.search(rtotalPage[:\s]*(\d), html) if match: return int(match.group(1)) return 1主函数整合起来大概长这样import time import random import pandas as pd from urllib.parse import quote def main(): keyword Python all_data [] first_url fhttps://search.51job.com/list/000000,000000,0000,00,9,99,{quote(keyword)},2,1.html first_html get_html(first_url) if not first_html: print(首页请求失败程序退出) return total_pages get_total_pages(first_html) print(f共找到 {total_pages} 页数据) for page in range(1, total_pages 1): url fhttps://search.51job.com/list/000000,000000,0000,00,9,99,{quote(keyword)},2,{page}.html html get_html(url) if html: page_data parse_page(html) # 清洗薪资字段 for item in page_data: low, high parse_salary(item.get(薪资, )) item[最低月薪] low item[最高月薪] high all_data.extend(page_data) print(f第 {page} 页抓取完成累计 {len(all_data)} 条) else: print(f第 {page} 页请求失败跳过) # 随机延时模拟真人访问 time.sleep(random.uniform(1, 3)) df pd.DataFrame(all_data) df df.drop_duplicates(subset职位链接, keepfirst) df.to_excel(51job_python_jobs.xlsx, indexFalse, engineopenpyxl) print(f数据保存完成共 {len(df)} 条去重记录) if __name__ __main__: main()翻页变量就是URL最后那个数字这个设计让多页爬取非常顺手。quote(keyword)函数负责把中文关键词转换成URL编码如果搜索词是中文比如“数据分析”不转码的话URL会报错。去重用drop_duplicates(subset职位链接)因为同一个职位可能在搜索列表里出现多次但链接是唯一的。文件保存用to_excel需要指定engineopenpyxl否则可能报错。这段代码跑下来当时大概抓了1500多条Python岗位数据耗时十几分钟算是比较温和的节奏了。4. 常见问题与排查技巧实录4.1 高频请求触发了百度安全验证怎么处理项目进行到一半时我遇到了一个典型的反爬问题连续请求了大概40多页之后获取到的HTML不再是职位列表而是一个“百度安全验证”页面。51job部分页面接入了百度安全验证用来拦截异常流量。我第一次遇到时也很头疼毕竟请求频率已经控制在1到3秒了还是被拦了。排查下来主要原因有三个一是长时间高频率访问同一关键词触发了风控阈值二是请求头缺少Cookie没有会话状态三是没有携带浏览器访问的其他特征头。解决思路也分步骤来。先把请求频率降下来延时拉大到3到5秒。然后在请求头中带上Cookie重新尝试。如果还是被拦截就停止爬取一段时间等IP解封再继续。另外可以打乱爬取顺序不要一直从第1页顺序爬到第100页而是定期更换关键词模拟真实用户的多样搜索行为。实测下来前两个措施组合使用后验证码出现的概率大幅下降。提示遇到验证码是爬虫绕不开的正常现象保持克制、放慢速度比硬碰硬有效得多。4.2 解析结果为空多半是页面结构变了另一个高频问题请求正常返回但BeautifulSoup解析出来的列表是空的。这不是爬虫功力的问题而是页面调整了结构。网站在改版时HTML里class命名的调整是常事。排查手段很简单把拿到的HTML保存成本地文件用浏览器打开看一眼页面上职位数据到底在哪个节点下面。代码里加一行就能调试with open(debug.html, w, encodingutf-8) as f: f.write(html)然后用浏览器打开debug.htmlF12定位到任意一条职位数据看看它的父节点是什么结构再更新代码里的选择器。如果整个HTML里根本找不到职位数据说明页面数据是通过JavaScript动态加载的也就是async接口返回这种时候requests直接拿HTML的方式就行不通了需要用浏览器开发者工具的Network面板找到真正的数据接口URL直接请求接口。我当时实际遇到过新版页面改成这种模式后来直接在Network里找到了数据接口返回的是JSON内容解析起来比HTML还方便。用requests请求JSON接口再用json模块解析速度和效率都比解析HTML高。这说明爬虫工作中分析数据来源比死磕解析代码更重要。4.3 乱码、Cookie失效、去重失败等高频坑中文乱码。页面显示正常Python打印出来却是一堆Ã¥Â之类的乱码。多半是编码设置不对。51job老版页面是GBK新版页面是UTF-8不同页面用不同编码最简单的方式是打印resp.apparent_encoding看看requests检测到的编码是什么再手动指定。Cookie失效。带Cookie跑了一段时间之后突然又开始出现验证码大概率是Cookie过期了。去浏览器里重新复制一次就行。不要试图自己伪造Cookie服务端的加解密逻辑你猜不透。重复数据。翻页抓取过程中部分职位会因为你搜的关键词匹配到多个字段而重复出现。解决办法就是drop_duplicates。但要注意去重最好在拼装完所有数据之后做避免每页单独去重导致后续页面重复数据混入。Excel写入报错。pandas的to_excel方法依赖openpyxl或xlsxwriter库没装会报ModuleNotFoundError。装一下openpyxl即可。另外如果字段里包含非法字符比如某些公司名称里的特殊符号也可能会写入失败可以清洗字段时把非法的控制字符替换掉。请求超时。有些页面响应慢尤其翻到后面几页时。设置timeout参数并加重试机制我习惯用三层重试重试间隔3秒。单条解析异常。一条记录里缺了某个字段导致取属性时报错。用try/except把单条数据解析包起来失败的就跳过保证整页数据不受影响。4.4 问题排查速查表整理一个速查表方便你定位问题时自查现象可能原因排查方向请求返回验证页请求频率过高、无Cookie降低频率带Cookie重试解析结果为空页面结构调整、数据动态加载保存HTML调试F12定位中文乱码编码设置错误手动指定encoding401/403状态码UA特征明显、IP受限更换UA带完整请求头数据有重复搜索结果交叉匹配按职位链接去重部分字段缺失原页面没展示对应信息设为空值不影响整体Excel保存报错缺依赖库或字段有非法字符安装openpyxl清洗特殊字符Cookie失效会话过期重新从浏览器复制5. 项目扩展与学习路径建议5.1 从批量爬虫升级成增量爬虫上面这套代码是典型的批量型爬虫每次运行都把全部数据重新抓一遍。问题是51job的职位信息更新很快今天抓的数据明天可能有增减全量重抓既慢又浪费。改进方向是把项目升级成增量型爬虫。核心思路很简单每次抓取前先读取上一次保存的职位链接集合抓取过程中如果发现某个职位链接已经存在就跳过只把新出现的职位添加进去。这样每次运行只抓新增数据耗时短、对目标网站压力也小。代码实现上只需要在main函数里加一步import os existing_links set() if os.path.exists(51job_python_jobs.xlsx): old_df pd.read_excel(51job_python_jobs.xlsx) existing_links set(old_df[职位链接]) # 在循环里判断 for item in page_data: if item[职位链接] not in existing_links: all_data.append(item)这样就把批量爬虫升级成了增量爬虫每天定时跑一次维护一个持续更新的岗位数据库。再进一步还可以把变化记录到日志里分析每天新增岗位的薪资趋势这已经不是爬虫的范畴而是数据运营的玩法了。5.2 数据可视化拿到数据之后还能做什么爬虫只是手段分析才是目的。我当时把1500多条件岗位数据存进Excel之后用pandas做了几个简单维度的分析收获比爬数据本身大得多。比如按城市分组统计Python岗位数量能看出来哪些城市招聘需求量最大按薪资区间统计能看出市场薪资的中位数和分位数按经验要求分组能看出不同年限对应的薪资天花板。这些维度拼起来基本就是一份Python岗位市场行情报告。如果你愿意再进一步用pyecharts或matplotlib画几张图表把城市岗位数量做成柱状图、把薪资分布做成箱线图效果会更直观。我当时顺手做了一个按城市的平均薪资排行发现一线城市Python岗位的平均薪资差异比想象中要大这对求职者选城市、选公司都有直接参考价值。5.3 写在最后的经验总结这次爬虫项目做下来我个人最大的感受是爬虫的核心价值不在代码本身而在分析和适应。写爬虫的过程就是把网站的语言翻译成数据的语言网站一改版你的翻译规则就得跟着变这恰恰是爬虫技术最有意思的地方。实际踩过几次坑之后我也养成了一些习惯每天爬数据前先看一条测试数据确认页面结构没变重要数据阶段性地做一次全量备份爬取任务放在凌晨执行避开业务高峰期既高效又更不容易触发反爬。另外提醒一句爬虫只适合用于个人学习研究和合规的数据分析场景实际使用时一定要遵守目标网站的规则和当地法律法规把频率控制在合理范围内切勿对目标网站造成压力。如果你也正打算写一个招聘网站爬虫从51job开始按这篇文章的步骤一步步跟着做大概率一个下午就能跑通全流程。项目跑通之后把代码里的解析逻辑替换成其他网站的HTML结构一套思路能复用到很多场景里。这大概是爬虫这个方向回报率最高的一件事一次投入永久复利。本文还有配套的精品资源点击获取
返回列表