一次数据处理心得

处理1.5G的CSV文件,有370万行和16列,做均值、透视、聚合、匹配查找。

我需要统计每个关键词不同时期的排名,然后得出最高排名、最低排名、平均排名。根据关键词、排名、ASIN进行模糊和精确匹配筛选。使用这种方式可以发现数据之间的关联性,比如有四组元素((1,A),(1,D)), ((2,B),(2,A),(2,C)),((3,C),(3,D)),((4,E),(4,F))。有两组元素中都有A,可以发现A与1和2有关联,A,B,C在同一组中,A,B,C存在关联性,由于各自与1,2,3关联,因此1,2,3又相互关联,与4不关联。通过选择匹配方式,可以把1,2,3组单独筛选出来。

image-20220116162026377

一开始的时候想都没想就用字典实现了,也比较容易,匹配速度非常快。但键值对层级多了之后容易搞混。之所以用字典,是因为原来的数据是近似随机分布的,对原数据进行遍历后生成格式化的字典,以便在匹配的时候按需索引。

同时panda也有尝试,但是pandas的处理速度耗时惊人,后来发现是我的思路不正确。最初思路是把表格数据二次展开,但是这样原本300多万行就变成了1000多万,一是消耗数据展开的时间,二是展开会出现重复数据,增加内存消耗,三是匹配数大幅增加,匹配关键词量级从X变成3X。

后来发现字典有一个很大的问题,就是内存占用,一个大型字典是完全载入内存的,原数据有1.5G,载入内存后占用飙升了好几个G。处理一套更复杂的数据时,发现内存占用100%,程序就再也运行不下了。

如何解决内存问题?

一个思路是不要把数据完全载入内存,这样就涉及到内存交换,只在需要时载入部分数据。可以用文件或者数据库实现。但是配置数据库实在太麻烦了,如果是sqlite还好说。此外IO型的数据更新会非常慢,尤其是这几百万离散数据更新。我尝试过sqlitedict,数据处理速度影响可能是光速换步行。

在这中间还有一个小插曲,我想试试协程读文件会不会提高读取速度,比如同时读几个文件,疯狂切换,用asyncioaiofiles实现并测试,发现速度大幅下降。还是老老实实流式读文件最好。异步不要乱用,异步是提高资源利用率,而不是添乱。

回到另一个思路,压缩字典。根据业务需求选择不同的数据载入方式。比如有的数据在全程都不会用到,那么在载入的时候就应该排除掉。另外就是尽可能避免数据冗余。能够折叠的就尽量折叠,比如分层,在一些情况下还可以提高索引效率。但是对于需要完整数据的时候,还是没有办法了。

最后只能寄托于pandas了,之前说过pandas运算速度太慢,但pandas可以解决内存占用的问题,至少不会因为内存爆满而无法继续运行。用时间换空间是可以理解的。

时隔几天又重新写了一个同样功能的程序,只不过是pandas方法实现的,测试后发现耗时从原来的的50秒变成了5分钟。显然我是不甘心的。我需要即时生成数据,5分钟实在没有耐心,耗时主要花在了匹配上,二次展开后的源数据有1000多万行,与字典的一百万条左右相比,匹配时间真是无法比。分析数据后,发现是我的思路不对,源数据没必要二次展开,因为同时损失了空间和运算时间。

read_csv时,明确dtype=strusecols=range(x)engine='c'进一步提升数据加载速度。

在关键词匹配的时候,先用groupby将关键词分组,进一步提升匹配速度。

同理用groupby分组运算最大、最小,transform生成列,然后匹配。

把匹配结果放入字典,再做二次运算,与原来的字典相比,体积大幅降低。

测试运行时间字典方式与pandas方式不相上下。

最后我将纯python和pandas方式统一了,写了一个新的匹配引擎。

让人惊讶的是新引擎的python方式比原来的还要快,虽然方法都差不多,还加了引擎判断。

提高性能的一些tips

判断尽量在循环外。

及时中断,避免重复循环。

可以避免的运算,请放在判断中或循环外。

复杂运算在筛选后进行。

提高pandas性能的一些tips

能用itertuples的时候就不要用iterrows()

合理使用groupby整理数据,然后使用aggtransform运算列。

合理使用apply运算数据。

避免数据冗余。

读取文件时选择性载入数据,明确dtype

这次的程序让我同时加深了:unittestpandasasyncio,以及工厂函数的构建思路。

稍微有点理解了接口统一性。因为结构不变,但是支持两种引擎操作。

Python KMP算法

"""
思路:
1、建立next_list,next_list[i]表示当pattern[i]不匹配时,跳转的pattern最大公共区域的下一级值。
"""

s = 'abdabdabababaaababac'
p = 'ababaaababac'
lens, lenp = len(s), len(p)


def build_next():
    next_list = [0] * lenp
    for i in range(1, lenp):
        if p[i] == p[next_list[i - 1]]:
            next_list[i] = next_list[i - 1] + 1
    return next_list


def kmp_match():
    i = 0
    j = 0
    next_list = build_next()
    while i < len(s):
        if lens - i >= lenp - j:
            if s[i] == p[j]:
                i += 1
                j += 1
            elif j == 0:
                i += 1
            else:
                j = next_list[j - 1]
        else:
            return -1
    return lens - j


print('next_list: ', build_next())
print('match_at: ', kmp_match())

Python 异步下载

def async_down_image(urls_dict, save_dirpath):
    async def down_image(client, url, save_path):
        root, name = os.path.split(save_path)
        if os.path.exists(save_path):
            return True
        try_count = 0
        while try_count < 3:
            try:
                async with client.get(url) as resp:
                    if resp.status == 200:
                        content = await resp.read()
                        print(f"Downloading {name}")
                        with open(save_path, 'wb') as fp:
                            fp.write(content)
                        return True
                    else:
                        try_count += 1
                        await asyncio.sleep(try_count)
            except:
                try_count += 1
                await asyncio.sleep(try_count)
        return False

    async def main(urls):
        async with aiohttp.ClientSession() as client:
            tasks = []
            for filename, url in urls.items():
                save_path = os.path.join(save_dirpath, filename)
                tasks.append(
                    asyncio.create_task(down_image(client, url, save_path=save_path))
                )
            await asyncio.wait(tasks)

    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    asyncio.run(main(urls_dict))

Excel VBA代码找回

开启了宏命令的Excel一不小心就会出现莫名奇妙的问题,轻则VBA代码丢失,重则工作薄损坏,无法打开或保存。确实很恶心。

在中文语言的Excel中新建了宏,在英文语言的Excel中很有可能就打不开了,甚至中文Excel保存后再次打开也会提示文件损坏,辛辛苦苦写的VBA代码就那样没了。

修复是不可能真正修复的,顶多就删除vbaProject.bin这样子,那么写的VBA代码就直接放弃了吗?显然不应该。

xlsx/xlsb实际上是一个压缩包文件,将其后缀改为zip解压,获得大致如下的目录结构。vbaProject.bin就存储了VBA工程文件,包括VBA代码。vbaProject.bin实际上也是一个压缩文件,还可以再解压,但是不能使用常规方式提取出代码。

这篇文章说明了office三件套的文件格式实际是ole文件(oledump.py | Didier Stevens

DESKTOP-K65CV8V :: Users/Desktop/book % tree -L 3
.
├── [Content_Types].xml
├── _rels
├── customXml
│   ├── _rels
│   │   └── item1.xml.rels
│   ├── item1.xml
│   └── itemProps1.xml
├── docProps
│   ├── app.xml
│   └── core.xml
└── xl
    ├── _rels
    │   └── workbook.bin.rels
    ├── calcChain.bin
    ├── connections.bin
    ├── metadata.bin
    ├── printerSettings
    │   └── printerSettings1.bin
    ├── queryTables
    │   └── queryTable1.bin
    ├── sharedStrings.bin
    ├── styles.bin
    ├── tables
    │   ├── _rels
    │   ├── table1.bin
    │   ├── table2.bin
    │   └── table3.bin
    ├── theme
    │   └── theme1.xml
    ├── vbaProject.bin
    ├── workbook.bin
    └── worksheets
        ├── _rels
        ├── binaryIndex1.bin
        ├── binaryIndex2.bin
        ├── binaryIndex3.bin
        ├── sheet1.bin
        ├── sheet2.bin
        └── sheet3.bin

借助该文提供的oledump.py(oledump.py | Didier Stevens)可对ole文件进行分析。

以下简单介绍利用oledump.py提取VBA代码的用法。

1、查看vbaProject.bin结构

image-20211118222429078

确认模块位置-以Module或模块开头的编号。

查看模块代码内容:

image-20211118222621902

Python about the metaclass

花了好多天去理解unittest.main()和Base.metadata.create_all()的实现方式,如何在不实例化的情况下知道子类的信息?

以下为实验代码,详细参考《Python Cookbook(第3版)中文版》

class Metaclass(type):
    childs = []
    def __init__(self, clsname, bases, clsdict):
        super().__init__(clsname, bases, clsdict)
        self.childs.append(clsname)

class Base(metaclass=Metaclass):
    pass


class Tree1(Base):
    pass

class Tree2(Base):
    pass

if __name__ == '__main__':
    for c in Base.childs:
        print(c)

输出结果:

Base
Tree1
Tree2

Typora + OSS + Python,我实现了一个简单又自由的博文发布流程

原来的标题为:《Typora + 阿里云OSS,我用Python写了一个简单又自由的图床脚本》,重点在图床脚本。不过写到后面发现更像是一个图文发布流程,就改了一下标题,也确实如此。

Continue reading “Typora + OSS + Python,我实现了一个简单又自由的博文发布流程”