场景描述
- 在Python爬虫中经常使用MongoDB数据库来存储爬虫爬取的结果,于是乎就有了一个问题:百万级的MongoDB数据如何去重?
- 常见的思路便是在数据入库的时候检查该数据在数据库中是否已经存在,如果存在则忽略(效率高点)或者覆盖,这样做在数据量比较少的时候是适用的,但是在数据量比较大的时候(百万级及以上)这样做往往是效率非常低的!而且如果是已有的未去重的百万级数据库又该怎么办呢?
- 也可以使用distinct语句进行去重,但是还是那个问题,distinct语句并不适用于百万级数据,甚至在数据量大的时候就会报错:
distinct too big, 16mb cap
。这时候就需要使用 aggregate 聚合框架来去重了!
实现方法
import pymongo
from tqdm import tqdm
class CleanData:
def __init__(self):
"""
连接数据库
"""
self.client = pymongo.MongoClient(host='host', port=27017)
self.db = self.client.crawlab_master
def clean(self):
"""
清理gkld_useful_data集合中重复的内容
"""
# 查询重复数据
if results := self.db.gkld_useful_data.aggregate([
# 对特定字段分组
{
'$group': {
'_id': {
'radar_range': '$radar_range',
'notice_tags': '$notice_tags',
'notice_title': '$notice_title',
'notice_content': '$notice_content',
},
# 统计出现的次数
'count': {
'$sum': 1}
}},
# 过滤分组的字段,选择显示大于一条的数据
{
'$match': {
'count': {
'$gt': 1}}}
# allowDiskUse=True:避免出现超出内存阈值的异常
], allowDiskUse=True):
for item in tqdm(list(results), ncols=100, desc='去重进度'):
count = item['count'] # count默认为整型(int)
radar_range = item['_id']['radar_range']
notice_tags = item['_id']['notice_tags']
notice_title = item['_id']['notice_title']
notice_content = item['_id']['notice_content']
for i in range(1, count):
# 仅留一条数据,删除重复数据
self.db.gkld_useful_data.delete_one({
'radar_range': radar_range,
'notice_tags': notice_tags,
'notice_title': notice_title,
'notice_content': notice_content
})
if __name__ == '__main__':
clean = CleanData()
clean.clean()
解释说明
$group
:用于根据给定的字段进行分组'$sum': 1
:满足分组条件的数据每出现一次,count的值则加1count
:用来统计出现的次数$match
:{'count': {'$gt': 1}}
选择显示出现次数大于一条的数据(大于一条说明数据是重复数据)allowDiskUse=True
:避免出现超出内存阈值的异常tqdm
:一个专门生成进度条的工具包,参考链接- for item in tqdm(
list(results)
):会显示百分比进度条 - for item in tqdm(
results
):不会显示百分比进度条 - 当数据库中无重复数据时
list(results)
和results
效果相同,都没有百分比进度条
✨新方案✨
简介
- 使用以上方案确实可以解决百万级数据去重的问题,但是如果百万级数据中重复的数量比较大(如:十万+),而且MongoDB的配置也比较低,并且用于检测数据是否重复的字段又比较多,这时候使用以上方案进行去重的话,效率就会比较低。当然你可以通过在以上方案的基础上使用多线程或多进程的方式,用来提高以上方案的去重效率也是可以的。
- 不过,最好的方案还是在入库的时候解决数据重复的问题,当然对于已有的未去重的百万级数据库还是推荐通过以上方案进行去重,那么要如何做到在数据入库的时候快速且可靠的进行去重呢?
- 这里我采取的方式是通过将用来去重的特征字段拼接成一个字符串,然后对拼接后的字符串进行MD5加密,最后用MD5加密后生成的密文去替换MongoDB数据库自动生成的
_id
字段,这样在插入重复的数据时,因为数据库里已有了相同的_id
字段对应的数据,所以这个时候插入数据的程序会报异常,我们可以通过捕获这个异常来进行对重复数据的处理(如:覆盖或忽略)。 - 这样做的好处是避免了在插入数据时对数据库中的数据通过
find
等语法进行数据是否重复的查询,你只需要直接插入数据,不用事先检查数据库中的数据是否已存在重复数据,仅仅只需要捕获插入数据时的异常,并且在异常中采取对应的重复数据去重策略即可!
代码示例
- spider
from hashlib import md5 from NewScheme.items import NewschemeItem # 使用MD5加密生成_id字段 encrypt = province_name + city_name + county_name + exam_type_name + info_type_name + notice_title + update_time md = md5(f'{ encrypt}'.encode('utf8')) _id = md.hexdigest()[8:-8] # 保存数据 item = NewschemeItem() item['_id'] = _id item['province_name'] = province_name item['city_name'] = city_name item['county_name'] = county_name item['county_show'] = county_show item['exam_type_name'] = exam_type_name item['info_type_name'] = info_type_name item['notice_title'] = notice_title item['update_time'] = update_time item['notice_source'] = notice_source item['job_info'] = job_info item['job_people_num'] = int(job_people_num) item['job_position_num'] = int(job_position_num) item['job_start_time'] = job_start_time item['job_end_time'] = job_end_time item['notice_content'] = notice_content item['attachment_info'] = attachment_info yield item
这里因为我项目的要求,
_id
字段为16位,如果项目没有特别要求的话可以用默认的32位 - pipelines
# 忽略策略 try: self.collection.insert_one(dict(item)) except Exception as e: # 数据重复则忽略 print(f'{ item.get("_id")}已存在 - ignore') else: print(item.get('notice_title')) # 覆盖策略 try: self.collection.insert_one(dict(item)) except Exception as e: # 数据重复则覆盖 _id = item.get("_id") print(f'{ _id}已存在 - cover') # 删除旧数据 self.collection.delete_one({ '_id': _id}) # 插入新数据 self.collection.insert_one(dict(item)) else: print(item.get('notice_title'))