2011-09-19 70 views
5

Ứng dụng của tôi yêu cầu tôi tải xuống một lượng lớn trang web vào bộ nhớ để phân tích và xử lý thêm. Cách nhanh nhất để làm điều đó là gì? Phương pháp hiện tại của tôi (được hiển thị bên dưới) có vẻ quá chậm và đôi khi dẫn đến hết thời gian chờ.Tải xuống hàng loạt các trang web C#

for (int i = 1; i<=pages; i++) 
{ 
    string page_specific_link = baseurl + "&page=" + i.ToString(); 

    try 
    {  
     WebClient client = new WebClient(); 
     var pagesource = client.DownloadString(page_specific_link); 
     client.Dispose(); 
     sourcelist.Add(pagesource); 
    } 
    catch (Exception) 
    { 
    } 
} 
+4

Bạn cần một kết nối T1 –

+2

Vì nhiều câu trả lời được gợi ý quyến rũ song song, tôi muốn cảnh báo bạn không nên gửi quá nhiều yêu cầu đồng thời; bạn có thể bị cấm nếu trang web không thân thiện. Ngoài ra sẽ có một giới hạn cho mỗi chuỗi bổ sung giúp và vượt quá một điểm nó sẽ gây ra sự xuống cấp. –

+0

@ Pandal Pandya: Một mối quan tâm hợp lệ, đó không phải là * rằng * nhiều mối quan tâm; lớp 'WebClient' cuối cùng sẽ sử dụng các lớp' HttpWebRequest'/'HttpWebResponse' sử dụng lớp' ServicePointManager'. 'ServicePointManager' theo mặc định sẽ giới hạn hầu hết các lần tải xuống hai lần mỗi lần cho một miền cụ thể (theo khuyến nghị trong đặc tả HTTP 1.1). – casperOne

Trả lời

3

Cách bạn tiếp cận vấn đề này sẽ phụ thuộc rất nhiều vào số lượng trang bạn muốn tải xuống và số lượng trang web bạn đang tham chiếu.

Tôi sẽ sử dụng số vòng tốt như 1.000. Nếu bạn muốn tải xuống nhiều trang từ một trang web, sẽ mất nhiều thời gian hơn nếu bạn muốn tải xuống 1.000 trang được trải ra trên hàng chục hoặc hàng trăm trang web. Lý do là nếu bạn nhấn một trang web duy nhất với một loạt các yêu cầu đồng thời, có thể bạn sẽ bị chặn.

Vì vậy, bạn phải triển khai một loại "chính sách lịch sự", đưa ra sự chậm trễ giữa nhiều yêu cầu trên một trang web. Độ dài của sự chậm trễ đó phụ thuộc vào một số thứ. Nếu tệp robots.txt của trang web có mục nhập crawl-delay, bạn nên tôn trọng điều đó. Nếu họ không muốn bạn truy cập nhiều hơn một trang mỗi phút, thì nhanh như bạn nên thu thập dữ liệu. Nếu không có crawl-delay, bạn nên căn cứ vào sự chậm trễ của bạn về thời gian cần một trang web để phản hồi. Ví dụ: nếu bạn có thể tải xuống một trang từ trang web trong 500 mili giây, bạn đặt độ trễ của mình thành X. Nếu mất một giây đầy đủ, hãy đặt độ trễ của bạn thành 2X. Bạn có thể giới hạn độ trễ của mình trong 60 giây (trừ khi crawl-delay dài hơn) và tôi khuyên bạn nên đặt thời gian trễ tối thiểu là 5 đến 10 giây.

Tôi sẽ không khuyên bạn sử dụng Parallel.ForEach cho việc này. Thử nghiệm của tôi đã chỉ ra rằng nó không làm tốt công việc. Đôi khi nó quá thuế kết nối và thường nó không cho phép đủ kết nối đồng thời. Tôi thay vào đó sẽ tạo ra một danh sách các WebClient trường và sau đó viết một cái gì đó như:

// Create queue of WebClient instances 
BlockingCollection<WebClient> ClientQueue = new BlockingCollection<WebClient>(); 
// Initialize queue with some number of WebClient instances 

// now process urls 
foreach (var url in urls_to_download) 
{ 
    var worker = ClientQueue.Take(); 
    worker.DownloadStringAsync(url, ...); 
} 

Khi bạn khởi tạo WebClient trường mà đi vào hàng đợi, thiết OnDownloadStringCompleted xử lý sự kiện của họ để trỏ đến một xử lý sự kiện hoàn thành. Trình xử lý đó sẽ lưu chuỗi đó vào một tệp (hoặc có lẽ bạn chỉ nên sử dụng DownloadFileAsync) và sau đó ứng dụng khách, tự thêm trở lại vào ClientQueue.

Trong thử nghiệm của mình, tôi đã có thể hỗ trợ 10 đến 15 kết nối đồng thời với phương pháp này. Hơn thế nữa và tôi gặp phải vấn đề với độ phân giải DNS (`DownloadStringAsync 'không thực hiện độ phân giải DNS một cách không đồng bộ). Bạn có thể nhận được nhiều kết nối hơn, nhưng làm như vậy là rất nhiều công việc.

Đó là phương pháp tôi đã thực hiện trong quá khứ và nó hoạt động rất tốt để tải xuống hàng nghìn trang một cách nhanh chóng. Nó chắc chắn không phải là cách tiếp cận tôi đã thực hiện với trình thu thập dữ liệu Web hiệu suất cao của tôi, mặc dù.

Tôi cũng nên lưu ý rằng có một sự khác biệt rất lớn trong sử dụng tài nguyên giữa hai khối các mã:

WebClient MyWebClient = new WebClient(); 
foreach (var url in urls_to_download) 
{ 
    MyWebClient.DownloadString(url); 
} 

--------------- 

foreach (var url in urls_to_download) 
{ 
    WebClient MyWebClient = new WebClient(); 
    MyWebClient.DownloadString(url); 
} 

Đầu tiên phân bổ một WebClient trường hợp duy nhất được sử dụng cho tất cả các yêu cầu. Thứ hai phân bổ một WebClient cho mỗi yêu cầu. Sự khác biệt là rất lớn. WebClient sử dụng rất nhiều tài nguyên hệ thống và phân bổ hàng nghìn tài nguyên trong một thời gian tương đối ngắn sẽ ảnh hưởng đến hiệu suất. Tin tôi đi ... Tôi đã chạy vào điều này. Bạn nên phân bổ chỉ 10 hoặc 20 WebClient s (nhiều như bạn cần để xử lý đồng thời), thay vì phân bổ một yêu cầu.

+0

Tôi đã đọc ở đâu đó để giải quyết thủ công các dns cho trang web và sử dụng nó cho DownloadStringAsync giúp hiệu suất. Đã từng thử Jim chưa? – paradox

+0

@paradox: Có, bạn xử lý DNS trước thời hạn để có thể nằm trong bộ nhớ cache của trình phân giải DNS của máy. Tôi làm điều gì đó khá giống với trình thu thập thông tin của mình và tôi có thể nhận được 100 kết nối mỗi giây bằng cách thực hiện. Đó là loại một nỗi đau để làm cho một ứng dụng tải đơn giản, mặc dù. Tuy nhiên, lưu ý rằng đối với một yêu cầu duy nhất, thực hiện DNS và sau đó thực hiện yêu cầu sẽ không thực thi nhanh hơn là chỉ phát hành yêu cầu. Giải quyết DNS trước thời hạn chỉ làm cho mọi thứ nhanh hơn nếu bạn có thể làm điều đó trong khi các trang khác đang được tải xuống. –

+0

những gì về việc song song foreach thực hiện theo cách này? https://stackoverflow.com/questions/46284818/parallel-request-to-scrape-multiple-pages-of-a-website – sofsntp

1

Bạn nên sử dụng lập trình song song cho mục đích này.

Có rất nhiều cách để đạt được những gì bạn muốn; dễ nhất sẽ là một cái gì đó như thế này:

var pageList = new List<string>(); 

for (int i = 1; i <= pages; i++) 
{ 
    pageList.Add(baseurl + "&page=" + i.ToString()); 
} 


// pageList is a list of urls 
Parallel.ForEach<string>(pageList, (page) => 
{ 
    try 
    { 
     WebClient client = new WebClient(); 
     var pagesource = client.DownloadString(page); 
     client.Dispose(); 
     lock (sourcelist) 
     sourcelist.Add(pagesource); 
    } 

    catch (Exception) {} 
}); 
+1

Nó cũng sai vì nó viết cho 'sourcelist' mà không đồng bộ hóa quyền truy cập vào nó. Có một cơ hội tốt mà danh sách sẽ bị hỏng như là kết quả. – casperOne

+0

hoàn toàn phù hợp với bạn;) – David

+0

'foreach' không chạy song song ngay cả khi bạn sử dụng' AsParallel'. bạn phải sử dụng 'Parallel.ForEach'. – Dani

0

I Had một trường hợp tương tự, và đó là cách tôi giải quyết

using System; 
    using System.Threading; 
    using System.Collections.Generic; 
    using System.Net; 
    using System.IO; 

namespace WebClientApp 
{ 
class MainClassApp 
{ 
    private static int requests = 0; 
    private static object requests_lock = new object(); 

    public static void Main() { 

     List<string> urls = new List<string> { "http://www.google.com", "http://www.slashdot.org"}; 
     foreach(var url in urls) { 
      ThreadPool.QueueUserWorkItem(GetUrl, url); 
     } 

     int cur_req = 0; 

     while(cur_req<urls.Count) { 

      lock(requests_lock) { 
       cur_req = requests; 
      } 

      Thread.Sleep(1000); 
     } 

     Console.WriteLine("Done"); 
    } 

private static void GetUrl(Object the_url) { 

     string url = (string)the_url; 
     WebClient client = new WebClient(); 
     Stream data = client.OpenRead (url); 

     StreamReader reader = new StreamReader(data); 
     string html = reader.ReadToEnd(); 

     /// Do something with html 
     Console.WriteLine(html); 

     lock(requests_lock) { 
      //Maybe you could add here the HTML to SourceList 
      requests++; 
     } 
    } 
} 

Bạn nên suy nghĩ sử dụng song song vì tốc độ chậm là do bạn phần mềm đang chờ cho I/O và tại sao không trong khi một chủ đề tôi chờ đợi cho I/O một số khác bắt đầu.

2

Ngoài @Davids perfectly valid answer, tôi muốn thêm "phiên bản" gọn nhẹ hơn của cách tiếp cận của anh ấy.

var pages = new List<string> { "http://bing.com", "http://stackoverflow.com" }; 
var sources = new BlockingCollection<string>(); 

Parallel.ForEach(pages, x => 
{ 
    using(var client = new WebClient()) 
    { 
     var pagesource = client.DownloadString(x); 
     sources.Add(pagesource); 
    } 
}); 

Tuy nhiên, cách tiếp cận khác, sử dụng async:

static IEnumerable<string> GetSources(List<string> pages) 
{ 
    var sources = new BlockingCollection<string>(); 
    var latch = new CountdownEvent(pages.Count); 

    foreach (var p in pages) 
    { 
     using (var wc = new WebClient()) 
     { 
      wc.DownloadStringCompleted += (x, e) => 
      { 
       sources.Add(e.Result); 
       latch.Signal(); 
      }; 

      wc.DownloadStringAsync(new Uri(p)); 
     } 
    } 

    latch.Wait(); 

    return sources; 
} 
0

Trong khi câu trả lời khác là hoàn toàn hợp lệ, tất cả trong số họ (tại thời điểm viết bài này) đều được bỏ qua một cái gì đó rất quan trọng: các cuộc gọi tới trang web là IO bound, việc chờ một luồng trên một thao tác như thế này sẽ làm căng thẳng tài nguyên hệ thống và có tác động đến tài nguyên hệ thống của bạn.

Những gì bạn thực sự muốn làm là tận dụng các phương pháp không đồng bộ trên WebClient class (như một số đã chỉ ra) cũng như khả năng xử lý của Event-Based Asynchronous Pattern.

Trước tiên, bạn sẽ nhận được các url mà bạn muốn tải về:

IEnumerable<Uri> urls = pages.Select(i => new Uri(baseurl + 
    "&page=" + i.ToString(CultureInfo.InvariantCulture))); 

Sau đó, bạn sẽ tạo một đối tượng WebClient mới cho mỗi url, sử dụng TaskCompletionSource<T> class để xử lý các cuộc gọi không đồng bộ (điều này sẽ không burn a thread):

IEnumerable<Task<Tuple<Uri, string>> tasks = urls.Select(url => { 
    // Create the task completion source. 
    var tcs = new TaskCompletionSource<Tuple<Uri, string>>(); 

    // The web client. 
    var wc = new WebClient(); 

    // Attach to the DownloadStringCompleted event. 
    client.DownloadStringCompleted += (s, e) => { 
     // Dispose of the client when done. 
     using (wc) 
     { 
      // If there is an error, set it. 
      if (e.Error != null) 
      { 
       tcs.SetException(e.Error); 
      } 
      // Otherwise, set cancelled if cancelled. 
      else if (e.Cancelled) 
      { 
       tcs.SetCanceled(); 
      } 
      else 
      { 
       // Set the result. 
       tcs.SetResult(new Tuple<string, string>(url, e.Result)); 
      } 
     } 
    }; 

    // Start the process asynchronously, don't burn a thread. 
    wc.DownloadStringAsync(url); 

    // Return the task. 
    return tcs.Task; 
}); 

Bây giờ bạn có một IEnumerable<T> mà bạn có thể chuyển đổi sang một mảng và chờ đợi trên tất cả các kết quả sử dụng Task.WaitAll:

// Materialize the tasks. 
Task<Tuple<Uri, string>> materializedTasks = tasks.ToArray(); 

// Wait for all to complete. 
Task.WaitAll(materializedTasks); 

Sau đó, bạn chỉ có thể sử dụng Result property trên Task<T> trường hợp để có được những cặp url và nội dung:

// Cycle through each of the results. 
foreach (Tuple<Uri, string> pair in materializedTasks.Select(t => t.Result)) 
{ 
    // pair.Item1 will contain the Uri. 
    // pair.Item2 will contain the content. 
} 

Lưu ý rằng đoạn mã trên có sự báo trước của việc không có một xử lý lỗi.

Nếu bạn muốn nhận được nhiều thông lượng hơn, thay vì đợi toàn bộ danh sách được hoàn tất, bạn có thể xử lý nội dung của một trang sau khi tải xong; Task<T> có nghĩa là được sử dụng như một đường ống, khi bạn đã hoàn thành đơn vị công việc của mình, hãy tiếp tục làm việc tiếp theo thay vì đợi tất cả các mục cần thực hiện (nếu chúng có thể được thực hiện theo cách không đồng bộ).

+0

Chuyển cùng một chỉnh sửa được đề xuất (bị từ chối): * Tải xuốngStringAsync không mất quá tải cho "chuỗi" - chỉ cho "Uri". * – user7116

+0

@sixlettervariables: Cảm ơn bạn đã đề xuất; sửa đổi nó để sử dụng 'Uri' toàn bộ cách thức thông qua. – casperOne

+0

Điều này trông giống như pseduocode. Bạn đang thiếu '>' ở một vài nơi. Ví dụ: tại đây => 'IEnumerable > tasks' Mã sẽ không biên dịch và một số loại sai. – Shiva

4

Tại sao không chỉ sử dụng khung thu thập dữ liệu web. Nó có thể xử lý tất cả các công cụ cho bạn như (đa luồng, httprequests, phân tích liên kết, lập kế hoạch, lịch sự, vv ..).

Bỏ phiếu (https://code.google.com/p/abot/) xử lý tất cả nội dung đó cho bạn và được viết bằng C#.

+2

Tôi đã sử dụng Abot trong một vài tháng nay và đã tìm thấy nó rất dễ mở rộng và được viết rất tốt. Nó cũng được quản lý tốt, do đó, có những cập nhật khá thường xuyên cho cơ sở mã. Bạn có tùy chọn để tinh chỉnh cách trình thu thập thông tin của bạn xuất hiện dưới dạng ứng dụng khách, tôn trọng rô bốt và tiêm các trình xử lý của riêng bạn với khả năng mở rộng trình thu thập dữ liệu khác được tạo trong các lớp khác. – jamesbar2

0

Tôi đang sử dụng một Chủ đề hoạt động đếm và giới hạn tùy ý:

private static volatile int activeThreads = 0; 

public static void RecordData() 
{ 
    var nbThreads = 10; 
    var source = db.ListOfUrls; // Thousands urls 
    var iterations = source.Length/groupSize; 
    for (int i = 0; i < iterations; i++) 
    { 
    var subList = source.Skip(groupSize* i).Take(groupSize); 
    Parallel.ForEach(subList, (item) => RecordUri(item)); 
    //I want to wait here until process further data to avoid overload 
    while (activeThreads > 30) Thread.Sleep(100); 
    } 
} 

private static async Task RecordUri(Uri uri) 
{ 
    using (WebClient wc = new WebClient()) 
    { 
     Interlocked.Increment(ref activeThreads); 
     wc.DownloadStringCompleted += (sender, e) => Interlocked.Decrement(ref iterationsCount); 
     var jsonData = ""; 
     RootObject root; 
     jsonData = await wc.DownloadStringTaskAsync(uri); 
     var root = JsonConvert.DeserializeObject<RootObject>(jsonData); 
     RecordData(root) 
    } 
} 
Các vấn đề liên quan