← cd ../blog

Migrarea a 380+ articole legacy: Lecții din tranșee

Migrarea a 380+ articole legacy: Lecții din tranșee — imagine de copertă

Migrarea conținutului dintr-un sistem legacy cu 19 ani de istorie (2008–2026) este un exercițiu de arheologie digitală. Când am preluat proiectul pentru Liceul pentru Deficienți de Vedere, m-am lovit de un CMS custom, fără API, unde fiecare pagină era o aventură de markup non-semantic. Iată cum am reușit să portăm peste 380 de articole într-un mod robust și idempotent.

Arhitectura Scraper-ului: CLI PHP și Caching

Am construit un scraper în PHP CLI care folosește cURL pentru request-uri și DOMDocument cu DOMXPath pentru parsare. Pentru a evita ban-urile și pentru a fi idempotenți, am implementat un strat de caching bazat pe hash-ul MD5 al URL-ului.

foreach (range(2008, 2026) as $year) {
    $url = "noutati.php?do=year&year=$year";
    $cacheFile = "cache/" . md5($url) . ".html";
    
    if (!file_exists($cacheFile)) {
        $html = curl_get($url);
        file_put_contents($cacheFile, $html);
    }
    $dom = new DOMDocument();
    @$dom->loadHTML(file_get_contents($cacheFile));
    $xpath = new DOMXPath($dom);
    // ... procesare link-uri
}

Cosmarul CP1252: Repararea diacriticelor la nivel de byte

Cea mai mare provocare a fost dubla encodare CP1252. Diacriticele apăreau corupte deoarece serverul legacy trimitea datele ca UTF-8, dar baza de date era stocată într-un format mixt. Am creat un map de fixup la nivel de byte.

function fix_encoding($str) {
    $map = [
        "\xC3\x88\xE2\x84\xA2" => "\xC8\x99", // ș
        "\xC3\x83\xC2\x83" => "\xC4\x83",    // ă
        // ... 20+ mapări
    ];
    return strtr($str, $map);
}

Extragerea conținutului: Strategia “Sibling Walk”

Deoarece totul era într-un div generic, am folosit o funcție de parsare care scanează frații nodului de dată până întâlnește o barieră (tag-uri ca aside, footer sau clase de sidebar).

function extract_content_after_date($node) {
    $content = '';
    $sibling = $node->nextSibling;
    while ($sibling) {
        if (in_array($sibling->nodeName, ['aside', 'nav', 'footer'])) break;
        if (strpos($sibling->getAttribute('class'), 'sidebar') !== false) break;
        $content .= $sibling->ownerDocument->saveHTML($sibling);
        $sibling = $sibling->nextSibling;
    }
    return $content;
}

Inferența categoriilor și XML pentru WP All Import

Am dedus 7 categorii (ex: “Concursuri”, “Proiecte europene”) printr-o funcție de matching pe cuvinte cheie. XML-ul generat pentru WP All Import folosește CDATA pentru a proteja conținutul și câmpuri meta pentru a asigura deduplicarea prin ldv_id.

$xml = new SimpleXMLElement('<root/>');
$item = $xml->addChild('article');
$item->addChild('title', htmlspecialchars($title));
$item->addChild('content', '<![CDATA[' . $content . ']]>');
$item->addChild('ldv_id', $original_id); // Meta pentru deduplicare

Importul de documente și idempotența

Ultima piesă a puzzle-ului a fost importul fișierelor PDF. Am creat un script care scanează folderul uploads/documente-import/, mapând numele folderului la taxonomii WordPress. Verificarea _doc_file_url în baza de date meta previne crearea de fișiere duplicate la fiecare rulare a scriptului.

Această abordare ne-a permis să migrăm 19 ani de istorie fără a pierde integritatea datelor și, cel mai important, fără a crea duplicate, oferind o bază curată pentru viitorul digital al liceului.