清理收藏夹

下面的代码是gemini生成的,基本满足了我的要求,但是其实上还是有很多无效的页面没有被去掉,比如有的网站已经出售,有的网站只有一个页面,不是当时的页面了,不过由于自己的收藏乱七八糟,还是比较有效的过滤了好几千个。

还要说明的是,用c#的这个代码,使用下面的设置,然后发布的时候,可以让最后生成的可执行文件从60多兆缩减到6兆。主要是true这行。

最后生成的清理过的文件是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; }
        }
    }
}
| 访问量:
Table of Contents