锁定老帖子 主题:java中用正则表达式解析LRC文件
精华帖 (0) :: 良好帖 (1) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2010-11-15
跟着Mars老师 一起写android中的Mp3播放器 真是受益匪浅 再次感谢老师的无私奉献
不过其中问题也确实不少 感觉老师的代码重构做的不够 其中对LRC文件的解析也弄的比较马虎
今天特意花了一天的时间 好好研究了正则表达式 也仔细思索了LRC文件到底应该怎么来解析
以下先分析思路 再给出实现代码
首先 我们应该明白LRC文件的组成 LRC文件本质就是个符合一定格式规范的文本文件 这一点对照XML文件就很好理解了 一个LRC文件的组成 通常由以下几个部分组成 [ti:约定]-------标题 同时应该注意到
然后 我们 用一个实体类 LrcInfo 来封装每个Lrc文件的具体内容
package javamzd.mp3player.Info; import java.util.HashMap; /** * 用来封装歌词信息的类 * @author Administrator * */ public class LrcInfo { private String title;//歌曲名 private String singer;//演唱者 private String album;//专辑 private HashMap<Long,String> infos;//保存歌词信息和时间点一一对应的Map //以下为getter() setter() }
3.读入Lrc文件,开始逐行解析 解析步骤: 1.读入文件 2.封装为BufferedReader对象 3.调用readline()方法逐行读取数据,得到String str 4.用parser()方法解析每一条具体的String语句 5.每句解析完后,将得到的内容在LrcInfo对象中进行设置
import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 此类用来解析LRC文件 将解析完整的LRC文件放入一个LrcInfo对象中 并且返回这个LrcInfo对象s author:java_mzd */ public class LrcParser { private LrcInfo lrcinfo = new LrcInfo(); private long currentTime = 0;//存放临时时间 private String currentContent = null;//存放临时歌词 private Map<Long, String> maps = new HashMap<Long, String>();//用户保存所有的歌词和时间点信息间的映射关系的Map /** * 根据文件路径,读取文件,返回一个输入流 * * @param path * 路径 * @return 输入流 * @throws FileNotFoundException */ private InputStream readLrcFile(String path) throws FileNotFoundException { File f = new File(path); InputStream ins = new FileInputStream(f); return ins; } public LrcInfo parser(String path) throws Exception { InputStream in = readLrcFile(path); lrcinfo = parser(in); return lrcinfo; } /** * 将输入流中的信息解析,返回一个LrcInfo对象 * * @param inputStream * 输入流 * @return 解析好的LrcInfo对象 * @throws IOException */ public LrcInfo parser(InputStream inputStream) throws IOException { // 三层包装 InputStreamReader inr = new InputStreamReader(inputStream); BufferedReader reader = new BufferedReader(inr); // 一行一行的读,每读一行,解析一行 String line = null; while ((line = reader.readLine()) != null) { parserLine(line); } // 全部解析完后,设置info lrcinfo.setInfos(maps); return lrcinfo; } /** * 利用正则表达式解析每行具体语句 * 并在解析完该语句后,将解析出来的信息设置在LrcInfo对象中 * * @param str */ private void parserLine(String str) { // 取得歌曲名信息 if (str.startsWith("[ti:")) { String title = str.substring(4, str.length() - 1); System.out.println("title--->" + title); lrcinfo.setTitle(title); }// 取得歌手信息 else if (str.startsWith("[ar:")) { String singer = str.substring(4, str.length() - 1); System.out.println("singer--->" + singer); lrcinfo.setSinger(singer); }// 取得专辑信息 else if (str.startsWith("[al:")) { String album = str.substring(4, str.length() - 1); System.out.println("album--->" + album); lrcinfo.setAlbum(album); }// 通过正则取得每句歌词信息 else { // 设置正则规则 String reg = "\\[(\\d{2}:\\d{2}\\.\\d{2})\\]"; // 编译 Pattern pattern = Pattern.compile(reg); Matcher matcher = pattern.matcher(str); // 如果存在匹配项,则执行以下操作 while (matcher.find()) { // 得到匹配的所有内容 String msg = matcher.group(); // 得到这个匹配项开始的索引 int start = matcher.start(); // 得到这个匹配项结束的索引 int end = matcher.end(); // 得到这个匹配项中的组数 int groupCount = matcher.groupCount(); // 得到每个组中内容 for (int i = 0; i <= groupCount; i++) { String timeStr = matcher.group(i); if (i == 1) { // 将第二组中的内容设置为当前的一个时间点 currentTime = strToLong(timeStr); } } // 得到时间点后的内容 String[] content = pattern.split(str); // 输出数组内容 for (int i = 0; i < content.length; i++) { if (i == content.length - 1) { // 将内容设置为当前内容 currentContent = content[i]; } } // 设置时间点和内容的映射 maps.put(currentTime, currentContent); System.out.println("put---currentTime--->" + currentTime + "----currentContent---->" + currentContent); } } } /** * 将解析得到的表示时间的字符转化为Long型 * * @param group * 字符形式的时间点 * @return Long形式的时间 */ private long strToLong(String timeStr) { // 因为给如的字符串的时间格式为XX:XX.XX,返回的long要求是以毫秒为单位 // 1:使用:分割 2:使用.分割 String[] s = timeStr.split(":"); int min = Integer.parseInt(s[0]); String[] ss = s[1].split("\\."); int sec = Integer.parseInt(ss[0]); int mill = Integer.parseInt(ss[1]); return min * 60 * 1000 + sec * 1000 + mill * 10; } public static void main(String[] args) { LrcParser lp = new LrcParser(); try { lp.parser("G:\\WebRoot\\a1.lrc"); } catch (Exception e) { System.out.println("parser erro"); e.printStackTrace(); } } }
以上代码难度都不大 个人觉得 正则表达式其实并不难 只是因为有很多不规则符号堆叠在一起 让我们直观的很难理解 掌握符号规则后 还是挺好用的
正则表达在JAVA中都被封装在 regex包下面 主要是Pattern类与Matcher类
其实我个人在掌握了正则的基本概念后 用JAVA写这个代码却花了不少时间
主要是对这两个对象中的一些方法理解错误
以下简单总结下 两个类中易理解错的方法
Matcher对象中 matcher()方法是匹配整个字符串
使用find()方法 得到一个字符串中的匹配后 matcher.start()得到这个匹配的startIndex
matcher.group()能得到满足匹配的全部内容(最大的一个组)
我们可能在某一行可以得到多个匹配结果 每当调用一次matcher.find() 当前匹配对象就自动换为下个匹配成功对象
要遍历所有匹配结果
//遍历每个匹配成功对象
最后 我们解析完LRC文件后 在播放Mp3时 只需要根据播放时间 取出HashMap中的内容进行显示即可 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2010-11-15
没你这么干的
这个东西是被播放器调用的,他怎么知道你的下一条歌词是在几分几秒?我估计你自己都不知道。难道你还打算让他每隔一毫秒来问一下,有就是有,没有就往外抛个异常? |
|
返回顶楼 | |
发表时间:2010-11-15
nuclearg 写道 没你这么干的
这个东西是被播放器调用的,他怎么知道你的下一条歌词是在几分几秒?我估计你自己都不知道。难道你还打算让他每隔一毫秒来问一下,有就是有,没有就往外抛个异常? 呵呵, 那么我想问你 你觉得播放器是怎么调用的呢? 还不是根据LRC歌词的每个时间点来匹配当前播放时间 写这个的目的就是把LRC文件解析封装为LRC对象 然后供播放器调用 至于播放器怎么调用 问题就更简单了 首先: 我们可以观察到Lrc格式中的时间点是00.00S 也就是最小单位是10ms 在播放器中 我们获取当前已经播放时间的时间playedTime为Long型 然后处理 playedTime 对象----去掉个位数 启动轮询线程 每隔10MS 用处理过的的playedTime去Map中判断 如果在这个时间点有记录 则取出记录,更新UI 如果没有记录,则不操作 播放器的显示歌词的原理难道不就是这样实现的嘛? |
|
返回顶楼 | |
发表时间:2010-11-15
最后修改:2010-11-15
你这段程序的目的就是“把LRC文件解析封装为LRC对象”对吧
我在写播放器,现在拿到了一堆(注意是一堆)时间与歌词的名值对 好我现在要做歌词滚动,因为你用的是HashMap,不保存条目之间的顺序,我根本不知道下一条歌词应该在什么时候显示出来 我怎么滚?滚多快?我甚至根本连下一条歌词是什么都不知道 难道我还要遍历一遍这个歌词列表,然后自己冒泡排出一个顺序么……这种事情难道不应该是你的这个“LRC读取组件”该做的么 这事就应该对每一条歌词起一个类,里面是文本、起始时间、持续时间,然后返回一个歌词类的ArrayList或LinkedList。非得用HashMap,不是只用过这个吧 |
|
返回顶楼 | |
发表时间:2010-11-16
规则文件直接用语法解析器
ANTLR http://www.antlr.org/ |
|
返回顶楼 | |
发表时间:2010-11-16
最后修改:2010-11-16
播放器可以提前加载整个歌词,然后在每次计算下句时间到本句时间之间的间隔和歌词间距,然后使用平均速度进行滚动就可以了。
如果是时间轮询反复去查是非常浪费资源的,尤其你是为手机开发程序的话,更要重视资源啊,因为手机的内存资源和硬件都是很有限。一点一滴不能浪费啊 |
|
返回顶楼 | |
发表时间:2010-11-16
楼主的羊羔体写的很不错。
|
|
返回顶楼 | |
发表时间:2010-11-16
nuclearg 写道 你这段程序的目的就是“把LRC文件解析封装为LRC对象”对吧
我在写播放器,现在拿到了一堆(注意是一堆)时间与歌词的名值对 好我现在要做歌词滚动,因为你用的是HashMap,不保存条目之间的顺序,我根本不知道下一条歌词应该在什么时候显示出来 我怎么滚?滚多快?我甚至根本连下一条歌词是什么都不知道 难道我还要遍历一遍这个歌词列表,然后自己冒泡排出一个顺序么……这种事情难道不应该是你的这个“LRC读取组件”该做的么 这事就应该对每一条歌词起一个类,里面是文本、起始时间、持续时间,然后返回一个歌词类的ArrayList或LinkedList。非得用HashMap,不是只用过这个吧 不过真的是我表述的不清楚还是你自己写播放器写成定式思维了 为什么我一定要先从Collection里面 按顺序一条一条的取出歌词,再根据每句歌词的时间等到了这个播放时间再来显示呢? 我就不能以歌曲播放时间,每10MS为一个单位,用这个时间去HashMap里面取歌词呢? 如果取到了,就更新,没取到,不产生任何操作 这样不就完美的解决这个问题了吗? 至于用List或者Quenu,呵呵,你可以真的当我没文化从来没用过吧 只是你就看这样的一行 [02:55.00][01:22.00]也答应永远都不让对方担心 [03:02.00][01:28.00]要做快乐的自己 照顾自己 因为歌曲可能会分段,每句歌词可能在后面的断再出现 于是,我们解析的时候,每次就可能解析的不止一个时间,这时候有两个时间,而且不是按顺序来的,我想问大虾你怎么解决呢? 先保存所有时间,再排序来一个个存? 如果我用HashMap来存的话,只是保存每个时间点和该句歌词的关系,我只要用播放时间去Map中取就好了。 |
|
返回顶楼 | |
发表时间:2010-11-16
Foxswily 写道 规则文件直接用语法解析器
ANTLR http://www.antlr.org/ 多谢兄弟指导 |
|
返回顶楼 | |
发表时间:2010-11-16
bitray 写道 播放器可以提前加载整个歌词,然后在每次计算下句时间到本句时间之间的间隔和歌词间距,然后使用平均速度进行滚动就可以了。
如果是时间轮询反复去查是非常浪费资源的,尤其你是为手机开发程序的话,更要重视资源啊,因为手机的内存资源和硬件都是很有限。一点一滴不能浪费啊 呵呵 Good Question 在手机开发中确实资源是个很重要的问题 “按时间轮询”这句话确实是我自己表述错误了 在android中,用Handler的post()方法启动新线程的时候是调用的该线程的run()方法 然后我只需要在这个run()方法的最后,加上一句handler.postDelayed(updateTimeCallback, 10);这样就能实现每10MS重新启动这个更新歌词线程一次,而且其实都是在主线程内运行的 当然 可能这样又会有朋友质疑:其实这就是个方法的递归调用了。 不过Android中用Handler调用新线程,不用Looper对象,直接post()的话,确实是在主线程中运行的,这样实现确实效果和递归调用个更新歌词方法很相似 不过这样的有点却也是递归调用没有的 递归调用如果这样一直递归的话,会死递归,再也挑不出来了 但是用Handler的post()方法这样启动的线程的run()方法,可以在任何需要的时候remove()掉, 于是当我们需要暂停的时候,只需要先remove()线程,然后记录下已经播放的时间就行。 当继续的时候,就继续post()该线程 总而言之,其实是一直在main()方法中执行的 |
|
返回顶楼 | |