28

Web ページからデータをスクレイピングする C# コンソール アプリケーションを作成しています。

このアプリケーションは、約 8000 の Web ページに移動し、データをスクレイピングします (各ページのデータの形式は同じです)。

非同期メソッドもマルチスレッドも使用せずに、現在動作しています。

ただし、もっと速くする必要があります。CPU の約 3% ~ 6% しか使用していません。これは、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 を使用していますが、私の理解は async であり、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 ;
}
4

4 に答える 4

42

asyncandキーワードを使用する場合await(必須ではありませんが、.NET 4.5 ではこれらを使用すると作業が簡単になります)、まず次のように、キーワードを使用してインスタンスScrapeDataを返すようにメソッドを変更する必要があります。Task<T>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サポートされていないため、おそらくクラスから離れたいと思うことに注意してください。Task<T>.NET 4.5 でより適切な選択肢は、HttpClientclassです。上記を使用することにしましたHttpClient。また、HttpClientHandlerclass、特に各リクエストで Cookie を送信するために使用するCookieContainerプロパティを確認してください。

ただし、これは、キーワードを使用して別の非同期操作 (この場合はページのダウンロードである可能性が高い)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メソッドを呼び出し、データを に追加するタスクを実行します。Task<T>allData

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.
        }
    });
}

次に、クラスWhenAllメソッドを使用してすべてのタスクを待機できます。Taskawait

// After your loop.
await Task.WhenAll(tasks);

// Process allData

foreachただし、WhenAllがあり、IEnumerable<T>実装が必要であることに注意してください。これは、これが LINQ の使用に適していることを示す良い指標です。

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

必要に応じてクエリ構文を使用しないことも選択できますが、この場合は問題ありません。

含まれているメソッドがとしてマークされていない場合async(コンソール アプリケーションを使用していて、アプリが終了する前に結果を待つ必要があるため)、 を呼び出したときに返されたWaitメソッドを単純に呼び出すことができることに注意してください。TaskWhenAll

// 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お勧めします。データ処理に全体が必要でない限り、 すべてが返されるのを待つのではなく、返されDataSetデータをできるだけ多く処理することで、さらにパフォーマンスが向上します。

于 2012-07-24T21:16:59.930 に答える
11

この種の問題に適したTPL Dataflowを使用することもできます。

この場合、「データフロー メッシュ」を構築すると、データがその中を流れます。

これは、実際には「メッシュ」というよりもパイプラインに似ています。URL から (文字列) データをダウンロードします。(文字列) データを HTML に解析し、次にDataSet;に解析します。DataSetをmaster にマージし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
  });

次に、3 つのブロックをリンクしてメッシュを作成します。

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 を使用するため、マージ時にロックは必要ありません。

于 2012-07-25T21:23:58.313 に答える
1

/のかなり完全な紹介をasyncawait読むことをお勧めします。

まず、下位レベルのものから始めて、すべてを非同期にします。

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;
}

次に、次のように使用できます(asyncLINQで使用)。

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);

これはコンソールアプリなAsyncContextので、AsyncExライブラリから使用します。

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)
  {
    ...
  }
}

それでおしまい。ロックや継続などの必要はありません。

于 2012-07-25T20:59:52.087 に答える
-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); 
于 2012-07-25T08:51:14.420 に答える