scrapy由浅入深(三) selenium模拟爬取ajax动态页面(智联招聘)

爬取智联招聘的网址:https://sou.zhaopin.com/?pageSize=60&jl=489&kw=python&kt=3

        上一篇博客爬取了前程无忧的职位招聘信息,总体来说前程无忧的网站信息并不难爬取,前程无忧的网站并没有ajax,直接请求网站就能获得职位信息,但是智联招聘的页面涉及到ajax,直接get网站的url获取不到任何有用的信息,这也是反爬虫的一种手段,因此我们需要借助selenium模拟浏览器访问智联招聘网站。在爬取的过程中有一些非常有意思的问题,下面我会把这些问题以及解决的办法一一列举出来。

        1.首先我在分析职位详情(注意不是职位列表页面)网页的结构的时候遇到的一个问题,在分析网页的源码构造xpath的时候,发现无论怎么修改xpath及css选择器,获得的数据都是空([ ])。原来使用爬虫获取到的网页源码与我们在网页上看到的源码不一样,使用scrapy请求网站的时候,网页会将class属性替换掉,所以直接通过网页上的源码来构造xpath和css选择器是不可行的。正确的做法是通过scrapy shell +""(请求的网址),打开浏览器查看正确的class属性,然后再构造xpath及css选择器。2.然后就是涉及到ajax的职位列表页面,细心一点的同学会发现当输入网址之后,下方的职位列表会加载一段时间才会展示出来,如果我们直接get网页的源码,不会得到任何有用的信息,使用scrapy shell + ""(职位列表页面) 可以看到在浏览器中不会显示查询之后的结果,因此我们需要使用selenium模拟获取职位列表页面的所有信息。3.编写使用selenium模拟点击下一页的中间件,职位的详细信息通过scrapy系统的中间件下载,这就会产生数据丢失的问题,因为点击下一页这个动作运行的非常快,那么在点击下一页之后,scrapy会接受该页面的所有职位链接,一个页面有60个职位链接,我试验的时候基本上当selenium中间件点击到将近30页的时候,第一页的所有职位链接才会爬取完,那么就有一个问题,现在scrapy已经接受了几百个职位的url,在请求这些url的时候很有可能会丢掉大部分的数据,造成很多页面没有爬取的漏洞,解决的办法也很简单,设置网页跳转的限制,当一个网页的数据爬取的差不多的时候,比如爬取了50多条数据的时候就能跳转到下一页。

        代码思路:1.定义一个中间件处理两种不同的请求,点击下一页或者下载详情页。2.抽取职位列表的所有url,通过scrapy系统的中间件请求职位的详细信息页,防止覆盖掉senium的职位列表的url。3.判断该职位列表页的数据爬取了多少条,如果超过50页,那么点击到下一页。4.将数据保存到数据库

一.创建项目

(1)

scrapy startproject zhipinSpider

创建一个名称为zhipinSpider的项目

(2)手动创建job_detail.py文件,实现爬虫的主要逻辑

二.项目配置

(1)编写items文件

import scrapy


class ZhipinspiderItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    salary = scrapy.Field()
    detail = scrapy.Field()

定义三个字段,分别用来保存职位的标题,薪资,职位的要求

(2)编写pipelines文件

import sqlite3
db = sqlite3.connect("./../../zhi.db")
cursor = db.cursor()

class ZhipinspiderPipeline(object):
    def process_item(self, item, spider):
        cursor.execute("insert into jobs(title, salary, detail) values (?,?,?)",[item["title"],item["salary"],item["detail"]])
        db.commit()

将数据保存到sqlite数据库中,也可以保存到MySQL数据库中,写法类似

(3)编写selenium模拟中间件

class SeleniumMiddleware(object):
    def __init__(self):
        self.options = Options()
        # self.options.add_argument('-headless')
        self.browser = webdriver.Firefox(executable_path="D:\geckodriver\geckodriver.exe",firefox_options=self.options)

        # self.option = Options()
        # self.option.add_argument('-headless')
        # self.browser1 = webdriver.Firefox(executable_path="D:\geckodriver\geckodriver.exe",firefox_options=self.options)

    def process_request(self, request, spider):
        """
        通过meta携带的数据判断应该跳转到下一页还是下载详情页
        这里没有处理详情页的函数,所以当下载详情页的时候会调用scrapy系统的中间件,也就不会覆盖跳                
        转页面的url
        :param request: 
        :param spider: 
        :return: 
        """
        if int(request.meta["page"]) == 0:
            self.browser.get(request.url)
            self.browser.execute_script("window.scrollTo(0,document.body.scrollHeight)")
            time.sleep(1)
            pages = self.browser.find_element_by_css_selector('button.btn:nth-child(8)')
            pages.click()
            time.sleep(5)
            return HtmlResponse(url=self.browser.current_url,body=self.browser.page_source,encoding="utf-8",request=request)

        if int(request.meta["page"]) == 2:
            # 不需要get网页的url否则浏览器会在第一第二页之间来回跳转
            # self.browser.get(self.browser.current_url)
            self.browser.execute_script("window.scrollTo(0,document.body.scrollHeight)")
            time.sleep(1)
            pages = self.browser.find_element_by_css_selector('button.btn:nth-child(8)')
            pages.click()
            time.sleep(5)
            return HtmlResponse(url=self.browser.current_url,body=self.browser.page_source,encoding="utf-8",request=request)

(4)配置setting文件

启用selenium中间件

DOWNLOADER_MIDDLEWARES = {
   'zhipinSpider.middlewares.SeleniumMiddleware': 543,
}

启用管道文件

ITEM_PIPELINES = {
   'zhipinSpider.pipelines.ZhipinspiderPipeline': 300,
}

下载延迟

DOWNLOAD_DELAY = 3
RANDOMIZE_DOWNLOAD_DELAY = True

(5)编写spider文件

爬虫的主要逻辑

import scrapy
from scrapy import Request
import lxml.html
from zhipinSpider.items import ZhipinspiderItem
import sqlite3

db = sqlite3.connect("./../../zhi.db")
cursor = db.cursor()

i = 0

def select_from_sql():
    """
    :return: 当前数据库中数据的总数
    """
    count = cursor.execute("select * from jobs")
    return len(count.fetchall())

class JobDetailSpider(scrapy.Spider):
    name = "jobSpider"

    def start_requests(self):
        url_str = "https://sou.zhaopin.com/?pageSize=60&jl=489&kw=python&kt=3"
        yield Request(url=url_str,callback=self.parse,meta={"page":"0"})

    def parse(self, response):
        """
        抽取出包含职位url的html,并通过函数分离url
        :param response: 
        :return: 
        """
        html_str = response.xpath('//div[@class="listItemBox clearfix"]').extract()
        for html in html_str:
            job_url = self.parse_one_job(html)
            yield Request(url=job_url,callback=self.parse_job_text,meta={"page":"1"})

    def parse_one_job(self,html):
        """
        分理处html中的职位url
        :param html: 
        :return: 
        """
        xtree = lxml.html.fromstring(html)
        job_url = xtree.xpath('//div[@class="jobName"]/a/@href')[0]
        return job_url

    def parse_job_text(self,response):
        global i
        count = select_from_sql()

        # 使用selenium模拟的结果
        # title = response.xpath('//ul/li/h1/text()').extract()
        # detail = response.xpath('//div[@class="pos-ul"]/p/text()').extract()
        # salary = response.xpath('//div[@class="l info-money"]/strong/text()').extract()

        # 使用view(response)获得的结果
        title = response.xpath('//div[@class="inner-left fl"]/h1/text()').extract_first()
        salary = response.xpath('//ul[@class="terminal-ul clearfix"]/li/strong/text()').extract_first()
        detail = response.xpath('//div[@class="tab-inner-cont"]/*/text()').extract()
        detail_span = response.xpath('//div[@class="tab-inner-cont"]/p/span/text()').extract()
        if detail_span is not None:
            detail = detail_span + detail
        contents = ""
        for content in detail:
            contents += content
        contents = ' '.join(contents.split())
        item = ZhipinspiderItem()
        item["title"] = title
        item["salary"] = salary
        item["detail"] = contents
        # 判断有没有爬取完当前页面的职位信息(是否达到分页的条件)
        if count - i > 58:
            i = count
            yield Request(url="http://www.baidu.com", callback=self.parse, meta={"page": "2"}, dont_filter=True)
        yield item

这里分析职位要求的xpath之所以有两个是因为智联招聘所有的职位要求信息,它的html标签会有个别不一样的情况,这里我直接定义了两个xpath处理两种情况,将两种情况得到的信息合并,这样就会减少爬取不到职位要求信息的情况。另外最后这个请求url之所以使用www.baidu.com是因为这里我们不需要使用其他的url,只需要点击下一页就行了,这里的url只作为占位使用。

总结:爬取智联招聘难点在于如何爬取ajax数据,如何使用selenium模拟区分职位列表页跟职位详情页,以及为了防止数据丢失而查询数据库数据的条数,判断是否达到了分页的条件。

猜你喜欢

转载自blog.csdn.net/WanYu_Lss/article/details/82807099