1819 字
9 分钟
清理收藏夹
下面的代码是gemini生成的,基本满足了我的要求,但是其实上还是有很多无效的页面没有被去掉,比如有的网站已经出售,有的网站只有一个页面,不是当时的页面了,不过由于自己的收藏乱七八糟,还是比较有效的过滤了好几千个。
还要说明的是,用c#的这个代码,使用下面的设置,然后发布的时候,可以让最后生成的可执行文件从60多兆缩减到6兆。主要是
最后生成的清理过的文件是html文件,需要将当前的收藏夹清空,然后再导入这个html文件就好了。
如果不喜欢使用收藏夹,也有可以直接将这个文件作为页面来看待。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols>
</PropertyGroup>
</Project>下面是Program.cs代码:
using System;using System.Collections.Concurrent;using System.Collections.Generic;using System.IO;using System.Linq;using System.Net.Http;using System.Text;using System.Text.Json;using System.Text.Json.Nodes;using System.Threading;using System.Threading.Tasks;
namespace BookmarkCleaner{ class Program { // 配置:并发线程数 private const int MaxConcurrency = 50; // 配置:超时时间(秒) private const int TimeoutSeconds = 10;
// HTTP 客户端 (单例) private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = true, // 允许重定向 CheckCertificateRevocationList = false, ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true // 忽略SSL错误(很多旧网站证书过期但内容还在) }) { Timeout = TimeSpan.FromSeconds(TimeoutSeconds) };
static async Task Main(string[] args) { Console.OutputEncoding = Encoding.UTF8; _httpClient.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");
Console.WriteLine("=========================================="); Console.WriteLine(" Chrome/Edge 收藏夹强力清理工具"); Console.WriteLine("==========================================");
// 1. 自动寻找路径 var bookmarkPaths = GetBookmarkPaths(); if (bookmarkPaths.Count == 0) { Console.WriteLine("未找到 Chrome 或 Edge 的收藏夹文件。"); return; }
Console.WriteLine("检测到以下收藏夹文件:"); for (int i = 0; i < bookmarkPaths.Count; i++) { Console.WriteLine($"{i + 1}. {bookmarkPaths[i].Browser}: {bookmarkPaths[i].Path}"); }
Console.Write("\n请输入要处理的序号 (例如 1): "); if (!int.TryParse(Console.ReadLine(), out int choice) || choice < 1 || choice > bookmarkPaths.Count) { Console.WriteLine("输入无效。"); return; }
var selected = bookmarkPaths[choice - 1];
// 2. 读取并解析 JSON Console.WriteLine($"\n正在读取文件: {selected.Path}..."); string jsonString = await File.ReadAllTextAsync(selected.Path);
// 使用 System.Text.Json 解析为可变的 JsonNode var rootNode = JsonNode.Parse(jsonString); var roots = rootNode?["roots"]?.AsObject();
if (roots == null) { Console.WriteLine("无法解析收藏夹结构。"); return; }
// 3. 收集所有 URL 节点 var allUrlNodes = new List<UrlItem>(); CollectUrls(roots, allUrlNodes); Console.WriteLine($"共发现 {allUrlNodes.Count} 个书签。准备开始检测...");
// 4. 多线程检测 var processedCount = 0; var invalidCount = 0; var lockObj = new object();
Console.WriteLine($"\n启动 {MaxConcurrency} 线程进行极速检测...\n");
var options = new ParallelOptions { MaxDegreeOfParallelism = MaxConcurrency }; await Parallel.ForEachAsync(allUrlNodes, options, async (item, token) => { bool isValid = await CheckUrlAsync(item.Url);
lock (lockObj) { processedCount++; if (!isValid) { invalidCount++; item.IsValid = false; // 标记为无效 // 实时打印失败的链接(可选) // Console.WriteLine($"[无效] {item.Url}"); }
// 简单的进度条 if (processedCount % 10 == 0 || processedCount == allUrlNodes.Count) { Console.Write($"\r进度: {processedCount}/{allUrlNodes.Count} | 已发现无效: {invalidCount} "); } } });
Console.WriteLine($"\n\n检测完成!共删除 {invalidCount} 个无效链接。");
// 5. 从 JSON 树中移除无效节点 Console.WriteLine("正在重组收藏夹结构..."); RemoveInvalidNodes(roots, allUrlNodes);
// 6. 导出为 HTML string exportFileName = $"{selected.Browser}_Cleaned_{DateTime.Now:yyyyMMdd_HHmmss}.html"; string exportPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), exportFileName);
Console.WriteLine("正在生成 HTML 文件..."); ExportToHtml(roots, exportPath);
Console.WriteLine("=========================================="); Console.WriteLine($"✅ 成功!文件已导出到桌面:"); Console.WriteLine(exportPath); Console.WriteLine("您可以打开浏览器 -> 收藏夹管理 -> 导入书签,选择此文件。"); Console.WriteLine("=========================================="); Console.ReadKey(); }
// ---------------- 核心逻辑方法 ----------------
// 检查 URL 有效性 static async Task<bool> CheckUrlAsync(string url) { // 过滤非 HTTP 协议(如 javascript:, file:, chrome:) if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return true;
try { // 先尝试 HEAD 请求 (省流量) var request = new HttpRequestMessage(HttpMethod.Head, url); using var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode) return true;
// 如果 HEAD 失败 (有些服务器不支持),再尝试 GET using var getResponse = await _httpClient.GetAsync(url);
// 检查状态码 if (!getResponse.IsSuccessStatusCode) return false;
// 检查内容长度 (如果连接成功但内容为空) // 注意:有些动态网站 Content-Length 可能为 null,需谨慎判断 if (getResponse.Content.Headers.ContentLength.HasValue && getResponse.Content.Headers.ContentLength < 50) { // 这里是个简单的启发式判断:如果页面小于 50 字节,可能是有问题的 return false; }
return true; } catch { return false; // 超时、DNS解析失败、连接被拒绝 } }
// 递归收集 URL static void CollectUrls(JsonObject node, List<UrlItem> collection) { foreach (var property in node) { var childNode = property.Value; if (childNode is JsonObject obj) { // 检查是否是文件夹还是具体的 URL 条目 if (obj.ContainsKey("type") && obj["type"]?.GetValue<string>() == "url") { string url = obj["url"]?.GetValue<string>() ?? ""; string id = obj["id"]?.GetValue<string>() ?? Guid.NewGuid().ToString();
if (!string.IsNullOrEmpty(url)) { collection.Add(new UrlItem { Url = url, JsonRef = obj, IsValid = true, Id = id }); } } // 如果有 children 属性,说明是文件夹,继续递归 else if (obj.ContainsKey("children") && obj["children"] is JsonArray children) { foreach (var child in children) { if (child is JsonObject childObj) { // 这里需要构造一个临时的父节点结构来递归,或者直接针对 children 数组递归 // 由于 JsonNode API 的特性,我们直接对数组里的对象递归即可 // 但为了复用 CollectUrls 方法,我们需要判断 CollectUrlsRecursion(childObj, collection); } } } else { // 继续遍历其他可能的嵌套 CollectUrls(obj, collection); } } } }
static void CollectUrlsRecursion(JsonObject obj, List<UrlItem> collection) { if (obj.ContainsKey("type") && obj["type"]?.GetValue<string>() == "url") { string url = obj["url"]?.GetValue<string>() ?? ""; if (!string.IsNullOrEmpty(url)) { collection.Add(new UrlItem { Url = url, JsonRef = obj, IsValid = true }); } } else if (obj.ContainsKey("children") && obj["children"] is JsonArray children) { foreach (var child in children) { if (child is JsonObject childObj) CollectUrlsRecursion(childObj, collection); } } }
// 移除无效节点 static void RemoveInvalidNodes(JsonObject roots, List<UrlItem> items) { // 建立一个待删除的 Set var invalidItems = items.Where(x => !x.IsValid).ToHashSet();
// 定义递归移除函数 void Prune(JsonNode node) { if (node is JsonObject obj && obj.ContainsKey("children") && obj["children"] is JsonArray children) { // 倒序遍历以便删除 for (int i = children.Count - 1; i >= 0; i--) { var child = children[i]; if (child is JsonObject childObj) { // 如果是 URL 节点 if (childObj["type"]?.GetValue<string>() == "url") { // 检查这个节点是否在无效列表中 // 这里我们通过对象引用的相等性来判断,或者比对 URL // 由于我们在 Collect 时保存了 JsonRef,直接比对引用最准确 var isInvalid = invalidItems.Any(x => x.JsonRef == childObj); if (isInvalid) { children.RemoveAt(i); } } else { // 是文件夹,递归进去 Prune(childObj); } } } }
// 遍历 roots 下的顶层节点 (bookmark_bar, other, synced) if (node == roots) { foreach (var kvp in roots) { if (kvp.Value is JsonObject rootChild) Prune(rootChild); } } }
Prune(roots); }
// 导出为 HTML (Netscape Bookmark Format) static void ExportToHtml(JsonObject roots, string outputPath) { var sb = new StringBuilder(); sb.AppendLine("<!DOCTYPE NETSCAPE-Bookmark-file-1>"); sb.AppendLine(""); sb.AppendLine("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">"); sb.AppendLine("<TITLE>Bookmarks</TITLE>"); sb.AppendLine("<H1>Bookmarks</H1>"); sb.AppendLine("<DL><p>");
// 定义递归写入 HTML void WriteNode(JsonNode node) { if (node is JsonObject obj) { string type = obj["type"]?.GetValue<string>(); string name = obj["name"]?.GetValue<string>() ?? "Untitled";
if (type == "url") { string url = obj["url"]?.GetValue<string>() ?? "#"; sb.AppendLine($"<DT><A HREF=\"{url}\">{System.Net.WebUtility.HtmlEncode(name)}</A>"); } else // 文件夹 { // 根节点处理 (bookmark_bar 等) 没有 type 属性,或者 type 是 folder // 或者是 roots 的直接子节点 sb.AppendLine($"<DT><H3>{System.Net.WebUtility.HtmlEncode(name)}</H3>"); sb.AppendLine("<DL><p>");
if (obj["children"] is JsonArray children) { foreach (var child in children) WriteNode(child); }
sb.AppendLine("</DL><p>"); } } }
// 处理主要的几个根目录 var folders = new[] { "bookmark_bar", "other", "synced" }; foreach (var folderKey in folders) { if (roots.ContainsKey(folderKey) && roots[folderKey] is JsonObject folderNode) { // 顶层文件夹通常不想显示 "bookmark_bar" 这种名字,可以做个映射,也可以直接递归 // 为了结构好看,我们直接递归其 children,把它们放在最外层,或者保留文件夹结构 // 这里选择保留文件夹结构 WriteNode(folderNode); } }
sb.AppendLine("</DL><p>"); File.WriteAllText(outputPath, sb.ToString()); }
static List<(string Browser, string Path)> GetBookmarkPaths() { var list = new List<(string, string)>(); var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
// Chrome string chromePath = Path.Combine(localAppData, @"Google\Chrome\User Data\Default\Bookmarks"); if (File.Exists(chromePath)) list.Add(("Google Chrome", chromePath));
// Edge string edgePath = Path.Combine(localAppData, @"Microsoft\Edge\User Data\Default\Bookmarks"); if (File.Exists(edgePath)) list.Add(("Microsoft Edge", edgePath));
return list; }
class UrlItem { public string Id { get; set; } public string Url { get; set; } public JsonObject JsonRef { get; set; } // 保持对原始 JSON 对象的引用以便删除 public bool IsValid { get; set; } } }}