SpringBoot3集成TDengine自适应裂变存储

news2024/9/27 15:34:51

前言

        首先很遗憾的告诉大家,今天这篇分享要关注才可以看了。原因是穷啊,现在基本都是要人民币玩家了,就比如chatGPT、copilot,这些AI虽然都是可以很好的辅助编码,但是都是要钱。入驻CSDN有些年头了,中间有几年大学毕业,失恋了没有写,沉沦了几年。后面逐渐捡起来,我们之间应该说是互相成就吧,亦师亦友亦笔记。说实话,其实CSDN之前有出一些插件,我很欣慰,也一直在用,其实我一直希望CSDN能出个copilot采用AI辅助就好了。或者国内几大技术论坛能一起搞个也行,其实大家都是有这方面的优势的,至少代码、训练库是足够的。
        言归正传,今天要要分享的要是紧接之前的设计:物联网设备流水入库TDengine改造方案,这里是具体的实现过程。这个是TDengine可自动扩展列方案,这个方案实现代码绝对是目前独家,关注我,你值得拥有。


一、整体思路

        整体思路:消费信息 》》 数据转换 》》组织sql 》》orm框架自动配备数据源》》执行入库TDengine》》异常处理(扩展的核心)》》DDL执行扩列》》再次执行入库。。。。
        这里大家应该可以猜到具体做法了,其实要不是因为这个列不固定,实现起来可简单多了,也可以用超级表,而且性能也会好很多。更重要的是可以用ORM框架,基本不用写啥sql。而且查询结果用实体接受数据,不会出现VARCHAR字段不能正确显示字符串的问题(我就是被这个坑了下)。
        其实也可以用flink等消费信息,做入库处理,当然这样处理可就不能用ORM框架了,只能用经典的JDBC。
        核心思路:根据设备上报数据,做插入数据转换sql,执行入库处理异常,根据异常做DDL操作,实现自动扩列,最后入库。上报的数据:json串做数据转换,数据值做反射获取类型,转换为对应的扩列sql执行、组织入库sql。

二、实现流程图

在这里插入图片描述
我的整体环境:SpringBoot3 + mybatisPlus + 双数据源(mysql、TDengine)+ 集成kafka
消费上游平台放入kafka的信息,然后走以上流程,目标执行入库TDengine。

三、核心代码

这里的整体框架我之前的博文有写,并且是公开独家分享到csdn的gitCode:https://gitcode.net/zwrlj527/data-trans.git

1.引入库

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

2.配置文件

spring:
#kafka配置
  kafka:
    #bootstrap-servers: 192.168.200.72:9092,192.168.200.73:9092
    #bootstrap-servers: 192.168.200.83:9092,192.168.200.84:9092
    bootstrap-servers: localhost:9092
    client-id: dc-device-flow-analyze
    consumer:
      group-id: dc-device-flow-analyze-consumer-group
      max-poll-records: 10
      #Kafka中没有初始偏移或如果当前偏移在服务器上不再存在时,默认区最新 ,有三个选项 【latest, earliest, none】
      auto-offset-reset: earliest
      #是否开启自动提交
      enable-auto-commit: false
      #自动提交的时间间隔
      auto-commit-interval: 1000
    listener:
      ack-mode: MANUAL_IMMEDIATE
      concurrency: 1  #推荐设置为topic的分区数
      type: BATCH #开启批量监听

#消费topic配置
xiaotian:
  analyze:
    device:
      flow:
        topic:
          consumer: device-flow

3.kafka消费监听

package com.xiaotian.datagenius.kafka;

import com.xiaotian.datagenius.service.DataTransService;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 消费者listener
 *
 * @author zhengwen
 **/
@Slf4j
@Component
public class KafkaListenConsumer {

    @Autowired
    private DataTransService dataTransService;

    /**
     * 设备流水listenner
     *
     * @param records 消费信息
     * @param ack     Ack机制
     */
    @KafkaListener(topics = "${easylinkin.analyze.device.flow.topic.consumer}")
    public void deviceFlowListen(List<ConsumerRecord> records, Acknowledgment ack) {
        log.debug("=====设备流水deviceFlowListen消费者接收信息====");
        try {

            for (ConsumerRecord record : records) {
                log.debug("---开启线程解析设备流水数据:{}", record.toString());
                dataTransService.deviceFlowTransSave(record);
            }

        } catch (Exception e) {
            log.error("----设备流水数据消费者解析数据异常:{}", e.getMessage(), e);
        } finally {
            //手动提交偏移量
            ack.acknowledge();
        }
    }


}

4.消息具体处理方法(实现)

package com.xiaotian.datagenius.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.xiaotian.datagenius.mapper.tdengine.DeviceFlowRecordMapper;
import com.xiaotian.datagenius.mapper.tdengine.TableOperateMapper;
import com.xiaotian.datagenius.service.DataTransService;
import com.xiaotian.datagenius.utils.TDengineDbUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * @author zhengwen
 */
@Slf4j
@Service
public class DataTransServiceImpl implements DataTransService {
    /**
     * 专门记录业务错误日志
     */
    private final static Logger logger = LoggerFactory.getLogger("businessExp");

    @Autowired
    private KafkaTemplate kafkaTemplate;

    @Autowired
    private TableOperateMapper tableOperateMapper;

    @Autowired
    private DeviceFlowRecordMapper deviceFlowRecordMapper;


    @Override
    public void deviceFlowTransSave(ConsumerRecord record) {
        log.debug("----设备流水转换解析存储----");
        log.debug(String.format("offset = %d, key = %s, value = %s%n \n", record.offset(), record.key(), record.value()));
        //字段不可控,所以没有实体可言,只能直接sql
        //先直接执行插入,try异常 -> 如果是报字段不存在 -> 执行校验字段 -> dml创建字段
        //再执行插入
        String stableName = "device_flow_mater";
        String tableName = "device_flow_record";
        String recordStr = record.value().toString();
        if (JSONUtil.isTypeJSON(recordStr)) {
            JSONObject recordJson = JSONUtil.parseObj(recordStr);
            //初始化语句
            Map<String, Map<String, String>> columnData = new HashMap<>();
            String insertSql = initDataInsertSql(recordJson, tableName, columnData);
            //保存数据
            saveRecord(recordJson, insertSql, columnData, tableName);
        } else {
            logger.error("---设备上报数据推送信息格式异常,无法解析---");
        }

    }


    /**
     * 初始化数据插入语句
     *
     * @param recordJson 记录json
     * @param tableName  表名
     * @param columnData 字段信息
     * @return 数据插入语句
     */
    private String initDataInsertSql(JSONObject recordJson, String tableName, Map<String, Map<String, String>> columnData) {
        //这里先转换成sql的字段、value
        StringJoiner columnSj = new StringJoiner(",");
        StringJoiner valueSj = new StringJoiner(",");
        String insertSql = transInitInsertSql(tableName, columnSj, valueSj, recordJson, columnData);
        if (StringUtils.isBlank(insertSql)) {
            logger.error("---上报数据转插入语句异常,上报数据:{}", JSONUtil.toJsonStr(recordJson));
            return null;
        }
        return insertSql;
    }


    /**
     * 保存记录
     *
     * @param recordJson 记录json对象
     * @param insertSql  插入语句
     * @param columnData 字段信息
     * @param tableName  普通表或子表
     */
    private void saveRecord(JSONObject recordJson, String insertSql, Map<String, Map<String, String>> columnData, String tableName) {

        try {
            //boolean insertRes = SqlRunner.db(DeviceFlowMaterRecord.class).insert(insertSql, '1');
            int num = deviceFlowRecordMapper.insert(insertSql);
        } catch (Exception e) {
            logger.error("Error inserting,{}", e.getMessage());
            Throwable throwable = e.getCause();
            String msg = throwable.getMessage();
            //报缺少字段、字段长度不够
            if (msg.contains("Invalid column name:") || msg.contains("Value too long for column/tag")) {
                transAddOrChangeColumnsSql(columnData, tableName, recordJson, insertSql);
            }

        }

    }


    /**
     * 转换扩展列
     *
     * @param columnData 上报数据字段信息map
     * @param tableName  表名
     * @param recordJson 上报数据json
     * @param insertSql  插入语句
     */
    private void transAddOrChangeColumnsSql(Map<String, Map<String, String>> columnData, String tableName, JSONObject recordJson, String insertSql) {
        String showColumnsSql = "desc " + tableName;
        List<Map<String, Object>> columnLs = tableOperateMapper.operateSql(showColumnsSql);
        if (CollectionUtil.isNotEmpty(columnLs)) {


            //StringBuffer sbf = new StringBuffer();
            //sbf.append("ALTER TABLE ").append(tableName).append(" ADD COLUMN ");
            Map<String, Map<String, Object>> tableColumns = new HashMap<>();

            columnLs.stream().forEach(c -> {
                Object byBufferObj = c.get("field");
                //获取字段
                String field = TDengineDbUtil.getColumnInfoBy(byBufferObj);

                tableColumns.put(field, c);

            });
            columnData.entrySet().forEach(c -> {
                String key = c.getKey();
                Map<String, String> columnMp = c.getValue();
                String length = columnMp.get("length");
                if (tableColumns.containsKey(key)) {
                    //包含字段,比较数据类型长度
                    Map<String, Object> tcMp = tableColumns.get(key);
                    Object byBufferObj = tcMp.get("length");
                    //获取字段长度
                    String dbLength = TDengineDbUtil.getColumnInfoBy(byBufferObj);
                    if (dbLength != null) {
                        if (Integer.parseInt(length) > Integer.parseInt(dbLength)) {
                            String changeColumnSql = TDengineDbUtil.getColumnChangeSql(tableName,length,key);

                            tableOperateMapper.operateSql(changeColumnSql);
                        }
                    }
                } else {
                    //不包含需要执行增加字段
                    String addColumnSql = TDengineDbUtil.getColumnAddSql(tableName,length,key);

                    tableOperateMapper.operateSql(addColumnSql);
                }
            });
            //复调存储
            saveRecord(recordJson, insertSql, columnData, tableName);
        }
    }


    /**
     * 转换初始化插入语句sql
     *
     * @param tableName  表名
     * @param columnSj   字段字符串
     * @param valueSj    值字符串
     * @param recordJson 上报数据json
     * @param columnData 字段Map
     * @return 插入语句sql
     */
    private String transInitInsertSql(String tableName, StringJoiner columnSj, StringJoiner
            valueSj, JSONObject recordJson, Map<String, Map<String, String>> columnData) {
        StringBuffer sb = new StringBuffer();
        //子表不能扩展列,所以超级表思路走不通
        sb.append("insert into ").append(tableName);
        if (!JSONUtil.isNull(recordJson)) {
            JSONObject tmpRecordJson = recordJson;
            JSONObject dataJson = tmpRecordJson.getJSONObject("data");
            Date collectTime = tmpRecordJson.getDate("collectTime");

            tmpRecordJson.remove("data");
            tmpRecordJson.entrySet().forEach(entry -> {
                //TODO 这里要设置调整下数据库区分大小写后去掉
                //String key = entry.getKey().toLowerCase();
                String key = entry.getKey();
                columnSj.add("`" + key + "`");
                Object val = entry.getValue();
                //TODO 校验字符串类型处理sql
                int length = 5;
                if (val != null) {
                    //TODO 几个时间字段传的是long,是转时间类型,还是改字段为字符串?
                    String valStr = TDengineDbUtil.convertValByKey(val,key);
                    valueSj.add(valStr);
                    length = valStr.length() + 5;
                } else {
                    valueSj.add(null).add(",");
                }
                //TODO 字段数据类型后面要优化处理
                Map<String, String> columnMp = TDengineDbUtil.checkColumnType(key, val, length);

                columnData.put(key, columnMp);
            });
            if (!JSONUtil.isNull(dataJson)) {
                dataJson.entrySet().forEach(entry -> {
                    //TODO 这里要设置调整下数据库区分大小写后去掉
                    String key = entry.getKey();
                    columnSj.add("`" + key + "`");
                    Object val = entry.getValue();
                    int length = 3;
                    if (val != null) {
                        //TODO 几个时间字段传的是long,是转时间类型,还是改字段为字符串?
                        String valStr = TDengineDbUtil.convertValByKey(val,key);
                        valueSj.add(valStr);
                        length = valStr.length() + 1;
                    } else {
                        valueSj.add(null).add(",");
                    }
                    //TODO 字段数据类型后面要优化处理
                    Map<String, String> columnMp = TDengineDbUtil.checkColumnType(key, val, length);
                    columnData.put(key, columnMp);
                });
            }
            //Tags
            //sb.append(" TAGS (").append(dataJson).append(",").append(deviceUnitCode).append(",").append(deviceCode).append(") ");
            //sb.append(" TAGS ('").append(JSONUtil.toJsonStr(dataJson)).append("') ");
            //Columns
            columnSj.add("`data_ts`");
            sb.append("(").append(columnSj.toString()).append(") ");
            //Values
            //valueSj.add("'" + DateUtil.format(collectTime, DatePattern.NORM_DATETIME_MS_FORMAT) + "'");
            //主键应该是时间,不能是设备上报数据的时间,因为设备上报数据万一相同就更新了
            valueSj.add("NOW");
            sb.append(" VALUES (").append(valueSj.toString()).append(")");
            logger.debug("----插入语句sql:", sb.toString());
            return sb.toString();
        }

        return null;
    }
}

5.工具类

package com.xiaotian.datagenius.utils;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import io.micrometer.core.instrument.util.TimeUtils;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

/**
 * TDengine数据库工具类
 *
 * @author zhengwen
 */
@Slf4j
public class TDengineDbUtil {
    /**
     * orm框架执行ddl语句返回的字段是byte数组处理
     *
     * @param byBufferObj byte数组object对象
     * @return
     */
    public static String getColumnInfoBy(Object byBufferObj) {
        try {
            if (byBufferObj instanceof byte[]) {
                byte[] bytes = (byte[]) byBufferObj;
                ByteArrayOutputStream bos = new ByteArrayOutputStream();

                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.write(bytes);
                oos.flush();

                String strRead = new String(bytes);
                oos.close();
                bos.close();
                return strRead;
            }
        } catch (IOException e) {
            log.error("----字段异常:{}", e.getMessage());
        }
        return null;
    }

    /**
     * 校验字段类型返回字段信息
     *
     * @param key    字段
     * @param val    值
     * @param length 长度
     * @return 字段信息
     */
    public static Map<String, String> checkColumnType(String key, Object val, int length) {
        Map<String, String> columnMp = new HashMap<>();
        columnMp.put("type", "String");
        columnMp.put("length", String.valueOf(length));
        return columnMp;
    }

    /**
     * @param tableName
     * @param length
     * @param key
     * @return
     */
    public static String getColumnAddSql(String tableName, String length, String key) {
        String beforeSql = "ALTER TABLE " + tableName + " ADD COLUMN ";
        //TODO 处理字段类型
        String addColumnSql = beforeSql + "`" + key + "` NCHAR(" + Integer.parseInt(length) + ")";

        return addColumnSql;
    }

    /**
     * @param tableName
     * @param length
     * @param key
     * @return
     */
    public static String getColumnChangeSql(String tableName, String length, String key) {
        String changeLengthSql = "ALTER TABLE " + tableName + " MODIFY COLUMN ";
        //TODO 处理字段类型
        String changeColumnSql = changeLengthSql + "`" + key + "` NCHAR(" + length + ")";

        return changeColumnSql;
    }

    /**
     * 根据字段、字段值对插入sql的字段值做处理
     *
     * @param val 字段原始值
     * @param key 字段
     * @return 字段转换后的值
     */
    public static String convertValByKey(Object val, String key) {
        //其他全部当字符串处理
        String valStr = "'" + val.toString() + "'";
        //TODO 根据字段处理转换后的字段值,这里暂时对几个时间字段做特殊处理
        if (key.equals("collectTime") || key.equals("createTime") || key.equals("storageTime")) {
            if (val instanceof Long){
                LocalDateTime localDateTime = LocalDateTimeUtil.of(Long.parseLong(val.toString()));
                valStr = "'" + DateUtil.format(localDateTime,DatePattern.NORM_DATETIME_MS_PATTERN) + "'";
            }
            if (val instanceof Integer){
                LocalDateTime localDateTime = LocalDateTimeUtil.of(Long.parseLong(val.toString() + 100));
                valStr = "'" + DateUtil.format(localDateTime,DatePattern.NORM_DATETIME_MS_PATTERN) + "'";
            }
        }
        return valStr;
    }
}

6.Mapper的重要方法

TableOperateMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.xiaotian.datagenius.mapper.tdengine.TableOperateMapper">

    <select id="operateSql" resultType="java.util.Map">
        ${sql}
    </select>
</mapper>

DeviceFlowRecordMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaotian.datagenius.mapper.tdengine.DeviceFlowRecordMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.xiaotian.datagenius.entity.DeviceFlowRecord">
        <id column="data_ts" property="dataTs" />
        <result column="deviceUnitCode" property="deviceUnitCode" />
        <result column="deviceUnitName" property="deviceUnitName" />
        <result column="deviceCode" property="deviceCode" />
        <result column="deviceName" property="deviceName" />
        <result column="deviceTypeName" property="deviceTypeName" />
        <result column="collectTime" property="collectTime" />
        <result column="createTime" property="createTime" />
        <result column="storageTime" property="storageTime" />
        <result column="projectId" property="projectId" />
        <result column="companyId" property="companyId" />
        <result column="data" property="data" />
        <result column="showMessage" property="showMessage" />
    </resultMap>

    <insert id="insert">
        ${sql}
    </insert>

    <select id="selectPageMap" resultType="java.util.Map">
        ${sql}
    </select>

    <select id="selectPageBy" parameterType="com.easylinkin.datagenius.vo.DeviceFlowRecordVo" resultType="java.util.Map">
        select r.*
        from device_flow_record r
        where 1 = 1
        <if test="param2 != null">
            <if test="param2.deviceCode != null and param2.deviceCode != ''">
                and r.`deviceCode` = #{param2.deviceCode}
            </if>
            <if test="param2.startTime != null">
                <![CDATA[ and r.`collectTime` >= #{param2.startTime} ]]>
            </if>
            <if test="param2.endTime != null">
                <![CDATA[ and r.`collectTime` <= #{param2.endTime} ]]>
            </if>
        </if>
        order by r.`data_ts` desc
    </select>
</mapper>

        核心点就以上这些位置了,大家自行体会。


总结

  • TDengine还不错,官方有交流群,群里也有技术支持,不过肯定不是每一个问题都有回复
  • 各方面都在支持它,它的优化空间还很多,我们用开源实际也是在帮忙测试。就从开年到现在我就整这玩意,刚开始是3.0.2.3现在都迭代到3.0.2.5了
  • ORM框架也在逐步支持,但是官方支持明确跟我说了可能ORM框架会拖慢,影响性能。
  • 里面坑还是很多的,我就踩了乱码、返回字节码的问题
            就写到这里,希望能帮到大家!!有需要帮助的可以在CSDN发消息我。

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

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

相关文章

Python没有指针怎么实现链表?

Python没有指针怎么实现链表&#xff1f; 学习数据结构的的链表和树时&#xff0c;会遇到节点&#xff08;node&#xff09;这个词&#xff0c;节点是处理数据结构的链表和树的基础。节点是一种数据元素&#xff0c;包括两个部分&#xff1a;一个是实际需要用到的数据&#xff…

考研复试机试 | C++ | 尽量不要用python,很多学校不支持

目录1.1打印日期 &#xff08;清华大学上机题&#xff09;题目&#xff1a;代码&#xff1a;1.2改一改&#xff1a;上一题反过来问题代码&#xff1a;2.Day of Week &#xff08;上交&&清华机试题&#xff09;题目&#xff1a;代码&#xff1a;3.剩下的树&#xff08;清…

sni第三种模式复现、幽灵猫网络抓包方式复现、所有漏洞复现

sni第三种模式复现 搭建环境 [rootlocalhost nginx]# mkdir certificate [rootlocalhost nginx]# cd certificate/[rootlocalhost certificate]# openssl genrsa -des3 -out ssl.key 4096 [rootlocalhost certificate]# openssl req -new -key ssl.key -out aaa.csr [rootlo…

Django框架之模型视图-URLconf

URLconf 浏览者通过在浏览器的地址栏中输入网址请求网站对于Django开发的网站&#xff0c;由哪一个视图进行处理请求&#xff0c;是由url匹配找到的 配置URLconf 1.settings.py中 指定url配置 ROOT_URLCONF 项目.urls2.项目中urls.py 匹配成功后&#xff0c;包含到应用的urls…

ChatGPT时代,别再折腾孩子了

今天这篇完全是从两件事儿有感而发。昨天在文印店&#xff0c;在复印机上看到装订好的几页纸&#xff0c;我瞥了一眼&#xff0c;是历史知识点&#xff1a;隋朝大运河分为四段&#xff0c;分别是___ ___ ___ ___&#xff0c;连接了五大河___ ___ ___ ___ ______ 年&#xff…

开源云真机平台——Sonic应用实践

前言 Sonic是一款开源、支持分布式部署、在线自动化测试的私有云真机平台。想着写一篇总结分享。 一、云真机平台 1.云真机平台对比 目前市面上常见的云真机平台有两种&#xff0c;一种是各大服务商如阿里、腾讯、百度推出的公共云真机平台&#xff0c;如&#xff1a;WeTes…

2月18日绿健简报,星期六,农历正月廿八

2月18日绿健简报&#xff0c;星期六&#xff0c;农历正月廿八坚持阅读&#xff0c;静待花开1. 中国证监会发布全面实行股票发行注册制相关制度规则&#xff0c;即日起施行。2. 返回地面75天后神舟十四号乘组航天员首次与媒体和公众正式见面。3. 银保监会查处5家金融机构违法违规…

C语言结构体复习总结

目录 一、结构体引入 1.1 为什么要用结构体&#xff1a; 1.2 定义一个结构体&#xff1a; 1.3 结构体在声明的时候不要定义变量&#xff1a; 1.4 根据业务需求定义不同的结构体&#xff1a; 二、定义结构体和使用变量 2.1 最好不要在声明结构体时定义变量&#xff1a; …

找工作必看,用Python爬取数据分析岗位信息并可视化分析

导读&#xff1a; 最近经常收到人事小姐姐和猎头小哥哥的面试邀请&#xff0c;想想最近也不是招聘旺季呀。但又想到许多小伙伴们有找工作这方面的需求&#xff0c;今天就来分享一篇简单的爬虫案例&#xff0c;旨在跟大家一起分析一下部分招聘市场。以"数据分析"为例。…

Linux之Xshell工具使用

shell简介Xshell是一个远程工具&#xff0c;可以远程连接linux系统 &#xff0c;SSH&#xff0c;远程管理 Xshell来远程访问Linux系统的终端 。shell的英文含义是“壳”&#xff1b;它是相对于内核来说的&#xff0c;因为它是建立在内核的基础上&#xff0c;面向于用户的一种表…

Eclipse下Maven的集成

Eclipse下Maven的集成 2.1指定本地maven环境 参考&#xff1a;Eclipse的Maven创建_叶书文的博客-CSDN博客_eclipse创建maven项目 指定用本地maven指定maven仓库设置和地址2.2创建maven项目 1.新建 2.目录设置 3.坐标设置&#xff08;随便写就行&#xff09; 4.目录结构 2.3配置…

事件驱动型架构

事件驱动型架构是一种软件设计模式&#xff0c;其中微服务会对状态变化&#xff08;称为“事件”&#xff09;作出反应。事件可以携带状态&#xff08;例如商品价格或收货地址&#xff09;&#xff0c;或者事件也可以是标识符&#xff08;例如&#xff0c;订单送达或发货通知&a…

【看表情包学Linux】进程地址空间 | 区域和页表 | 虚拟地址空间 | 初识写时拷贝

&#x1f923; 爆笑教程 &#x1f449; 《看表情包学Linux》&#x1f448; 猛戳订阅 &#x1f525; &#x1f4ad; 写在前面&#xff1a;本章核心主题为 "进程地址空间"&#xff0c;会通过验证 Linux 进程的地址空间来开头&#xff0c;抛出 "同一个值能有不同内…

【IOS逆向】dumpdecrypted砸壳

【IOS逆向】dumpdecrypted砸壳 前面简单尝试了下frida-trace,发现可以追踪对应pid动态运行时的各种函数&#xff0c;但是对于一个完整APP应用&#xff0c;我们如何得到关键的运行函数&#xff0c;这里就需要对IPA进行一个逆向拆解&#xff0c;找代码逻辑&#xff0c;然后结合f…

idea同时编辑多行-winmac都支持

1背景介绍 idea编辑器非常强大&#xff0c;其中一个功能非常优秀&#xff0c;很多程序员也非常喜欢用。这个功能能够大大大提高工作效率-------------多行代码同时编辑 2win 2.1方法1 按住alt鼠标左键上/下拖动即可 这样选中多行后&#xff0c;可以直接多行编辑。 优点&a…

C++ STL 学习之【string】

✨个人主页&#xff1a; Yohifo &#x1f389;所属专栏&#xff1a; C修行之路 &#x1f38a;每篇一句&#xff1a; 图片来源 The key is to keep company only with people who uplift you, whose presence calls forth your best. 关键是只与那些提升你的人在一起&#xff0c…

前端开发常用案例(二)

这里写目录标题1.loding加载动画2.全屏加载动画效果3.吃豆豆4.鼠标悬停3D翻转效果5.3D旋转木马效果6.flex弹性布局-酷狗音乐播放列表flex弹性布局-今日头条首页热门视频栏grid网格布局-360图片展示小米商城左侧二级菜单1.loding加载动画 代码如下&#xff1a; <!DOCTYPE h…

干货 | PCB电路板短路了!试试这六种检查方法

首先&#xff0c;了解一下常见的电路板短路的种类&#xff1a;短路按照功能性可分为&#xff1a;焊接短路&#xff08;如&#xff1a;连锡&#xff09;、PCB短路&#xff08;如&#xff1a;残铜、孔偏等&#xff09;、器件短路、组装短路、ESD/EOS击穿、电路板内层微短路、电化…

九龙证券|房企纷纷驶入代建赛道 抢占千亿新蓝海

跟着房地产职业进入深度调整期&#xff0c;代建形式日益受到房企青睐&#xff0c;不少房企纷繁入局或加快布局&#xff0c;成为了近期商场关注的焦点。 2月11日&#xff0c;上坤集团宣告将布局共建办理事务&#xff0c;这也意味着其正式入局代建商场。实际上&#xff0c;上一年…

谷歌seo快排技术怎么做?Google排名霸屏推广原理

本文主要分享关于谷歌快速排名的方法和所需要的条件。 本文由光算创作&#xff0c;有可能会被剽窃和修改&#xff0c;我们佛系对待这种行为吧。 首先提出一个问题&#xff1a;谷歌seo快排技术怎么做&#xff1f;如何达到谷歌霸屏的效果&#xff1f; 答案是&#xff1a;利用谷…