准确记录用户观看视频内容时长

2023-10-28


问题的产生

to be or not to be, that is a question. 不是问题解决不了,只是你自己不够努力,当然,也可能是你不够聪明0.0。有效地记录用户观看某一视频的总时长,问题的来源在于用户拥有自由意志,可以随意对观看的视频进行 快进 快退,倍数播放等等。那么,对于要拿视频卖钱的主儿们来说,怎么才能精确记录下来呢?


闲话少叙,让我们骑上心爱的小摩托,温柔地开到主题中来。根据爱因斯坦的相对论,我把解决这个问题的方案,大致分为四种:一、天真可爱法。二、录点法。三、打点法。四、暴力打点法。


一、从最简单的开始

正常情况,用户(假设是小王好了)一个视频的播放进度如下:设 视频总时长为10,小王从0开始,看到6他就退出了,也就是A点,那么现在的观看总时长t=6-0=6,如下图;
也就是说视频的播放进度 p 和观看时长 t 是相等的。如果小王在A点没有退出,而是直接快进到了8,然后又看到了9,这时才退出,那么现在的观看总时长t=(6-0)+(9-8)=7;那么这时播放进度 p(=9),和总观看时长t(=7),就显然不一致了。如果小王在B点,也就是8的时候开启了 2倍速度,那么现在的总时长就应该是 t= 6+1*(1/2)=6.5了。。。他和观看进度p(=9)之间似乎更是无情地天人永隔了。
在这里插入图片描述

不过,先别慌,问题不大,我们的难点其实是下面的。我们假设小王看的是一段小视频,其中的某个片断引起了他身体的激烈共鸣,于是他就将这个片断重播了N次,如下图 ,3-5这一段他看了整整30遍,然后在7的时候退出。那么这时p=7,t=7+(5-3)*29=65,这里减掉本来看的一次所以乘29。对于只关心内容进度的来说,重复的29次我并不关心,我想要的是数据是7。。。
在这里插入图片描述

本文讨论的重点也是针对小王到底看了哪些内容,而不是他重复了多少遍。


二、天真可爱法

刚开始,你也许会想,不如我拿一个小王开始播放的时间点 startPoint,再拿一个结束播放的时间点endPoint,两者一减,得一个值 T0,再看一下进度T1,然后看根据产品大人的需要,两者之间取个最小值 或 最大值 好了。对于本文关心的问题,也就是取较小值。即 t=min(T0,T1)。考虑到用户小王同志一次看不完,那这个数据得存服务器,aka 后台。于是最终的计算公式为 t=min(T0,T1),其中T0=endPoint- startPoint + t0-tp,t0就是从后台取的上次的播放时长,tp为中间视频暂停的时长,也就是pauseTime。结束播放时将t丢给后台。但是别忘了我们一开始提出的问题,重复播放和小王对进度条的大气而任性地随意拖动。所以这种方案我们只能希望小王是个天真的人,而使用这个方案的,则是比较可爱的团队。

当然如果只关心用户实际观看总时长(即与内容无关,重复也算上,也与倍数、播放进度、快进快退无关),用这种方案就可以了。下面的东西主要是针对内容敏感的时长,也就是需要计算用户 【播放内容】 占 【视频总时长】 的百分比。


三、录点法

其实我们通过画的进度条图,已经可以发现一些事情的端倪。假设 上一次小王已经看了1秒钟,t0=1,进入播放时直接是1,播放到3又快退到2,然后播放到4又快进到6,看到8的时候离开。如下图。那么他现在一共看的时长应该是t=t0+(3-1)+(4-2)+(8-6),发现在什么了没有,对的,需要去除重复的播放时段。我们只需要记录下用户播放时的进度改变点jumpPoint,然后做成区间,对这个区间再做一个去除重复,就万事大吉了。
在这里插入图片描述

比如我当前记录了用户的一次播放记录为列表list ={[0,7],[1,10],[12,15]}因为[0,7]这个区间在和[1,10]有重复部分,所以我们算的时候就先对记录的表里的数据做一次合并去重算法M,现在的列表就为list={[0,10],[12,15]},现在的总时长很明显就是t=(10-0)+(15-12)。如下图:
在这里插入图片描述

好了,我们的方案就是这么简单,在每一次用户发生jumpPoint的时候,记录下当前的点,放进一个list表中,最后计算总时长。具体怎么做呢?仔细看看以上洒家车的几张简单又优美的刻度图,您就会有所了悟了。那就是在发生 开始播放、 快进、快退、步进、步退,暂停,退出等行为时,记录下当前视频进度点,为了方便,后面 快进快退步进步退 统一称为 jumpPoint,因为他们的实质是一样的,而暂停、退出、结束统一称为pausePoint,因为对于我们研究的问题来讲,他们也是一个东西的。

好了,有了方法了,具体要怎么搞呢?就算你给我一个漂亮的机器人女友,不能不教我怎么激活呀。为了方便理解,本文用全世界都明白的java作示例。首先我们定义一个Section对象,section 有两个字段:startPoint,和endPoint,用来记录一次区间的发生,如section=[3,5],那么startPoint=3,endPoint=5,然后我们需要一个漂亮的列表,为了印象深刻一点,我们就叫它红茶表list吧,用这个list来存放这些Section。看图说话,由A点开始,视频开始播放了,我们先来一个section0,它的startPoint就记录下来为1(A点),section0=[1,]。假设看到4(B点)的时候,快退到C点,这时我们section0的endPoint就记录为4(B点)section0=[1,4],同时再来一个section1,将section1的startPoint记录为jump行为发生后的进度3(C点)section1=[3,],假设看到5(D点)用户又快进到7(E点),这时就将section1的endPoint记录为5(D点) section1=[3,5],同时将section2的startPoint记录为7(E点) section2=[7,],9的时候pause了,记录为section2的endPoint , section2=[7,9]。这时我们就得到了list ={[1,4],[3,5],[7,9]}这样一个红茶表。我们将表里的点做一次merge去重,得到list ={[1,5],[7,9]},这就是最终我们要的东西了。
在这里插入图片描述

好了,你说的道理我都懂了,但是臣妾真的做不到呀。这些奇奇怪怪的点要怎么录呀?考虑到很多人会用第三方的播放sdk,或者你说我根本就拿不到这些点的数据,那么您先别急,下边还有办法。这里先把能拿到这些点的东西讲完。

拿到这些点了,你以为就完了吗? 当然如果正常的话,没有其他情况,用户默默地看完了一整个视频,那么我们每次的红茶表里应该就一个section记录,终点减起点,over找小妹子去了。但考虑到这是个复杂的社会,很多人都有着复杂的人生,他们的自由意志不是我们能控制的。万一他就是不停地点拖进度条呢?那你最后的列表不是特别大?那算起来不是很麻烦?所以针对这种情况,我们可以每次有新的露点记录进入红茶表时,都做一次去重算法M,大大优化性能。比如正常情况,第一次录点为,[1,3],list={[1,3]},第二次露点为[2,4],那么第二个点入表的时候,我们可以直接M算法merge掉它,那么红茶表里就是list={[1,4]}。
在实际中,如果不涉及离线视频播放,也就是用户可以下载下来视频观看,那么前端直接算好时长丢给后台就好了,涉及到离线视频播放,那我们就需要把这个做完最后M算法的红茶表丢给后台,后台拿到这个表后,取出数据库的之前存的表,做一次M算法,更新入库。而前端下一次播放的时候,也会先请求后台的表,拿到上次的记录做为初始表,使用此表进行本次优雅的露点行为。考虑到有网络错误,上一次没上传成功的情况,初始的时候还需要检查本地是否存了没有上传成功的表,和后台拿回来的表做一次M算法,做为初始表。

好了,露点法已经介绍完了,相关的示例代码会用java写在最后。至于倍速问题,请耐心看到最后哦亲~

这种方法的性能最高,资源消耗最小,比较推荐。这边呢,建议您仔细研究您的视频播放Sdk,看能不能使用代码注入,方法重载加接口,反射等方式顺利进行露点行为。


四、打点法

在明白了我们要干的情事之后,面临的另一个问题是,很多点我拿不到,于是打点(酱油)法就应运而生。

这里直接说我们要的干的事情,然后再解释它的道理。
我们需要一条计时线程,从视频开始播放后,每隔一段时间d,就打下一个点,记录入表。这里为了方便,取d=5s,也就是每隔5秒打一个点。最初的样子如下:A点打下sA=[0,],B点打下sA=[0,5],sB=[5,],C点打下sB=[5,10],sC=[10,],以次类推。。。初始表如下list={[0,5],[5,10],[10,15]},做一次M算法后,list={[0,15]},最终结果就是15秒。请注意洒家画的乃是一条射线了。
在这里插入图片描述

好了,我们继续进化。 虽然以上的情况会是大多数,但正常情况我们说用户播放中会进行视频jump行为,小王同志说,我跳,我跳,我跳跳跳,诶,你要把我怎么着?下面进入打点 (酱油)法的正式传说。 首先我们要开一条计时线程,从视频开始播放,每隔d开始打点,发生pause行为和jump行为的时候重新打点,为了计算出jumpPoint的具体值,我们需要一个counter,在d时间里,每隔1秒增加1,大于等于d重新从0开始计数,具体的风骚操作如下:

在这里插入图片描述

如图,d=5,那么我们的counter在打点间隔5s内,就重复计数1,2,3,4,5,数到5再循环1,2,3,4,5。正常打点都没什么问题,如图,假设我们正常打了点,红茶表list={[0,5],[5,10]},优化起见,每次新点入表进行一次M算法,现在list={[0,10]},然后在未来的某个点X,小王同志发现昨晚忘了喂猫,于是快退到了6s,这个G点。如图,我们A,B,C,每个点都打好了点,现在的状态用伪代码可表示为list={[0,10],[10,]},其中sX=[10,]。那我们要做的,就是找出X点的值,因为让人兴奋的G点是点击跳转的点,它的值我们肯定拿得到。所以jump行为发生后,我们需要保持list表现在的状态,重新打个新点,即在G点处,新记一个section,sG=[6,],然后我们那条计时的线程从C点开始的一个d(=5s)跑完了,到了H,这个时候,敲黑板!就比较重要了!我们看图说话,从C点到X,G,H,现在经过的总时间是5s,假设现在H点的值是8,那么t(H-G)=8-6=2,那么t(X-C)=d-t(H-G)=5-2=3,那么X点的值,pX=10+t(X-C)=10+3=13,实际上 t(XD)=t(GH),这时我们就知道了sG=[6,8],sX=[10,13],将两个section入表list={[0,10],[10,13],[6,8]},同时H点重新开始新一轮的打点sH=[8,],list做一次合并merge算法list={[0,13],[8,]}。

大家可能会问,那快退呢?快退其实是一样的道理,这理不再重复了,有兴趣可以自己画图算一下就很明白了。那么我们的counter来干嘛?counter没卵用吗?还是拿刚刚的例子,counter的作用,是在于X点跳转后,用户在未到达H点的时候就pause掉了。假如我在P点pause掉了,现在一个d还没计时完,所以刚刚的算法就没办法进行,思前想后,也只好加个counter。比如小王在C点之后,看到X点发生了jump,jump到G点,还没到我们的d跑完,也就是还没到打点的地方,没办法只能看现在counter的值了,比如counter现在的值是4,P的值是7.1,那么t(P-G)=7.1-6=1.1,,那么t(X-C)=4-1.1=2.9,那么现在sG=[6,7.1],sX=[10,12.9],那么list={[0,10],[10,12.9],[6,7.1]},做一次M算法之后,list={[0,12.9]}。

很明显,这种方式会有误差,而且误差与counter的精细度成反比,比如counter每隔0.5s计一次,那么由于pause造成的误差就会更小。
另外,由于有倍速播放,所以我们计算t(X-C)的时候,应该除以倍数speed,才能得到更为精确的值。拿最开始的例子来讲,从C到X,到G到H,其他值不变,但若是1.5倍播放,那么t(H-G)=(8-6)/1.5=1.33,那么 t(X-C)=5-1.33,即d(X-C)=t(X-C)/1.5=5.5,这里的d(X-C),表示从C到X进度条往前跑的长度,所以X的值就是15.5,那么现在list={[0,10],[6,8],[10,15.5]},使用SM算法之,list={[0,15.5]}。

综上,打点法,很考验初中数学知识,要精确点的话,很烦琐。而且每次pause行为都会面临不同程度的精度丢失。



五、暴力打点法

打点法真的是一种无比烦琐而蛋疼的打法呀,那有没有简单粗暴一点的办法呢?诶,今天您算是问对人了。下面呀,就让我们跟随作者的脚步,一起走进 暴力打点法 背后,那不为人知的秘密。

很简单,在视频开始播放后,直接开一条线程,每隔1s打一个点,如果某个section的startPoint和endPoint差值的绝对值大于1,则直接丢弃该section。如下图:正常情况,我们会得到一个这样的红茶表list={[0,1],[1,2],[2,3],[3,4]…}如果某个section长成这样[5,9],或者[6,4.5]那我们就判定为这两个section是发生了快进 或者快退之类的jump行为,直接无情地丢掉它。list={[0,1],[1,2],[2,8],[8,4],[4,5]}那么丢弃掉[2,8],[8,4]后,list={[0,1],[1,2],[4,5]},使用SM算法暴之,list={[0,2],[4,5]}。实际上每打一个点我们就可以做一次M算法,大大提高性能,需要丢弃的section直接判断不入list即可。在实际应用中,播放器的时间单位基本上都是ms,所以判断某个section该不该丢弃,可以这样 abs(startPoint-endPoint)>1000 ms,考虑到播放进度中的网络延迟等情况,可以将判断标准适当放宽一点,比如 abs(startPoint-endPoint)>1100 ms。这样基本就o了,非要再精确点,再除以现在的倍数就好了,即 abs(startPoint-endPoint)>1100 ms/speed。

在这里插入图片描述

这种方式相较于打点法,简单易行,就是性能上会差些,毕竟每隔1s都要做一次记录section入表和M算法,而且用户每一次jump行为就会丢失一次精度,增加一丢丢误差。


六、小结

实际上,不同前端可以选择自己中意的方式进行录表,比如pc端可以用打点法,IOS可以用录点法,他们都是基于录点去重的方案,这也是此种方案的优点之一。

下面简单地写两段代码好了,用以卑微地表示这是一篇技术文章:


Section可以定义成这样:为什么要放speed在这里我也忘了,也许用得上,也许用不上,望少侠自行斟酌。

public class Section {
   private long startPoint; 
   private long endPoint;
   private float speed;

    public Section() {
    }
    
    public Section(float speed) {
        this.speed = speed;
    }
    
 	 public Section(long startPoint, long endPoint) {
        this.startPoint = startPoint;
        this.endPoint = endPoint;
    }

    /*get set 省略*/
}

然后我们的M法算长这样。这里我只是随手写了一种比较容易理解的方式,当然还有其他很多高效的算法,各位大佬都那么聪明,请自行研究了。简单的测试了下,好像没啥问题,实际不知道,有问题欢迎随时反馈在下边的评论里,以便我及时的不作处理。


import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

/**
 * Author:v
 * Time:2021/4/22
 */
public class MergeUtil {
    public static void main(String[] args) {
        final LinkedList<Section> redTeaList0 = recordedList0();
        final List<Section> redTeaList1 = recordedList1();

//        mergeList(redTeaList0);
//        getTotalTime(redTeaList0);

        mergeList(redTeaList1);
        getTotalTime(redTeaList1);
    }

    private static long getTotalTime(List<Section> list) {
        if (list == null || list.size() == 0) {
            return 0L;
        }

        long t = 0L;
        for (Section s : list) {
            t += s.getEndPoint() - s.getStartPoint();
        }
        System.out.println("Total time is:" + t);
        return t;
    }


    public static List<Section> mergeList(List<Section> rawList) {
        if (rawList == null || rawList.size() == 0) {
            return rawList;
        }

        System.out.println("********before sort*******************");
        printList(rawList);

        rawList.sort(new SectionComparator());

        System.out.println("********after sort*******************");
        printList(rawList);

        merge(rawList);

        System.out.println("********after merge*******************");
        printList(rawList);

        return rawList;
    }

    private static void merge(List<Section> rawList) {
        ListIterator<Section> iterator = rawList.listIterator();
        Section tmp = iterator.next();
        //System.out.println("tmp  " + tmp.toString());
        while (iterator.hasNext()) {
            Section next = iterator.next();
           // System.out.println("next  " + next.toString());
            if (mergeSuccess(tmp, next)) {
                iterator.remove();
            } else {
                tmp = next;
            }
        }
    }

    private static boolean mergeSuccess(Section pre, Section next) {
        if (next.getStartPoint() <= pre.getEndPoint()) {
            pre.setEndPoint(Math.max(pre.getEndPoint(), next.getEndPoint()));
            return true;
        }

        return false;
    }


    /**
     * @return 模拟记录的表
     */
    private static LinkedList<Section> recordedList0() {
        final LinkedList<Section> ret = new LinkedList<>();

        Section s1 = new Section(0L, 12_000L);
        Section s2 = new Section(12_000L, 53_000L);
        Section s3 = new Section(56_000L, 99_100L);
        Section s4 = new Section(99_100L, 120_000L);


        ret.add(s2);
        ret.add(s1);
        ret.add(s4);
        ret.add(s3);

        return ret;
    }


    /**
     * @return 模拟记录的表
     */
    private static LinkedList<Section> recordedList1() {
        final LinkedList<Section> ret = new LinkedList<>();

        Section s1 = new Section(1_000L, 5_000L);
        Section s2 = new Section(3_000L, 10_000L);
        Section s3 = new Section(0L, 6_000L);
        Section s4 = new Section(11_000L, 15_000L);


        ret.add(s1);
        ret.add(s2);
        ret.add(s3);
        ret.add(s4);

        return ret;
    }


    private static void printList(List<Section> list) {
        for (Section s : list) {
            System.out.println(s.toString());
        }
    }

    /**
     * 按 startPoint 由低到高排序
     */
    private static final class SectionComparator implements Comparator<Section> {

        @Override
        public int compare(Section o1, Section o2) {
            return (int) (o1.getStartPoint() - o2.getStartPoint());
        }
    }
}
}

下面是简单测试的两个红茶表的输出,各位可以多加点用例测试一下,看看有没有啥问题。

********before sort*******************
Section{startPoint=12000, endPoint=53000}
Section{startPoint=0, endPoint=12000}
Section{startPoint=99100, endPoint=120000}
Section{startPoint=56000, endPoint=99100}
********after sort*******************
Section{startPoint=0, endPoint=12000}
Section{startPoint=12000, endPoint=53000}
Section{startPoint=56000, endPoint=99100}
Section{startPoint=99100, endPoint=120000}
********after merge*******************
Section{startPoint=0, endPoint=53000}
Section{startPoint=56000, endPoint=120000}
Total time is:117000

//第二个
********before sort*******************
Section{startPoint=1000, endPoint=5000}
Section{startPoint=3000, endPoint=10000}
Section{startPoint=0, endPoint=6000}
Section{startPoint=11000, endPoint=15000}
********after sort*******************
Section{startPoint=0, endPoint=6000}
Section{startPoint=1000, endPoint=5000}
Section{startPoint=3000, endPoint=10000}
Section{startPoint=11000, endPoint=15000}
********after merge*******************
Section{startPoint=0, endPoint=10000}
Section{startPoint=11000, endPoint=15000}
Total time is:14000

七、大结

End

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

准确记录用户观看视频内容时长 的相关文章

随机推荐

  • C语言-二维数组做函数的参数

    文章目录 1 引例 2 观点1 这种使用方法是错误的 3 观点2 根本不需要这么做 4 二维数组做函数参数的方法 4 1 方法1 4 2 方法2 4 3 方法3 5 与Java的不同 1 引例 下面的程序很简单 定义了一个PrintMatr
  • Talib技术因子详解(一)

    talib安装方式 pip install Ta lib Tushare数据获取请参考 金融量化分析基础环境搭建 数据获取代码 import tushare as ts ts set token Tushare的token pro ts p
  • 如何解决从git上下载很慢的问题

    在国内从git上面下载代码的速度峰值通常都是20kB s 这种速度对于那些小项目还好 而对于大一些的并且带有很多子模块的项目来讲就跟耽误时间 虽然有很多提速的方法 但是实际用起来并不稳定 这里提供一套新的方法 下载速度可以至少达到 2MB
  • [1214]基于Python实现视频去重

    文章目录 基于Python实现视频去重 基本原理 实现方法 其它视频去重code 基于Python实现视频去重 基本原理 一款基于Python语言的视频去重复程序 它可以根据视频的特征参数 将重复的视频剔除 以减少视频的存储空间 它的基本原
  • MySQL之字符串函数

    字符串是由零个或多个字符组成的有限序列 一般记为 s a1a2 an n gt 0 通常以串的整体作为操作对象 如 在串中查找某个子串 求取一个子串 在串的某个位置上插入一个子串以及删除一个子串等 假如结果的长度大于 max allowed
  • OKL4 的故事

    转自 弯曲评论 编者注 Gernot 的这篇 blog 介绍了一些 NICTA 和 OK lab 的故事 关于 NICTA 和 OK lab 的来历 读者如果感兴趣 可以阅读我以前写的这篇文章General Dynamics 收购 Open
  • java.util.ConcurrentModificationException

    增强for底层用了迭代器 会导致遍历的时候修改集合中的元素出现java util ConcurrentModificationException 这是因为ArrayList底层维护了一个modCount用于记录list集合修改的次数 每操作
  • 揭示常见的重构误区

    作者 Danijel Arsenovski译者 张逸 公正地说 NET社区对于重构技术的研究起步太晚 直到今天 Net开发的旗舰产品Visual Studio仍然无法在C 中突破重构的界限 http www martinfowler com
  • java一朵玫瑰花代码

    这是一个简单的 Java 程序 画一朵玫瑰花 import java awt import java awt geom import javax swing public class Rose extends JFrame public R
  • C++动态库制作

    简介 概念 Linux系统 Windows系统中都有静态库 动态库 静态库是链接阶段与某程序指令链接到一块 打包为一个整体 而动态库是程序执行过程中动态装载到内存中的 链接时不打包 命名规则 Linux 静态库 libxxx a 动态库 l
  • [数据分析]-目标检测python删除xml文件中指定name的object类别

    1 问题描述 在整理数据集的时候 由于部分数据集来自网络 其一张图片标注了多个类别 而我们只想使用其中的某些类别 如果不去管那些无效类别 在训练时可能会出错 这就需要我们读取xml文件 根据指定的name名称 删除掉无效的object标注
  • android 4.4中的流媒体渲染过程

    第一次写blog 只是为了记下学习的过程 android中东西很多 架构和流程都很复杂 经常发现以前学习过的很多东西 即使当时看明白没多久就忘记了 只能重新拾起再看 于是想起blog这个东东 写下来总不会忘记 也和别人一起共享 以下基于an
  • 多种马尔可夫链预测方法

    一 基于绝对分布的马尔可夫链预测 步骤1 对历史数据进行分组 步骤2 确定观测值的状态 写出频数矩阵 nij i j E和一步转移概率矩阵 fij i j E 其中fij nij n 1 其中n为样本容量 当n 时 可用频数估计概率 从而得
  • tensorflow2.0(三)----循环神经网络(RNN)

    class DataLoader def init self path tf keras utils get file nietzsche txt origin http s3 amazonaws com text data with op
  • 排序子序列

    问题描述 将一段数组分为若干个排序子序列 排序子序列就是非递增或非递减的排序序列 然后输出至少可以分为几个排序子序列 解决方法 输入数组之后 开始遍历数组 如果数组满足非递增或者非递减 就进入对应的条件判断 然后此时如果i 1的元素依然满足
  • javascript构造函数如果没有形参允许省略圆括号

    javascript构造函数如果没有形参允许省略圆括号 var o new Object var o new Object 两条语句完全等价
  • element-ui表单校验

    默认表单校验 在最外层
  • 【业务功能篇82】微服务SpringCloud-ElasticSearch-Kibanan-docker安装-Nginx安装-进阶实战

    四 ElasticSearch进阶 https www elastic co guide en elasticsearch reference 7 4 getting started search html 1 ES中的检索方式 在Elas
  • ubuntu重装NVIDIA显卡(经过记录)遇到问题到卸载ubuntu再到成功安装

    2019 3 10 本来准备升级英伟达官方驱动下载搭配合适CUDA cuDNN GPU tensorflow 网上教程很多 按照教程下载了显卡匹配的最新驱动 卸载了安装ubuntu后在附加驱动里面自动下载的官方驱动390版本 准备禁止X W
  • 准确记录用户观看视频内容时长

    文章目录 问题的产生 一 从最简单的开始 二 天真可爱法 三 录点法 四 打点法 五 暴力打点法 六 小结 七 大结 问题的产生 to be or not to be that is a question 不是问题解决不了 只是你自己不够努