【python】探讨两list筛选差异项

一、背景

环境中,有多种数据规格很大,通常通过嵌套循环检测,效率低下,不通用。

二、分析过程

大容量数据基本上以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字段则只需要对账反查即可出结果,对我们的代码改进也是可以值得探讨的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335