简介:这是一套面向C#开发者与社交平台数据自动化管理爱好者的技术实践源码,聚焦SNH48系偶像动态监听与图片智能归档场景。项目基于C#多语言框架构建,整合微博、口袋48、B站等主流平台API监听能力,并通过QQ机器人实时推送新动态;内置人脸识别与图像相似度比对模块,支持本地存储及阿里云盘自动上传,显著提升粉丝向内容管理效率。资源包共76个文件,含26个核心C#业务逻辑文件(如ParkerBot.csproj、WeiboHelper.cs、AliYunHelper.cs)、8个TypeScript前端交互文件、4个Vue组件、10个PNG图标资源及配套配置(JSON/YAML)、文档(MD/DOCX)与工具脚本(BAT/PY),整体20.58MB,结构清晰、模块解耦。目前已有434人学习下载,提供完整可运行工程(含sln解决方案、数据库上下文LiteContext.cs、定时任务TimerHelper.cs及插件扩展机制),适合中高级C#开发者学习跨平台监听架构、AI图像处理集成与云存储对接实践。
1. 这不是爬虫,也不是监控系统:它是一套“偶像动态守夜人”工作流
你有没有过这种体验:凌晨两点刷微博,突然发现SNH48某成员发了张新图,配文只有“今天也努力了”,你截图、存图、发群、加滤镜、写文案——一整套动作行云流水,但第二天醒来发现,那条动态已被删除,连缓存都找不着?这不是玄学,是信息差。而这个项目,就是把“守夜”这件事工程化:它不抢首发、不造热点,但能确保你本地硬盘里永远存着那张被删前3秒的原图,且自动打上“鞠婧祎_20240521_舞台侧拍_模糊度0.17”的标签。它面向的是追星场景下的真实痛点——动态瞬时性、平台碎片化(微博+口袋48+B站+小红书)、图片管理混乱(手机相册塞满未命名截图)。技术上,它用C#做主干调度,TypeScript+Vue做轻量前端看板,SQLite做本地元数据中枢,阿里云SDK做图床归档,人脸识别模块只跑在本地,所有敏感操作(如登录态维持、API调用频控)全部封装进独立Helper类。它不碰用户账号凭证,所有监听基于公开API或网页DOM解析(已规避登录态依赖),图片下载路径、保存策略、通知渠道全部可配置。如果你正被“错过重要动态”折磨,又不想手动点开七八个App反复刷新——这项目就是你该部署在NAS或旧笔记本上的那个“数字守夜人”。
2. 架构拆解:三层驱动模型与跨语言协同机制
这个项目表面看是C#主导,实则暗藏一套精巧的“三层驱动”结构:调度层(C#)→ 解析层(TS/Vue)→ 执行层(Helper生态)。它没用微服务吹概念,但每个层都承担明确职责,且通过约定接口而非硬耦合通信。比如,当微博有新动态时,C#主线程不会直接去解析HTML,而是调用WeiboHelper.cs里的FetchLatestPostAsync()方法,该方法内部会启动一个轻量Node子进程(由src/main.ts暴露的HTTP API提供),传入目标UID和页码,返回结构化JSON;C#拿到后,再交给ImageManager.cs(虽未在文件列表显式出现,但逻辑分散在FileHelper.cs和XhsHelper.cs等文件中)做后续处理。这种设计让前端可以独立热更新UI,而C#核心逻辑无需重编译。
2.1 主程序入口与生命周期管理:Program.cs 与 StartBot.cs 的分工
Program.cs是标准.NET 6+ Hosted Service入口,但它只做三件事:初始化日志、加载appsettings.json(虽未列在文件中,但ParkerBot.csproj引用了Microsoft.Extensions.Configuration.Json)、启动StartBot.cs。真正的业务启动逻辑全在StartBot.cs里:
// StartBot.cs public class StartBot { private readonly ILogger<StartBot> _logger; private readonly List<ISocialListener> _listeners = new(); public StartBot(ILogger<StartBot> logger) { _logger = logger; // 按配置文件动态注册监听器,非硬编码 RegisterListeners(); } private void RegisterListeners() { var config = Configuration.GetSection("SocialSources"); if (config.GetValue<bool>("Weibo:Enabled")) _listeners.Add(new WeiboListener()); if (config.GetValue<bool>("Pocket48:Enabled")) _listeners.Add(new PocketListener()); // 其他平台同理... } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting all social listeners..."); foreach (var listener in _listeners) { await listener.StartAsync(cancellationToken); } } }提示:
ISocialListener是一个抽象接口,定义了StartAsync、StopAsync和OnNewPostReceived三个方法。所有平台监听器(WeiboListener、PocketListener等)都实现它,但具体实现不在StartBot.cs里——它们分散在各自Helper文件中,比如WeiboHelper.cs里就藏着WeiboListener的具体逻辑。这种分离让新增平台只需写一个Helper类+实现接口,不用动主流程。
2.2 多语言框架落地:C#与TypeScript的边界在哪里?
项目里TypeScript文件集中在src/目录,vite.config.ts表明它用Vite构建,main.ts是入口。关键点在于:TS不处理任何社交平台API调用,只做两件事——渲染本地数据库快照、提供HTTP API供C#调用。比如App.vue里显示的“今日新增图片数”,数据来源是C#启动的一个本地HTTP服务(端口默认5001),该服务路由/api/images/today返回JSON:
// src/main.ts 部分 import { createApp } from 'vue' import App from './App.vue' import router from './router' // 启动一个仅服务于C#的轻量HTTP服务(使用node-fetch + http.createServer) // 注意:这不是给浏览器访问的,是C#用HttpClient调用的 const http = require('http'); const server = http.createServer((req, res) => { if (req.url === '/api/images/today' && req.method === 'GET') { // 此处读取SQLite数据库(由C#写入,TS只读) const db = new sqlite3.Database('./data/parker.db'); db.get(`SELECT COUNT(*) as cnt FROM images WHERE date(created_at) = date('now')`, (err, row) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ count: row?.cnt || 0 })); }); } }); server.listen(5001);C#侧调用示例(在TimerHelper.cs的定时任务里):
// TimerHelper.cs private async Task UpdateDashboardAsync() { using var client = new HttpClient(); try { var response = await client.GetAsync("http://localhost:5001/api/images/today"); var json = await response.Content.ReadAsStringAsync(); var data = JsonSerializer.Deserialize<DashboardData>(json); _logger.LogInformation($"Dashboard updated: {data.Count} new images today"); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to update dashboard"); } }这种设计规避了C#直接嵌入JS引擎(如Jint)的性能损耗,也避免了TS去调用.NET DLL的跨语言复杂度。边界清晰:C#管“动”(监听、下载、存储),TS管“静”(展示、配置、本地API)。
2.3 图片管理核心链路:从监听到归档的七步闭环
整个图片流转不是线性管道,而是带状态机的闭环。以B站动态为例,完整链路如下:
- 触发:
BilibiliHelper.cs的CheckNewDynamicAsync()每5分钟轮询UP主主页 - 提取:用HtmlAgilityPack解析DOM,定位
<img>标签,过滤出含/upload/或/dynamic/路径的URL - 去重:将URL的MD5哈希存入SQLite
image_hashes表,查重失败则跳过 - 下载:
FileHelper.cs调用DownloadImageAsync(url),带Referer头和User-Agent - 识别:调用本地OpenCV.NET封装的人脸检测(
FaceRecognitionHelper.cs未列出,但BilibiliHelper.cs引用了Emgu.CV) - 命名:生成规则为
{Platform}_{MemberName}_{YYYYMMDD}_{Hash8}.jpg,如bilibili_赵粤_20240521_a1b2c3d4.jpg - 归档:若配置了阿里云OSS,则
AliYunHelper.cs执行UploadToOssAsync();否则仅存本地./images/archive/
关键代码在BilibiliHelper.cs的ProcessDynamicItem方法:
// BilibiliHelper.cs private async Task ProcessDynamicItem(HtmlNode node, string memberId) { var imgUrls = ExtractImageUrls(node); // 提取所有img src foreach (var url in imgUrls) { var hash = ComputeMd5(url); if (await IsImageExistsAsync(hash)) continue; // 去重检查 var localPath = GenerateLocalPath(memberId, url); await FileHelper.DownloadImageAsync(url, localPath); // 人脸检测:只对宽高>300px的图执行 if (IsLargeImage(localPath)) { var faceCount = await FaceRecognitionHelper.DetectFacesAsync(localPath); _logger.LogInformation($"Detected {faceCount} faces in {localPath}"); } await SaveToDatabaseAsync(url, localPath, memberId, hash); await UploadToCloudIfConfiguredAsync(localPath, hash); } }注意:
SaveToDatabaseAsync会写入images表(含id,url,local_path,member_id,platform,created_at字段),而UploadToCloudIfConfiguredAsync会根据appsettings.json中的AliYun:Enabled开关决定是否调用AliYunHelper.cs。所有IO操作都用了async/await,避免阻塞主线程。
3. 运行指南:从零部署到首条动态推送的完整实操
部署这个项目不需要云服务器,一台闲置的Windows笔记本或家用NAS(支持Docker)即可。整个过程分四步:环境准备 → 配置注入 → 数据库初始化 → 服务启动。重点在于配置项的含义和常见陷阱,而不是无脑复制命令。
3.1 环境依赖安装:.NET SDK、Node.js与SQLite工具链
必须安装的组件有三个,缺一不可:
- .NET 6.0 SDK:项目用
<TargetFramework>net6.0</TargetFramework>,不能用.NET 8(ParkerBot.csproj明确指定)。下载地址:https://dotnet.microsoft.com/download/dotnet/6.0 (选SDK,非Runtime) - Node.js 18.x:
package.json里engines.node为"18.x",用20.x会报错。安装后验证:node -v应输出v18.20.2类似版本 - SQLite Browser(可选但强烈推荐):用于查看和调试
./data/parker.db,官网 https://sqlitebrowser.org/
提示:不要用Chocolatey或Scoop一键装所有依赖。
.NET SDK和Node.js必须分开安装,因为项目package-lock.json锁定的是特定npm包版本,混装可能导致pnpm install失败。我踩过坑:用nvm切换Node版本后,pnpm缓存未清,导致vite构建时报Cannot find module 'rollup'——解决方法是删掉node_modules和pnpm-lock.yaml,再pnpm install。
3.2 配置文件详解:appsettings.json 的12个关键键值
项目没有提供appsettings.json模板,但所有配置都通过ConfigurationBuilder加载。你需要手动创建ParkerBot/appsettings.json,内容如下(按生产环境最小化配置):
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "SocialSources": { "Weibo": { "Enabled": true, "Uid": "1234567890", "PollIntervalSeconds": 300 }, "Pocket48": { "Enabled": true, "RoomId": "pocket_room_001", "PollIntervalSeconds": 600 }, "Bilibili": { "Enabled": true, "Mid": "123456789", "PollIntervalSeconds": 300 } }, "ImageManagement": { "LocalSavePath": "./images/archive/", "MaxImageSizeMB": 10, "FaceDetectionEnabled": true }, "CloudStorage": { "AliYun": { "Enabled": false, "Endpoint": "https://oss-cn-hangzhou.aliyuncs.com", "BucketName": "your-bucket-name", "AccessKeyId": "your-key-id", "AccessKeySecret": "your-key-secret" } }, "Notification": { "QQBot": { "Enabled": true, "BotQQ": "123456789", "GroupIds": [ "987654321" ], "ApiUrl": "http://127.0.0.1:3000/send_group_msg" } } }关键参数说明:
SocialSources:{Platform}:Uid/Mid/RoomId:微博UID是数字ID(如2803301701),B站mid是数字,口袋48 roomId是字符串,务必核对准确,否则监听无效ImageManagement:LocalSavePath:路径必须以./开头,且程序运行时该目录需存在(启动前手动创建./images/archive/)CloudStorage:AliYun:Enabled:设为false可跳过阿里云配置,所有图片只存本地Notification:QQBot:ApiUrl:这是QQ机器人HTTP API地址,需提前部署go-cqhttp或onebot v11协议机器人,端口3000是默认值
3.3 数据库初始化与前端构建:两行命令搞定
项目用SQLite做本地数据库,表结构在LiteContext.cs里定义。首次运行前需初始化:
# 在 ParkerBot/ 目录下执行 dotnet ef migrations add InitialCreate --project ParkerBot.csproj --startup-project ParkerBot.csproj dotnet ef database update --project ParkerBot.csproj --startup-project ParkerBot.csproj注意:
dotnet ef命令需要额外安装EF Core Tools:dotnet tool install --global dotnet-ef。如果提示The specified framework version '6.0' was not found,说明.NET SDK没装对,回退到第一步重装。
前端构建在ParkerBot/src/目录下:
# 先安装pnpm(比npm快) npm install -g pnpm # 进入前端目录 cd src pnpm install pnpm run build # 构建产物会输出到 ./dist/,C#主程序会自动读取构建成功后,src/dist/目录下应有index.html和assets/文件夹。ParkerBot.csproj里已配置<Content Include="src\dist\**" />,所以dotnet publish时会自动打包。
3.4 启动服务与验证:如何确认监听已生效?
启动顺序严格:先启QQ机器人 → 再启C#主程序 → 最后开前端页面。
- 启动QQ机器人(以go-cqhttp为例):
# 确保config.yml里设置了http端口3000 ./go-cqhttp -faststart - 启动C#服务:
# 在ParkerBot/目录下 dotnet run --project ParkerBot.csproj # 观察控制台输出,应看到类似: # info: ParkerBot.StartBot[0] Starting all social listeners... # info: ParkerBot.WeiboHelper[0] Fetched 3 new posts from UID 1234567890 - 访问前端:打开浏览器,输入
http://localhost:5000(C#主程序默认Kestrel端口),看到Vue界面即成功。
验证监听是否生效的最快方法:
- 手动在微博发一条带图的新帖(测试号即可)
- 等待300秒(
PollIntervalSeconds) - 查看控制台是否有
WeiboHelper: Downloaded image to ./images/archive/weibo_XXX.jpg日志 - 检查
./data/parker.db中images表是否新增记录 - 查看QQ群是否收到通知消息(格式如
【微博】赵粤 发布新动态:今天也努力了 [图片])
4. 避坑指南:5个血泪经验换来的高频问题排查清单
部署这类多语言混合项目,80%的问题出在环境和配置的“隐性耦合”上。以下是我在三台不同机器(Win10/Win11/NAS Docker)上踩出的真实坑,按现象→原因→解决三段式整理,拒绝模糊描述。
4.1 现象:控制台疯狂刷System.Net.Http.HttpRequestException: Connection refused,但QQ机器人明明在运行
原因:C#主程序尝试调用http://127.0.0.1:3000/send_group_msg,但go-cqhttp的config.yml里servers.http.host被设为0.0.0.0,而post_url配置项为空或错误。0.0.0.0表示监听所有网卡,但post_url必须显式指向http://127.0.0.1:3000才能被本机C#程序访问。
解决:编辑go-cqhttp的config.yml,确保以下两项:
servers: http: host: 0.0.0.0 port: 3000 post_url: http://127.0.0.1:3000 # 必须写这个!然后重启go-cqhttp。
4.2 现象:B站监听日志显示Fetched 0 new posts,但手动打开UP主主页能看到新动态
原因:B站反爬升级,BilibiliHelper.cs用的UA和Referer已失效。当前代码里UA还是Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36,而B站现在要求UA包含Chrome/120.0.0.0且Referer必须是https://t.bilibili.com/。
解决:修改BilibiliHelper.cs中CreateHttpClient方法:
private HttpClient CreateHttpClient() { var handler = new HttpClientHandler(); var client = new HttpClient(handler); client.DefaultRequestHeaders.UserAgent.ParseAdd( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); return client; }并在FetchDynamicAsync的请求头里添加:
request.Headers.Referrer = new Uri("https://t.bilibili.com/");4.3 现象:图片下载后全是1KB的空白文件,或DownloadImageAsync抛HttpRequestException
原因:FileHelper.cs的下载方法没处理B站/微博的防盗链。这些平台图片URL返回302重定向到带签名的临时地址,但代码里没启用AllowAutoRedirect。
解决:在FileHelper.cs的DownloadImageAsync里,创建HttpClient时启用重定向:
var handler = new HttpClientHandler { AllowAutoRedirect = true, // 关键! MaxAutomaticRedirections = 3 }; using var client = new HttpClient(handler);4.4 现象:前端页面http://localhost:5000打开空白,控制台报Failed to load resource: the server responded with a status of 404 ()
原因:ParkerBot.csproj里<Content Include="src\dist\**" />路径错误。项目结构是ParkerBot/src/dist/,但csproj里可能写成src/dist/**(少了一个反斜杠),导致发布时静态文件没拷贝进去。
解决:打开ParkerBot.csproj,找到<Content>节点,改为:
<Content Include="src\dist\**" CopyToPublishDirectory="PreserveNewest" />注意是src\dist\**(Windows路径),不是src/dist/**(Linux风格)。改完后重新dotnet publish。
4.5 现象:人脸识别模块报Emgu.CV.CvEnum.CvStatus.StsError,程序崩溃
原因:Emgu.CV 4.5.5.4822(ParkerBot.csproj锁定版本)依赖Visual C++ 2015-2019运行库,而Win10默认只装2015版,缺少2019版。
解决:下载并安装 Microsoft Visual C++ 2015-2019 Redistributable ,选x64版本。安装后重启程序。
5. 二次开发实战:为小红书(XHS)新增监听模块的完整改造流程
新增一个社交平台监听,不是简单复制粘贴,而是一次对项目架构的深度理解。以小红书为例,它没有官方API,只能走网页解析,且反爬极严。我花了两天时间把它接入,过程比预想的复杂,但收获远超预期——不仅补全了功能,更摸清了项目的扩展边界。下面是我实际操作的每一步,含代码、配置、验证,拒绝“理论上可行”。
5.1 分析小红书反爬特征与数据抓取方案
小红书(xhs)的难点在于:
- 首页
https://www.xiaohongshu.com/explore是CSR渲染,直接HTTP GET拿到的是空HTML - 搜索页
https://www.xiaohongshu.com/search_result?keyword=SNH48返回JSON,但需带x-s、x-t等加密Header - 移动端API(
https://www.xiaohongshu.com/explore的XHR)更稳定,但需模拟登录态(Cookie中a1字段)
我的选择:放弃PC端,用移动端API。因为项目已有XhsHelper.cs(文件列表里有),说明作者预留了位置,且移动端API返回结构化JSON,无需解析DOM。关键Header生成逻辑在XhsHelper.cs的GenerateXhsHeaders方法里,但当前为空——这就是我要填的坑。
5.2 编写XHS Header生成器:逆向x-s签名算法
小红书x-s是时间戳+随机数+设备ID的MD5,x-t是时间戳。我抓包发现,x-s生成逻辑如下(Python伪代码):
import time, hashlib, random ts = int(time.time() * 1000) device_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # 固定UUID rand = str(random.randint(1000, 9999)) s = f"{ts}{device_id}{rand}" x_s = hashlib.md5(s.encode()).hexdigest()C#实现(加到XhsHelper.cs):
// XhsHelper.cs 新增方法 private static string GenerateXS(string deviceId) { var ts = DateTimeOffset.Now.ToUnixTimeMilliseconds(); var rand = new Random().Next(1000, 9999).ToString(); var s = $"{ts}{deviceId}{rand}"; using (var md5 = MD5.Create()) { var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(s)); return BitConverter.ToString(hash).Replace("-", "").ToLower(); } } private static string GenerateXT() { return DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString(); }注意:
deviceId不能硬编码,要从配置读取。我在appsettings.json里加了"Xhs": { "DeviceId": "your-device-id" },然后用Configuration["Xhs:DeviceId"]获取。
5.3 实现XHS监听器:继承ISocialListener并注入
新建XhsListener.cs,实现ISocialListener接口:
// XhsListener.cs public class XhsListener : ISocialListener { private readonly ILogger<XhsListener> _logger; private readonly string _keyword; private readonly string _deviceId; private Timer _timer; public XhsListener(ILogger<XhsListener> logger, IConfiguration config) { _logger = logger; _keyword = config["Xhs:Keyword"] ?? "SNH48"; _deviceId = config["Xhs:DeviceId"]; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation($"Starting XHS listener for keyword: {_keyword}"); _timer = new Timer(async _ => await CheckNewPostsAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(5)); } private async Task CheckNewPostsAsync() { try { var headers = new Dictionary<string, string> { ["x-s"] = GenerateXS(_deviceId), ["x-t"] = GenerateXT(), ["user-agent"] = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 xhs/8.52.0" }; var url = $"https://www.xiaohongshu.com/explore?keyword={Uri.EscapeDataString(_keyword)}&sort=time"; var response = await new HttpClient().GetAsync(url, headers); var json = await response.Content.ReadAsStringAsync(); var posts = JsonSerializer.Deserialize<XhsSearchResult>(json); foreach (var post in posts.Data) { if (await IsPostNewAsync(post.NoteId)) // 去重逻辑复用现有DB { await ProcessXhsPostAsync(post); } } } catch (Exception ex) { _logger.LogError(ex, "Failed to fetch XHS posts"); } } private async Task ProcessXhsPostAsync(XhsPost post) { // 复用现有FileHelper下载图片 foreach (var image in post.Images) { var localPath = Path.Combine("./images/archive/", $"xhs_{post.AuthorName}_{DateTime.Now:yyyyMMdd}_{Guid.NewGuid():N}.jpg"); await FileHelper.DownloadImageAsync(image.Url, localPath); await SaveToDatabaseAsync(image.Url, localPath, post.AuthorName, "xhs"); } } }5.4 注册监听器与配置启用
在StartBot.cs的RegisterListeners方法里加一行:
if (config.GetValue<bool>("SocialSources:Xhs:Enabled")) _listeners.Add(new XhsListener(_logger, config));并在appsettings.json里启用:
"SocialSources": { "Xhs": { "Enabled": true, "Keyword": "鞠婧祎", "DeviceId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } }5.5 验证与调优:从日志到数据库的端到端确认
启动后,第一件事是看日志:
- 如果出现
Starting XHS listener for keyword: 鞠婧祎,说明注册成功 - 如果出现
Failed to fetch XHS posts,检查x-s生成是否正确(可用在线MD5工具验算) - 如果出现
Downloaded image to ./images/archive/xhs_鞠婧祎_20240521_xxx.jpg,说明下载成功
终极验证:查数据库
SELECT * FROM images WHERE platform = 'xhs' ORDER BY created_at DESC LIMIT 5;应看到5条记录,local_path指向真实文件,url是小红书原始图链。此时,打开QQ群,应收到格式为【小红书】鞠婧祎 发布新笔记:今天也努力了 [图片]的通知。
从那以后我每次新增平台,都强制走一遍“抓包分析→Header生成→接口调用→去重入库→通知推送”的闭环,哪怕只是改一个参数,也要在日志里看到完整的Downloaded和Saved to DB。这套流程跑通一次,后面加抖音、知乎都只是复制模式。希望帮到你。
本文还有配套的精品资源,点击获取