python-scrapy爬蟲框架爬取拉勾網招聘信息


本文實例為爬取拉勾網上的python相關的職位信息, 這些信息在職位詳情頁上, 如職位名, 薪資, 公司名等等.

分析思路

分析查詢結果頁

在拉勾網搜索框中搜索'python'關鍵字, 在瀏覽器地址欄可以看到搜索結果頁的url為: 'https://www.lagou.com/jobs/list_python?labelWords=&fromSearch=true&suginput=', 嘗試將?后的參數刪除, 發現訪問結果相同.

打開Chrome網頁調試工具(F12), 分析每條搜索結果(即每個職位)在html中所處的元素定位, 發現每條結果都在<ul class="item_con_list">下的li標簽中. 

因為我們需要每個職位的具體信息, 因此需要獲取到每條搜索結果的詳情url, 即點擊搜索結果后進入的詳情頁的url.

繼續查看li標簽中的元素, 找到想要的詳情url, 找到后的url為: href="https://www.lagou.com/jobs/6945237.html?show=b6e8e778fcae4c2aa2111ba58f9ebfa0"

查看其它搜索結果的詳情url, 發現其格式都為: href="https://www.lagou.com/jobs/{某個id}.html?show={show_id}"

對於第一個ID, 每條結果的id都不一樣, 猜想其為標記每個職位的唯一id, 對於show_id, 每條結果的id都是一樣的, 嘗試刪除show參數, 發現一樣可以訪問到具體結果詳情頁

那么我們直接通過xpath提取到每個職位的第一個ID即可, 但是調試工具的elements標簽下的html是最終網頁展示的html, 並不一定就是我們訪問 https://www.lagou.com/jobs/list_python 返回的response的html, 因此點到Network標簽, 重新刷新一下頁面, 找到 https://www.lagou.com/jobs/list_python 對應的請求, 查看其對應的response, 搜索 'position_link'(即前面我們在elements中找到的每條搜索結果的詳情url), 發現確實返回了一個網址, 但是其重要的兩個ID並不是直接放回的, 而是通過js生成的, 說明我們想要的具體數據並不是這個這個請求返回的.

 那么我們就需要找到具體是那個請求會返回搜索結果的信息, 一般這種情況首先考慮是不是通過ajax獲取的數據, 篩選類型為XHR(ajax)的請求, 可以逐個點開查看response, 發現 positionAjax.json 返回的數據中就存在我們想要的每條搜索結果的信息. 說明確實是通過ajax獲取的數據, 其實點擊下一頁, 我們也可以發現地址欄url地址並沒有發生變化, 只是局部刷新了搜索結果的數據, 也說明了搜索結果是通過ajax返回的.

分析上面ajax的response, 查看其中是否有我們想要的職位ID, 在preview中搜索之前在elements中找到的某個職位的url的兩個ID, 確實兩個ID都存在response中, 分析發現第一個ID即為positionId, 第二個即為showId, 我們還可以發現response中返回了當前的頁碼數pageNo

因此我們只需要訪問上面ajax對應的url: https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false 就可以拿到我們想要的ID, 然后填入詳情url模板: https://www.lagou.com/jobs/{position_id}.html?show={show_id}中即可訪問詳情頁了.

但是當我們直接訪問 https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false 時 ,返回的結果卻是:  {"status":false,"msg":"您操作太頻繁,請稍后再訪問","clientIp":"139.226.66.44","state":2402}

 經過百度查詢后發現原來直接訪問上述地址是不行的, 這也是拉鈎的一個反爬策略, 需要我們帶上之前訪問查詢結果頁(https://www.lagou.com/jobs/list_python?)的cookie才行, 因為我們這里使用的是scrapy框架, 該框架是能夠自帶上次請求的cookie來訪問下一個請求的, 所以我們這里不需要手動去添加cookie信息, 只需要首先訪問一下查詢結果頁就可以了. 即start_url = https://www.lagou.com/jobs/list_python

此外發現這個ajax請求是通過POST方式發送的, 因此還需要分析它提交的form數據, 在第一頁中有三條數據信息, first為true, pn為1 kd為python , 第二頁中first為false, pn為2, kd同樣為python, 且多了一個sid

分析這四個參數, 第一個first為表示是否是第一頁, 第二個pn為表示當前頁碼數, 第三個kd為表示搜索的關鍵字, 第四個sid經過和上面showId對比發現其值就為showId

分析職位詳情頁

前面分析完后就可以拼接出職位詳情頁url了, 點開詳情頁, 同樣的思路分析我們想要的數據是不是就在詳情頁的url中, 這里想要職位名稱, 工資, 地點, 經驗, 關鍵字, 公司信息等

在network中查找對應的response, 發現數據確實就存在response中, 因此直接通過xpath就可以提取想要的數據了

編寫爬蟲代碼

具體代碼在github: Alex-GCX/ScrapyLagouSpider (github.com)

這里只放出關鍵代碼

創建scrapy項目

scrapy startproject LaGou

創建爬蟲

scrapy genspider lagou www.lagou.com

編寫items.py, 設置要想爬取的字段

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


class LagouItem(scrapy.Item):
    # define the fields for your item here like:
    job_url = scrapy.Field()
    job_name = scrapy.Field()
    salary = scrapy.Field()
    city = scrapy.Field()
    area = scrapy.Field()
    experience = scrapy.Field()
    education = scrapy.Field()
    labels = scrapy.Field()
    publish_date = scrapy.Field()
    company = scrapy.Field()
    company_feature = scrapy.Field()
    company_public = scrapy.Field()
    company_size= scrapy.Field()

編寫爬蟲代碼 lagou.py

# -*- coding: utf-8 -*-
import scrapy
from LaGou.items import LagouItem
import json
from pprint import pprint
import time


class LagouSpider(scrapy.Spider):
    name = 'lagou'
    allowed_domains = ['www.lagou.com']
    start_urls = ['https://www.lagou.com/jobs/list_python?']

    def __init__(self):
        # 設置頭信息, 若不設置的話, 在請求第二頁時即被拉勾網認為是爬蟲而不能爬取數據
        self.headers = {
            "Accept": "application/json, text/javascript, */*; q=0.01",
            "Connection": "keep-alive",
            "Host": "www.lagou.com",
            "Referer": 'https://www.lagou.com/jobs/list_Python?',
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
            "referer": "https://www.lagou.com/jobs/list_python?"
        }
        self.sid = ''
        self.job_url_temp = 'https://www.lagou.com/jobs/{}.html?show={}'
        # 清空文件
        with open('jobs.json', 'w') as f:
            f.truncate()

    def parse(self, response):
        """
        解析起始頁
        """
        # response為GET請求的起始頁, 自動獲取cookie
        # 提交POST帶上前面返回的cookies, 訪問數據結果第一頁
        yield scrapy.FormRequest(
            'https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false',
            callback=self.parse_list,
            formdata={"first": "false",
                      "pn": "1",
                      "kd": "python",
                      },
            headers=self.headers
        )
    def parse_list(self, response):
        """
        解析結果列表頁的json數據
        """
        # 獲取返回的json,轉為字典
        res_dict = json.loads(response.text)
        # 判斷返回是否成功
        if not res_dict.get('success'):
            print(res_dict.get('msg', '返回異常'))
        else:
            # 獲取當前頁數
            page_num = res_dict['content']['pageNo']
            print('正在爬取第{}頁'.format(page_num))
            # 獲取sid
            if not self.sid:
                self.sid = res_dict['content']['showId']
            # 獲取響應中的職位url字典
            part_url_dict = res_dict['content']['hrInfoMap']
            # 遍歷職位字典
            for key in part_url_dict:
                # 初始化保存職位的item
                item = LagouItem()
                # 拼接完整職位url
                item['job_url'] = self.job_url_temp.format(key, self.sid)
                # 請求職位詳情頁
                yield scrapy.Request(
                    item['job_url'],
                    callback=self.parse_detail,
                    headers=self.headers,
                    meta={'item': item}
                )

            # 獲取下一頁
            if page_num < 30:
                # time.sleep(2)
                yield scrapy.FormRequest(
                    'https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false',
                    callback=self.parse_list,
                    formdata={"first": "false",
                              "pn": str(page_num+1),
                              "kd": "python",
                              "sid": self.sid
                             },
                    headers=self.headers
                )

    def parse_detail(self, response):
        """
        解析職位詳情頁
        """
        # 接收item
        item = response.meta['item']
        # 解析數據
        # 獲取職位頭div
        job_div = response.xpath('//div[@class="position-content-l"]')
        if job_div:
            item['job_name'] = job_div.xpath('./div/h1/text()').extract_first()
            item['salary'] = job_div.xpath('./dd/h3/span[1]/text()').extract_first().strip()
            item['city'] = job_div.xpath('./dd/h3/span[2]/text()').extract_first().strip('/').strip()
            item['area'] = response.xpath('//div[@class="work_addr"]/a[2]/text()').extract_first()
            item['experience'] = job_div.xpath('./dd/h3/span[3]/text()').extract_first().strip('/').strip()
            item['education'] = job_div.xpath('./dd/h3/span[4]/text()').extract_first().strip('/').strip()
            item['labels'] = response.xpath('//ul[@class="position-label clearfix"]/li/text()').extract()
            item['publish_date'] = response.xpath('//p[@class="publish_time"]/text()').extract_first()
            item['publish_date'] = item['publish_date'].split('&')[0]
            # 獲取公司dl
            company_div = response.xpath('//dl[@class="job_company"]')
            item['company'] = company_div.xpath('./dt/a/img/@alt').extract_first()
            item['company_feature'] = company_div.xpath('./dd//li[1]/h4[@class="c_feature_name"]/text()').extract_first()
            item['company_feature'] = item['company_feature'].split(',')
            item['company_public'] = company_div.xpath('./dd//li[2]/h4[@class="c_feature_name"]/text()').extract_first()
            item['company_size'] = company_div.xpath('./dd//li[4]/h4[@class="c_feature_name"]/text()').extract_first()
            yield item

編寫middlewares.py, 自定義downloadermiddleware, 用來每次發送請求前, 隨機設置user-agent, 這里使用了第三方庫 fake_useragent, 能夠隨機提供user-agent, 使用前先安裝: pip install fake_useragent

from fake_useragent import UserAgent
import random

class RandomUserAgentDM:
    """
    隨機獲取userAgent
    """
    def __init__(self):
        self.user_agent = UserAgent()

    def process_request(self, request, spider):
        request.headers['User-Agent'] = self.user_agent.random

編寫pipelines.py, 將數據存為json文件

import json

class LagouPipeline:
    def process_item(self, item, spider):
        with open('jobs.json', 'a', encoding='utf-8') as f:
            item_json = json.dumps(dict(item), ensure_ascii=False, indent=2)
            f.write(item_json)
            f.write('\n')

編寫settings.py

# 設置日志顯示
LOG_LEVEL = 'WARNING'

# 設置ROBOTSTXT協議, 若為true則不能爬取數據
ROBOTSTXT_OBEY = False

# 設置下載器延遲, 反爬蟲的一種策略
DOWNLOAD_DELAY = 0.25

# 開啟DOWNLOADER_MIDDLEWARES
DOWNLOADER_MIDDLEWARES = {
  # 'LaGou.middlewares.LagouDownloaderMiddleware': 543,
    'LaGou.middlewares.RandomUserAgentDM' :100,
}

# 開啟ITEM_PIPELINES
ITEM_PIPELINES = {
  'LaGou.pipelines.LagouPipeline': 300,
}

啟動爬蟲

scrapy crawl lagou

發現依然只能5 6頁, 說明拉勾網的反爬確實做得比較好, 還可以繼續通過使用代理來進行反反爬, 這里就不再演示了, 

 查看爬取結果

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM