【Java爬蟲】爬取“軟件工程師”招聘信息

分析

要爬的網站是 前程無憂 https://jobs.51job.com/shanghai-hpq/42713907.html?s=01&t=1

在這裏插入圖片描述

流程分析

我們需要解析職位列表頁,獲取職位的詳情頁,再解析頁面獲取數據。
獲取url地址的流程如下
在這裏插入圖片描述
但是在這裏有個問題:在解析頁面的時候,很可能會解析出相同的url地址(例如商品標題和商品圖片超鏈接,而且url一樣),如果不進行處理,同樣的url會解析處理多次,浪費資源。所以我們需要有一個url去重的功能。

Scheduler組件。

WebMagic提供了Scheduler可以幫助我們解決以上問題。

Scheduler 是WebMagic 中進行URL管理的組件。一般來說,Scheduler包括

  • 對待抓取的URL隊列進行管理。
  • 對已抓取的URL進行去重。

WebMagic內置了幾個常用的scheduler。如果你只是在本地執行規模比較小的爬蟲,那麼基本無需定製Scheduler,但是瞭解一下已經提供的幾個Scheduler還是有意義的。
在這裏插入圖片描述
在這裏插入圖片描述
去重部分被單獨抽象成了一個接口:DuplicateRemover,從而可以爲同一個Scheduler選擇不同的去重方式,以適應不同的需要,目前提供了兩種去重方式。
在這裏插入圖片描述
RedisScheduler 是使用Redis的set 進行去重,其他的Scheduler默認都使用HashSetDuplicateRemover來進行去重。

三種去重方式

  • HashSet使用java中的Hashset不能重複的特點去重。優點是容易理解。使用方便。
    缺點:佔用內存大,性能較低。

  • Redis去重
    A使用Redis的set進行去重。優點是速度快(Redis本身速度就很快),而且去重不會佔用爬蟲服務器的資源,可以處理更大數據量的數據爬去。
    缺點:需要準備Redis服務器,增加開發和使用成本。

  • 布隆過濾器(BloomFilter)
    使用布隆過濾器也可以實現去重。優點是佔用的內存要比使用HashSet要小的多,也適合大量數據的去重操作。
    缺點:有誤判的可能。沒有重複可能會判定重複,但是重複數據一定會判定重複。

布隆過濾器(Bloom Filter)是由Burton Howard Bloom於1970年提出,它是一種space efficient的概率型數據結構,用於判斷一個元素是否在集合中。在垃圾郵件過濾的黑白名單方法、爬蟲(Crawler)的網址判重模塊中等等經常被用到。

哈希表也能用於判斷元素是否在集合中,但是布隆過濾器只需要哈希表的1/8或1/4的空間複雜度就能完成同樣的問題。布隆過濾器可以插入元素,但不可以刪除已有元素。其中的元素越多,誤報率越大,但是漏報是不可能的。。

原理:
布隆過濾器需要的是一個位數組(和位圖類似)和K個映射函數(和Hash表類似),在初始狀態時,對於長度爲m的位數組array,它的所有位被置0。

在這裏插入圖片描述

對於有n個元素的集合S={S1,52…Sn},通過k個映射函數{f1,f2…….fk},將集合S中的每個元素Sj(1<=j<=n)映射爲K個值(g1,g2…gk},然後再將位數組array中相對應的array[g1],aray[g2]……arraylgk]置爲1:

在這裏插入圖片描述
如果要查找某個元素item是否在S中,則通過映射函數{1,f2.…fk}得到k個值{81,g2…gk},然後再判斷 arraylg1],array[g2]…array[gk]是否都爲1,若全爲1,則item在s中,否則item不在s中。

布隆過濾器會造成一定的誤判,因爲集合中的若干個元素通過映射之後得到的數值恰巧包括g1,g2.…gk,在這種情況下可能會造成誤判,但是概率很小。

環境準備

建立數據庫

CREATE TABLE job_info(
	id BIGINT(20) NOT NULL AUTO_INCREMENT,
	company_name VARCHAR(100) DEFAULT NULL,
	company_addr VARCHAR(200) DEFAULT NULL,
	company_info VARCHAR( 5000) DEFAULT NULL,
	job_name VARCHAR(100) DEFAULT NULL,
	job_info VARCHAR( 5000) DEFAULT NULL,
	salary_min int(10) DEFAULT NULL,
	salary_max int(10) DEFAULT NULL,
	url VARCHAR(150) DEFAULT NULL,
	time VARCHAR(10) DEFAULT NULL,PRIMARY KEY(id)
)DEFAULT CHARSET = utf8

建表如下:
在這裏插入圖片描述
Maven配置

   <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-core -->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-core</artifactId>
            <version>0.7.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-extension -->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-extension</artifactId>
            <version>0.7.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.github.alexandrnikitin/bloom-filter -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0</version>
        </dependency>

結構:
在這裏插入圖片描述
創建JobInfo對象
與數據庫對應

package spider.info;

public class JobInfo {
    private Long id;
    private String companyName;
    private String companyAddr;
    private String companyInfo;
    private String jobName;
    private String jobInfo;
    private Integer salaryMin;
    private Integer salaryMax;
    private String url;
    private String time;

    public Long getId() {
        return id;
    }

    public String getCompanyName() {
        return companyName;
    }

    public String getCompanyAddr() {
        return companyAddr;
    }

    public String getCompanyInfo() {
        return companyInfo;
    }

    public String getJobName() {
        return jobName;
    }



    public String getJobInfo() {
        return jobInfo;
    }

    public Integer getSalaryMin() {
        return salaryMin;
    }

    public Integer getSalaryMax() {
        return salaryMax;
    }

    public String getUrl() {
        return url;
    }

    public String getTime() {
        return time;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setCompanyName(String companyName) {
        this.companyName = companyName;
    }

    public void setCompanyAddr(String companyAddr) {
        this.companyAddr = companyAddr;
    }

    public void setCompanyInfo(String companyInfo) {
        this.companyInfo = companyInfo;
    }

    public void setJobName(String jobName) {
        this.jobName = jobName;
    }

    public void setJobInfo(String jobInfo) {
        this.jobInfo = jobInfo;
    }

    public void setSalaryMin(Integer salaryMin) {
        this.salaryMin = salaryMin;
    }

    public void setSalaryMax(Integer salaryMax) {
        this.salaryMax = salaryMax;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void setTime(String time) {
        this.time = time;
    }
}

SpiderGoGO.java
實現爬蟲的核心功能

package spider.todo;

import org.jsoup.Jsoup;
import spider.info.JobInfo;
import spider.utils.SalaryMath;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Html;
import us.codecraft.webmagic.selector.Selectable;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class SpiderGoGO implements PageProcessor {
    private Site site = Site.me()
            .setCharset("gbk")  //設置編碼
            .setTimeOut(10*1000)   //設置超時時間
            .setSleepTime(3000)  //設置重試間隔時間
            .setRetryTimes(3);  //設置重試次數

    public void process(Page page) {
        //解析頁面 獲取信息詳情的url
        List<Selectable> list =  page.getHtml().css("div.el p.t1 span a").nodes();
        //判斷獲取到的集合是否爲空
        if(list.size() == 0){
            //如果爲空 表示這是招聘詳情頁 解析頁面 獲取招聘詳情 保存數據
            saveJobInfo(page);  //調用方法保存數據  這個方法寫下面啦
        }else {
            //如果不爲空 表示這是列表頁 解析出詳情頁的url 放到任務隊列中
            for (Selectable selectable:list){
                //獲取url地址
                String jobInfo = selectable.links().toString();
                //把獲取的url地址放到任務隊列中
                page.addTargetRequest(jobInfo);
            }
            //獲取下一頁的的url
           String s =  page.getHtml().css("div.p_in li.bk").regex("<a[\\s\\S]*?下一頁</a>").toString();
            // 按指定模式在字符串查找
            String pattern = "<a href=\"(.*)\"";

            // 創建 Pattern 對象
            Pattern r = Pattern.compile(pattern);

            // 現在創建 matcher 對象
            Matcher m = r.matcher(s);
            if (m.find( )) {
                String url = m.group(1);
                //把下一頁的url加入任務隊列中
                //***************************************************************************
              page.addTargetRequest(url);
                //***************************************************************************
            }

        }
    }

    //解析頁面,獲取招聘詳情信息.保存數據
    private void saveJobInfo(Page page){
        //創建招聘詳情對象
        JobInfo jobInfo = new JobInfo();
        //解析解析頁面
        Html html = page.getHtml();
        //保存數據,封裝到對象中
        jobInfo.setCompanyAddr(Jsoup.parse(html.css("div.bmsg").nodes().get(1).toString()).text());
        jobInfo.setCompanyInfo(Jsoup.parse(html.css("div.tmsg").toString()).text());
        jobInfo.setCompanyName(html.css("div.cn p.cname a","text").toString());
       // jobInfo.setId();  ID不寫 是自增的
        jobInfo.setJobInfo(Jsoup.parse(html.css("div.job_msg").toString()).text());
        jobInfo.setJobName(html.css("div.cn h1","text").toString());
        //獲取薪資
        Integer[] salary = SalaryMath.getSalary(html.css("div.cn strong", "text").toString());
        jobInfo.setSalaryMax(salary[1]);
        jobInfo.setSalaryMin(salary[0]);
        //獲取發佈時間
        String content = Jsoup.parse(html.css("div.cn p").regex(".*發佈").toString()).text();
        jobInfo.setTime(content.substring(content.length()-7,content.length()-2));
        jobInfo.setUrl(page.getUrl().toString());


       // 上面的內容設置完成後將結果保存在內存中
        page.putField("JobInfo",jobInfo);
        //實際上數據存放在resultItems中

    }

    public Site getSite() {
        return site;
    }
}

DataPipline.java
實現數據庫存儲的功能

package spider.todo;

import spider.info.JobInfo;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

import java.sql.*;

public class DataPipline implements Pipeline {
    public void process(ResultItems resultItems, Task task) {
        //獲取封裝號的招聘詳情對象
        JobInfo jobInfo = resultItems.get("JobInfo");
        //判斷數據是否不爲空
        if(jobInfo != null){
            //保存自數據庫中
            Connection conn;
            //註冊驅動
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                //獲取數據庫連接對象 Connection
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/spider?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC","root","root");
                //定義sql
                String sql="insert into job_info (company_name,company_addr,company_info,job_name,job_info,salary_min,salary_max,url,time) " +
                        "values(?,?,?,?,?,?,?,?,?);";

                //給?賦值
                PreparedStatement pstmt = conn.prepareStatement(sql);
                pstmt.setString(1,jobInfo.getCompanyName());
                pstmt.setString(2,jobInfo.getCompanyAddr());
                pstmt.setString(3,jobInfo.getCompanyInfo());
                pstmt.setString(4,jobInfo.getJobName());
                pstmt.setString(5,jobInfo.getJobInfo());
                pstmt.setInt(6,jobInfo.getSalaryMin());
                pstmt.setInt(7,jobInfo.getSalaryMax());
                pstmt.setString(8,jobInfo.getUrl());
                pstmt.setString(9,jobInfo.getTime());
                //執行sql,接受返回結果
                int count = pstmt.executeUpdate();
                //處理結果
                System.out.println(count);
                //釋放資源
                conn.close();
                pstmt.close();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

爬取的信息 中的公司給的工資有以年爲單位的 有以月的 這裏轉換一下
SalaryMath.java

package spider.utils;

public class SalaryMath{

    public static Integer[] getSalary(String salaryStr) {
        //聲明存放薪水範圍的數組
        Integer[] salary = new Integer[2];

        String date = salaryStr.substring(salaryStr.length() - 1, salaryStr.length());
        //如果是按天,則直接乘以240進行計算
        if (!"月".equals(date) && !"年".equals(date)) {
            salaryStr = salaryStr.substring(0, salaryStr.length() - 2);
            salary[0] = salary[1] = str2Num(salaryStr, 240);
            return salary;
        }

        String unit = salaryStr.substring(salaryStr.length() - 3, salaryStr.length() - 2);
        String[] salarys = salaryStr.substring(0, salaryStr.length() - 3).split("-");

        salary[0] = mathSalary(date, unit, salarys[0]);
        salary[1] = mathSalary(date, unit, salarys[1]);

        return salary;
    }

    //根據條件計算薪水
    private static Integer mathSalary(String date, String unit, String salaryStr) {
        Integer salary = 0;

        //判斷單位是否是萬
        if ("萬".equals(unit)) {
            //如果是萬,薪水乘以1000
            salary = str2Num(salaryStr, 1000);
        } else {
            //否則乘以100
            salary = str2Num(salaryStr, 100);
        }

        //判斷時間是否是月
        if ("月".equals(date)) {
            //如果是月,薪水乘以12
            salary = str2Num(salary.toString(), 12);
        }

        return salary;
    }

    private static int str2Num(String salaryStr, int num) {
        try {
            // 把字符串轉爲小數,必須用Number接受,否則會有精度丟失的問題
            Number result = Float.parseFloat(salaryStr) * num;
            return result.intValue();
        } catch (Exception e) {
        }
        return 0;
    }
}

測試類

package spiderTest;

import org.junit.Test;
import spider.todo.DataPipline;
import spider.todo.SpiderGoGO;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.scheduler.BloomFilterDuplicateRemover;
import us.codecraft.webmagic.scheduler.QueueScheduler;

public class SpiderTest {
    @Test
    public void main(){
        long startTime, endTime;
        System.out.println("開始爬取數據");
        startTime = System.currentTimeMillis();
        String url = "https://search.51job.com/list/010000%252C020000,000000,0000,00,9,99,Java%2B%25E5%2590%258E%25E5%258F%25B0,2,1.html?lang=c&stype=1&postchannel=0000&workyear=99&cotype=99&degreefrom=99&jobterm=99&companysize=99&lonlat=0%2C0&radius=-1&ord_field=0&confirmdate=9&fromType=1&dibiaoid=0&address=&line=&specialarea=00&from=&welfare=";
        Spider.create(new SpiderGoGO())
                .addUrl(url)
               .setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))
                .thread(10)
                .addPipeline(new DataPipline()) //存儲數據
                .run();
        endTime = System.currentTimeMillis();
        System.err.println("爬取結束,耗時約" + ((endTime - startTime) / 1000) + "秒");
    }
}

結果:
在這裏插入圖片描述
這裏爬取了 1000條數據

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章