您的目标网页使用 SSRS ReportViewer 控件来管理报表的呈现,该控件严重依赖 ASP.Net Session State 通过调用 @987654327 在后台呈现报表@资源处理程序。
这意味着要使用您已识别的这个axd 链接,您必须首先触发要在会话上下文中创建和缓存的内容,然后才能下载它,然后您必须从相同的地方下载它 上下文。
- 我们不能只运行一次页面并找出 URL,我们必须找到一种方法,使用请求之间的相同会话以编程方式执行此操作。
当单击下载按钮时,ReportViewer 控件通过 javascript 执行此操作,这意味着没有指向 Reserved.ReportViewerWebControl.axd 的简单链接可以从 html 中抓取。
这意味着我们必须手动执行相同的脚本或模拟用户点击链接。
此解决方案将使用一些屏幕抓取技术(UX 自动化)来模拟单击导出按钮并捕获结果,但如果可以的话,我会避免这种情况。
您确实应该尝试直接联系开发人员寻求指导,他们可能已经实现了一些简单的 URL 参数来直接导出,而无需自动化界面。
概念比较简单:
- 创建到报告页面的 Web 浏览器会话
- 单击导出为 CSV 按钮
- 这将尝试在新窗口中打开另一个链接,我们需要禁止该链接!
- 从新窗口抓取网址
- 使用相同的会话上下文下载导出文件
- 我们不能为此使用 Web 浏览器控件,因为它的界面是 UI 驱动的。
我们不能使用HttpWebRequest 或WebClient 来针对HTMl DOM 执行javascript,我们必须使用Web 浏览器来实现这一点。
出现的另一个问题是我们不能简单地在控件上使用 WebBrowser NewWindow 或 FileDownload 事件,因为这些事件不提供新窗口的 URL 或文件下载源或目标等信息。
相反,我们必须引用内部 COM 浏览器(实际上是 IE)并使用本机 NewWindow3 事件来捕获到 Reserved.ReportViewerWebControl.axd 的 url,以便我们可以手动下载它。
我使用这些主要参考资料来解释这项技术
最后,正如我上面提到的,我们不能使用 Web 浏览器直接从 URL 下载文件,因为它会在新的 Web 浏览器中弹出 SAVE AS 对话框或直接保存到配置的下载文件夹。
如参考文章中所述,我们使用 Erika Chinchio 的 GetGlobalCookies 方法,该方法可在 @Pedro Leonardo 提供的优秀文章中找到(here)
我已将所有这些都放入一个简单的控制台应用程序中,您可以运行它,只需更改报告的 url、导出链接的标题和保存路径:
以下是我如何获得我要下载的链接,具体的链接标题和组成会因实现而异:
class Program
{
[STAThread]
static void Main(string[] args)
{
SaveReportToDisk("http://localhost:13933/reports/sqlversioninfo", "CSV (comma delimited)", "C:\\temp\\reportDump.csv");
}
/// <summary>
/// Automate clicking on the 'Save As' drop down menu in a report viewer control embedded at the specified URL
/// </summary>
/// <param name="sourceURL">URL that the report viewer control is hosted on</param>
/// <param name="linkTitle">Title of the export option that you want to automate</param>
/// <param name="savepath">The local path to save to exported report to</param>
static void SaveReportToDisk(string sourceURL, string linkTitle, string savepath)
{
WebBrowser wb = new WebBrowser();
wb.ScrollBarsEnabled = false;
wb.ScriptErrorsSuppressed = true;
wb.Navigate(sourceURL);
//wait for the page to load
while (wb.ReadyState != WebBrowserReadyState.Complete) { Application.DoEvents(); }
// We want to find the Link that is the export to CSV menu item and click it
// this is the first link on the page that has a title='CSV', modify this search if your link is different.
// TODO: modify this selection mechanism to suit your needs, the following is very crude
var exportLink = wb.Document.GetElementsByTagName("a")
.OfType<HtmlElement>()
.FirstOrDefault(x => (x.GetAttribute("title")?.Equals(linkTitle, StringComparison.OrdinalIgnoreCase)).GetValueOrDefault());
if (exportLink == null)
throw new NotSupportedException("Url did not resolve to a valid Report Viewer web Document");
bool fileDownloaded = false;
// listen for new window, using the COM wrapper so we can capture the url
(wb.ActiveXInstance as SHDocVw.WebBrowser).NewWindow3 +=
(ref object ppDisp, ref bool Cancel, uint dwFlags, string bstrUrlContext, string bstrUrl) =>
{
Cancel = true; //should block the default browser from opening the link in a new window
Task.Run(async () =>
{
await DownloadLinkAsync(bstrUrl, savepath);
fileDownloaded = true;
}).Wait();
};
// execute the link
exportLink.InvokeMember("click");
//wait for the page to refresh
while (!fileDownloaded) { Application.DoEvents(); }
}
private static async Task DownloadLinkAsync(string documentLinkUrl, string savePath)
{
var documentLinkUri = new Uri(documentLinkUrl);
var cookieString = GetGlobalCookies(documentLinkUri.AbsoluteUri);
var cookieContainer = new CookieContainer();
using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
using (var client = new HttpClient(handler) { BaseAddress = documentLinkUri })
{
cookieContainer.SetCookies(documentLinkUri, cookieString);
var response = await client.GetAsync(documentLinkUrl);
if (response.IsSuccessStatusCode)
{
var stream = await response.Content.ReadAsStreamAsync();
// Response can be saved from Stream
using (Stream output = File.OpenWrite(savePath))
{
stream.CopyTo(output);
}
}
}
}
// from Erika Chinchio which can be found in the excellent article provided by @Pedro Leonardo (available here: http://www.codeproject.com/Tips/659004/Download-of-file-with-open-save-dialog-box),
[System.Runtime.InteropServices.DllImport("wininet.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)]
static extern bool InternetGetCookieEx(string pchURL, string pchCookieName,
System.Text.StringBuilder pchCookieData, ref uint pcchCookieData, int dwFlags, IntPtr lpReserved);
const int INTERNET_COOKIE_HTTPONLY = 0x00002000;
private static string GetGlobalCookies(string uri)
{
uint uiDataSize = 2048;
var sbCookieData = new System.Text.StringBuilder((int)uiDataSize);
if (InternetGetCookieEx(uri, null, sbCookieData, ref uiDataSize,
INTERNET_COOKIE_HTTPONLY, IntPtr.Zero)
&&
sbCookieData.Length > 0)
{
return sbCookieData.ToString().Replace(";", ",");
}
return null;
}
}
我建议在进入屏幕抓取兔子洞之前与开发人员交谈的原因是,作为标准,当我使用报表查看器控件时,我总是尝试实现 SSRS native rc: and rs: URL parameters 或至少确保我提供了一种方法直接通过 url 导出报告。
您不能直接使用这些参数,它们被设计为在您直接查询 SSRS 服务器时使用,而您的示例没有这样做。
我没有自己想出这个,不知道我是从哪个资源中学到的,但这意味着其他人有机会得出类似的结论。我主要实现了这一点,因此我可以在整个应用程序的其余部分中使用这些概念。但是,当涉及到报表时,我们选择 SSRS 和 RDL 作为报表解决方案的原因之一是它的多功能性,我们编写报表定义,控件允许用户根据需要使用它们。如果我们仅限于用户导出报告的能力,那么我们确实没有充分利用该框架。