March 2016 Blog Posts

使用Scrapy构建自己的定制网络爬虫

MitchellChu 2016-03-12 其他技术 编程语言
爬虫(Web Spider or Web Crawler),顾名思义:一个在网站之间互相游走的虫子,专好觅食各类页面数据。随着Spider技术门槛降低,爬虫也开始泛滥起来,很多时候爬虫变成了扒虫。然,技术本无善恶,全在用者之念。此处不做过多计较,我们当下要讨论也仅仅是定制一款自己的网络爬虫,仅此。
爬虫就Mitchell个人经验来说分为两大类:
•通用型网络爬虫:该类爬虫并无明确采集目标,每个能够爬及的页面都是其采集对象,除非满足系统指定条件,否则此类爬虫基本全年无休的辛勤劳作。最常见的就是搜索引擎的爬虫,如:Google,Baidu,Yahoo,Bing等;
•专用型网络爬虫:这种爬虫仅针对自身偏好的网站或者主题作为采集目标,采集到的内容或涉及到的URL为此爬虫不感兴趣的,将被爬虫直接忽略,此类爬虫根据需要采集的目标多寡采集时间有所不同。如:各类垂直搜索站,金融爬虫,站点采集等;

C# HttpWebResponse下载限速

MitchellChu 2016-03-10 .NET技术

在使用请求网络内容的时候,一般如下面这种方法:

// 非生产环境代码,请在实际使用时进行必要调整
// —— Mitchell Chu
// Blog:blog.useasp.net

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);

using(var response = (HttpWebResponse)request.GetResponse())
using(var stream = response.GetResponseStream())
{
    byte[] buffer = new byte[4096];
    int read = 0;
    while((read = stream.Read(buffer, 0, buffer.Length)) >0){
        // buffer...
    }
}

 使用这种方式,一旦我们开始下载数据,即是全速下载——即理论上是客户端和服务端的最小速度为当前速度。如果以这个速度持续下载,要不服务端被拖垮,要不就是客户端被服务端封禁。当然,一般情况下,后者居多。在持续的针对某站资源下载的时候,限速是非常必要的。

在HttpWebResponse里面比较简单,只需要控制读取的速度,即可达到限速的目的:

// 非生产环境代码,请在实际使用时进行必要调整
// —— Mitchell Chu
// Blog:blog.useasp.net

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);

using(var response = (HttpWebResponse)request.GetResponse())
using(var stream = response.GetResponseStream())
{
    byte[] buffer = new byte[4096];
    int read = 0;
    long total_read = 0;
    DateTime begin = DateTime.Now;
    TimeSpan ts = new TimeSpan();
    while((read = stream.Read(buffer, 0, buffer.Length)) >0){
        total_read += read;
        ts = DateTime.Now - begin;
        if((total_read / ts.TotalSeconds) > 1024){
            Thread.Sleep(1);  // 休息一下.
        }
    }
}

 

BitConverter.IsLittleEndian在x86的机器上返回false

MitchellChu 2016-03-09 .NET技术 编程语言

在DEBUG写的程序时,发现一个有趣的现象,就是本应该返回trueBitConverter.IsLittleEndian,返回的却是false!但在使用BitConverter之后,再次查看其值,却发现变成了正常的true状态。难道在X86的系统上存在两种状态?

DEBUG的代码如下:

...
// 前面没有使用BitConverter
// DEBUG断点设置到这里的时候,可以看到是false的值
// BitConverter.IsLittleEndian
// 如果使用BitConverter之后,得到的值却是true
byte[] bdata = BitConverter.GetBytes(num);
// 此时DEBUG中看到的是true.

但这明显不科学,首先BitConverter.IsLittleEndian在X86下应该是false,其次,不应该在一个系统内,同时出现truefalse这两种状态。

抱着好奇的心态,翻开了源代码看了一下,看到如下代码:

public static readonly bool IsLittleEndian = true;

赫然写着true啊!哪里来的false?

经过一番搜索,发现有这么一句:

通过调试器读取成员并不会触发执行成员的初始化代码。1

也就是说:通过调试器读取内存中的成员仅仅是读取到该对象的默认初始值而已。有这么一句话,解释起来就豁然开朗了。

为了验证这个说法的真实性,我们可以自己定义一个类来进行验证下:

public static class DebuggerInitTester
{
    public static bool BoolData = true;
}

中我们可以看到,我们可以在没有调用前尝试获取到BoolData值,发现是true,并非是默认的false!这是什么情况,再仔细看了一遍代码,发现BitConverter里面有静态构造函数:

static BitConverter()
{
    // Note: this type is marked as 'beforefieldinit'.
    BitConverter.IsLittleEndian = true;
}

BitConverter中使用了静态的构造函数,而我们的并没有,看来问题在这里了,我们也改造下代码:

static DebuggerInitTester()
{
    DebuggerInitTester.BoolData = true;
}

再次测试,确实,这个时候默认是false了。看起来问题就在这个静态构造函数了,原来不是一直听说static的是在加载前初始化了的么?看来并不是这个样子的,还是Too Young Too Simple!

看来是beforefieldinit引起的问题了(可参见Mitchell的另一篇关于BeforeFieldInit的文章)

如果没有标记,则CLR会在第一次访问静态成员的时候调用静态构造函数对静态类中的字段进行初始化。比如,DebuggerInitTester这个类,我们定义了静态构造函数,因此将初始化BoolData的时机锁定了。

一切看起来是这样了,可再仔细看,又蒙了——压根就没有静态构造函数!BitConverter那个构造函数是自动生成的,是有BeforeFieldInit标记的。

.class public abstract auto ansi sealed beforefieldinit System.BitConverter
       extends System.Object
{
  .custom instance void __DynamicallyInvokableAttribute::.ctor() = ( 01 00 00 00 ) 
}

根据BeforeFieldInit规则,也就是说BitConverter的初始化会在访问前的任意时候完成,自然也可能会和有自定义静态构造函数那样在第一次访问的时候初始化,也许CLR对于是这样处理。

 如果能解释初始化时间点后,调试器读取成员不触发成员初始化代码倒是非常可以理解的,因为这样调试器的调试才安全可靠,不至于导致意外的发生(如:读取导致数据变化带来后继逻辑的)。

 

困惑

BitConverter由于使用了BeforeFieldInit标记,将初始化时机交给了CLR,也就是说这将导致IsLittleEndian初始化时间点不确定,很有可能就是在第一次访问的时候进行初始化的。

令人疑惑的是,为什么自定义的DebuggerInitTester类中的BoolData会那么快就完成了初始化呢?另外测试了好几次(其他类)均是非常早的完成初始化。

这里面到底是什么原因决定CLR来选择BeforeFieldInit标记过的类的初始化时机?

  

参考

  1. Why does BitConverter.LittleEndian return false on my x86 machine?
  2. IsLittleEndian field reports false, but it must be Little-Endian?

 

C#的类型构造器和beforefieldinit标志

MitchellChu 2016-03-09 .NET技术 编程语言

首先让我们来看两个类型的定义:

// 代码来自C# in Depth
// 为了后继的讨论方便,对类名进行了更改(Test改为Test1和Test2)
// ——  Mitchell Chu
class Test1
{
    static object o = new object();
}

class Test2
{
    static object o;

    static Test2()
    {
        o = new object();
    }
}

这两个类,经常会被误认为是一样的,但实际情况是如何呢?让我们编译后,反编译后来看看他们产生的代码:

// IL代码
// Test1和Test2在定义的时候出现了不同.
// —— Mitchell Chu
.class private auto ansi beforefieldinit ClassLibrary1.Test1
    extends [mscorlib]System.Object
{
    // Fields
    .field private static object o

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x206b
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Test1::.ctor

    .method private hidebysig specialname rtspecialname static 
        void .cctor () cil managed 
    {
        // Method begins at RVA 0x205f
        // Code size 11 (0xb)
        .maxstack 8

        IL_0000: newobj instance void [mscorlib]System.Object::.ctor()
        IL_0005: stsfld object ClassLibrary1.Test1::o
        IL_000a: ret
    } // end of method Test1::.cctor

} // end of class ClassLibrary1.Test1



.class private auto ansi ClassLibrary1.Test2
    extends [mscorlib]System.Object
{
    // Fields
    .field private static object o

    // Methods
    .method private hidebysig specialname rtspecialname static 
        void .cctor () cil managed 
    {
        // Method begins at RVA 0x2073
        // Code size 11 (0xb)
        .maxstack 8

        IL_0000: newobj instance void [mscorlib]System.Object::.ctor()
        IL_0005: stsfld object ClassLibrary1.Test2::o
        IL_000a: ret
    } // end of method Test2::.cctor

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x207f
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Test2::.ctor

} // end of class ClassLibrary1.Test2

比较后发现,大体一致,但Test1多了一个beforefieldinit标志。那beforefieldinit这个标志是做什么用的?

 

是什么

MSDN上的表述是:

  • BeforeFieldInit:指定调用此类型的静态方法并不强制系统初始化此类型。

然而,这并不能为我们释惑。

我们再参考下规范(MSDN上关于静态类也有类似描述)1

类的静态构造函数在给定的程序域中最多只执行一次。静态构造函数的执行是通过程序域中的下列事件中第一个出现的事件来触发的:

  • 类的一个实例被创建
  • 类中的任一静态成员被引用

 

和CLI规范(ECMA 335)第8.9.5节有如下表述1*

  1. 类型可以有一个类型初始器方法(type-initializer method),或没有;
  2. 类型可为其类型初始器方法指定一个松语义(为了后继方便,我们称此松语义为BeforeFieldInit)
  3. 若标记为BeforeFieldInit,则类型初始器方法将在或某些时候先于第一次访问此类中定义的任一静态字段的时候执行;
  4. 如果没有标记为BeforeFieldInit,则类型的初始器方法将在下列情况下执行(即被触发):
    • 第一次访问类中的任意静态或实例字段
    • 第一次调用任意类的静态,实例或虚方法

从CLI规范中我们明白,BeforeFieldInit是一种类中成员初始化的方式,用来标明初始化时机。同时,还有一种没有使用BeforeFieldInit的方式,这种方式我们称之为—— 方式3

简单来说:

  • BeforeFieldInit方式,我们不知道.acctor在什么时候运行,一切都将交由CLR做处理;
  • Precise方式,我们可以明确的了解到.acctor的执行时间点。

 

如何定义BeforeFieldInit和Precise

如果我们在类中提供了静态的构造函数,那么,此类将自动变为Precise方式,这也就是为什么类的静态构造函数和CLI规范中未标记BeforeFieldInit的说明一致的原因。

而如果我们在类的定义中没有提供静态构造函数,.NET将会自动为我们提供一个静态构造函数(见后面静态VS实例构造函数),并将此类标记为BeforeFieldInit。

 

使用BeforeFieldInit或Precise

BeforeFieldInit将自身的初始化任务交给了CLR来处理,可以自行决定在合适的时机来调用类的静态进行对静态成员的初始化,这样的一个好处就是CLR可以对其进行更好的优化时机,来提高程序的使用效能。当然,这也有一些小细节需要注意,当多个含有BeforeFieldInit的类同时存在时,正如我们无法了解和控制初始化时机一样,我们同样无法控制和了解到这些类的初始化顺序——一切都在CLR的掌控中,而不是我们。这在需要有序的情况下,就会出故障了,此时,我们需要考虑的是使用Precise方式。

Precise方式初始化时间点是明确的,因此我们可以利用这点,尽量减少对资源的浪费3—— 比如,我们常用的单例模式。

此处应有例子~~~

// BeforeFieldInit 与 Precise的性能比较
// Blog:blog.useasp.net
// Mitchell Chu
class Precise
{
    public static int i = 1;

    static Precise()
    {
    }
}

class BeforeFieldInit
{
    public static int i = 1;
}

class Test
{
    static void Main(string[] args)
    {
        // 测试100,000,000次
        int times = 100000000;
        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < times; i++)
        {
            BeforeFieldInit.i = i;
        }
        sw.Stop();
        Console.WriteLine("BeforeFieldInit take time:{0}ms", sw.ElapsedMilliseconds);
        sw = Stopwatch.StartNew();
        for (int i = 0; i < times; i++)
        {
            Precise.i = i;
        }
        sw.Stop();
        Console.WriteLine("Precise take time:{0}ms", sw.ElapsedMilliseconds);
    }
}

得出结果:

BeforeFieldInit take time:204ms
Precise take time:316ms

 

行文至此,应该是对BeforeFieldInit有所有了解了,那么让我们再看看另外一个例子,这个和哪个类一样?:

// 来自C# in Depth
// 这个例子和开篇两个类中的那个类是一样的呢?
class Test
{
    static object o = new object();

    static Test()
    {
    }
}

 

静态构造函数 VS 实例构造函数

中类的初始化是在类的构造函数中进行的,因此一般类都有构造函数(也叫构造器,Constructor)。

在.NET中,对于没有构造函数的类,一般会自动为其分配一个默认的构造函数,如:对于含有静态成员的类,在没有使用静态构造函数的情况下,.NET会自动为其分配一个静态构造函数(.acctor,需要有字段并默认给定初始化值的情况),此构造函数与实例构造函数(.ctor)异同在于:

  • 两者不存在的时候,.NET均会自动为其生成;
  • .ctor是可编程访问的(在没有定义时,实例化会自动调用默认的),.accotr则无法直接访问;
  • 静态的构造函数(.acctor)提供对静态字段进行初始化,而实例构造函数(.ctor)可以对静态和实例字段进行初始化。

 

 CLI 规范第六版关于BeforeFieldInit的描述:

The semantics of when and what triggers execution of such type initialization methods, is as
follows:
1. A type can have a type-initializer method, or not.
2. A type can be specified as having a relaxed semantic for its type-initializer method
(for convenience below, we call this relaxed semantic BeforeFieldInit).
3. If marked BeforeFieldInit then the type’s initializer method is executed at, or
sometime before, first access to any static field defined for that type.
4. If not marked BeforeFieldInit then that type’s initializer method is executed at (i.e.,
is triggered by): 
a. first access to any static field of that type, or
b. first invocation of any static method of that type, or
c. first invocation of any instance or virtual method of that type if it is a value
type or
d. first invocation of any constructor for that type.
5. Execution of any type's initializer method will not trigger automatic execution of
any initializer methods defined by its base type, nor of any interfaces that the type
implements. 


[Note: BeforeFieldInit behavior is intended for initialization code with no interesting sideeffects,
where exact timing does not matter. Also, under BeforeFieldInit semantics, type
initializers are allowed to be executed at or before first access to any static field of that type, at
the discretion of the CLI. 

  

 参考:

  1. C# and beforefieldinit
  2. 静态构造函数(C#编程指南)
  3. JIT compiler and type constructors(.acctors)

 

FLV音频视频文件的加密解密方式

MitchellChu 2016-03-02 其他技术

FLV文件结构一文中,我们已经了解了FLV文件的大致结构信息,同时在FLV文件校验一文中,我们可以完成对FLV文件的完整性进行校验。此文在这两篇文章的基础上尝试进行更深入一些的探讨——对FLV文件进行加密和解密操作。

在互联网时代,资源的分发已经变得非常容易,也不例外,对于资源的版权方来说,分发难度的降低一方面降低了自身资源的传播成本,另一方面却由于没有良好的防止非法分发而带来对自身的利益侵害。稍稍驻步,我们就能看到已经有很多为保护版权而创造的各种方法,CSS。AACS,Key 2 Audio,Always-Online等等。本文作为保护FLV来说,也算是一种尝试,在实践中,灵活的使用也可一定程度保护资源的非法传播。下面让我们来简单聊聊:

 

Mitchell Chu 注:以下方法名称并非“官方”说法,而是自创,请注意!

 

第一式: 反位

反位的方法算是最基础的方式,原理就是将二进制中的比特位全部取反(注意:不是反码,是特定的异或操作)。代码如下:

byte GetReverse(byte b)
{
    return (byte)(b ^ 0xFF);
}

byte Encrypt(byte data)
{
    return GetReverse(data);
}

byte Decrypt(byte data)
{
    return GetReverse(data);
}

加密:

对每个字节进行操作之后,我们就能得到一个被加密的新的FLV文件,此时,FLV文件的Header,Tag之类的已经全部被抹掉了,剩下的就是一堆数据而已。

解密:

解密此文件的方式非常简单,仅需要对FLV重新再来一遍异或操作即可得到原始文件。

优缺点:

 这种加密方法的优点是,简单的没有负担,无需任何高达上的算法,不需要复杂的操作。但缺点是,只要有原始文件对比后,基本瞬间能了解加密算法,破解难度很低。

说明:

这种方法并非一无是处,相反,操作的恰当,还是有可为之处,比如,我们仅对某些敏感数据进行操作(哪些是敏感的?可以在FLV文件结构一文中找找:P),那么,在表面看起来这是一个完整(也可以是不完整)的FLV,但得到数据,没有得到具体算法的人拿到FLV也不再那么容易发现数据的加工之处。播放之时,却会因为数据的错误导致播放失败。

说到这里,看到网上曾经有了对FLV的加密方式是对每个字节进行-128,在此表示怀疑:此方法能否保证加密后的数据是真的能够被安全还原么?

 

 第二式:对称加密

对称加密这个方法的原理是让合法用户在请求之后,系统同时发送解密密码,这样,用户在获得FLV资源的时候,同时拥有一个匹配的密码,这样,在播放之前,可以通过这个密码来得到正常的FLV源文件。

这个方法的实现种类很多,这里就不放出代码,只要是对称加密方式均可用在此处。

加密:

初始化密码,而后用对称加密算法+密码对FLV文件进行加密,得到加密后的FLV文件。

解密:

获得密码,而后用相匹配的对称加密算法+密码来完成对FLV的解密,得到正常的FLV文件。

优缺点:

如果不嫌事多,可以达到一人一码或者说一机一码(广告词啊,这是),但劣势也明显,一旦获得一次密码,也就丧失了对FLV的控制权,因为解密后,就是FLV原始文件嘛——所以,很多时候,要隐藏机密算法。

说明:

稍显复杂的一个算法,但在使用配套的客户端(Flash Player/PC 播放器等),还是能够比较好的完成对FLV的保护工作的,不同重点的防护是对自身加解密算法和FLV对应文件密码做保护,同时还得顾虑到解密的文件处理方式。

 

恩,本来还有第三式的,但还是到此为止吧,以免贻误众生。

使用上面的加密解密方法基本上已经能够完成对FLV的保护,当然,要保护FLV也不一定要使用加密方式,当年的Key 2 Audio不是通过创造多余的数据区来达到毁灭消费者的光驱么。

本人并不是积极的DRM()拥护者,个人觉得适当保护对版权所有者,正版消费者有一定的益处,但过度的保护反而会让版权者迷失,而失去精进的动力,陷入过度的依赖DRM,反而可能会伤及正版消费者利益,比如:Online-Always的DRM,当年刺客信条的正版用户体验竟然不如盗版 。

 

关于博主

  一枚成分复杂的网络IT分子,属于互联网行业分类中的杂牌军。