清理收藏夹
下面的代码是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; }
}
}
}