├── .gitignore ├── img └── support.png ├── Tool ├── IHttpService.cs └── HttpService.cs ├── Properties └── PublishProfiles │ ├── win.pubxml │ └── win-selfcontained.pubxml ├── WeBook.csproj ├── README.md ├── WeBook.sln ├── Model ├── User.cs └── Blog.cs └── Program.cs /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | .vs 4 | .suo 5 | *.csproj.user 6 | *.pubxml.user 7 | /dist -------------------------------------------------------------------------------- /img/support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Uchiha-Peng/weibo2markdown/HEAD/img/support.png -------------------------------------------------------------------------------- /Tool/IHttpService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace WeBook.Tool 9 | { 10 | interface IHttpService 11 | { 12 | Task HttpRequestAsync(string url, HttpMethod method, HttpContent content = null, List> headers = null); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/win.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | ./dist 10 | FileSystem 11 | net5.0 12 | win-x64 13 | false 14 | True 15 | True 16 | true 17 | 18 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/win-selfcontained.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | ./dist 10 | FileSystem 11 | net5.0 12 | win-x64 13 | true 14 | True 15 | True 16 | true 17 | 18 | -------------------------------------------------------------------------------- /WeBook.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | embedded 10 | 11 | 12 | 13 | embedded 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Never 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 是什么? 2 | 3 | ​ **是一个用.NET Core开发的控制台应用程序,可以导出微博内容生成Markdown的工具** 4 | 5 | 6 | 7 | ## 为什么? 8 | 9 | ​ 可能很多人都想过要注销微博,也许是因为微博网络环境太喧嚣,想慢下来静静;也许是不愿再置身于信息洪流中盲目为他人的观点站队,希望能独自的思考; 也许是生活、感情有某种变故,不得不尘封往事开启新的人生篇章........过往的经历,无论美好或痛苦,和未来一样,都是此刻无法改变的。**这些都是废话,简而言之,就是一些人由于某种原因,希望注销掉微博,但是又舍不得微博上的内容。我自己曾经也有这种需求,所以就做了这个。它可以用最简洁快速的方法,导出你的微博内容,生成Markdown文件,你可以导入到语雀文档、有道云笔记等各种协同文档平台,也可以方便的转换为html文件,放在你的个人博客站点或是GitPages下,随时浏览。** 10 | 11 | 12 | 13 | ## 怎么用? 14 | 15 | 方法一:克隆代码到本地,安装.NET Core 5 Runtime,`dotnet run`即可,输入你想导出的微博用户Id即可。(如何获取微博用户id请自行百度,非常简单) 16 | 17 | 方法二:下载Release的可执行文件,支持Mac、Win、Linux。Windows下双击运行即可,Linux下请先` chmod +x`赋予读写权限 18 | 19 | 20 | ## 它有什么特点(优点、缺点) 21 | 22 | ​ **优点:** 轻量级、高效、安全、无需登录 23 | 24 | ​ **缺点:** 只能导出公开的微博内容 25 | 26 | 27 | 28 | ## FBI Warning,注意事项!!! 29 | 30 | ​ 请勿用来从事商业活动或是违法事宜....否则**后果自行承担**...... 31 | 32 | 33 | 34 | ## 支持或赞助 35 | 36 | 如果帮到你,并且你觉得很好用,请star一下,也可以用鸡付宝支给一块钱让我听听撒币的声音,有问题欢迎提出,反正我也不一定会改,哈哈...... 37 | 38 | ![](./img/support.png) 39 | 40 | 41 | ## 人生忽如寄,怜取眼前人 42 | 43 | 昨夜西风凋碧树,独上高楼,望尽天涯路。——晏殊《蝶恋花》 44 | 45 | 衣带渐宽终不悔,为伊消得人憔悴。——柳永《凤栖梧》 46 | 47 | 众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。——辛弃疾《青玉案》 -------------------------------------------------------------------------------- /WeBook.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31129.286 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeBook", "WeBook.csproj", "{8192EBF4-E0AB-41B8-B77C-F846BEB422FF}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {8192EBF4-E0AB-41B8-B77C-F846BEB422FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {8192EBF4-E0AB-41B8-B77C-F846BEB422FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {8192EBF4-E0AB-41B8-B77C-F846BEB422FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {8192EBF4-E0AB-41B8-B77C-F846BEB422FF}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {ACAF61E3-8357-42CA-9564-2B5899B794C9} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Model/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace WeBook.Model 9 | { 10 | public class User 11 | { 12 | public string Name { get; set; } 13 | public string Description { get; set; } 14 | public string Avatar { get; set; } 15 | public string Cover { get; set; } 16 | public string FollowerCount { get; set; } 17 | public string FollowCount { get; set; } 18 | public int BlogCount { get; set; } 19 | 20 | public User() 21 | { 22 | } 23 | 24 | public User(JsonElement obj) 25 | { 26 | Name = obj.GetProperty("screen_name").GetString(); 27 | Description = obj.GetProperty("description").GetString(); 28 | Avatar = obj.GetProperty("avatar_hd").GetString(); 29 | Cover = obj.GetProperty("cover_image_phone").GetString(); 30 | FollowerCount = obj.GetProperty("followers_count").GetString(); 31 | FollowCount = obj.GetProperty("followers_count_str").GetString(); 32 | BlogCount = obj.GetProperty("statuses_count").GetInt32(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tool/HttpService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace WeBook.Tool 10 | { 11 | public class HttpService : IHttpService 12 | { 13 | private readonly IHttpClientFactory _clientFactory; 14 | 15 | const string baseUrl = "https://m.weibo.cn/api/container/getIndex"; 16 | public HttpService(IHttpClientFactory clientFactory) 17 | { 18 | _clientFactory = clientFactory; 19 | } 20 | 21 | public async Task HttpRequestAsync(string url, HttpMethod method, HttpContent content = null, List> headers = null) 22 | { 23 | var request = new HttpRequestMessage(method, url); 24 | if (content != null) 25 | { 26 | request.Content = content; 27 | } 28 | if (headers != null) 29 | { 30 | foreach (var keyValue in headers) 31 | { 32 | request.Headers.Add(keyValue.Key, keyValue.Value); 33 | } 34 | } 35 | var client = _clientFactory.CreateClient(); 36 | client.BaseAddress = new Uri(baseUrl); 37 | return await client.SendAsync(request); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Model/Blog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Text.RegularExpressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace WeBook.Model 11 | { 12 | public class Blog 13 | { 14 | public long Id { get; set; } 15 | public DateTime CreateAt { get; set; } 16 | public string Source { get; set; } 17 | // 0、1、2=文本、图文、视频 18 | public int BlogType { get; set; } = 0; 19 | public int RepostCount { get; set; } 20 | public int CommentCount { get; set; } 21 | public int AttitudesCount { get; set; } 22 | public bool IsLongText { get; set; } 23 | public bool IsRepost { get; set; } = false; 24 | public String Text { get; set; } 25 | public string Video { get; set; } 26 | public List Images { get; set; } = new List(); 27 | 28 | 29 | 30 | public Blog() 31 | { 32 | } 33 | 34 | public Blog(JsonElement weibo, long id) 35 | { 36 | Id = id; 37 | CultureInfo cultureInfo = CultureInfo.CreateSpecificCulture("en-US"); 38 | string format = "ddd MMM d HH:mm:ss zz00 yyyy"; 39 | CreateAt = DateTime.ParseExact(weibo.GetProperty("created_at").GetString(), format, cultureInfo); 40 | Source = Regex.Unescape(weibo.GetProperty("source").GetString()); 41 | IsLongText = weibo.GetProperty("isLongText").GetBoolean(); 42 | RepostCount = weibo.GetProperty("reposts_count").GetInt32(); 43 | CommentCount = weibo.GetProperty("comments_count").GetInt32(); 44 | AttitudesCount = weibo.GetProperty("attitudes_count").GetInt32(); 45 | Text = weibo.GetProperty("text").GetString(); 46 | if (weibo.TryGetProperty("retweeted_status", out var isRepost)) 47 | { 48 | IsRepost = true; 49 | } 50 | if (weibo.TryGetProperty("pics", out var pics)) 51 | { 52 | foreach (var item in pics.EnumerateArray()) 53 | { 54 | if (item.TryGetProperty("large", out var large)) 55 | { 56 | var url = large.GetProperty("url"); 57 | Images.Add(Regex.Unescape(url.GetString())); 58 | } 59 | else if (item.TryGetProperty("url", out var url)) 60 | { 61 | Images.Add(Regex.Unescape(url.GetString())); 62 | } 63 | } 64 | BlogType = 1; 65 | } 66 | else 67 | { 68 | if (weibo.TryGetProperty("page_info", out var pageInfo) && !IsRepost) 69 | if (pageInfo.TryGetProperty("media_info", out var mediaInfo)) 70 | { 71 | if (pageInfo.TryGetProperty("page_pic", out var pagePic)) 72 | { 73 | if (pagePic.TryGetProperty("url", out var img)) 74 | { 75 | Images.Add(Regex.Unescape(img.GetString())); 76 | } 77 | } 78 | if (mediaInfo.TryGetProperty("stream_url", out var streamUrl)) 79 | { 80 | Video = Regex.Unescape(streamUrl.GetString()); 81 | BlogType = 2; 82 | } 83 | } 84 | } 85 | 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using WeBook.Tool; 8 | using System.Text.Json; 9 | using System.Text; 10 | using System.Text.RegularExpressions; 11 | using WeBook.Model; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.IO; 15 | 16 | namespace WeBook 17 | { 18 | public class Program 19 | { 20 | static string weiboId = string.Empty; 21 | static async Task Main(string[] args) 22 | { 23 | if (!Directory.Exists("img")) 24 | Directory.CreateDirectory("img"); 25 | using IHost host = BuildHost(args); 26 | await DoWorkAsync(host); 27 | await host.RunAsync(); 28 | } 29 | 30 | public static IHost BuildHost(string[] args) 31 | { 32 | return Host.CreateDefaultBuilder(args) 33 | .ConfigureServices((context, services) => 34 | { 35 | services.AddHttpClient(); 36 | services.AddTransient(); 37 | }) 38 | .UseConsoleLifetime() 39 | .Build(); 40 | } 41 | static async Task DoWorkAsync(IHost host) 42 | { 43 | try 44 | { 45 | PrintGod(); 46 | InputUserId(); 47 | if (string.IsNullOrWhiteSpace(weiboId)) 48 | { 49 | Console.WriteLine("\nEND:按Ctrl+C键结束程序".PadLeft(12, '*').PadRight(12, '*')); 50 | } 51 | else 52 | { 53 | 54 | var httpService = host.Services.GetRequiredService(); 55 | var url = $"?type=uid&value={weiboId}"; 56 | (var containerId, var userInfo) = await GetUserInfo(httpService, url); 57 | Console.WriteLine($"已经找到用户:{userInfo.Name} {userInfo.Description} ".PadLeft(12, '*').PadRight(12, '*')); 58 | var list = await GetWeiboList(httpService, url, containerId); 59 | await WriteBookContent(list, userInfo); 60 | Console.WriteLine("\n导出完成,推荐使用Typora打开Markdown文件,按Ctrl+C键结束程序".PadLeft(12, '*').PadRight(12, '*')); 61 | } 62 | } 63 | catch (Exception ex) 64 | { 65 | Console.WriteLine($"\n程序异常{ex.Message}"); 66 | Console.WriteLine("\nERROR:按Ctrl+C键结束程序".PadLeft(12, '*').PadRight(12, '*')); 67 | } 68 | } 69 | 70 | static async Task WriteBookContent(List list, User user) 71 | { 72 | string avatar = await DownloadImg(user.Avatar); 73 | string start = $"## 初篇·人生忽如寄,怜取眼前人\n\n**编者·含光** **编于二〇二一** *视不可见,运之不知其所触,泯然无际,经物而物不觉。*\n\n**著者·{user.Name}** **著于{list[list.Count - 1].CreateAt.Year}—{list[0].CreateAt.Year}** *{user.Description}*\n\n**著者生平** *自{list[list.Count - 1].CreateAt.ToString("yyyy年M月d日")}至{list[0].CreateAt.ToString("yyyy年M月d日")},关注{user.FollowCount}位网友,收获{user.FollowerCount}位网友关注,发布微博{user.BlogCount}篇*\n\n\n\n昨夜西风凋碧树,独上高楼,望尽天涯路。——晏殊《蝶恋花》\n\n衣带渐宽终不悔,为伊消得人憔悴。——柳永《凤栖梧》\n\n众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。——辛弃疾《青玉案》\n\n\n\n![]({avatar})\n\n\n\n"; 74 | StringBuilder sb = new StringBuilder(start); 75 | int year = 0, mounth = 0; 76 | foreach (var item in list) 77 | { 78 | if (year != item.CreateAt.Year) 79 | { 80 | sb.Append($"\n\n## {item.CreateAt.ToString("yyyy年")}\n\n"); 81 | } 82 | if (mounth != item.CreateAt.Month) 83 | { 84 | sb.Append($"\n\n### {item.CreateAt.ToString("yyyy年M月")}\n\n"); 85 | } 86 | string title = $"\n\n\n\n#### **{user.Name}**   {item.CreateAt.ToString("yyyy年M月d日 HH:mm:ss")}\n\n"; 87 | sb.Append(title); 88 | var contentType = item.BlogType == 2 ? "视频" : "图文"; 89 | var repost = item.IsRepost ? "转发" : "原创"; 90 | string blogData = $"[{repost}{contentType}] 来自:{item.Source} 获得:{item.RepostCount} 转发 {item.CommentCount} 评论 {item.AttitudesCount} 点赞\n"; 91 | sb.Append(blogData); 92 | while (item.Text.Contains("<") && item.Text.Contains(">")) 93 | { 94 | int beg = item.Text.IndexOf("<"); 95 | int ed = item.Text.IndexOf(">"); 96 | item.Text = item.Text.Remove(beg, ed - beg + 1); 97 | } 98 | if (string.IsNullOrEmpty(item.Video)) 99 | sb.Append($"\n{item.Text}"); 100 | else 101 | sb.Append($"\n[{item.Text}]({item.Video})"); 102 | if (item.Images.Count > 0) 103 | { 104 | foreach (var img in item.Images) 105 | { 106 | if (!string.IsNullOrWhiteSpace(img)) 107 | { 108 | var url = await DownloadImg(img); 109 | sb.Append($"\n\n![]({url})"); 110 | } 111 | } 112 | } 113 | year = item.CreateAt.Year; 114 | mounth = item.CreateAt.Month; 115 | } 116 | 117 | string cover = await DownloadImg(user.Cover); 118 | string end = $"\n\n\n\n## 终章·从前过往,皆为序章\n\n莫道光阴慢,流年忽已远。\n\n\n\n![]({cover})\n\n\n\n"; 119 | sb.Append(end); 120 | using var outputFile = new StreamWriter($"./{user.Name} WeBook-By含光.md"); 121 | await outputFile.WriteAsync(sb.ToString()); 122 | } 123 | 124 | static async Task> GetWeiboList(IHttpService httpService, string url, string containerId) 125 | { 126 | var blogList = new List(); 127 | int page = 1; 128 | long i = 1; 129 | bool doAgain = true; 130 | while (doAgain) 131 | { 132 | try 133 | { 134 | 135 | var weiboObj = await GetJsonObjectAsync(httpService, $"{url}&containerid={containerId}&page={page}"); 136 | Console.WriteLine($"{url}&containerid={containerId}&page={page}"); 137 | doAgain = weiboObj.GetProperty("ok").GetInt32() == 1; 138 | if (doAgain) 139 | { 140 | var cards = weiboObj.GetProperty("data").GetProperty("cards"); 141 | foreach (var item in cards.EnumerateArray()) 142 | { 143 | var blogItem = new Blog(item.GetProperty("mblog"), i); 144 | blogList.Add(blogItem); 145 | i++; 146 | } 147 | } 148 | } 149 | catch (Exception) 150 | { 151 | Console.WriteLine($"第{page}页出现异常,已经获取到{i}条数据"); 152 | } 153 | page++; 154 | } 155 | Console.WriteLine($"累计抓取{i}条微博数据"); 156 | return blogList; 157 | } 158 | 159 | static async Task<(string, User)> GetUserInfo(IHttpService httpService, string url) 160 | { 161 | var weiboUserInfo = await GetJsonObjectAsync(httpService, url); 162 | string containerId = weiboUserInfo.GetProperty("data").GetProperty("tabsInfo").GetProperty("tabs")[1].GetProperty("containerid").GetString(); 163 | return (containerId, new User(weiboUserInfo.GetProperty("data").GetProperty("userInfo"))); 164 | } 165 | 166 | static void InputUserId() 167 | { 168 | var tips = "请勿用于商业或非法用途,输入微博ID,如:223417235(非微博昵称),Enter键确认".PadLeft(12, '*').PadRight(12, '*'); 169 | Console.WriteLine(tips); 170 | int total = 5; 171 | while (total > 0) 172 | { 173 | Console.Write(":"); 174 | weiboId = Console.ReadLine(); 175 | total--; 176 | if (string.IsNullOrWhiteSpace(weiboId)) 177 | { 178 | Console.WriteLine($"输入有误:还有{total}次重试机会"); 179 | continue; 180 | } 181 | else 182 | { 183 | break; 184 | } 185 | } 186 | } 187 | static async Task GetJsonObjectAsync(IHttpService httpService, string url) 188 | { 189 | Console.WriteLine(url); 190 | JsonElement result = new JsonElement(); 191 | var responseMessage = await httpService.HttpRequestAsync(url, HttpMethod.Get); 192 | if (responseMessage.IsSuccessStatusCode) 193 | { 194 | string json = await responseMessage.Content.ReadAsStringAsync(); 195 | //Regex.Unescape(); 196 | result = JsonSerializer.Deserialize(json); 197 | } 198 | return result; 199 | } 200 | 201 | static async Task DownloadImg(string url) 202 | { 203 | // 设置要下载的图片的URL 204 | string imageUrl = $"https://image.baidu.com/search/down?url={url}"; 205 | var imageSrc = $"img/{url.Split("/").Last()}"; 206 | if (File.Exists(imageSrc)) 207 | return imageSrc; 208 | Console.WriteLine($"正在下载图片{url}"); 209 | using var httpClient = new HttpClient(); 210 | try 211 | { 212 | HttpResponseMessage response = await httpClient.GetAsync(imageUrl); 213 | 214 | if (response.IsSuccessStatusCode) 215 | { 216 | byte[] imageBytes = await response.Content.ReadAsByteArrayAsync(); 217 | File.WriteAllBytes(imageSrc, imageBytes); 218 | return imageSrc; 219 | } 220 | } 221 | catch (Exception ex) 222 | { 223 | Console.WriteLine("下载异常: " + ex.Message); 224 | } 225 | return url; 226 | } 227 | 228 | #region 229 | static void PrintGod() 230 | { 231 | Console.WriteLine("||=========================================================================||\n|| _oo8oo_ ||\n|| o8888888o ||\n|| 88\" . \"88 ||\n|| (| -_- |) ||\n|| 0\\ = /0 ||\n|| ___/'==='\\___ ||\n|| .' \\\\| |// '. ||\n|| / \\\\||| : |||// \\ ||\n|| / _||||| -:- |||||_ \\ ||\n|| | | \\\\\\ - /// | | ||\n|| | \\_| ''\\---/'' |_/ | ||\n|| \\ .-\\__ '-' __/-. / ||\n|| ___'. .' /--.--\\ '. .'___ ||\n|| .\"\" '< '.___\\_<|>_/___.' >' \"\". ||\n|| | | : `- \\`.:`\\ _ /`:.`/ -` : | | ||\n|| \\ \\ `-. \\_ __\\ /__ _/ .-` / / ||\n|| =====`-.____`.___ \\_____/ ___.`____.-`===== ||\n|| `= ---=` ||\n||=========================================================================||\n"); 232 | } 233 | #endregion 234 | } 235 | } 236 | --------------------------------------------------------------------------------