拉勾的反爬机制做得特别残暴。
javascript加密和直接访问json数据会给你返回伪装的数据不说。最残暴也是最简单的,限制短时间内的多次访问。只要爬虫速度稍快点,就会要你进行验证。还有未登陆状态下,每访问10个页面,就会要求你登录。不管你是用爬虫还是正常访问。
一般是有两种爬取思路。一种是requests,一种是selenium。
requests逆向
requests需要逆向。逆推源网址的构造方法。 拉勾的逆向比较简单,从XHR文件中可以看到,在请求搜索的时候传递了三个数据:first,pn(页数),kd(搜索关键词)。所以可以在请求的时候带上这三个数据,获取json中的数据。 不过还有一个问题,获取的json数据构造有点特殊。拉勾这里又藏了一手,导致无法获取json字典中的数据,目前还没解析出来,所以这次我们用的是另外一种方法。selenium
selenium相比于要去逆向网站这种方法来的简单,主要就是模仿浏览器的行为,无需进行逆向等操作。 在用selenium进行爬取的时候会遇到以下几个问题: 1.新打开的职业搜索列表首页会有一个红包弹窗,可以用显示等待相关元素加载出来后点击退出。 2.拉勾职业列表最多显示30页(有的城市可能不足30页),每页15条数据。所以需要判断是否爬取到最后一页。 3.为了防止每打开10个页面拉勾跳出登录界面,需要将爬取到的链接分为10个一组。4.因为爬取的时候,selenium会不断的调用浏览器,所以没有添加'--headless'的话,在爬取的时候你就无法使用你的电脑,因为浏览器会不断的弹出来。
源码如下 :"""@Author: Newyee@Python: 3.6.5@selenium: 3.141.0@Chrome: 72.0.3626.81"""# 导入相关模块(未安装可执行 pip install xxx 命令安装)from selenium import webdriverfrom lxml import etreeimport randomimport timefrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.common.by import Byimport csvfrom selenium.webdriver import ChromeOptions# 创建类class LagouSpider(): def __init__(self): option = ChromeOptions() option.add_argument('--headless') # 初始化类实例时打开谷歌浏览器(可查看测试过程) self.driver = webdriver.Chrome(options=option) self.driver.set_window_size(1920, 1080) # 搜索页面的url self.url = "https://www.lagou.com/jobs/list_%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90/p-city_6?px=default#filterBox" # 存放所有职位详情页的url self.all_links = [] def run2(self, ten_links): ''' 每次对10个职位详情url请求并解析,保存职位详细信息,退出浏览器 :param ten_links: 10个职位详情页url组成的list :return: ''' # 遍历每个detail_url for link in ten_links: # 调用request_detail_page请求并解析 self.request_detail_page(link) # 随机间隔3-6s,避免反爬 time.sleep(random.randint(5, 9)) # 获取10个职位信息后退出浏览器 self.driver.quit() def run1(self): ''' 打开搜索页面,并循环翻页至最后一页,解析html获得all_detail_links :return: ''' # 在当前打开的浏览器中加载页面 self.driver.get(self.url) time.sleep(10) #点击弹出的广告按钮 button = self.driver.find_element_by_class_name('body-btn') button.click() # 用于记录当前是第几页 count_page = 1 # 循环翻页直到最后一页 while True: # 获取当前页的网页源代码 source = self.driver.page_source # 利用xpath解析source获得detail_links并保存到 self.get_all_detail_links(source) print('Fetched page %s.' % str(count_page)) # 找到【下一页】按钮所在的节点 next_btn = self.driver.find_element_by_xpath('//div[@]/span[last()]') # 判断【下一页】按钮是否可用 if "pager_next_disabled" in next_btn.get_attribute("class"): # 【下一页】按钮不可用时即达到末页,退出浏览器 self.driver.quit() # 返回所有职位详情页url列表(去重后的) return list(set(self.all_links)) else: # 【下一页】按钮可用则点击翻页 next_btn.click() count_page += 1 time.sleep(random.randint(2, 4)) time.sleep(random.randint(3, 5)) def get_all_detail_links(self, source): ''' 利用xpath解析source获得detail_links并保存到self.all_links :param source: 网页源代码html :return: ''' html = etree.HTML(source) links = html.xpath('//a[@]/@href') self.all_links += links def request_detail_page(self, url): ''' 请求职位详情页面,并调用parse_detail_page函数 :param url: 职位详情页url :return: 这部分不建议修改 ''' # 在当前窗口中同步执行javascript self.driver.execute_script("window.open('%s')" % url) # 执行后打开新页面(句柄追加一个新元素) # driver.switch_to.window:将焦点切换到指定的窗口 # driver.window_handles:返回当前会话中所有窗口的句柄 self.driver.switch_to.window(self.driver.window_handles[1]) # 切换到新打开的窗口,即第2个--index==1 source = self.driver.page_source self.parse_detail_page(source) self.driver.close() self.driver.switch_to.window(self.driver.window_handles[0]) # 切换到主窗口(否则不能再次打开新窗口) def parse_detail_page(self, source): ''' 解析详情页,用xpath提取出需要保存的职位详情信息并保存 :param source: 职位详情页的网页源代码html :return: ''' # 将source传入lxml.etree.HTML()解析得到etree.HTML文档 html = etree.HTML(source) # 对html用xpath语法找到职位名称所在节点的文本,即position_name position_name = html.xpath("//h1[@class='name']/text()")[0] # 对html用xpath语法找到职位id所在的节点,提取获得position_id #position_id = html.xpath("//link[@rel='canonical']/@href")[0].split('/')[-1].replace('.html', '') # 找到职位标签,依次获取:薪资、城市、年限、受教育程度、全职or兼职 job_request_spans = html.xpath('//dd[@]//span') salary = job_request_spans[0].xpath('.//text()')[0].strip() # 列表索引0==xpath第1个节点 city = job_request_spans[1].xpath('.//text()')[0].strip().replace("/", "").strip() work_year = job_request_spans[2].xpath('.//text()')[0].strip("/").strip() education = job_request_spans[3].xpath('.//text()')[0].strip("/").strip() work_full = job_request_spans[4].xpath('.//text()')[0] # 找到公司标签,获取company_short_name company_short_name = html.xpath('//dl[@]//em/text()')[0].replace("\n", "").strip() # 找到公司标签中的industry_field和finance_stage、scale规模 company_infos = html.xpath('//dl[@]//li') # 注意该节点下的text()索引0和2是空的 industry_field = company_infos[0].xpath('.//h4[@]/text()')[0] finance_stage = company_infos[1].xpath('.//h4[@]/text()')[0] scale = company_infos[2].xpath('.//h4[@]/text()')[0] # 找到工作地址所在的区 district = html.xpath('//div[@]/a[2]/text()')[0].strip() # 找到职位诱惑,获取position_advantage position_advantage = html.xpath('//dd[@]//p/text()')[0].strip("/").strip().replace(",", ",") # 职位描述 job_des = html.xpath('//div[@]/p/text()') # 以追加的方式写入csv文建 with open('test.csv', 'a', encoding='utf-8', newline='') as f: writer = csv.writer(f) writer.writerow([position_name, salary, city, work_year, education, work_full, company_short_name, industry_field, finance_stage, scale, district, position_advantage, job_des])if __name__ == "__main__": # 记录项目开始时间 start_time = time.time() # 实例化LagouSpider类,调用run1方法获取所有职位详情页的url needed_all_links = LagouSpider().run1() # 将所有职位详情url以10位单位拆分成嵌套列表 nested_all_links = [needed_all_links[i:i + 10] for i in range(0, len(needed_all_links), 10)] count = 10 # 连续请求10个详情页就会弹出登录页,故每请求10个重启一次浏览器 for ten_links in nested_all_links: # 每10个为一组,打开一次浏览器,调用run2方法保存职位详细信息 LagouSpider().run2(ten_links) # count计数调整间隔时间,请求过多弹出登录 time.sleep(random.randint(6, 12) * (count // 100 + 1)) count += 10 print('-------------------------') print('Have fetched %s positions.' %str(count)) # 记录项目结束时间 end_time = time.time() print('\n【项目完成】\n【总共耗时:%.2f分钟】' %((end_time - start_time) / 60))
简单一点的改善的话,可以将城市代号作为参数写入函数中,循环爬取。再进一步的话,就是做多线程,提高爬取速度。
参考:
1.拉勾网Python爬虫:Selenium+Xpath 反反爬、免登陆获取全部职位详情
2.python爬虫:爬取拉勾网职位并分析