Cool_Breeze 发表于 2021-3-14 01:01

C# 学习笔记 多线程爬取小说

本帖最后由 Cool_Breeze 于 2021-3-27 14:46 编辑

219 行 正确内容为 Regex rex = new Regex(@"( ){4}(.*?)<"); // (.*?)是我们想要的类容,?是匹配到 第一个 < 结束匹配


using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Text.RegularExpressions;

namespace DoubanTop250
{
    public class Program
    {
      static void Main()
      {
            //StreamReader Content = new StreamReader(@"test.txt", Encoding.GetEncoding(936));
            string Content = "https://www.biqugeso.com/book/277988/";

            BiQu biqu = new BiQu(Content);

            // 事件订阅
            Logger.LoggerEvent += LogEventHandler.ConsolePrintHandler;
            Logger.LoggerEvent += LogEventHandler.NextChapterHandler;

            //biqu.GetContent("test.txt");
            // 保存所有章节
            Dictionary<string, string> BookInfo = new Dictionary<string, string>();

            // 储存目录
            string SaveDir = biqu.GetBookChapterName();
            if (!Directory.Exists(SaveDir)) Directory.CreateDirectory(SaveDir);
            BookInfo = biqu.GetUrlAndTitle();

            int[] TotolChapterCount = new int;
            //Console.WriteLine(BookInfo.Count);
            //Console.ReadLine();
            // Environment.Exit(0);

            // 线程池用于接受数据的队列(先进先出的线程安全队列)
            ConcurrentQueue<NextChapterEventArgs> queue = new ConcurrentQueue<NextChapterEventArgs>();

            // 创建线程池 4 个线程
            Thread[] ThreadPool = new Thread;
            // 初始化
            for (sbyte i = 0; i < ThreadPool.Length; i++)
            {
                ThreadPool = new Thread(new ThreadStart(new FreeThread(queue, TotolChapterCount).Run));
                ThreadPool.Start();
            }

            // 向线程池提交任务
            foreach (var key in BookInfo.Keys)
            {
                Console.WriteLine("{0} {1}", key, Path.Combine(SaveDir, BookInfo.ToString()));

                NextChapterEventArgs e = new NextChapterEventArgs()
                {
                  Url = key,
                  BookName = SaveDir,
                  ChapterName = Path.Combine(SaveDir, BookInfo.ToString()),
                };
                queue.Enqueue(e);
                Thread.Sleep(1000); // 休息1秒
            }

            while (true) // 等待所以章节下载完
            {
                if (TotolChapterCount == BookInfo.Count)
                {
                  // 关闭所有线程
                  foreach (var th in ThreadPool)
                  {
                        NextChapterEventArgs e = new NextChapterEventArgs()
                        {
                            ThreadExit = true,
                        };
                        queue.Enqueue(e);
                  }
                  break;
                }
                Thread.Sleep(1000);
                continue;
            }


            Console.WriteLine("Done!");
            Console.ReadLine();
      }
    }


    // 静态类,访问网址返回内容 报错WebClient does not support concurrent I/O operations.
    /*
    public static class MyWebClient
    {
      static WebClient Web = new WebClient();

      // 返回网址内容
      public static string Content(string Url)
      {
            // The request was aborted: Could not create SSL/TLS secure channel. 报错需要执行以下代码
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
               | SecurityProtocolType.Tls11
               | SecurityProtocolType.Tls12
               | SecurityProtocolType.Ssl3;
            // end

            Web.Encoding = Encoding.GetEncoding(936); // 设定网页编码
            // 添加请求,响应头部信息
            Web.Headers.Add("Content-Type", "text/html; charset=gbk");
            Web.Headers.Add("Referer", "https://www.biqugeso.com/book/5215/37477378.html");
            Web.Headers.Add("UserAgent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3641.400 QQBrowser/10.4.3284.400");

            return Web.DownloadString(Url);
      }

    }
    */

    public class WebContent
    {
      protected internal readonly string UrlHome = "https://www.biqugeso.com"; // 网站根目录
      protected internal string Content = null;
      private readonly WebClient Web = new WebClient();

      // 返回网址内容
      public string WebContentString(string Url)
      {
            // The request was aborted: Could not create SSL/TLS secure channel. 报错需要执行以下代码
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
               | SecurityProtocolType.Tls11
               | SecurityProtocolType.Tls12
               | SecurityProtocolType.Ssl3;
            // end

            Web.Encoding = Encoding.GetEncoding(936); // 设定网页编码
            // 添加请求,响应头部信息
            Web.Headers.Add("Content-Type", "text/html; charset=gbk");
            Web.Headers.Add("Referer", "https://www.biqugeso.com/book/5215/37477378.html");
            Web.Headers.Add("UserAgent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3641.400 QQBrowser/10.4.3284.400");

            return Web.DownloadString(Url);
      }
      public WebContent(string Url)
      {
            // 获取网页文本内容
            Content = WebContentString(Url);
      }

      public WebContent() { }
    }

    // 获取所有章节连接
    public class BiQu : WebContent
    {
      //protected event EventHandler<NextChapterEventArgs> NextChapter; // 下一章事件

      // 获取网页文本内容
      public BiQu(string Url) : base(Url) { } // Url 网址

      public BiQu(StreamReader file) // 本地文件
      {
            Content = file.ReadToEnd();
      }

      // 获取所有章节连接 和章节名
      public virtual Dictionary<string, string> GetUrlAndTitle()
      {
            // 字典 《Url :title》
            //< a href = "/book/277988/109392372.html" title = "第一章 南柯" > 第一章 南柯 </ a >
            Dictionary<string, string> UrlAndTitle = new Dictionary<string, string>();
            Regex rex = new Regex(@"href=""(/book/.*?/\d+.html)""\stitle=""(.*?)"">\2</a>");
            foreach (Match match in rex.Matches(Content))
            {
                UrlAndTitle.Add(UrlHome + match.Groups.ToString(), match.Groups.ToString() + ".txt");
            }
            return UrlAndTitle;
      }


      // 获取主页信息,保存到本地文件。调试正则表达式时使用
      public virtual void GetContent(string FullFileChapterName)
      {
            StreamWriter sFile = new StreamWriter(FullFileChapterName, false, Encoding.GetEncoding(936));
            sFile.Write(Content);
            sFile.Flush(); // 清理当前写入器的所有缓冲区,并使所有缓冲数据写入基础流。
            // Console.WriteLine(Content);
      }

      // 书名
      public string GetBookChapterName()
      {
            // <h1 class="bookTitle">匹配内容</h1>
            Regex rex = new Regex(@"class=""bookTitle"">(.*?)<");
            Match match = rex.Match(Content);
            return match.Groups.ToString();
      }
    }

    // 章节内容获取类
    public class BiQuChapter : WebContent
    {
      private readonly NextChapterEventArgs NextArgs;
      public BiQuChapter(NextChapterEventArgs e) : base(e.Url) // 初始化父类
      {
            NextArgs = e;
      }

      // 提取一章网页类容里面小说类容
      public virtual string GetChapterContent()
      {

            List<string> strList = new List<string>();
            // "    需要的文本内容"
            Regex rex = new Regex(@"( ){4}(.*?)<"); // (.*?)是我们想要的类容,?是匹配到 第一个 < 结束匹配
            foreach (Match match in rex.Matches(Content))
            {
                strList.Add(match.Groups.ToString());
            }
            return String.Join("\r\n", strList.ToArray()); // "\r\n" 将列表元素连接在一起
      }
      
      // 这个网站将每一张内容分为好几页
      // 我们需要匹配一下是否出现下一页
      public NextChapterEventArgs GetNextPage()
      {
            // href="/book/5215/37477378_2.html">下一页<i class="fa fa-arrow-circle-right fa-fw">
            // @字符串里面出现的 " 需要写成 ""

            NextChapterEventArgs NextE = new NextChapterEventArgs() // 换了新实例
            {
                BookName = NextArgs.BookName,
                ChapterName = NextArgs.ChapterName,
            };

            Regex rex = new Regex(@"href=""(.*)"">(.*)<i class=""fa fa-arrow-circle-right fa-fw"">");
            Match match = rex.Match(Content);
            if (match.Groups.ToString() == "下一页")
            {
                NextE.Next = true;
                NextE.Url = UrlHome + match.Groups.ToString();
            }
            return NextE;
      }
    }

    // 下章事件参数
    public class NextChapterEventArgs : EventArgs
    {
      public string Url;
      public string ChapterName;
      public string BookName;
      public string Error;
      public bool ThreadExit = false;
      public bool Next; // 是否还有下一页 默认值为false
      public LogLevel Level;
    }

    // 日志级别
    public enum LogLevel
    {
      DEBUG = 0,
      INFO = 10,
      WARNING = 20,
      ERROR = 30,
    }

    // 处理事件
    public static class Logger
    {
      public static event EventHandler<NextChapterEventArgs> LoggerEvent;

      public static void OnLoggerHandler(object sender, NextChapterEventArgs e)
      {
            LoggerEvent?.Invoke(sender, e);
      }
    }

    // 日志事件处理
    public static class LogEventHandler
    {
      private static readonly object LogWriteLock = new object(); // 日志锁
      private static readonly object ConsoleLock = new object(); // 控制台锁
      // 控制台输出
      public static void ConsolePrintHandler(object sender, NextChapterEventArgs e)
      {
            lock (ConsoleLock)
            {
                switch (e.Level)
                {
                  case LogLevel.DEBUG:

                        break;
                  case LogLevel.INFO:
                        Console.ForegroundColor = ConsoleColor.Green; // 控制台颜色
                        Console.WriteLine(e.Error);
                        Console.ResetColor();
                        break;
                  case LogLevel.WARNING:
                        Console.ForegroundColor = ConsoleColor.Yellow;
                        Console.WriteLine(e.Error);
                        Console.ResetColor();
                        break;
                  case LogLevel.ERROR:
                        Console.ForegroundColor = ConsoleColor.Red;
                        Console.WriteLine(e.Error);
                        Console.ResetColor();
                        break;
                  default: break;
                }
            }
      }

      // 日志记录
      // 事件处理器
      // 简单写写
      public static void NextChapterHandler(object sender, NextChapterEventArgs e)
      {
            lock (LogWriteLock)
            {
                // 日志文件
                // 日志文件名为 ChapterPage.BookName
                StreamWriter sFile = new StreamWriter(e.BookName + ".log", true, Encoding.GetEncoding(936)); // 追加写入文本
                sFile.Write(DateTime.Now + "" + sender + "" + e.Error + "\r\n");
                sFile.Flush();
                sFile.Close();
            }
      }
    }

    // 多线程 下载每一章节类容
    public class FreeThread
    {
      private ConcurrentQueue<NextChapterEventArgs> QueueBiqu { get; set; } // 任务队列
      private BiQuChapter EachChapter; // 网页内容
      private NextChapterEventArgs ChapterPage; // 网页参数
      private readonly int[] __TotalChapterCount = new int; //
      private static readonly object CountLock = new object(); // 任务完成计数锁
      private static readonly object FileLock = new object(); // 写入文件锁

      public FreeThread(ConcurrentQueue<NextChapterEventArgs> queue, int[] count)
      {
            QueueBiqu = queue;
            __TotalChapterCount = count;
      }


      public void Run()
      {
            try
            {
                while (true) // 深度爬取
                {
                  try
                  {
                        if (QueueBiqu.TryDequeue(out ChapterPage) == false)
                        {
                            Thread.Sleep(10);
                            continue;
                        }
                  }
                  catch (Exception)
                  {
                        // 捕获空异常, 短暂休息一下,不然 CPU 吃不消啊
                        Thread.Sleep(10);
                        continue;
                  }
                  if (ChapterPage.ThreadExit) break;
                  // Console.WriteLine(ChapterPage.ChapterName);

                  try
                  {
                        EachChapter = new BiQuChapter(ChapterPage);
                        lock (FileLock)
                        {
                            StreamWriter sFile = new StreamWriter(ChapterPage.ChapterName, true, Encoding.GetEncoding(936)); // 追加写入文本
                            Regex rex = new Regex(@"美女小说(.*)威信公众号,看更多好看的小说!"); // 去广告
                            sFile.Write(rex.Replace(EachChapter.GetChapterContent(), ""));
                            sFile.Flush();
                            sFile.Close();
                        }

                        ChapterPage = EachChapter.GetNextPage();

                        if (ChapterPage.Next)// 检查下一页, false 说明当前页为最后一页
                        {
                            QueueBiqu.Enqueue(ChapterPage); // 把结果从队列发送出去
                        }
                        else
                        {
                            lock (CountLock) // 只能有一个线程访问
                            {
                              __TotalChapterCount++; // 完成一个计数一次
                              ChapterPage.Level = LogLevel.INFO;
                              ChapterPage.Error = String.Format("已经完成 {0} 章 下载完成!{1}", __TotalChapterCount, ChapterPage.ChapterName);
                              Logger.OnLoggerHandler(this, ChapterPage);
                            }
                        }

                  }
                  catch (Exception e)
                  {
                        //错误日志写入
                        Console.WriteLine(e.Message);
                        ChapterPage.Error += e.Message;
                        ChapterPage.Level = LogLevel.ERROR;
                        Logger.OnLoggerHandler(this, ChapterPage);
                  }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("退出线程{0}", e.Message);
                // Logger.OnLoggerHandler
                // 退出线程 Abort
            }
      }
    }}

# C# 多线程爬虫记录

## 获取网页信息

### 浏览器打开目标网页

按<kbd>F12</kbd>打开开发者工具找到<kbd>Network</kbd> 然后刷新网页

在<kbd>Name</kbd> 找到我们的目标。这里是<kbd>277988</kbd> 单机鼠标左键选中

我们需要一些头部信息,这个浏览器代{过}{滤}理<kbd>User-Agent</kbd>的值等。。。   

然后还需要一个网页编码,在<kbd>Network</kbd> 左边<kbd>Elements</kbd> 点击,获取到网页编码   

## 使用 WebClient 类访问网页获取类容

### 创建一个访问网页的对象

<!--我们需要多线程爬取网页,所有不能创建静态的 WebClient对象,它是不支持并发的-->

```c#
WebClient Web = new WebClient();
```



设置网页编码

```c#
Web.Encoding = Encoding.GetEncoding(936); // 设定网页编码
```

使用对象的<b>Headers.Add</b>方法添加一些头部信息。。。

<b>Web.DownloadString</b>方法返回网页内容

### 访问网页类

```c#
public class WebContent
{
    protected internal readonly string UrlHome = "https://www.biqugeso.com"; // 网站根目录
    protected internal string Content = null;
    private readonly WebClient Web = new WebClient();

    // 返回网址内容
    public string WebContentString(string Url)
    {
      // The request was aborted: Could not create SSL/TLS secure channel. 报错需要执行以下代码
      ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
            | SecurityProtocolType.Tls11
            | SecurityProtocolType.Tls12
            | SecurityProtocolType.Ssl3;
      // end

      Web.Encoding = Encoding.GetEncoding(936); // 设定网页编码
      // 添加请求,响应头部信息
      Web.Headers.Add("Content-Type", "text/html; charset=gbk");
      Web.Headers.Add("Referer", "https://www.biqugeso.com/book/5215/37477378.html");
      Web.Headers.Add("UserAgent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3641.400 QQBrowser/10.4.3284.400");

      return Web.DownloadString(Url);
    }
    public WebContent(string Url)
    {
      // 获取网页文本内容
      Content = WebContentString(Url);
    }

    public WebContent() { }
}
```

## 提取书名网页所有章节与链接

从访问网页类继承下来(都会用到访问网页获取内容)

有两个重载

### 从网页获取

```c#
public BiQu(string Url) : base(Url) { } // Url 网址
```

### 从保存好的本地文件获取

```c#
public BiQu(StreamReader file) // 本地文件
```

###GetUrlAndTitle 获取所有章节连接 和章节名

打开开发者工具,按如下步骤查看我们需要的信息

这个网页格式都是固定的,那么使用正则表达式获取(查看网页源代码)

```c#
Regex rex = new Regex(@"href=""(/book/.*?/\d+.html)""\stitle=""(.*?)"">\2</a>");
```

第一个<b>(/book/.*?/\d+.html)匹配<b>/book/277988/109392372.html</b>

第二个<b>"(.*?)"</b>匹配**第一章 南柯**

将这些数据放入一个容器保存好,以后使用(也可是直接发送给任务队列,不用保存)

我使用的字典保存:

```c#
Dictionary<string, string> UrlAndTitle = new Dictionary<string, string>();
```

**key**为网页地址**value**章节名

### GetContent 方法保存网页源代码

保存到本地文件,调试正则表达式

```c#
public virtual void GetContent(string FullFileChapterName)
```

### GetBookChapterName 书名

```c#
public string GetBookChapterName()
```

### 获取所有章节连接类

```c#
// 获取所有章节连接
public class BiQu : WebContent
{
    //protected event EventHandler<NextChapterEventArgs> NextChapter; // 下一章事件

    // 获取网页文本内容
    public BiQu(string Url) : base(Url) { } // Url 网址

    public BiQu(StreamReader file) // 本地文件
    {
      Content = file.ReadToEnd();
    }

    // 获取所有章节连接 和章节名
    public virtual Dictionary<string, string> GetUrlAndTitle()
    {
      // 字典 《Url :title》
      //< a href = "/book/277988/109392372.html" title = "第一章 南柯" > 第一章 南柯 </ a >
      Dictionary<string, string> UrlAndTitle = new Dictionary<string, string>();
      Regex rex = new Regex(@"href=""(/book/.*?/\d+.html)""\stitle=""(.*?)"">\2</a>");
      foreach (Match match in rex.Matches(Content))
      {
            UrlAndTitle.Add(UrlHome + match.Groups.ToString(), match.Groups.ToString() + ".txt");
      }
      return UrlAndTitle;
    }


    // 获取主页信息,保存到本地文件。调试正则表达式时使用
    public virtual void GetContent(string FullFileChapterName)
    {
      StreamWriter sFile = new StreamWriter(FullFileChapterName, false, Encoding.GetEncoding(936));
      sFile.Write(Content);
      sFile.Flush(); // 清理当前写入器的所有缓冲区,并使所有缓冲数据写入基础流。
      // Console.WriteLine(Content);
    }

    // 书名
    public string GetBookChapterName()
    {
      // <h1 class="bookTitle">匹配内容</h1>
      Regex rex = new Regex(@"class=""bookTitle"">(.*?)<");
      Match match = rex.Match(Content);
      return match.Groups.ToString();
    }
}
```

## 提取章节内容

获取本章节所有内容,和是否下章信息

### BiQuChapter

构造函数

```c#
public BiQuChapter(NextChapterEventArgs e) : base(e.Url) // 初始化父类
```

### NextArgs

保存下一章信息字段

```c#
private readonly NextChapterEventArgs NextArgs;
```

### 获取当前网页章节内容

GetChapterContent

返回(string)提取到一章网页内容里面小说内容

使用的正则

```c#
Regex rex = new Regex(@"( ){4}(.*?)<");
```

需要的是**(.*?)**这个分组匹配的内容

**(\ ){4}** 解释:**(\ )** 这是一个分组**{4}** 前面分组连续出现的次数

**(.*?)<** 解释:匹配所有的字符,**?** 直到第一个 **<** 字符出现

### 获取下一章网页信息

**GetNextPage**

返回(**NextChapterEventArgs**)

这个网站将每一张内容分为好几页

我们需要匹配一下是否出现下一页

网页源码:

```html
href="/book/5215/37477378_2.html">下一页<i class="fa fa-arrow-circle-right fa-fw">
```

正则提取下一页字符串:

```c#
Regex rex = new Regex(@"href=""(.*)"">(.*)<i class=""fa fa-arrow-circle-right fa-fw"">");
```

这里应该可以不需要重新创建参数对象,修改**Next**和**Url**值就可以了

### 获取章节内容类

```c#
public class BiQuChapter : WebContent
{
    private readonly NextChapterEventArgs NextArgs;
    public BiQuChapter(NextChapterEventArgs e) : base(e.Url) // 初始化父类
    {
      NextArgs = e;
    }

    // 提取一章网页类容里面小说类容
    public virtual string GetChapterContent()
    {

      List<string> strList = new List<string>();
      // "    需要的文本内容"
      Regex rex = new Regex(@"( ){4}(.*?)<"); // (.*?)是我们想要的类容,?是匹配到 第一个 < 结束匹配
      foreach (Match match in rex.Matches(Content))
      {
            strList.Add(match.Groups.ToString());
      }
      return String.Join("\r\n", strList.ToArray()); // "\r\n" 将列表元素连接在一起
    }

    // 这个网站将每一张内容分为好几页
    // 我们需要匹配一下是否出现下一页
    public NextChapterEventArgs GetNextPage()
    {
      // href="/book/5215/37477378_2.html">下一页<i class="fa fa-arrow-circle-right fa-fw">
      // @字符串里面出现的 " 需要写成 ""

      NextChapterEventArgs NextE = new NextChapterEventArgs() // 换了新实例
      {
            BookName = NextArgs.BookName,
            ChapterName = NextArgs.ChapterName,
      };

      Regex rex = new Regex(@"href=""(.*)"">(.*)<i class=""fa fa-arrow-circle-right fa-fw"">");
      Match match = rex.Match(Content);
      if (match.Groups.ToString() == "下一页")
      {
            NextE.Next = true;
            NextE.Url = UrlHome + match.Groups.ToString();
      }
      return NextE;
    }
```

# 在开始爬取前还要记录爬取日志

以下是日志相关

## 下章事件参数

### NextChapterEventArgs

**Url** 网页地址

**ChapterName** 章节名

**BookName** 书名

**Error** 消息

**ThreadExit** 是否线程退出

**Next** 是否有下一章

**Level** 日志级别

### 参数类

```c#
public class NextChapterEventArgs : EventArgs
{
    public string Url;
    public string ChapterName;
    public string BookName;
    public string Error;
    public bool ThreadExit = false;
    public bool Next; // 是否还有下一页 默认值为false
    public LogLevel Level;
}
```

## 日志级别

### 日志级别类

```c#
public enum LogLevel
{
    DEBUG = 0,
    INFO = 10,
    WARNING = 20,
    ERROR = 30,
}
```

## 日志事件处理

静态处理对象

### 日志事件

LogEventHandler

### 日志事件处理器

OnLoggerHandler

## 日志控制台和文本

控制台写入和log文件写入都会造成资源竞争,需要锁!

### 写入文本日志锁

LogWriteLock

### 写入控制台锁

ConsoleLock

### 控制台打印

ConsolePrintHandler

### 文件写入

NextChapterHandler



### 日志事件处理类

```c#
public static class LogEventHandler
{
    private static readonly object LogWriteLock = new object(); // 日志锁
    private static readonly object ConsoleLock = new object(); // 控制台锁
    // 控制台输出
    public static void ConsolePrintHandler(object sender, NextChapterEventArgs e)
    {
      lock (ConsoleLock)
      {
            switch (e.Level)
            {
                case LogLevel.DEBUG:

                  break;
                case LogLevel.INFO:
                  Console.ForegroundColor = ConsoleColor.Green; // 控制台颜色
                  Console.WriteLine(e.Error);
                  Console.ResetColor();
                  break;
                case LogLevel.WARNING:
                  Console.ForegroundColor = ConsoleColor.Yellow;
                  Console.WriteLine(e.Error);
                  Console.ResetColor();
                  break;
                case LogLevel.ERROR:
                  Console.ForegroundColor = ConsoleColor.Red;
                  Console.WriteLine(e.Error);
                  Console.ResetColor();
                  break;
                default: break;
            }
      }
    }

    // 日志记录
    // 事件处理器
    // 简单写写
    public static void NextChapterHandler(object sender, NextChapterEventArgs e)
    {
      lock (LogWriteLock)
      {
            // 日志文件
            // 日志文件名为 ChapterPage.BookName
            StreamWriter sFile = new StreamWriter(e.BookName + ".log", true, Encoding.GetEncoding(936)); // 追加写入文本
            sFile.Write(DateTime.Now + "" + sender + "" + e.Error + "\r\n");
            sFile.Flush();
            sFile.Close();
      }
    }
}
```

# 线程下载章节内容

## FreeThread

### 字段

#### 接收外部给出的任务队列

QueueBiqu

#### 访问网页对象

EachChapter

#### 访问网页对象的参数

ChapterPage

#### 任务进度计数

__TotalChapterCount

### 构造函数

```c#
public FreeThread(ConcurrentQueue<NextChapterEventArgs> queue, int[] count)
{
    QueueBiqu = queue;
    __TotalChapterCount = count;
}
```

### 线程开始工作

死循环:

####   从任务队列里面获取任务

```c#
try
{
    if (QueueBiqu.TryDequeue(out ChapterPage) == false)
    {
      Thread.Sleep(10);
      continue;
    }
}
catch (Exception)
{
    // 捕获空异常, 短暂休息一下,不然 CPU 吃不消啊
    Thread.Sleep(10);
    continue;
}
```

####   检查任务是否是线程退出

```c#
if (ChapterPage.ThreadExit) break;
```

####   开始获取访问网页,获取章节内容,并以追加模式写入文本

(bug,多次启动程序将多次写入内容)

```c#
EachChapter = new BiQuChapter(ChapterPage);
lock (FileLock)
{
    StreamWriter sFile = new StreamWriter(ChapterPage.ChapterName, true, Encoding.GetEncoding(936)); // 追加写入文本
    Regex rex = new Regex(@"美女小说(.*)威信公众号,看更多好看的小说!"); // 去广告
    sFile.Write(rex.Replace(EachChapter.GetChapterContent(), ""));
    sFile.Flush();
    sFile.Close();
}
```

####   获取下一章网页参数

```c#
ChapterPage = EachChapter.GetNextPage();
```

​    检查是否存在下一页,如果存在,将下一章网页参数提交到任务队列

```c#
if (ChapterPage.Next)// 检查下一页, false 说明当前页为最后一页
{
    QueueBiqu.Enqueue(ChapterPage); // 把结果从队列发送出去
}
```

####   章节内容下载完成   

没有下一页,说明这一章下载完成,任务进程计数加一,日志触发事件

```c#
if (ChapterPage.Next)// 检查下一页, false 说明当前页为最后一页
{
    QueueBiqu.Enqueue(ChapterPage); // 把结果从队列发送出去
}
else
{
    lock (CountLock) // 只能有一个线程访问
    {
      __TotalChapterCount++; // 完成一个计数一次
      ChapterPage.Level = LogLevel.INFO;
      ChapterPage.Error = String.Format("已经完成 {0} 章 下载完成!{1}", __TotalChapterCount, ChapterPage.ChapterName);
      Logger.OnLoggerHandler(this, ChapterPage);
    }
}
```

#### 线程工作异常

```c#
catch (Exception e)
{
    //错误日志写入
    Console.WriteLine(e.Message);
    ChapterPage.Error += e.Message;
    ChapterPage.Level = LogLevel.ERROR;
    Logger.OnLoggerHandler(this, ChapterPage);
}
```

### 线程异常

```c#
catch (Exception e)
{
    Console.WriteLine("退出线程{0}", e.Message);
    // Logger.OnLoggerHandler
    // 退出线程 Abort
}
```

# Main(主函数)

## 书名地址

```c#
string Content = "https://www.biqugeso.com/book/277988/";
```

## 事件订阅

```c#
Logger.LoggerEvent += LogEventHandler.ConsolePrintHandler;
Logger.LoggerEvent += LogEventHandler.NextChapterHandler;
```

## 获取章节,章节名信息

### 保存信息对象

**BookInfo**

```c#
BiQu biqu = new BiQu(Content);
Dictionary<string, string> BookInfo = new Dictionary<string, string>();
BookInfo = biqu.GetUrlAndTitle();
```

### 创建书名目录

**SaveDir**

```c#
// 储存目录
string SaveDir = biqu.GetBookChapterName();
if (!Directory.Exists(SaveDir)) Directory.CreateDirectory(SaveDir);
```

## 任务进度计数

**TotolChapterCount** 数组才是进度值

```c#
int[] TotolChapterCount = new int;
```

## 线程任务队列

**queue** 线程安全的

```c#
ConcurrentQueue<NextChapterEventArgs> queue = new ConcurrentQueue<NextChapterEventArgs>();
```

## 线程池

**ThreadPool** 里面存储的是线程实例

每个线程都在等待从任务队列 **queue** 接收任务,直到它们收到收到线程退出任务**ThreadExit**

```c#
Thread[] ThreadPool = new Thread;
// 初始化
for (sbyte i = 0; i < ThreadPool.Length; i++)
{
    ThreadPool = new Thread(new ThreadStart(new FreeThread(queue, TotolChapterCount).Run));
    ThreadPool.Start();
}
```

## 提交任务

每隔一秒从 **BookInfo** 拿出章节信息 从 **queue** 向线程提交任务

```c#
// 向线程池提交任务
foreach (var key in BookInfo.Keys)
{
    Console.WriteLine("{0} {1}", key, Path.Combine(SaveDir, BookInfo.ToString()));

    NextChapterEventArgs e = new NextChapterEventArgs()
    {
      Url = key,
      BookName = SaveDir,
      ChapterName = Path.Combine(SaveDir, BookInfo.ToString()),
    };
    queue.Enqueue(e);
    Thread.Sleep(1000); // 休息1秒
}
```

## 等待线程结束

每隔一秒检查任务计数是否等于章节大小

如果等于向进程池提交结束线程任务 **ThreadExit = true,**

```c#
while (true) // 等待所以章节下载完
{
    if (TotolChapterCount == BookInfo.Count)
    {
      // 关闭所有线程
      foreach (var th in ThreadPool)
      {
            NextChapterEventArgs e = new NextChapterEventArgs()
            {
                ThreadExit = true,
            };
            queue.Enqueue(e);
      }
      break;
    }
    Thread.Sleep(1000);
    continue;
}
```

## 结束

```c#
Console.WriteLine("Done!");
Console.ReadLine();
```

Cool_Breeze 发表于 2021-3-20 09:50

Queue.Dequeue(不是线程安全的)
using System;
using System.Threading;
using System.Collections;

class Multimedia
{
    static Args args = new Args();
    public static void Main()
    {
      Thread[] th = new Thread;
      for (int i=0; i<th.Length; i++)
      {
            th = new Thread(Multimedia.getNumber);
            th.Name = i.ToString();
            th.Start();
      }
      
      for (int i=0; i<100; i++)
            Args.queue.Enqueue(i);
      
      Thread.Sleep(1000);
      for (int i=0; i<th.Length; i++)
      {
            th.Abort();
      }
      Console.WriteLine(args.GetCount());
      Console.ReadKey();
    }
   
    class Args
    {
      public static readonly Queue queue = new Queue(); // 只会创建一次
      private static object Lock = new object();
      private static int Count = 0;
      
      public void Add()
      {
            lock(Lock)
            {
                Count++;
            }
      }
      public int GetCount()
      {
            return Count;
      }
    }
   
    static void getNumber()
    {
      while (true)
      {
            int temp;
            try
            {
                lock(Args.queue) // 只有保证这四个线程锁的是同一个引用对象就行
                {
                  temp = (int)Args.queue.Dequeue(); // 不是线程安全的
                }
                Console.WriteLine("{0} {1}", Thread.CurrentThread.Name, temp);
                args.Add();
            }
            catch
            {
                Thread.Sleep(1000);
            }
      }
    }
}

Cool_Breeze 发表于 2021-3-14 01:03

本帖最后由 Cool_Breeze 于 2021-3-14 07:48 编辑

太多类不熟悉了。写起来好吃力。这个网址没有反扒,所以很适合练手!TotolChapterCout 等于 所有章节数量时将触发向任务队列提交 线程退出任务。这是线程退出的唯一途径。这里的BUG就是线程没有退出。导致程序悬挂!

Cool_Breeze 发表于 2021-3-14 01:13

xiaoli518800 发表于 2021-3-14 01:11
多谢。正愁没多线程例子。

我也不是很熟悉。多线程我自己看着都头疼&#128553;

yywapj 发表于 2021-3-14 01:27

一看就很高达感{:1_921:}

donggua1118 发表于 2021-3-14 01:31

看的不是很明白,学了点点,要继续领悟才好

PrincessSnow 发表于 2021-3-14 03:10

多谢大佬的例子

wxk0248 发表于 2021-3-14 03:51

有个疑问,这里边是怎么控制4个线程并发的,比如100章,4个线程会不会交叉重复下载

wylksy 发表于 2021-3-14 07:22

感谢您的分享,辛苦了

Cool_Breeze 发表于 2021-3-14 07:28

本帖最后由 Cool_Breeze 于 2021-3-14 07:30 编辑

wxk0248 发表于 2021-3-14 03:51
有个疑问,这里边是怎么控制4个线程并发的,比如100章,4个线程会不会交叉重复下载
任务是由队列 queue (先进先出)保管的。线程要想工作必须从队列里面获取任务,不然都是闲置状态,不往队列里面提交相同任务,是不可能重复下载的。

明月相照 发表于 2021-3-14 07:57

学习下 多线程的调度。
页: [1] 2 3 4
查看完整版本: C# 学习笔记 多线程爬取小说