1

I am working on an app that searches for email addresses in Google search results' URLs. The problem is it needs to return the value it found in each page + the URL in which it found the email, to a datagridview with 2 columns: Email and URL. I am using Parallel.ForEach for this one but of course it returns random URLs and not the ones it really found the email on.

public static string htmlcon;  //htmlsource

public static List<string> emailList = new List<string>();

public static string Get(string url, bool proxy)
    {
        htmlcon = "";

        try
        {
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
            if (proxy)
                req.Proxy = new WebProxy(proxyIP + ":" + proxyPort);
            req.Method = "GET";
            req.UserAgent = Settings1.Default.UserAgent;
            if (Settings1.Default.EnableCookies == true)
            {
                CookieContainer cont = new CookieContainer();
                req.CookieContainer = cont;
            }
            WebResponse resp = req.GetResponse();
            StreamReader SR = new StreamReader(resp.GetResponseStream());
            htmlcon = SR.ReadToEnd();

            Thread.Sleep(400);
            resp.Close();
            SR.Close();
        }
        catch (Exception)
        {
            Thread.Sleep(500);
        }

        return htmlcon;

    }



  private void copyMails(string url)
    {    
        string emailPat = @"(\b[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}\b)";
        MatchCollection mailcol = Regex.Matches(htmlcon, emailPat, RegexOptions.Singleline);
        foreach (Match mailMatch in mailcol)
        {
            email = mailMatch.Groups[1].Value;
            if (!emailList.Contains(email))
            {
                emailList.Add(email);



                  Action dgeins = () => mailDataGrid.Rows.Insert(0, email, url);
                  mailDataGrid.BeginInvoke(dgeins);

             }
        }
     }

  private void SEbgWorker_DoWork(object sender, DoWorkEventArgs e)
 {
     //ALOT OF IRRELEVAMT STUFF BEING RUN

     Parallel.ForEach(allSElist.OfType<string>(), (s) =>
        {
            //Get URL
            Get(s, Settings1.Default.Proxyset);


            //match mails 1st page 
            copyMails(s);
            });

 }

so this is it: I execute a Get request(where "s" is the URL from the list) and then execute copyMails(s) from the URL's html source. It uses regex to copy the emails. If I do it without parallel it returns the correct URL for each email in the datagridview. How can I do this parallel an still get the correct match in the datagridview?

Thanks

4

2 回答 2

7

您最好使用 PLINQWhere进行过滤(伪代码):

var results = from i in input.AsParallel()
              let u = get the URL from i
              let d = get the data from u
              let v = try get the value from d
              where v is found
              select new {
                Url = u,
                Value = v
              };

AsParallel意味着使用了 TPL 的LINQ 运算符( Select, Where, ...) 实现。


更新:现在有更多信息

首先,您的代码中有许多问题:

  1. 该变量htmlconstatic多个线程直接使用。这很可能是您的根本问题。只考虑两个输入值。第一个Get完成设置htmlcon,在该线程的调用copyMails开始之前,第二个线程Get完成其 HTML GET 并写入htmlcon。用`电子邮件

  2. 该列表emailList也可以在不被多个线程锁定的情况下访问。.NET(和任何其他编程平台)中的大多数集合类型都不是线程安全的,您需要一次限制对单个线程的访问。

  3. 您在每种方法中混合了各种活动。考虑应用单一责任原则。

  4. Thread.Sleep处理异常?!如果您无法处理异常(即解决条件),则什么也不做。在这种情况下,如果动作抛出然后Parallel.Foreach将抛出:直到你定义如何处理 HTML GET 失败。

三个建议:

  1. 以我的经验,干净的代码(在某种程度上)使事情变得更容易:格式的细节并不重要(真正的大括号样式更好,但一致性是关键)。只需检查并清理格式就会出现问题 #1 和 #2。

  2. 好命名。不要缩写超过几行代码使用的任何内容,除非这是该领域的重要术语。例如。s因为并行循环中的 action 参数实际上是一个 url,所以就这么称呼它。这种事情立即使代码更容易理解。

  3. 想想电子邮件的正则表达式:有许多有效的电子邮件不匹配(例如,使用+提供多个逻辑地址:exmaple+one@gamil.com将被传递到example@gmail.com本地规则,然后可以用于本地规则)。此外,撇号 (" '") 是一个有效字符(并且已知的人因网站因错误而拒绝其地址而感到沮丧)。

:比较直接的清理:

public static string Get(string url, bool proxy) {

    HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
    if (proxy) {
        req.Proxy = new WebProxy(proxyIP + ":" + proxyPort);
    }
    req.Method = "GET";
    req.UserAgent = Settings1.Default.UserAgent;
    if (Settings1.Default.EnableCookies == true) {
        CookieContainer cont = new CookieContainer();
        req.CookieContainer = cont;
    }
    using (WebResponse resp = req.GetResponse())
    using (StreamReader SR = new StreamReader(resp.GetResponseStream())) {
        return SR.ReadToEnd();
    }

}

private static Regex emailMatcher = new Regex(@"(\b[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}\b)", RegexOptions.Singleline);

private static string[] ExtractEmails(string htmlContent) {    

    return emailMatcher.Matches(htmlContent).OfType<Match>
                       .Select(m => m.Groups[1].Value)
                       .Distinct()
                       .ToArray();
 }

private void SEbgWorker_DoWork(object sender, DoWorkEventArgs e) {

    Parallel.ForEach(allSElist.OfType<string>(), url => {
        var htmlContent = Get(url, Settings1.Default.Proxyset);
        var emails = ExtractEmails(htmlContent);

        foreach (var email in emails) {
            Action dgeins = () => mailDataGrid.Rows.Insert(0, email, url);
            mailDataGrid.BeginInvoke(dgeins);
        }
}

在这里我有:

  • 使用using语句来自动清理资源。
  • 消除了所有可变的共享状态。
  • Regex被明确记录为具有线程安全的实例方法。所以我只需要一个实例。
  • 消除噪音:无需将 URL 传递给,ExtractEmails因为提取不使用 URL。
  • Get现在只执行 HTML 获取,ExtreactEMail只是提取

第三:上面将阻塞线程上最慢的操作:HTML GET。

真正的并发好处是HttpWebRequest.GetResponse用它们的异步等价物替换和读取响应流。

使用Task将是 .NET 4 中的答案,但您需要直接使用Stream和编码自己,因为StreamReader不提供任何BeginABC/EndABC方法对。但是 .NET 4.5 快到了,所以应用一些async/ await

  • 中无事可做ExtractEMails
  • Get现在是异步的,既不阻塞 HTTP GET 也不读取结果。
  • SEbgWorker_DoWork直接使用任务来避免混合太多不同的方式来使用 TPL。由于Get返回一个Task<string>可以简单的继续(当它没有失败时 - 除非您另有说明,否则ContinueWith只有在前一个任务成功完成时才会继续):

这应该可以在 .NET 4.5 中使用,但是如果没有一组有效的 URL,我无法对其进行测试。

public static async Task<string> Get(string url, bool proxy) {

    HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
    if (proxy) {
        req.Proxy = new WebProxy(proxyIP + ":" + proxyPort);
    }
    req.Method = "GET";
    req.UserAgent = Settings1.Default.UserAgent;
    if (Settings1.Default.EnableCookies == true) {
        CookieContainer cont = new CookieContainer();
        req.CookieContainer = cont;
    }
    using (WebResponse resp = await req.GetResponseAsync())
    using (StreamReader SR = new StreamReader(resp.GetResponseStream())) {
        return await SR.ReadToEndAsync();
    }

}

private static Regex emailMatcher = new Regex(@"(\b[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}\b)", RegexOptions.Singleline);

private static string[] ExtractEmails(string htmlContent) {    

    return emailMatcher.Matches(htmlContent).OfType<Match>
                       .Select(m => m.Groups[1].Value)
                       .Distinct()
                       .ToArray();
 }

private void SEbgWorker_DoWork(object sender, DoWorkEventArgs e) {

    tasks = allSElist.OfType<string>()
                     .Select(url => {
        return Get(url, Settings1.Default.Proxyset)
                .ContinueWith(htmlContentTask => {
                    // No TaskContinuationOptions, so know always OK here
                    var htmlContent = htmlContentTask.Result;
                    var emails = ExtractEmails(htmlContent);
                    foreach (var email in emails) {
                        // No InvokeAsync on WinForms, so do this the old way.
                        Action dgeins = () => mailDataGrid.Rows.Insert(0, email, url);
                        mailDataGrid.BeginInvoke(dgeins);
                    }
                });
    });

    tasks.WaitAll();
}
于 2012-07-21T13:14:07.853 回答
0

公共静态字符串 htmlcon; //html源码

公共静态列表 emailList = new List();

问题是因为这些成员 htmlcon 和 emailList 是线程之间和迭代之间的共享资源。Parallel.ForEach 中的每次迭代都是并行执行的。这就是为什么你有奇怪的行为。

如何解决问题:

  • 修改您的代码并尝试在没有静态变量或共享状态的情况下实现它。

作为一个选项,从 Parallel.ForEach 更改为 TPL 任务链接,当您进行此更改时,一个并行操作的结果将成为另一个并行操作的输入数据,并且它是许多如何修改代码以避免共享状态的选项之一。

  • 使用锁定或并发集合。您的 htmlcon 变量可能会变得易变,但使用 list 您应该锁定或并发集合。

更好的方法是修改您的代码以避免共享状态,以及如何做到这一点是基于您的实现的许多选项,而不仅仅是任务链。

于 2012-07-23T13:20:14.993 回答