我正在编写一个从网页上抓取数据的C#控制台应用程序。
此应用程序将转到大约8000个网页并刮取数据(每页上的数据格式相同)。
我现在没有异步方法,也没有多线程。
但是,我需要它更快。它只使用了大约3%-6%的CPU,我想是因为它花时间等待下载html。(WebClient.DownloadString(url))
这是我程序的基本流程
DataSet alldata;
foreach(var url in the8000urls)
{
// ScrapeData downloads the html from the url with WebClient.DownloadString
// and scrapes the data into several datatables which it returns as a dataset.
DataSet dataForOnePage = ScrapeData(url);
//merge each table in dataForOnePage into allData
}
// PushAllDataToSql(alldata);
我一直试图多线程,但我不确定如何正确开始。我正在使用.net 4.5而且我的理解是异步并且等待4.5以使这更容易编程,但我仍然有点迷失。
我的想法是继续制作与此行异步的新线程
DataSet dataForOnePage = ScrapeData(url);
然后当每个人完成时,运行
//merge each table in dataForOnePage into allData
任何人都可以指出我正确的方向,如何在.net 4.5 c#中使该行异步,然后让我的合并方法运行完成?
谢谢。
编辑:这是我的ScrapeData方法:
public static DataSet GetProperyData(CookieAwareWebClient webClient, string pageid)
{
var dsPageData = new DataSet();
// DOWNLOAD HTML FOR THE REO PAGE AND LOAD IT INTO AN HTMLDOCUMENT
string url = @"https://domain.com?&id=" + pageid + @"restofurl";
string html = webClient.DownloadString(url);
var doc = new HtmlDocument();
doc.LoadHtml(html );
// A BUNCH OF PARSING WITH HTMLAGILITY AND STORING IN dsPageData
return dsPageData ;
}
答案 0 :(得分:41)
如果您想使用async
和await
关键字(虽然您不必使用,但它们确实在.NET 4.5中更容易),您首先要更改{ {1}}使用ScrapeData
关键字返回Task<T>
instance的方法,如下所示:
async
请注意,您可能希望远离async Task<DataSet> ScrapeDataAsync(Uri url)
{
// Create the HttpClientHandler which will handle cookies.
var handler = new HttpClientHandler();
// Set cookies on handler.
// Await on an async call to fetch here, convert to a data
// set and return.
var client = new HttpClient(handler);
// Wait for the HttpResponseMessage.
HttpResponseMessage response = await client.GetAsync(url);
// Get the content, await on the string content.
string content = await response.Content.ReadAsStringAsync();
// Process content variable here into a data set and return.
DataSet ds = ...;
// Return the DataSet, it will return Task<DataSet>.
return ds;
}
类,因为它在异步操作中不支持WebClient
。 .NET 4.5中更好的选择是HttpClient
class。我选择使用上面的Task<T>
。另外,请查看HttpClientHandler
class,特别是您将用于向每个请求发送Cookie的CookieContainer
property。
但是,这意味着您很可能不得不使用HttpClient
关键字等待另一个异步操作,在这种情况下,很可能是下载这页纸。您必须定制下载数据的调用以使用异步版本,并使用await
。
一旦完成,您通常会在其上调用await
,但在这种情况下您无法执行此操作,因为您会对变量await
。在这种情况下,您正在运行循环,因此每次迭代都会重置变量。在这种情况下,最好将await
存储在一个数组中,如下所示:
Task<T>
将数据合并到DataSet alldata = ...;
var tasks = new List<Task<DataSet>>();
foreach(var url in the8000urls)
{
// ScrapeData downloads the html from the url with
// WebClient.DownloadString
// and scrapes the data into several datatables which
// it returns as a dataset.
tasks.Add(ScrapeDataAsync(url));
}
。为此,您要调用返回的allData
实例上的ContinueWith
method,并执行将数据添加到Task<T>
的任务:
allData
然后,您可以使用WhenAll
method和DataSet alldata = ...;
var tasks = new List<Task<DataSet>>();
foreach(var url in the8000urls)
{
// ScrapeData downloads the html from the url with
// WebClient.DownloadString
// and scrapes the data into several datatables which
// it returns as a dataset.
tasks.Add(ScrapeDataAsync(url).ContinueWith(t => {
// Lock access to the data set, since this is
// async now.
lock (allData)
{
// Add the data.
}
});
}
上的Task
class等待所有任务:
await
但请注意,您有一个// After your loop.
await Task.WhenAll(tasks);
// Process allData
,foreach
需要IEnumerable<T>
个实施。这是一个很好的指标,它适合使用LINQ,它是:
WhenAll
如果您愿意,也可以选择不使用查询语法,在这种情况下无关紧要。
请注意,如果包含方法未标记为DataSet alldata;
var tasks =
from url in the8000Urls
select ScrapeDataAsync(url).ContinueWith(t => {
// Lock access to the data set, since this is
// async now.
lock (allData)
{
// Add the data.
}
});
await Task.WhenAll(tasks);
// Process allData
(因为您在控制台应用程序中并且必须在应用程序终止之前等待结果),那么您只需调用Wait
method即可致电async
时返回Task
:
WhenAll
即,您需要将// This will block, waiting for all tasks to complete, all
// tasks will run asynchronously and when all are done, then the
// code will continue to execute.
Task.WhenAll(tasks).Wait();
// Process allData.
个实例收集到一个序列中,然后在处理Task
之前等待整个序列。
但是,如果可以的话,我建议在将数据合并到allData
之前尝试处理数据;除非数据处理需要整个 allData
,否则当您获得 时,您将获得更多的数据,从而获得更多性能提升回来,而不是等待所有回来。
答案 1 :(得分:11)
你也可以使用TPL Dataflow,这非常适合这类问题。
在这种情况下,您构建一个&#34;数据流网格&#34;然后你的数据流过它。
这个实际上更像是一个管道,而不是一个&#34;网格&#34;。我分三步:从URL下载(字符串)数据;将(字符串)数据解析为HTML,然后解析为DataSet
;并将DataSet
合并到主DataSet
。
首先,我们创建将进入网格的块:
DataSet allData;
var downloadData = new TransformBlock<string, string>(
async pageid =>
{
System.Net.WebClient webClient = null;
var url = "https://domain.com?&id=" + pageid + "restofurl";
return await webClient.DownloadStringTaskAsync(url);
},
new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded,
});
var parseHtml = new TransformBlock<string, DataSet>(
html =>
{
var dsPageData = new DataSet();
var doc = new HtmlDocument();
doc.LoadHtml(html);
// HTML Agility parsing
return dsPageData;
},
new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded,
});
var merge = new ActionBlock<DataSet>(
dataForOnePage =>
{
// merge dataForOnePage into allData
});
然后我们将三个块链接在一起以创建网格:
downloadData.LinkTo(parseHtml);
parseHtml.LinkTo(merge);
接下来,我们开始将数据泵入网格:
foreach (var pageid in the8000urls)
downloadData.Post(pageid);
最后,我们等待网格中的每一步完成(这也会干净地传播任何错误):
downloadData.Complete();
await downloadData.Completion;
parseHtml.Complete();
await parseHtml.Completion;
merge.Complete();
await merge.Completion;
TPL Dataflow的优点在于您可以轻松控制每个部分的并行。目前,我已将下载和解析块设置为Unbounded
,但您可能希望限制它们。合并块使用默认的最大并行度1,因此合并时不需要锁定。
答案 2 :(得分:1)
我建议您阅读reasonably-complete introduction to async
/await
。
首先,让所有东西都异步,从较低级别的东西开始:
public static async Task<DataSet> ScrapeDataAsync(string pageid)
{
CookieAwareWebClient webClient = ...;
var dsPageData = new DataSet();
// DOWNLOAD HTML FOR THE REO PAGE AND LOAD IT INTO AN HTMLDOCUMENT
string url = @"https://domain.com?&id=" + pageid + @"restofurl";
string html = await webClient.DownloadStringTaskAsync(url).ConfigureAwait(false);
var doc = new HtmlDocument();
doc.LoadHtml(html);
// A BUNCH OF PARSING WITH HTMLAGILITY AND STORING IN dsPageData
return dsPageData;
}
然后您可以按如下方式使用它(使用async
和LINQ):
DataSet alldata;
var tasks = the8000urls.Select(async url =>
{
var dataForOnePage = await ScrapeDataAsync(url);
//merge each table in dataForOnePage into allData
});
await Task.WhenAll(tasks);
PushAllDataToSql(alldata);
并使用我的AsyncEx库中的AsyncContext
,因为这是a console app:
class Program
{
static int Main(string[] args)
{
try
{
return AsyncContext.Run(() => MainAsync(args));
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return -1;
}
}
static async Task<int> MainAsync(string[] args)
{
...
}
}
就是这样。无需锁定或延续或任何此类。
答案 3 :(得分:-1)
我相信你这里不需要async
和await
。他们可以帮助您在需要将工作转移到非GUI线程的桌面应用程序中。在我看来,最好在你的情况下使用Parallel.ForEach
方法。像这样:
DataSet alldata;
var bag = new ConcurrentBag<DataSet>();
Parallel.ForEach(the8000urls, url =>
{
// ScrapeData downloads the html from the url with WebClient.DownloadString
// and scrapes the data into several datatables which it returns as a dataset.
DataSet dataForOnePage = ScrapeData(url);
// Add data for one page to temp bag
bag.Add(dataForOnePage);
});
//merge each table in dataForOnePage into allData from bag
PushAllDataToSql(alldata);