上一篇文章已经写了有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;
}
}
}
}
代码比较简单,就是使用了HttpWebRequest和HttpWebResponse对象进行网络请求,其中加入了代理的使用。
而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指定网页的编码格式,而ILRCSearch和IMusicSearch也都继承了此接口。因为不同页面编码方式可能不同,所以必须指定编码方式。而在ILRCSearch和IMusicSearch接口中都定义了返回请求页面的地址的方法,并且都有一个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。这个有机会要解决掉。呵呵。下一篇就开始介绍下载部分了。