Flask框架+Scrapy框架合并项目

953 阅读6分钟

第一步:

  找到可以爬取的三个招聘网站(估计只能爬取一个网站了,因为才一个小网站,就耗费了大量时间):

   1   job.cn      

  a.广东广州:www.job.cn/job/list/0-…

  b.上海:www.job.cn/job/list/0-…

 c. 北京:www.job.cn/job/list/0-…

第二步:

     1 settings设置日志级别,和日志存储文件等。

# 设置日志
# 以日期来命名日志名称,这样达到每天建立一个日志文件的要求。
today = datetime.datetime.today().now()
log_file_path = "./log_{}_{}_{}_{}_{}.log".format
(today.year,today.month,today.day,today.hour,today.minute)

# 1.设置日志等级  (这样在控制台输出的信息中就只会有爬取的数据,除非出现warning以上的日志信息。)
LOG_LEVEL = "WARNING"
# 2.保存日志文件到本地
LOG_FILE = log_file_path


# 这里一定要注释掉或者改为False# scrapy的ROBOTSTXT_OBEY 字面意思是:遵循Robots协议
#(也称为爬虫协议、机器人协议等,全称是“网络爬虫排除标准”(Robots Exclusion Protocol))
# 如果爬取目标目录在目标网站的robots.txt文本中列为disallow,则scrapy会放弃爬取动作。
# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# 开启管道:
ITEM_PIPELINES = {   
        'lyfScrapy.pipelines.LyfscrapyPipeline': 300,
}


    2 创建scrapy爬虫项目,创建爬虫文件。测试网站是否可以爬取。

      发现问题1:

# 在爬取时出现403错误  
# Crawled (403) 和  HTTP status code is not handled or not allowed
# 则在setting下添加如下代理
USER_AGENT = 'Mozilla/5.0(Windows NT 10.0; Win64; x64)
 AppleWebKit/537.36 (KHTML, like Gecko)Chrome/70.0.3538.110 Safari/537.36'

spider代码:

import scrapy

# 配置日志,通过日志来监控爬虫程序
import logging
logger = logging.getLogger(__name__)

#  设置时间
import datetime

# 导入items,items是保存数据的容器,它使用的方法和字典很相似,但是相比字典item多了额外的保护机制,可以避免拼写错误
from  lyfScrapy.items import  LyfscrapyItem,LyfscrapyItem_jd

class JobSpider(scrapy.Spider):
    # 爬虫的名字:必须
    name = 'job'
    # 爬取的域:可选
    allowed_domains = ['job.cn']

    # 设定需要爬取的岗位页数
    count = 1
    jd_count = 0

    # 记录爬取的页码
    i = 0
    # 爬取的网页起始页码
    offset = 1
    baseURL = 'http://www.job.cn/job/list/'   # 这是网页的头部

    # 爬取的网站列表,爬虫会从这里加载爬取的网页。
    #  3409:北京 3411:上海   6_76:广东广州
    start_urls = [baseURL + '0-3409-0-0_0_0_0_0_0_0_0-0-0-0-' + str(offset) + '.html']

    def parse(self, response):

        #  *********************** 测试************************************

        #  检测是否可以爬取这个网站,不可以则在settings中添加代理。
        # print(response.body)
        # 数据长度:为了判断是否爬取到了网页
        lenth = len(response.body)
        # print(lenth)
        if lenth != 0:
            logger.warning("这里是parse,访问状态码:200,该网站数据已经爬取到,可以进行解析")
        else:
            logger.error("网站爬取的数据为空,不能进行解析!!!")

        #  ************************* 测试end****************************************

        self.i += 1
        logger.warning("正在爬取第%d页的数据" % self.i)
        #  ********************* 解析数据**********************

        # 在谷歌浏览器使用xpath helper 插件 可以快速定位数据
        # 注意:这个class的值后面有个空格
        node_list = response.xpath("//div[@class='search_job_list ']")

        if len(node_list) != 0:
            logger.warning("爬取成功,爬取的数据数量为:"+str(len(node_list)))
        else:
            logger.warning("爬取失败!!!请检查xpath!!")

        logger.warning("正在解析数据第%d页的数据...."%self.i)
        for node in node_list:
            item = LyfscrapyItem()
            # 岗位名称
            item['positionName'] = node.xpath(".//a[@class='yunjoblist_newname_a']/text()").extract_first()

            # 岗位链接
            item['positionLink'] = node.xpath(".//a[@class='yunjoblist_newname_a']/@href").extract_first()

            # 将岗位描述网址,提取出来,发送给调度器,交给下载器去下载,然后数据再返回给pare1中处理

            yield scrapy.Request(item['positionLink'], callback=self.parse1)

            # 岗位类别
            item['positionType'] = node.xpath(".//div[@class='company_det_hy']/div/text()[1]").extract_first()

            # 工作地点   node.xpath("//span[1]/em[@class='com_search_job_em']/text()").extract_first()
            item['workPlace'] = '北京'

            # 公司名称
            item['companyName'] = node.xpath(".//a[@class='search_job_com_name']/text()").extract_first()
            # 公司主页
            item['companyLink'] = node.xpath(".//a[@class='search_job_com_name']/@href").extract_first()
            # 薪资
            item['monthlyPay'] = node.xpath(".//div[@class='yunjoblist_newxz']/text()").extract_first()
            # 发布时间
            pubTime = node.xpath(".//span[@class='yunjoblist_new_time']/text() | .//span[@class='yunjoblist_new_time']/span/text()").extract_first()

            # 反爬机制的作用,不得不使用这样的方法.因为爬取到的时间有可能是‘14:00’ 或者 ”10:00“等,所以直接赋值当前的日期
            if  pubTime== "  " or pubTime =='   ' or pubTime ==' ':
                item['publishTime'] = datetime.datetime.today().date()
            else:
                item['publishTime'] = pubTime

            # 拿到数据就发送给管道,交给管道处理,拿一些就交给管道处理一些,这样就不用一直存在内存里面,占系统内存,特别是大量数据的情况下,不会导致内存瞬间增大。
            logger.warning("正在将parse的数据传送给管道>>>>>>>>")
            yield item

            #  方法一:爬取多页,爬取指定的页
            if self.offset < self.count:
                self.offset += 1
                # print("当前爬取的网页为 :" + self.url)
                self.url = self.baseURL+'0-3409-0-0_0_0_0_0_0_0_0-0-0-0-'+str(self.offset)+'.html'
                # 这个函数可以不断将提取的url提交给调度器,发给当前函数进行处理
                yield scrapy.Request(self.url, callback=self.parse)

        if len(item) != 0:
            logger.warning("解析数据成功!")
        else:
            logger.warning("解析数据失败")

        #  ********************* 解析数据end**********************


#   岗位名称 //div[@class='search_job_list']//a[@class='yunjoblist_newname_a']/text()

#   公司名称  //div[@class='search_job_list']//a[@class='search_job_com_name']/text()

#   工资://div[@class='search_job_list']//div[@class='yunjoblist_newxz']/text()

#   工作地点: 北京

#   岗位类别: //div[@class='search_job_list']//div[@class='company_det_hy']/div/text()

#   发布时间: //div[@class='search_job_list']//span[@class='yunjoblist_new_time']/text()

#   工作描述链接://div[@class='search_job_list']//a[@class='yunjoblist_newname_a']/@href

#   职位描述  jobDescription

    def parse1(self, response):
        # print("我是parse1")
        # print(response.body)
        # 数据长度
        lenth = len(response.body)
        if lenth != 0:
            self.jd_count += 1
            logger.warning("这里是parse1,GET:200,可以解析,正在解析第%d个子网页数据...!"%self.jd_count)
        else:
            logger.error("子网页爬取数据为空!!!")



        # 通过items存值
        item_jd = LyfscrapyItem_jd()
        #   normalize-space(): 去掉空格和换行符,但是不能和text()一起使用。
        item_jd['positionName_1'] = response.xpath("normalize-space(//div[@class='job_details_topleft']/h1)").extract()
        item_jd['jobDescription'] = response.xpath("normalize-space(//div[@class='job_details_describe'])").extract()

        str1 = ''
        for item in item_jd['positionName_1']:
            str1 = str1 + item + ''
            if len(str1) > 0:
                item_jd['positionName_1'] = str1
            else:
                item_jd['positionName_1'] = 'NopositionName'

        str = ''
        for item in item_jd['jobDescription']:
            str = str+item + ''
            # self.jd_count = self.jd_count+1
            if len(str) > 0:
                item_jd['jobDescription'] = str
            else:
                item_jd['jobDescription'] = '暂无公司描述'
        # print(self.jd_count)
        # print(item_jd)
        logger.warning("第%d个子网页解析成功"%self.jd_count)
        logger.warning("正在将parse1的数据传送给管道>>>>>>>>")
        yield item_jd

    #  岗位描述: //div[@class='job_details_describe']/text()
    #  限制://div[@class='job_details_describe']/span[1]/text()

    pass

   3 同一个爬虫爬取主页和不同的子网页

在爬取子网后,需要在items中创建两个类,这样才能在同一个管道中,根据类型的不同,取出不同的数据。

items代码:

import scrapy
# items是保存数据的容器,它使用的方法和字典很相似,但是相比字典item多了额外的保护机制,
#可以避免拼写错误# 为了保证能从同一个管道中取出不同线程拿到的数据,需要在items中创建两个类,在管道中,通过判断类的不同,来存储数据。
class LyfscrapyItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    # 岗位名称
    positionName = scrapy.Field()
    # 岗位链接
    positionLink = scrapy.Field()
    # 岗位类型
    positionType = scrapy.Field()
    # 工作地点
    workPlace = scrapy.Field()
    # 公司名称
    companyName = scrapy.Field()
    # 公司链接
    companyLink = scrapy.Field()
    # 月薪
    monthlyPay = scrapy.Field()
    # 发布时间
    publishTime = scrapy.Field()
    pass
class LyfscrapyItem_jd(scrapy.Item):
    # 子网页的岗位名称
    positionName_1 = scrapy.Field()
    # 子网页中的岗位描述
    jobDescription = scrapy.Field()
    pass

4  所有的数据通过yield交给管道后,在管道文件pipelines.py里面处理

 pipelines.py代码

from itemadapter import ItemAdapter

# 配置日志,通过日志来监控管道文件
import logging
logger = logging.getLogger(__name__)

# # 新建excel时导入这个包
from openpyxl import Workbook

# 导入items,items是保存数据的容器,它使用的方法和字典很相似,但是相比字典item多了额外的保护机制,可以避免拼写错误
# 这里用来判断和提取数据
from  lyfScrapy.items import  LyfscrapyItem,LyfscrapyItem_jd

class LyfscrapyPipeline:
    # *************************存入excel************************

    # 此函数只执行一次
    def __init__(self):
        # 定义初始的行号
        self.row_count = 0
        # 打开新的Excel表
        self.wb = Workbook()
        logger.warning("表格创建成功,表格打开成功,准备写入数据>>>>>>")
        # 打开默认的sheet工作表
        self.ws = self.wb.active
        # 添加一行,作为列标题
        self.ws.append(['positionName', 'positionLink', 'positionType', 'workPlace', 'companyName', 'companyLink', 'monthlyPay', 'publishTime','positionName_1','jobDescription'])

    # 此函数会被频繁调用,执行多次
    def process_item(self, item, spider):
        if len(item) != 0:
            logger.warning("管道已经拿到数据,正在处理>>>>>>>>")
        # print(item)
        # 为了保证能从同一个管道中取出不同线程拿到的数据,需要在items中创建两个类,在管道中,通过判断类的不同,来存储数据。

        # 提取第一个类中的数据
        if type(item) == LyfscrapyItem:
            # print(item)
            # 把数据每一行整理出来
            line = [item['positionName'], item['positionLink'], item['positionType'], item['workPlace'],
                    item['companyName'], item['companyLink'], item['monthlyPay'], item['publishTime']]
            # 将数据以行的形式添加到xlsx中
            self.ws.append(line)

        # 提取第二个类中的数据
        if type(item) == LyfscrapyItem_jd:
            self.row_count += 1
            # print(item)
            # print("最大列数:",self.ws.max_column)

            # 将数据以列的形式,加入到表中。
            self.ws.cell(row=self.row_count+1,column=self.ws.max_column-1).value = item['positionName_1']
            self.ws.cell(row=self.row_count+1,column=self.ws.max_column).value = item['jobDescription']

        # return:将item再返回给其他管道处理。
        logger.warning("处理成功!!!")
        return item

    # 此函数只执行一次
    def close_spider(self, spider):
        # 保存xlsx文件
        self.wb.save('job.xlsx')
        logger.warning("表格关闭成功,数据存入表格成功!")

    # *************************存入excel**end************************