[C#] 纯文本查看 复制代码
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[1];
//Console.WriteLine(BookInfo.Count);
//Console.ReadLine();
// Environment.Exit(0);
// 线程池用于接受数据的队列(先进先出的线程安全队列)
ConcurrentQueue<NextChapterEventArgs> queue = new ConcurrentQueue<NextChapterEventArgs>();
// 创建线程池 4 个线程
Thread[] ThreadPool = new Thread[4];
// 初始化
for (sbyte i = 0; i < ThreadPool.Length; i++)
{
ThreadPool[i] = new Thread(new ThreadStart(new FreeThread(queue, TotolChapterCount).Run));
ThreadPool[i].Start();
}
// 向线程池提交任务
foreach (var key in BookInfo.Keys)
{
Console.WriteLine("{0} {1}", key, Path.Combine(SaveDir, BookInfo[key].ToString()));
NextChapterEventArgs e = new NextChapterEventArgs()
{
Url = key,
BookName = SaveDir,
ChapterName = Path.Combine(SaveDir, BookInfo[key].ToString()),
};
queue.Enqueue(e);
Thread.Sleep(1000); // 休息1秒
}
while (true) // 等待所以章节下载完
{
if (TotolChapterCount[0] == 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[1].ToString(), match.Groups[2].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[1].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[2].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[2].ToString() == "下一页")
{
NextE.Next = true;
NextE.Url = UrlHome + match.Groups[1].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[1]; //
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[0]++; // 完成一个计数一次
ChapterPage.Level = LogLevel.INFO;
ChapterPage.Error = String.Format("已经完成 {0} 章 下载完成!{1}", __TotalChapterCount[0], 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对象,它是不支持并发的-->
WebClient Web = new WebClient();
,
设置网页编码
Web.Encoding = Encoding.GetEncoding(936); // 设定网页编码
使用对象的<b>Headers.Add</b>方法添加一些头部信息。。。
<b>Web.DownloadString</b>方法返回网页内容
访问网页类
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 BiQu(string Url) : base(Url) { } // Url 网址
从保存好的本地文件获取
public BiQu(StreamReader file) // 本地文件
GetUrlAndTitle 获取所有章节连接 和章节名
打开开发者工具,按如下步骤查看我们需要的信息
这个网页格式都是固定的,那么使用正则表达式获取(查看网页源代码)
Regex rex = new Regex(@"href=""(/book/.*?/\d+.html)""\stitle=""(.*?)"">\2</a>");
第一个<b>(/book/.*?/\d+.html)匹配<b>/book/277988/109392372.html</b>
第二个<b>"(.*?)"</b>匹配第一章 南柯
将这些数据放入一个容器保存好,以后使用(也可是直接发送给任务队列,不用保存)
我使用的字典保存:
Dictionary<string, string> UrlAndTitle = new Dictionary<string, string>();
key为网页地址value章节名
GetContent 方法保存网页源代码
保存到本地文件,调试正则表达式
public virtual void GetContent(string FullFileChapterName)
GetBookChapterName 书名
public string GetBookChapterName()
获取所有章节连接类
// 获取所有章节连接
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[1].ToString(), match.Groups[2].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[1].ToString();
}
}
提取章节内容
获取本章节所有内容,和是否下章信息
BiQuChapter
构造函数
public BiQuChapter(NextChapterEventArgs e) : base(e.Url) // 初始化父类
NextArgs
保存下一章信息字段
private readonly NextChapterEventArgs NextArgs;
获取当前网页章节内容
GetChapterContent
返回(string)提取到一章网页内容里面小说内容
使用的正则
Regex rex = new Regex(@"( ){4}(.*?)<");
需要的是*(.?)**这个分组匹配的内容
(\ ){4} 解释:(\ ) 这是一个分组{4} 前面分组连续出现的次数
*(.?)< 解释:匹配所有的字符,? 直到第一个 <** 字符出现
获取下一章网页信息
GetNextPage
返回(NextChapterEventArgs)
这个网站将每一张内容分为好几页
我们需要匹配一下是否出现下一页
网页源码:
href="/book/5215/37477378_2.html">下一页<i class="fa fa-arrow-circle-right fa-fw">
正则提取下一页字符串:
Regex rex = new Regex(@"href=""(.*)"">(.*)<i class=""fa fa-arrow-circle-right fa-fw"">");
这里应该可以不需要重新创建参数对象,修改Next和Url值就可以了
获取章节内容类
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[2].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[2].ToString() == "下一页")
{
NextE.Next = true;
NextE.Url = UrlHome + match.Groups[1].ToString();
}
return NextE;
}
在开始爬取前还要记录爬取日志
以下是日志相关
下章事件参数
NextChapterEventArgs
Url 网页地址
ChapterName 章节名
BookName 书名
Error 消息
ThreadExit 是否线程退出
Next 是否有下一章
Level 日志级别
参数类
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,
}
日志事件处理
静态处理对象
日志事件
LogEventHandler
日志事件处理器
OnLoggerHandler
日志控制台和文本
控制台写入和log文件写入都会造成资源竞争,需要锁!
写入文本日志锁
LogWriteLock
写入控制台锁
ConsoleLock
控制台打印
ConsolePrintHandler
文件写入
NextChapterHandler
日志事件处理类
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
构造函数
public FreeThread(ConcurrentQueue<NextChapterEventArgs> queue, int[] count)
{
QueueBiqu = queue;
__TotalChapterCount = count;
}
线程开始工作
死循环:
从任务队列里面获取任务
try
{
if (QueueBiqu.TryDequeue(out ChapterPage) == false)
{
Thread.Sleep(10);
continue;
}
}
catch (Exception)
{
// 捕获空异常, 短暂休息一下,不然 CPU 吃不消啊
Thread.Sleep(10);
continue;
}
检查任务是否是线程退出
if (ChapterPage.ThreadExit) break;
开始获取访问网页,获取章节内容,并以追加模式写入文本
(bug,多次启动程序将多次写入内容)
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); // 把结果从队列发送出去
}
章节内容下载完成
没有下一页,说明这一章下载完成,任务进程计数加一,日志触发事件
if (ChapterPage.Next)// 检查下一页, false 说明当前页为最后一页
{
QueueBiqu.Enqueue(ChapterPage); // 把结果从队列发送出去
}
else
{
lock (CountLock) // 只能有一个线程访问
{
__TotalChapterCount[0]++; // 完成一个计数一次
ChapterPage.Level = LogLevel.INFO;
ChapterPage.Error = String.Format("已经完成 {0} 章 下载完成!{1}", __TotalChapterCount[0], 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
}
Main(主函数)
书名地址
string Content = "https://www.biqugeso.com/book/277988/";
事件订阅
Logger.LoggerEvent += LogEventHandler.ConsolePrintHandler;
Logger.LoggerEvent += LogEventHandler.NextChapterHandler;
获取章节,章节名信息
保存信息对象
BookInfo
BiQu biqu = new BiQu(Content);
Dictionary<string, string> BookInfo = new Dictionary<string, string>();
BookInfo = biqu.GetUrlAndTitle();
创建书名目录
SaveDir
// 储存目录
string SaveDir = biqu.GetBookChapterName();
if (!Directory.Exists(SaveDir)) Directory.CreateDirectory(SaveDir);
任务进度计数
TotolChapterCount 数组[0]才是进度值
int[] TotolChapterCount = new int[1];
线程任务队列
queue 线程安全的
ConcurrentQueue<NextChapterEventArgs> queue = new ConcurrentQueue<NextChapterEventArgs>();
线程池
ThreadPool 里面存储的是线程实例
每个线程都在等待从任务队列 queue 接收任务,直到它们收到收到线程退出任务ThreadExit
Thread[] ThreadPool = new Thread[4];
// 初始化
for (sbyte i = 0; i < ThreadPool.Length; i++)
{
ThreadPool[i] = new Thread(new ThreadStart(new FreeThread(queue, TotolChapterCount).Run));
ThreadPool[i].Start();
}
提交任务
每隔一秒从 BookInfo 拿出章节信息 从 queue 向线程提交任务
// 向线程池提交任务
foreach (var key in BookInfo.Keys)
{
Console.WriteLine("{0} {1}", key, Path.Combine(SaveDir, BookInfo[key].ToString()));
NextChapterEventArgs e = new NextChapterEventArgs()
{
Url = key,
BookName = SaveDir,
ChapterName = Path.Combine(SaveDir, BookInfo[key].ToString()),
};
queue.Enqueue(e);
Thread.Sleep(1000); // 休息1秒
}
等待线程结束
每隔一秒检查任务计数是否等于章节大小
如果等于向进程池提交结束线程任务 ThreadExit = true,
while (true) // 等待所以章节下载完
{
if (TotolChapterCount[0] == 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();