C#实现URL认证站点文件下载 追踪Meta重定向获取真实地址
.NET 6 实现类文件托管站点Meta重定向追踪与文件下载
核心问题定位
- 原链接提取逻辑错误:
Select(p => p.InnerText)获取的是按钮显示文本download,并非onclick属性内的跳转地址,需要从button的onclick属性中提取目标URL - 默认
HttpClient仅能自动处理301/302/307/308这类HTTP状态码的重定向,无法识别返回200状态码、写在HTML<meta http-equiv="refresh">标签内的元刷新重定向,这是请求pt参数链接仅能拿到页面内容的核心原因 - 站点会校验会话Cookie,首次访问分享页时站点下发的Cookie需要在后续所有请求中携带,否则pt参数会校验失败,无法进入后续跳转流程
- 链路终止判断规则:当跳转目标地址为CDN域名、且query参数中携带
download_token时,即为可直接下载的真实文件地址
实现步骤
1. 初始化HttpClient
全程复用单例HttpClient避免端口耗尽,开启自动Cookie管理,配置浏览器请求头绕过基础反爬校验:
// 全局单例HttpClient,WPF中可放在App.xaml.cs中初始化 private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler() { UseCookies = true, CookieContainer = new CookieContainer(), AllowAutoRedirect = true, // 自动处理HTTP 3xx重定向,减少手动逻辑 AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli }) { Timeout = TimeSpan.FromSeconds(30) }; // 初始化默认请求头,模拟Chrome浏览器 static App() { _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"); _httpClient.DefaultRequestHeaders.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); _httpClient.DefaultRequestHeaders.AcceptLanguage.ParseAdd("zh-CN,zh;q=0.9,en;q=0.8"); }
2. 从初始分享页提取带pt参数的跳转链接
修正原提取逻辑,从按钮的onclick属性中正则匹配跳转地址,用相对灵活的属性匹配替代容易失效的绝对XPath路径:
/// <summary> /// 从分享页提取携带pt参数的中间跳转链接 /// </summary> /// <param name="sharePageUri">初始分享页地址</param> /// <returns></returns> /// <exception cref="InvalidOperationException"></exception> private async Task<Uri> ExtractPtJumpLinkAsync(Uri sharePageUri) { var pageHtml = await _httpClient.GetStringAsync(sharePageUri); var htmlDoc = new HtmlDocument(); htmlDoc.LoadHtml(pageHtml); // 匹配下载按钮,兼容页面结构微调 var downloadBtn = htmlDoc.DocumentNode.SelectSingleNode("//button[contains(@class, 'btn--primary') and normalize-space(text())='download']"); if (downloadBtn == null) throw new InvalidOperationException("页面结构变更,未找到下载按钮"); var onclickAttr = downloadBtn.GetAttributeValue("onclick", string.Empty); // 从onclick脚本中提取window.location赋值的URL var urlMatch = Regex.Match(onclickAttr, @"window\.location\s*=\s*'([^']+)'"); if (!urlMatch.Success) throw new InvalidOperationException("下载按钮跳转规则变更,未找到pt参数链接"); return new Uri(urlMatch.Groups[1].Value, UriKind.Absolute); }
3. 实现Meta Refresh跳转链路追踪
手动解析每一跳200响应中的元刷新标签,直到命中最终CDN下载地址,设置最大跳转次数避免死循环:
/// <summary> /// 追踪包含Meta Refresh的完整跳转链路,获取真实下载地址 /// </summary> /// <param name="startUri">pt参数起始链接</param> /// <returns>携带download_token的CDN真实地址</returns> /// <exception cref="InvalidOperationException"></exception> private async Task<Uri> TraceMetaRedirectAsync(Uri startUri) { var currentUri = startUri; // 最多追踪10跳,防止死循环 for (int jumpCount = 0; jumpCount < 10; jumpCount++) { // 命中CDN地址且携带download_token,直接返回 if (currentUri.Host.EndsWith("cdnrobot.xyz") && HttpUtility.ParseQueryString(currentUri.Query)["download_token"] != null) { return currentUri; } // 请求当前地址,仅先读取响应头减少开销 using var response = await _httpClient.GetAsync(currentUri, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); // 如果响应不是HTML类型,说明已经是文件流,直接返回当前地址 if (response.Content.Headers.ContentType?.MediaType != "text/html") { return response.RequestMessage.RequestUri; } // 读取HTML内容解析Meta Refresh标签 var htmlContent = await response.Content.ReadAsStringAsync(); var metaDoc = new HtmlDocument(); metaDoc.LoadHtml(htmlContent); // 兼容http-equiv属性大小写不固定的情况 var metaRefreshNode = metaDoc.DocumentNode.SelectSingleNode("//meta[translate(@http-equiv, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')='refresh']"); if (metaRefreshNode == null) throw new InvalidOperationException($"跳转中断,第{jumpCount+1}跳未找到Meta Refresh标签"); var contentAttr = metaRefreshNode.GetAttributeValue("content", string.Empty); // 从content中提取跳转url,格式为 [秒数];url=[目标地址] var urlMatch = Regex.Match(contentAttr, @"\d+;\s*url\s*=\s*(.+)", RegexOptions.IgnoreCase); if (!urlMatch.Success) throw new InvalidOperationException($"Meta Refresh规则变更,content值:{contentAttr}"); var nextUrl = urlMatch.Groups[1].Value.Trim().Trim('\'', '"'); // 处理相对路径跳转 currentUri = new Uri(nextUrl, UriKind.RelativeOrAbsolute); if (!currentUri.IsAbsoluteUri) currentUri = new Uri(response.RequestMessage.RequestUri, currentUri); } throw new InvalidOperationException("跳转次数超过上限,链路存在死循环"); }
4. 完整调用与文件下载
WPF中直接await调用即可,不要用同步方法阻塞UI线程,大文件下载可自行扩展进度上报逻辑:
private async void StartDownloadBtn_OnClick(object sender, RoutedEventArgs e) { try { var shareLink = new Uri("https://games-database.com/1JD4"); // 第一步:提取pt中间链接 var ptLink = await ExtractPtJumpLinkAsync(shareLink); // 第二步:追踪跳转拿真实CDN地址 var realDownloadUrl = await TraceMetaRedirectAsync(ptLink); // 第三步:下载文件 var fileBytes = await _httpClient.GetByteArrayAsync(realDownloadUrl); // 提取文件名保存到桌面 var fileName = Path.GetFileName(realDownloadUrl.AbsolutePath); var savePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), fileName); await File.WriteAllBytesAsync(savePath, fileBytes); MessageBox.Show($"下载完成,文件已保存至:{savePath}"); } catch (Exception ex) { MessageBox.Show($"下载失败:{ex.Message}"); } }
适配扩展说明
- 适配1cloudfile、bowfiles等同机制站点时,仅需修改下载按钮匹配规则、CDN域名判断逻辑即可,跳转追踪核心逻辑可直接复用
- 若遇到pt参数校验失败,可在请求pt链接前给
_httpClient.DefaultRequestHeaders.Referrer赋值为初始分享页地址,部分站点会校验该字段 - 若页面结构变更导致XPath匹配失败,优先用class、文本内容等相对稳定的属性做匹配,不要用浏览器复制的绝对XPath路径
内容的提问来源于stack exchange,提问作者Poggman Friedolin
相关产品推荐
相关产品推荐

