使用 lxml etree 在 Python 中将大 xml 文件聚合到字典中花费的时间太长

Aggregating big xml file to dictionary takes too long in Python using lxml etree

我在将大 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 值,我可以将其转换为纬度、经度。

我目前的方法是使用 lxml etree iterparse 遍历文件并使用 lat,long 和 dict 键对值求和。

我正在使用本文中的 fast_iter https://www.ibm.com/developerworks/xml/library/x-hiperfparse/

from lxml import etree

raw_pollution_data = {}

def fast_iter(context, func):
    for _, elem in context:
        func(elem)
        elem.clear()
        while elem.getprevious() is not None:
            del elem.getparent()[0]
    del context

def aggregate(vehicle):
    veh_id = int(vehicle.attrib["id"])
    veh_co2 = float(vehicle.attrib["CO2"])
    veh_co = float(vehicle.attrib["CO"])
    veh_nox = float(vehicle.attrib["NOx"]) 
    veh_pmx = float(vehicle.attrib["PMx"]) # mg/s
    lng, lat = net.convertXY2LonLat(float(vehicle.attrib["x"]), float(vehicle.attrib["y"]))

    coordinate = str(round(lat, 4)) + "," + str(round(lng, 4))

    if coordinate in raw_pollution_data:
        raw_pollution_data[coordinate]["CO2"] += veh_co2
        raw_pollution_data[coordinate]["NOX"] += veh_nox
        raw_pollution_data[coordinate]["PMX"] += veh_pmx
        raw_pollution_data[coordinate]["CO"] += veh_co
    else:
        raw_pollution_data[coordinate] = {}
        raw_pollution_data[coordinate]["CO2"] = veh_co2
        raw_pollution_data[coordinate]["NOX"] = veh_nox
        raw_pollution_data[coordinate]["PMX"] = veh_pmx
        raw_pollution_data[coordinate]["CO"] = veh_co

def parse_emissions():
    xml_file = "/path/to/emission_output.xml"
    context = etree.iterparse(xml_file, tag="vehicle")
    fast_iter(context, aggregate)
    print(raw_pollution_data)

但是,这种方法需要大约 25 分钟来解析整个文件。我不确定如何做不同的事情。我知道全局变量很糟糕,但我认为这会使它更干净?

你能想到别的吗?我知道这是因为字典。如果没有聚合函数,fast_iter 大约需要 25 秒。

您的代码运行缓慢有两个原因:

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

  • sumolib.net.convertXY2LonLat() implementation 用于每个单独的 (x, y) 坐标时效率非常低;它每次都从头开始加载偏移量和 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) 对。

这取决于所使用的确切投影,但您可以简单地量化 xy 坐标本身来进行分组。您将首先应用偏移量,然后通过相同的转换和舍入量来“舍入”坐标。当投影 (1, 0)(0, 0) 并取经度差时,我们知道投影使用的粗略转换率,然后将其除以 10.000 得到聚合区域的大小 xy 值:

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

对于标准的 UTM 投影,我得到了大约 11.5,因此乘以然后将 xy 坐标除以该因子应该会给你大致相同的分组量无需为每个时间步数据点进行完整的坐标转换:

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 度经度和纬度的区域进行聚合,那么你可以将 (x, y) 对转换为 long/lat 对,如果你改为输入 整个 numpy 数组net.convertXY2LonLat()。这是因为 pyproj.Proj() 接受数组以批量 转换坐标 ,从而节省了 可观的 时间,避免进行数十万次单独的转换调用,我们只会打一个电话。

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

因此,首先将您的数据加载到 DataFrame ,然后从该对象一步转换您的 (x, y) 坐标 ,然后才使用 Pandas grouping functionality:

按区域汇总值
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,一个样本相扑网络定义文件,和一个重建的 XML 文件,通过重复你的 2 个样本时间步条目 5900 次,我可以在大约 1 秒内解析整个数据集,总时间。但是,我怀疑您的 11800 时间集数太少了(因为它小于 10MB XML 数据),所以我将 11800 * 20 == 236000 倍的样本写到一个文件中,这花了 22 秒使用 Pandas.

处理

你也可以看看GeoPandas, which would let you aggregate by geographical areas