为了账号安全,请及时绑定邮箱和手机立即绑定

使用 lxml etree 在 Python 中将大 xml 文件聚合到字典需要很长时间

使用 lxml etree 在 Python 中将大 xml 文件聚合到字典需要很长时间

三国纷争 2022-06-22 18:02:54
我在将大 xml 文件(~300MB)的值迭代和汇总到 python 字典中时遇到问题。我很快意识到,不是 lxml etrees iterparse 会减慢速度,而是每次迭代都访问字典。以下是我的 XML 文件中的代码片段:    <timestep time="7.00">        <vehicle id="1" eclass="HBEFA3/PC_G_EU4" CO2="0.00" CO="0.00" HC="0.00" NOx="0.00" PMx="0.00" fuel="0.00" electricity="0.00" noise="54.33" route="!1" type="DEFAULT_VEHTYPE" waiting="0.00" lane="-27444291_0" pos="26.79" speed="4.71" angle="54.94" x="3613.28" y="1567.25"/>        <vehicle id="2" eclass="HBEFA3/PC_G_EU4" CO2="3860.00" CO="133.73" HC="0.70" NOx="1.69" PMx="0.08" fuel="1.66" electricity="0.00" noise="65.04" route="!2" type="DEFAULT_VEHTYPE" waiting="0.00" lane=":1785290_3_0" pos="5.21" speed="3.48" angle="28.12" x="789.78" y="2467.09"/>    </timestep>    <timestep time="8.00">        <vehicle id="1" eclass="HBEFA3/PC_G_EU4" CO2="0.00" CO="0.00" HC="0.00" NOx="0.00" PMx="0.00" fuel="0.00" electricity="0.00" noise="58.15" route="!1" type="DEFAULT_VEHTYPE" waiting="0.00" lane="-27444291_0" pos="31.50" speed="4.71" angle="54.94" x="3617.14" y="1569.96"/>        <vehicle id="2" eclass="HBEFA3/PC_G_EU4" CO2="5431.06" CO="135.41" HC="0.75" NOx="2.37" PMx="0.11" fuel="2.33" electricity="0.00" noise="68.01" route="!2" type="DEFAULT_VEHTYPE" waiting="0.00" lane="-412954611_0" pos="1.38" speed="5.70" angle="83.24" x="795.26" y="2467.99"/>        <vehicle id="3" eclass="HBEFA3/PC_G_EU4" CO2="2624.72" CO="164.78" HC="0.81" NOx="1.20" PMx="0.07" fuel="1.13" electricity="0.00" noise="55.94" route="!3" type="DEFAULT_VEHTYPE" waiting="0.00" lane="22338220_0" pos="5.10" speed="0.00" angle="191.85" x="2315.21" y="2613.18"/>    </timestep>每个时间步都有越来越多的车辆。该文件中有大约 11800 个时间步长。现在我想根据它们的位置总结所有车辆的值。提供了 x、y 值,我可以将其转换为 lat、long。我目前的方法是使用 lxml etree iterparse 遍历文件,并使用 lat,long 作为 dict 键对值求和。我正在使用本文中的 fast_iter https://www.ibm.com/developerworks/xml/library/x-hiperfparse/但是,这种方法需要大约 25 分钟来解析整个文件。我不确定如何以不同的方式做到这一点。我知道全局变量很糟糕,但我认为这会让它更干净?你能想到别的吗?我知道这是因为字典。如果没有聚合函数,fast_iter 大约需要 25 秒。
查看完整描述

1 回答

?
慕田峪7331174

TA贡献1828条经验 获得超13个赞

您的代码很慢有两个原因:

  • 您做了不必要的工作,并使用了低效的 Python 语句。您不使用veh_id但仍用于int()转换它。您创建一个空字典只是为了在单独的语句中在其中设置 4 个键,您使用单独的str()和 round()调用以及字符串连接,其中字符串格式化可以一步完成所有工作,您重复引用.attrib,因此 Python 必须重复查找该字典属性为你。

  • 当用于每个单独的 (x, y) 坐标时,sumolib.net.convertXY2LonLat()实现效率非常低;pyproj.Proj()它每次都从头开始加载偏移量和对象。pyproj.Proj()例如,我们可以通过缓存实例来切断这里的重复操作。或者我们可以避免使用它,或者通过一步处理所有坐标来使用它一次。

第一个问题可以通过删除不必要的工作和缓存属性字典之类的东西、只使用一次以及在函数参数中缓存重复的全局名称查找来避免(本地名称使用起来更快);关键字纯粹是_...为了避免查找全局变量:

from operator import itemgetter


_fields = ('CO2', 'CO', 'NOx', 'PMx')


def aggregate(

    vehicle,

    _fields=_fields,

    _get=itemgetter(*_fields, 'x', 'y'),

    _conv=net.convertXY2LonLat,

):

    # convert all the fields we need to floats in one step

    *values, x, y = map(float, _get(vehicle.attrib))

    # convert the coordinates to latitude and longitude

    lng, lat = _conv(x, y)

    # get the aggregation dictionary (start with an empty one if missing)

    data = raw_pollution_data.setdefault(

        f"{lng:.4f},{lat:.4f}",

        dict.fromkeys(_fields, 0.0)

    )

    # and sum the numbers

    for f, v in zip(_fields, values):

        data[f] += v

为了解决第二个问题,我们可以用至少重用Proj()实例的东西来替换位置查找;在这种情况下,我们需要手动应用位置偏移:


proj = net.getGeoProj()

offset = net.getLocationOffset()

adjust = lambda x, y, _dx=offset[0], _dy=offset[1]: (x - _dx, y - _dy)


def longlat(x, y, _proj=proj, _adjust=adjust):

    return _proj(*_adjust(x, y), inverse=True)

然后通过替换_conv本地名称在聚合函数中使用它:


def aggregate(

    vehicle,

    _fields=_fields,

    _get=itemgetter(*_fields, 'x', 'y'),

    _conv=longlat,

):

    # function body stays the same

这仍然会很慢,因为它要求我们(x, y)分别转换每一对。


这取决于所使用的确切投影,但您可以简单地量化x并y坐标自己进行分组。您将首先应用偏移量,然后将坐标“四舍五入”,转换和舍入将实现的量相同。在投影(1, 0)和(0, 0)取经度差时,我们知道投影使用的粗略转换率,然后将其除以 10.000 就可以得出聚合区域的大小x和y值:


 (proj(1, 0)[0] - proj(0, 0)[0]) / 10000

对于标准的 UTM 投影,它给了我大约11.5,因此将x和y坐标乘以该因子应该可以得到大致相同数量的分组,而不必对每个时间步长数据点进行完整的坐标转换:


proj = net.getGeoProj()

factor = abs(proj(1, 0)[0] - proj(0, 0)[0]) / 10000

dx, dy = net.getLocationOffset()


def quantise(v, _f=factor):

    return v * _f // _f


def aggregate(

    vehicle,

    _fields=_fields,

    _get=itemgetter(*_fields, 'x', 'y'),

    _dx=dx, _dy=dy,

    _quant=quantise,

):

    *values, x, y = map(float, _get(vehicle.attrib))

    key = _quant(x - _dx), _quant(y - _dy)

    data = raw_pollution_data.setdefault(key, dict.fromkeys(_fields, 0.0))

    for f, v in zip(_fields, values):

        data[f] += v

对于问题中共享的非常有限的数据集,这给了我相同的结果。


但是,如果投影在经度上不同,这可能会导致地图上不同点的结果失真。我也不知道您究竟需要如何聚合整个区域的车辆坐标。


如果您真的只能按经度和纬度 1/10000 度的区域进行聚合,那么如果您将整个 numpy 数组输入到net.convertXY2LonLat(). 这是因为接受数组来批量pyproj.Proj()转换坐标,节省了大量时间,避免进行数十万次单独的转换调用,我们只需要进行一次调用。


与其使用 Python 字典和浮点对象来处理这个问题,不如在这里真正使用 Pandas DataFrame。它可以轻松地从每个元素属性字典中获取字符串(使用具有所有所需键的operator.itemgetter()对象可以非常快速地为您提供这些值),并在摄取数据时将所有这些字符串值转换为浮点数。这些值以紧凑的二进制形式存储在连续内存中,11800 行坐标和数据条目在这里不会占用太多内存。


因此,首先将您的数据加载到 DataFrame中,然后从该对象中一步转换您的 (x, y) 坐标,然后使用Pandas 分组功能按区域聚合值:


from lxml import etree

import pandas as pd

import numpy as np


from operator import itemgetter


def extract_attributes(context, fields):

    values = itemgetter(*fields)

    for _, elem in context:

        yield values(elem.attrib)

        elem.clear()

        while elem.getprevious() is not None:

            del elem.getparent()[0]

    del context


def parse_emissions(filename):

    context = etree.iterparse(filename, tag="vehicle")


    # create a dataframe from XML data a single call

    coords = ['x', 'y']

    entries = ['CO2', 'CO', 'NOx', 'PMx']

    df = pd.DataFrame(

        extract_attributes(context, coords + entries),

        columns=coords + entries, dtype=np.float)


    # convert *all coordinates together*, remove the x, y columns

    # note that the net.convertXY2LonLat() call *alters the 

    # numpy arrays in-place* so we don’t want to keep them anyway. 

    df['lng'], df['lat'] = net.convertXY2LonLat(df.x.to_numpy(), df.y.to_numpy())

    df.drop(coords, axis=1, inplace=True)


    # 'group' data by rounding the latitude and longitude

    # effectively creating areas of 1/10000th degrees per side

    lnglat = ['lng', 'lat']

    df[lnglat] = df[lnglat].round(4)


    # aggregate the results and return summed dataframe

    return df.groupby(lnglat)[entries].sum()


emissions = parse_emissions("/path/to/emission_output.xml")

print(emissions)

使用 Pandas、一个示例 sumo 网络定义文件和一个重构的 XML 文件,通过重复您的 2 个示例时间步长条目 5900 次,我可以在大约 1 秒(总时间)内解析整个数据集。但是,我怀疑您的 11800 次集数太低(因为它小于 10MB XML 数据),所以我将 11800 * 20 == 236000 次样本写入文件,并且使用 Pandas 处理需要 22 秒。


您还可以查看GeoPandas,它可以让您按地理区域进行汇总。


查看完整回答
反对 回复 2022-06-22
  • 1 回答
  • 0 关注
  • 220 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信