xShopum • XML Entegrasyon Rehberi

Token Korumalı XML ile Ürün Çekme ve Upsert Rehberi

Bu rehber; xShopum ürün verisini token ile korunan XML üzerinden çekip,
ister WooCommerce/WordPress, ister OpenCart, ister hazır e-ticaret altyapıları (ikas / Ticimax / IdeaSoft vb.),
ister pazaryeri entegrasyon servisleri (StockMount tarzı) üzerinden
doğru şekilde güncellemen (insert / update, yani upsert) için hazırlanmıştır.

✅ Büyük feed’lerde stream parse
✅ SKU bazlı upsert mantığı
✅ WooCommerce / OpenCart için pratik yol
✅ Hepsiburada / Trendyol / N11 vb. için listeleme senaryosu

Bu rehber kimler için hazırlanmıştır?

Bu doküman; XML entegrasyonu konusunda temel teknik bilgisi olan, kendi altyapısını geliştiren veya mevcut sistemine özel entegrasyon yazan yazılımcılar, ajanslar ve entegrasyon firmaları için hazırlanmıştır. İçerik, teknik detaylar ve örnek senaryolar içerir.

Önemli not:
Eğer bir entegrasyon firması, hazır yazılım veya pazaryeri entegratörü (ör. StockMount, T-Soft vb.) ile çalışıyorsanız, bu dokümandaki teknik detaylara ihtiyaç duymazsınız. Bu durumda entegrasyon sağlayıcınıza sadece XML bağlantısını (token’lı link) iletmeniz yeterlidir.

Benzer şekilde, WooCommerce veya OpenCart üzerinde bir eklenti / modül kullanıyorsanız; XML alanları, parse mantığı veya upsert senaryoları ile manuel olarak ilgilenmenize gerek yoktur. Gerekli tüm yapılandırmalar ilgili eklenti/modül arayüzü üzerinden yapılmaktadır.

Bu rehber, hazır çözümler dışında kendi entegrasyonunu yazmak isteyen geliştiricilere yol göstermek amacıyla hazırlanmış bir teknik dokümantasyon niteliğindedir.

Bu sayfada ne var?
1) Mantık & kapsam • 2) Token güvenliği • 3) XML alanları • 4) Mimari • 5) Stream parse •
6) Alan eşleme • 7) Upsert stratejisi • 8) WooCommerce/OpenCart pratik yol •
9) Hazır altyapılar checklist • 10) Pazaryerlerine listeleme (Hepsiburada/Trendyol/N11/Çiçeksepeti) • 11) Sık yapılan hatalar

1) Kapsam ve Temel Mantık

XML entegrasyonunun özü şudur: Sisteminiz belli aralıklarla XML’i indirir, XML içindeki her ürünü SKU ile eşleştirir ve veritabanınızı günceller.
“Bugün 1.000 ürün var, yarın 10.000 ürün var” gibi düşünün; her sync’te gelen listeye bakıp “bu SKU bende var mı?” diye karar verirsiniz.

Bu rehberdeki örnek ürün:
▸ SKU: XSU-K1012
▸ Başlık: 14 cm Vantuzlu Jel Anal Plug
▸ Kategori: Anal Oyuncaklar
▸ Stok: 247
▸ Satış Fiyatı: 305.00 TL
▸ Video: Var (Product_Video_URL)
Kimler Kullanabilir?
  • WooCommerce / WordPress (import eklentileri veya özel entegrasyon)
  • OpenCart (import modülü / cron tabanlı script)
  • Hazır altyapılar (ikas, Ticimax, IdeaSoft vb. — plan/modül durumuna göre)
  • Pazaryeri entegrasyonları (StockMount tarzı servisler / özel yazılım)
  • Özel yazılım (ASP.NET, PHP, Node.js, Python, Java)
Stok Notu
Stok bilgisi feed içinde <Stock> alanından gelir.
Entegrasyonunuz stok güncellemesini bu alanı baz alarak yapmalıdır.
(Örn: Stock=247)

2) Güvenlik: Token ile XML Erişimi

xShopum XML erişimi ticari gizlilik nedeniyle token ile korunur. Token bayi panelinden oluşturulur ve sadece size özeldir.
Kısacası XML linkini “şifreli bir kapı” gibi düşünün; token olmadan feed açılmaz.

Örnek XML URL
https://www.xshopum.com/xml-feed?token=YOUR_TOKEN
  • Token’ı kod içine gömmek yerine config / environment variable olarak saklayın.
  • Token’lı linkleri repo, ekran görüntüsü, destek talebi gibi yerlerde paylaşmayın.
  • İsteklerde timeout + retry kurgusu yapın (internet her zaman stabil değil).

3) XML Yapısı: “post” Düğümü ve Alanlar

Bu XML’de her ürün <post> … </post> bloğu olarak gelir. Bizim işimiz bu bloğun içinden alanları okuyup kendi sistemimizde doğru yerlere yazmaktır.

<post>
  <ID>1615</ID>
  <SKU>XSU-K1012</SKU>
  <Title>14 cm Vantuzlu Jel Anal Plug</Title>
  <Stock>247</Stock>
  <Wholesale_Incl_Tax_TL>189.88</Wholesale_Incl_Tax_TL>
  <Sale_Price_TL>305.00</Sale_Price_TL>
  <Product_Category>Anal Oyuncaklar</Product_Category>
  <Content><![CDATA[ ... HTML ... ]]></Content>
  <Image1>...</Image1>
  ...
  <Permalink>...</Permalink>
  <Product_Video_URL>...</Product_Video_URL>
</post>
Hangi alan en kritik?
En kritik alan SKU. Upsert’te “bu ürün kim?” sorusunun cevabı SKU’dur.
ID ise çoğu senaryoda referans bilgidir.
Marka alanları

Kendi sitenizde genelde Original_Brand mantıklı; pazaryerlerinde çoğu zaman Marketplace_Brand daha doğru sonuç verir.

▸ Örnek: Original_Brand: Baile   |   Marketplace_Brand: LyBaile

4) Önerilen Mimari (İndir → Stream → Upsert → Log)

Büyük XML’lerde sağlıklı yaklaşım genelde şu akış: İndir → Stream Parse → SKU bazlı Upsert → Logla. Bu sayede RAM şişmez, sync yarıda kalırsa “nerede kaldım?” takibi kolay olur.

  1. XML’i indir (token ile HTTP GET)
  2. Stream parse et (tüm dosyayı RAM’e alma)
  3. Her <post> bloğunda SKU’yu al
  4. DB’de SKU var mı bak: varsa UPDATE, yoksa INSERT
  5. İş bitince log yaz: kaç ürün işlendi / kaç hata çıktı / kaç ürün güncellendi
Performans notu: Görselleri anında indirmeniz şart değil.
İlk etapta sadece Image1..Image7 URL alanlarını kaydedip, görsel indirmeyi ayrı bir job ile yapmak çoğu sistemde daha stabil çalışır.

5) Stream Parse Nedir ve Neden Önemli?

XML okurken iki yaklaşım vardır: DOM (tüm XML’i belleğe al) ve Stream (parça parça oku).
Ürün sayısı büyüdükçe DOM yaklaşımı RAM’i şişirebilir. Stream parse ise her <post> bloğunu tek tek işlediği için daha stabil olur.

  • RAM kullanımı düşük kalır
  • İş yarıda kalsa bile “nerede kaldım” takibi daha kolaydır
  • Hatalı bir ürün bloğunu atlayıp devam etmek mümkündür
Pro ipucu: Stream parse kullansan bile bazı kütüphanelerde “node temizleme / clear” unutulursa bellek şişebilir.
(Örn: Python iterparse’de elem.clear())

6) Alan Eşleme: XML → Veritabanı (Kısa ve Net)

Entegrasyonda en çok zaman kaybettiren yer “hangi alan nereye yazılacak?” kısmı.
Aşağıdaki tablo, pratikte %80 işi çözen temel eşlemedir. (Şeman farklıysa sadece DB kolon adlarını uyarlaman yeter.)

XML Alanı Ne işe yarar? Öneri
SKU Tekil ürün anahtarı UNIQUE alan yap (upsert bunun üstünden)
Stock Stok adedi Her sync’te güncelle
Sale_Price_TL Satış fiyatı Kendi fiyat modelin yoksa güncelle
Wholesale_* B2B maliyet alanları Raporlama için sakla
Title / Content Başlık / HTML açıklama İstersen “overwrite_content” mantığıyla yönet
Image1..Image7 Görsel URL’leri İlk import + sonradan değişiklik kontrolü
GTIN / Marketplace_Brand Pazaryeri uyumu Hepsiburada/Trendyol/N11 için kritik alanlar
Content (CDATA) notu: XML içindeki Content zaten HTML formatındadır. Çoğu sistemde “ham HTML” olarak saklamak en pratik yöntemdir.

7) Upsert Mantığı: Ne Güncellenecek, Ne Dokunulmayacak?

Upsert demek “varsa güncelle, yoksa ekle” ama işin kritik kısmı şudur: Her sync’te her alanı overwrite etmek zorunda değilsin. Çünkü bazı kullanıcılar ürün başlığını, SEO metnini veya açıklamayı kendi sisteminde düzenlemek ister.

Alan Öneri Neden?
Stock Her sync’te güncelle Stok değişir; anlık doğru kalmalı
Sale_Price_TL Kendi fiyat modelin yoksa güncelle “Source of truth” XML olur
Wholesale_* Her sync’te güncelle B2B raporlama / marj hesapları için kritik
Title / Content Opsiyon: overwrite açık/kapalı Yerelde SEO düzenleyenler olabilir
Images / Video İlk import’ta al, sonra değiştiyse güncelle Her seferinde sil-yaz bazen gereksiz yük
Pratik ayar önerisi
Sisteme basit bir bayrak koy: overwrite_content = true/false.
Böylece aynı entegrasyon hem “tam otomatik” hem de “SEO metnini ben yazacağım” diyen müşteriler için çalışır.
xShopum stok notu
WooCommerce kullanan bazı projelerde stok, standart _stock yanında ayrıca _stock2 gibi özel alanlarda tutulabilir.
Bu tip durumda entegrasyonun “hangi alanı gerçek stok kabul edeceği” net olmalı.

8) WooCommerce & OpenCart İçin “En Pratik Yol”

WooCommerce ve OpenCart tarafında çoğu kullanıcı “sıfırdan kod” yerine import eklentisi/modülü ile ilerlemek istiyor. Bu çok mantıklı: hız kazanırsın, bakım yükü azalır, test etmek kolaylaşır. Önemli olan şu: hangi aracı seçersen seç, SKU eşlemesi ve overwrite stratejisi net olmalı.

WooCommerce (WordPress) tarafında
  • WP All Import + WooCommerce Add-On (XML içe aktarma/eşleme)
  • Product Import Export for WooCommerce (CSV/XML odaklı iş akışları olanlar kullanıyor)
  • Custom Import (çok özel kural/stock2 mantığı olan projelerde)
Mutlaka kontrol: Eşleme alanı SKU mı? İçerik overwrite açık mı kapalı mı? Stok hangi alandan güncellenecek?
OpenCart tarafında
  • XML Import / Import-Export modülleri (3. parti modüller)
  • CSV/Excel Import modülleri + XML → CSV dönüştürme iş akışı
  • Cron tabanlı importer (yüksek adet ürün + özel kurallar gereken projeler)
Mutlaka kontrol: SKU nerede tutuluyor? (çoğu projede model) Kategori eşlemesi otomatik mi? Görseller “URL’den çekme” destekliyor mu?
En kritik mesaj: Eklenti/modül ne olursa olsun, entegrasyonun kalbi SKU ve upsert stratejisidir. Doğru eşleme + doğru overwrite ayarı = sorunsuz operasyon.

9) Hazır Altyapılar (ikas / Ticimax / IdeaSoft) — Teknik Checklist

Bu platformların çoğunda XML entegrasyonu “alan eşleme” mantığıyla çalışır. Teknik ekibe aşağıdaki bilgileri net verirseniz süreç genelde sorunsuz ilerler:

Gönderilecek minimum bilgi
  • XML URL (token’lı)
  • SKU → ana eşleme alanı (unique)
  • Stock → stok alanı
  • Sale_Price_TL → satış fiyatı
  • Content → HTML / CDATA açıklama
  • Image1…Image7 → görseller
  • GTIN / Marketplace_Brand → pazaryeri uyumu için
Gerçekçi beklenti
Bazı altyapılar XML import özelliğini pakete bağlı ücretli sunabilir veya görsel/varyasyon import konularında sınırlı olabilir.
Sizin için önemli olan: SKU eşlemesi ve stok güncellemesi sorunsuz çalışsın.
İpucu: İçerik (Title/Content) tarafını “manuel düzenlenebilir” bırakmak istiyorsanız, teknik ekipten “overwrite kapatma” veya “sadece ilk import’ta yaz” gibi seçenekler isteyin.

10) Pazaryerlerine Listeleme (Hepsiburada / Trendyol / N11 / Çiçeksepeti)

Bu XML’i alıp sadece kendi sitenizde değil; pazaryerlerine ürün listelemek için de kullanabilirsiniz. Burada iki yol var:
(A) Entegrasyon servisi (StockMount tarzı) veya (B) kendi özel yazılımınız.

A) Entegrasyon servisi ile
Örn: StockMount ve benzeri pazaryeri entegrasyon firmaları; XML’i çekip
Hepsiburada/Trendyol/N11/Çiçeksepeti gibi kanallara ürünlerinizi taşır, stok/fiyat günceller.
Siz genelde sadece “alan eşleme” ve “kanal ayarları” yaparsınız.
Mutlaka kontrol: GTIN zorunluluğu, marka alanı (Marketplace_Brand), kategori eşlemesi ve kargo ağırlık/dimensions (varsa).
B) Kendi özel yazılımınız ile
XML → (kendi DB’niz) → pazaryeri API akışı kurarsınız.
Bu yaklaşım daha esnektir: fiyat kuralı, marka dönüşümü, görsel optimizasyonu, varyasyon mantığı gibi konuları tamamen siz yönetirsiniz.
Mutlaka kontrol: Her pazaryerinin kategori/özellik seti farklıdır; “tek şablon” ile her yerde kusursuz olmaz. Ara katmanda mapping gerekir.
Pazaryeri için en kritik 6 alan:
SKU • Stock • Sale_Price_TL • GTIN • Marketplace_Brand • Image1..Image7Bonus: Ürün içeriklerini pazaryeri kurallarına göre sadeleştirmek gerekebilir (yasak kelimeler / format / uzunluk).

Son kontrol: En çok yapılan 9 hata
1) XML’i DOM ile RAM’e almak • 2) SKU yerine ID ile eşlemek • 3) Timeout/retry koymamak •
4) Her sync’te görseli sil-yaz yapmak • 5) Content’i yanlışlıkla overwrite etmek •
6) Log tutmamak • 7) Token’ı repo/ekran görüntüsünde paylaşmak •
8) Pazaryerinde GTIN/marka eşlemesini ihmal etmek • 9) Kategori mapping yapmadan “otomatik” beklemek

Devam / Yardım
Bu sayfadaki yapı “en doğru entegrasyon mantığını” verir. Uygulama tarafında siz ister eklenti/modül kullanın, ister özel yazılım yazın,
kritik olan SKU eşlemesi, stok doğruluğu ve overwrite stratejisidir.
Kod örneklerini ve hazır kısayolları aşağıdaki alanda sunuyoruz:

Kullanılan Yazılım Dili
Örnek Kodlama
ASP.NET / C# Örnek
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;

public class XshopumPostFeedSync
{
    private readonly HttpClient _http;

    public XshopumPostFeedSync(HttpClient httpClient)
    {
        _http = httpClient;
        _http.Timeout = TimeSpan.FromSeconds(90);
    }

    public async Task SyncAsync(string xmlUrlWithToken, CancellationToken ct)
    {
        using var resp = await _http.GetAsync(xmlUrlWithToken, HttpCompletionOption.ResponseHeadersRead, ct);
        resp.EnsureSuccessStatusCode();

        await using var stream = await resp.Content.ReadAsStreamAsync(ct);

        var settings = new XmlReaderSettings
        {
            Async = true,
            IgnoreComments = true,
            IgnoreWhitespace = true,
            DtdProcessing = DtdProcessing.Prohibit
        };

        using var reader = XmlReader.Create(stream, settings);

        while (await reader.ReadAsync())
        {
            if (reader.NodeType == XmlNodeType.Element && reader.Name == "post")
            {
                var model = await ReadPostAsync(reader, ct);
                if (string.IsNullOrWhiteSpace(model.SKU))
                    continue;

                await UpsertProductAsync(model, ct);
                await UpsertImagesAsync(model, ct);
            }
        }
    }

    private static async Task<PostProduct> ReadPostAsync(XmlReader reader, CancellationToken ct)
    {
        var m = new PostProduct();
        var images = new List<string>();

        if (reader.IsEmptyElement) return m;

        while (await reader.ReadAsync())
        {
            ct.ThrowIfCancellationRequested();

            if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "post")
                break;

            if (reader.NodeType != XmlNodeType.Element)
                continue;

            var name = reader.Name;
            string value = await reader.ReadElementContentAsStringAsync();

            switch (name)
            {
                case "ID": m.SourceID = TryLong(value); break;
                case "SKU": m.SKU = value?.Trim() ?? ""; break;
                case "Title": m.Title = value?.Trim() ?? ""; break;
                case "Stock": m.Stock = TryInt(value); break;
                case "Wholesale_Excl_Tax_TL": m.WholesaleExclTaxTL = TryDec(value); break;
                case "Wholesale_Incl_Tax_TL": m.WholesaleInclTaxTL = TryDec(value); break;
                case "Wholesale_Incl_Tax_USD": m.WholesaleInclTaxUSD = TryDec(value); break;
                case "Sale_Price_TL": m.SalePriceTL = TryDec(value); break;
                case "Tax_Rate": m.TaxRate = TryInt(value); break;
                case "Product_Category": m.CategoryName = value?.Trim() ?? ""; break;
                case "Original_Brand": m.OriginalBrand = value?.Trim() ?? ""; break;
                case "Marketplace_Brand": m.MarketplaceBrand = value?.Trim() ?? ""; break;
                case "GlobalTradeItemNumber": m.GTIN = value?.Trim() ?? ""; break;
                case "Permalink": m.Permalink = value?.Trim() ?? ""; break;
                case "Product_Video_URL": m.VideoUrl = value?.Trim() ?? ""; break;
                case "Content": m.ContentHtml = value ?? ""; break;
                default:
                    if (name.StartsWith("Image", StringComparison.OrdinalIgnoreCase))
                    {
                        var url = value?.Trim();
                        if (!string.IsNullOrWhiteSpace(url))
                            images.Add(url);
                    }
                    break;
            }
        }

        m.ImageUrls = images;
        return m;
    }

    private static long? TryLong(string s) => long.TryParse(s, out var v) ? v : null;
    private static int? TryInt(string s) => int.TryParse(s, out var v) ? v : null;

    private static decimal? TryDec(string s)
    {
        return decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) ? v : null;
    }

    private Task UpsertProductAsync(PostProduct m, CancellationToken ct) => Task.CompletedTask;
    private Task UpsertImagesAsync(PostProduct m, CancellationToken ct) => Task.CompletedTask;
}

public class PostProduct
{
    public long? SourceID { get; set; }
    public string SKU { get; set; } = "";
    public string Title { get; set; } = "";
    public string ContentHtml { get; set; } = "";

    public int? Stock { get; set; }

    public decimal? WholesaleExclTaxTL { get; set; }
    public decimal? WholesaleInclTaxTL { get; set; }
    public decimal? WholesaleInclTaxUSD { get; set; }
    public decimal? SalePriceTL { get; set; }

    public int? TaxRate { get; set; }

    public string CategoryName { get; set; } = "";
    public string OriginalBrand { get; set; } = "";
    public string MarketplaceBrand { get; set; } = "";

    public string GTIN { get; set; } = "";
    public string Permalink { get; set; } = "";
    public string VideoUrl { get; set; } = "";

    public List<string> ImageUrls { get; set; } = new List<string>();
}
PHP Örnek
<?php
declare(strict_types=1);

$xmlUrl = "https://www.xshopum.com/xml-feed?token=YOUR_TOKEN";

$tmpFile = sys_get_temp_dir() . "/xshopum_feed.xml";
download_xml_to_file($xmlUrl, $tmpFile);

$reader = new XMLReader();
if (!$reader->open($tmpFile, null, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING)) {
    throw new Exception("XMLReader open failed");
}

while ($reader->read()) {

    if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'post') {
        $post = read_post_node($reader);

        if (empty($post['SKU'])) {
            continue;
        }

        upsert_product($post);
        upsert_images($post['SKU'], $post['Images']);
    }
}

$reader->close();
@unlink($tmpFile);


// -------------------- HELPERS --------------------

function download_xml_to_file(string $url, string $filePath): void {
    $fp = fopen($filePath, "w");
    if (!$fp) {
        throw new Exception("Cannot open tmp file");
    }

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_FILE => $fp,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT => 90,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_FAILONERROR => true,
    ]);

    $ok = curl_exec($ch);
    if (!$ok) {
        $err = curl_error($ch);
        curl_close($ch);
        fclose($fp);
        throw new Exception("XML download failed: " . $err);
    }

    curl_close($ch);
    fclose($fp);
}

function read_post_node(XMLReader $reader): array {
    $data = [
        "ID" => null,
        "SKU" => "",
        "Title" => "",
        "Content" => "",
        "Stock" => null,
        "Wholesale_Excl_Tax_TL" => null,
        "Wholesale_Incl_Tax_TL" => null,
        "Wholesale_Incl_Tax_USD" => null,
        "Sale_Price_TL" => null,
        "Tax_Rate" => null,
        "Product_Category" => "",
        "Original_Brand" => "",
        "Marketplace_Brand" => "",
        "GlobalTradeItemNumber" => "",
        "Permalink" => "",
        "Product_Video_URL" => "",
        "Images" => [],
    ];

    if ($reader->isEmptyElement) {
        return $data;
    }

    $depth = $reader->depth;

    while ($reader->read()) {
        if (
            $reader->nodeType === XMLReader::END_ELEMENT &&
            $reader->name === 'post' &&
            $reader->depth === $depth
        ) {
            break;
        }

        if ($reader->nodeType !== XMLReader::ELEMENT) continue;

        $name = $reader->name;
        $value = $reader->readString();

        if (stripos($name, 'Image') === 0) {
            $url = trim($value);
            if ($url !== "") {
                $data["Images"][] = $url;
            }
            continue;
        }

        switch ($name) {
            case "ID":
                $data["ID"] = (int)trim($value);
                break;
            case "SKU":
                $data["SKU"] = trim($value);
                break;
            case "Title":
                $data["Title"] = trim($value);
                break;
            case "Content":
                $data["Content"] = $value;
                break;
            case "Stock":
                $data["Stock"] = (int)trim($value);
                break;
            case "Wholesale_Excl_Tax_TL":
            case "Wholesale_Incl_Tax_TL":
            case "Wholesale_Incl_Tax_USD":
            case "Sale_Price_TL":
                $data[$name] = (float)trim($value);
                break;
            case "Tax_Rate":
                $data["Tax_Rate"] = (int)trim($value);
                break;
            case "Product_Category":
            case "Original_Brand":
            case "Marketplace_Brand":
            case "GlobalTradeItemNumber":
            case "Permalink":
            case "Product_Video_URL":
                $data[$name] = trim($value);
                break;
        }
    }

    return $data;
}


// -------------------- DB (PSEUDO) --------------------

function upsert_product(array $p): void {
    // PSEUDO:
    // 1) SELECT id FROM products WHERE sku = ?
    // 2) varsa UPDATE
    // 3) yoksa INSERT
    // 4) last_sync_at = NOW()
}

function upsert_images(string $sku, array $images): void {
    // PSEUDO:
    // - Önce sku için görselleri sil
    // - Sonra image_url + sort_order ile yeniden ekle
}
Node.js Örnek
/**
 * Node.js stream + SAX parse örneği (xShopum <post> feed'i için)
 * Paketler:
 *   npm i sax node-fetch
 *
 * DB katmanı burada pseudo. (mysql2 / pg / prisma / sequelize fark etmez)
 */

import fetch from "node-fetch";
import sax from "sax";

const XML_URL = "https://www.xshopum.com/xml-feed?token=YOUR_TOKEN";

async function run() {
  const res = await fetch(XML_URL, { timeout: 90_000 });
  if (!res.ok) throw new Error("XML download failed: " + res.status);

  const parser = sax.createStream(true, { trim: true });

  let inPost = false;
  let bufferText = "";

  /** @type {any} */
  let post = null;

  parser.on("opentag", (node) => {
    bufferText = "";

    if (node.name === "post") {
      inPost = true;
      post = {
        ID: null,
        SKU: "",
        Title: "",
        Content: "",
        Stock: null,
        Wholesale_Excl_Tax_TL: null,
        Wholesale_Incl_Tax_TL: null,
        Wholesale_Incl_Tax_USD: null,
        Sale_Price_TL: null,
        Tax_Rate: null,
        Product_Category: "",
        Original_Brand: "",
        Marketplace_Brand: "",
        GlobalTradeItemNumber: "",
        Permalink: "",
        Product_Video_URL: "",
        Images: []
      };
    }
  });

  parser.on("text", (txt) => {
    if (!inPost) return;
    bufferText += txt;
  });

  parser.on("cdata", (txt) => {
    if (!inPost) return;
    bufferText += txt;
  });

  parser.on("closetag", async (tagName) => {
    if (!inPost) return;

    const value = (bufferText || "").trim();

    if (post && tagName !== "post") {
      if (tagName.startsWith("Image")) {
        if (value) post.Images.push(value);
      } else {
        switch (tagName) {
          case "ID": post.ID = toInt(value); break;
          case "SKU": post.SKU = value; break;
          case "Title": post.Title = value; break;
          case "Content": post.Content = value; break;

          case "Stock": post.Stock = toInt(value); break;
          case "Wholesale_Excl_Tax_TL": post.Wholesale_Excl_Tax_TL = toFloat(value); break;
          case "Wholesale_Incl_Tax_TL": post.Wholesale_Incl_Tax_TL = toFloat(value); break;
          case "Wholesale_Incl_Tax_USD": post.Wholesale_Incl_Tax_USD = toFloat(value); break;
          case "Sale_Price_TL": post.Sale_Price_TL = toFloat(value); break;

          case "Tax_Rate": post.Tax_Rate = toInt(value); break;

          case "Product_Category": post.Product_Category = value; break;
          case "Original_Brand": post.Original_Brand = value; break;
          case "Marketplace_Brand": post.Marketplace_Brand = value; break;
          case "GlobalTradeItemNumber": post.GlobalTradeItemNumber = value; break;
          case "Permalink": post.Permalink = value; break;
          case "Product_Video_URL": post.Product_Video_URL = value; break;
        }
      }
    }

    if (tagName === "post") {
      inPost = false;

      if (!post || !post.SKU) {
        post = null;
        return;
      }

      try {
        await upsertProduct(post);
        await upsertImages(post.SKU, post.Images);
      } catch (e) {
        console.error("Upsert error SKU:", post.SKU, e);
      } finally {
        post = null;
      }
    }

    bufferText = "";
  });

  parser.on("error", (err) => {
    console.error("SAX parse error:", err);
    parser._parser.error = null;
    parser._parser.resume();
  });

  await new Promise((resolve, reject) => {
    res.body.on("error", reject);
    parser.on("end", resolve);
    res.body.pipe(parser);
  });

  console.log("Sync completed.");
}

function toInt(s) {
  const n = parseInt(s, 10);
  return Number.isFinite(n) ? n : null;
}

function toFloat(s) {
  const n = parseFloat(s);
  return Number.isFinite(n) ? n : null;
}

// -------------------- DB (PSEUDO) --------------------
async function upsertProduct(p) {
  // PSEUDO:
  // 1) SELECT id FROM products WHERE sku=?
  // 2) varsa UPDATE; yoksa INSERT
  // 3) last_sync_at = NOW()
}

async function upsertImages(sku, images) {
  // En basit yöntem:
  // - DELETE FROM product_images WHERE sku = ?
  // - sonra images listesini sort_order ile INSERT et
}

run().catch(console.error);
Python Örnek
"""
Python iterparse örneği (xShopum <post> feed'i için)

pip install requests

DB katmanı pseudo. İstersen:
- MySQL: mysql-connector-python / pymysql
- Postgres: psycopg2
- ORM: SQLAlchemy
"""

import os
import requests
import xml.etree.ElementTree as ET

XML_URL = "https://www.xshopum.com/xml-feed?token=YOUR_TOKEN"
TMP_FILE = "/tmp/xshopum_feed.xml"

def download_xml(url: str, out_path: str) -> None:
    with requests.get(url, stream=True, timeout=90) as r:
        r.raise_for_status()
        with open(out_path, "wb") as f:
            for chunk in r.iter_content(chunk_size=1024 * 64):
                if chunk:
                    f.write(chunk)

def safe_text(el):
    return (el.text or "").strip()

def parse_post(post_el):
    # post_el: <post> element
    data = {
        "ID": None,
        "SKU": "",
        "Title": "",
        "Content": "",
        "Stock": None,
        "Wholesale_Excl_Tax_TL": None,
        "Wholesale_Incl_Tax_TL": None,
        "Wholesale_Incl_Tax_USD": None,
        "Sale_Price_TL": None,
        "Tax_Rate": None,
        "Product_Category": "",
        "Original_Brand": "",
        "Marketplace_Brand": "",
        "GlobalTradeItemNumber": "",
        "Permalink": "",
        "Product_Video_URL": "",
        "Images": []
    }

    for child in list(post_el):
        tag = child.tag
        val = safe_text(child)

        if tag.startswith("Image"):
            if val:
                data["Images"].append(val)
            continue

        if tag == "ID":
            data["ID"] = to_int(val)
        elif tag == "SKU":
            data["SKU"] = val
        elif tag == "Title":
            data["Title"] = val
        elif tag == "Content":
            # CDATA içindeki HTML genelde .text içine gelir
            data["Content"] = child.text or ""
        elif tag == "Stock":
            data["Stock"] = to_int(val)
        elif tag in ("Wholesale_Excl_Tax_TL", "Wholesale_Incl_Tax_TL", "Wholesale_Incl_Tax_USD", "Sale_Price_TL"):
            data[tag] = to_float(val)
        elif tag == "Tax_Rate":
            data["Tax_Rate"] = to_int(val)
        elif tag in ("Product_Category", "Original_Brand", "Marketplace_Brand", "GlobalTradeItemNumber", "Permalink", "Product_Video_URL"):
            data[tag] = val

    return data

def to_int(s: str):
    try:
        return int(s)
    except:
        return None

def to_float(s: str):
    try:
        # "305.00" gibi değerler için float OK
        return float(s)
    except:
        return None

# -------------------- DB (PSEUDO) --------------------
def upsert_product(p: dict) -> None:
    # 1) SELECT id FROM products WHERE sku=%s
    # 2) varsa UPDATE (stock, prices, title, content, vb.)
    # 3) yoksa INSERT
    # 4) last_sync_at = NOW()
    pass

def upsert_images(sku: str, images: list[str]) -> None:
    # Basit:
    # - DELETE FROM product_images WHERE sku=%s
    # - sonra (sku, url, sort_order) INSERT
    pass

def sync():
    download_xml(XML_URL, TMP_FILE)

    # iterparse: end eventinde <post> tamamlanınca işliyoruz
    context = ET.iterparse(TMP_FILE, events=("end",))

    for event, elem in context:
        if elem.tag == "post":
            p = parse_post(elem)
            if p.get("SKU"):
                try:
                    upsert_product(p)
                    upsert_images(p["SKU"], p["Images"])
                except Exception as e:
                    print("Upsert error SKU:", p.get("SKU"), e)

            # Çok önemli: belleği temizle
            elem.clear()

    try:
        os.remove(TMP_FILE)
    except:
        pass

if __name__ == "__main__":
    sync()
Java Örnek
import javax.xml.stream.*;
import java.io.InputStream;
import java.net.URL;
import java.util.*;

public class XshopumXmlSync {

    private static final String XML_URL =
        "https://www.xshopum.com/xml-feed?token=YOUR_TOKEN";

    public static void main(String[] args) throws Exception {
        new XshopumXmlSync().sync();
    }

    public void sync() throws Exception {
        InputStream stream = new URL(XML_URL).openStream();

        XMLInputFactory factory = XMLInputFactory.newInstance();
        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);

        XMLStreamReader reader = factory.createXMLStreamReader(stream);

        Map<String, Object> post = null;
        String currentTag = null;
        StringBuilder buffer = new StringBuilder();
        List<String> images = null;

        while (reader.hasNext()) {
            int event = reader.next();

            switch (event) {
                case XMLStreamConstants.START_ELEMENT:
                    currentTag = reader.getLocalName();
                    buffer.setLength(0);

                    if ("post".equals(currentTag)) {
                        post = new HashMap<>();
                        images = new ArrayList<>();
                    }
                    break;

                case XMLStreamConstants.CHARACTERS:
                case XMLStreamConstants.CDATA:
                    if (post != null) buffer.append(reader.getText());
                    break;

                case XMLStreamConstants.END_ELEMENT:
                    String tag = reader.getLocalName();
                    String val = buffer.toString().trim();

                    if (post != null && !"post".equals(tag)) {
                        if (tag.startsWith("Image")) {
                            if (!val.isEmpty()) images.add(val);
                        } else {
                            post.put(tag, val);
                        }
                    }

                    if ("post".equals(tag) && post != null) {
                        post.put("Images", images);

                        String sku = (String) post.get("SKU");
                        if (sku != null && !sku.isEmpty()) {
                            upsertProduct(post);
                            upsertImages(sku, images);
                        }

                        post = null;
                        images = null;
                    }

                    buffer.setLength(0);
                    currentTag = null;
                    break;
            }
        }

        reader.close();
        stream.close();
    }

    private void upsertProduct(Map<String, Object> p) {
        // PSEUDO:
        // SELECT id FROM products WHERE sku=?
        // varsa UPDATE (Stock, Sale_Price_TL, Wholesale_Incl_Tax_TL, vb.)
        // yoksa INSERT
        // last_sync_at = NOW()
    }

    private void upsertImages(String sku, List<String> images) {
        // DELETE FROM product_images WHERE sku=?
        // sonra (sku, image_url, sort_order) INSERT
    }
}