吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2714|回复: 31
收起左侧

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

  [复制链接]
Cool_Breeze 发表于 2021-3-14 01:01
本帖最后由 Cool_Breeze 于 2021-3-27 14:46 编辑

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

[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
            }
        }
    }}


1.png 4.png 4.png

C# 多线程爬虫记录

获取网页信息

浏览器打开目标网页

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

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

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

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

使用 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 获取所有章节连接 和章节名

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

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

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"">");

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

获取章节内容类

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();

免费评分

参与人数 5吾爱币 +3 热心值 +5 收起 理由
李玉风我爱你 + 1 我很赞同!
jonasr + 1 + 1 我很赞同!
明月相照 + 1 + 1 谢谢@Thanks!
李雪墨 + 1 用心讨论,共获提升!
夜泉 + 1 + 1 用心讨论,共获提升!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| Cool_Breeze 发表于 2021-3-20 09:50
Queue.Dequeue(不是线程安全的)
[C#] 纯文本查看 复制代码
using System;
using System.Threading;
using System.Collections;

class Multimedia
{
    static Args args = new Args();
    public static void Main()
    {
        Thread[] th = new Thread[4];
        for (int i=0; i<th.Length; i++)
        {
            th[i] = new Thread(Multimedia.getNumber);
            th[i].Name = i.ToString();
            th[i].Start();
        }
        
        for (int i=0; i<100; i++)
            Args.queue.Enqueue(i);
        
        Thread.Sleep(1000);
        for (int i=0; i<th.Length; i++)
        {
            th[i].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 [0] 等于 所有章节数量时将触发向任务队列提交 线程退出任务。这是线程退出的唯一途径。这里的BUG就是线程没有退出。导致程序悬挂!
 楼主| Cool_Breeze 发表于 2021-3-14 01:13
xiaoli518800 发表于 2021-3-14 01:11
多谢。正愁没多线程例子。

我也不是很熟悉。多线程我自己看着都头疼&#128553;
yywapj 发表于 2021-3-14 01:27
一看就很高达感
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
学习下 多线程的调度。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-25 16:43

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表