使用Python实现DLT645-2007智能电表协议

文章目录

      • 🌴通讯支持
      • 🌴 功能完成情况
      • 服务端架构设计
        • 一、核心模块划分
        • 二、数据层定义
        • 三、协议解析层
        • 四、通信业务层(以DLT645服务端为例)
        • 五、通信层(以TCP为例)
        • 使用例子

🌴通讯支持

功能状态
TCP客户端(方便通讯测试) 🐾
TCP服务端(方便通讯测试) 🐾
RTU主站 🐾
RTU从站 🐾

🌴 功能完成情况

功能状态
读、写通讯地址 🐾
广播校时 🐾
电能量 🐾
最大需量及发生时间 🐾
变量 🐾
事件记录 🐾
参变量 🐾
冻结量 🐾
负荷纪录 🐾

项目地址:https://gitee.com/chen-dongyu123/dlt645

服务端架构设计

一、核心模块划分
  • 数据层:模拟电表数据,存储数据标识与值的映射关系。
  • 协议解析层:负责帧的组装、拆卸、校验和转义。
  • 业务逻辑层:根据解析出的控制码和DI,执行相应操作并组织回复数据。
  • 通信层:负责底层的字节流收发(串口/TCP)。

代码结构

├── config      		# 测点json,用于初始化导入
├── src        			# 源文件夹
│   ├── common			# 存放通用函数
│   ├── model			# 数据模型
│   │   ├── data
│   │   │   └── define
│   │   └── types		# dlt645数据类型
│   ├── protocol		# 协议解析层
│   ├── service
│   │   ├── clientsvc	# dlt645客户端api及实现
│   │   └── serversvc	# dlt645服务端api及实现
│   └── transport
│       ├── client		# 客户端通讯接口,支持TCP客户端、RTU主站
│       └── server		# 服务端通讯接口,支持TCP服务端、RTU从站
└── test				# 测试文件

流程图:启动 -> 监听连接 -> 接收数据 -> 解析帧 -> 处理请求 -> 组织回复帧 -> 发送数据

二、数据层定义
  1. 通过读取config文件夹里的json导入测点,json示例如下

    [{"Di": "02010100","Name": "A相电压","Unit": "V","DataFormat": "XXX.X"},{"Di": "02010200","Name": "B相电压","Unit": "V","DataFormat": "XXX.X"},...
    ]
    
  2. 定义数据类型和映射map

    class DataItem:def __init__(self, di: int, name: str, data_format: str, value: float = 0, unit: str = '', timestamp: int = 0):self.di = diself.name = nameself.data_format = data_formatself.value = valueself.unit = unitself.timestamp = timestampdef __repr__(self):return (f"DataItem(name={self.name}, di={format(self.di, '#x')}, value={self.value}, "f"unit={self.unit},data_format={self.data_format}, timestamp={self.timestamp})")DIMap: Dict[int, DataItem] = {}
    
  3. 定义设置DataItem值接口和获取DataItem接口

    def get_data_item(di: int) -> Optional[DataItem]:"""根据 di 获取数据项"""item = DIMap.get(di)if item is None:log.info(f"未通过di {hex(di)} 找到映射")return Nonereturn itemdef set_data_item(di: int, data: Any) -> bool:"""设置指定 di 的数据项"""if data is None:log.info("data is nil")return Falseif di in DIMap:DIMap[di].value = datalog.info(f"设置数据项 {hex(di)} 成功, 值 {DIMap[di]}")return Truereturn False
    
  4. 初始化数据标识map

    def init_variable_def(VariableTypes: List[DataItem]):for date_type in VariableTypes:DIMap[date_type.di] = DataItem(di=date_type.di,name=date_type.name,data_format=date_type.data_format,unit=date_type.unit)
    
  5. 初始化DLT645相关测点数据

    EnergyTypes = []
    DemandTypes = []
    VariableTypes = []def init():global EnergyTypesEnergyTypes = initDataTypeFromJson(os.path.join(conf_path, 'energy_types.json'))DemandTypes = initDataTypeFromJson(os.path.join(conf_path, 'demand_types.json'))VariableTypes = initDataTypeFromJson(os.path.join(conf_path, 'variable_types.json'))init_energy_def(EnergyTypes)init_demand_def(DemandTypes)init_variable_def(VariableTypes)# 执行初始化
    init()
    
三、协议解析层
  1. 帧类型

    # 常量定义
    FRAME_START_BYTE = 0x68
    FRAME_END_BYTE = 0x16
    BROADCAST_ADDR = 0xAAclass Frame:def __init__(self, preamble: List[int] = None, start_flag: int = 0, addr: List[int] = None,ctrl_code: int = 0, data_len: int = 0, data: List[int] = None,check_sum: int = 0, end_flag: int = 0):self.preamble = preamble if preamble is not None else []  # 前导字节self.start_flag = start_flag  # 起始字节self.addr = addr if addr is not None else [0] * 6  # 地址字节self.ctrl_code = ctrl_code  # 控制字节self.data_len = data_len  # 数据长度字节self.data = data if data is not None else []  # 数据字节self.check_sum = check_sum  # 校验字节self.end_flag = end_flag  # 结束字节
    
  2. 协议解析

    • 数据域解码

          @classmethoddef decode_data(cls, data: bytes) -> bytes:"""数据域解码(±33H转换)"""return bytes([b - 0x33 for b in data])
      
    • 数据域编码

          @classmethoddef encode_data(cls, data: bytes) -> bytes:"""数据域编码"""return bytes([b + 0x33 for b in data])
      
    • 计算校验和

          @classmethoddef calculate_checksum(cls, data: bytes) -> int:"""校验和计算(模256求和)"""return sum(data) % 256
      
    • 构建帧

          @classmethoddef build_frame(cls, addr: bytes, ctrl_code: int, data: bytes) -> bytearray:"""帧构建(支持广播和单播)"""if len(addr) != 6:raise ValueError("地址长度必须为6字节")buf = []buf.append(FRAME_START_BYTE)buf.extend(addr)buf.append(FRAME_START_BYTE)buf.append(ctrl_code)# 数据域编码encoded_data = DLT645Protocol.encode_data(data)buf.append(len(encoded_data))buf.extend(encoded_data)# 计算校验和check_sum = DLT645Protocol.calculate_checksum(bytes(buf))buf.append(check_sum)buf.append(FRAME_END_BYTE)return bytearray(buf)
      
    • 字节数组反序列化Frame结构体

          @classmethoddef deserialize(cls, raw: bytes) -> Optional[Frame]:"""将字节切片反序列化为 Frame 结构体"""# 基础校验if len(raw) < 12:raise Exception(f"frame too short: {raw}")# 帧边界检查(需考虑前导FE)try:start_idx = raw.index(FRAME_START_BYTE)except ValueError:log.error(f"invalid start flag: {raw}")raise Exception("invalid start flag")if start_idx == -1 or start_idx + 10 >= len(raw):log.error(f"invalid start flag: {raw}")raise Exception("invalid start flag")if start_idx + 7 >= len(raw) or raw[start_idx + 7] != FRAME_START_BYTE:log.error(f"missing second start flag: {raw}")raise Exception("missing second start flag")# 构建帧结构frame = Frame()frame.start_flag = raw[start_idx]frame.addr = raw[start_idx + 1:start_idx + 7]frame.ctrl_code = raw[start_idx + 8]frame.data_len = raw[start_idx + 9]# 数据域提取(严格按协议1.2.5节处理)data_start = start_idx + 10data_end = data_start + frame.data_lenif data_end > len(raw) - 2:log.error(f"invalid data length {frame.data_len}")raise Exception(f"invalid data length {frame.data_len}")# 数据域解码(需处理加33H/减33H)frame.data = DLT645Protocol.decode_data(raw[data_start:data_end])# 校验和验证(从第一个68H到校验码前)checksum_start = start_idxchecksum_end = data_endif checksum_end >= len(raw):log.error(f"frame truncated: {raw}")raise Exception(f"frame truncated: {raw}")calculated_sum = DLT645Protocol.calculate_checksum(raw[checksum_start:checksum_end])if calculated_sum != raw[checksum_end]:log.error(f"checksum error: calc=0x{calculated_sum:02X}, actual=0x{raw[checksum_end]:02X}")raise Exception(f"checksum error: calc=0x{calculated_sum:02X}, actual=0x{raw[checksum_end]:02X}")# 结束符验证if checksum_end + 1 >= len(raw) or raw[checksum_end + 1] != FRAME_END_BYTE:log.error(f"invalid end flag: {raw[checksum_end + 1]}")raise Exception(f"invalid end flag: {raw[checksum_end + 1]}")# 转换为带缩进的JSONlog.info(f"frame: {frame}")return frame
      
    • Frame 结构体序列化为字节数组

          @classmethoddef serialize(cls, frame: Frame) -> Optional[bytes]:"""将 Frame 结构体序列化为字节切片"""if frame.start_flag != FRAME_START_BYTE or frame.end_flag != FRAME_END_BYTE:log.error(f"invalid start or end flag: {frame.start_flag} {frame.end_flag}")raise Exception(f"invalid start or end flag: {frame.start_flag} {frame.end_flag}")buf = []# 写入前导字节buf.extend(frame.preamble)# 写入起始符buf.append(frame.start_flag)# 写入地址buf.extend(frame.addr)# 写入第二个起始符buf.append(frame.start_flag)# 写入控制码buf.append(frame.ctrl_code)# 数据域编码encoded_data = DLT645Protocol.encode_data(frame.data)# 写入数据长度buf.append(len(encoded_data))# 写入编码后的数据buf.extend(encoded_data)# 计算并写入校验和check_sum = DLT645Protocol.calculate_checksum(bytearray(buf))buf.append(check_sum)# 写入结束符buf.append(frame.end_flag)return bytearray(buf)
      
    四、通信业务层(以DLT645服务端为例)
    1. 定义电表Service

      class MeterServerService:def __init__(self,server: Union[TcpServer, RtuServer],address: bytearray = bytearray([0x00] * 6),password: bytearray = bytearray([0x00] * 4)):self.server = server	self.address = addressself.password = password
      
    2. 设置值接口,以电能为例

          # 写电能量def set_00(self, di: int, value: float) -> bool:ok = set_data_item(di, value)if not ok:log.error(f"写电能量失败")return ok
      
    3. 处理数据函数

          # 处理读数据请求(协议与业务分离)def handle_request(self, frame):# 1. 验证设备if not self.validate_device(frame.addr):log.info(f"验证设备地址: {bytes_to_spaced_hex(frame.addr)} 失败")raise Exception("unauthorized device")# 2. 根据控制码判断请求类型if frame.ctrl_code == CtrlCode.BroadcastTimeSync:  # 广播校时log.info(f"广播校时: {frame.Data.hex(' ')}")self.set_time(frame.Data)return DLT645Protocol.build_frame(frame.addr, frame.ctrl_code | 0x80, frame.data)elif frame.ctrl_code == CtrlCode.CtrlReadData:# 解析数据标识di = frame.datadi3 = di[3]if di3 == 0x00:  # 读取电能# 构建响应帧res_data = bytearray(8)# 解析数据标识为 32 位无符号整数data_id = struct.unpack("<I", frame.data[:4])[0]data_item = data.get_data_item(data_id)if data_item is None:raise Exception("data item not found")res_data[:4] = frame.data[:4]  # 仅复制前 4 字节数据标识value = data_item.value# 转换为 BCD 码bcd_value = float_to_bcd(value, data_item.data_format, 'little')res_data[4:] = bcd_valuereturn DLT645Protocol.build_frame(frame.addr, frame.ctrl_code | 0x80, bytes(res_data))elif di3 == 0x01:  # 读取最大需量及发生时间res_data = bytearray(12)data_id = struct.unpack("<I", frame.data[:4])[0]data_item = data.get_data_item(data_id)if data_item is None:raise Exception("data item not found")res_data[:4] = frame.data[:4]  # 返回数据标识value = data_item.value# 转换为 BCD 码bcd_value = float_to_bcd(value, data_item.data_format, 'little')res_data[4:7] = bcd_value[:3]# 需量发生时间res_data[7:12] = time_to_bcd(time.time())log.info(f"读取最大需量及发生时间: {res_data}")return DLT645Protocol.build_frame(frame.addr, frame.ctrl_code | 0x80, bytes(res_data))elif di3 == 0x02:  # 读变量data_id = struct.unpack("<I", frame.data[:4])[0]data_item = data.get_data_item(data_id)if data_item is None:raise Exception("data item not found")# 变量数据长度data_len = 4data_len += (len(data_item.data_format) - 1) // 2  # (数据格式长度 - 1 位小数点)/2# 构建响应帧res_data = bytearray(data_len)res_data[:4] = frame.data[:4]  # 仅复制前 4 字节value = data_item.value# 转换为 BCD 码(小端序)bcd_value = float_to_bcd(value, data_item.data_format, 'little')res_data[4:data_len] = bcd_valuereturn DLT645Protocol.build_frame(frame.addr, frame.ctrl_code | 0x80, bytes(res_data))else:log.info(f"unknown: {hex(di3)}")return Exception("unknown di3")elif frame.ctrl_code == CtrlCode.ReadAddress:# 构建响应帧res_data = self.address[:6]return DLT645Protocol.build_frame(self.address, frame.ctrl_code | 0x80, res_data)elif frame.ctrl_code == CtrlCode.WriteAddress:res_data = b''  # 写通讯地址不需要返回数据# 解析数据addr = frame.data[:6]self.set_address(addr)  # 设置通讯地址return DLT645Protocol.build_frame(self.address, frame.ctrl_code | 0x80, res_data)else:log.info(f"unknown control code: {hex(frame.ctrl_code)}")raise Exception("unknown control code")
      
    4. 注入通讯协议到Service

      def new_tcp_server(ip: str, port: int, timeout: int) -> MeterServerService:# 1. 先创建 TcpServer(不依赖 Service)tcp_server = TcpServer(ip, port, timeout, None)# 2. 创建 MeterServerService,注入 TcpServer(作为 Server 接口)meter_service = MeterServerService(tcp_server)# 3. 将 MeterServerService 注入回 TcpServertcp_server.service = meter_servicereturn meter_servicedef new_rtu_server(port: str, dataBits: int, stopBits: int, baudRate: int, parity: str,timeout: float) -> MeterServerService:rtu_server = RtuServer(port, dataBits, stopBits, baudRate, parity, timeout)# 2. 创建 MeterServerService,注入 RtuServer(作为 Server 接口)meter_service = MeterServerService(rtu_server)# 3. 将 MeterServerService 注入回 RtuServerrtu_server.service = meter_servicereturn meter_service
      
    五、通信层(以TCP为例)
    1. 定义TCP服务端

      class TcpServer:def __init__(self, ip: str, port: int, timeout: float, service):self.ip = ipself.port = portself.timeout = timeoutself.ln = Noneself.service = service	# Dlt645业务
      
    2. 处理数据

          def handle_connection(self, conn):try:while True:try:# 接收数据buf = conn.recv(256)if not buf:breaklog.info(f"Received data: {bytes_to_spaced_hex(buf)}")# 协议解析try:frame = DLT645Protocol.deserialize(buf)except Exception as e:log.error(f"Error parsing frame: {e}")continue# 业务处理try:resp = self.service.handle_request(frame)except Exception as e:log.error(f"Error handling request: {e}")continue# 响应if resp:try:conn.sendall(resp)log.info(f"Sent response: {bytes_to_spaced_hex(resp)}")except Exception as e:log.error(f"Error writing response: {e}")except socket.timeout:breakexcept Exception as e:log.error(f"Error handling connection: {e}")finally:try:conn.close()except Exception as e:log.error(f"Error closing connection: {e}")
      
    3. 启动服务端

          def start(self):try:# 创建 TCP 套接字self.ln = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 设置地址可重用self.ln.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)# 绑定地址和端口self.ln.bind((self.ip, self.port))# 开始监听self.ln.listen(5)log.info(f"TCP server started on port {self.port}")while True:try:# 接受连接conn, addr = self.ln.accept()log.info(f"Accepted connection from {addr}")# 设置超时时间conn.settimeout(self.timeout)# 启动新线程处理连接import threadingthreading.Thread(target=self.handle_connection, args=(conn,)).start()except socket.error as e:if isinstance(e, socket.timeout):continuelog.error(f"Failed to accept connection: {e}")if not e.errno == 10038:  # 非套接字关闭错误return eexcept Exception as e:log.error(f"Failed to start TCP server: {e}")return e
      
    4. 关闭服务端

          def stop(self):if self.ln:log.info("Shutting down TCP server...")try:self.ln.close()except Exception as e:log.error(f"Error closing server: {e}")return ereturn None
      
    使用例子
    1. 启动服务端

      if __name__ == '__main__':server_svc = new_tcp_server("127.0.0.1", 8021, 3000)# server_svc = new_rtu_server("COM4", 8, 1, 9600, "N", 1000)server_svc.set_00(0x00000000, 100.0)server_svc.set_02(0x02010100, 86.0)server_svc.server.start()
      

      在这里插入图片描述

    2. 使用模拟DLT645客户端软件测试

      在这里插入图片描述

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

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

相关文章

未来已来:基于IPv6单栈隔离架构的安全互联实践报告

未来已来&#xff1a;基于IPv6单栈隔离架构的安全互联实践报告 报告摘要 随着IPv4地址资源彻底枯竭&#xff0c;全球网络基础设施正加速向IPv6单栈&#xff08;IPv6-Only&#xff09;演进。传统“IPv4为主、IPv6为辅”的双栈模式已无法满足数字化转型对海量地址、端到端连接与原…

Ubuntu24.04 安装 Zabbix

Ubuntu24.04 安装 Zabbix 环境&#xff1a; 软件版本Ubuntu24.04.3Nginx1.24.0MySQL8.4.6PHP8.3.6phpMyAdmin5.2.2Zabbix7.4.1 LNMP 1. 更新本地软件包索引并升级已安装软件 更新可用软件包列表 把已安装的软件升级到最新版 安装常用工具 sudo apt update && sud…

【动手学深度学习】6.2. 图像卷积

目录6.2. 图像卷积1&#xff09;互相关运算2&#xff09;卷积层3&#xff09;图像中目标的边缘检测4&#xff09;学习卷积核5&#xff09;互相关与卷积6&#xff09;特征映射和感受野7&#xff09;小结. 6.2. 图像卷积 卷积神经网络的设计是用于探索图像数据&#xff0c;本节…

游戏引擎中的Billboard技术

一.视觉公告板为解决场景中Mesh网格面数过多问题,使用2D平面Mesh替换为3D平面Mesh的技术即为Billboard技术.常用于场景中植被,树叶,粒子系统等对面数有要求的场景.二.Billboard着色器实现着色器输入参数:摄像机坐标,网格坐标,摄像机观察方向着色器输出:实际2D平面随视角不变

vue-admin-template权限管理

在基于 vue-admin-template 实现权限管理时&#xff0c;通常需要结合角色权限模型和动态路由机制&#xff0c;以满足不同用户角色对页面访问权限的控制需求。分为路由页面权限和按钮权限&#xff1a;下面是具体实现思路的思维导图和具体代码流程&#xff1a;0.实现逻辑思维导图…

微信小程序,事件总线(Event Bus) 实现

1、util.js文件/*** 事件总线*/ function createEventBus() {// 私有事件存储对象&#xff0c;通过闭包保持私有性const events {};return {/*** 监听事件&#xff0c;只执行一次* param {string} eventName - 事件名称* param {Function} callback - 回调函数*/once(eventNam…

OpenCV结构光三维重建类cv::structured_light::GrayCodePattern

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::structured_light::GrayCodePattern 是 OpenCV 库中用于结构光三维重建 的一个类&#xff0c;属于 OpenCV 的 structured_light 模块。 它用于…

变频器实习DAY35 引脚电平测试 退耦电阻

目录变频器实习DAY35一、工作内容1.1 硬性平台RO7测试二、学习内容2.1 退耦电阻核心原理&#xff1a;2大特性抑制干扰四大关键作用选型&#xff1a;4个核心参数典型应用场景四大常见误区附学习参考网址欢迎大家有问题评论交流 (* ^ ω ^)变频器实习DAY35 一、工作内容 1.1 硬性…

C++标准库算法:从零基础到精通

算法库的核心理念与设计哲学 C标准库算法的设计遵循着一个令人称道的哲学&#xff1a;算法与容器的分离。这种设计并非偶然&#xff0c;而是经过深思熟虑的结果。传统的面向对象设计可能会将排序功能绑定到特定的容器类中&#xff0c;但C标准库却选择了一条更加优雅的道路——…

为什么存入数据库的中文会变成乱码

从产生、传输、处理到最终存储的整个生命周期中采用统一且正确的字符集编码。具体原因纷繁复杂&#xff0c;主要归结为&#xff1a;客户端操作系统或应用与数据库服务端字符集编码不一致、Web应用服务器到数据库驱动的连接层编码配置缺失或错误、数据库本身及其表、字段各层级的…

13种常见机器学习算法面试总结(含问题与优质回答)

目录 1. K近邻&#xff08;K-NN&#xff09; 2. 线性回归&#xff08;一元/多元&#xff09; 3. 逻辑回归 4. 决策树 5. 集成学习之随机森林 6. 贝叶斯&#xff08;朴素/高斯&#xff09; 7. SVM&#xff08;支持向量机&#xff09; 8. K-means聚类 9. DBSCAN 10. TF-…

sfc_os!SfcValidateFileSignature函数分析之WINTRUST!SoftpubLoadMessage

第一部分&#xff1a;0: kd> kc# 00 WINTRUST!SoftpubLoadMessage 01 WINTRUST!_VerifyTrust 02 WINTRUST!WinVerifyTrust 03 sfc_os!SfcValidateFileSignature 04 sfc_os!SfcGetValidationData 05 sfc_os!SfcValidateDLL 06 sfc_os!SfcQueueValidationThread 07 kernel32!B…

python写上位机并打包250824

1.python写的串口上位机软件程序 import serial import serial.tools.list_ports import tkinter as tk from tkinter import ttk, scrolledtext, messagebox, filedialog import threading import time from datetime import datetime class SerialPortAssistant: def init(se…

Wagtail CRX 简介

Wagtail CRX&#xff08;前身为 CodeRed CMS&#xff0c;由 CodeRed Corp 开发&#xff09;是一个基于 Wagtail 的 CMS 扩展包&#xff0c;主要用于快速构建营销型网站&#xff0c;提供预置组件和增强功能。最新版本为 5.0.1&#xff08;发布于 2025 年 5 月 9 日&#xff09;。…

docker compose 安装zabbix 7

docker compose 安装zabbix 7 1.环境 # hostnamectlStatic hostname: ky10Icon name: computer-vmChassis: vmMachine ID: f554764e21b74c2fa057d9aaa296af63Boot ID: 4c155f0185c24a14970ab5ea60de34f4Virtualization: vmwareOperating System: Kylin Linux Advanced Server…

EtherCAT的几种邮箱通信介绍

1. COE&#xff08;CANopen over EtherCAT&#xff09;技术特点&#xff1a;直接复用 CANopen 的对象字典&#xff08;Object Dictionary&#xff09;机制&#xff0c;通过 EtherCAT 的邮箱通信实现非周期性数据交换&#xff0c;同时支持过程数据对象&#xff08;PDO&#xff0…

【Java】springboot的自动配置

如果你用过 Spring Boot&#xff0c;一定对 “引入依赖就能用” 的体验印象深刻 —— 加个spring-boot-starter-web就有了 Web 环境&#xff0c;这个是 SpringBoot 的自动装配&#xff08;Auto-Configuration&#xff09;机制。自动装配的核心注解自动装配的逻辑看似复杂&#…

高通机型QPST平台线刷教程 线刷全分区 只通过引导文件提取单分区 写入单分区

高通芯片机型刷机平台很多&#xff0c;除过一些厂家专用的平台外。qpst是高通芯片类通用刷写平台。其操作简单 可以刷写完整固件。也可以通过单个引导文件来读取 提取整个分区。而且包含读写基带qcn等等的一些功能。 qpst工具下载 QPST 的不同版本可在多个开源平台或技术论坛中…

ES_预处理

1. 预处理的核心概念&#xff1a;什么是 Ingest Pipeline&#xff1f; 想象一下数据进入 Elasticsearch 的旅程。原始数据&#xff08;Raw Data&#xff09;往往并不完美&#xff1a;格式可能混乱&#xff0c;字段可能缺失&#xff0c;或者需要被丰富和转换后才能发挥最大的价值…

我从零开始学习C语言(15)- 基本类型 PART2

开始学习第七章其余部分。7.3.4 转义序列正如在前面示例中见到的那样&#xff0c;字符常量通常是用单引号括起来的单个字符。然而&#xff0c;一些特殊符号&#xff08;比如换行符&#xff09;是无法采用上述方式书写的&#xff0c;因为它们不可见&#xff08;非打印字符&#…