Zum Inhalt

Path of Building (PoB) Link Parsing + Trade-Link-Generator

Anleitung wie ich (Claude mit Playwright/Python) einen PoB-Link analysiere, die Items extrahiere und daraus Trade-Such-Links generiere.

1. pastebin.com

Legacy-Standard. Format: https://pastebin.com/xxxxxxxx - Raw-URL-Transform: https://pastebin.com/raw/xxxxxxxx - Inhalt: Base64 der PoB-XML (deflate-komprimiert)

2. pobb.in

Moderne Alternative, aktuell dominierend. Format: https://pobb.in/xxxxxxxxxx - Raw-Export via: https://pobb.in/<ID>/raw (gibt Base64-Code direkt) - Oder via JSON: https://pobb.in/<ID>/json

3. poe.ninja Build-Profiles

https://poe.ninja/poe1/builds/mirage/character/<characterName> — hat "Export to Path of Building"-Button der einen PoB-Code generiert

4. Direkter PoB-Code

Base64-String, typisch 10-50 KB, ohne URL-Wrapper. User paste direkt.

Dekodierung eines PoB-Codes

PoB-Codes sind Base64-URL-Safe mit zlib-deflate-Kompression:

import base64, zlib

def decode_pob(code):
    # Normalize url-safe base64
    code = code.strip().replace('-', '+').replace('_', '/')
    # Pad to multiple of 4
    code += '=' * (-len(code) % 4)
    # Decode + decompress
    decoded = base64.b64decode(code)
    xml = zlib.decompress(decoded).decode('utf-8')
    return xml

Output: XML-String mit Struktur: - <PathOfBuilding> Root - <Build> — Class, Ascendancy, Level, Bandit, Pantheon - <Items> — Item-Set(s) mit <Item id="N"> Child-Elements - <Skills> — Skill-Groups mit Gem-Slots - <Tree> — Passive-Tree-URL - <Config> — Buff/Config-Inputs - <Notes> — User-Notes

XML-Quirks die beim Parsing oft vergessen werden

Beim Bauen eines Trade-Tools 3 mal reingelaufen — alles aus echten PoBs verifiziert:

1. Aktives Item-Set vs. aktiver Tree-Spec sind unabhängig - <Items activeItemSet="N">N ist die id des aktiven <ItemSet> (nicht die Position!) - <Tree activeSpec="N">N ist der 1-basierte Index in die Liste der <Spec>-Elemente (nicht die id) - Beide können theoretisch divergieren. In der Praxis matched man besser per Title zwischen ItemSet und Spec, sonst wechselt der User das ItemSet im UI und kriegt Jewels aus dem falschen Tree.

2. Tree-gesockelte Jewels stehen NICHT im ItemSet - ItemSet-<Slot>-Liste enthält nur Equipment + Flasks + Abyssal-Sockets - Jewels in Tree-Sockets (Cluster, Watcher's Eye, Voices, Healthy Mind, Megalomaniac, Timeless …) liegen als <Socket itemId="X" nodeId="Y"/> innerhalb des <Spec>-Elements - Im ItemSet gibt's nur <SocketIdURL nodeId="Y"/> — das ist nur für die Tree-URL-Anzeige, kein itemId. Wer hier nach itemId greift, verliert alle Tree-Jewels.

3. Set/Spec-Titles haben PoB-Color-Codes inline Format: ^xRRGGBB (Hex-Color, 6 Hex-Chars) oder ^N (Single-Digit-Short-Color 0-9). Beispiele aus echten PoBs: ^xDDF6D2Mid Budget, ^xFAA4BDPre-PS Totem, ^2LEVELING TREE, ^x00FFDEMageblood Setup. Stripping:

s.replace(/\^x[0-9A-Fa-f]{6}/g, '').replace(/\^[0-9]/g, '').trim()

4. Aktiver SkillSet via <Skills activeSkillSet="N"> Analog zu Items — N ist die id, nicht der Index. Skills in inaktiven Sets sollte man nicht in den Trade-Filter übernehmen.

5. Gem-Detection via nameSpec + skillId - nameSpec-Attribut hat den Klartext-Namen ("Vaal Lightning Trap", "Awakened Greater Multiple Projectiles") - skillId startet mit Awakened für Awakened-Gems, Support für Supports, Vaal für Vaal-Skills - variantId enthält für transfigured Gems die Disc: AltX / AltY (z.B. "Raise Zombie of Falling" hat variantId AltY) - Disabled Gems: enabled="false" — überspringen

6. PathOfBuilding2-Root = PoE2 Wer PoE1-Trade-URLs baut: <PathOfBuilding2> als Root oder <Build targetVersion="poe2"|"2_..."|"4_0"> → ablehnen, das Trade-System ist komplett anders.

Item-Element Format

Jeder Item im <Items>-Block ist mehrzeilige Text-Repräsentation:

Rarity: UNIQUE
Atziri's Foible
Paua Amulet
Unique ID: abc123...
Item Level: 75
Quality: 20
Implicits: 0
+100 to maximum Mana
(16-24)% increased maximum Mana
(80-100)% increased Mana Regeneration Rate
Items and Gems have 25% reduced Attribute Requirements

Parse-Regeln: 1. Erste Zeile: Rarity: UNIQUE|RARE|MAGIC|NORMAL 2. Zweite Zeile (Unique/Magic/Rare): Item-Name 3. Dritte Zeile (bei Unique): Base-Type 4. Explicit/Implicit Mods folgen nach Implicits: N-Zeile

Offizielle Trade-URL (pathofexile.com/trade)

Base: https://www.pathofexile.com/trade/search/<LEAGUE>

Query-Format (URL-encoded JSON):

{
  "query": {
    "status": {"option": "online"},
    "name": "Atziri's Foible",
    "type": "Paua Amulet",
    "stats": [
      {"type": "and", "filters": []}
    ]
  },
  "sort": {"price": "asc"}
}

Komplett-URL:

https://www.pathofexile.com/trade/search/Mirage?q={URL-encoded-JSON}

Wenn nur Unique-Name bekannt:

import urllib.parse, json

def trade_link(unique_name, league='Mirage'):
    q = {
        "query": {
            "status": {"option": "online"},
            "name": unique_name,
            "stats": [{"type": "and", "filters": []}]
        },
        "sort": {"price": "asc"}
    }
    return f"https://www.pathofexile.com/trade/search/{league}?q={urllib.parse.quote(json.dumps(q))}"

Mit spezifischen Mod-Filtern (z.B. "nur T1-Life")

Für die Mod-Filter braucht man Trade-API Stat-IDs — diese gibt es unter:

https://www.pathofexile.com/api/trade/data/stats
JSON-Response enthält alle verfügbaren Stat-IDs (Explicit, Implicit, Pseudo, Crafted, Enchant).

Beispiel Stat-ID für "+# to maximum Life": - explicit.stat_3299347043 — "+# to maximum Life"

Mit Filter:

{
  "query": {
    "stats": [{
      "type": "and",
      "filters": [
        {"id": "explicit.stat_3299347043", "value": {"min": 90}}
      ]
    }]
  }
}

Workflow-Ablauf für mich

  1. Typ erkennen:
  2. Beginnt mit https:// → Raw-Content holen
  3. Reine Base64 → direkt dekodieren
  4. Dekodieren via decode_pob()
  5. XML parsen (stdlib xml.etree.ElementTree oder lxml)
  6. Items extrahieren:
  7. Loop über <Item> Elements
  8. Erste Zeile nach <![CDATA[ parsen für Rarity + Name
  9. Identifiziere Unique vs. Rare vs. Magic
  10. Pro Item Trade-Link bauen:
  11. Unique: Name-Search
  12. Rare: Stat-basierter Search (Key-Mods extrahieren, Stat-IDs matchen, min-Werte setzen)
  13. Magic: seltener, meist Flask
  14. Ausgabe: Markdown-Tabelle Item → Trade-Link

Python-Skelett

import base64, zlib, re, json, urllib.parse, urllib.request
import xml.etree.ElementTree as ET

def fetch_pob_raw(url_or_code):
    """Given URL or raw code, return the base64-code."""
    if url_or_code.startswith('https://'):
        if 'pobb.in' in url_or_code:
            # pobb.in raw endpoint
            raw_url = url_or_code.rstrip('/') + '/raw'
        elif 'pastebin.com' in url_or_code:
            raw_url = url_or_code.replace('/pastebin.com/', '/pastebin.com/raw/')
        else:
            raw_url = url_or_code
        with urllib.request.urlopen(raw_url) as r:
            return r.read().decode('utf-8').strip()
    return url_or_code.strip()

def decode_pob(code):
    code = code.replace('-', '+').replace('_', '/')
    code += '=' * (-len(code) % 4)
    return zlib.decompress(base64.b64decode(code)).decode('utf-8')

def parse_items(xml_string):
    root = ET.fromstring(xml_string)
    items = []
    for item_elem in root.iter('Item'):
        text = (item_elem.text or '').strip()
        lines = text.split('\n')
        if len(lines) < 2: continue
        rarity_line = lines[0]
        if not rarity_line.startswith('Rarity:'): continue
        rarity = rarity_line.split(':', 1)[1].strip()
        name = lines[1].strip() if len(lines) > 1 else ''
        base = lines[2].strip() if len(lines) > 2 else ''
        items.append({
            'rarity': rarity,
            'name': name,
            'base': base,
            'raw': text
        })
    return items

def trade_link_unique(name, league='Mirage'):
    q = {
        "query": {"status": {"option": "online"}, "name": name},
        "sort": {"price": "asc"}
    }
    return f"https://www.pathofexile.com/trade/search/{league}?q={urllib.parse.quote(json.dumps(q))}"

def trade_link_rare(base_type, top_mods, league='Mirage'):
    """top_mods = list of (stat_id, min_value) tuples."""
    q = {
        "query": {
            "status": {"option": "online"},
            "type": base_type,
            "stats": [{
                "type": "and",
                "filters": [{"id": sid, "value": {"min": v}} for sid, v in top_mods]
            }]
        },
        "sort": {"price": "asc"}
    }
    return f"https://www.pathofexile.com/trade/search/{league}?q={urllib.parse.quote(json.dumps(q))}"

# Usage:
# pob_code = fetch_pob_raw('https://pobb.in/abcdefg')
# xml = decode_pob(pob_code)
# items = parse_items(xml)
# for it in items:
#     if it['rarity'] == 'UNIQUE':
#         print(f"{it['name']}: {trade_link_unique(it['name'])}")

Wichtige Stat-IDs (cheat sheet)

Typische Trade-Stat-IDs (aus Trade-API):

Mod Stat-ID
+# to maximum Life explicit.stat_3299347043
+#% to maximum Life explicit.stat_983749596
+# to maximum Energy Shield explicit.stat_3489782002
+#% to Fire Resistance explicit.stat_3372524247
+#% to Cold Resistance explicit.stat_4220027924
+#% to Lightning Resistance explicit.stat_1671376347
+#% to Chaos Resistance explicit.stat_2923486259
+# to maximum Mana explicit.stat_1050105434
+#% increased Movement Speed explicit.stat_2250533757
+#% Increased Attack Speed explicit.stat_210067635
+#% Increased Cast Speed explicit.stat_2891184298
+#% to Global Critical Strike Multiplier explicit.stat_3556824919

Komplette Liste: https://www.pathofexile.com/api/trade/data/stats (JSON-Dump, 2000+ Stat-IDs).

  • Official GGG Trade benötigt Login für volles Browsing (robots.txt + Anti-Bot)
  • Anonym: Link funktioniert aber Ergebnisse nur teilweise sichtbar
  • API-Access: Session-Cookie über POESESSID nötig für programmatische Queries
  • Rate-Limit: GGG throttled heftig bei Bot-Access

Alternative zu GGG-Trade

  • poe.ninja/poe1/economy/ für aggregierte Preise (keine Einzel-Listings)
  • tftt.io (Trade Facilitator) für Bulk-Trades
  • FilterBlade / Awakened PoE Trade als Desktop-Tools

Anwendungsbeispiel

User postet: https://pobb.in/abc123xyz

Mein Ablauf: 1. fetch_pob_raw('https://pobb.in/abc123xyz') → Base64-Code 2. decode_pob(code) → XML 3. parse_items(xml) → Liste von Items 4. Pro Unique: trade_link_unique(name) → Klickbarer Link 5. Pro Rare: Parse Top-3-Mods → trade_link_rare(base, [(stat_id, min), ...]) → Link mit Filter

Output: Markdown-Tabelle

| Slot | Item | Rarity | Trade-Link |
|---|---|---|---|
| Helm | The Gull | Unique | [search](https://...) |
| Body | Rare | Rare | [search-T1-Life](https://...) |

Playwright-Fallback für Login-geschützten Trade

Wenn programmatischer Zugriff auf Trade-Ergebnisse nötig: 1. Playwright mit persistentem User-Data-Dir (wir haben das schon eingerichtet!) 2. User loggt sich manuell einmal in pathofexile.com ein 3. POESESSID-Cookie bleibt gespeichert 4. Ab dann kann ich Trade-Seiten scrapen

TODO bei nächster Iteration

  • Stat-ID-Complete-Map lokal cachen (aus api/trade/data/stats)
  • Rare-Item-Prioritäts-Heuristik (welche Mods sind im Build wichtig?)
  • PoB-XML-Parser als wiederverwendbares Script in /home/derbe/bin/pob_parse.py
  • Build-Vergleich-Tool: zwei PoB-Codes, Delta in Items/Gems/Tree

Querverweise