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.
PoB-Link-Formate¶
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:
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
Trade-Link-Generierung¶
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:
Simplified: Name-Only Search¶
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:
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¶
Input: PoB-Link oder PoB-Code¶
- Typ erkennen:
- Beginnt mit
https://→ Raw-Content holen - Reine Base64 → direkt dekodieren
- Dekodieren via decode_pob()
- XML parsen (stdlib xml.etree.ElementTree oder lxml)
- Items extrahieren:
- Loop über
<Item>Elements - Erste Zeile nach
<![CDATA[parsen für Rarity + Name - Identifiziere Unique vs. Rare vs. Magic
- Pro Item Trade-Link bauen:
- Unique: Name-Search
- Rare: Stat-basierter Search (Key-Mods extrahieren, Stat-IDs matchen, min-Werte setzen)
- Magic: seltener, meist Flask
- 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).
Trade-Link-Limitationen¶
- 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
POESESSIDnö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¶
../items/uniques/— Unique-Stats zum Nachschlagen../economy/trends_current_league.md— Preise zum Abgleich../mechanics/— Skill-/Tree-Mechanik für PoB-Config