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();
``` 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 07:48 编辑
太多类不熟悉了。写起来好吃力。这个网址没有反扒,所以很适合练手!TotolChapterCout 等于 所有章节数量时将触发向任务队列提交 线程退出任务。这是线程退出的唯一途径。这里的BUG就是线程没有退出。导致程序悬挂! xiaoli518800 发表于 2021-3-14 01:11
多谢。正愁没多线程例子。
我也不是很熟悉。多线程我自己看着都头疼😩 一看就很高达感{:1_921:} 看的不是很明白,学了点点,要继续领悟才好 多谢大佬的例子 有个疑问,这里边是怎么控制4个线程并发的,比如100章,4个线程会不会交叉重复下载 感谢您的分享,辛苦了 本帖最后由 Cool_Breeze 于 2021-3-14 07:30 编辑
wxk0248 发表于 2021-3-14 03:51
有个疑问,这里边是怎么控制4个线程并发的,比如100章,4个线程会不会交叉重复下载
任务是由队列 queue (先进先出)保管的。线程要想工作必须从队列里面获取任务,不然都是闲置状态,不往队列里面提交相同任务,是不可能重复下载的。 学习下 多线程的调度。