标签存档: python

python内置对象的实现

准备回顾一下python源代码,不过不准备说的太细,尽量勾勒框架,不引用代码。

0. python中对象的概念

python中所有东西都是对象,进一步地,这些对象可以分为类型对象(type)or实例对象,有时一个对象即可以是类型,也可以是实例。所有这些对象中,除了内置的类型对象外,别的都生存于堆上,内置的类型对象则静态分配内存。

每个对象头部都有一个PyObject_HEAD(其实对于某些需要被gc管理的对象,它的头部先为PyGC_Head,再为PyObject_HEAD)。变长对象在HEAD后还有一个ob_size表示变长对象元素个数的多少,非字节数。

类型的信息都在它的type对象里,源码中为struct _typeobject,也就是PyTypeObject。比如实例化一个类型,那会先找它的tp_new(找不到的话在父类找),在tp_new中根据该type的tp_basesize进行分配内存,再调用tp_init进行初始化。对类型的实例做运算,比如相加其实也是找type对象中相应的函数指针。type对象中的信息到后来基本都会在类型的dict中和相应的key对应起来。

1. int的实现

int比较简单,关键在于如何高效地实现。python首先有小整数对象。默认在[-5, 257)。如果超出范围则使用通用的缓冲池,对于大整数则有PyIntBlock,用来作缓冲池。一个block大小大概为1000个字节,去掉头部(8字节),可以存82个整数对象。block之间通过指针相连,首指针为block_list,free_list则维护着一条可以链表,free_list链表的下一项由未用的PyIntObject的ob_type来维持。

一些细节:当无可以用缓冲池可用时python会调用fill_free_list来创建一个新的block,并将其插入block_list,再把free_list指向这个block的objects中的最后一个元素。当某个block中的某个int被释放时,它将自己的ob_type指向free_list,并修改free_list等于它的地址,其实就是一个头部插入,这样把多个block间的objects数组联系起来防止出现内存泄漏。一个值得注意的地方是小整数对象池其实也是生活在block里面,在是整个python环境初始化的时候生成。这里可以看出,为int分配的内存是永远也不会被python释放的,所有的int对象使用的内存大小和同时存在的int数量的最大值有关。

2. string的实现

string复杂一些,它是变长对象。对变长对象内存的管理。每个string对象除了头部外还保存了hash值(ob_shash,避免重复计算,初始-1)、是否已经被intern机制处理过(ob_sstate)、指向实际内存的指针(ob_sval),ob_sval指向的应该是一段ob_size+1长度的内存(为了兼容C,字符串要以'\0'结尾)。在从char *创建string时还是比较直接的,就是检查一些边界情况、初始化hash等,最后逐个拷贝char。python中有一个nullstring指向空字符串,通过intern机制共享,所以不会同时存在多个空字符串。

传统缓冲池。相当于int小整数缓冲池,对单个的char,python也会维持一个缓冲池。创建单个char的string时,如果缓冲池里已有,则直接返回。如果没有,根据char创建string,再对它进行intern,再存入缓冲池。

intern机制。python会维持一个dict,用来保存当前已经被创建的string,如果新创建的string已经在这个dict,也就是已经被intern机制处理过了,那么就会直接返回dict中的值。也就是说一般两个相同的字符串的id是相同的。要注意的是,无论字符串有没有存在于这个dict中,python都会创建一个新要string,原因是因为保存在dict中只能是PyObject,因此肯定要创建一个python对象。intern后的string有两种状态,mortal和immortal,区别在于后者永远不会被gc回收。

要提到的是,创建string时使用的是PyObject_Malloc开头的分配函数,一般来讲它不会每次都从os分配内存,而是从python维持的一个内存池中分配。

3. list的实现

list不仅是变长对象,还是可变对象。在变长头部之外PyListObject还保存了一个PyObject **ob_item指针,一个int allocated。ob_item就是指向实际成员的指针,allocated代表了list当前申请的内存能装多少个PyObject,变长头内的ob_size则代表list中已有多少个PyObject。当创建一个list时,需要指定list的大小(参数size),要申请的内存可以分为两部分,一个是list本身,一个是指向成员变量的指针。如果list缓冲池可用(numfree > 0),那就从缓冲池中给list分配一块内存,否则使用PyObject_GC_NEW来分配空间(和string不同,因为list中的成员可以指向其它python对象,这个函数和python垃圾回收的三色标记法有关)。然后再根据size大小分配一段连续的内存来保存指向各个成员的指针,新创建的list中的ob_size和allocated都为size。

创建list后给list里的某个位置赋值比较简单,就是简单的設定指针而已。添加操作要复杂一些,首先会调整list的大小,使得allocate>=size>=allocate/2。如果该等式已经满足,那么不更改list大小,如果不满足的话通过new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) + newsize得出新的allocated大小,再通过PyMem_Resize来调整保存成员指针的内存。resize后则使用最简单的策略移动从插入到结尾的成员指针,再设置该位置上的成员。删除成员时实际删除后的也要进行resize操作,和插入时类似。实际删除的操作则由list_ass_slice来实现,它有两种用法,一是更改list中的某一段为特定的值,另一种就是删除list中的某一段。list中每删除一个元素都会造成内存拷贝。一个值得注意的细节是由于从list中删除或是更改的对象需要减少引用计数,但减少引用计数时又会循环调用一些list函数,可能会造成list索引值的破坏,因而函数中得用一个temp数组保留从list中剥离的成员,等删除工作完成,list结构已经确定的情况下再逐一减少其引用。

当list被销毁时,对其成员逐个减少引用,然后检查缓冲区是否已经满,如果没有的话就将该list放入缓冲区,这样下次再创建list的时候就不用为list对象本身再分配对象了。

4. dict的实现

python中的dict是用hash表实现,解决冲突的方法是开放定址法,即二次探测,删除时使用伪删除技术(顺便说下在一致性假设下,如果用k来表示hash表的使用率的话,那么一次成功查找需要的比较次数为1/k * ln(1/(1-k)),插入时的比较次数最多为1/(1-k))。

在一个dict中,每个(key,value)对组成一个entry,entry中还另外存储了key的hash值。对每个entry来说有三种状态unused(key,value皆为NULL,初始状态),active(key,value不为NULL,存储了元素),dump(对应于伪删除技术,key=dummy,value=NULL)。dict实际上就是entry的集合,定义如下:

typedef struct _dictobject PyDictObject;
struct _dictobject {
	PyObject_HEAD
	Py_ssize_t ma_fill;  /* # Active + # Dummy */
	Py_ssize_t ma_used;  /* # Active */

	/* The table contains ma_mask + 1 slots, and that's a power of 2.
	 * We store the mask instead of the size because the mask is more
	 * frequently needed.
	 */
	Py_ssize_t ma_mask;

	/* ma_table points to ma_smalltable for small tables, else to
	 * additional malloc'ed memory.  ma_table is never NULL!  This rule
	 * saves repeated runtime null-tests in the workhorse getitem and
	 * setitem calls.
	 */
	PyDictEntry *ma_table;
	PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
	PyDictEntry ma_smalltable[PyDict_MINSIZE];
};

PyDict_MINSIZE一般被设为8。使用PyDict_New来创建一个新dict时,其实就是分配相应的内存,将ma_fill,ma_used设为0,ma_mask设为7(8-1),ma_table指向ma_smalltable,ma_lookup默认为lookdict_string。在实现dict时python也使用了缓冲池,方法和list基本一样。

ma_lookup这个函数指针确定了散裂函数和冲突发生时的二次散裂函数,在dict中进行搜索有两种方法,lookdict_string和lookdict,后一种是更一般的方法。由于python中dict用的非常广泛,而这些dict中大多数key都是string,因而专门提供了一个搜索方法来进行加速,其实两种方法的本质都是一样的。搜索的具体过程如下:1. 获得探测链中当前应该检测的entry;2. 如果是unused状态,那搜索失败,应该返回一个可用的(可以被设置值)的entry,所以如果freeslot不为空(之前找到了dummy态的entry)就返回freeslot指向的entry,否则返回当前entry;3. 如果entry为dummy态且freeslot未设置,则设置freeslot,继续查找下一个;4. 依次检查当前entry的key和查找的key是否引用相同、值相同,若不是继续查找下一个。需要注意的是,dict使用哪种方法进行查找取决于待查找的key,如果是string的话则利用lookdict_string,和本身已经有的entry中的key无关。

dict的插入和删除基于之前的搜索,很好理解。当插入时先搜索该key,得到一个entry,再根据它的状态来修改它达到插入的上目的。删除操作也类似。需要注意的是插入元素的时候,会检查ma_fill/(ma_mask+1)是否大于2/3,如果装载率的确大于这个值并且有unused或dummy的entry被填充的时候,就会调整dict的大小,新的大小最小为minsize=ma_userd*(ma_used>50000?2:4),显然改变dict大小会造成内存移动,因此这时候可以丢弃dummy的entry。新的dict大小从8开始逐次*2增长,直到大于minsize。接下来就是一些初始化动作,逐个检查entry并插入新的dict等。

python中yield的实现

Python中的iter可是个好东西,时常都会要用到,不过《Python源码剖析》中只提到了for中的iter,对另一种形式,也就是包含yield語句的函数未做研究。这篇文章就讲讲yield是如何实现的。

其实最后的实现并不困难,但实现yield的部分从源码中找出来可费了我好大功夫。先来看看要研究的范例代码:

#!/usr/bin/python
#encoding:utf8

def iter_func():
  4           0 LOAD_CONST               0 (<code object iter_func at 0xb789a410, file "mod1.py", line 4>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (iter_func)

    i = 5
  5           0 LOAD_CONST               1 (5)
              3 STORE_FAST               0 (i)
    yield i
  6           6 LOAD_FAST                0 (i)
              9 YIELD_VALUE
             10 POP_TOP
             11 LOAD_CONST               0 (None)
             14 RETURN_VALUE        

for i in iter_func():
  9           9 SETUP_LOOP              22 (to 34)
             12 LOAD_NAME                0 (iter_func)
             15 CALL_FUNCTION            0
             18 GET_ITER
        >>   19 FOR_ITER                11 (to 33)
             22 STORE_NAME               1 (i)

    print i
 10          25 LOAD_NAME                1 (i)
             28 PRINT_ITEM
             29 PRINT_NEWLINE
             30 JUMP_ABSOLUTE           19
        >>   33 POP_BLOCK
        >>   34 LOAD_CONST               1 (None)
             37 RETURN_VALUE

第一眼看到这个编译出来的字节码觉得没有什么滑头,只有一句是不了解的:yield_value,于是认为实现iter估计就是在这句里面了,于是看看yield_value中python虚拟机都做了什么:

...
		case YIELD_VALUE:
			retval = POP();
			f->f_stacktop = stack_pointer;
			why = WHY_YIELD;
			goto fast_yield;

...

fast_yield:
	if (tstate->use_tracing) {
		if (tstate->c_tracefunc) {
			if (why == WHY_RETURN || why == WHY_YIELD) {
				if (call_trace(tstate->c_tracefunc,
					       tstate->c_traceobj, f,
					       PyTrace_RETURN, retval)) {
					Py_XDECREF(retval);
					retval = NULL;
					why = WHY_EXCEPTION;
				}
			}
			else if (why == WHY_EXCEPTION) {
				call_trace_protected(tstate->c_tracefunc,
						     tstate->c_traceobj, f,
						     PyTrace_RETURN, NULL);
			}
		}
		if (tstate->c_profilefunc) {
			if (why == WHY_EXCEPTION)
				call_trace_protected(tstate->c_profilefunc,
						     tstate->c_profileobj, f,
						     PyTrace_RETURN, NULL);
			else if (call_trace(tstate->c_profilefunc,
					    tstate->c_profileobj, f,
					    PyTrace_RETURN, retval)) {
				Py_XDECREF(retval);
				retval = NULL;
				why = WHY_EXCEPTION;
			}
		}
	}

	if (tstate->frame->f_exc_type != NULL)
		reset_exc_info(tstate);
	else {
		assert(tstate->frame->f_exc_value == NULL);
		assert(tstate->frame->f_exc_traceback == NULL);
	}

	/* pop frame */
exit_eval_frame:
	Py_LeaveRecursiveCall();
	tstate->frame = f->f_back;

	return retval;
}

这代码和我想象中的可不太相同,yield_value好像就pop()了一个值(范例中是5),将它设为返回值,再设置why为why_yield,就路到了fast_yield段,而在fast_yield段,由于python初始化线程环境和帧时,tstate->use_tracing和frame->f_exc_type分别为0和NULL,也就是说基本上yield_value就像return一样,直接返回了一个值就跳出了那个大大的switch和外层的for(;;)循环。这就奇怪了,如果返回是5的话,那在call_function的后一句get_iter肯定会出错啊,不仅是返回5不行,返回一个函数也不对,因为函数和tp_iter也为空。

python总要知道iter_func是一个iter这程序才能正常进行下去啊,到底是在哪里标识的呢?make_function 0,call_function 0这两句创建和调用代码和平常的函数一模一样。我又把范例程序里的yield i改成了return i,也没有看出什么来。又一次地仔细检查call_function才发现问题(其实如果早点写个pycparser再查看编译出来的code对象应该就发现了)。

很容易看出来,范例中call_function会走入fast_function通道,而在fast_function中,co->co_flags并不满足条件,因而会调用到PyEval_EvalCodeEx(),如下:

static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
	if (argdefs == NULL && co->co_argcount == n && nk==0 &&
	    co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)) {
	        /* do something */
	}

	return PyEval_EvalCodeEx(co, globals,
				 (PyObject *)NULL, (*pp_stack)-n, na,
				 (*pp_stack)-2*nk, nk, d, nd,
				 PyFunction_GET_CLOSURE(func));
}

奥秘就隐藏在这个PyEval_EvalCodeEx中。


PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
	   PyObject **args, int argcount, PyObject **kws, int kwcount,
	   PyObject **defs, int defcount, PyObject *closure)
{
	register PyFrameObject *f;
	register PyObject *retval = NULL;
	register PyObject **fastlocals, **freevars;
	PyThreadState *tstate = PyThreadState_GET();
	PyObject *x, *u;

	if (co->co_flags & CO_GENERATOR) {
		/* Don't need to keep the reference to f_back, it will be set
		 * when the generator is resumed. */
		Py_XDECREF(f->f_back);
		f->f_back = NULL;

		PCALL(PCALL_GENERATOR);

		/* Create a new generator that owns the ready to run frame
		 * and return that as the value. */
		return PyGen_New(f);
	}
}

这下终于发现了,原来python通过 code对象的co_flags来知道这是一个iter,然后根据已经创建的frame来得到一个iter对象。

PyObject *
PyGen_New(PyFrameObject *f)
{
	PyGenObject *gen = PyObject_GC_New(PyGenObject, &PyGen_Type);
	if (gen == NULL) {
		Py_DECREF(f);
		return NULL;
	}
	gen->gi_frame = f;
	Py_INCREF(f->f_code);
	gen->gi_code = (PyObject *)(f->f_code);
	gen->gi_running = 0;
	gen->gi_weakreflist = NULL;
	_PyObject_GC_TRACK(gen);
	return (PyObject *)gen;
}

总结一下,call_function 0这一句就得到一个iter对象,然后压栈,这样在后面的get_iter中就对这个iter对象进行处理。iter对象就是对 frame的一个简单的包装,回想一下get_iter,它要调用对象的type对象中的的tp_iter操作。而PyGen_Type中的tp_iter被设置为PyObject_SelfIter,简单地返回自身。而接下来的for_iter则会调用对象的type中的ip_iternext。

PyTypeObject PyGen_Type = {
	PyVarObject_HEAD_INIT(&PyType_Type, 0)
	"generator",				/* tp_name */
	sizeof(PyGenObject),			/* tp_basicsize */
	/* ... */
	PyObject_SelfIter,			/* tp_iter */
	(iternextfunc)gen_iternext,		/* tp_iternext */
	/* ... */
};

static PyObject *
gen_iternext(PyGenObject *gen)
{
	return gen_send_ex(gen, NULL, 0);
}

static PyObject *
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
{
	PyThreadState *tstate = PyThreadState_GET();
	PyFrameObject *f = gen->gi_frame;
	PyObject *result;

	if (gen->gi_running) {
		PyErr_SetString(PyExc_ValueError,
				"generator already executing");
		return NULL;
	}

	/* Generators always return to their most recent caller, not
	 * necessarily their creator. */
	Py_XINCREF(tstate->frame);
	assert(f->f_back == NULL);
	f->f_back = tstate->frame;

	gen->gi_running = 1;
	result = PyEval_EvalFrameEx(f, exc);
	gen->gi_running = 0;

	/* If the generator just returned (as opposed to yielding), signal
	 * that the generator is exhausted. */
	if (result == Py_None && f->f_stacktop == NULL) {
		Py_DECREF(result);
		result = NULL;
		/* Set exception if not called by gen_iternext() */
		if (arg)
			PyErr_SetNone(PyExc_StopIteration);
	}

	return result;
}

调用iter的next函数时可能有很多情况,这里简化了代码只显示最基本的无参调用。从逻辑上考虑一下,在例子中调用iter_func().next()时,func肯定要从上次yield的地方开始执行起。回头再看看yield_value对应的代码我们可以发现一句

f->f_stacktop = stack_pointer;

这句话将iter对象所对应的frame的f_stacktop置为当前的栈顶。这个f_stacktop是干什么的呢?再在ceval.c里面往上找找就可以发现这样一句

f->f_stacktop = NULL;   /* remains NULL unless yield suspends frame */

因而猜测f_stacktop保存着一个指针,当frame执行的时候,如果它不为空,那将栈顶指针初始化为f_stacktop,而什么情况下需要frame需要在一个不为空的“环境”(就是不按正常的方式在一个空栈上运行)中开始执行呢,答案只有yield。再找找代码发现果然是这样

stack_pointer = f->f_stacktop;

到这里iter对象的next()函数基本就清楚了,而iter的开始、停止等其实也就是调用gen_send_ex。yield的实现基本也就清楚了。

Python GIL在Linux下的实现

《Python源码剖析:深度探索动态语言核心技术》真是好书,讲的深入浅出,语言又风趣。让我有点怨念的就是研究平台是Windows,这一般都没什么问题,不过碰到一些平台相关特性,比如thread时就对不上了。今天看到第15章Python多线程机制,里面对GIL的讲解就全是基于Windows的,自己研究了一下Linux下的源码,做点笔记。

在Python初始化多线程环境这部分Linux和Windows一样,就是把initialized变量设为1,并没有做其它的事情。Linux下的GIL实现有两套,由USE_SEMAPHORES宏决定。由名字就可以看出一种实现是使用信号量,一种是不使用信号量的。主要代码都在Python/thread_pthread.h中。

在Python中,GIL的数据结构为PyThread_type_lock,它是一个指向void的指针,这也是因为不同平台实现不同,无法确定一个具体的类型。对GIL的操作共有4种,分别是PyThread_allocate_lock,PyThread_free_lock,PyThread_acquire_lock,PyThread_release_lock。分别执行创建、销毁、锁、解锁GIL的动作。在Python内部还有一个变量会影响到线程請求GIL时的行为,这就是waitflag,如果waitflag为True,那么线程将被阻塞在請求GIL的操作上,反之非阻塞。下面分别分析两种不同的实现。

一、使用信号量。
使用信号量时的实现相当地简单明了。GIL就是一个信号量(sem_t)。直接上代码(有精简)。

PyThread_type_lock
PyThread_allocate_lock(void)
{
	sem_t *lock;
	int status, error = 0;

	lock = (sem_t *)malloc(sizeof(sem_t));

	if (lock) {
		status = sem_init(lock,0,1);
	}

	return (PyThread_type_lock)lock;
}

void
PyThread_free_lock(PyThread_type_lock lock)
{
	sem_t *thelock = (sem_t *)lock;
	int status, error = 0;

	status = sem_destroy(thelock);

	free((void *)thelock);
}

/*
 * As of February 2002, Cygwin thread implementations mistakenly report error
 * codes in the return value of the sem_ calls (like the pthread_ functions).
 * Correct implementations return -1 and put the code in errno. This supports
 * either.
 */
static int
fix_status(int status)
{
	return (status == -1) ? errno : status;
}

int
PyThread_acquire_lock(PyThread_type_lock lock, int waitflag)
{
	int success;
	sem_t *thelock = (sem_t *)lock;
	int status, error = 0;

        // [1]
	do {
		if (waitflag)
			status = fix_status(sem_wait(thelock));
		else
			status = fix_status(sem_trywait(thelock));
	} while (status == EINTR); /* Retry if interrupted by a signal */

	if (waitflag) {
		CHECK_STATUS("sem_wait");
	} else if (status != EAGAIN) {
		CHECK_STATUS("sem_trywait");
	}

	success = (status == 0) ? 1 : 0;

	return success;
}

void
PyThread_release_lock(PyThread_type_lock lock)
{
	sem_t *thelock = (sem_t *)lock;
	int status, error = 0;

	status = sem_post(thelock);
}

在代码中看到的sem_*开头的函数是符合POSIX的信号量操作函数,可以看到Python其实就是对信号量的操作进行了一下包装,而在[1]处的代码则保证請求锁这个操作不会被信号所中断,看过Unix Networking Programming的应该会对这样的語句很熟悉吧。

二、使用一个自定义结构。这种情况稍微复杂一点点。先来看看在这种情况下GIL是个什么东西。

/* A pthread mutex isn't sufficient to model the Python lock type
 * because, according to Draft 5 of the docs (P1003.4a/D5), both of the
 * following are undefined:
 *  -> a thread tries to lock a mutex it already has locked
 *  -> a thread tries to unlock a mutex locked by a different thread
 * pthread mutexes are designed for serializing threads over short pieces
 * of code anyway, so wouldn't be an appropriate implementation of
 * Python's locks regardless.
 *
 * The pthread_lock struct implements a Python lock as a "locked?" bit
 * and a <condition, mutex> pair.  In general, if the bit can be acquired
 * instantly, it is, else the pair is used to block the thread until the
 * bit is cleared.     9 May 1994 tim@ksr.com
 */

typedef struct {
	char             locked; /* 0=unlocked, 1=locked */
	/* a <cond, mutex> pair to handle an acquire of a locked lock */
	pthread_cond_t   lock_released;
	pthread_mutex_t  mut;
} pthread_lock;

可以看到Python自定义了一个struct,其中的locked属性用来表示GIL是否被锁,lock_released和mut共同来实现解锁和锁操作。为什么要这样做呢?注意代码中的注释,可以看到pthread_mutex在两种情况下的行为是不可以预测的:一个线程再次請求被它占有的锁和請求解锁属于另一个线程的锁。我又找了找这方面的资料(链接),发现pthread_mutex有一个kind属性,它可能是fast,recursive或error checking。不同kind属性的pthread_mutex在上述两个操作下的行为是不相同的,而这几个属性都是NP,也就是Non-Portable的,并不通用。在注释中还提到了设计理念的原因,不过这个我就没有太多研究了。接下来看看具体的对GIL的操作。

PyThread_type_lock
PyThread_allocate_lock(void)
{
	pthread_lock *lock;
	int status, error = 0;

	lock = (pthread_lock *) malloc(sizeof(pthread_lock));
	if (lock) {
		memset((void *)lock, '\0', sizeof(pthread_lock));
		lock->locked = 0;

		status = pthread_mutex_init(&lock->mut,
					    pthread_mutexattr_default);

		status = pthread_cond_init(&lock->lock_released,
					   pthread_condattr_default);
	}

	return (PyThread_type_lock) lock;
}

void
PyThread_free_lock(PyThread_type_lock lock)
{
	pthread_lock *thelock = (pthread_lock *)lock;
	int status, error = 0;

	status = pthread_mutex_destroy( &thelock->mut );

	status = pthread_cond_destroy( &thelock->lock_released );

	free((void *)thelock);
}

可以看到创建和销毁GIL的代码还是非常地直观地,就是分别地创建和销毁其中的lock_released和mut。請求锁和解锁的操作稍微绕了一点点弯,如下。


int
PyThread_acquire_lock(PyThread_type_lock lock, int waitflag)
{
	int success;
	pthread_lock *thelock = (pthread_lock *)lock;
	int status, error = 0;

	status = pthread_mutex_lock( &thelock->mut );
	success = thelock->locked == 0;

	if ( !success && waitflag ) {
		/* continue trying until we get the lock */

		/* mut must be locked by me -- part of the condition
		 * protocol */
		while ( thelock->locked ) {
			status = pthread_cond_wait(&thelock->lock_released,
						   &thelock->mut);
		}
		success = 1;
	}
	if (success) thelock->locked = 1;
	status = pthread_mutex_unlock( &thelock->mut );

	if (error) success = 0;
	return success;
}

void
PyThread_release_lock(PyThread_type_lock lock)
{
	pthread_lock *thelock = (pthread_lock *)lock;
	int status, error = 0;

	status = pthread_mutex_lock( &thelock->mut );

	thelock->locked = 0;

	status = pthread_mutex_unlock( &thelock->mut );

	/* wake up someone (anyone, if any) waiting on the lock */
	status = pthread_cond_signal( &thelock->lock_released );
}

具体每一步各个参数返回什么值可以自己手动拿张纸画画,还是很好推的。直接给出结论:GIL在上述两种情况下的动作分别是:当一个线程试图去锁定已经被它锁定过的GIL时,阻塞(waitflag == 1)或返回失败(waitflag == 0),分别类似于kind属性为fast 和 error checking的pthread_mutex;当一个线程试图去解锁并不是由它锁定的GIL时,马上返回,并成功解锁,类似于kink为fast的pthread_mutex。至此解析完成。

去除冗余css

话说前端果然是不行啊。。总是被要求抄某个网站的样式什么的,于是总会发现css多了好多,一条条的去除又太麻烦了。于是昨天抽空写了个脚本来处理,现在还很简陋,输入一个html,一个css文件,把css中有用的标出来写入另一个文件。本来还想再处理一下代码,不过要出去玩了,就先丢上来吧。大致的思路是分别解析css文件和html文件,再遍历html节点,和css规则相匹配。

#!/usr/bin/python
#encoding:utf8

import re
import copy
import BeautifulSoup as bs

html_file_name = 'result.html'
css_file_name = 'mobile.css'
class css_rule:

    selecters = []
    raw = ''
    style = ''

    def __init__(self, selecters, raw, style):
        self.selecters = selecters
        self.raw = raw
        self.style = style

def print_css(css_rule):
    if css_rule['used'] > 0:
        for attr in ['selecters', 'raw', 'style']:
            print "%s : %s \n" % (attr, getattr(css_rule['rule'], attr))
        print "used : %d" % css_rule['used']

class html_tag:

    myclass = ''
    name = ''
    myid = ''

    def __init__(self, myclass, name, myid):
        self.myclass = myclass
        self.name = name
        self.myid = myid

def walk_html(tag):
    if not isinstance(tag, bs.NavigableString):
        yield tag
        for i in tag.contents:
            for tmp in walk_html(i):
                yield tmp

def get_format_node(tag):
    return html_tag(tag.get('class', ''), tag.name, tag.get('id', ''))

def rule_match(format_nodes, selecters):
    if not selecters:
        return True
    if not format_nodes:
        return False
    n = format_nodes.pop(0)
    s = selecters.pop(-1)

    if node_match_selecter(n, s):
        return rule_match(format_nodes, selecters)
    else:
        selecters.append(s)
        return rule_match(format_nodes, selecters)

def node_match_selecter(node, selecter):
    name = re.findall('^(\w*)', selecter)
    if name:name = name[0]
    if name and name != node.name:
        return False

    class_name = re.findall('\.([\w]+)', selecter)
    if class_name: class_name = class_name[0]
    if class_name and class_name not in node.myclass.split(' '):
            return False

    id_name = re.findall('#([\w]+)', selecter)
    if id_name: id_name = id_name[0]
    if id_name and id_name != node.myid:
            return False

    return True

def print_node(node):
    for attr in ['myclass', 'myid', 'name']:
        print "%s : %s \n" % (attr, getattr(node, attr))

css_list = []
with open(css_file_name) as css_file:
    css_content = re.sub('/\*[\d\D]+?\*\/', '', css_file.read().lower())
    css_re = re.compile('([\d\D]+?)\{([\d\D]+?)\}')
    for (selecter, style) in css_re.findall(css_content):
        selecter = re.sub('[\n\t]', '', selecter)
        style = re.sub('[\n\t]', '', style)

        selecter = re.sub('\s*,\s*', ',', selecter)
        parent_list = selecter.split(' ')
        #print parent_list
        last = parent_list.pop(-1)
        #print last
        selecter_list = last.split(',')
        #print selecter_list
        map(lambda x:re.sub('[\n\t ]', '', x), selecter_list)

        for a_selecter in selecter_list:
            #if ':' in a_selecter:
            #    (se, st) = a_selecter.split(':')
            #else:
            #    se = a_selecter
            #    st = ''
            nodes = copy.copy(parent_list)
            nodes.append(a_selecter)
            c = css_rule(nodes, "%s{%s}" % (selecter, style), style)
            css_list.append({'rule' : c, 'used' : 0})

with open(html_file_name) as html_file:
    soup = bs.BeautifulSoup(html_file.read().replace('\n', '').lower())
    body = soup.body

    for i in walk_html(body):
        #print i.name, "finished! \n"
        format_nodes = [get_format_node(k) for k in i.findParents() if k.name not in ['html', '[document]']]
        format_nodes.insert(0, get_format_node(i))

        for rule in css_list:
            tmp_rule = copy.deepcopy(rule['rule'].selecters)
            tmp_nodes = copy.deepcopy(format_nodes)
            if rule_match(tmp_nodes, tmp_rule):
                rule['used'] += 1

#map(print_css, css_list)
aaa = open('processed.css', 'w')
for rule in css_list:
    if rule['used'] > 0 :
        aaa.write(rule['rule'].raw + '\n')

aaa.close()

在遍历html树时用了yield,注意下递归时yield的用法,http://www.iteye.com/topic/338111。代码就是随手写写,有些命名规则什么的实在是很囧,要是以后想做成一个成熟的项目的话还得再完善下。

有一个bug:css规则我不是很熟,只考虑了如selecter1 selecter2,selecter3这种用法,试了下后发现样式漏掉了一点,检查后原来css可以这么写selecter1 selecter2,selecter3 selecter4,fix起来也简单,逻辑没有问题就好说了。还有点小问题,比如现在只能匹配一个html文件(这改起来很方便),写入css文件后会有冗余(每行一个规则,再去除一下即可)。

然后用不会用gdb,pdb真是坑爹啊,还是得学,为了debug下专门开eclipse,好麻烦,然后写代码还坑爹,编辑效率太低了。

虾米自动签到

罪过罪过。。好久不写。最近用了虾米,歌是挺多,就是觉得网站好乱,下歌也还要积分。我又从来不能坚持登陆的,于是用cron + python写个定时登录脚本。很简单,直接上代码。

#!/usr/bin/python
# encoding:utf-8

import cookielib, urllib2, urllib, StringIO, gzip, os, time
import sys

if len(sys.argv) != 3:
    exit(0)

def get_raw_content(rev):
    if rev.headers.has_key('Content-Encoding'):
        fileobj = StringIO.StringIO()
        fileobj.write(rev.read())
        fileobj.seek(0)
        gzip_file = gzip.GzipFile(fileobj=fileobj)
        content = gzip_file.read()
        fileobj.close()
    else:
        content = rev.read()

    return content

LOG = True
LOG_FILE = os.environ['HOME'] + '/.xiami_signin_log'

SIGNIN_HEADERS = {
    'Accept' : '*/*',
    'Accept-Charset' : 'UTF-8,*;q=0.5',
    'Accept-Encoding' : 'gzip,deflate,sdch',
    'Accept-Language' : 'en-US,en;q=0.8',
    'Connection' : 'keep-alive',
    'Content-Length' : 0,
    'Content-Type' : 'application/x-www-form-urlencoded',
    'Host' : 'www.xiami.com',
    'Origin' : 'http://www.xiami.com',
    'Referer' : 'http://www.xiami.com/',
    'User-Agent' : 'Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Ubuntu/10.04 Chromium/11.0.672.2 Chrome/11.0.672.2 Safari/534.20',
    'X-Requested-With' : 'XMLHttpRequest'
}

LOGIN_HEADERS = {
    'Accept' : 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
    'Accept-Charset' : 'UTF-8,*;q=0.5',
    'Accept-Encoding' : 'gzip,deflate,sdch',
    'Accept-Language' : 'en-US,en;q=0.8',
    'Cache-Control' : 'max-age=0',
    'Connection' : 'keep-alive',
    'Content-Type' : 'application/x-www-form-urlencoded',
    'Host' : 'www.xiami.com',
    'Origin' : 'http://www.xiami.com',
    'Referer' : 'http://www.xiami.com/member/logout',
    'User-Agent' : 'Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Ubuntu/10.04 Chromium/11.0.672.2 Chrome/11.0.672.2 Safari/534.20',
}

LOGIN_DATA = {
    'done' : 'http://www.xiami.com/',
    'type' : '',
    'email' : sys.argv[1],
    'password' : sys.argv[2],
    'submit' : '登 录'
}
signin_url = 'http://www.xiami.com/task/signin'
login_url = 'http://www.xiami.com/member/login'

TIMEOUT = 100

def do_sign(data):
    success = True
    try:
        opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookielib.CookieJar()))
        urllib2.install_opener(opener)
        request1 = urllib2.Request(login_url, data = urllib.urlencode(data), headers = LOGIN_HEADERS)
        rev1 = urllib2.urlopen(request1)

        request2 = urllib2.Request(signin_url, headers = SIGNIN_HEADERS, data = {})
        rev2 = urllib2.urlopen(request2)
        res = get_raw_content(rev2)
    except:
        success = False

    if success and len(res) > 100:
        success = False

    if LOG:
        log_file = open(LOG_FILE, 'a')
        log_file.write('%s %s signin %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), data['email'], str(success)))
        log_file.close()

    if not success:
        time.sleep(TIMEOUT)
        do_sign(data)

do_sign(LOGIN_DATA)

存好后再 crontab -e,输入
0 12 */1 * * python /path-to-xiami_signin.py email password

python源代码学习笔记(一)

这段时间又懒下来了,加上事情也比较忙,完全荒废了这里啊,罪过罪过。

这一系列文章是我根据《Python源码剖析——深度探索动态语言核心技术》学习python源代码的笔记,书中分析的是python2.5的代码,我下载的是python2.6的,有些许不同,不过大致一样。

先来看看python总体的架构吧。如下图(从源码剖析中截图,以下如果未有说明的话都是从书中截的)。
python总体架构

在左边是python和各种模块、库及第三方模块。模块一般是由python编写而成,不过在一些对速度要求比较严格的地方,也可以用C写,python和C的兼容性是相当好的恩。不过这一块并不是学习的主要部分,它对应着python docs里的Library Reference,而书中主要讲解的是Language Reference的部分。图的右边是python的运行环境,可以理解为系统运行一个进程时堆栈,寄存器等所保存的上下文环境。中间是python虚拟机,所有的代码都是在它上面运行的。在虚拟机的上层,scanner, parser, compiler其实和一般的编译过程没有太大区别,只不是一般的C编译器最终是将代码翻译成机器码,再由操作系统作为运行环境来执行;而python则是把代码翻译成字节码,以python虚拟机来作为运行环境而执行。python解释器和python运行环境是我们学习的重点。

让我们从python中的对象开始吧。python中一切都是对象。从内建的int、string到用户自定义的class,这些都是对象。进一步的,它们的类型也是对象。对于一般的对象来说,它们是不能被静态初始化的,并且都是在堆上,但有一些例外,就是内建的类型对象,比如int类型这个对象,它们是被静态初始化并在栈上分配内存的。python中的对象一旦被创建,在内存中的大小就再也不变化了,这样是为了保证设计的简单性,其实这种思想在各种地方都是有体现,比如hbase对外提供的接口,也就是get, put, scan不多几个,如果在保证可扩展性和可靠性的前提下还要设计诸如关系型数据库中的那些比如事务性,原子性等,那将会相当复杂。

对象所对应的类型定义在object.h中。代码如下

typedef struct _object
{
    PyObject_HEAD
} PyObject;
#ifdef Py_TRACE_REFS
/* Define pointers to support a double-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA       \
    struct _object *_ob_next;
    struct _object *ob_prev;

#define _PyObject_EXTRA_INIT 0, 0,

#else
#define _PyObject_HEAD_EXTRA
#define _PyObject_EXTRA_INIT
#endif

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD                  \
    _PyObject_HEAD_EXTRA           \
    Py_ssize_t ob_refcnt;                  \
    struct _typeobject *ob_type;

上面的代码可以简化为

typedef struct _object
{
    int ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

ob_refcnt是用来实现引用户数的gc。ob_type指向了这个对象的类型对象,换句话说,它存储了这个对象的类型。对象可以分为两种,一种是定长对象,比如int,另一种是变长的对象,比如list。对于前一种,只用在实际的对象中再分配一个固定的区域用来存储值就可以了,但这种方法对后一种对象就失效了,因此python中定义了表示变长对象的结构体。

#define PyObject_VAR_HEAD          \
    PyObject_HEAD                         \
    Py_ssize_t ob_size;

typedef struct
{
    PyObject_VAR_HEAD
} PyVarObject;

可以看出来,其实就是将一般的对象包装了一下,加了一个ob_size。表示所容纳的元素个数,比如一个list里面有5个string的话,那这个size就为5。从这里我们可以看出,只需要一个PyObject *指针就可以引用任何一个对象。这对于python实现各种语言特性很关键。接下来我们来看看_typeobject中到底有什么。

typedef struct _typeobject
{
    PyObject_VAR_HEAD
    const char * tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicssize, tpitemsize; /* For allocation */

    /* Methods to implement standard operations */
    destructor tp_dealloc;
    printfunc tp_print;
    ....

    /* Method suites for stadard classes */
    PyNumberMethods *tp_as_number;
    PYSequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More stadard operations (here for binary compatibility) */
    hashfunc tp_hash;
    ....
} PyTypeObject;

首先可以看到,这个类型变量也是一个对象,因为它有PyObject_VAR_HEAD,接下来的tp_name是它的类型名。tp_basicsize和tb_itemsize给定了分配内存时的相关信息。在这个结构体中还主要定义了三类方法,一是基本操作,比如打印对象,析构对象等。一类是对于在用运算符做运算时所应该调用的函数,比如加减、取键值等等。还有一类比如使用hash(oneobject)时调用的函数。其实这个类中还有很多信息,不过为了简明起见,先列出这些。

我们已经知道,一切都是对象,每个对象都有类型。那么对一个int来说,它的类型是,这是一个对象,再精确一点,是一个类型对象,分配在栈上的静态初始化的对象,那么,这个类型对象的类型是什么呢?答案是。在源码中就是PyType_Type。

PyTypeObject PyType_Type =
{
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type", /* tp_name */
    sizeof(PyHeapTypeObject), /* tp_basicsize */
    sizeof(PyMemberDef), /*tp_itemsize*/
    ...
}

看看整数对象是怎么和它联系起来的。

PyTypeObject PyInt_Type =
{
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "int",
    ...
}

下图可以更好地说明这个问题

python运行时整数对象及其类型之间的关系

python运行时整数对象及其类型之间的关系

一个上传客户端的python实现(二)

FTP、C/S通讯都已经搞定了,接下来主要就是界面编程以及客户端逻辑了。本来随便写的一个界面,给老师看了后,说实在太土了,得重新写。现在想想当时写的真是不能用啊,都不知道设置layout,这样程序在窗口缩小的时候,里面的元素都不会变小的,很容易就看不见了。重写之前老老实实地看了Rapid_GUI_Programming_with_PyQt,果然还是得系统地学习。主要有三点:

1、用layout来布局,最后要给顶级窗口设置layout属性,这样缩放的时候就能自动改变大小

2、可以设置label 和其它控件的buddy关系,这样能利用快捷键迅速让某个控件获得焦点。

3、QStackedWidgete来设置安装程序时上一步下一步,主窗口内容切换的那种效果。不用我自己设置几个Group然后再手动控制。

界面画好了,接下来将里面都填充好,SIGNAL和SLOT都连接好。然后qt除了提供信号与槽这种方式外,还有和MFC类似的事件处理机制(其实SIGNAL、SLOT都是用event来实现的)。如果要简单地定义响应一个按钮被单击的函数,可以直接这样写:

@pyqtSignature(“”)
def on_ButtonName_clicked:

这样就不用再connect了,适合于事件和函数一对一的简单关系,不过也看个人习惯了。

当处理下拉框的时候碰到了问题,PyQt的QComboBox的下拉选项被更改的SIGNAL中,只提供了更改成哪一个,而没有从哪一个更改的。但是我有很多用户输入的框,我希望在用户点击下拉框更改它的选项的时候,保存前一个,再把这些框的值更新为当前被选中的元素的值。想了半天,没有什么好的办法,除非把每一个input的focusout都与保存的函数连接起来,但这样太累了。上网搜了下,最终确定要自己更改ComboBox对事件的处理方式。

PyQt中的app.exec_()函数其实就是启动了主窗口的事件循环,程序会保持着一个事件队列,里面的事件可能是系统发出的,可能是用户发出的。一般如果新产生一个事件,它会被利用postEvent()方法添加到队列尾,等待被处理,也就是说是异步处理,如果想要马上被处理的话,可以用sendEvent(),窗口则会不断地在某些时候处理这些事件(也可手动强制处理,通过processEvent()),简单地来说,它接收事件,然后查看是不是有对应的处理函数,如果有的话就调用相关函数进行处理,否则就丢弃。QT中还有一个事件过滤器的概念,如果把A对象添加为B对象的事件过滤器,那么所有送到B的事件都会先到达A,然后A再处理这些事件,一般A有两种选择,丢弃掉事件或转发给B。

知道了这些要解决这个问题就比较简单了,我自己创建了一个继承QComboBox的对象,在它的focusin事件中,emit一个信号,然后再在主窗口中把这个信号连接到处理函数。不过这样有一点坏处就是UI的代码是通过pyuic4编译ui文件得到的,每次都得重新手动去改那个文件,因为改一下ui,重新编译下我自己的改动就被覆盖了。不过在QtDesigner中好像也是可以使用自定义的累的,如果那样的话就不用这么麻烦了,但我没有去研究,估计也不会太难吧。

在文件上传的时候,我希望界面能够不被锁死,能继续和用户交互。在最初的版本中,我就直接用继承了threading.Thread类,每一个文件开一个线程然后调用它们的run,甚至都没有用一个list存储它们的名称。重写的时候这块当然是得大改的。首先要知道怎么管理多线程,线程间怎么通信。

在看了这篇后,对多线程大概有一个了解了。Queue其实就相当于是缓冲区,而往里面放数据的则是生产者,取数据的是消费者。Lock之类的也都比较简单,想想当时OS的知识,设计一下之间的通信流程管理逻辑也不难了。但是等我开始动手时,偶然在邮件列表看到有人说子线程不能直接操作GUI的,又去网上搜了下,发现绝大多数GUI框架都不是线程安全的。这下可又要重新想了。

主线程给子线程分配任务,这个可以直接对过Queue解决,子线程完成后如果通知主线程呢?我采用的方法是再开一个结果队列,子线程往上面放东西,而主线程去检测,当子线程没有再运行了并且两个队列都为空的时候处理过程完成,并且所有的结果都被主线程更新到GUI上去了。但如果去检测这个结果队列呢?

再次求助Google后,我找到了这篇,当时以为问题应该可以解决了,但是动手编完了后一试发现不行。分析了一下,limodou那篇文章里更新GUI就是在Controller里面,而我这里一定要在主窗口上,所以如果有Controller的话也要通知主窗口,所以主窗口一定要有一种机制去查询事件处理的状态。而不管用time.sleep(),还是用ChildThread.join()都无一会锁死窗口。我给limodou发了邮件询问,他也并没有说出一个解决的方法。不过大概地提供了几个子线程和主线程通信的方法,分别有queue,全局变量,lock,socket,事件循环等。

后来乱翻python的官方手册,发现在threading里有一个Timer类,当时就想如果这个能设定为一段时间后调用某个方法不是正好?比如每隔一秒调用一下检测函数,如果结果队列里有新数据的话就更新到主窗口,但仔细阅读后才发现它的用法是多长时间后调用一个子线程执行某个操作,大失所望,子线程就算执行了检测发现有新数据的话还是要和主线程通信的,又绕回来了。有没有这样一种时间函数,让主线程过一段时间来执行一个函数,但在这一段时间内没有阻塞呢?后来翻来翻去终于在PyQt里发现了QTimer这个类,它可以定时,并且不会阻塞!那问题就解决了,设定一个定时器,比如0.5秒钟,再把它的timeout SIGNAL连接到检测函数。在函数中先停止这个记时器,如果满足退出条件的话就退出,如果不满足的话,进行一些处理后让记时器重新开始记时。这样问题就解决了。

后来我开始思考这个问题时想到,不可能存在这样的一种记时函数的,让某个线程即不阻塞,又会在一段时间后来响应记时器,除非记时这个操作本身就是在另一个线程里进行的,它们到时间后进行通信,而这些都对用户透明,所以才会觉得它即不会阻塞线程,又能达到记时的目的。如果是在QT中的话,那它应该是通过事件来实现的,主线程可以和子线程通过事件来通信的。本来我也可以直接用事件,但当时觉得事件这块太大了,程序又比较急,以后再系统地学习一下比较好。其实到现在想到一个看起来最简单的方法,就是子线程emit一个SIGNAL,主线程来接收,不过我不知道信号能不能跨线程。因为connect里要指定发出SIGNAL的对象,我都不知道应该怎么写。

一个上传客户端的python实现(一)

实验室之前要做一个可以上传动漫、视频和海报的客户端。正好当时正在研究如何实现我的小闹钟,于是就决定继续用PyQt做了。其实当时老师是说要MFC,可我完全不会,就说用QT好了(虽然我当时QT也只会Hello World,囧)。

功能需求:

客户端:1、能添加动画信息,如作者,集数、海报等;2、能对视频进行描述,标题、tags等;3、上传要可以断点续传,因为视频可能比较大,所以不能每次从头开始传。

服务端:1、要能同时处理多个请求;2、最后要把文件排列成一个动画一个文件夹,插入数据库时要保证完整性,因为要同时操作多表。

工作流程:先由用户输入元数据,然后从服务器取得海报、视频的上传的远程地址,上传完毕后再调用服务方法移动文件、重命名、插入数据库。

需要技术术:1、GUI编程;2、C/S 编程;3、ftp 协议的实现。

(我fk曹楼的这网络,让我写了快一小时的博没了!)

C/S编程和FTP协议的实现都得用到socket编程。socket是对Tcp/ip 族的一个抽象,可以认为它实现了这些协议并向用户提供了诸如bind(),accept()这些类似的API,不过同时也要记住,socket并不仅限于Tcp/ip协议族。socket用通信双方的地址和端口号来标识一个连接,因而它可以为两台机器上或同一台机器上占用不同端口号的两个进程提供通信方式。有关更多的资料可以参考以下两个地址:

http://www.chinaunix.net/jh/4/198859.html

http://learn.akae.cn/media/ch37.html

等我看了挺多的资料准备开始动手编的时候,我发现了另一个好东西:rpyc。它将这些通信过程封装起来,使客户端和服务端的通信就像调用普通的函数一样简单,真是相当方便啊,不过有一个缺点就是它使用了BaseException,这使得你必须手动打印错误信息,如果不使用log的话。虽然最后没有直接用socket,不过看了这么多还是很有好处的。

FTP协议的实现方面先在网上找了找现有的代码,发现都太繁琐了,又由于其实上传文件和和服务端完全可以分开,因而不用修改普通的FTP服务器。于是决定自己用ftplib来实现客户端,服务端的话就用普通的FTP服务器如Serv-U,Xlight之类的。自己写了写简单的代码来测试下ftplib,发现里面的函数太多了,眼睛都看花了,于是除了真正的传输二进制信息的时候外,基本都是自己用ntransfercmd()得到socket连接,自己发送命令。有个小trick,在得到服务端当前目录文件列表的时候,如果用ftp.nlst()的话,在最后得到结果时是调用了retrlines(),它会设定TYPE A,因而在传输前要手动地TYPE I,否则传输出出错。

在具体实现的时候碰到了一个问题。RFC959里讲到:STOR 会引起服务器DTP接受经过数据连接传送的数据并将这些数据存储为服务器端的一个文件,如果在路径参数里指定的文件在服务器端已经存在,那么这个文件会被传送过来的数据覆盖。如果指定的文件不存在则会在服务器端新建一个文件。而APPE 则引起服务DTP接受从数据连接传送过来的数据并存储在服务器端的一个文件里。如果指定的文件在服务器端已经存在,则这个数据会附加到文件的后面;否则服务器端会创建这个文件。

但是在实际的程序里测试的结果是,如果文件不存在,那么STOR就正常地工作,就像平常上传一个文件一样,APPE则报错说找不到文件,如果文件存在的话,那么STOR也会在设置了REST的地方开始传输,APPE表现相同,也就是说STOR并没有被传送过来的数据覆盖。我猜测可能和所用的ftp服务端对命令的实现的不同有关吧,本机上使用的是Xlight FTP服务器。为了防止在别的FTP服务器上得到不同的结果,在传输前先做判断,如果文件已经存在,则用APPE,否则STOR。

用python抓取网站数据

淘宝网站有一个后台,记录了和卖家进行交易过的所有买家信息,现在需要把它们抓下来,得到用户名与消费额度。这个还有点意思,整好继续锻炼python。

首先到客户管理页面一看,比我想象中的要复杂一些,不是比如通过get方式在url里指定开始的页数之类。不能被吓到恩,分析了一下下一页的按钮,发现 href 为 javascript:navigate(‘customerListform’,2,425); 。继续,navigate干了什么呢?有意义的只有两步:document.getElementById(‘__CUR_PAGE’).value=page; gotoPageNew(formname); 后面的formname就是customerListform,继续gotoPageNew,也是两件事:document.getElementById(‘__IS_REPLY_SELECT_COUNT’).value=”N”; document.getElementById(formname).submit();

那回头看名为customerListform的form,里面有很多input,根据名字猜测一下含义,大概有第几页,每页多少之类。于是我来了个暴力的,直接把每页改成4200行(总共4147),提交,结果服务端真的没有判断的,于是整个页面就卡在那里了,慢慢开。我汗一下,并且服务端记下了每个用户的设置,也就是说我再打开,还是每页4200的。幸好一直很悲剧的网络现在比较争气,吃了个饭回来居然打开了,赶紧改掉,然后网页保存下来,其实现在就可以开始提取数据了,但这样完全达不到要求啊,每次要手动改网页里的东西,网速慢还可能打不开。

有个保底的了,就可以慢慢研究了,现在的情况就是要构造一个POST了,LiveHttpHeader看了下,然后又搜了搜python构造http请求,最后用了下面的方法:

raw_dict = {"name" : "test", "pwd" : "test"}
args = urllib.urlencode(raw_dict)
con2 = httplib.HTTPConnection("eshop.alisoft.com")
con2.request("POST", "/qbuilder/c2cCustomer!customerList.jspa", args, headers)
r2 = con2.getresponse()
while 1:
    ttt = r2.read(2048)
    if not ttt:
        break;
    temp += ttt
con2.close()

发送的数据的话本来是准备自己构造的,就是把那些input全都发送了,但返回总是500错误。= =这个我也无法调试了,谁知道服务器内部发生了什么,以为是自己参数发少了,又把所有的input全发送了,但还是这个问题。后来干脆将LiveHTTPHeader里的所有数据全拷过来了,再加上所有的Headers,果然200了。

数据是有了,但还没处理,要分析html,用sax我是没想出来怎么办,minidom据说很慢,于是改用lxml,然后,悲剧的事出现了,又是字符编码问题,如果python没有这个问题的话真的是完美了,怎么也无法用etree生成一棵树,如果直接从string parser的话,不管是print 出来还是转换成utf-8后写入utf-8文件,永远有错误。最后放弃掉了,只好用re。还真是悲剧啊!
每一个用户的话都是一行,id都是9位数字加tr,还是比较好匹配的,再对每一行配置span和td,取得中间的内容就是用户名和钱了。

当然,一个程序写起来只占一小部分时间,debug才是要人命的地方。乱码又出现了,最后解决方案是在抓数据时decode成unicode,然后处理的时候先转换成utf8,最后连MySQL的时候,不要忘记了数据库、表都要是utf8的,并且要set names utf8。

完了吗?NO,还有问题,这次是网络问题了,如果每页40的话,总共有105页,而抓下来一个就处理的话,多了就会出现10060 socket错误,oh,这就是我悲剧的网络啊,没办法,每抓一个页面都sleep(1)。然后亲爱的MySQL也有问题了,取完数据马上insert,速度过快,会出现命令不同步的错误,我觉得不至于吧应该,这每秒才几十条语句啊,后来找到据说是因为MySQLdb模块太old了,= =|||。没办法,每次execute后都commit一下。最后还被网速卡了下,又是10060,没办法,上50m VPN吧,uu,我爱你!

可以改进的地方:用urllib2实例化一个cookie对象,然后可以要用户输入用户名密码,先连taobao的网站,取得cookie,再在接下来的请求里使用cookie这样就不用手动更改cookie了,但是由于最后的网站后台是在eshop.alisoft.com,我估计中间还有另外的setcookie之类的过程,应该有点麻烦,不过思路是很清晰的了。