Como preservo as quebras de linha ao usar jsoup para converter html em texto simples?

101

Eu tenho o seguinte código:

 public class NewClass {
     public String noTags(String str){
         return Jsoup.parse(str).text();
     }


     public static void main(String args[]) {
         String strings="<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN \">" +
         "<HTML> <HEAD> <TITLE></TITLE> <style>body{ font-size: 12px;font-family: verdana, arial, helvetica, sans-serif;}</style> </HEAD> <BODY><p><b>hello world</b></p><p><br><b>yo</b> <a href=\"http://google.com\">googlez</a></p></BODY> </HTML> ";

         NewClass text = new NewClass();
         System.out.println((text.noTags(strings)));
}

E eu tenho o resultado:

hello world yo googlez

Mas eu quero quebrar a linha:

hello world
yo googlez

Eu olhei para o TextNode # getWholeText () do jsoup, mas não consigo descobrir como usá-lo.

Se houver um <br>na marcação que analiso, como posso obter uma quebra de linha na saída resultante?

Billy
fonte
edite seu texto - não há quebra de linha aparecendo em sua pergunta. Em geral, leia a prévia de sua pergunta antes de postá-la, para verificar se está tudo certo.
Robin Green de
Eu fiz a mesma pergunta (sem o requisito jsoup), mas ainda não tenho uma boa solução: stackoverflow.com/questions/2513707/…
Eduardo
veja a resposta de @zeenosaur.
Jang-Ho Bae

Respostas:

102

A solução real que preserva as quebras de linha deve ser assim:

public static String br2nl(String html) {
    if(html==null)
        return html;
    Document document = Jsoup.parse(html);
    document.outputSettings(new Document.OutputSettings().prettyPrint(false));//makes html() preserve linebreaks and spacing
    document.select("br").append("\\n");
    document.select("p").prepend("\\n\\n");
    String s = document.html().replaceAll("\\\\n", "\n");
    return Jsoup.clean(s, "", Whitelist.none(), new Document.OutputSettings().prettyPrint(false));
}

Ele atende aos seguintes requisitos:

  1. se o html original contiver nova linha (\ n), ele será preservado
  2. se o html original contém tags br ou p, eles são traduzidos para nova linha (\ n).
user121196
fonte
5
Esta deve ser a resposta escolhida
duy
2
br2nl não é o nome de método mais útil ou preciso
DD.
2
Esta é a melhor resposta. Mas que tal for (Element e : document.select("br")) e.after(new TextNode("\n", ""));acrescentar nova linha real e não a sequência \ n? Veja Node :: after () e Elements :: append () para a diferença. O replaceAll()não é necessário neste caso. Semelhante para pe outros elementos de bloco.
user2043553
1
A resposta de @ user121196 deve ser a resposta escolhida. Se você ainda tiver entidades HTML após limpar o HTML de entrada, aplique StringEscapeUtils.unescapeHtml (...) Apache commons à saída do Jsoup clean.
karth500
6
Veja github.com/jhy/jsoup/blob/master/src/main/java/org/jsoup/… para uma resposta abrangente para este problema.
Malcolm Smith
44
Jsoup.clean(unsafeString, "", Whitelist.none(), new OutputSettings().prettyPrint(false));

Estamos usando este método aqui:

public static String clean(String bodyHtml,
                       String baseUri,
                       Whitelist whitelist,
                       Document.OutputSettings outputSettings)

Ao passá-lo Whitelist.none(), garantimos que todo o HTML foi removido.

Ao passar new OutputSettings().prettyPrint(false), certificamo-nos de que a saída não é reformatada e que as quebras de linha são preservadas.

Paulius Z
fonte
Esta deve ser a única resposta correta. Todos os outros pressupõem que apenas as brtags produzem novas linhas. O que sobre qualquer outro elemento de bloco em HTML, como div, p, uletc? Todos eles apresentam novas linhas também.
adarshr
7
Com esta solução, o html "<html> <body> <div> linha 1 </div> <div> linha 2 </div> <div> linha 3 </div> </body> </html>" produziu a saída: "line 1line 2line 3" sem novas linhas.
JohnC
2
Isso não funciona para mim; <br> não estão criando quebras de linha.
JoshuaD 01 de
43

Com

Jsoup.parse("A\nB").text();

você tem saída

"A B" 

e não

A

B

Para isso estou usando:

descrizione = Jsoup.parse(html.replaceAll("(?i)<br[^>]*>", "br2n")).text();
text = descrizione.replaceAll("br2n", "\n");
Mirco Attocchi
fonte
2
Na verdade, este é um paliativo fácil, mas IMHO isso deve ser totalmente controlado pela própria biblioteca Jsoup (que neste momento tem alguns comportamentos perturbadores como este - caso contrário, é uma ótima biblioteca!).
SRG
5
O JSoup não fornece um DOM? Por que não apenas substituir todos os <br>elementos por nós de texto contendo novas linhas e depois chamar em .text()vez de fazer uma transformação regex que causará saída incorreta para algumas strings como<div title=<br>'not an attribute'></div>
Mike Samuel
5
Legal, mas de onde vem essa "descrição"?
Steve Waters
"descrizione" representa a variável à qual o texto simples é atribuído
enigma969
23

Tente fazer isso usando jsoup:

public static String cleanPreserveLineBreaks(String bodyHtml) {

    // get pretty printed html with preserved br and p tags
    String prettyPrintedBodyFragment = Jsoup.clean(bodyHtml, "", Whitelist.none().addTags("br", "p"), new OutputSettings().prettyPrint(true));
    // get plain text with preserved line breaks by disabled prettyPrint
    return Jsoup.clean(prettyPrintedBodyFragment, "", Whitelist.none(), new OutputSettings().prettyPrint(false));
}
mkowa
fonte
bom, funciona comigo com uma pequena mudança new Document.OutputSettings().prettyPrint(true)
Ashu
Esta solução deixa "& nbsp;" como texto em vez de analisá-los em um espaço.
Andrei Volgin
13

No Jsoup v1.11.2, agora podemos usar Element.wholeText().

Código de exemplo:

String cleanString = Jsoup.parse(htmlString).wholeText();

user121196's a resposta ainda funciona. Mas wholeText()preserva o alinhamento dos textos.

zeenossauro
fonte
Excelente recurso!
Denis Kulagin
8

Para HTML mais complexo, nenhuma das soluções acima funcionou perfeitamente; Consegui fazer a conversão com sucesso, preservando as quebras de linha com:

Document document = Jsoup.parse(myHtml);
String text = new HtmlToPlainText().getPlainText(document);

(versão 1.10.3)

Andy Res
fonte
1
Melhor de todas as respostas! Obrigado Andy Res!
Bharath Nadukatla
6

Você pode percorrer um determinado elemento

public String convertNodeToText(Element element)
{
    final StringBuilder buffer = new StringBuilder();

    new NodeTraversor(new NodeVisitor() {
        boolean isNewline = true;

        @Override
        public void head(Node node, int depth) {
            if (node instanceof TextNode) {
                TextNode textNode = (TextNode) node;
                String text = textNode.text().replace('\u00A0', ' ').trim();                    
                if(!text.isEmpty())
                {                        
                    buffer.append(text);
                    isNewline = false;
                }
            } else if (node instanceof Element) {
                Element element = (Element) node;
                if (!isNewline)
                {
                    if((element.isBlock() || element.tagName().equals("br")))
                    {
                        buffer.append("\n");
                        isNewline = true;
                    }
                }
            }                
        }

        @Override
        public void tail(Node node, int depth) {                
        }                        
    }).traverse(element);        

    return buffer.toString();               
}

E para o seu código

String result = convertNodeToText(JSoup.parse(html))
pipoca
fonte
Eu acho que você deveria testar se isBlockem tail(node, depth)vez disso, e acrescentar \nao sair do bloco em vez de ao entrar. Estou fazendo isso (ou seja, usando tail) e funciona bem. No entanto, se eu usar headcomo você, então isso: <p>line one<p>line twoacaba como uma única linha.
KajMagnus
4
text = Jsoup.parse(html.replaceAll("(?i)<br[^>]*>", "br2n")).text();
text = descrizione.replaceAll("br2n", "\n");

funciona se o próprio html não contiver "br2n"

Assim,

text = Jsoup.parse(html.replaceAll("(?i)<br[^>]*>", "<pre>\n</pre>")).text();

funciona de forma mais confiável e fácil.

Boina verde
fonte
4

Tente fazer isso usando jsoup:

    doc.outputSettings(new OutputSettings().prettyPrint(false));

    //select all <br> tags and append \n after that
    doc.select("br").after("\\n");

    //select all <p> tags and prepend \n before that
    doc.select("p").before("\\n");

    //get the HTML from the document, and retaining original new lines
    String str = doc.html().replaceAll("\\\\n", "\n");
Abhay Gupta
fonte
3

Use textNodes()para obter uma lista dos nós de texto. Em seguida, concatene-os com um \nseparador. Aqui estão alguns códigos de scala que uso para isso, a porta java deve ser fácil:

val rawTxt = doc.body().getElementsByTag("div").first.textNodes()
                    .asScala.mkString("<br />\n")
Michael Bar-Sinai
fonte
3

Com base nas outras respostas e nos comentários sobre esta questão, parece que a maioria das pessoas que vêm aqui estão realmente procurando por uma solução geral que fornecerá uma representação de texto simples bem formatada de um documento HTML. Eu sei que estava.

Felizmente, o JSoup já fornece um exemplo bastante abrangente de como fazer isso: HtmlToPlainText.java

O exemplo FormattingVisitorpode ser facilmente ajustado de acordo com sua preferência e lida com a maioria dos elementos de bloco e quebra de linha.

Para evitar o apodrecimento do link, aqui está a solução completa de Jonathan Hedley :

package org.jsoup.examples;

import org.jsoup.Jsoup;
import org.jsoup.helper.StringUtil;
import org.jsoup.helper.Validate;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;

import java.io.IOException;

/**
 * HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted
 * plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a
 * scrape.
 * <p>
 * Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend.
 * </p>
 * <p>
 * To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:</p>
 * <p><code>java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]</code></p>
 * where <i>url</i> is the URL to fetch, and <i>selector</i> is an optional CSS selector.
 * 
 * @author Jonathan Hedley, [email protected]
 */
public class HtmlToPlainText {
    private static final String userAgent = "Mozilla/5.0 (jsoup)";
    private static final int timeout = 5 * 1000;

    public static void main(String... args) throws IOException {
        Validate.isTrue(args.length == 1 || args.length == 2, "usage: java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]");
        final String url = args[0];
        final String selector = args.length == 2 ? args[1] : null;

        // fetch the specified URL and parse to a HTML DOM
        Document doc = Jsoup.connect(url).userAgent(userAgent).timeout(timeout).get();

        HtmlToPlainText formatter = new HtmlToPlainText();

        if (selector != null) {
            Elements elements = doc.select(selector); // get each element that matches the CSS selector
            for (Element element : elements) {
                String plainText = formatter.getPlainText(element); // format that element to plain text
                System.out.println(plainText);
            }
        } else { // format the whole doc
            String plainText = formatter.getPlainText(doc);
            System.out.println(plainText);
        }
    }

    /**
     * Format an Element to plain-text
     * @param element the root element to format
     * @return formatted text
     */
    public String getPlainText(Element element) {
        FormattingVisitor formatter = new FormattingVisitor();
        NodeTraversor traversor = new NodeTraversor(formatter);
        traversor.traverse(element); // walk the DOM, and call .head() and .tail() for each node

        return formatter.toString();
    }

    // the formatting rules, implemented in a breadth-first DOM traverse
    private class FormattingVisitor implements NodeVisitor {
        private static final int maxWidth = 80;
        private int width = 0;
        private StringBuilder accum = new StringBuilder(); // holds the accumulated text

        // hit when the node is first seen
        public void head(Node node, int depth) {
            String name = node.nodeName();
            if (node instanceof TextNode)
                append(((TextNode) node).text()); // TextNodes carry all user-readable text in the DOM.
            else if (name.equals("li"))
                append("\n * ");
            else if (name.equals("dt"))
                append("  ");
            else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5", "tr"))
                append("\n");
        }

        // hit when all of the node's children (if any) have been visited
        public void tail(Node node, int depth) {
            String name = node.nodeName();
            if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5"))
                append("\n");
            else if (name.equals("a"))
                append(String.format(" <%s>", node.absUrl("href")));
        }

        // appends text to the string builder with a simple word wrap method
        private void append(String text) {
            if (text.startsWith("\n"))
                width = 0; // reset counter if starts with a newline. only from formats above, not in natural text
            if (text.equals(" ") &&
                    (accum.length() == 0 || StringUtil.in(accum.substring(accum.length() - 1), " ", "\n")))
                return; // don't accumulate long runs of empty spaces

            if (text.length() + width > maxWidth) { // won't fit, needs to wrap
                String words[] = text.split("\\s+");
                for (int i = 0; i < words.length; i++) {
                    String word = words[i];
                    boolean last = i == words.length - 1;
                    if (!last) // insert a space if not the last word
                        word = word + " ";
                    if (word.length() + width > maxWidth) { // wrap and reset counter
                        accum.append("\n").append(word);
                        width = word.length();
                    } else {
                        accum.append(word);
                        width += word.length();
                    }
                }
            } else { // fits as is, without need to wrap text
                accum.append(text);
                width += text.length();
            }
        }

        @Override
        public String toString() {
            return accum.toString();
        }
    }
}
Malcolm Smith
fonte
3

Esta é a minha versão de tradução de html para texto (a versão modificada da resposta user121196, na verdade).

Isso não apenas preserva as quebras de linha, mas também formata o texto e remove quebras de linha excessivas, símbolos de escape HTML, e você obterá um resultado muito melhor do seu HTML (no meu caso, estou recebendo por e-mail).

Ele foi originalmente escrito em Scala, mas você pode alterá-lo para Java facilmente

def html2text( rawHtml : String ) : String = {

    val htmlDoc = Jsoup.parseBodyFragment( rawHtml, "/" )
    htmlDoc.select("br").append("\\nl")
    htmlDoc.select("div").prepend("\\nl").append("\\nl")
    htmlDoc.select("p").prepend("\\nl\\nl").append("\\nl\\nl")

    org.jsoup.parser.Parser.unescapeEntities(
        Jsoup.clean(
          htmlDoc.html(),
          "",
          Whitelist.none(),
          new org.jsoup.nodes.Document.OutputSettings().prettyPrint(true)
        ),false
    ).
    replaceAll("\\\\nl", "\n").
    replaceAll("\r","").
    replaceAll("\n\\s+\n","\n").
    replaceAll("\n\n+","\n\n").     
    trim()      
}
abdolência
fonte
Você precisa acrescentar uma nova linha às tags <div> também. Caso contrário, se um div seguir as tags <a> ou <span>, não estará em uma nova linha.
Andrei Volgin
2

Experimente isto:

public String noTags(String str){
    Document d = Jsoup.parse(str);
    TextNode tn = new TextNode(d.body().html(), "");
    return tn.getWholeText();
}
Manji
fonte
1
<p> <b> Olá, mundo </b> </p> <p> <br /> <b> sim </b> <a href=" google.com"> googlez </a> </ p > mas eu preciso de olá, mundo, yo googlez (sem tags html)
Billy
Esta resposta não retorna texto simples; ele retorna HTML com novas linhas inseridas.
KajMagnus
1
/**
 * Recursive method to replace html br with java \n. The recursive method ensures that the linebreaker can never end up pre-existing in the text being replaced.
 * @param html
 * @param linebreakerString
 * @return the html as String with proper java newlines instead of br
 */
public static String replaceBrWithNewLine(String html, String linebreakerString){
    String result = "";
    if(html.contains(linebreakerString)){
        result = replaceBrWithNewLine(html, linebreakerString+"1");
    } else {
        result = Jsoup.parse(html.replaceAll("(?i)<br[^>]*>", linebreakerString)).text(); // replace and html line breaks with java linebreak.
        result = result.replaceAll(linebreakerString, "\n");
    }
    return result;
}

Usado chamando com o html em questão, contendo o br, junto com qualquer string que você deseja usar como marcador temporário de nova linha. Por exemplo:

replaceBrWithNewLine(element.html(), "br2n")

A recursão garantirá que a string que você usa como placeholder de nova linha / linebreaker nunca estará realmente no html de origem, pois continuará adicionando um "1" até que a string de placeholder do linkbreaker não seja encontrada no html. Não haverá o problema de formatação que os métodos Jsoup.clean parecem encontrar com caracteres especiais.

Chris6647
fonte
Boa, mas você não precisa de recursão, apenas adicione esta linha: while (dirtyHTML.contains (linebreakerString)) linebreakerString = linebreakerString + "1";
Dr. NotSoKind
Ah sim. Completamente verdadeiro. Acho que minha mente ficou presa pela primeira vez, podendo usar recursão :)
Chris6647
1

Baseado em user121196 de e resposta da boina verde com as selects e <pre>s, a única solução que funciona para mim é:

org.jsoup.nodes.Element elementWithHtml = ....
elementWithHtml.select("br").append("<pre>\n</pre>");
elementWithHtml.select("p").prepend("<pre>\n\n</pre>");
elementWithHtml.text();
Bevor
fonte