实现自己的音乐搜索软件(二)

上一篇文章已经写了有10个月之久了,那之后出差半年,都一直没时间继续。最近不太忙,终于有时间继续做下去,花了1周半的时间,把后续的下载功能实现了。这篇文章开始主要就介绍一下具体的实现。

 

一 程序结构

 

 

前一篇文章只是一个开头,介绍了搜索部分的大概结构,现在主要看一下整个程序的结构。程序主要的功能目前是搜索和下载。他们都已DLL的形式存在,供主程序调用。

以上是整个工程的结构。主要功能有两大块,搜索音乐和下载音乐。

  • ISearh:定义了一套接口,指定了搜索时的编码方式,搜索歌词的方式和搜索音乐文件的方式。如果工程引入并实现了这些接口,那么编译出来的DLL就能被我们的主程序直接使用了。这样可以方便的开发搜索插件。
  • MusicPiugin:这个目录下的工程就是搜索插件。我们搜索来源于百度网页,soso网页以及百度提供的快搜接口(只返回5首歌曲);而歌词是来源于千千静听提供的接口,所以我们一共有4个插件,他们都实现了ISearh下定义的接口。
  • MusicCrawler:这个项目中主要是封装了搜索时的网络请求操作,并且对ISearh下定义的接口进行编程,提供了搜索音乐和歌词方法。
  • MusicRunner:这个项目的主要作用就是负责加载搜索插件并提供搜索方法。其中引入了MusicCrawler工程的DLL,在加载了搜索插件DLL后,调用他的方法来实现具体的应约搜索。
  • MusicDownload:这个项目主要功能是负责文件的下载,其中对下载的网络请求进行封装,并且提供了基于事件驱动的多任务的调度和管理,并且包含了文件管理功能。
  • MusicCommon:这个项目定义了一些公用的方法,实体类,常量,枚举等类型。
  • Test :是一个测试DEMO,他只引用了MusicRunner、MusicDownload、MusicCommon这三个工程,而其他工程对他都是不可见的。并且在其中提供了简单的播放功能。

 

 

二 搜索功能的实现

 

 

上一篇文章介绍了搜索的思路,这里不详细介绍了。关于搜索我采用的是最原始的方法,请求页面然后分析,其中百度有提供API,但是只能搜索5首歌曲。下面就具体看看MusicCrawler是怎么工作的。

 

1.MusicCrawler

 

    public class Crawler
    {
        /// <summary>
        /// 获取音乐信息列表
        /// </summary>
        /// <param name="info">歌曲信息的实体类</param>
        /// <param name="objSearch">外部对象</param>
        /// <returns>音乐列表</returns>
        public List<MusicInfo> GetMusicList(SearchMusicInfo info,IMusicSearch objSearch)


        /// <summary>
        /// 获取音乐歌词列表
        /// </summary>
        /// <param name="info">歌词信息的实体类</param>
        /// <param name="objSearch">外部对象</param>
        /// <returns>返回的歌词列表</returns>
        public List<MusicLrcInfo> GetMusicLrcList(SearchMusicInfo info, ILRCSearch objSearch)
   
        /// <summary>
        /// 获取音乐歌词内容
        /// </summary>
        /// <param name="info">歌词信息的实体类</param>
        /// <param name="objSearch">外部对象</param>
        /// <returns>歌词字符串</returns>
        public string GetMusicLyric(MusicLrcInfo info, ILRCSearch objSearch)
}

////////////////////////////////

    public class NetworkRequest
    {

        /// <summary>
        /// 对指定URL页面进行请求,获取页面流
        /// </summary>
        /// <param name="pageHtml">返回的页面HTML字符串</param>
        /// <param name="reqUrl">请求的页面URL</param>
        /// <param name="encode">请求的页面编码</param>
        /// <param name="filterReg">页面要过滤的正则表达式</param>
        /// <returns>请求结果</returns>
        public PageRequestResults RequestPage(out string pageHtml, string reqUrl, Encoding encode, string filterReg)
       
        /// <summary>
        /// 对指定URL页面进行请求,获取页面流,去除文字修饰的标签
        /// </summary>
        /// <param name="pageHtml">返回的页面HTML字符串</param>
        /// <param name="reqUrl">请求的页面URL</param>
        /// <param name="encode">请求的页面编码</param>
        /// <returns>请求结果</returns>
        public PageRequestResults RequestPage(out string pageHtml, string reqUrl, Encoding encode)


        /// <summary>
        /// 去除网页HTML中指定的标签
        /// </summary>
        /// <param name="pageContext">要处理的HTML字符串</param>
        /// <param name="filterReg">要去除内容的正则表达式</param>
        /// <returns>消除后的字符串</returns>
        private string ReplaceHtml(string pageContext, string filterReg)
       

这个工程中只包含两个类,NetworkRequest是用来进行http请求的。其中主要的RequestPage方法就是通过输入请求的URL地址和编码方式,然后获得页面的字符串存放到pageHtml。其中还提供了一个重载方法,用来过滤一些不需要的HTML元素。

public PageRequestResults RequestPage(out string pageHtml, string reqUrl, Encoding encode, string filterReg)
        {
            pageHtml = string.Empty;
            if (!string.IsNullOrEmpty(reqUrl))
            {
                // 设置请求信息,使用GET方式获得数据
                HttpWebRequest musicPageReq = (HttpWebRequest)WebRequest.Create(reqUrl);
                musicPageReq.AllowAutoRedirect = false;
                musicPageReq.Method = "GET";
                //设置超时时间
                musicPageReq.Timeout = SearchConfig.TIME_OUT;
                //获取代理
                IWebProxy proxy = SearchConfig.GetConfiguredWebProxy();
                //判断代理是否有效
                if (proxy != null)
                {
                    //代理有效时设置代理
                    musicPageReq.Proxy = proxy;
                }
               
                try
                {
                    // 获取页面响应
                    using (HttpWebResponse musicPageRes = (HttpWebResponse)musicPageReq.GetResponse())
                    {
                        // 如果HTTP为200,并且是同一页面,获取相应的页面流
                        if (musicPageRes.StatusCode == HttpStatusCode.OK)
                        {
                            // 获取响应的页面流
                            using (Stream pageStrem = musicPageRes.GetResponseStream())
                            {
                                // 读取页面流,获取页面HTML字符串,去除指定的标签
                                using (StreamReader reader = new StreamReader(pageStrem, encode))
                                {
                                    pageHtml = ReplaceHtml(reader.ReadToEnd(), filterReg);
                                    return PageRequestResults.Success;
                                }
                            }
                        }
                        else
                        {
                            return PageRequestResults.UnknowException;
                        }
                    }
                }
}

代码比较简单,就是使用了HttpWebRequestHttpWebResponse对象进行网络请求,其中加入了代理的使用。

Crawler类提供了3个方法,是搜索歌曲列表,歌词列表,以及歌词类容。传入的是搜索信息的实体类,以及搜索歌词和歌曲的接口。然后返回搜索结果列表。主要看一下如何搜索歌曲的。

        public List<MusicInfo> GetMusicList(SearchMusicInfo info,IMusicSearch objSearch)
        {
            List<MusicInfo> _musicList = new List<MusicInfo>();

            string _pageHTML;
                // 请求页面
                NetworkRequest nwr = new NetworkRequest ();
                PageRequestResults result = nwr.RequestPage(out _pageHTML, objSearch.CreateMusicUrl(info), objSearch.PageEncode());
                if (result == PageRequestResults.Success)
                {
                    // 如果请求成功,分析页面
                    _musicList = objSearch.PageAnalysis(_pageHTML);
                    break;
                }
}

上面并不是全部代码,省略的只是请求失败时重试的操作。内部实际是调用NetworkRequest中的RequestPage方法,但这里传入的方法是使用接口IMusicSearch来调用的。这样在实际运行时,会根据加载的搜索插件来决定具体的搜索行为。当得到请求的值之后,调用了接口的PageAnalysis方法,来对页面进行分析,得到音乐文件地址列表。

所以对于MusicCrawler来说,他提供了一个搜索框架,而具体的搜索功能是在插件内部实现的。对于调用者,不需要知道插件的存在;而对于插件,只需要满足接口就能被主程序调用。

 

 

2.ISearch接口

 

对于我们程序来说,制作一个搜索插件都是针对一个网站。所以我们要能从这个网站获得数据,必须知道要请求的页面地址、页面的编码方式、以及如何分析页面得到想要的数据。 所以我们制定接口时就是从这3点出发的。

    /// <summary>
    /// 编码接口
    /// </summary>
    public interface IEncoding
    {
        /// <summary>
        /// 指定页面的编码方式
        /// </summary>
        /// <returns>页面编码方式</returns>
        Encoding PageEncode();
    }

    /// <summary>
    /// 获取歌词信息时要实现的接口
    /// </summary>
    public interface ILRCSearch : IEncoding
    {
        /// <summary>
        /// 分析要采集的HTML页面内容,并返回采集到的歌词信息
        /// </summary>
        /// <param name="PageContent">HTML页面内容</param>
        /// <returns>搜索到的歌词列表</returns>
        List<MusicLrcInfo> PageAnalysis(string PageContent);

        /// <summary>
        /// 创建采集音乐歌词列表时请求的地址
        /// </summary>
        /// <param name="info">音乐搜索信息</param>
        /// <returns>获取所有歌词信息时请求的URL</returns>
        string CreateAllLrcUrl(SearchMusicInfo info);

        /// <summary>
        /// 创建采集指定歌词内容时请求的地址
        /// </summary>
        /// <param name="info">歌词信息</param>
        /// <returns>获取指定歌词内容时请求的URL</returns>
        string CreateLrcUrl(MusicLrcInfo info);
    }



    /// <summary>
    /// 获取歌曲信息要实现的接口
    /// </summary>
    public interface IMusicSearch : IEncoding
    {
        /// <summary>
        /// 分析要采集的HTML页面内容,并返回采集到的歌曲信息
        /// </summary>
        /// <param name="PageContent">HTML页面内容</param>
        /// <returns>搜索到的歌曲列表</returns>
        List<MusicInfo> PageAnalysis(string PageContent);

        /// <summary>
        /// 创建采集音乐信息时请求的URL
        /// </summary>
        /// <param name="info">音乐搜索信息</param>
        /// <returns>请求的URL</returns>
        string CreateMusicUrl(SearchMusicInfo info);
    }

IEncoding指定网页的编码格式,而ILRCSearchIMusicSearch也都继承了此接口。因为不同页面编码方式可能不同,所以必须指定编码方式。而在ILRCSearchIMusicSearch接口中都定义了返回请求页面的地址的方法,并且都有一个PageAnalysis方法用来分析页面得到数据。歌词因为需要请求列表和歌词内容,所以返回2个地址。

在MusicCrawler中我们看到,都是针对这2个接口进行编程,所以,我们的插件实现了这些接口方法就能被使用了。

 

 

3 搜索插件的实现

 

我们先看看音乐搜索的插件,针对的是soso的页面。百度的页面类似,而使用百度API搜索时,返回的是XML。过程是一样,只是分析的方法不同。

    /// <summary>
        /// 分析搜搜音乐搜索
        /// </summary>
        public List<MusicInfo> PageAnalysis(string PageContent)
        {
            List<MusicInfo> lstMusic = new List<MusicInfo>();
            try
            {
                // 获取所有歌曲的DIV块信息,此块包含了MusicInfo信息
                List<string> musicDIV = RegexHelper.GetRegexStringList(PageContent, SOSO_TR_PATTREN, RegexOptions.IgnoreCase | RegexOptions.Singleline);
                if (musicDIV != null)
                {
                    foreach (string div in musicDIV)
                    {
                        // 把每个DIV块中取得的歌曲信息存入歌曲列表
                        lstMusic.AddRange(MusicInfoBuild(div));
                    }
                }
            }
            catch
            {

            }
            return lstMusic;
        }

        /// <summary>
        /// 创建音乐获取URL
        /// </summary>
        /// <param name="info">音乐搜索信息</param>
        /// <returns>URL</returns>
        public string CreateMusicUrl(SearchMusicInfo info)
        {
            try
            {
                int ntype = 0;//ALL
                switch (info.MusicFormat)
                {
                    case SearchMusicFormat.MP3:
                        ntype = 1;
                        break;
                    case SearchMusicFormat.WMA:
                        ntype = 2;
                        break;
                }
                string sosoUrl = "http://cgi.music.soso.com/fcgi-bin/m.q?w={0}&p=1&t={1}";
                return string.Format(sosoUrl, new object[] { info.MusicName + CommonSymbol.SYMBOL_SPACE + info.SingerName, ntype.ToString() });
            }
            catch
            {
                return string.Empty;
            }
        }

        /// <summary>
        /// 指定当前页面编码方式
        /// </summary>
        /// <returns></returns>
        public Encoding PageEncode()
        {
            return Encoding.GetEncoding("GB2312");
        }

在插件中,我们实现了这三个接口。因为我们已知道要请求的页面,所以制定编码方式不是很么难事;而指定请求地址时,需要根据搜索条件进行一定的拼接,关于请求的地址可在前一篇文章中有介绍。而PageAnalysis方法中,则是使用了正则表达式进行分析。这个需要结合页面的HTML接口,从中获得MP3的信息。这里主要的任务是写正则表达式,我们可以在分析页面之前,把一些不需要的标签过滤掉,以简化我们的正则表达式。

因为分析的内容和要获取的数据都很多,所以我们的正则表达式可能会很长,这个时候执行的速度可能会很慢,一个有效的办法就是分2次进行匹配。在分析百度页面时我就遇到过,分析一次要900多毫秒。而拆分成2个表达式进行2次分析一共只需要几毫秒。

/*
 *  eg.
 *  //<result>
 *  //<lrc id="124962" artist="Jason Mraz" title="I/'m Yours" /> 
 *  //<lrc id="108753" artist="Jason Mraz" title="17 I'm Yours (Original Demo)" /> 
 *  //<lrc id="240096" artist="Jason Mraz (英汉对照)" title="I'm Yours" /> 
 *  //<lrc id="54134" artist="Jason Mraz" title="I'm Yours" /> 
 *  //<lrc id="3252" artist="Jason Mraz" title="I'm Yours (Corrected Album Version)" /> 
 *  //<lrc id="73843" artist="Jason Mraz (Album)" title="I'm Yours" /> 
 *  //<lrc id="60945" artist="Jason Mraz (MV version)" title="I'm Yours" /> 
 *  //<lrc id="57234" artist="smelly" title="I'm yours" /> 
 *  //<lrc id="27861" artist="Lonny Bereal" title="I'm Yours" /> 
 *  //<lrc id="278148" artist="TWO-MIX" title="I'M YOURS" /> 
 *  //</result>
 */

而在搜索歌词时,我们使用的是千千静听提供的接口,他和百度搜索API一样,返回的是XML,所以我们可以以XML进行操作,效率要比正则高很多。只是搜索歌词时,首先返回的是歌词列表。我们需要从歌词列表中获得歌词的id,并且需要进行编码之后才能请求到真正的歌词,但是千千静听并没有公开编码方式,不过网上已经有了,项目的TTEncode中包含了编码方法。

 

 

4.存在的问题

 

因为一开始想法比较简单,就是做一搜索软件。所以考虑的并不周全,而当基本实现后发现了问题,又比较难在去全局感动了。

 

问题1:一个页面需要多次请求

MusicCrawler的好处是帮我们封装了网络请求操作,所以我们实现插件时,只用去关注如何分析得到数据,而不需要管如何请求数据。因为一开始并没有坐成插件形式,而且只有百度和搜搜2个站点获取数据,所以并没有考虑多次请求的问题。后来就发现了问题。soso是把所有MP3地址都放在了一个DIV中,这样只需要获取一次;而百度的MP3却是需要点击搜索列表的歌曲名,进入下一个页面,在获得MP3的地址。于是这就有了问题,我们的MusicCrawler只能提供一次搜索。

那么让插件自己去做第2次请求,那么是使用我们的NetworkRequest,还是自己实现呢?这是个挺麻烦的问题。Crawler并不可能知道你要请求几次,并不知道那一个返回的htmlString是用于分析的。但是插件自己去做请求,那么一些设置就无法使用,比如代理。

 

问题2:无法执行页面的JS

同样是百度页面的问题,百度页面的中的MP3地址是通过JS获得的,我们直接http请求是得不到的。我们可以在程序中使用webBrowser来执行JS,这也是遗留下来的一个问题。这个问题解决的话,讲有更多的网站可以作为数据源。因为很多网站MP3地址都是通过JS获得的。

 

问题3:执行速度

执行速度来说,在网络良好的情况下,速度还是可以;即便不是用百度的页面搜索,目前搜索出来的歌曲也足够,soso搜索到的歌曲基本都是有效的。但是在网络不好的情况下,速度就不行。程序受网络影响比较大。而类似QQ音乐,也有搜索其他站点的数据,但是他应该是有自己的数据库来存放这些数据。所以我们搜索,实际是对数据库的请求;而我们软件是完全来自于网络的。当然也考虑过是用数据库进行存储,但是对于单机是用,意义不大。

 

 

 

三 加载插件进行搜索

 

 

前面已经介绍了搜索的接口,搜索插件的实现方法和存在的问题,最后就来看看是如何是用这些插件,来进行搜索的。MusicRunner工程主要负责这些操作。

        /// <summary>
        /// Runner初始化
        /// 功能:插件加载
        /// </summary>
        public static bool Initialize()
        {
            MusicClientConfig innermcc = MusicClientConfig.GetInstance();
            //获取“Plugin”目录下的文件
            string[] filepaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory + "/Plugin/","*.dll");
            foreach (string ItemPath in filepaths)
            {
                FileInfo fi = new FileInfo(ItemPath);
                //找出后缀为DLL的文件,并实例化它
                if (fi.Extension.ToLower().Equals(".dll"))
                {
                    Assembly assembly = Assembly.LoadFile(ItemPath);
                    //动态实例化类库
                    object objSearch = assembly.CreateInstance(string.Format("CMusicSearch.{0}.MainSearch", fi.Name.Replace(fi.Extension, string.Empty)), false);
                    if (objSearch == null)
                    {
                        continue;
                    }
                    //加载音乐搜索插件
                    if (objSearch is IMusicSearch)
                    {
                        innermcc.MusicSearcher.Add(new KeyValuePair<string, IMusicSearch>(Guid.NewGuid().ToString(), (IMusicSearch)objSearch));
                    }
                    //加载歌词搜索插件
                    else if (objSearch is ILRCSearch)
                    {
                        //lrcSearcher.Add(new KeyValuePair<string,ILRCSearch>(Guid.NewGuid().ToString(),(ILRCSearch)objSearch));
                        innermcc.LRCSearcher.Add((ILRCSearch)objSearch);
                    }
                }
            }
            if (innermcc.MusicSearcher.Count < 1)
            {
                return false;
            }
            return true;
        }

在Initialize方法中,我们去Plugin目录下加载了所有的插件。因为搜索音乐和歌词实现的接口不同,我们容易区分出这两种插件。然后把加载的插件的实例对象,存放到一个内部列表中。

         /// <summary>
        /// 搜索歌曲方法
        /// </summary>
        /// <param name="info">搜索信息</param>
        /// <returns>歌曲列表</returns>
        public List<MusicInfo> SearchM(SearchMusicInfo info)
        {
            Crawler crawler = new Crawler();
            List<MusicInfo> lstMusic = new List<MusicInfo>();

            // 遍历插件,搜索音乐
            foreach (var item in mcc.MusicSearcher)
            {
                //根据加载的插件所提供的方法,获取音乐信息
                lstMusic.AddRange(crawler.GetMusicList(info, item.Value));
            }
            MusicDistinctHelper.Distinct(ref lstMusic);
            //TODO:在各自的插件中过滤MusicFormat后,在这里是否最终输出也过滤一次
            return lstMusic;
        }

        /// <summary>
        /// 搜索歌词列表方法
        /// </summary>
        /// <param name="info">搜索信息</param>
        /// <returns>歌词列表</returns>
        public List<MusicLrcInfo> SearchL(SearchMusicInfo info)
        {
            Crawler crawler = new Crawler();
            List<MusicLrcInfo> lstMusic = new List<MusicLrcInfo>();
            foreach (var item in mcc.LRCSearcher)
            {
                //根据加载的插件所提供的方法,获取歌词信息
                lstMusic.AddRange(crawler.GetMusicLrcList(info, item));
            }
            return lstMusic;
        }

而在Runner中,我们提供了3个对外的方法,用来搜索歌曲和歌词。我们看到,在内部,我们实际是调用的Crawler对象中的方法。我们遍历了歌词和歌曲搜索插件的对象,并把他们传递给了Crawler的搜索方法。也就是告诉了Crawler,我们使用这些插件去搜索。因为这些插件都实现了ISearch中定义的接口,所以在Crawler内部,他们调用了接口方法,通过多态,实现了各自的搜索功能。最终返回结果数据。

         MSLRCRunner Finder = new MSLRCRunner(); //搜索对象
        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                Thread searchThread = new Thread(
                    delegate()
                    {
                        SearchMusicInfo info = new SearchMusicInfo() { MusicName = EncodeConverter.UrlEncode(textBox1.Text.Trim()), MusicFormat = SearchMusicFormat.MP3 };
                        var list = Finder.SearchM(info);
                        info.MusicName = textBox1.Text;
                        var lstlrc = Finder.SearchL(info);

                        //多线程使用UI控件
                        this.Invoke(new Action<List<MusicInfo>, List<MusicLrcInfo>>(DataBind), new object[] { list, lstlrc });
                    });
                searchThread.Start();

            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }

        }

而在Test中,调用就非常简单,创建搜索对象的实体类,然后使用MSLRCRunner的对象Finder调用SearchM,就能获得搜索结果了。

 

 

四 总结

 

 

有关搜索就介绍到这里,其实搜索的思想很简单,类似一个简单的采集程序。但是目前最大的问题就是没有实现执行JS。这个有机会要解决掉。呵呵。下一篇就开始介绍下载部分了。


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注