Java爬虫——WebMagic案例

news2024/9/24 17:18:08

抓取51Job的招聘信息

一, Scheduler组件

在解析页面的时候,很可能会解析出相同的url地址(例如商品标题和商品图片超链接,而且url一样),如果不进行处理,同样的url会解析处理多次,浪费资源。所以我们需要有一个url去重的功能

WebMagic提供了Scheduler可以帮助我们解决以上问题。

Scheduler是WebMagic中进行URL管理的组件。一般来说,Scheduler包括两个作用

*对待抓取的URL队列进行管理。

*对已抓取的URL进行去重。

WebMagic内置了几个常用的Scheduler。如果你只是在本地执行规模比较小的爬虫,那么基本无需定制Scheduler,但是了解一下已经提供的几个Scheduler还是有意义的

去重部分被单独抽象成了一个接口:DuplicateRemover,从而可以为同一个Scheduler选择不同的去重方式,以适应不同的需要,目前提供了两种去重方式

RedisScheduler是使用Redis的set进行去重,其他的Scheduler(包括未指定的)默认都使用HashSetDuplicateRemover来进行去重

如果要使用BloomFilter,必须要加入以下依赖:

<!--WebMagic对布隆过滤器的支持-->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>16.0</version>
</dependency>

添加布隆过滤器:

    public static void main(String[] args) {
        Spider.create(new JobProcessor())
                .addUrl("https://kuaibao.jd.com/")//添加需要爬取的网页url
                //不添加addPipeline则会将结果输出在控制台
//                .addPipeline(new FilePipeline("C:\\Users\\admin\\Desktop\\result\\"))//将结果保存在文件中
                .thread(5)//设置多线程
                //创建Schedule抽取url,将url保存在内存队列中,并使用布隆过滤器去重,最大去重100000条;如果不设置Schedule,默认采用HashSet去重
                .setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))
                .run();//执行
        //不需要执行打印,不设置输入流的位置,WebMagic默认输出在控制台
    }

三种去重方式

去重就有三种实现方式,那有什么不同呢?

HashSet

使用java中的HashSet不能重复的特点去重。优点是容易理解。使用方便。

缺点:占用内存大,性能较低。

Redis去重

​ 使用Redis的set进行去重。优点是速度快(Redis本身速度就很快),而且去重不会占用爬虫服务器的资源,可以处理更大数据量的数据爬取。

缺点:需要准备Redis服务器,增加开发和使用成本。

布隆过滤器 (BloomFilter) (使用较多)

使用布隆过滤器也可以实现去重。优点是占用的内存要比使用HashSet要小的多,也适合大量数据的去重操作。

缺点:有误判的可能。没有重复可能会判定重复,但是重复数据一定会判定重复。

布隆过滤器 (Bloom Filter)是由Burton Howard Bloom于1970年提出,它是一种space efficient的概率型数据结构,用于判断一个元素是否在集合中。在垃圾邮件过滤的黑白名单方法、爬虫(Crawler)的网址判重模块中等等经常被用到。

哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。布隆过滤器可以插入元素,但不可以删除已有元素。其中的元素越多,误报率越大,但是漏报是不可能的。

3. Pipeline

在WebMagic中,Pileline是抽取结束后,进行处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。在这里我们会定制Pipeline实现数据导入到数据库中

Pipeline的接口定义如下:

public interface Pipeline {

    // ResultItems保存了抽取结果,它是一个Map结构,
    // 在page.putField(key,value)中保存的数据,
    //可以通过ResultItems.get(key)获取
    public void process(ResultItems resultItems, Task task);
}

可以看到,Pipeline其实就是将PageProcessor抽取的结果,继续进行了处理的,其实在Pipeline中完成的功能,你基本上也可以直接在PageProcessor实现,那么为什么会有Pipeline?有几个原因:

1.为了模块分离

“页面抽取”和“后处理、持久化”是爬虫的两个阶段,将其分离开来,一个是代码结构比较清晰,另一个是以后也可能将其处理过程分开,分开在独立的线程以至于不同的机器执行。

Pipeline的功能比较固定,更容易做成通用组件

每个页面的抽取方式千变万化,但是后续处理方式则比较固定,例如保存到文件、保存到数据库这种操作,这些对所有页面都是通用的。

在WebMagic里,一个Spider可以有多个Pipeline,使用Spider.addPipeline()即可增加一个Pipeline。这些Pipeline都会得到处理,例如可以使用实现输出结果到控制台,并且保存到文件的目标。

spider.addPipeline(new ConsolePipeline()).addPipeline(new FilePipeline())

2. 环境搭建

建数据表

CREATE Table job_info(
    id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
    company_name VARCHAR(100) DEFAULT NULL COMMENT '公司名称',
    company_addr VARCHAR(200) DEFAULT NULL COMMENT '公司地址',
    company_info TEXT  COMMENT '公司信息',
    job_name VARCHAR(100) DEFAULT NULL COMMENT '职位名称',
    job_addr VARCHAR(100) DEFAULT NULL COMMENT '工作地点',
    job_info TEXT COMMENT "职位信息",
    salary_min int(10) DEFAULT NULL COMMENT '最低薪资',
    salary_max INT(10) DEFAULT NULL COMMENT '最高薪资',
    url VARCHAR(150) DEFAULT NULL COMMENT '招聘信息详情页',
    time VARCHAR(20) DEFAULT NULL COMMENT '职位最新发布时间',
    PRIMARY KEY (id)
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

在pom.xml中添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.ayulong</groupId>
    <artifactId>ayulong-crawler-job</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--SpringMVC-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--SpringData Jpa-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!--MySQL连接包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>

        <!--WebMagic核心包-->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-core</artifactId>
            <version>0.7.3</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--WebMagic扩展-->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-extension</artifactId>
            <version>0.7.3</version>
        </dependency>
        <!--WebMagic对布隆过滤器的支持-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0</version>
        </dependency>

        <!--工具包  stringUtils -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

加入配置文件

在resources目录下添加application.yml文件

链接数据库

spring:
  # 数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/crawlerDB?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: 
    password: 
  # jpa配置
  jpa:
    database: MySQL
    show-sql: false

在resources目录下添加log4j.properties文件

log4j.rootLogger=INFO,A1 

log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH🇲🇲ss,SSS} [%t] [%c]-[%p] %m%n

编写pojo

@Entity
@Data
@ToString
public class JobInfo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String companyName;
    private String companyAddr;
    private String companyInfo;
    private String jobName;
    private String jobAddr;
    private String jobInfo;
    private Integer salaryMin;
    private Integer salaryMax;
    private String url;
    private String time;

}

编写Dao

package cn.ayulong.job.dao;

import cn.ayulong.job.pojo.JobInfo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JobInfoDao extends JpaRepository<JobInfo, Long> {
}

编写Service

编写service接口

此处知识 jpa 的查询方式

public interface JobInfoService {

    /**
     * 保存工作信息
     * @param jobInfo
     */
    public void save(JobInfo jobInfo);

    /**
     * 根据条件查询工作信息
     * @param jobInfo
     * @return
     */
    public List<JobInfo> findJobInfo(JobInfo jobInfo);
}

编写service实现类


@Service
public class JobInfoServiceImpl implements JobInfoService {

    @Autowired
    private JobInfoDao jobInfoDao;

    @Override
    @Transactional
    public void save(JobInfo jobInfo) {

        // 根据每条工作信息的url和发布时间查询数据
        JobInfo param = new JobInfo();
        param.setUrl(jobInfo.getUrl());
        param.setTime(jobInfo.getTime());

        // 执行查询
        List<JobInfo> list = this.findJobInfo(param);

        // 判断查询结果是否为空
        if (list.size() == 0) {
            // 如果查询结果为空, 表示招聘信息数据不存在, 或者已经更新了, 需要增或者更新数据库
            this.jobInfoDao.saveAndFlush(jobInfo);
        }

    }

    @Override
    public List<JobInfo> findJobInfo(JobInfo jobInfo) {

        // 设置查询条件
        Example example = Example.of(jobInfo);

        // 执行查询
        List list = this.jobInfoDao.findAll(example);

        return list;
    }
}

创建引导类

@SpringBootApplication
@EnableScheduling // 开启定时任务
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

环境搭建完毕

3. 代码实现

添加工具类MathSalary(计算薪资的工具类,可忽略)

package cn.ayulong.job.task;

// 转换薪资
public class MathSalary {


    /**
     * 获取薪水范围
     *
     * @param salaryStr
     * @return
     */
    public static Integer[] getSalary(String salaryStr) {
        //声明存放薪水范围的数组
        Integer[] salary = new Integer[2];

        //"500/天"
        //0.8-1.2万/月
        //5-8千/月
        //5-6万/年
        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)) {
            //如果是万,薪水乘以10000
            salary = str2Num(salaryStr, 10000);
        } else {
            //否则乘以1000
            salary = str2Num(salaryStr, 1000);
        }

        //判断时间是否是月
        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;
    }
}

自定义Pipeline存储数据到数据库中

在WebMagic中,Pileline是抽取结束后,进行处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。在这里我们会定制Pipeline实现数据导入到数据库中

@Component
public class SpringDatePipeline implements Pipeline {
    @Autowired
    private JobInfoDao jobInfoDao;
     @Override
   public void process(ResultItems resultItems, Task task) {
        //获取封装好的对象
        JobInfo jobInfo = resultItems.get("jobInfo");
        //判断数据的hi否不为空
        if (jobInfo != null){
            //不为空,保存到数据库
            this.jobInfoDao.saveAndFlush(jobInfo);
        }

    }
}

编写主要的爬虫类(核心)

稍微自己修改了一下, 因为 51job 现在的网页和之前相比改了一些, 数据不能直接在网页的元素上拿到, 所以解析数据的方式也得有所变化, 只能想办法解析网页中的js代码, 测试基本没有什么问题

@Component
public class JobProcessor implements PageProcessor {

    // 因为拿不到下一页的链接只好设置一个变量
    private int count = 1;
    private String url = "https://search.51job.com/list/030200,000000,0000,00,9,99,java,2,1.html?lang=c&postchannel=0000&workyear=99&cotype=99&degreefrom=99&jobterm=99&companysize=99&ord_field=0&dibiaoid=0&line=&welfare=";
    private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-");
    private String today = formatter.format(new Date());
    /**
    *解析页面的方法
    **/
    @Override
    public void process(Page page) {
        // 因为在51招聘的页面中不能直接拿到页面元素, 经分析数据在页面的js中
        // 于是想办法将js对象(json数据)解析出来, 获取到详情页面的链接放到任务队列中
        // 解析页面, 获取script中招聘详情信息
        String dataJs = page.getHtml().css("script").regex(".*SEARCH_RESULT.*").get();
        // 判断获取到的页面是否为空
        if (!StringUtils.isEmpty(dataJs)) {
            System.out.println("开始抓取第" + count++ + "页");
            // 如果不为空, 表示这是列表页
            // 解析拿到json字符串
            dataJs = dataJs.substring(dataJs.indexOf("{"), dataJs.lastIndexOf("}") + 1);
            // 创建json对象
            JSONObject jsonObject = (JSONObject) JSONObject.parse(dataJs);
            // 根据分析拿到放置信息的数组
            JSONArray resArr = jsonObject.getJSONArray("engine_search_result");
            // 判断数组中是否存在数据
            if (resArr.size() > 0) {
                for (int i = 0; i < resArr.size(); i++) {
                    // 获取数组中的每一个对象
                    JSONObject resObj = (JSONObject) resArr.get(i);
                    //把获取到的url地址放到任务队列中
                    page.addTargetRequest(String.valueOf(resObj.get("job_href")));
                }
                // 获取下一页的url
                String bkUrl = "https://search.51job.com/list/030200,000000,0000,00,9,99,java,2," + (count + 1) + ".html?lang=c&postchannel=0000&workyear=99&cotype=99&degreefrom=99&jobterm=99&companysize=99&ord_field=0&dibiaoid=0&line=&welfare=";
                // 把url放到任务队列中
                page.addTargetRequest(bkUrl);
            } else {
                // 设置变量为初始值
                count = 0;
                // 如果没有数据那么爬虫结束
                return;
            }
        } else {
            // 如果为空, 表示这是招聘详情页, 解析页面, 获取招聘详情信息, 保存数据
            this.saveJobInfo(page);
        }

    }

    // 解析页面, 获取招聘详情信息, 保存数据
    private void saveJobInfo(Page page) {
        // 创建招聘详情对象
        JobInfo jobInfo = new JobInfo();

        // 解析页面
        Html html = page.getHtml();

        // 获取数据, 封装到对象中
        // 拿到有地址和发布时间的那条文字
        String desc = Jsoup.parse(html.css("p.msg.ltype").toString()).text();
        if (desc == null) {
            // 有极少数非51内部网站无法找到
            return;
        }
        // 为了能够找到发布时间截取了字符串
        desc = desc.substring(0, desc.lastIndexOf("发布"));
        // 设置公司名称
        jobInfo.setCompanyName(html.css("div.cn p.cname a", "text").toString().trim());
        // 设置公司地址
        jobInfo.setCompanyAddr(desc.substring(0, desc.indexOf("|")).trim());
        // 设置公司信息
        jobInfo.setCompanyInfo(Jsoup.parse(html.css("div.tmsg").toString()).text());
        // 设置职位名称
        jobInfo.setJobName(html.css("div.cn h1", "text").toString());
        // 设置工作地址
        jobInfo.setJobAddr(html.css("div.bmsg>p.fp", "text").toString());
        // 设置工作信息(要求)
        jobInfo.setJobInfo(Jsoup.parse(html.css("div.job_msg").toString()).text());
        // 设置当前链接
        jobInfo.setUrl(page.getUrl().toString());
        // 获取薪资
        // 有的没有写薪资, 先获取薪资的字符串
        String salaryText = html.css("div.cn strong", "text").toString();
        // 看看是否没有薪资这个字段
        if (!StringUtils.isEmpty(salaryText)) {
            // 使用工具类转换薪资字符串
            Integer[] salary = MathSalary.getSalary(salaryText);
            jobInfo.setSalaryMax(salary[0]);
            jobInfo.setSalaryMin(salary[1]);
        } else {
            // 没有则设为零
            jobInfo.setSalaryMax(0);
            jobInfo.setSalaryMin(0);
        }
        // 获取发布时间
       // 获取发布时间
        String time = desc.substring(desc.lastIndexOf("|") + 3);
        jobInfo.setTime(today + time.trim());
        // 把结果保存起来
        page.putField("jobInfo", jobInfo);
    }

    private Site site = Site.me()
            .setCharset("gbk") // 设置编码 如果乱码还utf-8
            .setTimeOut(10*1000) // 设置超时时间
            .setRetrySleepTime(3000) // 设置重试的间隔时间
            .setRetryTimes(3); // 设置重试的次数

    @Override
    public Site getSite() {
        return site;
    }
    //注入自定义的存储方式
    @Autowired
    private SpringDataPipeline springDataPipeline;

    // initialDelay当任务启动后, 等多久执行方法
    // fixedDelay每隔多久执行方法
    @Scheduled(initialDelay = 1000, fixedDelay = 1000 * 1000)
    public void process() {
        Spider.create(new JobProcessor())
                .addUrl(url)
                // 设置Secheduler
                .setScheduler(new QueueScheduler()
                        // 设置Bloom去重
                        // //QueueScheduler队列保存抓取url。BloomFilterDuplicateRemover布隆过滤器  值根据实际进行设置,最好比预估(数据)值大(越大越占内存)
                        .setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))
                .thread(10)
                // 设置自定义的Pipeline储存数据方式
                .addPipeline(this.springDataPipeline)
                .run();
    }
}

51Job使用爬虫抓取数据,已经开始需要验证是否是认为操作,小趴菜太菜,只学习了这么一点内容,不知道如何反反爬虫

下面是小趴菜的自己写的PageProcessor核心代码

@Component
public class JobProcessor implements PageProcessor {
    //访问的起始url地址
    private int count = 1;
    private String url = "https://search.51job.com/list/030200,000000,0000,00,9,99,java,2,1.html?lang=c&postchannel=0000&workyear=99&cotype=99&degreefrom=99&jobterm=99&companysize=99&ord_field=0&dibiaoid=0&line=&welfare=";
//    private String url = "https://www.liepin.com/zhaopin/?inputFrom=www_index&workYearCode=0&key=java&scene=input&ckId=gr3kkf194u30rv9s38bculwtahg54c6a&dq=";
    private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-");
    private String today = formatter.format(new Date());

    //核心解析页面
    @Override
    public void process(Page page) {
        //解析页面,获取招聘信息的url地址
        List<Selectable> lists = page.getHtml().css("div.j_joblist  div.e sensors_exposure").nodes();//div.j_joblist div.e_icons ick a.el

//      // 判断获取到的页面是否为空
        if (lists.size() == 0){
            //岗位详情页
            this.saveJobInfo(page);
        }else {
            //招聘信息列表页,解析列表的详情页url,放到任务中
            for (Selectable selectable : lists) {
                String jobInfoUrl = selectable.links().toString();
                //将获取到的url,提交到任务队列中
                page.addTargetRequest(jobInfoUrl);
            }
            //获取下一页的url
            String bkUrl = page.getHtml().css("div.p_in li.bk").nodes().get(1).links().toString();
            //添加到任务队列中
            page.addTargetRequest(bkUrl);
        }

    }

    //解析页面,获取招聘的详情信息
    private void saveJobInfo(Page page) {
        //创建 招聘详情对象
        JobInfo jobInfo = new JobInfo();
        Html html = page.getHtml();

        jobInfo.setCompanyName(html.css("div.cn p.cname a","text").toString());
        jobInfo.setCompanyAddr(Jsoup.parse(html.css("div.bmsg").nodes().get(1).toString()).text());
        jobInfo.setCompanyInfo(Jsoup.parse(html.css("div.tmsg").toString()).text());//获取标签内部的文本内容
        jobInfo.setJobInfo(Jsoup.parse(html.css("div.job_msg").toString()).text());
        jobInfo.setJobName(html.css("div.cn h1","text").toString());
        jobInfo.setJobAddr(html.css("div.cn span.lname","text").toString());
//        jobInfo.setSalaryMin();
//        jobInfo.setSalaryMax();
        jobInfo.setUrl(page.getUrl().toString());
//        jobInfo.setTime();
        //把结果保存在Pipeline中
        page.putField("jobInfo",jobInfo);
    }

    private Site site = Site.me()
            .setCharset("utf8")//设置编码
            .setTimeOut(10 * 1000)//设置超时时间
            .setRetryTimes(3)//设置重试次数
            .setRetrySleepTime(3 * 1000);//设置重试时间间隔

    @Override
    public Site getSite() {
        return site;
    }

    @Autowired
    private SpringDatePipeline springDatePipeline;

    //@Scheduled:开启定时任务
    //initialDelay:当任务启动后,等待多久启动方法
    //fixedDelay:每隔多久执行方法
    @Scheduled(initialDelay = 1000,fixedDelay = 100 * 1000)
    public void process(){
        Spider.create(new JobProcessor())//创建一个Spider对象
                .addUrl(url)//添加开始地址
                //设置url去重
                .setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))
                .thread(10)//线程数量设置
                .addPipeline(springDatePipeline)//持久化到数据库
                .run();//运行
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/355074.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Unity3d】Unity与Android之间通信

在unity开发或者sdk开发经常遇到unity与移动端原生层之间进行通信&#xff0c;这里把它们之间通信做一个整理。 关于Unity与iOS之间通信&#xff0c;参考【Unity3d】Unity与iOS之间通信 Unity(c#)调用Android (一)、编写Java代码 实际上&#xff0c;任何已经存在的Java代码…

【webpack5】一些常见优化配置及原理介绍(二)

这里写目录标题介绍sourcemap定位报错热模块替换&#xff08;或热替换&#xff0c;HMR&#xff09;oneOf精准解析指定或排除编译开启缓存多进程打包移除未引用代码配置babel&#xff0c;减小代码体积代码分割&#xff08;Code Split&#xff09;介绍预获取/预加载(prefetch/pre…

HydroD 实用教程(四)水动力模型

目 录一、前言二、Hydro Properties2.1 Compartment Properties2.2 Rudder and Thruster2.3 Wind Properties三、Hydro Structure3.1 Load Cross Sections四、Loading Conditions4.1 Mass Model4.2 Second Order Surface Model4.3 Wadam Offbody Points4.4 Additional Matrices…

Redis的常见操作和Session的持久化

安装Redis使用yum命令&#xff0c;直接将redis安装到linux服务器&#xff1a;yum -y install redis启动redis使用以下命令&#xff0c;以后台运行方式启动redis&#xff1a;redis -server /etc/redis.conf &操作redis使用以下命令启动redis客户端&#xff1a;redis-cli设置…

vscode连接服务器(腾讯云)

文章目录1. vscode远程总是报错2. vscode能连上腾讯云但密码不对或者登录后不能打开文件或文件夹1. vscode远程总是报错 报错如图所示 Could not establish connection to *** 过程试图写入的管道不存在。 在百度、csdn找了好久都是说删掉.ssh文件下的某个文件但我压根没有&a…

使用 nutjs实现前端RPA需求

nutjs 相关概念 nut.js 是 Node.js 的桌面自动化框架&#xff0c;我们可以使用 js / ts 来控制鼠标和键盘&#xff0c;来模拟人的操作完成一系列动作。它主要分为以下三块内容&#xff1a; KeyboardMouseScreen Keyboard 常用 API type: 它允许我们输入 键 或 字符串&#…

linux013之文件和目录的权限管理

用户、组、文件目录的关系&#xff1a; 简介&#xff1a;用户和组关联&#xff0c;组合文件目录关联&#xff0c;这样就实现了用户对文件的权限管理。首先来看一下&#xff0c;一个文件或目录的权限是怎么查看的&#xff0c;ls -l&#xff0c; 如下&#xff0c;这个信息怎么看呢…

71. Python 库与模块

71. 库与模块 文章目录71. 库与模块1. Python 就是一个解释器2. 什么模块3. 什么是包5. 什么是库6. 形象理解模块与库7. 库的分类1. 标准库2. 第三方库8. 总结1. Python 就是一个解释器 我们把程序员编写的代码形象理解为写了一个纯文本文件。 我们希望这个文本文件能被计算机…

锁相环(1)

PLL代表相位锁定环。顾名思义&#xff0c;如下图所示&#xff0c;PLL是一种具有反馈循环的电路&#xff0c;可将反馈信号的相/频率保持与参考输入信号的相/频率相同&#xff08;锁定&#xff09;。 如下图所示&#xff0c;如果参考输入和反馈输入之间存在相位差&#xff0c;则…

是德Keysight E4991A/e4991B射频阻抗/材料分析仪

Keysight E4991A 射频阻抗/材料分析仪提供终极阻抗测量性能和强大的内置分析功能。它将为评估 3 GHz 范围内组件的组件和电路设计人员的研发提供创新。E4991A 使用 RF-IV 技术&#xff0c;而不是反射测量技术&#xff0c;可在宽阻抗范围内进行更精确的阻抗测量。基本阻抗精度为…

在屎山代码中快速找到想要的代码法-锁表法(C#)

由于本人水平有限&#xff0c;文中有写得不对的地方请指正&#xff0c;本文的方法有些投机取巧&#xff0c;实在是没招的情况下可以酌情使用&#xff0c;如有侵权&#xff0c;请联系删除。 前几天接到一个需求&#xff0c;要在医嘱签署时对检验项目进行分方操作&#xff0c;分…

Linux Socket Buffer介绍

一. 前言 Linux内核网络子系统的实现之所以灵活高效&#xff0c;主要是在于管理网络数据包的缓冲器-socket buffer设计得高效合理。在Linux网络子系统中&#xff0c;socket buffer是一个关键的数据结构&#xff0c;它代表一个数据包在内核中处理的整个生命周期。 二. Socket Bu…

2.5|iot|第1章嵌入式系统概论|操作系统概述|嵌入式操作系统

目录 第1章&#xff1a; 嵌入式系统概论 1.嵌入式系统发展史 2.嵌入式系统定义* 3.嵌入式系统特点* 4.嵌入式处理器的特点 5.嵌入式处理分类 6.嵌入式系统的应用领域及嵌入式系统的发展趋势 第8章&#xff1a;Linux内核配置 1.内核概述 2.内核代码结构 第1章&#xf…

[安装之3] 笔记本加装固态和内存条教程(超详细)

由于笔记本是几年前买的了&#xff0c;当时是4000&#xff0c;现在用起来感到卡顿&#xff0c;启动、运行速度特别慢&#xff0c;就决定换个固态硬盘&#xff0c;加个内存条&#xff0c;再给笔记本续命几年。先说一下加固态硬盘SSD的好处&#xff1a;1.启动快 2.读取延迟小 3.写…

Java每日一练——Java简介与基础练习

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 文章目录 目录 系列文章目录 文章目录 前言 一、简述解释型语言与编译型语言 二、Java语言的执行流程 2.1、…

电子技术——CS和CE放大器的高频响应

电子技术——CS和CE放大器的高频响应 在绘制出MOS和BJT的高频响应模型之后&#xff0c;我们对MOS和BJT的高频响应有了进一步的认识。现在我们想知道的是在高频响应中 fHf_HfH​ 的关系。 高频响应分析对电容耦合还是直接耦合都是适用的&#xff0c;因为在电容耦合中高频模式下…

智能小车红外避障原理

红外避障电路红外避障电路由电位器R17&#xff0c;R28&#xff1b;发光二极管D8&#xff0c;D9&#xff1b;红外发射管 D2&#xff0c;D4和红外接收管D3&#xff0c;D5和芯片LM324等组成,LM234用于信号的比较&#xff0c;并产生比较结果输出给单片机进行处理。智能小车红外避障…

作为一名开发工程师,我对 ChatGPT 的一些看法

ChatGPT 又又火了。 ChatGPT 第一次爆火是2022年12月的时候,我从一些球友的讨论中知道了这个 AI 程序。 今年2月,ChatGPT 的热火更加猛烈,这时我才意识到,原来上次的热火只是我们互联网圈子内部火了,这次是真真正正的破圈了,为大众所熟悉了。 这个 AI 程序是一个智能问…

(考研湖科大教书匠计算机网络)第四章网络层-第八节:网际控制报文协议ICMP

获取pdf&#xff1a;密码7281专栏目录首页&#xff1a;【专栏必读】考研湖科大教书匠计算机网络笔记导航 文章目录一&#xff1a;网际控制报文协议ICMP&#xff08;1&#xff09;ICMP差错报告报文A&#xff1a;终点不可达B&#xff1a;源点抑制C&#xff1a;时间超过D&#xff…

C#的委托原理刨析and事件原理刨析和两者的比较

什么是委托委托是一种引用类型&#xff0c;表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时&#xff0c;你可以将其实例与任何具有兼容参数和返回类型的方法进行绑定。 你可以通过委托实例调用方法。简单的理解&#xff0c;委托是方法的抽象类&#xff0c;它定…