协程,又称微线程,纤程。英文名Coroutine

我们可以将协程理解为一个子程序/函数的特例。与子程序/函数不同的是,子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

子程序与主程序是调用与被调用关系,而协程之间是平等的关系,先执行一个协程A,在适当的时候去执行另一个协程B,在适当的时候又返回协程A继续执行。协程在单线程中实现了类似多线程的效果,但因为协程始终属于一个线程,不同协程的执行只是一个线程在执行不同的“函数”而已,因此它省去了线程切换带来的开销,具有很高的并发和性能。

但协程是一个线程执行,如何利用多核CPU呢,答案就是协程+进程,每个核维持一个进程,在该进程中有一个线程来执行协程,这样就能充分利用多核以及协程的高效率,获得极大地性能。

在Python中,由于GIL(Global Interpreter Lock)的存在,导致在python中的多线程其实是伪多线程,同一时刻只有一个线程正在执行。因此在python中多线程往往(对于CPU密集型)还没有单线程高效,(当然可以用multiprocessing模块,利用多进程实现多核CPU并行),因此python的多线程对于CPU密集型工作其实是个鸡肋。多进程+协程才是python性能的大杀器。在IO密集型工作中,python的多线程能发挥得好一些,但还是没有协程高效。

回到本文实现的目标:做一个爬虫。爬虫属于IO密集型,那么如果我们不利用任何多线程或者协程技术,也就是使用同步来实现爬虫,那么对于每一次网络调用,CPU都会阻塞,直到网络调用response的到来。这就使CPU的大部分时间阻塞在等待网络IO上,效率及其低。而利用协程的话,每当一个协程进行网络调用时,CPU不用阻塞等待而是直接执行下一个协程,当网络调用返回后,将注册在事件循环中,在未来的某个时刻接着执行。这就是异步IO,能极大地提高性能。

Scrapy框架使用多线程来实现爬虫这种IO密集型的工作,也能取得不错的性能。但我们今天不打算利用线程池来实现高效            爬虫,而是使用协程来实现。Python 3.5以后已经直接支持async, await关键字,使得协程的实现变得很简单。

回想一下如果我们不使用协程+异步IO,我们将这么实现,代码基于python3.5:

from urllib import request
from urllib.error import HTTPError
import json

def crawl(url):
    try:
        res = request.urlopen(url)
    except HTTPError:
        return None
    return res.read() #返回字节流
if name == 'main':
    url = 'https://api.github.com/users/ayonel/followers?per_page=99&page=1'
    res_data = crawl(url)
    json_data = json.loads(res_data.decode()) # 字节流解码,并转为json
    print(json_data)

上面这段代码,利用python3内置的urllib模块,爬取github API, 返回的是笔者的github的粉丝(虽然没几个…)。

在上面代码中,程序将阻塞在这一行,直到网络调用返回结果。

 res = request.urlopen(url)

我们的协程+异步IO爬虫,就是解决这个问题,当发出网络请求,程序不用阻塞,CPU直接执行下一个协程。
在urllib中的request是基于同步实现的,我们需要aiohttp这个模块,用来发出异步的网络请求。另外,我还将爬取到的结果,存入了MongoDB,但是python中最常用的操作MongoDB的驱动pymongo也不是基于异步实现的,还好有motor,一个基于异步IO的python MongoDB驱动。

还是原来的需求,我们需要爬取github上一些用户的粉丝。爬取链接为:

https://api.github.com/users/{user}/followers?per_page=99&page={page}

上述这个URL返回一个用户的粉丝,并且每页最多返回99个,如果超过99个,则需要对page+1

先定义两个协程函数, fetch 以及 do_insert,分别用于异步爬取网页,以及异步插入MongoDB。

async def fetch(session, user, page):
    url = 'https://api.github.com/users/' + user + '/followers?per_page=99&page=' + str(page)
    with async_timeout.timeout(20):  # 默认超时时间20s
        async with session.get(url, headers={}) as response:
            if response.status == 200:
                return True, user, await response.read()  # True代表爬取成功
            else:
                return False, user, await response.read()   # False代表爬取失败

还有插入数据库函数:

async def do_insert(arg):
    await collection.insert_one(arg)  # collection是全局变量,代表你要插入的MongoDB的collection

上面这两个协程函数,是我们异步IO调用的关键函数,也是我们爬虫性能高效的关键所在。
有了协程函数,我们就需要定义一个主函数来执行,我们的主函数也是协程函数,另外还传入了你要爬取的用户集

async def main(loop, peopleSet):
    async with aiohttp.ClientSession(loop=loop) as session:
        for people in peopleSet:
            page = 1
            while True:  # 因为有些人不止有99个粉丝,所以遇到一页返回99个粉丝的,不能直接下一个人,还需要将page+1,去爬取该人的其他粉丝
                is_ok, user, data = await fetch(session, people, page)
                print(user)
                if is_ok:  #如果网络返回正常,status==200
                    if data[0] == 31 and data[1] == 139:  # 这儿是个坑,判断是不是gzip压缩过的字节流。原因见下文解释
                        print('gzip data returned!')
                        data = zlib.decompress(data, zlib.MAX_WBITS | 16)  # 解gzip字节流

                follower_list = []
                for follower in json.loads(data.decode()): # 将字节流解码为str,并转为json
                    follower_list.append(follower['login'])

                await do_insert({'user': user, 'follower': follower_list}) # 异步调用插入数据库
                if len(follower_list) == 99: # 如果返回99个粉丝,需要page+1
                    page += 1
                else: # 否则,直接break,爬取下一个人
                    break
            else: # 网络调用失败,直接break,爬取下一个人
                break

解释一下上面为什么要判断字节流是否是gzip压缩过的。Github API 有个坑,它有时会将响应内容压缩为gzip返回,我们用浏览器查看时没问题,因为浏览器会自动判断是否是gzip格式的字节流,如果是将会自动解压。而aiohttp返回的response.read()是原始的字节流,你不知道它有没有经过gzip压缩。按常理来说,如果你在你的请求头中不要设置Accept-Encoding字段,服务器应该认为该客户端不接受任何压缩过的内容,应该返回未压缩的字节流。但我强制指定请求头,不设置Accept-Encoding字段,Github API还是玄幻地随机返回gzip字节流。并且它的response的headers中永远显示其Content-Encoding = gzip,这儿应该是个bug吧。所以我不得不自行判断是否是gzip格式的,判断方法很简单,利用字节流的前两个字节:
前两个字节用于标识gzip文件,其中第一个字节ID1 = 31(0x1f,\037),第二个字节ID2 = 139(0x8b,\213),如果判断该字节流以这两个字节开头,那么可以初步认为这是gzip字节流。

我们还需要开启事件循环,才能达到异步IO的效果,完整程序见下文:

'''
Author: ayonel
Date:
Blog: https://ayonel.malash.net
GitHub: https://github.com/ayonel
E-mail: ayonel@qq.com
提取出所有pull中的人员,爬取其关注关系。
'''

import itertools
import json
import aiohttp
import asyncio
import time
from motor.motor_asyncio import AsyncIOMotorClient
import zlib
import async_timeout

async_client = AsyncIOMotorClient('localhost', '27017') # 建立motor的client
db = async_client['follow'] # 指定要插入的数据库
collection = db['asyncfollow'] # 指定要插入的collection

#异步插入函数

async def do_insert(arg):
    await collection.insert_one(arg)

#异步爬取函数

async def fetch(session, user, page):
    url = 'https://api.github.com/users/' + user + '/follower?per_page=99&page=' + str(page)
    with async_timeout.timeout(20):  # 默认超时时间20s
        async with session.get(url, headers={}) as response:
            if response.status == 200:
                return True, user, await response.read()  # True代表爬取成功
            else:
                return False, user, await response.read()   # False代表爬取失败

#主函数

async def main(loop, peopleSet):
    async with aiohttp.ClientSession(loop=loop) as session:
        for people in peopleSet:
            page = 1
            while True:  # 因为有些人不止有99个粉丝,所以遇到一页返回99个粉丝的,不能直接下一个人,还需要将page+1,去爬取该人的其他粉丝
                is_ok, user, data = await fetch(session, people, page)
                print(user)
                if is_ok:  #如果网络返回正常,status==200
                    if data[0] == 31 and data[1] == 139:  # 这儿是个坑,判断是不是gzip压缩过的字节流。原因见下文解释
                        print('gzip data returned!')
                        data = zlib.decompress(data, zlib.MAX_WBITS | 16)  # 解gzip字节流

                follower_list = []
                for follower in json.loads(data.decode()): # 将字节流解码为str,并转为json
                    follower_list.append(follower['login'])

                await do_insert({'user': user, 'follower': follower_list}) # 异步调用插入数据库
                if len(follower_list) == 99: # 如果返回99个粉丝,需要page+1
                    page += 1
                else: # 否则,直接break,爬取下一个人
                    break
            else: # 网络调用失败,直接break,爬取下一个人
                break


if name == 'main': # 程序入口
    start = time.time() # 标记开始时间
    peopleSet = set() # 初始化需要爬取的用户集
    peopleSet.add('ayonel')
    peopleSet.add('malash')
    # 上面我只添加了2个人,实际我的peopleSet中有2万多人
    loop = asyncio.get_event_loop() # 开启事件循环
    try:
        loop.run_until_complete(main(loop, peopleSet)) # 开始执行main
    except Exception:
        pass
    finally:
        print('总耗时:' + str((time.time()-start)/60))

我另外还是实现了同步的urllib.request作为对比测试,实验显示,在我的异步爬虫爬取了9000+人的时间里,同步爬虫爬取了300人不到。性能差距是巨大滴…

上面完整代码给出的爬虫还能不能提高速度呢?答案是可以的!上述代码中其实把整个爬取任务封装成了一个大协程,这个大协程由许多个网络调用小协程以及数据库插入小协程构成。同时我们的所有aiohttp的请求复用了同一个aiohttp.ClientSession(),这样做的好处是我们的程序运行期间只有一个ClientSession对象,十分节约内存资源,如果我们对每个请求新开一个ClientSession的话,请求一多,内存直接就爆掉了。我实际了测试20000个请求,很快内存就会吃到10G,所以说对于ClientSession我们需要复用,那是不是所有请求都复用一个呢,就像上面完整代码中一样。当然不是,我们可以自己组织,比如500个请求复用一个ClientSession对象,或者200个请求复用一个ClientSession对象,在实际测试中,爬取效率会迅速提升,远远高于所有请求复用同一个ClientSession对象的情况。

aiohttp中还有connector(连接池),相信利用connector肯定还能更进一步提高爬取效率。

最后总结,协程+异步IO是提高并发的大杀器,python一直被诟病的性能问题,可以从这儿找回点自信,但是缺点也比较明显,只适合于IO密集型的工作,另外异步代码确实不如同步代码好理解,虽然3.5已经实现了async await关键字,但还是需要多coding才能加深理解。 听说tornado是个不错的东东,有时间可以看看。

scipy是python下的一个数值计算工具包,sklearn依赖于这个包。当我调用sklearn的CountVectorizer时,提示没有发现包scipy,直接安装又总是失败。
问题就在于 scipy的安装需要依赖很多别的包,在windows这个蛋疼的平台下极有可能安装失败。并且在windows上scipy的安装依赖于mkl+numpy这个包。这个包官方并没有提供,而在http://www.lfd.uci.edu/~gohlke/pythonlibs/#scipy这里面有提供。

我的python是3.5,64位。 所以我选择numpy-1.12.0b1+mkl-cp35-cp35m-win_amd64.whl和scipy-0.18.1-cp35-cp35m-win_amd64.whl这两个文件。cp35代表python3.5,其他版本要选择其他对应的文件。下载好之后先用pip 安装numpy+mkl:

pip install numpy-1.12.0b1+mkl-cp35-cp35m-win_amd64.whl

再安装scipy:

pip install scipy-0.18.1-cp35-cp35m-win_amd64.whl

大功告成。
pip
不过这个网址“http://www.lfd.uci.edu/~gohlke/pythonlibs/”是在国外,下载起来速度简直是龟速。我把我安装的这个两个文件已经上传到百度云了。另外还有32位的,有需要的同学可以自行下载。
分享链接:http://pan.baidu.com/s/1miloOe0 密码:edo4

来源于阮一峰的博客
对两张图片进行相似度检测,思路就是对两张图片分别计算其指纹信息,然后对比两张图片的指纹信息的差异度。
基本算法为:
第一步,缩小尺寸。
将图片缩小到8×8的尺寸,总共64个像素。这一步的作用是去除图片的细节,只保留结构、明暗等基本信息,摒弃不同尺寸、比例带来的图片差异。

第二步,简化色彩。
将缩小后的图片,转为64级灰度。也就是说,所有像素点总共只有64种颜色。

第三步,计算平均值。
计算所有64个像素的灰度平均值。

第四步,比较像素的灰度。
将每个像素的灰度,与平均值进行比较。大于或等于平均值,记为1;小于平均值,记为0。

第五步,计算哈希值
将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。组合的次序并不重要,只要保证所有图片都采用同样次序就行了。
=8f373714acfcf4d0
得到指纹以后,就可以对比不同的图片,看看64位中有多少位是不一样的。在理论上,这等同于计算”汉明距离”(Hamming distance)。如果不相同的数据位不超过5,就说明两张图片很相似;如果大于10,就说明这是两张不同的图片。

代码由Wote利用Python实现,本文是对Wote的源码做一点修改并附上详细注释。

本程序需要安装pillow,

pip install pillow
#coding: utf-8
'''
本程序运行时需要两张图片路径,需要输入两张图片路径,
'''

from PIL import Image

#计算指纹
def avhash(img):
    if not isinstance(img, Image.Image):
        img = Image.open(img)
    img = img.resize((8, 8), Image.ANTIALIAS).convert('L') #将image压缩为8*8,转化为灰度图
    avg = reduce(lambda x, y: x + y, img.getdata()) / 64. #对每个像素点的灰度累和,最后除以64,得到灰度的平均值

    #这一句代码很pythonic,需要仔细消化
    #map对每个像素做判断,大于平均值为1,否则为0
    #enumerate函数返回一个列表的下标及该下标对应的元素,用tuple装起来: (index, element)
    #reduce,对每个元素右移对应的下标位,并且与前一个元素做或运算,最终得到的结果为一个
    # 64位的二进制数,每一位的0,1代表该位的像素灰度与平均像素灰度的比较结果
    return reduce(lambda x, (y, z): x | (z << y), enumerate(map(lambda i: 0 if i < avg else 1, img.getdata())), 0)

#计算汉明距离
def hamming(h1, h2):
    #直接对两个数按位做异或操作,这样得到一个64位的二进制数,该二进制数包含的1的个数,即为汉明距离
    h, d = 0, h1 ^ h2
    #求d中包含的1的个数
    while d:
        h += 1
        d &= d - 1
    return h

if __name__ == '__main__':
    img1 = raw_input('请输入第一张图片路径:')
    img2 = raw_input('请输入第二张图片路径:')
    h1 = avhash(img1)
    h2 = avhash(img2)
    print "两张图片的指纹汉明距离为:%s" % hamming(h1, h2)

本文打算长期更新一些有意思的python代码片段,就当做是学习笔记。

1.python多线程死锁

import time,threading

locka = threading.Lock()
lockb = threading.Lock()

def fa():
    print('a')
def fb():
    print('b')

def fab():
    locka.acquire()
    try:
        fa()
        time.sleep(1)
        lockb.acquire()
        try:
            fb()
        finally:
            lockb.release()
    finally:
        locka.release()
def fba():
    lockb.acquire()
    try:
        fb()
        time.sleep(1)
        locka.acquire()
        try:
            fa()
        finally:
            locka.release()
    finally:
        lockb.release()

t1 = threading.Thread(target=fab)
t2 = threading.Thread(target=fba)
t1.start()
t2.start()
t1.join()
t2.join()
print('end')

2.协程coroutine(1)

来源:廖雪峰

def consumer():
    r = ''
    while True:

        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'

def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

c = consumer()
produce(c)

运行结果为:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

解释:
yield表达式本身没有返回值,它的返回值需要等到下次调用generator函数时,由send(args)函数的参数赋予。

3.协程coroutine(2)

来源:http://blog.csdn.net/gvfdbdf/article/details/49254037

# -*- 异步IO -*-  
import asyncio  
import threading  
 
# @asyncio.coroutine把一个generator标记为coroutine类型  
@asyncio.coroutine  
def sub():  
    print('sub start: ...')  
    n = 10  
    while True:  
        print('yield start')  
        # asyncio.sleep()也是一个coroutine类型的generator,所以线程不会中断,而是直接执行下一个循环,等待yield from的返回  
        # 可以简单的理解为出现yield之后则开启一个协程(类似开启一个新线程),不管这个协程是否执行完毕,继续下一个循环  
        # 开启新协程后,print('yield start')会因为继续执行循环被立即执行,可以通过打印结果观察  
        r = yield from asyncio.sleep(1)  
        n = n - 1  
        print('---sub: %s,  thread:%s' %(n, threading.currentThread()))  
        if n == 0:  
            break  
 
@asyncio.coroutine  
def add():  
    print('add start: ...')  
    n = 10  
    while True:  
        print('yield start')  
        r = yield from asyncio.sleep(2)  
        n = n + 1  
        print('+++add: %s,  thread:%s' %(n, threading.currentThread()))  
        if n > 20:  
            break  
 
 
# 获取EventLoop:  
loop = asyncio.get_event_loop()  
# 执行coroutine  
tasks = [add(),sub()]  
loop.run_until_complete(asyncio.wait(tasks))  
loop.close()

执行结果:

add start: ...
yield start add
sub start: ...
yield start sub
---sub: 9,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start sub
+++add: 11,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start add
---sub: 8,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start sub
---sub: 7,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start sub
+++add: 12,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start add
---sub: 6,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start sub
---sub: 5,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start sub
+++add: 13,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start add
---sub: 4,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start sub
---sub: 3,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start sub
+++add: 14,  thread:<_MainThread(MainThread, started 140735116865536)>
yield start add
......

GitHub API其实是一座宝藏,它拥有着海量项目以及开发者的各类信息,可以作为社交编程以及经验软件工程课题的数据载体。

本次教程较大家如何使用scrapy来爬取GitHub API,抓取我们所需要的特定信息。GitHub API 是GitHub基于OAuth2协议开放出来的数据获取接口,我们能够在GitHub API上获取各类信息,比如一个项目的commit,issue,pull request;一个用户的粉丝,关注,提交活动,评论等等。GitHub API 有着详细的官方文档教程,上面各类数据的获取接口地址,以及一些过滤参数等。本教程将以爬取rails的issue信息为例,教大家如何使用scrapy来爬取GitHub API。

再开始爬取之前,我们需要进行一些准备工作,由于GitHub API采用OAuth2认证,需要我们提供认证token。当然不提供token可以进行爬取,但是爬取速率会大大降低。

For requests using Basic Authentication or OAuth, you can make up to 5,000 requests per hour. For unauthenticated requests, the rate limit allows you to make up to 60 requests per hour. Unauthenticated requests are associated with your IP address, and not the user making requests. Note that the Search API has custom rate limit rules.

上面显示如果我们提供token可以每小时进行5000次请求,对于非认证(不提供token)一小时只能提供60次的请求,超出请求速率限制后,会返回状态码403 forbidden。有人会问,即使认证后5000次/小时的速率也有些慢啊,如果要更快地进行爬取,那就多注册几个GitHub账号,同时利用多个账号的token进行爬取。GitHub的爬取限制针对的是每个用户,而不是IP,也就是同一台机器,只要你保证每个账号每小时进行小于5000次的爬取,也是完全没有问题的。

接下来我们来展示如何获取自己的token,其实全名为personal access token,每个账号可以拥有多个token,所以不小心哪一天忘了的话,重新生成一个新的即可。

首先我们需要登录自己的GitHub账号。然后在设置里面有一个personal access token选项:
QQ截图20160830092436
点进去,再点击Generate new token .

选择你想让你的token拥有的权限,一般默认全选即可。点击绿色的Generate token按钮。
QQ截图20160830092627

这样就生成我们的token了。注意一定要在这时候把token保存下来,这将是你在GitHub上最后一次看见你的token。一旦刷新,token就会隐藏掉。这时候只有生成新的token了。

QQ截图20160830092306

好了,有了预先的准备工作,接下来就可以编写爬虫了。这次爬虫采用scrapy,关于scrapy相关的知识就不再赘述了,网上有很多教程。

简单说一下爬取思路。

GitHub API 返回的数据都是json,这极大地方便了数据的解析。我们本次的任务是爬取rails项目的所有issue,先来大概看一下返回的issue是什么样子。由于内容太长,我就直接放链接,大家可以点击去看。

https://api.github.com/repos/rails/rails/issues

我们可以看到返回的页面是一个json数组,每个元素其实就是一个issue.而每个issue里面又有诸多信息,比如number,title,body等等。我们本次任务就爬取rails所有issue的信息number,提交者(user.login),body以及title.

注意,https://api.github.com/repos/rails/rails/issues返回的是按时间倒叙排列的issue,并且每页默认返回30条,我们需要在url后面接上一些参数,来爬取指定的页。我们将url接上参数构造成如下的样子:

https://api.github.com/repos/rails/rails/issues?per_page=99&page=num

其中num代表页数,我们需要从1开始自增。per_page代表每页返回的元素个数,GitHub最大只能制定到99。

所以我们的爬虫,应该是从1开始不停地自增num,直到返回的json数组元素个数不足99,就说明爬取完了。另外,由于GitHub API 爬取速率的限制,我事先准备了10个不同账号的token,对于每次请求,重新自定义请求header,带上不同的token.

下文是源代码:

# -*- coding: utf-8 -*-
__author__ = 'ayonel'
import itertools
import json
import os
import scrapy
from scrapy import Request

class IssueSpider(scrapy.spiders.Spider):

    name = "issue" #爬虫名称
    allowed_domains = ["github.com"] #制定爬取域名
    num = 1 # 页数,默认从第一页开始
    handle_httpstatus_list = [404, 403, 401] #如果返回这个列表中的状态码,爬虫也不会终止
    output_file = open('issue.txt', "a") #输出文件
    #token列表,隐去部分
    token_list = [
        '293a06ac6ed5a746f7314be5a25f3d**********',
        '66de084042a7d3311544c656ad9273**********',
        'a513f61368e16c2da229e38e139a8e**********',
        '9055150c8fd031468af71cbb4e12c5**********',
        'ba119dc83af804327fa9dad8e07718**********',
        'b93e6996a4d76057d16e5e45788fbf**********',
        'c9c13e5c14d6876c76919520c9b05d**********',
        '3e41cbfc0c8878aec935fba68a0d3c**********',
        '402ff55399ca08ca7c886a2031f49f**********',
        '7cb6e20a24000968983b79b5de705c**********',
    ]
    token_iter = itertools.cycle(token_list) #生成循环迭代器,迭代到最后一个token后,会重新开始迭代


    def __init__(self): #初始化
        scrapy.spiders.Spider.__init__(self)

    def __del__(self): #爬虫结束时,关闭文件
        self.output_file.close()

    def start_requests(self):
        start_urls = [] #初始爬取链接列表
        url = "https://api.github.com/repos/rails/rails/issues?per_page=99&page="+str(self.num) #第一条爬取url
        #添加一个爬取请求
        start_urls.append(scrapy.FormRequest(url, headers={
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'en',
            'Authorization': 'token ' + self.token_iter.next(),#这个字段为添加token字段
            }, callback=self.parse))

        return start_urls

    def yield_request(self): #定义一个生成请求函数
        url = "https://api.github.com/repos/rails/rails/issues?per_page=99&page="+str(self.num) #生成url
        #返回请求
        return Request(url,headers={
                'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language': 'en',
                'Authorization': 'token ' + self.token_iter.next(),
                },callback=self.parse)

    #解析函数
    def parse(self, response):
        if response.status in self.handle_httpstatus_list:#如果遇见handle_httpstatus_list中出现的状态码
            self.num += 1 #num自增,相当于直接跳过,可以输出当前url到log文件
            yield self.yield_request() #产生新的请求
            return

        json_data = json.loads(response.body_as_unicode()) #获取json
        length = len(json_data) #获取json长度

        if length == 99:
            self.num = self.num + 1
            for issue in json_data:
                data = {}
                data['number'] = issue['number']
                data['owner'] = issue['user']['login']
                data['title'] = issue['title']
                data['body'] = issue['body']
               
                self.output_file.write(json.dumps(data)+'\n') #输出每一行,格式也为json
            self.output_file.flush()
            yield self.yield_request() #产生新的请求

        elif length < 99: #意味着爬取到最后一页
            for issue in json_data:
               data = {}
                data['number'] = issue['number']
                data['owner'] = issue['user']['login']
                data['title'] = issue['title']
                data['body'] = issue['body']
                self.output_file.write(json.dumps(data)+'\n')
            self.output_file.flush()

一款产品发布,其初始用户量很小,这时就需要伪造一批用户来增加平台的用户量,搭起初步的用户生态系统。那么伪造用户时,我们必然需要网名以及头像(也有可能需要签名)等等信息。所以前些日子写了个小爬虫来爬取一批(30000+)网名及头像。

对于头像的爬取,我选择的站点是“我要个性网”(http://www.woyaogexing.com),它的图片都没有做防盗链,因此我只需要爬它图片的url就行。对于昵称,我选择的网站是“QQ网名”(http://www.oicq88.com)。选择好爬取目标,就需要咱的爬虫登场了。以前爬虫经常用scrapy,但对于我这次的量显然是杀鸡用牛刀。这个爬虫是利用python自带的urllib2,解析网页用的是BeautifulSoup(可用pip安装),与xpath原理类似。

下面是头像爬虫源码,代码很简单:

# coding:utf-8
import time
import urllib2
import random
from bs4 import BeautifulSoup

def filter(tag):
    if cmp(tag.name, 'img') == 0:
        if tag.has_attr('class'):
            if cmp(tag['class'][0], 'lazy' == 0):
                return True

outfile = open("./20160717/avatar.txt", "a")

for i in range(135, 1500):
    print i
    url = 'http://www.woyaogexing.com/touxiang/index_'+str(i)+'.html'
    response = urllib2.urlopen(url)
    data = response.read()
    soup = BeautifulSoup(data, "lxml")
    imgs = soup.find_all(filter)

    for img in imgs:
        outfile.write(img['src'] + ',' + str(random.randint(0, 1))+ '\n')
    time.sleep(0.5)

#最后输出的时候在每个头像url后面接了个随机的0或者1,这是用随机数来标志这个人是男生还是女生。

下面是爬取昵称的爬虫:

# coding:utf-8
import sys
reload(sys)
sys.setdefaultencoding("utf-8")

import urllib2
from bs4 import BeautifulSoup

def filter(tag):#解析包含网名的标签
    if cmp(tag.name, "ul") == 0:
        if tag.has_attr("class"):
            if cmp(tag['class'][0], 'list') == 0:
                return True

outfile = open("./name.txt", "a")#输出文件

for i in range(55, 145):
    print i
    url = 'http://www.oicq88.com/nvsheng/'+str(i)+'.htm'
    response = urllib2.urlopen(url)#/nvsheng/可以替换为其他的
    data = response.read()
    soup = BeautifulSoup(data, "lxml")
    ul = soup.find_all(filter)

    ulsoup = BeautifulSoup(str(ul[0]), "lxml")
    lis = ulsoup.find_all("li")


    for li in lis:
        outfile.write(str(li.p.text)+'\n')</pre>

可以直接复制源码进行爬取,前提是“我要个性网”以及“QQ网名”的前端页面没变,为了防止这种情况发生,文末我会附上爬取的源文件链接,大家可以自行下载。

文件下载链接:https://ayonel.malash.net/files/name_avatar_over.zip