一、目标网站
BOSS 直聘 (www.zhipin.com)
二、分析思路
考虑到要进行整站爬取, 首先要熟悉这个网站的各个板块结构是怎么样的.
首先用浏览器访问 BOSS 直聘首页 (www.zhipin.com). 在首页面, 按照从上到下从左到右的顺序浏览各个板块, 通过分析比较各个板块内容, 决定采用左侧导航栏提供的职位分类来根据职位结合城市信息获取整站数据.
分析页面源码, 解析出所有职位分类信息以及深层链接, 将链接拼接完整, 根据数据特点构建数据结构, 将构建好的数据结构采用 MongoDB 进行持久化. (这里采用 MongoDB 的原因是其为文档型数据库, 存储数据较为灵活, 不用像 MySQL 这种关系型数据库需要事先考虑表结构以及字段相关的设计) 到此, 对整站所有职位按照分类已全部解析入库, 接下来需要根据职位分类结合城市信息获取到所有城市的招聘岗位信息.
采用抽样分析, 随机选择几个职位与城市的组合进行查询, 对比查询结果发现是以 URL 参数影响最终查询结果的. 职位编号唯一对应某一个职位, 城市编号唯一对应某一个城市. 由上面的步骤获取到了所有职位对应的链接地址, 其中包含职位的编号, 因此这里不需要再考虑职位编号. 点击页面上 所有城市 标签, 弹出一个模态框可进行城市选择, 考虑此为一个异步请求, 接着分析页面元素, 发现 所有城市 对应 <a> 标签存在点击事件监听, 跳转到对应 JS 代码中继续分析其所绑定的点击事件, 在当前 JS 文件中根据关键字进行全文搜索, 配合 Debug 添加断点调试 JS 代码, 最终获取到 Ajax 请求地址, 尝试将该地址拼接完整后模拟浏览器请求, 得到包含所有城市信息的 JSON 数据, 从中可得到所有城市对应的其唯一编号, 根据获取到的 JSON 数据的特点构建数据结构存储所有城市以及其对应的唯一编号, 采用 MongoDB 进行持久化.
上述步骤获取到了所有职位的链接地址以及城市编号作为查询参数, 接下来将链接与参数进行组合, 通过进一步的请求即可得到所有职位在全国各个城市的招聘岗位信息.
将上述链接与参数信息从数据库中取出进行组合, 对组合后新的链接发起请求, 解析响应数据获取基本的招聘岗位信息以及详情页链接, 将详情页链接拼接完整并进一步请求获取详情页响应数据进行解析入库
三、部分代码展示
# 导入相关模块
import datetime import requests import xlsxwriter from pymongo import MongoClient from lxml import etree
# 基本配置
# 创建数据库连接 client = MongoClient("localhost", 27017) # 初始化数据库 db = client["Bosspro"] # 设置请求头 headers = {"user-agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" "AppleWebKit/537.36 (KHTML, like Gecko)" "Chrome/79.0.3915.0 Safari/537.36 Edg/79.0.287.3" ) } # 设置目标 URL url = "https://www.zhipin.com"
# 数据解析
# 获取响应数据 response = requests.get(url=url, headers=headers) # 获取 HTML 源文件内容 page_html = response.text # 转换为 etree 对象 page_tree = etree.HTML(page_html) dl_list = page_tree.xpath('//div[@class="job-menu"]//dl') # 数据结构 """ [ "category": "A" "sub_category": [ { "node": "a", "data": [ { "position": "a1", "param": "/a1/001/" }, { "position": "a2", "param": "/a2/001" } ] }, { "node": "b", "data": [ { "position": "b1", "param": "/b1/001/" }, { "position": "b2", "param": "/b2/001/" } ] } ] ] """ data_list = [] for dl in dl_list: category = dl.xpath('./div[2]/p/text()')[0] li_list = dl.xpath('./div[2]/ul//li') li_data_dict = { "category": category, "sub_category": [ { "node": li.xpath('./h4/text()')[0], "data": [ { "position": a.xpath('./text()')[0], "param": a.xpath('./@href')[0] } for a in li.xpath('./div//a') ] } for li in li_list ] } data_list.append(li_data_dict)
# 持久化
# 将分好类的职位数据以构造好的数据结构存入数据库 # 检测数据更新, 若最新数据与数据库中数据存在差异则更新为最新数据 for data in data_list: db.position.update_one( data, {"$setOnInsert": data}, upsert=True ) data = db.position.find({}) # 网站数据更新, 数据库更新并去重 d = {} for dic in data: d.setdefault(dic["category"], []) d.get(dic["category"]).append(1) for k, v in d.items(): if len(v) > 1: delete_obj = db.position.find_one({"category": k}) db.position.delete_one(delete_obj)
# 其他数据请求
# 获取城市信息及其唯一编码 url = "https://www.zhipin.com/wapi/zpCommon/data/city.json" response = requests.get(url=url, headers=headers) page_json = response.json() zpData = page_json["zpData"]["cityList"] # 数据结构 """ [ { 'name': '北京', 'subLevelModelList': [ { 'name': '北京', 'code': '101010100' } ] }, { 'name': '上海', 'subLevelModelList': [ { 'name': '上海', 'code': '101020100' } ] }, { 'name': '天津', 'subLevelModelList': [ { 'name': '天津', 'code': '101030100' } ] } ] """ country_data_list = [ { "name": dic["name"], "subLevelModelList": [ { "name": subLevelModel["name"], "code": str(subLevelModel["code"]) } for subLevelModel in dic["subLevelModelList"] ] } for dic in zpData]
# 其他数据持久化
# 将分好类的职位数据以构造好的数据结构存入数据库 for data in country_data_list: db.country.update_one( data, {"$setOnInsert": data}, upsert=True ) data = db.country.find({}) # 网站数据更新, 数据库更新并去重 d = {} for dic in data: d.setdefault(dic["name"], []) d.get(dic["name"]).append(1) for k, v in d.items(): if len(v) > 1: delete_obj = db.country.find_one({"name": k}) db.country.delete_one(delete_obj)
# 生成 xls 文件
# 将分好类的职位数据存入 Excel workbook = xlsxwriter.Workbook('{}.xlsx'.format(datetime.date.today())) cell_format = workbook.add_format({ 'border': 1, 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1 }) merge_format = workbook.add_format({ 'bold': True, 'border': 1, 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1 }) param_list = [] for dic in data_list: row = 0 col = 0 worksheet = workbook.add_worksheet(dic["category"].replace("/", "")) worksheet.set_column(0, 0, 20) worksheet.set_column(1, 1, 30) worksheet.set_column(2, 2, 50) worksheet.write(0, 0, "职位大类", merge_format) worksheet.write(0, 1, "职位细分", merge_format) country_col = 2 for country in country_data_list: worksheet.write(0, country_col, country["name"], merge_format) worksheet.set_column(country_col, country_col, 50) country_col += 1 for node_dic in dic["sub_category"]: if len(node_dic["data"]) == 1: worksheet.write( row+1, col, '{}\n({}个细分职业)'.format(node_dic["node"], len(node_dic["data"])), cell_format ) else: worksheet.merge_range( row+1, col, row+len(node_dic["data"]), col, '{}\n({}个细分职业)'.format(node_dic["node"], len(node_dic["data"])), cell_format ) data_row = row + 1 data_col = col + 1 for data in node_dic["data"]: country_data_col = data_col + 1 worksheet.write(data_row, data_col, data["position"], cell_format) for country in country_data_list: code = country["subLevelModelList"][0]["code"] param_list.append(data["param"]) worksheet.write( data_row, country_data_col, data["param"], cell_format ) country_data_col += 1 data_row += 1 row += len(node_dic["data"]) workbook.close()
四、部分数据展示