一种一行代码实现分布式爬虫的方案

栏目: 后端 · 发布时间: 5年前

内容简介:我在写爬虫的时候就经常喜欢造轮子,不太喜欢用框架写。所以一般用的就是requests+解析库+mongodb。写起来的话灵活性非常强。这里介绍一种爬虫的场景,就是我们要爬去的数据量是已知的,或者页面数目是已知的,先爬取前10页,或者先更新后10页没有区别。这里假如说我想爬链家北京房价,我可以先跑一遍最基本的链接找出来,可能东城区有10万页要更新(肯定没有这么多举例子),海淀区有8万页,西城区有12万页,玄武区5万页,等等。。。。,假如说加起来是100万页。

我在写爬虫的时候就经常喜欢造轮子,不太喜欢用框架写。所以一般用的就是requests+解析库+mongodb。写起来的话灵活性非常强。

这里介绍一种爬虫的场景,就是我们要爬去的数据量是已知的,或者页面数目是已知的,先爬取前10页,或者先更新后10页没有区别。

这里假如说我想爬链家北京房价,我可以先跑一遍最基本的链接找出来,可能东城区有10万页要更新(肯定没有这么多举例子),海淀区有8万页,西城区有12万页,玄武区5万页,等等。。。。,假如说加起来是100万页。

然后这个时候我可能有10台服务器,如何将这100页分配给10台或者不同台的服务器呢,这里其实我想到的最简单的一个方法就是通过一个分类器。

一行代码怎么加

这里假如这100万页放在 mongodb 的lianjia数据库的tasks表里面,我们可以通过以下的方式完成一个分布式爬取功能。

import pymongo

client = pymongo.MongoClient()
db = client.lianjia
tasks_coll = db.tasks

machine_id = 1

for task in tasks_coll.find({"crawled": False}):
    if get_class_num(str(task["_id"])) == machine_id:  # 根据机器编号判断是不是自己的任务
        crawl_func(task)  # 爬数据
        tasks_coll.update({"_id": task["_id"]}, {  # 更新任务状态
            "$set": {
                "crawled": True
            }
        })

这里最主要的是通过这个 get_class_num 函数的实现,它接受一个字符串,并把这个字符串映射成一个唯一的一个数字。

一个简单的分类函数的实现

说到映射成唯一的一个数字,那么就可以想到md5了。我这里用的办法也就是md5。这里一个简单的方案实现如下:

import hashlib
import string


def get_md5(_str):
    """ 对一个字符串进行md5 """
    hl = hashlib.md5()
    _bytes = _str.encode("utf-8")
    hl.update(_bytes)
    return hl.hexdigest()


def get_class_num(_str):
    """ 取字符串的最后一个字母的ascii码之和 """
    _str = str(_str)
    md5_str = get_md5(_str)
    return ord(md5_str[-1])

上面的代码也就是先把字符串转成md5串,然后取md5串的最后一个字母的ascii码作为分类的id。然后就可以通过取模的方法分类了。

分成2类

我们这里测试一下分成2类的分布如何,我们随机生成10万个任务,看看能不能将这10万个分成两半。

import random

def generate_random_string():
    return ''.join(random.sample(string.ascii_letters + string.digits, 20))


class_map = {}

for i in range(100000):
    class_id = get_class_num(generate_random_string()) % 2  # 分成两类
    if class_id not in class_map.keys():
        class_map[class_id] = 1
    else:
        class_map[class_id] += 1

print(class_map)

运行之后可以看到两个类别的数量差100多,基本不差什么。

一种一行代码实现分布式爬虫的方案

分成3类

然后再测一下三类的。

一种一行代码实现分布式爬虫的方案

这里一个是3.75万,两个是3.12万。可以发现差了很多。

分类器的完善

这里其实是因为我们的id分布不均匀的缘故。md5输出的结果是26个小写字母+10个数字,一共36种字母。数字的ascii码从48到57,小写字母的ascii码从97到122。除了二分类,其实很难保证分配很均匀。

这里其实有一个简单的方法,就是多加几个字母,我们不只是取最后面一个字母的ascii码,而是把最后面好几个的ascii码加起来。

def get_class_num(_str):
    """ 取字符串的最后五个字母的ascii码之和 """
    _str = str(_str)
    md5_str = get_md5(_str)
    return ord(md5_str[-1]) + ord(md5_str[-2]) + ord(md5_str[-3]) + ord(md5_str[-4]) + ord(md5_str[-5])

然后在测试一下三个类的。

一种一行代码实现分布式爬虫的方案

这里就好了,我们再测一下7个类。

一种一行代码实现分布式爬虫的方案

看着也很均匀。

我测了一下五个字符相加20个类都很okay,不过再多的比如说100个类这种肯定是不行。如果类别特别多,可以再多加几个字符的ascii码提高随机性。

添加id重置服务

以上的办法虽然解决了一定的分布式爬虫问题。但是实际中,服务器处理任务的能力不一样,每个任务的难度也不一样。还是会出现分配不均匀的情况。

这里可以添加一个id重置服务,我们有一个进程每隔一段时间,比如说1天或者几个小时,生成一个随机字符串,然后通过 get_class_num(task["id"] + random_string) 的方式,重新设置一遍id。

这样可以保证不会出现一直空机器的情况。

自我总结

爬虫的设计除了爬取之后,我觉得就是处理任务分配的逻辑,每个任务都十分的隔离。

这种方法可以保证没有额外的代码编写的情况下,完成相对高效的分布式爬虫。主要还是懒得写别的代码,所以想出了这种方法把。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

参与的胜利

参与的胜利

亨利·詹金斯 / 高芳芳 / 浙江大学出版社 / 2017-9-30 / CNY 42.00

《参与的胜利:网络时代的参与文化》是一场学者之间的对话,三位学者(亨利·詹金斯、伊藤瑞子和丹娜·博伊德)虽然来自不同的代际、不同的学科背景,但他们在相同的参与文化项目中展开合作,并试图解决相似的问题。 希望《参与的胜利:网络时代的参与文化》能够进一步激发团体内部及团体之间的对话,这些团体包括教育者、政策制定者、学者、关注参与文化的公民、业内人士、粉丝及其他任何关心我们文化的未来的人。理想的参......一起来看看 《参与的胜利》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具