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

我在将大 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 秒。


三国纷争
浏览 332回答 1
1回答

慕田峪7331174

您的代码很慢有两个原因:您做了不必要的工作,并使用了低效的 Python 语句。您不使用veh_id但仍用于int()转换它。您创建一个空字典只是为了在单独的语句中在其中设置 4 个键,您使用单独的str()和&nbsp;round()调用以及字符串连接,其中字符串格式化可以一步完成所有工作,您重复引用.attrib,因此 Python 必须重复查找该字典属性为你。当用于每个单独的 (x, y) 坐标时,sumolib.net.convertXY2LonLat()实现效率非常低;pyproj.Proj()它每次都从头开始加载偏移量和对象。pyproj.Proj()例如,我们可以通过缓存实例来切断这里的重复操作。或者我们可以避免使用它,或者通过一步处理所有坐标来使用它一次。第一个问题可以通过删除不必要的工作和缓存属性字典之类的东西、只使用一次以及在函数参数中缓存重复的全局名称查找来避免(本地名称使用起来更快);关键字纯粹是_...为了避免查找全局变量:from operator import itemgetter_fields = ('CO2', 'CO', 'NOx', 'PMx')def aggregate(&nbsp; &nbsp; vehicle,&nbsp; &nbsp; _fields=_fields,&nbsp; &nbsp; _get=itemgetter(*_fields, 'x', 'y'),&nbsp; &nbsp; _conv=net.convertXY2LonLat,):&nbsp; &nbsp; # convert all the fields we need to floats in one step&nbsp; &nbsp; *values, x, y = map(float, _get(vehicle.attrib))&nbsp; &nbsp; # convert the coordinates to latitude and longitude&nbsp; &nbsp; lng, lat = _conv(x, y)&nbsp; &nbsp; # get the aggregation dictionary (start with an empty one if missing)&nbsp; &nbsp; data = raw_pollution_data.setdefault(&nbsp; &nbsp; &nbsp; &nbsp; f"{lng:.4f},{lat:.4f}",&nbsp; &nbsp; &nbsp; &nbsp; dict.fromkeys(_fields, 0.0)&nbsp; &nbsp; )&nbsp; &nbsp; # and sum the numbers&nbsp; &nbsp; for f, v in zip(_fields, values):&nbsp; &nbsp; &nbsp; &nbsp; 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):&nbsp; &nbsp; return _proj(*_adjust(x, y), inverse=True)然后通过替换_conv本地名称在聚合函数中使用它:def aggregate(&nbsp; &nbsp; vehicle,&nbsp; &nbsp; _fields=_fields,&nbsp; &nbsp; _get=itemgetter(*_fields, 'x', 'y'),&nbsp; &nbsp; _conv=longlat,):&nbsp; &nbsp; # function body stays the same这仍然会很慢,因为它要求我们(x, y)分别转换每一对。这取决于所使用的确切投影,但您可以简单地量化x并y坐标自己进行分组。您将首先应用偏移量,然后将坐标“四舍五入”,转换和舍入将实现的量相同。在投影(1, 0)和(0, 0)取经度差时,我们知道投影使用的粗略转换率,然后将其除以 10.000 就可以得出聚合区域的大小x和y值:&nbsp;(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]) / 10000dx, dy = net.getLocationOffset()def quantise(v, _f=factor):&nbsp; &nbsp; return v * _f // _fdef aggregate(&nbsp; &nbsp; vehicle,&nbsp; &nbsp; _fields=_fields,&nbsp; &nbsp; _get=itemgetter(*_fields, 'x', 'y'),&nbsp; &nbsp; _dx=dx, _dy=dy,&nbsp; &nbsp; _quant=quantise,):&nbsp; &nbsp; *values, x, y = map(float, _get(vehicle.attrib))&nbsp; &nbsp; key = _quant(x - _dx), _quant(y - _dy)&nbsp; &nbsp; data = raw_pollution_data.setdefault(key, dict.fromkeys(_fields, 0.0))&nbsp; &nbsp; for f, v in zip(_fields, values):&nbsp; &nbsp; &nbsp; &nbsp; data[f] += v对于问题中共享的非常有限的数据集,这给了我相同的结果。但是,如果投影在经度上不同,这可能会导致地图上不同点的结果失真。我也不知道您究竟需要如何聚合整个区域的车辆坐标。如果您真的只能按经度和纬度 1/10000 度的区域进行聚合,那么如果您将整个 numpy 数组输入到net.convertXY2LonLat(). 这是因为接受数组来批量pyproj.Proj()转换坐标,节省了大量时间,避免进行数十万次单独的转换调用,我们只需要进行一次调用。与其使用 Python 字典和浮点对象来处理这个问题,不如在这里真正使用 Pandas DataFrame。它可以轻松地从每个元素属性字典中获取字符串(使用具有所有所需键的operator.itemgetter()对象可以非常快速地为您提供这些值),并在摄取数据时将所有这些字符串值转换为浮点数。这些值以紧凑的二进制形式存储在连续内存中,11800 行坐标和数据条目在这里不会占用太多内存。因此,首先将您的数据加载到 DataFrame中,然后从该对象中一步转换您的 (x, y) 坐标,然后使用Pandas 分组功能按区域聚合值:from lxml import etreeimport pandas as pdimport numpy as npfrom operator import itemgetterdef extract_attributes(context, fields):&nbsp; &nbsp; values = itemgetter(*fields)&nbsp; &nbsp; for _, elem in context:&nbsp; &nbsp; &nbsp; &nbsp; yield values(elem.attrib)&nbsp; &nbsp; &nbsp; &nbsp; elem.clear()&nbsp; &nbsp; &nbsp; &nbsp; while elem.getprevious() is not None:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; del elem.getparent()[0]&nbsp; &nbsp; del contextdef parse_emissions(filename):&nbsp; &nbsp; context = etree.iterparse(filename, tag="vehicle")&nbsp; &nbsp; # create a dataframe from XML data a single call&nbsp; &nbsp; coords = ['x', 'y']&nbsp; &nbsp; entries = ['CO2', 'CO', 'NOx', 'PMx']&nbsp; &nbsp; df = pd.DataFrame(&nbsp; &nbsp; &nbsp; &nbsp; extract_attributes(context, coords + entries),&nbsp; &nbsp; &nbsp; &nbsp; columns=coords + entries, dtype=np.float)&nbsp; &nbsp; # convert *all coordinates together*, remove the x, y columns&nbsp; &nbsp; # note that the net.convertXY2LonLat() call *alters the&nbsp;&nbsp; &nbsp; # numpy arrays in-place* so we don’t want to keep them anyway.&nbsp;&nbsp; &nbsp; df['lng'], df['lat'] = net.convertXY2LonLat(df.x.to_numpy(), df.y.to_numpy())&nbsp; &nbsp; df.drop(coords, axis=1, inplace=True)&nbsp; &nbsp; # 'group' data by rounding the latitude and longitude&nbsp; &nbsp; # effectively creating areas of 1/10000th degrees per side&nbsp; &nbsp; lnglat = ['lng', 'lat']&nbsp; &nbsp; df[lnglat] = df[lnglat].round(4)&nbsp; &nbsp; # aggregate the results and return summed dataframe&nbsp; &nbsp; 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,它可以让您按地理区域进行汇总。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Python