2013. május 17., péntek

Munka Heritrix-el és WARC-okkal

1. Bevezetés, Heritrix

Munkám során megismerkedtünk a Heritrix nevű crawlerrel, és el is kezdtük használni. Az első dolog, ami nem tetszett benne, hogy WARC fájlokban köpi vissza a lementett adatokat (erre még visszatérek a cikk végén). Ez persze valamilyen szempontból tök jó, tárolja a HTTP header-öket, meg még 1-2 crawling infót, meg azonosítókat, mittudomén, de zavaró, hogy nem látom egyből, hogy mik jöttek le a letöltéskor. Külön programot kell írni, hogy egyáltalán a fájllistát lássam.

(Persze más egyéb dolgok sem szimpatikusak ebben a crawler-ben, például ez a webes UI megoldás, meg az XML-es config, de főleg a fantasztikus dokumentáció, amiben a keresett feature-ök, beállítások többségét nem találtam meg, mindig órákig kellett guglizni, vagy próbálkozni.)



2. WARC feldolgozás

2.1. Alapdolgok

No, de visszatérve a WARC feldolgozásra: szerencsére a Heritrix lib mappája tele van csemegékkel, pár tucat JAR fájllal, amiből ügyesen kiválogatva a megfelelő 6-7 darabot összerakható egy egyszerű WARC olvasó.

Jah, még mielőtt elfelejtem, Heritrix 3.x-el dolgozunk.

A következő JAR fájlokra van szükség:
  • archive-overlay-commons-httpclient-3.1.jar
  • commons-io-1.4.jar
  • commons-lang-2.6.jar
  • commons-logging-1.0.4.jar
  • fastutil-5.0.7.jar
  • guava-r08.jar
  • heritrix-commons-3.1.1.jar

Az archívum bejegyzésein ezekkel már egyszerűen végig lehet lépkedni:
WARCReader reader = WARCReaderFactory.get(new File("valamilyen.warc.gz"));
for (ArchiveRecord rec : reader) {
    // ...
}
Amint látszik, az a jó, hogy a GZipped WARC-ot is simán lekezeli, ezzel nem is kell törődnünk.


2.2. Letöltött fájlok kimentése

Namost, a WARC-ban bejegyzések vannak, melyekből többféle van, pl. van request és van response. Ezek a HTTP üzeneteket tárolják. Nekünk most csak a response típusú bejegyzések kellenek, ebben vannak a fájlok, amik nekünk kellenek. Erre a következőképp tudunk szűkíteni:
WARCReader reader = WARCReaderFactory.get(new File("valamilyen.warc.gz"));
for (ArchiveRecord rec : reader) {
    if (rec.getHeader().getMimetype().matches(".*msgtype=response")) {
        // ...
    }
}

A fájl kimentésére én egy lehetőséget találtam a metódusok között: dump. A következőképp működik:
WARCReader reader = WARCReaderFactory.get(new File("valamilyen.warc.gz"));
for (ArchiveRecord rec : reader) {
    if (rec.getHeader().getMimetype().matches(".*msgtype=response")) {
        String fileName = generateFileNameFromURI(rec.getHeader().getUrl()); // ezt a függvényt rád bízom :-)
        File file = new File(fileName);
        FileOutputStream fos = new FileOutputStream(file);
        rec.dump(fos);
        fos.close();
    }
}
A fájlnév generálását az URL-ből az olvasóra bízom. Én is megoldottam majdnem tökéletesen, de sosem érne véget a cikk, ha mindent bemásolnék. :-) Meg nem is nagy kunszt, a fájlnévből tiltott karaktereket le kell cserélni, per jeleket megfelelő irányba dönteni, szétvágni útvonalra és fájlra, és létrehozni az útvonal mappáit.

A dump eredménye az, hogy a bejegyzést a megadott fájlba menti ki. HTTP header-rel együtt. Ami nem kell, főleg, ha szeretnénk nézegetni is a letöltött képeket, doksikat, lapokat.


2.3. Megszabadulás a HTTP headertől

De hogyan is lehetne levágni? Egyáltalán hogy néznek ki: változó számú és tartalmú sorok, de utánuk van egy üres sor, és utána jön a tartalom. Tehát egy "\n\n" részt kell keresni. Belekukkantva a fájl bájtjaiba azt látjuk, hogy ez egy 0D 0A 0D 0A szekvenciát jelent.

Java-ban bájtonként kezelni egy fájlt a FileInputStream és FileOutputStream osztályokkal lehetséges. Első megoldásom az volt, hogy átmásolom a fájlt bájtonként úgy, hogy a headert kihagyja, majd törlöm az eredetit. Ez persze lassú megoldás.

A gyors és talán elegáns megoldás erre az, hogy írunk saját FileOutputStream-et, ami felüldefiniálja a megfelelő write metódust. A dump metódus a write(byte[],int,int) változatot használja, ez némi próbálgatással kiderül.

Meg kell keresnünk a fenti szekvenciát, és az előtte levő bájtokat ignorálni, nem kiírni. Ezt én a következőképp oldottam meg:
package hu.juzraai.warcreader;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;

public class MyFOS extends FileOutputStream { // skips the first bytes until [13,10,13,10] sequence

    private ArrayList<Byte> lastFourBytes = new ArrayList<Byte>();
    private boolean afterHeader = false;

    public MyFOS(File file) throws FileNotFoundException {
        super(file);
    }

    // WARCReader.dump uses this:
    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        if (!afterHeader) { // előző write híváskor még nem volt meg a header vége
            for (int i = 0; i < b.length && i < len; i++) {
                byte c = b[i];
                if (!afterHeader) {
                    lastFourBytes.add(c);
                    if (lastFourBytes.size() > 4) {
                        lastFourBytes.remove(0); // csak az utolsó 4-et tároljuk
                    }
                    if (lastFourBytes.toString().equals("[13, 10, 13, 10]")) {
                        afterHeader = true;
                    }
                } else { // ebben a write hívásban léptük túl a header-t
                    write(c);
                }
            }
        } else { // előző write híváskor túlléptük a header-t
            super.write(b, off, len);
        }
    }
}

3. Slusszpoén

Ígértem, hogy visszatérek a WARC outputra a végén, egy szösszenet erejéig. A slusszpoén ugyanis az, hogy be lehet ám konfigurálni a Heritrix-et úgy, hogy fájlszerkezet tükrözést csináljon, mint a WGET (aki egyébként nagy kedvencemmé vált, és eddig csak egy hiányossága van). A dokumentációban a "Mirroring HTML files only" topikban van elrejtve egy rövidke bekezdésben. Ez az a topik, amit már a címe alapján meg sem nyitottam, mert nekünk nem csak HTML-ek kellenek. A releváns mondat: "Configure the warcWriter bean so that its class is org.archive.modules.writer.MirrorWriterProcessor." Vagyis a következőt kell csinálni:

  1. a job config fájljában (crawler-beans.cxml) rákeresel a "warcWriter"-re
  2. és ott a class paramétert átírod a fentire (tehát WARCWriterProcessor helyett MirrorWriterProcessor)
Az eredmény pedig az, hogy a job mappájában lesz egy "mirror" mappa és ott a fájlok szépen hostonként, és azokon belül az URL-ek által mutatott könyvtárszerkezetben.

Hátránya, hogy dátumozott mappákba manuálisan kell áthelyezni a fájlokat.

Persze lehetnék pipa, hogy ezt pontosan az után találtam meg, hogy megírtam a WARC olvasóm, de végül is ezzel is tanultam. :-)

Nincsenek megjegyzések: