2023年 5月 26日

Selenium&Chrome实战:动态爬取51job招聘信息

一、概述

Selenium自动化测试工具,可模拟用户输入,选择,提交。

爬虫实现的功能:

  1. 输入python,选择地点:上海,北京 —->就去爬取上海,北京2个城市python招聘信息
  2. 输入会计,选择地址:广州,深圳,杭州—->就去爬取广州,深圳,杭州3个城市会计招聘信息
  3. 根据输入的不同,动态爬取结果 

二、页面分析

输入关键字

selenium怎么模拟用户输入关键字,怎么选择城市,怎么点击搜索按钮?

Selenium模拟用户输入关键字,谷歌浏览器右键输入框,点检查,查看代码

通过selenium的find_element_by_id 找到 id = ‘kwdselectid’,然后send_keys(‘关键字’)即可模拟用户输入

代码为:

  1. textElement = browser.find_element_by_id('kwdselectid')
  2. textElement.send_keys('python')

选择城市

selenium模拟用户选择城市— (这个就难了,踩了很多坑)

点击城市选择,会弹出一个框

然后选择:北京,上海,  右键检查,查看源代码

可以发现:value的值变成了”北京+上海”

那么是否可以用selenium找到这个标签,更改它的属性值为”北京+上海”,可以实现选择城市呢?

答案:不行,因为经过自己的几次尝试,发现真正生效的是下面的”010000,020000″,这个是什么?城市编号,也就是说在输入”北京+上海”,实际上输入的是:”010000,020000″, 那这个城市编号怎么来的,这个就需要去爬取51job弹出城市选择框那个页面了,页面代码里面有城市对应的编号

获取城市编号

getcity.py代码:

  1. from selenium import webdriver
  2. from selenium.webdriver.chrome.options import Options
  3. import json
  4. # 设置selenium使用chrome的无头模式
  5. chrome_options = Options()
  6. chrome_options.add_argument("--headless")
  7. # 在启动浏览器时加入配置
  8. browser = webdriver.Chrome(options=chrome_options)
  9. cookies = browser.get_cookies()
  10. browser.delete_all_cookies()
  11. browser.get('https://www.51job.com/')
  12. browser.implicitly_wait(20)
  13. # 找到城市选择框,并模拟点击
  14. button = browser.find_element_by_xpath("//div[@class='ush top_wrap']//div[@class='el on']/p\
  15. [@class='addbut']//input[@id='work_position_input']").click()
  16. # 选中城市弹出框
  17. browser.current_window_handle
  18. # 定义一个空字典
  19. dic = {}
  20. # 找到城市,和对应的城市编号
  21. find_city_elements = browser.find_elements_by_xpath("//div[@id='work_position_layer']//\
  22. div[@id='work_position_click_center_right_list_000000']//tbody/tr/td")
  23. for element in find_city_elements:
  24. number = element.find_element_by_xpath("./em").get_attribute("data-value") # 城市编号
  25. city = element.find_element_by_xpath("./em").text # 城市
  26. # 添加到字典
  27. dic.setdefault(city, number)
  28. print(dic)
  29. # 写入文件
  30. with open('city.txt', 'w', encoding='utf8') as f:
  31. f.write(json.dumps(dic, ensure_ascii=False))
  32. browser.quit()

执行输出:

{'北京': '010000', '上海': '020000', '广州': '030200', '深圳': '040000', '武汉': '180200', '西安': '200200', '杭州': '080200', '南京': '070200', '成都': '090200', '重庆': '060000', '东莞': '030800', '大连': '230300', '沈阳': '230200', '苏州': '070300', '昆明': '250200', '长沙': '190200', '合肥': '150200', '宁波': '080300', '郑州': '170200', '天津': '050000', '青岛': '120300', '济南': '120200', '哈尔滨': '220200', '长春': '240200', '福州': '110200'}

通过selenium的find_element_by_xpath 找到城市编号这个input,然后读取city.txt文件,把对应的城市替换为城市编号,在用selenium执行js代码,就可以加载城市了—代码有点长,完整代码写在后面

selenium模拟用户点击搜索

通过selenium的find_element_by_xpath 找到 这个button按钮,然后click() 即可模拟用户点击搜索

代码为:

browser.find_element_by_xpath("//div[@class='ush top_wrap']/button").click()

以上都是模拟用户搜索的行为,下面就是对数据提取规则

先定位总页数:158页

找到每个岗位详细的链接地址:

最后定位需要爬取的数据

岗位名,薪水,公司名,招聘信息,福利待遇,岗位职责,任职要求,上班地点,工作地点 这些数据,总之需要什么数据,就爬什么

需要打开岗位详细的链接,比如:https://jobs.51job.com/shanghai-mhq/118338654.html?s=01&t=0

三、完整代码

代码介绍

新建目录51cto-selenium,结构如下:

  1. ./
  2. ├── get51Job.py
  3. ├── getcity.py
  4. └── mylog.py

文件说明:

getcity.py  (首先运行)获取城市编号,会生成一个city.txt文件

mylog.py     日志程序,记录爬取过程中的一些信息

get51Job.py 爬虫主程序,里面包含:

  1. Item类 定义需要获取的数据
  2. GetJobInfo类 主程序类
  3. getBrowser方法 设置selenium使用chrome的无头模式,打开目标网站,返回browser对象
  4. userInput方法 模拟用户输入关键字,选择城市,点击搜索,返回browser对象
  5. getUrl方法 找到所有符合规则的url,返回urls列表
  6. spider方法 提取每个岗位url的详情,返回items
  7. getresponsecontent方法 接收url,打开目标网站,返回html内容
  8. piplines方法 处理所有的数据,保存为51job.txt
  9. getPageNext方法 找到总页数,并获取下个页面的url,保存数据,直到所有页面爬取完毕

getcity.py

  1. # !/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. #!/usr/bin/env python
  4. # coding: utf-8
  5. from selenium import webdriver
  6. from selenium.webdriver.chrome.options import Options
  7. import json
  8. # 设置selenium使用chrome的无头模式
  9. chrome_options = Options()
  10. chrome_options.add_argument("--headless")
  11. # 在启动浏览器时加入配置
  12. browser = webdriver.Chrome(options=chrome_options)
  13. cookies = browser.get_cookies()
  14. browser.delete_all_cookies()
  15. browser.get('https://www.51job.com/')
  16. browser.implicitly_wait(20)
  17. # 找到城市选择框,并模拟点击
  18. button = browser.find_element_by_xpath("//div[@class='ush top_wrap']//div[@class='el on']/p\
  19. [@class='addbut']//input[@id='work_position_input']").click()
  20. # 选中城市弹出框
  21. browser.current_window_handle
  22. # 定义一个空字典
  23. dic = {}
  24. # 找到城市,和对应的城市编号
  25. find_city_elements = browser.find_elements_by_xpath("//div[@id='work_position_layer']//\
  26. div[@id='work_position_click_center_right_list_000000']//tbody/tr/td")
  27. for element in find_city_elements:
  28. number = element.find_element_by_xpath("./em").get_attribute("data-value") # 城市编号
  29. city = element.find_element_by_xpath("./em").text # 城市
  30. # 添加到字典
  31. dic.setdefault(city, number)
  32. print(dic)
  33. # 写入文件
  34. with open('city.txt', 'w', encoding='utf8') as f:
  35. f.write(json.dumps(dic, ensure_ascii=False))
  36. browser.quit()

View Code

get51Job.py

  1. # !/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. from selenium import webdriver
  4. from selenium.webdriver.chrome.options import Options
  5. from mylog import MyLog as mylog
  6. import json
  7. import time
  8. import requests
  9. from lxml import etree
  10. class Item(object):
  11. job_name = None # 岗位名
  12. company_name = None # 公司名
  13. work_place = None # 工作地点
  14. salary = None # 薪资
  15. release_time = None # 发布时间
  16. job_recruitment_details = None # 招聘岗位详细
  17. job_number_details = None # 招聘人数详细
  18. company_treatment_details = None # 福利待遇详细
  19. practice_mode = None # 联系方式
  20. class GetJobInfo(object):
  21. """
  22. the all data from 51job.com
  23. 所有数据来自前程无忧招聘网
  24. """
  25. def __init__(self):
  26. self.log = mylog() # 实例化mylog类,用于记录日志
  27. self.startUrl = 'https://www.51job.com/' # 爬取的目标网站
  28. self.browser = self.getBrowser() # 设置chrome
  29. self.browser_input = self.userInput(self.browser) # 模拟用户输入搜索
  30. self.getPageNext(self.browser_input) # 找到下个页面
  31. def getBrowser(self):
  32. """
  33. 设置selenium使用chrome的无头模式
  34. 打开目标网站 https://www.51job.com/
  35. :return: browser
  36. """
  37. try:
  38. # 创建chrome参数对象
  39. chrome_options = Options()
  40. # 把chrome设置成无界面模式,不论windows还是linux都可以,自动适配对应参数
  41. chrome_options.add_argument("--headless")
  42. # 在启动浏览器时加入配置
  43. browser = webdriver.Chrome(options=chrome_options)
  44. # 利用selenium打开网站
  45. browser.get(self.startUrl)
  46. # 等待网站js代码加载完毕
  47. browser.implicitly_wait(20)
  48. except Exception as e:
  49. # 记录错误日志
  50. self.log.error('打开目标网站失败:{},错误代码:{}'.format(self.startUrl, e))
  51. else:
  52. # 记录成功日志
  53. self.log.info('打开目标网站成功:{}'.format(self.startUrl))
  54. # 返回实例化selenium对象
  55. return browser
  56. def userInput(self, browser):
  57. """
  58. 北京 上海 广州 深圳 武汉 西安 杭州
  59. 南京 成都 重庆 东莞 大连 沈阳 苏州
  60. 昆明 长沙 合肥 宁波 郑州 天津 青岛
  61. 济南 哈尔滨 长春 福州
  62. 只支持以上城市,输入其它则无效
  63. 最多可选5个城市,每个城市用 , 隔开(英文逗号)
  64. :return:browser
  65. """
  66. time.sleep(1)
  67. # 用户输入关键字搜索
  68. search_for_jobs = input("请输入职位搜索关键字:")
  69. # 用户输入城市
  70. print(self.userInput.__doc__)
  71. select_city = input("输入城市信息,最多可输入5个,多个城市以逗号隔开:")
  72. # 找到51job首页上关键字输入框
  73. textElement = browser.find_element_by_id('kwdselectid')
  74. # 模拟用户输入关键字
  75. textElement.send_keys(search_for_jobs)
  76. # 找到城市选择弹出框,模拟选择"北京,上海,广州,深圳,杭州"
  77. button = browser.find_element_by_xpath("//div[@class='ush top_wrap']\
  78. //div[@class='el on']/p[@class='addbut']//input[@id='jobarea']")
  79. # 打开城市对应编号文件
  80. with open("city.txt", 'r', encoding='utf8') as f:
  81. city_number = f.read()
  82. # 使用json解析文件
  83. city_number = json.loads(city_number)
  84. new_list = []
  85. # 判断是否输入多值
  86. if len(select_city.split(',')) > 1:
  87. for i in select_city.split(','):
  88. if i in city_number.keys():
  89. # 把城市替换成对应的城市编号
  90. i = city_number.get(i)
  91. new_list.append(i)
  92. # 把用户输入的城市替换成城市编号
  93. select_city = ','.join(new_list)
  94. else:
  95. for i in select_city.split(','):
  96. i = city_number.get(i)
  97. new_list.append(i)
  98. select_city = ','.join(new_list)
  99. # 执行js代码
  100. browser.execute_script("arguments[0].value = '{}';".format(select_city), button)
  101. # 模拟点击搜索
  102. browser.find_element_by_xpath("//div[@class='ush top_wrap']/button").click()
  103. self.log.info("模拟搜索输入成功,获取目标爬取title信息:{}".format(browser.title))
  104. return browser
  105. def getPageNext(self, browser):
  106. # 找到总页数
  107. str_sumPage = browser.find_element_by_xpath("//div[@class='p_in']/span[@class='td'][1]").text
  108. sumpage = ''
  109. for i in str_sumPage:
  110. if i.isdigit():
  111. sumpage += i
  112. # sumpage = 1
  113. self.log.info("获取总页数:{}".format(sumpage))
  114. s = 1
  115. while s <= int(sumpage):
  116. urls = self.getUrl(self.browser)
  117. # 获取每个岗位的详情
  118. self.items = self.spider(urls)
  119. # 数据下载
  120. self.pipelines(self.items)
  121. # 清空urls列表,获取后面的url(去重,防止数据重复爬取)
  122. urls.clear()
  123. s += 1
  124. self.log.info('开始爬取第%d页' % s)
  125. # 找到下一页的按钮点击
  126. # NextTag = browser.find_element_by_partial_link_text("下一页").click()
  127. NextTag = browser.find_element_by_class_name('next').click()
  128. # 等待加载js代码
  129. browser.implicitly_wait(20)
  130. time.sleep(3)
  131. self.log.info('获取所有岗位成功')
  132. # browser.quit()
  133. def getUrl(self, browser):
  134. # 创建一个空列表,用来存放所有岗位详情的url
  135. urls = []
  136. # 创建一个特殊招聘空列表
  137. job_urls = []
  138. # 获取所有岗位详情url
  139. Elements = browser.find_elements_by_xpath("//div[@class='j_joblist']//div[@class='e']")
  140. for element in Elements:
  141. try:
  142. url = element.find_element_by_xpath("./a").get_attribute("href")
  143. title = element.find_element_by_xpath('./a/p/span[@class="jname at"]').get_attribute('title')
  144. except Exception as e:
  145. self.log.error("获取岗位详情失败,错误代码:{}".format(e))
  146. else:
  147. # 排除特殊的url,可单独处理
  148. src_url = url.split('/')[3]
  149. if src_url == 'sc':
  150. job_urls.append(url)
  151. self.log.info("获取不符合爬取规则的详情成功:{},添加到job_urls".format(url))
  152. else:
  153. urls.append(url)
  154. self.log.info("获取详情成功:{},添加到urls".format(url))
  155. return urls
  156. def spider(self, urls):
  157. # 数据过滤,爬取需要的数据,返回items列表
  158. items = []
  159. for url in urls:
  160. htmlcontent = self.getreponsecontent(url)
  161. html_xpath = etree.HTML(htmlcontent)
  162. item = Item()
  163. # 岗位名
  164. job_name = html_xpath.xpath("normalize-space(//div[@class='cn']/h1/text())")
  165. item.job_name = job_name
  166. # 公司名
  167. company_name = html_xpath.xpath("normalize-space(//div[@class='cn']\
  168. /p[@class='cname']/a/text())")
  169. item.company_name = company_name
  170. # 工作地点
  171. work_place = html_xpath.xpath("normalize-space(//div[@class='cn']\
  172. //p[@class='msg ltype']/text())").split('|')[0].strip()
  173. item.work_place = work_place
  174. # 薪资
  175. salary = html_xpath.xpath("normalize-space(//div[@class='cn']/strong/text())")
  176. item.salary = salary
  177. # 发布时间
  178. release_time = html_xpath.xpath("normalize-space(//div[@class='cn']\
  179. //p[@class='msg ltype']/text())").split('|')[-1].strip()
  180. item.release_time = release_time
  181. # 招聘岗位详细
  182. job_recruitment_details_tmp = html_xpath.xpath("//div[@class='bmsg job_msg inbox']//text()")
  183. if not job_recruitment_details_tmp:
  184. break
  185. item.job_recruitment_details = ''
  186. ss = job_recruitment_details_tmp.index("职能类别:")
  187. ceshi = job_recruitment_details_tmp[:ss - 1]
  188. for i in ceshi:
  189. item.job_recruitment_details = item.job_recruitment_details + i.strip() + '\n'
  190. # 招聘人数详细
  191. job_number_details_tmp = html_xpath.xpath("normalize-space(//div[@class='cn']\
  192. //p[@class='msg ltype']/text())").split('|')
  193. item.job_number_details = ''
  194. for i in job_number_details_tmp:
  195. item.job_number_details = item.job_number_details + ' ' + i.strip()
  196. # 福利待遇详细
  197. company_treatment_details_tmp = html_xpath.xpath("//div[@class='t1']//text()")
  198. item.company_treatment_details = ''
  199. for i in company_treatment_details_tmp:
  200. item.company_treatment_details = item.company_treatment_details + ' ' + i.strip()
  201. # 联系方式
  202. practice_mode_tmp = html_xpath.xpath("//div[@class='bmsg inbox']/p//text()")
  203. item.practice_mode = ''
  204. for i in practice_mode_tmp:
  205. item.practice_mode = item.practice_mode + ' ' + i.strip()
  206. items.append(item)
  207. return items
  208. def getreponsecontent(self, url):
  209. # 接收url,打开目标网站,返回html
  210. fakeHeaders = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 \
  211. (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36'}
  212. try:
  213. response = requests.get(url=url,headers=fakeHeaders)
  214. # 利用apparent_encoding,自动设置编码
  215. response.encoding = response.apparent_encoding
  216. html = response.text
  217. except Exception as e:
  218. self.log.error(u'Python 返回 url:{} 数据失败\n错误代码:{}\n'.format(url, e))
  219. else:
  220. self.log.info(u'Python 返回 url:{} 数据成功\n'.format(url))
  221. time.sleep(1) # 1秒返回一个结果 手动设置延迟防止被封
  222. return html
  223. def pipelines(self, items): # 接收一个items列表
  224. # 数据下载
  225. filename = u'51job.txt'
  226. with open(filename, 'a', encoding='utf-8') as fp:
  227. for item in items:
  228. fp.write('job_name:{}\ncompany_name:{}\nwork_place:{}\nsalary:\
  229. {}\nrelease_time:{}\njob_recruitment_details:{}\njob_number_details:\
  230. {}\ncompany_treatment_details:\{}\n\
  231. practice_mode:{}\n\n\n\n' \
  232. .format(item.job_name, item.company_name, item.work_place,
  233. item.salary, item.release_time,item.job_recruitment_details,
  234. item.job_number_details, item.company_treatment_details,
  235. item.practice_mode))
  236. self.log.info(u'岗位{}保存到{}成功'.format(item.job_name, filename))
  237. if __name__ == '__main__':
  238. st = GetJobInfo()

View Code

mylog.py

  1. # !/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. import logging
  4. import getpass
  5. import sys
  6. # 定义MyLog类
  7. class MyLog(object):
  8. def __init__(self):
  9. self.user = getpass.getuser() # 获取用户
  10. self.logger = logging.getLogger(self.user)
  11. self.logger.setLevel(logging.DEBUG)
  12. # 日志文件名
  13. self.logfile = sys.argv[0][0:-3] + '.log' # 动态获取调用文件的名字
  14. self.formatter = logging.Formatter('%(asctime)-12s %(levelname)-8s %(message)-12s\r\n')
  15. # 日志显示到屏幕上并输出到日志文件内
  16. self.logHand = logging.FileHandler(self.logfile, encoding='utf-8')
  17. self.logHand.setFormatter(self.formatter)
  18. self.logHand.setLevel(logging.DEBUG)
  19. self.logHandSt = logging.StreamHandler()
  20. self.logHandSt.setFormatter(self.formatter)
  21. self.logHandSt.setLevel(logging.DEBUG)
  22. self.logger.addHandler(self.logHand)
  23. self.logger.addHandler(self.logHandSt)
  24. # 日志的5个级别对应以下的5个函数
  25. def debug(self, msg):
  26. self.logger.debug(msg)
  27. def info(self, msg):
  28. self.logger.info(msg)
  29. def warn(self, msg):
  30. self.logger.warning(msg)
  31. def error(self, msg):
  32. self.logger.error(msg)
  33. def critical(self, msg):
  34. self.logger.critical(msg)
  35. if __name__ == '__main__':
  36. mylog = MyLog()
  37. mylog.debug(u"I'm debug 中文测试")
  38. mylog.info(u"I'm info 中文测试")
  39. mylog.warn(u"I'm warn 中文测试")
  40. mylog.error(u"I'm error 中文测试")
  41. mylog.critical(u"I'm critical 中文测试")

View Code

运行程序

需要先运行getcity.py,获取城市编号,运行结果如下

{'北京': '010000', '上海': '020000', '广州': '030200', '深圳': '040000', '武汉': '180200', '西安': '200200', '杭州': '080200', '南京': '070200', '成都': '090200', '重庆': '060000', '东莞': '030800', '大连': '230300', '沈阳': '230200', '苏州': '070300', '昆明': '250200', '长沙': '190200', '合肥': '150200', '宁波': '080300', '郑州': '170200', '天津': '050000', '青岛': '120300', '济南': '120200', '哈尔滨': '220200', '长春': '240200', '福州': '110200'}

在运行主程序get51Job.py

关键字输入: python

城市选择:上海

pycharm运行截图:

生成的文件51job.txt截图

根据输入结果的不同,爬取不同的信息,利用selenium可以做到动态爬取

注意:如果遇到51job页面改版,本程序运行会报错。请根据实际情况,修改对应的爬虫规则。

本文参考链接:http://www.py3study.com/Article/details/id/344.html