有人知道 trae 这是要干什么吗? ai-agent 进程占用内存 18GB
看 ai-chat 这个目录感觉是个关键信息

xiaohack博客专注前沿科技动态与实用技术干货分享,涵盖 AI 代理、大模型应用、编程工具、文档解析、SEO 实战、自动化部署等内容,提供开源项目教程、科技资讯日报、工具使用指南,助力开发者、AI 爱好者获取前沿技术与实战经验。
看 ai-chat 这个目录感觉是个关键信息

大家好,我是良许 在嵌入式开发中,存储器的选择往往决定了产品的成本和性能。 作为一名从事嵌入式开发多年的程序员,我在项目中经常需要为设备选择合适的存储方案。 SD 卡和 TF 卡作为两种最常见的可移动存储介质,它们的应用场景各有千秋。 今天就来和大家聊聊这两种存储卡在实际项目中的应用,以及我在开发过程中积累的一些经验。 SD 卡(Secure Digital Card)是一种基于半导体闪存的存储卡,由松下、东芝和 SanDisk 公司于 1999 年联合开发。 SD 卡的标准尺寸为 32mm×24mm×2.1mm,相对来说体积较大,但接口稳定性好,适合需要频繁插拔的应用场景。 在我早期做汽车电子项目时,车载导航系统普遍使用 SD 卡来存储地图数据。 这是因为 SD 卡的物理尺寸较大,插拔时不容易损坏,而且当时 SD 卡的容量已经能够满足地图存储的需求。 SD 卡支持 SPI 和 SDIO 两种通信协议,其中 SDIO 协议的传输速度更快,可以达到 104MB/s 甚至更高。 TF 卡(TransFlash Card),后来被 SD 协会收编后改名为 microSD 卡,是一种超小型的存储卡。 它的尺寸仅为 15mm×11mm×1mm,是目前最小的存储卡格式之一。 TF 卡虽然体积小,但功能和 SD 卡完全相同,只是物理尺寸不同而已。 在我目前的项目中,几乎所有的便携式设备都采用 TF 卡作为存储方案。 比如我们为客户开发的一款工业相机,就使用了 TF 卡来存储拍摄的图像数据。 TF 卡的小巧体积使得设备可以做得更加紧凑,这在空间受限的嵌入式系统中是非常重要的优势。 从技术角度来看,SD 卡和 TF 卡在电气特性和通信协议上基本相同,主要区别在于物理尺寸。 SD 卡更大更厚,接触面积大,插拔时的机械强度更好。 TF 卡则更小更薄,适合空间受限的应用。 在实际开发中,我们可以通过转接卡将 TF 卡转换为 SD 卡使用,但反过来就不行了。 另外一个重要区别是成本。 由于 TF 卡的生产工艺更复杂,相同容量和速度等级的 TF 卡通常比 SD 卡贵一些。 但在批量采购时,这个价格差异会缩小。 我在给客户做成本分析时,通常会综合考虑存储卡本身的价格、卡座的价格以及 PCB 板的空间成本。 在嵌入式系统中,SD 卡和 TF 卡最基本的应用就是数据存储。 我在做过的项目中,有很多设备需要记录运行日志、传感器数据或者用户配置信息。 比如我们开发的一款环境监测设备,需要每隔 10 秒钟记录一次温度、湿度、PM2.5 等数据,一天下来就会产生大量的数据。 使用 TF 卡存储这些数据,不仅成本低廉,而且可以方便地将数据导出到电脑进行分析。 在 STM32 平台上实现 SD 卡的基本读写操作,使用 HAL 库可以这样做: 这段代码展示了如何在 STM32 上初始化 SD 卡并进行基本的读写操作。 在实际项目中,我通常会在此基础上集成 FatFS 文件系统,这样就可以像操作普通文件一样操作 SD 卡了。 在我做过的很多项目中,SD 卡和 TF 卡被用作固件升级的介质。 这种方式特别适合那些部署在野外或者难以通过网络升级的设备。 用户只需要将新的固件文件拷贝到存储卡中,插入设备,设备启动时会自动检测并完成升级。 我在一个工业控制器项目中实现过这样的功能。 设备启动时,Bootloader 会检查 SD 卡中是否存在特定名称的固件文件。 如果存在,就会读取这个文件并烧写到 Flash 中,然后跳转到新的应用程序。 这个过程的关键代码如下: 这个固件升级方案在我们的产品中运行得非常稳定。 客户反馈说这种升级方式比通过串口或者网络升级要可靠得多,特别是在网络环境不好的工业现场。 在音视频相关的嵌入式项目中,SD 卡和 TF 卡的应用更是不可或缺。 我参与过一个行车记录仪项目,使用 TF 卡来存储录制的视频。 这类应用对存储卡的写入速度要求很高,因为视频数据是连续产生的,如果写入速度跟不上,就会导致丢帧。 在选择存储卡时,我们需要特别注意卡的速度等级。 SD 协会定义了多种速度等级标准,包括 Class 2/4/6/10,UHS-I/II/III 等。 对于 1080P 视频录制,至少需要 Class 10 或者 UHS-I U1 等级的卡。 在我们的项目中,为了保证 4K 视频的流畅录制,我们要求客户使用 UHS-I U3 或更高等级的 TF 卡。 在代码实现上,我们使用了 DMA 来提高数据传输效率: 使用 DMA 可以大大减轻 CPU 的负担,让 CPU 有更多时间处理视频编码等计算密集型任务。 在我们的行车记录仪项目中,使用 DMA 后 CPU 占用率从 85% 降低到了 60% 左右。 在工业数据采集系统中,SD 卡和 TF 卡也扮演着重要角色。 我曾经为一家制造企业开发过一套生产线监控系统,需要实时采集多个传感器的数据并存储到 TF 卡中。 这个项目的挑战在于数据量大、采样频率高,而且需要保证数据不丢失。 为了解决这个问题,我采用了双缓冲机制。 系统使用两块内存缓冲区,一块用于接收传感器数据,另一块用于写入 TF 卡。 当一块缓冲区写满后,立即切换到另一块,同时将写满的缓冲区数据写入 TF 卡。 这样可以保证数据采集的连续性。 这个双缓冲方案在我们的项目中表现很好,即使在采样频率达到 10kHz 的情况下,也能保证数据不丢失。 客户对这个方案非常满意,后来又追加了几套同样的系统。 在选择存储卡容量时,我通常会根据应用的具体需求来决定。 对于日志记录类应用,一般 4GB 到 16GB 就足够了。 但对于视频录制或者大量图像存储的应用,可能需要 32GB 甚至更大的容量。 需要注意的是,并不是容量越大越好。 在我的经验中,容量过大的存储卡在格式化和文件系统维护时会花费更多时间。 而且,如果使用 FAT32 文件系统,单个文件的大小限制是 4GB,这在长时间视频录制时需要特别注意。 对于需要存储超过 4GB 单个文件的应用,建议使用 exFAT 文件系统。 速度等级的选择直接影响系统的性能。 在我做过的项目中,如果只是存储日志或者配置文件,Class 4 的卡就够用了。 但如果是视频录制或者高速数据采集,就必须选择 Class 10 或者 UHS 等级的卡。 这里有一个实际的例子。 我们在开发一款工业相机时,最初使用的是 Class 10 的 TF 卡。 在测试中发现,当连续拍摄高分辨率图片时,偶尔会出现保存失败的情况。 后来更换为 UHS-I U3 等级的卡后,问题就完全解决了。 所以在选型时,一定要根据实际的数据传输速率来选择合适的速度等级,并且留有一定的余量。 在嵌入式产品中,存储卡的可靠性至关重要。 我在项目中一般会选择 SanDisk、Samsung、Kingston 等知名品牌的产品。 虽然价格可能贵一些,但质量和售后服务有保障。 我曾经遇到过一个教训。 在一个项目中,为了降低成本,客户坚持使用某个不知名品牌的 TF 卡。 结果产品上市后,陆续收到用户反馈说数据丢失。 后来排查发现,是 TF 卡的质量问题导致的。 最终不得不召回产品更换存储卡,损失远远超过了当初节省的成本。 所以我现在在给客户做方案时,都会强调存储卡质量的重要性。 对于工业应用,我强烈建议使用工业级的存储卡。 工业级存储卡在温度范围、抗震性、使用寿命等方面都比消费级产品要好得多。 虽然价格可能是消费级产品的 2 到 3 倍,但在恶劣环境下的可靠性是值得的。 在我参与的一个户外监控项目中,设备需要在-40°C 到 85°C 的温度范围内工作。 我们使用的是 SanDisk 的工业级 TF 卡,经过两年的实际运行,故障率几乎为零。 而同期使用消费级 TF 卡的竞品,在极端温度下频繁出现问题。 在嵌入式系统中使用 SD 卡或 TF 卡,通常需要配合文件系统使用。 最常用的是 FAT32 和 exFAT。 FAT32 兼容性好,几乎所有设备都支持,但有单个文件 4GB 的限制。 exFAT 没有这个限制,但不是所有设备都支持。 在我的项目中,如果不需要存储超过 4GB 的单个文件,我都会选择 FAT32。 因为 FAT32 的实现更简单,占用的资源更少。 在 STM32 这样的 MCU 上,使用 FatFS 库可以很方便地实现 FAT32 文件系统: 在嵌入式系统中,突然断电是常见的情况。 如果在写入 SD 卡时突然断电,可能会导致数据损坏甚至整个文件系统损坏。 为了避免这个问题,我在项目中通常会采取以下措施: 首先,尽量减少文件的打开关闭次数。 频繁打开关闭文件会增加文件系统损坏的风险。 其次,在关键数据写入后,调用 f\_sync()函数强制将缓冲区数据写入存储卡。 最后,可以考虑实现一个简单的日志系统,记录每次写入操作,这样即使发生数据损坏,也可以通过日志恢复。 在某些应用中,用户可能需要在设备运行时插拔存储卡。 这就需要我们的程序能够检测存储卡的插入和移除,并做出相应的处理。 大多数 SD 卡座都有一个检测引脚,可以用来检测卡的插入状态。 在我的项目中,我通常会使用 GPIO 中断来检测存储卡的插拔: 在实际应用中,SD 卡的读写性能可能会成为系统的瓶颈。 我在项目中总结了一些优化技巧: 第一,使用块读写而不是字节读写。 SD 卡的最小读写单位是 512 字节的块,使用块读写可以大大提高效率。 第二,尽量使用连续的存储空间。 碎片化的文件会降低读写速度。 在我的一个项目中,定期整理存储卡可以将写入速度提升 30% 左右。 第三,合理设置 FatFS 的扇区大小和缓冲区大小。 在 ffconf.h 配置文件中,可以调整这些参数来优化性能: 第四,对于大文件的写入,可以考虑使用 f\_expand()函数预分配空间,这样可以减少文件系统的开销: SD 卡和 TF 卡在嵌入式系统中的应用非常广泛,从简单的数据存储到复杂的多媒体应用,它们都能胜任。 在我多年的嵌入式开发经验中,选择合适的存储方案、正确地使用存储卡、做好数据保护和性能优化,是保证产品稳定运行的关键。 对于初学者来说,建议先从简单的文件读写开始,逐步掌握文件系统的使用。 对于有经验的开发者,则需要更多地关注可靠性和性能优化。 无论是哪个阶段,都要记住一点:存储卡虽然看起来简单,但在实际应用中有很多细节需要注意。 只有充分理解和掌握这些细节,才能开发出稳定可靠的产品。 希望这篇文章能够帮助大家更好地理解和应用 SD 卡和 TF 卡。 如果你在项目中遇到相关问题,欢迎交流讨论。 作为一名深耕嵌入式领域的程序员,我深知技术交流的重要性,也愿意将自己的经验分享给更多的同行。 更多编程学习资源1. SD 卡和 TF 卡的基本概念
1.1 什么是 SD 卡
1.2 什么是 TF 卡
1.3 两者的主要区别
2. SD 卡和 TF 卡在嵌入式系统中的应用
2.1 数据存储应用
#include "stm32f4xx_hal.h"
#include "fatfs.h"
// SD卡句柄
SD_HandleTypeDef hsd;
// 初始化SD卡
HAL_StatusTypeDef SD_Init(void)
{
hsd.Instance = SDIO;
hsd.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING;
hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE;
hsd.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
hsd.Init.BusWide = SDIO_BUS_WIDE_1B;
hsd.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
hsd.Init.ClockDiv = 0;
if (HAL_SD_Init(&hsd) != HAL_OK)
{
return HAL_ERROR;
}
// 配置为4位总线宽度以提高速度
if (HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_4B) != HAL_OK)
{
return HAL_ERROR;
}
return HAL_OK;
}
// 写入数据到SD卡
HAL_StatusTypeDef SD_WriteData(uint32_t blockAddr, uint8_t *pData, uint32_t numBlocks)
{
HAL_StatusTypeDef status;
status = HAL_SD_WriteBlocks(&hsd, pData, blockAddr, numBlocks, HAL_MAX_DELAY);
if (status == HAL_OK)
{
// 等待写入完成
while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER)
{
}
}
return status;
}
// 从SD卡读取数据
HAL_StatusTypeDef SD_ReadData(uint32_t blockAddr, uint8_t *pData, uint32_t numBlocks)
{
HAL_StatusTypeDef status;
status = HAL_SD_ReadBlocks(&hsd, pData, blockAddr, numBlocks, HAL_MAX_DELAY);
if (status == HAL_OK)
{
// 等待读取完成
while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER)
{
}
}
return status;
}2.2 固件升级应用
#include "stm32f4xx_hal.h"
#include "fatfs.h"
#define FIRMWARE_FILE_NAME "firmware.bin"
#define APP_START_ADDRESS 0x08010000 // 应用程序起始地址
// 从SD卡升级固件
HAL_StatusTypeDef UpdateFirmwareFromSD(void)
{
FIL file;
FRESULT fres;
UINT bytesRead;
uint8_t buffer[1024];
uint32_t flashAddress = APP_START_ADDRESS;
// 打开固件文件
fres = f_open(&file, FIRMWARE_FILE_NAME, FA_READ);
if (fres != FR_OK)
{
return HAL_ERROR; // 文件不存在或打开失败
}
// 解锁Flash
HAL_FLASH_Unlock();
// 擦除应用程序区域
FLASH_EraseInitTypeDef eraseInit;
uint32_t sectorError;
eraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
eraseInit.Sector = FLASH_SECTOR_4; // 根据实际情况调整
eraseInit.NbSectors = 4; // 擦除4个扇区
eraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3;
if (HAL_FLASHEx_Erase(&eraseInit, §orError) != HAL_OK)
{
HAL_FLASH_Lock();
f_close(&file);
return HAL_ERROR;
}
// 读取文件并写入Flash
while (1)
{
fres = f_read(&file, buffer, sizeof(buffer), &bytesRead);
if (fres != FR_OK || bytesRead == 0)
{
break;
}
// 将数据写入Flash
for (uint32_t i = 0; i < bytesRead; i += 4)
{
uint32_t data = *(uint32_t *)(buffer + i);
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, flashAddress, data) != HAL_OK)
{
HAL_FLASH_Lock();
f_close(&file);
return HAL_ERROR;
}
flashAddress += 4;
}
}
// 锁定Flash
HAL_FLASH_Lock();
f_close(&file);
// 删除固件文件,避免重复升级
f_unlink(FIRMWARE_FILE_NAME);
return HAL_OK;
}2.3 多媒体应用
// 使用DMA写入视频数据
HAL_StatusTypeDef SD_WriteDMA(uint32_t blockAddr, uint8_t *pData, uint32_t numBlocks)
{
HAL_StatusTypeDef status;
// 启动DMA传输
status = HAL_SD_WriteBlocks_DMA(&hsd, pData, blockAddr, numBlocks);
return status;
}
// DMA传输完成回调函数
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)
{
// 设置标志位,通知应用层写入完成
sdWriteComplete = 1;
}
// 在主循环中处理视频数据
void ProcessVideoData(void)
{
static uint8_t videoBuffer[VIDEO_BUFFER_SIZE];
static uint32_t currentBlock = 0;
// 从摄像头获取视频数据
if (GetVideoFrame(videoBuffer, VIDEO_BUFFER_SIZE) == HAL_OK)
{
// 等待上一次写入完成
while (sdWriteComplete == 0)
{
// 可以在这里处理其他任务
}
sdWriteComplete = 0;
// 启动DMA写入
SD_WriteDMA(currentBlock, videoBuffer, VIDEO_BUFFER_SIZE / 512);
currentBlock += VIDEO_BUFFER_SIZE / 512;
}
}2.4 数据采集应用
#define BUFFER_SIZE 4096
// 双缓冲区
uint8_t dataBuffer1[BUFFER_SIZE];
uint8_t dataBuffer2[BUFFER_SIZE];
uint8_t *currentBuffer;
uint8_t *writeBuffer;
uint32_t bufferIndex = 0;
// 初始化双缓冲
void InitDoubleBuffer(void)
{
currentBuffer = dataBuffer1;
writeBuffer = dataBuffer2;
bufferIndex = 0;
}
// 数据采集回调函数(在定时器中断中调用)
void DataAcquisitionCallback(void)
{
uint16_t sensorData;
// 读取传感器数据
sensorData = ReadSensorData();
// 将数据存入当前缓冲区
currentBuffer[bufferIndex++] = (sensorData >> 8) & 0xFF;
currentBuffer[bufferIndex++] = sensorData & 0xFF;
// 如果缓冲区满了,切换缓冲区
if (bufferIndex >= BUFFER_SIZE)
{
// 交换缓冲区指针
uint8_t *temp = currentBuffer;
currentBuffer = writeBuffer;
writeBuffer = temp;
bufferIndex = 0;
// 设置标志,通知主循环写入SD卡
bufferReadyFlag = 1;
}
}
// 在主循环中写入SD卡
void MainLoop(void)
{
static uint32_t fileBlock = 0;
while (1)
{
if (bufferReadyFlag)
{
bufferReadyFlag = 0;
// 将数据写入SD卡
SD_WriteData(fileBlock, writeBuffer, BUFFER_SIZE / 512);
fileBlock += BUFFER_SIZE / 512;
}
// 处理其他任务
}
}3. SD 卡和 TF 卡的选型建议
3.1 容量选择
3.2 速度等级选择
3.3 品牌和可靠性
3.4 工业级 vs 消费级
4. 使用中的注意事项
4.1 文件系统的选择
#include "ff.h"
FATFS fs; // 文件系统对象
FIL file; // 文件对象
FRESULT fres; // 操作结果
// 挂载文件系统
void MountFileSystem(void)
{
fres = f_mount(&fs, "0:", 1);
if (fres != FR_OK)
{
// 挂载失败,可能需要格式化
printf("Mount failed, error code: %d\n", fres);
}
}
// 写入日志文件
void WriteLog(const char *logMessage)
{
UINT bytesWritten;
// 打开文件,如果不存在则创建
fres = f_open(&file, "log.txt", FA_OPEN_APPEND | FA_WRITE);
if (fres == FR_OK)
{
// 写入时间戳
char timestamp[32];
sprintf(timestamp, "[%lu] ", HAL_GetTick());
f_write(&file, timestamp, strlen(timestamp), &bytesWritten);
// 写入日志内容
f_write(&file, logMessage, strlen(logMessage), &bytesWritten);
f_write(&file, "\n", 1, &bytesWritten);
// 关闭文件
f_close(&file);
}
}4.2 数据完整性保护
// 安全写入数据
HAL_StatusTypeDef SafeWriteData(const char *filename, uint8_t *data, uint32_t size)
{
FIL file;
FRESULT fres;
UINT bytesWritten;
// 打开文件
fres = f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE);
if (fres != FR_OK)
{
return HAL_ERROR;
}
// 写入数据
fres = f_write(&file, data, size, &bytesWritten);
if (fres != FR_OK || bytesWritten != size)
{
f_close(&file);
return HAL_ERROR;
}
// 强制同步,确保数据写入存储卡
fres = f_sync(&file);
if (fres != FR_OK)
{
f_close(&file);
return HAL_ERROR;
}
// 关闭文件
f_close(&file);
return HAL_OK;
}4.3 热插拔处理
// 卡检测引脚的GPIO中断回调
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == SD_DETECT_PIN)
{
// 延时去抖动
HAL_Delay(50);
if (HAL_GPIO_ReadPin(SD_DETECT_PORT, SD_DETECT_PIN) == GPIO_PIN_RESET)
{
// 卡插入
cardInserted = 1;
// 重新挂载文件系统
f_mount(&fs, "0:", 1);
}
else
{
// 卡移除
cardInserted = 0;
// 卸载文件系统
f_mount(NULL, "0:", 0);
}
}
}
// 在写入前检查卡是否存在
HAL_StatusTypeDef WriteDataToCard(uint8_t *data, uint32_t size)
{
if (!cardInserted)
{
return HAL_ERROR; // 卡未插入
}
// 执行写入操作
return SafeWriteData("data.bin", data, size);
}4.4 性能优化
// ffconf.h 中的关键配置
#define FF_MAX_SS 4096 // 最大扇区大小,增大可以提高性能
#define FF_MIN_SS 512 // 最小扇区大小
#define FF_USE_LFN 2 // 长文件名支持
#define FF_FS_LOCK 4 // 文件锁定功能// 预分配文件空间
FRESULT PreAllocateFile(const char *filename, FSIZE_t size)
{
FIL file;
FRESULT fres;
// 创建文件
fres = f_open(&file, filename, FA_CREATE_ALWAYS | FA_WRITE);
if (fres != FR_OK)
{
return fres;
}
// 预分配空间
fres = f_expand(&file, size, 1);
if (fres != FR_OK)
{
f_close(&file);
return fres;
}
f_close(&file);
return FR_OK;
}5. 总结
(可选)调整HCL命令行中的字体显示 打开本地cmd终端,验证安装。本示例版本为Python 3.14。 安装网络自动化需要的Python库 相关的库有很多,不过初期学习只安装一些基础的库即可,如SSH相关库paramiko,netmiko(基于paramiko);其他的可以选择一并安装或后期安装,如textfsm,ntc-templates,pyyaml等。 验证安装 在python中输入 SSH登录验证 使用windows命令行工具测试。 输入 输入 也可使用其他终端工具如SecureCRT,MobaXterm等,进行SSH连接测试。 SSH相关配置可参考H3C官方文档。H3C_SSH典型配置举例 文章已同步发布于博客园-Haosend 一、安装配置华三模拟器HCL




sys
interface GigabitEthernet 0/0
ip add 10.2.1.2 24
quit
save
HCL默认使用Putty终端,字体使用Courier New,视觉上个人觉得不是很友好,可以调整为Consolas或其他喜欢的字体。也可以在设置内调用其他终端工具,如SecureCRT,MobaXterm等。
默认字体如下。
调整后字体。
调整方法。



二、安装Python及相关库(windows 11环境示例)

PS C:\Users\coolh>python
Python 3.14.0 (tags/v3.14.0:ebf955d, Oct 7 2025, 10:15:03) [MSC v.1944 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>pip install xxxx #不指定版本则下载最新版本
#指定清华源下载,因为默认国外源下载较慢
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple netmiko
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple paramikoimport paramiko,不报错则说明安装成功。
三、配置路由器,交换机设备的SSH服务
#开启SSH和netconf服务。netconf后续练习会用到,也可以先不打开
sys
ssh server enable
netconf ssh server enable
#配置本地用户hao和密码,接入类型为ssh,权限为network-admin
local-user hao class manage
password simple admin12345
service-type ssh
authorization-attribute user-role network-admin
dis local-user
quit
#配置终端接入,认证类型为scheme
line vty 0 4
authentication-mode scheme
user-role network-admin
quit
savessh hao@10.2.1.2,提示设备使用了不安全的RSA算法,默认禁止使用。ssh -o HostKeyAlgorithms=+ssh-rsa hao@10.2.1.2,连接成功。
写在前面,本人目前处于求职中,如有合适内推岗位,请加:lpshiyue 感谢。 在深入探讨Kafka生态的数据入湖链路后,我们面临一个关键挑战:如何实时处理这些持续不断的数据流?Flink作为第三代流处理引擎的代表,通过其独特的流式优先架构和精确一次语义,为企业提供了处理无界数据流的能力。本文将深入解析Flink的五大核心概念——流、窗口、水位线、状态与Checkpoint的协同工作机制,帮助构建完整的实时计算心智模型。 传统大数据处理框架将流处理视为批处理的特殊形式,而Flink实现了根本性的范式转变——“批是流的特例”。这一设计哲学使Flink能够以统一的方式处理有界和无界数据集,在架构层面实现了真正的流批一体。 认知范式的对比: 根据2025年流处理市场分析,采用原生流处理架构的系统在实时性要求高的场景中,性能比微批处理提升5-10倍,特别是在欺诈检测、实时风控等低延迟场景中表现突出。 Flink凭借其原生流处理能力,在2025年已占据流处理市场40%的份额,年复合增长率超过18%。其核心优势在于: 这些特性使Flink在金融风控、实时推荐、物联网数据分析等场景中成为首选方案,某头部电商通过Flink将实时推荐响应时间从秒级优化到毫秒级,推荐点击率提升25%。 Flink将所有数据视为流,实现了处理范式的高度统一: Flink通过统一的API处理无界流和有界流 Flink的数据流模型建立在几个核心概念上: Source:数据输入端,支持Kafka、文件系统、Socket等多种数据源 执行模式对比: 根据数据特性选择合适的执行模式 这种统一性大幅降低了开发复杂度,同一套代码可同时用于实时数据处理和历史数据回溯。 时间是流处理中最核心且易误解的概念。Flink明确定义了三种时间语义: 事件时间的重要性:在分布式系统中,数据产生时间与处理时间存在差异,只有基于事件时间才能保证计算结果的准确性。某金融公司通过将处理时间切换到事件时间,成功将对账误差从5%降至0.1%以下。 水位线是Flink处理乱序数据的创新机制,它本质上是一个时间戳,表示“该时间之前的数据应该已经全部到达”。 水位线生成策略: 水位线生成策略选择 水位线传播机制: 水位线机制使Flink能够平衡延迟和准确性,通过合理设置最大乱序时间,在保证结果准确的同时控制处理延迟。 窗口是将无界流划分为有界数据块的核心抽象,Flink提供丰富的窗口类型满足不同需求: 滚动窗口:窗口间不重叠,固定大小,适合定期统计 滑动窗口:窗口间有重叠,固定窗口大小和滑动间隔,适合平滑趋势分析 会话窗口:基于活动间隔的动态窗口,适合用户行为分析 窗口的正确触发是保证计算结果准确的关键: 触发条件: 延迟数据处理: 延迟数据处理机制 这种机制确保即使在网络异常等情况下数据延迟到达,最终计算结果仍是准确的。 状态是Flink区别于其他流处理框架的核心能力,使得复杂的有状态计算成为可能。 键控状态:与特定键关联,在KeyedStream上可用 算子状态:与算子实例绑定,非键控 键控状态管理示例 Flink提供多种状态后端,满足不同场景需求: 内存状态后端:适合测试和小规模状态,重启后状态丢失 状态后端配置 状态后端的选择需要在性能、容量和可靠性之间权衡。某电商平台通过将状态后端从内存迁移到RocksDB,成功将可支持的用户会话状态从GB级提升到TB级。 Checkpoint是Flink实现容错和精确一次语义的核心技术,基于Chandy-Lamport算法实现分布式一致性快照。 Checkpoint执行流程: Checkpoint执行流程 仅靠Flink内部的Checkpoint机制无法实现真正的端到端精确一次,需要数据源和数据输出的协同配合。 两阶段提交协议: 两阶段提交Sink实现 某支付平台通过实现端到端精确一次语义,成功将重复支付事件降至0.001%以下,每年避免损失超千万元。 理解Flink实时计算心智模型的关键在于掌握五大核心概念如何协同工作: 事件流处理全链路: 乱序数据处理流程: 完整的事件时间处理链 合理配置资源是保证Flink作业稳定运行的关键: 并行度设置:根据数据量和处理复杂度设置合适的并行度 内存配置优化: 内存资源配置示例 有状态流处理作业的扩容和升级需要特别考虑状态一致性: 保存点机制:用于作业版本升级和状态迁移 状态兼容性检查: 完善的监控是生产环境稳定运行的保障: 关键监控指标: 某大型互联网公司通过建立完善的监控告警体系,将生产环境事故平均恢复时间从小时级缩短到分钟级。 Flink实时计算心智模型的构建需要深刻理解流、窗口、水位线、状态与Checkpoint五大核心概念的协同工作机制。这种理解不仅限于API调用,更在于掌握其背后的设计哲学和实现原理。 核心认知要点: 成功实践的关键: 随着实时计算需求的不断增长,掌握Flink实时计算心智模型已成为数据工程师的核心竞争力。通过深入理解这些核心概念及其协同机制,企业能够构建稳定、可靠的实时数据处理平台,为业务决策提供及时、准确的数据支持。 📚 下篇预告 点击关注,深入理解分布式系统一致性的本质代价! 今日行动建议:掌握Flink流处理的核心不在于API调用,而在于构建"事件时间优于处理时间"的心智模型,理解分布式有状态计算的一致性保证机制
1 流式优先:Flink的设计哲学与范式转变
1.1 批流一体认知范式的根本转变
1.2 Flink的架构优势与市场地位
2 流的概念深化:从无界数据到有状态计算
2.1 无界流与有界流的统一抽象
// 统一流处理示例:无界流与有界流使用相同API
DataStream<String> unboundedStream = env.addSource(new KafkaSource<>()); // 无界流
DataStream<String> boundedStream = env.readTextFile("hdfs://path/to/data"); // 有界流
// 相同的处理逻辑
DataStream<Tuple2<String, Integer>> processed = stream
.flatMap(new Tokenizer())
.keyBy(value -> value.f0)
.window(TumblingEventTimeWindows.of(Time.seconds(30)))
.sum(1);2.2 数据流编程模型的核心要素
Transformation:数据转换算子,如map、filter、keyBy、window等
Sink:数据输出端,将处理结果输出到外部系统// 流处理模式(默认)
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 批处理模式(有界数据优化)
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.BATCH);3 时间语义:流处理正确性的基石
3.1 三维时间模型的理解与应用
时间类型 定义 优点 缺点 适用场景 事件时间 事件实际发生的时间 结果准确,可重现 处理延迟较高 精确统计、计费对账 处理时间 数据被处理的时间 延迟最低,实现简单 结果不可重现 监控告警、低延迟需求 摄入时间 数据进入Flink的时间 平衡准确性与延迟 仍无法处理乱序 一般实时分析 3.2 水位线机制:处理乱序数据的核心创新
// 有序事件的水位线生成
WatermarkStrategy<Event> strategy = WatermarkStrategy
.<Event>forMonotonousTimestamps()
.withTimestampAssigner((event, timestamp) -> event.getCreationTime());
// 乱序事件的水位线生成(允许固定延迟)
WatermarkStrategy<Event> strategy = WatermarkStrategy
.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, timestamp) -> event.getCreationTime());4 窗口机制:无界流的有界化处理
4.1 窗口类型与适用场景
// 30秒的滚动事件时间窗口
windowedStream = stream
.keyBy(event -> event.getKey())
.window(TumblingEventTimeWindows.of(Time.seconds(30)));// 窗口大小1分钟,滑动间隔30秒
windowedStream = stream
.keyBy(event -> event.getKey())
.window(SlidingEventTimeWindows.of(Time.minutes(1), Time.seconds(30)));// 5分钟不活动则关闭会话
windowedStream = stream
.keyBy(event -> event.getUserId())
.window(EventTimeSessionWindows.withGap(Time.minutes(5)));4.2 窗口触发与延迟数据处理
// 允许延迟数据侧输出
OutputTag<Event> lateTag = new OutputTag<Event>("late-data"){};
WindowedStream<Event, String, TimeWindow> windowedStream = stream
.keyBy(event -> event.getKey())
.window(TumblingEventTimeWindows.of(Time.seconds(30)))
.sideOutputLateData(lateTag) // 侧输出延迟数据
.allowedLateness(Time.seconds(10)); // 允许10秒延迟
// 主流程计算结果
DataStream<Result> result = windowedStream.aggregate(new MyAggregateFunction());
// 处理延迟数据
DataStream<Event> lateData = result.getSideOutput(lateTag);5 状态管理:有状态流处理的核心
5.1 状态类型与使用场景
// 键控状态使用示例
public class CountWindowFunction extends RichFlatMapFunction<Event, Result> {
private transient ValueState<Integer> countState;
private transient ValueState<Long> lastTimeState;
@Override
public void open(Configuration parameters) {
ValueStateDescriptor<Integer> countDescriptor =
new ValueStateDescriptor<>("count", Integer.class);
countState = getRuntimeContext().getState(countDescriptor);
ValueStateDescriptor<Long> timeDescriptor =
new ValueStateDescriptor<>("lastTime", Long.class);
lastTimeState = getRuntimeContext().getState(timeDescriptor);
}
@Override
public void flatMap(Event event, Collector<Result> out) throws Exception {
Integer currentCount = countState.value();
if (currentCount == null) {
currentCount = 0;
}
currentCount++;
countState.update(currentCount);
// 业务逻辑处理
}
}5.2 状态后端与容错保障
文件系统状态后端:状态存储在磁盘,支持大状态,恢复速度较慢
RocksDB状态后端:本地磁盘+异步持久化,支持超大状态,生产环境推荐// 配置RocksDB状态后端
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new RocksDBStateBackend("hdfs://checkpoint-dir", true));6 Checkpoint机制:精确一次语义的实现
6.1 分布式快照原理
6.2 精确一次语义的端到端保障
// 精确一次Sink实现示例
stream.addSink(new TwoPhaseCommitSinkFunction<Event, Transaction, Context>(
new MyTransactionSupplier(), // 事务提供者
new MyTransactionSerializer(), // 事务序列化
new MyContextSerializer()) { // 上下文序列化
@Override
protected void invoke(Transaction transaction, Event value, Context context) {
// 在事务中写入数据
transaction.writeToExternalSystem(value);
}
@Override
protected void commit(Transaction transaction) {
// 提交事务
transaction.commit();
}
});7 五大核心概念的协同工作机制
7.1 完整数据处理链路分析
DataStream<Event> stream = env
.addSource(new KafkaSource<>())
.assignTimestampsAndWatermarks(
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, timestamp) -> event.getTimestamp()))
.keyBy(Event::getKey)
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.allowedLateness(Time.seconds(30))
.sideOutputLateData(lateOutputTag)
.aggregate(new MyAggregateFunction());7.2 性能优化与资源调配
# flink-conf.yaml 关键配置
taskmanager.memory.process.size: 4096m # TM进程总内存
taskmanager.memory.task.heap.size: 2048m # 任务堆内存
taskmanager.memory.managed.size: 1024m # 托管内存(状态后端)
taskmanager.numberOfTaskSlots: 4 # Slot数量8 生产环境实践与故障处理
8.1 状态扩容与作业升级
# 创建保存点
flink savepoint <jobId> [targetDirectory]
# 从保存点恢复
flink run -s <savepointPath> ...8.2 监控与告警体系
总结
《Exactly-once的真实成本——端到端一致性、两阶段提交与延迟权衡》—— 我们将深入探讨:
大家好!我是数字游民Hugo,聚焦AI轻创&Web3&OPC,深耕AI编程应用开发探路者。 你能想象吗?在斯坦福大学一个昏暗的地下室教室里,挤满了全世界最聪明的计算机系学生。空气里弥漫的不是兴奋,而是焦虑。 讲台上的讲师看着这些未来的科技精英,抛出了一条足以颠覆整个教育界的班规: "这门课,我要教你们如何在不写一行代码的情况下编程。如果你能上完整个学期一行代码都不亲手敲,那你才是真的牛。" 你没听错。当大多数高校还在像防贼一样禁止学生用 AI 写作业时,斯坦福这门名为"现代软件开发者"的课,正在禁止学生手写代码。 这不仅仅是一次教学实验,这是硅谷向全世界发出的信号:那个"会写代码等于金饭碗"的时代,彻底结束了。 看到这条新闻,我特别有感触。 2017 年,我第三次创业,想做一个基于邻里社交的本地生活服务平台。那时候我有个最大的痛苦——技术瓶颈。 我想找技术合伙人,找不到。想外包开发,30 万投进去效果不佳。想自己学,人到中年哪有那么多时间精力从零开始啃代码? 这个技术门槛,硬生生把我的创业梦卡住了。 后来我考察过各种项目,想扭转局面。直到 2025 年春节 DeepSeek 爆发,再到 2026 年初微信推出"AI 小程序成长计划",我才猛然发现: AI 编程的到来,把这个门槛给铲平了。 斯坦福这门课请来的客座讲师,全是行业大神:Cursor 创始人、Vercel 的 AI 研究负责人…… 他们都在传递同一个理念:AI Coding。 什么意思? 以前你写代码像个泥瓦匠,一块砖一块砖地砌,累死累活还容易歪。 现在你像个建筑师。 你只需要告诉 AI:"我要建一座大厦,风格是后现代,功能要有这些。"Cursor 和 Claude 就是你的施工队。 Cognition 的研究主管在课上说得直击痛点: 大佬们的话很直白: 现实很残酷,但机会也很真实。 我今年 40 多岁,山东人在上海打拼快 20 年。经历过三次创业,负债过,迷茫过,但依然选择相信:真诚这张牌,加上任何一张都是王炸,唯独不能单出,单出就是死牌。 现在,我选择把"真诚"加上"AI 编程"这张牌。 我不再是那个被技术门槛卡住的创业者,我是一个正在学习驾驭 AI 的超级个体。 斯坦福这门课告诉我们:代码正在贬值,但软件正在升值。 未来的核心竞争力: 能淘汰你的永远不是 AI,而是比你更会用 AI 的人。 现在的程序员(或者说想进入这个领域的人)只剩下两条路: 我选第二条路,而且我要带着大家一起走。 这个账号的定位很清晰:AI 编程开发创客实战。 我会边学边用边分享,输出倒逼输入,事上练干中学。不贩卖焦虑,只分享真实经历和可行方案。 先完成,再完美。 如果你也想成为驾驭 AI 的新贵,而不是被淘汰的旧码农,欢迎关注我。 我是数字游民Hugo,聚焦AI轻创&Web3&OPC,深耕AI编程应用开发实践,赋能个人蜕变成长&组织迭代进化,助力构建可持续的智能化增长体系。 在这个账号里,我会和你一起: 评论区聊聊你的观点,你会选择哪条路? 本文由mdnice多平台发布

一个 40 岁创业者的切身体会
码农死了,超级工程师诞生了

"如果你用昨天的方法学习,你就是在大刀长矛对战机关枪。但如果你掌握了这些工具,你一个人的战斗力能顶一支团队。"
我为什么选择这条路?
出路在哪里?
我的选择和邀请
Matrix 首页推荐
Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。
文章代表作者个人观点,少数派仅对标题和排版略作修改。
昨天上海降温明显,早上开车送娃上学的时候把空调、座椅加热、方向盘加热全都开了,车内温度上来的挺快。

返程的时候,路过一个红绿灯较长的路口,便看了看能耗情况,不同于之前的 6.5,新的 6.7 在中控大屏上看能耗方便不少,赫然写着 31.6,一个一年多以来未见过的能耗数值。

随即快速操作方向盘上的按钮,看仪表盘上面的信息做确认,没有看错,是 31.6 这个数值!

这整个过程很平静,拍完这两张图,反而不由自主地笑了起来。
从插混转到纯电这一年多来,对偶尔偏高的能耗已经完全不在意,心态上完全接受这种情况的出现,因为这并不影响用车体验,是无伤大雅的小插曲。
既然是要聊能耗与里程焦虑,必然会涉及到上一辆车,也就是从 2017 年 5 月份用到 2024 年 12 月下旬的比亚迪初代插混 SUV:唐 80。这七年的驾驶体验让我对能耗与里程焦虑有了基本认知。

下图是在卖车前的 app 截图,有展示出总里程、最近 50 公里平均能耗、累计平均能耗,插混车型的能耗还会显示耗电量和耗油量两个维度。

由于是初代插混 SUV,技术成熟度不够,整车能耗是偏高的,但在电量充足的情况下,有很好的加速性能。

上图左边部分为夏天电量较为充足情况下的能耗数据,右边是冬天电量不足情况下的能耗表现。
在 7 年多日常用车的过程中,过了刚开始的几个月,便很少关注能耗情况,基本就是没电了便找地方充电,要去远点的地方就去加油站加满油。
记得是 6 年多前的事情,当时从老家返程到上海,年初五的高速很堵,仅仅是进高速就排队了有一个小时。
天冷一直开空调,越堵车能耗越高,不到半程的时候油量已经只能一格半,显示的续航里程是 150km,此时最要紧的事情就是去服务区加油。
在这种情况下又遇到连续两个服务区关闭,中间的几十公里为了防止再遇到服务区关闭而不得不下高速的情况,是把空调关一会儿灯起雾了再开,希望能保留多一点油量。
终于在深夜排队进了第三个服务区,稍微休整下便去排队加油,大老远就听到工作人员在挨个敲车窗,重复说着一句话:车流量大,每辆车最多加 200 块钱的油……
对这次 600km 返程之旅,记忆犹新的两个点是:
① 耗时过长
加休息的几个小时,一起折腾了 17 个小时,日常大概 8 个小时。
② 里程焦虑了
一个因素是馈电+怠速的油耗偏高,另一个因素是没想到连续多个服务区不能进,遇到能加油的服务区的时候又叠加限制加油量的 buff。
按网络上的情况来分析,许多人对纯电车有不小的里程焦虑,冬季能耗高和节假日高速补能不方便的问题依然没有被解决。

也是在换成纯电之后才感知到的一个点:纯电车越用越不焦虑,其核心在于自身用车需求和场景与车辆特点的匹配。

仔细思考,不焦虑与下面几个因素息息相关:
① 单一的补能形式
纯电车的补能形式,有且仅有一个:充电,可快充、可慢充、部分车型支持换电。
之前使用插混的时候,宣传上是可油可电,实际日常使用的时候是既要充电也要加油,尤其是没有家充桩的情况,每次充电得去到附近的慢充桩进行。
② 车辆特点鲜明
由于纯电车电池的物理特性,必然让车辆变重,其整体续航和温度相关,冬天的高速能耗偏高,续航里程打折明显。
一体两面的是,纯电车的智能化、动力、舒适性配置等方面会有明显提升,各种因素杂糅在一起能带来用车体验的提升。
③ 匹配度
主要是指:用车需求、用车场景、车辆特点的匹配。
以下是梳理的个人情况:
用车需求:
1)日常接送娃上下学,单日 20km 左右;
2)周末偶尔长途出行,单程 100-300km 为主;
3)节假日往返老家,单程 600km,偶尔充电需排队。
用车场景:
1)多人或全家出行为主,单人出行偏少;
2)住在郊区,有家充,补能方便;
3)主要在长三角区域活动,冬季温度不低。
车辆特点:
1)选配 100kWh 大电池,续航有保障,支持快充;
2)舒适性配置充足,前后排座椅加热通风按摩,后排电动调节等;
3)尺寸大小适中,乘坐空间与载物空间充足。
我们无法抛开这几个因素,去聊现在主流的几个驱动形式的优劣:纯油、增程、插混、纯电。
整体来说,纯电车能更好地满足我的用车需求和场景。
于是,那个看到 31.6 这个能耗数据后不由自主的「会心一笑」,便有了一个确切的答案。
当出行工具与生活节奏同频,中控屏和仪表盘上的数字就只是数字, 选择一辆车,最终是在选择一种与自我需求高度匹配的生活方式。
> 参与 2025 年度少数派征文,分享你的观点和经验 ✍🏻️
> 关注 少数派小红书,感受精彩数字生活 🍃
> 实用、好用的 正版软件,少数派为你呈现 🚀
开发了一款视频加密播放器.名字叫 LockBox
防录屏,防虚拟机,防注入,防采集卡.
支持安卓,windows,ios,macos(不防录屏).
支持多重加密追踪水印.
支持多种授权方式.
如果只是自己加密,自己使用或小范围使用,完全免费.
欢迎有需要的朋友尝试.
下载地址:https://lockbox.movingshop.cn
注册后,回复注册的用户 ID 即送 10 年会员.
之前朋友向我推荐语音输入,说他用语音输入法后效率大幅提升,
但我之前用的一些语音输入法识别率不高。
而且一直对语音输入心理上多多少少有点抵触,平时本身就不怎么喜欢说话...
昨天我在手机上装了豆包输入法,准确率很高,会结合上下文修正输出。
感觉相比键盘输入,效率确实提升不少。
但电脑端豆包输入法没有出。
我在电脑上也试了秘塔回响,准确率还是比豆包低一些。
不过秘塔的优势是会用大模型对你说的内容自动进行整理后再输出。
但是感觉准确率还是差了一些,还需要手动改。
就用了一会,感觉还是有很多 BUG.
不知道当下有没有其他更优的工具或输入法推荐?
布尔类型使用Bool表示,用来表示逻辑中的真和假。 布尔类型只有两个字面量:true和false。 下面的例子展示了布尔字面量的使用: 布尔类型支持的操作符包括:逻辑操作符(逻辑非 字符类型使用Rune表示,可以表示Unicode字符集中的所有字符。 字符类型字面量有三种形式:单个字符、转义字符和通用字符。一个Rune字面量由字符r开头,后跟一个由一对单引号或双引号包含的字符。 单个字符的字符字面量举例: 转义字符是指在一个字符序列中对后面的字符进行另一种解释的字符。转义字符使用转义符号“ 通用字符以 编译并执行上述代码,输出结果为: 字符类型仅支持关系操作符:小于( Rune可以转换为UInt32,整数类型可以转换为Rune。 更多仓颉学习资料,详见:布尔类型
3.3.1 布尔类型字面量
let a: Bool = true
let b: Bool = false3.3.2 布尔类型支持的操作
!,逻辑与&&,逻辑或||)、部分关系操作符(==和!=)、赋值操作符、部分复合赋值操作符(&&=和||=)。字符类型
3.4.1 字符类型字面量
let a: Rune = r'a'
let b: Rune = r"b"\”开头,后面加需要转义的字符。举例如下:let slash: Rune = r'\\'
let newLine: Rune = r'\n'
let tab: Rune = r'\t'\u开头,后面加上定义在一对花括号中的1~8个十六进制数,即可表示对应的Unicode值代表的字符。举例如下:main() {
let he: Rune = r'\u{4f60}'
let llo: Rune = r'\u{597d}'
print(he)
print(llo)
}你好3.4.2 字符类型支持的操作
<)、大于(>)、小于等于(<=)、大于等于(>=)、相等(==)、不等(!=)。比较的是字符的Unicode值。参考引用

Scrapy Pipeline 的职责是分工协作: 如果去重 Pipeline 使用 Scrapy Pipeline:
raise DropItem() vs return item 的区别核心结论
在 Scrapy Pipeline 中,
return item 不会丢弃数据,item 会继续传递给后续 Pipeline;只有 raise DropItem() 才能真正终止 item 的传递链路。Pipeline 数据流向
item → Pipeline 1(去重过滤)→ Pipeline 2(写入 MongoDB)→ 存入数据库操作 效果 return itemitem 继续流转到下一个 Pipeline,最终仍会写入数据库 raise DropItem()item 立即被丢弃,后续所有 Pipeline 均不再处理 示例代码对比
❌ 错误写法(用 return item)
def process_item(self, item, spider):
md5_value = get_md5(item)
if self.redis_client.get(f'tx_work_item_filter:{md5_value}'):
return item # ⚠️ 错误!item 仍会流入 MongoPipeline 被写入数据库
return item✅ 正确写法(用 raise DropItem)
def process_item(self, item, spider):
md5_value = get_md5(item)
if self.redis_client.get(f'tx_work_item_filter:{md5_value}'):
raise DropItem('数据已存在...') # ✅ 正确!item 在此终止,不再传递
# 不重复则写入 Redis 标记
self.redis_client.set(f'tx_work_item_filter:{md5_value}', 1)
return item原理说明
return item,item 依然会到达存储 Pipeline 并被写入,去重逻辑完全失效。raise DropItem() 是 Scrapy 的官方丢弃机制,触发后框架会跳过该 item 的所有后续处理步骤。记忆口诀
return item = "我处理完了,传给下一个"raise DropItem() = "这个 item 不要了,整个链路终止"
坐标山西太原,移动 300M 带宽,这个月上传 115 网盘多了一些(主要是收藏经典电影,就是普通电影,只是蓝光版本,容量大一些),上行速率直接给我限制到了 2M ,打电话说可以解,但如果以后上传超量(上门的那个运维人员也不知道每个月限制多少上传容量),还是会被限速(除非你装企业宽带或者专线,问题是这 2 个我都用不起啊),以后就只能是上传超了容量被限制上行速率,然后打电话再解封? 还得提供身份证,手机卡密码,拍照,签同意告知书,这样也麻烦呀。。。大家有没有遇到类似的问题? 我几乎每个月都是几百 G 的上传,之前也没遇到这种限速问题啊,是因为今年管的越来越严格了吗?
先看一段常见的 Pipeline 写法: 既然真正的数据库连接是在 不多余,这是一种防御性编程的好习惯,原因如下: 1. 明确声明实例属性 Python 约定俗成的做法是把所有实例属性都在 2. 防止 如果 结论: Scrapy 在启动时,读取 这自然就触发了 如果你想在 Pipeline 中访问 如果定义了 Spider 的实例化是由 注意:Spider 默认就有 所以 Spider 和中间件一样,也是通过 中间件需要自己实现 框架直接 Item 完全不参与框架的生命周期管理,是开发者在 分两种情况: 情况一:没有重写 Python 直接使用父类的 情况二:重写了 此时父类的 一句话总结:重写了就要 super,没重写就不用管。 这是 Python 面向对象的通用原则,并非 Scrapy 特有。 理解 Scrapy 各组件的实例化机制,不仅能帮助你写出更健壮的代码,也能在遇到奇怪报错时快速定位问题根源。核心要点:Scrapy 各组件
__init__ 触发机制深度解析本文从一段 Scrapy Pipeline 代码出发,系统梳理 Spider、Pipeline、Middleware、Item 四大组件的实例化机制,以及
__init__ 与 from_crawler 的关系。一、引子:Pipeline 中为何先用
__init__ 初始化为 None?class TxWorkInfoPipeline:
def __init__(self):
self.mongo_client = None
self.redis_client = None
self.mongo_db = None
def open_spider(self, spider):
self.redis_client = redis.Redis()
self.mongo_client = pymongo.MongoClient()
self.mongo_db = self.mongo_client['py_spider']['tx_work']
def process_item(self, item, spider):
if spider.name == 'tx_work':
self.db.insert_one(item)
print('保存成功:', item)
return itemopen_spider 里完成的,__init__ 里赋值 None 是否多余?__init__ 中声明。这样一眼就能看出这个类有哪些属性,代码可读性更强。close_spider 报 AttributeErrordef close_spider(self, spider):
if self.mongo_client: # 若没有 __init__ 里的初始化,open_spider 未执行时此处直接崩溃
self.mongo_client.close()open_spider 因某些异常没有执行,而 close_spider 里又访问了 self.mongo_client,就会抛出 AttributeError。提前在 __init__ 里初始化为 None 可以避免这个问题。__init__ 里赋 None 并非必须,但是良好实践。如果你确定 open_spider 一定会被调用,直接在 open_spider 里初始化也完全没有问题。二、Pipeline 的
__init__ 是如何被触发的?settings.py 中 ITEM_PIPELINES 的配置,然后对每个管道类直接调用 cls() 来实例化:pipeline_instance = TxWorkInfoPipeline() # 框架内部的行为__init__。crawler(比如读取 settings), 可以自定义 from_crawler:@classmethod
def from_crawler(cls, crawler):
instance = cls() # 触发 __init__
instance.settings = crawler.settings
return instancefrom_crawler,Scrapy 会优先调用它;否则直接 cls() 实例化。三、各组件
__init__ 触发机制全对比Spider
CrawlerProcess / CrawlerRunner 触发的,Scrapy 内部会调用:spider = MySpider.from_crawler(crawler, *args, **kwargs)from_crawler,它定义在 scrapy.Spider 基类里:@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
spider = cls(*args, **kwargs) # 触发 __init__
spider._set_crawler(crawler)
return spiderfrom_crawler 间接触发 __init__ 的,只不过这个 from_crawler 是基类帮你写好的,你感知不到而已。Middleware(中间件)
from_crawler,由框架调用后,在其内部触发 __init__:@classmethod
def from_crawler(cls, crawler):
s = cls(crawler) # 触发 __init__
return sPipeline(管道)
cls() 实例化,触发 __init__。除非你自定义了 from_crawler,否则无法直接访问 crawler 对象。Item
parse 方法里手动 new 出来的:def parse(self, response):
item = MyItem() # 你自己触发的 __init__
item['title'] = response.css('h1::text').get()
yield item四、总结对比表
组件 __init__ 触发方式是否经过 from_crawlerSpider 基类的 from_crawler 内 cls()✅ 是(基类已实现,对开发者透明) Middleware 自定义 from_crawler 内 cls()✅ 是(需自己实现) Pipeline Scrapy 框架直接 cls()❌ 否(除非自定义 from_crawler)Item 开发者在 parse 中手动 cls()❌ 否,框架完全不介入 五、延伸:在自己的 Spider 中重写
__init__ 时需要 super().__init__() 吗?__init__(最常见)__init__,一切正常,无需任何额外操作。__init____init__ 被覆盖,必须手动调用 super().__init__(*args, **kwargs) 来补回父类的初始化逻辑,否则 self.name、self.crawler、self.settings 等属性会丢失,运行时报 AttributeError。class MySpider(scrapy.Spider):
name = 'my_spider'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # 必须!补回父类初始化逻辑
self.my_custom_data = [] # 自己额外的初始化结语
cls() 的,最简单from_crawler,但基类帮你封装好了from_crawler 需要自己实现
点赞 + 关注 + 收藏 = 学会了 yt-dlp 是一款强大的视频下载工具(命令行界面的王者),而今天我要推荐的是 yt-dlp-webui,它给硬核工具穿上了一层“漂亮的外衣”,让你在浏览器里就能点点鼠标,轻松下载 B 站、小红书、YouTube 等主流平台的视频。 有了这个神器,再也不怕喜欢的视频被 Up 主删除了! 本次以 飞牛 NAS (fnOS) 为例演示,绿联、群晖、极空间等 NAS 操作逻辑完全通用! 打开「文件管理」,在 在 打开「Docker」应用,切换到 「Compose」 面板,点击「新建项目」。 输入以下代码: 等待项目构建成功后,在“容器”面板找到它,点击旁边的 “链接” 按钮即可跳转。 这是 yt-dlp 的界面。 我以下载B站视频为例。 打开 B 站,找到你想“据为己有”的视频,直接复制浏览器地址栏的 URL。 回到 yt-dlp,点击首页右下方的加号。 把链接丢进来这个输入框里,然后点击“开始”按钮就开始下载了。 视频会自动保存到 你也可以直接在 Web 界面点击视频下方的下载按钮,保存到电脑本地。 亲测可下~ 对了,如果进去发现是英文,别慌!在 Settings(设置) 里面找到语言选项,改为 Chinese 即可。 以上就是本文的全部内容啦,好视频不会丢,现在担心的是硬盘扛不扛得住了😭 你有发现更好用的视频下载工具吗?欢迎在评论区交流讨论! 想了解更多NAS玩法记得关注《NAS邪修》👏 点赞 + 关注 + 收藏 = 学会了💡整理了一个 NAS 专属玩法专栏,感兴趣的工友可以戳这里关注 👉 《NAS邪修》

docker 目录下新建文件夹 yt-dlp。yt-dlp 文件夹内,再分别新建两个子文件夹:downloads(用于存放下载好的视频)config(用于存放配置文件)
docker 文件夹里创建的 yt-dlp 文件夹
services:
yt-dlp-webui:
image: marcobaobao/yt-dlp-webui
ports:
- 3456:3033 # 冒号左边的 3456 是访问端口,可按需修改
volumes:
- ./downloads:/downloads # 视频下载路径
- ./config:/config # 配置保存路径
healthcheck:
test: curl -f http://localhost:3033 || exit 1
restart: unless-stopped




/docker/yt-dlp/downloads 文件夹中。



1 、行车电脑
2 、u 盘的 mp4 文件的元数据层?
这关系到一段时间后,车机里是否还能看到车速和转向灯状态。
因为忘了点手动点击保存,但是从 RecentClips 复制出来的放到 SavedClips 里也能看到车速。
现在担心过几天这些状态数据会自动删除导致看不到车速和转向灯状态了,毕竟这个不是正常的保存逻辑。
如果车速和转向灯状态实际数据是保存在 mp4 文件里,那就不用担心了。
就在明天
点赞 + 关注 + 收藏 = 学会了 Tower Blocks 是一款超解压的堆叠积木休闲小游戏,NAS 小白用 Docker 就能一键部署,群晖、威联通等主流 NAS 都适配,不用复杂配置。核心玩法超简单:看准时机点击屏幕,让下落的方块精准叠在塔上,堆得越高分数越高,考验手眼协调与反应力,大人小孩都能玩。游戏占用资源极低,不会影响 NAS 存储核心功能。 本次使用飞牛 NAS 部署 Tower Blocks,其他品牌 NAS 操作方式基本一样。 打开“文件管理”,在“docker”文件夹下创建一个“tower-blocks”文件夹。 然后打开“Docker”应用,在”Compose“面板里创建一个新项目。 勾选“创建项目后立即启动”。 然后输入以下代码: 我给 Tower Blocks 配置了 项目构建成功后,在浏览器输入 以上就是本文的全部内容啦,有疑问可以在评论区讨论~ 想了解更多NAS玩法可以关注《NAS邪修》👏 点赞 + 关注 + 收藏 = 学会了整理了一个NAS小专栏,有兴趣的工友可以关注一下 👉 《NAS邪修》


创建docker-compose.yml。
services:
tower-blocks:
image: heizicao/tower-blocks:latest
container_name: tower-blocks
ports:
- 2332:3000
restart: always2332 这个端口,你可以按需配置。NAS的IP:2332 就可以开始玩了。
在快节奏的工作环境中,有效的沟通工具至关重要。访答作为新兴的沟通平台,正以其独特的功能改变着人们的协作方式。 访答专注于简化沟通流程,其智能问答系统能够快速响应各类查询。与传统沟通工具相比,它减少了信息传递的层级,让沟通更加直接高效。 无论是团队内部协作还是客户服务,访答都能提供稳定可靠的支持。其简洁的界面设计和强大的后台处理能力,确保了用户体验的流畅性。 随着人工智能技术的不断发展,访答有望在智能助手领域实现更多突破,为用户提供更加个性化的服务体验。访答:重新定义高效沟通
核心功能解析
应用场景优势
未来发展展望

有一个域名 xuanzhuji.com 在手里好几年了,之前做过 VPS 测评站(完全采集),电脑主机资讯站,都没有流量,现在想把这个网站好好的运营运营,有没有好的意见/建议啊?已经部署了测试站:v2.xuanzhuji.com ,请求各位大佬不吝赐教,感谢。
试了搜狗, Gboard, 微信, 手心, 最后留下了 百度 来用.
布局
习惯了 iOS 默认的九宫格输入布局, 觉得用起来挺舒服顺手, 不能调整布局的输入法就没考虑了.
词库
国内输入法很好的补全了词库的问题. 在词库设置里添加热门词库就可以用了.
隐私
剪贴板, 翻译, 计算器小功能都可以离线使用, 禁止联网权限能减轻隐私焦虑?
风评
百度 这个词确实很难评.
一直使用默认的输入法, 切到淘宝京东抖音立马给推荐相关产品也会出现.
存在的问题
百度的布局不完全 iOS, 九宫格输入时 1 键会变成分词, 而不是一直是逗号句号.
还有其他类 iOS 输入法推荐?
谁还在为OpenClaw的“笨重”忍气吞声?! 2026年,OpenClaw凭一己之力带火了开源AI智能体赛道,14天狂揽190K GitHub星标,一度碾压深耕数十年的行业巨头,让“本地AI数字员工”走进大众视野。可火归火,用过的人都懂那种“爱并痛苦着”的憋屈——号称“轻量化本地部署”,实则是个“资源吞噬巨兽”,部署要装Node.js、调试复杂依赖链,内存占用动辄1GB以上,启动要等几百秒,就算是低配服务器都扛不住,普通电脑运行起来更是卡顿到崩溃,所谓的“降本增效”,到最后全变成了“耗内存、拖速度”的负担。 多少开发者熬夜调试,就为了让OpenClaw正常启动;多少中小企业花大价钱升级硬件,就为了承载这个“笨重的数字员工”;多少小白看着复杂的部署教程,望而却步,只能眼睁睁看着别人用AI高效办公,自己却被工具卡脖子? 全网都在呐喊:OpenClaw很好,但能不能轻一点?能不能简单一点?能不能让普通人也能轻松上手? 就在所有人都以为,开源AI智能体的“轻量化”已经触达天花板,只能在笨重和功能之间二选一时,Claw家族新成员——NullClaw,带着“极致轻量、零门槛、高性能”的锋芒横空出世,直接打破困局,用实力改写赛道规则:原来AI智能体,真的可以做到“轻到极致,强到离谱”! 没有铺天盖地的宣传,没有夸张到离谱的噱头,NullClaw只用一组公开可查的实测数据,就炸翻了整个AI圈,把“轻量”两个字卷到了极致,狠狠打脸那些“伪轻量化”工具—— 内存占用仅0.8MB,不足OpenClaw的0.1%,比手机里的普通小程序还要轻,就算是十几年前的老旧电脑、10美元的廉价开发板,也能流畅运行,不会占用丝毫多余资源;启动时间快到离谱,仅需5毫秒,是OpenClaw的100倍以上,双击下载,一秒启动,无需等待,打开就能用,彻底告别“漫长启动焦虑”;二进制体积仅1.2MB,比OpenClaw小20多倍,下载速度快到飞起,就算是流量上网,几秒就能下载完成,不用再忍受动辄几十MB的缓慢下载。 更爽的是,NullClaw做到了“轻而不弱,简而全能”——它完美继承了OpenClaw的所有核心优势,甚至在功能上实现了全面超越,却把OpenClaw的所有痛点,全部斩草除根! OpenClaw部署复杂,小白望而却步?NullClaw直接做到“零门槛部署”,单文件自包含二进制,支持arm、x86、risc-v全架构,无需安装任何依赖,不用懂一行代码,不用调试任何参数,下载、双击、使用,三步到位,就算是电脑小白,也能轻松上手,真正做到“人人都能玩得转”的本地AI智能体,彻底打破了OpenClaw“不适合非技术用户”的局限。 OpenClaw功能单一,复杂场景全歇菜?NullClaw直接拉满功能上限:OpenClaw能做的,它做得更快、更稳;OpenClaw做不到的,它照样拿捏!本地文件处理、日程管理、邮件自动发送、代码调试、网页检索,甚至是多轮连续交互、长程推理,NullClaw都能轻松胜任,100+轮连续交互不卡顿、不丢失上下文,多源信息交叉验证,动态调整策略,比OpenClaw的响应速度快3倍以上,复杂任务的完成准确率直接飙升至99.2%,远超同级别工具,甚至对标部分大参数模型的表现。 OpenClaw安全隐患突出,数据泄露风险高?NullClaw天生自带安全buff,延续Claw家族“本地优先”的核心优势,所有数据都运行在用户自有设备上,不上传云端,守住数据主权,同时搭载严格的沙箱机制、显式允许列表、工作区隔离,自动拦截危险指令,杜绝提示词注入攻击,从设计上杜绝数据泄露和设备操控风险,比OpenClaw的安全防护更周全,企业和个人用着都能彻底放心,再也不用为“AI内鬼”担忧。 更绝的是,NullClaw还解决了OpenClaw的“隐性痛点”——零成本使用、全架构兼容、无任何功能限制!不用支付高额的部署费、月费,不用升级硬件,不用为了某个功能额外安装插件,免费开源、无商业壁垒,支持Windows、Mac、Linux所有系统,甚至能在边缘设备、移动终端部署,实现离线推理与本地交互,真正做到“零开销、零妥协、全自主”,让每一个人都能免费享受AI带来的高效便捷,彻底打破“AI高效办公=高成本”的误区。 有人说,NullClaw是站在OpenClaw的肩膀上成长起来的,但我要说,它不是继承,而是颠覆!OpenClaw没能解决的轻量化痛点,它一一破解;OpenClaw没能实现的零门槛体验,它完美落地;OpenClaw没能兼顾的“轻量与性能”,它做到了极致平衡。 短短3天,NullClaw的GitHub星标疯狂暴涨,Fork数直线飙升,无数开发者弃坑OpenClaw,转头投入NullClaw的怀抱,留言区全是“封神”“终于等到你”“这才是我想要的轻量化AI智能体”的欢呼。曾经,OpenClaw是赛道标杆,是所有人的“唯一选择”;如今,NullClaw的出现,不仅填补了“极致轻量化AI智能体”的市场空白,更重新定义了开源AI智能体的核心竞争力——轻量,不是妥协,而是极致的追求;简单,不是简陋,而是更懂用户的需求。 想想看,不用再为部署复杂而头疼,不用再为内存占用而焦虑,不用再为硬件升级而花钱,双击下载,一秒启动,老旧电脑也能流畅运行,小白也能轻松上手,既能享受本地AI的安全便捷,又能拥有极致高效的办公体验,这才是开源AI智能体该有的样子! 从OpenClaw到NullClaw,是Claw家族的迭代升级,更是开源AI智能体赛道的一次革命。OpenClaw打开了赛道的大门,而NullClaw,正在带领赛道走向更极致、更亲民、更高效的未来,让“轻量化AI”不再是口号,让每一个人、每一家企业,都能轻松拥有属于自己的高效AI数字员工。 那些还在忍受OpenClaw笨重、卡顿、部署复杂的人,别再硬扛了!现在就上手NullClaw,体验一把“轻到极致,强到离谱”的快乐,你会发现:原来AI办公,可以这么轻松、这么高效;原来开源AI智能体,早就该是这个样子! NullClaw来了,轻量化AI时代,正式开启!OpenClaw的辉煌已成过去,NullClaw的传奇,才刚刚开始! 更多AI咨询详见开源电子书《跟老卫学AI大模型开发》


