diff options
| author | Daniel Schadt <kingdread@gmx.de> | 2025-11-22 19:43:34 +0100 |
|---|---|---|
| committer | Daniel Schadt <kingdread@gmx.de> | 2025-11-22 19:43:34 +0100 |
| commit | 324fc8923b056bf985d4ae49a7de3ac028a89722 (patch) | |
| tree | 7abf8480c142f7ac3ae6e126135bb3a2d01eae35 | |
| parent | 8f0e56a9325d9b0b663c67fc7fe795b1de370778 (diff) | |
| download | fietsboek-324fc8923b056bf985d4ae49a7de3ac028a89722.tar.gz fietsboek-324fc8923b056bf985d4ae49a7de3ac028a89722.tar.bz2 fietsboek-324fc8923b056bf985d4ae49a7de3ac028a89722.zip | |
speed up track xml serialization
The comment explains it.
| -rw-r--r-- | fietsboek/models/track.py | 81 | ||||
| -rw-r--r-- | fietsboek/util.py | 22 |
2 files changed, 78 insertions, 25 deletions
diff --git a/fietsboek/models/track.py b/fietsboek/models/track.py index 3bf7eee..5f1e081 100644 --- a/fietsboek/models/track.py +++ b/fietsboek/models/track.py @@ -17,6 +17,7 @@ meta information. import datetime import enum import gzip +import io import json import logging from itertools import chain @@ -435,34 +436,64 @@ class Track(Base): :return: The XML representation (a GPX file). """ - gpx = gpxpy.gpx.GPX() - gpx.description = self.description - gpx.name = self.title - segment = gpxpy.gpx.GPXTrackSegment() + # This is a cumbersome way to do it, as we're re-implementing XML + # serialization logic. However, recreating the track in gpxpy and + # letting it serialize it is much slower: + # For a track with around 50,000 points, the gpxpy method takes + # ~5.9 seconds here, while the "manual" buffer takes only ~2.4 seconds. + # This is a speed-up we're happy to take! + buf = io.BytesIO() + buf.write(b'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>') + buf.write(b'<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">') + + buf.write(b"<metadata>") + if self.title: + buf.write(b"<name>%s</name>" % util.xml_escape(self.title)) + if self.description: + buf.write(b"<desc>%s</desc>" % util.xml_escape(self.description)) + buf.write(b"</metadata>") + + # Cache for easy access, especially the date is important since it's a + # dynamic property + date = self.date + write = buf.write + + write(b"<trk>") + write(b"<trkseg>") for point in self.path().points: - segment.points.append( - gpxpy.gpx.GPXTrackPoint( - latitude=point.latitude, - longitude=point.longitude, - elevation=point.elevation, - time=self.date + datetime.timedelta(seconds=point.time_offset), - ) - ) - track = gpxpy.gpx.GPXTrack() - track.segments.append(segment) - gpx.tracks.append(track) + write(b'<trkpt lat="') + write(str(point.latitude).encode("ascii")) + write(b'" lon="') + write(str(point.longitude).encode("ascii")) + write(b'">') + write(b"<ele>") + write(str(point.elevation).encode("ascii")) + write(b"</ele>") + write(b"<time>") + write(str(date + datetime.timedelta(seconds=point.time_offset)).encode("ascii")) + write(b"</time>") + write(b"</trkpt>\n") + write(b"</trkseg>") + write(b"</trk>") + + # This loop is not as hot: for wpt in self.waypoints: - gpx.waypoints.append( - gpxpy.gpx.GPXWaypoint( - longitude=wpt.longitude, - latitude=wpt.latitude, - elevation=wpt.elevation, - name=wpt.name, - comment=wpt.description, - description=wpt.description, - ) + write( + b'<wpt lat="%s" lon="%s">' + % (util.xml_escape(str(wpt.latitude)), util.xml_escape(str(wpt.longitude))) ) - return gpx.to_xml(prettyprint=False).encode("utf-8") + if wpt.elevation is not None: + write(b"<ele>%s</ele>" % util.xml_escape(str(wpt.elevation))) + if wpt.name is not None: + write(b"<name>%s</name>" % util.xml_escape(wpt.name)) + if wpt.description is not None: + write(b"<cmt>%s</cmt>" % util.xml_escape(wpt.description)) + write(b"<desc>%s</desc>" % util.xml_escape(wpt.description)) + write(b"</wpt>") + + write(b"</gpx>") + + return buf.getvalue() @property def date(self): diff --git a/fietsboek/util.py b/fietsboek/util.py index 156b7d4..37e0943 100644 --- a/fietsboek/util.py +++ b/fietsboek/util.py @@ -64,6 +64,9 @@ _windows_device_files = ( ) +_valid_xml_chars = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,-: ") + + def safe_markdown(md_source: str) -> Markup: """Transform a markdown document into a safe HTML document. @@ -480,6 +483,24 @@ def recursive_size(path: Path) -> int: return size +def xml_escape(value: str) -> bytes: + """Escapes and encodes a string to be embedded in a XML document. + + This replaces characters like < and > with their entities. + + :param value: The value. + :return: The escaped and encoded string. + """ + return b"".join( + ( + char.encode("ascii") + if char in _valid_xml_chars + else b"&#x%s;" % hex(ord(char))[2:].encode("ascii") + ) + for char in value + ) + + __all__ = [ "ALLOWED_TAGS", "ALLOWED_ATTRIBUTES", @@ -505,4 +526,5 @@ __all__ = [ "encode_gpx", "secure_filename", "recursive_size", + "xml_escape", ] |
