web-dev-qa-db-fra.com

Comment analyser de gros fichiers XML (50 Go) dans Java

Actuellement, j'essaie d'utiliser un analyseur SAX, mais environ 3/4 à travers le fichier, il se bloque complètement, j'ai essayé d'allouer plus de mémoire, etc., mais sans obtenir aucune amélioration.

Y a-t-il un moyen d'accélérer cela? Une meilleure méthode?

Je l'ai mis à nu, donc j'ai maintenant le code suivant et lors de l'exécution en ligne de commande, il ne va toujours pas aussi vite que je le voudrais.

En l'exécutant avec "Java -Xms-4096m -Xmx8192m -jar reader.jar" j'obtiens une limite de surcharge GC dépassée autour de l'article 700000

Principale:

public class Read {
    public static void main(String[] args) {       
       pages = XMLManager.getPages();
    }
}

XMLManager

public class XMLManager {
    public static ArrayList<Page> getPages() {

    ArrayList<Page> pages = null; 
    SAXParserFactory factory = SAXParserFactory.newInstance();

    try {

        SAXParser parser = factory.newSAXParser();
        File file = new File("..\\enwiki-20140811-pages-articles.xml");
        PageHandler pageHandler = new PageHandler();

        parser.parse(file, pageHandler);
        pages = pageHandler.getPages();

    } catch (ParserConfigurationException e) {
        e.printStackTrace();
    } catch (SAXException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }


    return pages;
    }    
}

PageHandler

public class PageHandler extends DefaultHandler{

    private ArrayList<Page> pages = new ArrayList<>();
    private Page page;
    private StringBuilder stringBuilder;
    private boolean idSet = false;

    public PageHandler(){
        super();
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {

        stringBuilder = new StringBuilder();

         if (qName.equals("page")){

            page = new Page();
            idSet = false;

        } else if (qName.equals("redirect")){
             if (page != null){
                 page.setRedirecting(true);
             }
        }
    }

     @Override
     public void endElement(String uri, String localName, String qName) throws SAXException {

         if (page != null && !page.isRedirecting()){

             if (qName.equals("title")){

                 page.setTitle(stringBuilder.toString());

             } else if (qName.equals("id")){

                 if (!idSet){

                     page.setId(Integer.parseInt(stringBuilder.toString()));
                     idSet = true;

                 }

             } else if (qName.equals("text")){

                 String articleText = stringBuilder.toString();

                 articleText = articleText.replaceAll("(?s)<ref(.+?)</ref>", " "); //remove references
                 articleText = articleText.replaceAll("(?s)\\{\\{(.+?)\\}\\}", " "); //remove links underneath headings
                 articleText = articleText.replaceAll("(?s)==See also==.+", " "); //remove everything after see also
                 articleText = articleText.replaceAll("\\|", " "); //Separate multiple links
                 articleText = articleText.replaceAll("\\n", " "); //remove new lines
                 articleText = articleText.replaceAll("[^a-zA-Z0-9- \\s]", " "); //remove all non alphanumeric except dashes and spaces
                 articleText = articleText.trim().replaceAll(" +", " "); //convert all multiple spaces to 1 space

                 Pattern pattern = Pattern.compile("([\\S]+\\s*){1,75}"); //get first 75 words of text
                 Matcher matcher = pattern.matcher(articleText);
                 matcher.find();

                 try {
                     page.setSummaryText(matcher.group());
                 } catch (IllegalStateException se){
                     page.setSummaryText("None");
                 }
                 page.setText(articleText);

             } else if (qName.equals("page")){

                 pages.add(page);
                 page = null;

            }
        } else {
            page = null;
        }
     }

     @Override
     public void characters(char[] ch, int start, int length) throws SAXException {
         stringBuilder.append(ch,start, length); 
     }

     public ArrayList<Page> getPages() {
         return pages;
     }
}
23
Joe Maher

Votre code d'analyse fonctionne probablement bien, mais le volume de données que vous chargez est probablement trop important pour être conservé en mémoire dans ce ArrayList.

Vous avez besoin d'une sorte de pipeline pour transmettre les données à leur destination réelle sans jamais les stocker toutes en mémoire à la fois.

Ce que j'ai parfois fait pour ce genre de situation est semblable au suivant.

Créez une interface pour traiter un seul élément:

public interface PageProcessor {
    void process(Page page);
}

Fournissez une implémentation de ceci au PageHandler via un constructeur:

public class Read  {
    public static void main(String[] args) {

        XMLManager.load(new PageProcessor() {
            @Override
            public void process(Page page) {
                // Obviously you want to do something other than just printing, 
                // but I don't know what that is...
                System.out.println(page);
           }
        }) ;
    }

}


public class XMLManager {

    public static void load(PageProcessor processor) {
        SAXParserFactory factory = SAXParserFactory.newInstance();

        try {

            SAXParser parser = factory.newSAXParser();
            File file = new File("pages-articles.xml");
            PageHandler pageHandler = new PageHandler(processor);

            parser.parse(file, pageHandler);

        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

Envoyez des données à ce processeur au lieu de les mettre dans la liste:

public class PageHandler extends DefaultHandler {

    private final PageProcessor processor;
    private Page page;
    private StringBuilder stringBuilder;
    private boolean idSet = false;

    public PageHandler(PageProcessor processor) {
        this.processor = processor;
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
         //Unchanged from your implementation
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
         //Unchanged from your implementation
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
            //  Elide code not needing change

            } else if (qName.equals("page")){

                processor.process(page);
                page = null;

            }
        } else {
            page = null;
        }
    }

}

Bien sûr, vous pouvez faire en sorte que votre interface gère des morceaux de plusieurs enregistrements plutôt qu'un seul et avoir les PageHandler collecter les pages localement dans une liste plus petite et envoyer périodiquement la liste pour traitement et effacer la liste.

Ou (peut-être mieux), vous pouvez implémenter l'interface PageProcessor telle que définie ici et y créer une logique qui met en mémoire tampon les données et les envoie pour une manipulation ultérieure par segments.

30
Don Roby

L'approche de Don Roby rappelle quelque peu l'approche que j'ai suivie pour créer un générateur de code conçu pour résoudre ce problème particulier (une première version a été conçue en 2008). Fondamentalement, chaque complexType a son Java POJO équivalent et les gestionnaires pour le type particulier sont activés lorsque le contexte change pour cet élément. J'ai utilisé cette approche pour le SEPA, les transactions bancaires et par exemple les discogs (30 Go). Vous pouvez spécifier les éléments que vous souhaitez traiter lors de l'exécution, de manière déclarative à l'aide d'un fichier de propriétés.

XML2J utilise le mappage de complexTypes vers Java POJO d'une part, mais vous permet de spécifier les événements que vous souhaitez écouter. Par ex.

account/@process = true
account/accounts/@process = true
account/accounts/@detach = true

L'essence est dans la troisième ligne. Le détachement garantit que les comptes individuels ne sont pas ajoutés à la liste des comptes. Il ne débordera donc pas.

class AccountType {
    private List<AccountType> accounts = new ArrayList<>();

    public void addAccount(AccountType tAccount) {
        accounts.add(tAccount);
    }
    // etc.
};

Dans votre code, vous devez implémenter la méthode de processus (par défaut, le générateur de code génère une méthode vide:

class AccountsProcessor implements MessageProcessor {
    static private Logger logger = LoggerFactory.getLogger(AccountsProcessor.class);

    // assuming Spring data persistency here
    final String path = new ClassPathResource("spring-config.xml").getPath();
    ClassPathXmlApplicationContext context = new   ClassPathXmlApplicationContext(path);
    AccountsTypeRepo repo = context.getBean(AccountsTypeRepo.class);


    @Override
    public void process(XMLEvent evt, ComplexDataType data)
        throws ProcessorException {

        if (evt == XMLEvent.END) {
            if( data instanceof AccountType) {
                process((AccountType)data);
            }
        }
    }

    private void process(AccountType data) {
        if (logger.isInfoEnabled()) {
            // do some logging
        }
        repo.save(data);
    }
}   

Notez que XMLEvent.END marque la balise de fermeture d'un élément. Ainsi, lorsque vous le traitez, il est terminé. Si vous devez l'associer (à l'aide d'un FK) à son objet parent dans la base de données, vous pouvez traiter le XMLEvent.BEGIN pour le parent, créez un espace réservé dans la base de données et utilisez sa clé pour stocker avec chacun de ses enfants. Dans la finale XMLEvent.END vous mettriez alors à jour le parent.

Notez que le générateur de code génère tout ce dont vous avez besoin. Il vous suffit d'implémenter cette méthode et bien sûr le code de colle DB.

Il existe des exemples pour vous aider à démarrer. Le générateur de code génère même vos fichiers POM, vous pouvez donc immédiatement après la génération créer votre projet.

La méthode de traitement par défaut est la suivante:

@Override
public void process(XMLEvent evt, ComplexDataType data)
    throws ProcessorException {


/*
 *  TODO Auto-generated method stub implement your own handling here.
 *  Use the runtime configuration file to determine which events are to be sent to the processor.
 */ 

    if (evt == XMLEvent.END) {
        data.print( ConsoleWriter.out );
    }
}

Téléchargements:

Premier mvn clean install le noyau (il doit être dans le référentiel maven local), puis le générateur. Et n'oubliez pas de configurer la variable d'environnement XML2J_HOME selon les instructions du manuel d'utilisation.

0
dexter