一、背景
环境中,有多种数据规格很大,通常通过嵌套循环检测,效率低下,不通用。
二、分析过程
大容量数据基本上以list为主,原来的检测工具经常用到如下代码,甚至叠加嵌套:
code示例:list嵌套对账查询
for _a in a:
if _a not in b:
in_a_not_in_b.append(_a)
在上面代码中,for循环遍历a每个元素,not in实际上也是遍历b的每个元素后,找到b中不存在a中存在的元素,本次循环一共执行了a*b次,时间复杂度为O(n^2).这样条件下,a,b数量越大,时间平方倍增长,若是a,b代表100w条数据,最坏情况下则要执行1万亿次才能得到结果,耗时数小时。
三、解决方案
针对此类大容量已知对比解决的办法有很多,都离不开排序和索引两类,目前共使用以下几种:
(1) 内置函数set()的交并查找(索引类)
(2) 构建索引改变数据结构从list变为dicr存取(索引类)
(3) 利用数据库自建索引的方式进行对账(索引类)
(4) 使用排序算法(归并,快排)对两边数据进行排序(排序类)
下面着重看下此各方法的适用性和对应的效果结论。
3.0 使用排序算法(归并,快排)对两边数据进行排序
在不排序的情况下,元素在列表中寻找自己可能要跨整个列表,如果两个列表都排序,就能大幅减少这种情况,在此基础之上,通过复制列表并将复制后的列表中已经找到的元素删除,会不断提高查询的速率:
效果:
code示例:利用排序进行对账
# coding=utf-8
import copy
from random import random, randint
def add_list(start_num, end_number):
s = []
for i in range(start_num, end_number + 1):
s.append(randint(start_num, end_number))
return s
a = add_list(10, 100000)
b = add_list(10, 100000)
in_a_not_in_b = []
a_sort = sorted(a)
a_sort_copy = copy.copy(a_sort)
b_sort = sorted(b)
b_sort_copy = copy.copy(a_sort)
import time
t1=time.time()
for _a in a:
if _a in b :
in_a_not_in_b.append(_a)
print "(非排序)比较时间为:",time.time()-t1,"s"
t2=time.time()
for _a in a_sort:
if _a not in b_sort_copy:
in_a_not_in_b.append(_a)
else:
b_sort_copy.remove(_a)
print "(排序)比较时间为:",time.time()-t2,"s"
输出为
(非排序)比较时间为: 100.325999975 s
(排序)比较时间为: 1.97900009155 s
优点:
该方法是针对原方法的优化,修改简单,实行方便
缺点:
时间复杂度在O(nlogn)和O(n2)之间,优化后的速度在大容量情况下满足不了要求(上面的例子是10W,100W排序后实测也需要260秒)
3.1 内置函数set()的交并查找
python自带函数set是个将迭代对象转换为无序集合的函数,本质上是hash存储,所以每个元素虽然无序但是hashcode固定,故而在查找元素时,比单纯的list查找,有复杂度上的优势,见下图:
序列 | list | deque | dict | set |
---|---|---|---|---|
x in s (查找) | √ O(n) | √ O(n) | √ O(1) | √ O(1) |
所以原来的代码变为:
code示例:set差值寻不一致
in_a_not_in_b=set(a)-set(b)
或者
in_a_not_in_b=set(a).difference(b)
效果:
时间负载降至O(n),同样100w数据规格下,只需要几秒甚至更短时间就能完成
code示例:set效果比较
a = add_list(10, 1100000)
b = add_list(11, 1120000)
import time
t1=time.time()
in_a_not_in_b=set(a).difference(b)
print "比较时间为:",time.time()-t1,"s"
输出:
比较时间为: 0.116999864578 s
优点:
编写简单,代码清晰,时间复杂度逼近O(n),是最理想结果
缺陷:
set集合要求其元素一定要是能够hash的,也就是不可变数据,但通常我们的数据是字典嵌列表的结构,如:
[{u'status': 1, u'layer': u'', u'protocol': u'OPENFLOW', u'hardware': u''}....]
而字典(dict)格式并不是个可以hash的数据结构,故在很多情况下,就算set优点很多,我们也是无法使用的。
code示例:set集合元素必须可hash
set([{u'status': 1, u'layer': u'', u'protocol': u'OPENFLOW', u'hardware': u''}])
输出:
TypeError: unhashable type: 'dict'
3.2 构建索引改变数据结构从list变为dict存取
虽然set集合无法用在我们大多数的数据结构上,但依然提供了一种可行的思路。
现在litst中嵌套dict的数据换成另外一个可hash同时又能存储及表达dict字典的值,利用元组(tuple)的不可变性,对dict进行编辑。
code示例:dict_list变为tuple_list
# dict:
dict_list = [{u'status': 1, u'layer': u'', u'protocol': u'OPENFLOW', u'hardware': u''}]
# 在已知其中键值得情况下事先构造一个元组key_tuple
key_tuple = (u'status', u'layer', u'protocol', u'hardware')
value_tuple = ()
tuple_list = []
for i in dict_list:
hash_key = tuple()
for key in key_tuple:
hash_key = hash_key + (i.get(key),)
tuple_list.append(hash_key)
print key_tuple
print tuple_list
print set(tuple_list)
输出结果
key_tuple (u'status', u'layer', u'protocol', u'hardware')
tuple_list [(1, u'', u'OPENFLOW', u'')]
set(tuple_list) set([(1, u'', u'OPENFLOW', u'')])
如上拆成2组数据就可以利用set的hash进行排序,但key_tuple存在不确定性,比如在数据并不是每个字段都需要对比的情况下可以构造成字典存取:
code示例:dict_list变为tuple_list,提取部分key构成index_map
# dict:
dict_list = [{u'status': 1, u'layer': u'', u'protocol': u'OPENFLOW', u'hardware': u''}]
# key_tuple减少参数
key_tuple = (u'status', u'layer')
index_map = {}
for i in dict_list:
hash_key = tuple()
for key in key_tuple:
hash_key = hash_key + (i.get(key),)
index_map[hash_key] = i
print "key_tuple", key_tuple
print "tuple_list", tuple_list
print "index_map", index_map
#输出结果
key_tuple (u'status', u'layer')
tuple_list [(1, u'', u'OPENFLOW', u'')]
index_map {(1, u''): {u'status': 1, u'hardware': u'', u'layer': u'', u'protocol': u'OPENFLOW'} }
对index_map进行对账后可以反查index_map的原数据,相比于之前占用了更多内存空间,但对比效率还是很高。
效果:
code示例:dict_list变为tuple_list后对账效果展示
import psutil
import os
# dict:
dict_list = [{u'status': i, u'layer': u'', u'protocol': u'OPENFLOW', u'hardware': u''} for i in range(1, 1000000)]
dict_list2 = [{u'status': i, u'layer': i, u'protocol': u'OPENFLOW', u'hardware': u''} for i in range(1, 1000000)]
#记录初始内存
m1=psutil.Process(os.getpid()).memory_info().rss
# 在已知其中键值的情况下事先构造一个元组key_tuple
key_tuple = (u'status', u'layer', u'protocol', u'hardware')
def transfer_tuple_list(key_tuple, dict_list):
tuple_list = []
for i in dict_list:
hash_key = tuple()
for key in key_tuple:
hash_key = hash_key + (i.get(key),)
tuple_list.append(hash_key)
return tuple_list
t1 = time.time()
tuple_list1 = transfer_tuple_list(key_tuple, dict_list)
tuple_list2 = transfer_tuple_list(key_tuple, dict_list2)
in_tuple_list1_not_in_tuple_list2 = set(tuple_list1).difference(tuple_list2)
print "比较时间为:", time.time() - t1, "s"
print u'内存使用为:%d M'%((psutil.Process(os.getpid()).memory_info().rss-m1)/1000/1024)
输出:
比较时间为: 2.10100007057 s
内存使用为:214 M
时间的缩短是以空间为代价进行平衡,消耗内存214M,但对比时间依然只有几秒。
(ps:案例中100w长度的list多是I/O,若是程序构造,应该使用生成器generator)
优点:
能够对目前的list中嵌套dict的结构进行索引,效率符合要求
缺陷:
将数据转换的思路存储hash的思路,针对list嵌套dict进行重新编辑形成list嵌套tuple,也可以将list嵌套list转成list嵌套tuple,但是针对完全不同的dict元素或是更复杂的结构则显得很无力。
code示例:dict_list中数据key不统一的情况无法使用统一的key_tuple
dict_list = [{u'status': 1, u'layer': u'', u'protocol': u'OPENFLOW', u'hardware': u''}
,{1:2,3:4,5:6},[(1,2,3,4),[1,2,3,4]]]
key_tuple = (u'status', u'layer', u'protocol', u'hardware')#无法通用
由此可见,在更加复杂的情况下,该方法的局限性凸显,仍然不够通用,所以在此基础之上必须要一个完整而统一的解决方案。
3.3 利用数据库自建索引的方式进行数据整理对账
通常情况下,list嵌套dict中的元素必有实际用途,构建数据库将两个来源的数据统一结构无论是适配、修改都非常方便:
code示例:利用sqlalchemy框架构建数据库存储数据并比较
from sqlalchemy import Column, String, Integer
from sqlalchemy.ext.declarative import declarative_base
TableBase = declarative_base()
class Classify(TableBase):
__tablename__ = "classify"
priority = Column(Integer, primary_key=True, default=0)
in_port = Column(Integer, primary_key=True, default=0)
tag = Column(String(50), primary_key=True)
#。。。。
此处省略数据库构建,数据插入和该方法类函数
def reconciliation(self):
q1 = self.dbf.query_by_filter(PortClassify.in_port, tag="A")
q2 = self.dbf.query_by_filter(PortClassify.in_port, tag="B")
#由于在数据库存在索引,利用sql的except和intersection方法可以进行迅速对比
self.dbf.except_queries(q1, q2).all()
self.dbf.except_queries(q2, q1).all()
效果:
import time
def reconciliation(self):
t1 = time.time()
q1 = self.dbf.query_by_filter(PortClassify.in_port, tag="A")
q2 = self.dbf.query_by_filter(PortClassify.in_port, tag="B")
#由于在数据库存在索引,利用sql的except和intersection方法可以进行迅速对比
self.dbf.except_queries(q1, q2).all()
self.dbf.except_queries(q2, q1).all()
print "比较时间为:", time.time() - t1, "s"
输出为:
比较时间为: 3.0150001049042 s
优点:
功能强大,全面,适用性高,对账只是该方案的其中一项功能,且速率达到O(n)
缺陷:
编写难度较大,为了对账去构建数据库代价较大,且插入速度依赖于系统cpu性能,数据量大的情况下插入时间可能会较长。
四、结论
上面四种方案各有千秋,一切皆以业务场景进行选择。当然也可以看到,前期代码中的一些业务项添加唯一索引ID字段则只需要对账反查即可出结果,对我们的代码改进也是可以值得探讨的。