¿Cómo conservo los saltos de línea cuando uso jsoup para convertir HTML a texto sin formato?


101

Tengo el siguiente 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)));
}

Y tengo el resultado:

hello world yo googlez

Pero quiero romper la línea:

hello world
yo googlez

He mirado el TextNode # getWholeText () de jsoup pero no puedo averiguar cómo usarlo.

Si hay un <br>marcado en el marcado que analizo, ¿cómo puedo obtener un salto de línea en mi salida resultante?


edite su texto: no se muestra ningún salto de línea en su pregunta. En general, lea la vista previa de su pregunta antes de publicarla, para comprobar que todo se muestra correctamente.
Robin Green

Hice la misma pregunta (sin el requisito de jsoup) pero todavía no tengo una buena solución: stackoverflow.com/questions/2513707/…
Eduardo

vea la respuesta de @zeenosaur.
Jang-Ho Bae

Respuestas:


102

La solución real que conserva los saltos de línea debería ser así:

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));
}

Cumple los siguientes requisitos:

  1. si el html original contiene una nueva línea (\ n), se conserva
  2. si el html original contiene etiquetas br o p, se traducen a una nueva línea (\ n).

5
Esta debería ser la respuesta seleccionada
duy

2
br2nl no es el nombre de método más útil o preciso
DD.

2
Esta es la mejor respuesta. Pero, ¿qué tal for (Element e : document.select("br")) e.after(new TextNode("\n", ""));agregar una nueva línea real y no la secuencia \ n? Consulte Node :: after () y Elements :: append () para conocer la diferencia. En replaceAll()este caso, no es necesario. Similar para py otros elementos de bloque.
user2043553

1
La respuesta de @ user121196 debe ser la respuesta elegida. Si aún tiene entidades HTML después de limpiar el HTML de entrada, aplique StringEscapeUtils.unescapeHtml (...) Apache commons a la salida de Jsoup clean.
karth500

6
Consulte github.com/jhy/jsoup/blob/master/src/main/java/org/jsoup/… para obtener una respuesta completa a este problema.
Malcolm Smith

44
Jsoup.clean(unsafeString, "", Whitelist.none(), new OutputSettings().prettyPrint(false));

Estamos usando este método aquí:

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

Al pasarlo, Whitelist.none()nos aseguramos de que se elimine todo el HTML.

Al pasar new OutputSettings().prettyPrint(false)nos aseguramos de que la salida no se reformatee y se conserven los saltos de línea.


Esta debería ser la única respuesta correcta. Todos los demás asumen que solo las bretiquetas producen nuevas líneas. ¿Qué pasa con cualquier otro elemento de bloque en HTML como div, p, uletc.? Todos ellos también introducen nuevas líneas.
adarshr

7
Con esta solución, el html "<html> <body> <div> línea 1 </div> <div> línea 2 </div> <div> línea 3 </div> </body> </html>" produjo el resultado: "línea 1 línea 2 línea 3" sin nuevas líneas.
JohnC

2
Esto no funciona para mí; Los de <br> no crean saltos de línea.
JoshuaD

43

Con

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

tienes salida

"A B" 

y no

A

B

Para esto estoy usando:

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

2
De hecho, este es un paliativo fácil, pero en mi humilde opinión, esto debería ser manejado por completo por la propia biblioteca Jsoup (que en este momento tiene algunos comportamientos perturbadores como este; de ​​lo contrario, ¡es una gran biblioteca!).
SRG

5
¿JSoup no te da un DOM? ¿Por qué no reemplazar todos los <br>elementos con nodos de texto que contienen nuevas líneas y luego llamar en .text()lugar de hacer una transformación de expresiones regulares que causará una salida incorrecta para algunas cadenas como<div title=<br>'not an attribute'></div>
Mike Samuel

5
Bien, pero ¿de dónde viene esa "descrizione"?
Steve Waters

"descrizione" representa la variable a la que se asigna el texto sin formato
enigma969

23

Pruebe esto 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));
}

agradable, me funciona con un pequeño cambio new Document.OutputSettings().prettyPrint(true)
Ashu

Esta solución deja "& nbsp;" como texto en lugar de analizarlos en un espacio.
Andrei Volgin

13

En Jsoup v1.11.2, ahora podemos usar Element.wholeText().

Código de ejemplo:

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

user121196's la respuesta todavía funciona. Pero wholeText()conserva la alineación de los textos.


¡Característica súper agradable!
Denis Kulagin

8

Para HTML más complejo, ninguna de las soluciones anteriores funcionó del todo bien; Pude hacer la conversión con éxito mientras conservaba los saltos de línea con:

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

(versión 1.10.3)


1
¡Lo mejor de todas las respuestas! ¡Gracias Andy Res!
Bharath Nadukatla

6

Puedes atravesar un elemento dado

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();               
}

Y por tu codigo

String result = convertNodeToText(JSoup.parse(html))

Creo que debería probar si está isBlocken su tail(node, depth)lugar y agregarlo \nal salir del bloque en lugar de al ingresarlo. Estoy haciendo eso (es decir, usando tail) y funciona bien. Sin embargo, si uso headcomo tú, entonces esto: <p>line one<p>line twotermina como una sola línea.
KajMagnus

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

funciona si el propio html no contiene "br2n"

Entonces,

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

funciona de forma más fiable y sencilla.


4

Pruebe esto 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");

3

Úselo textNodes()para obtener una lista de los nodos de texto. Luego concatenarlos con \ncomo separador. Aquí hay un código scala que utilizo para esto, el puerto java debería ser fácil:

val rawTxt = doc.body().getElementsByTag("div").first.textNodes()
                    .asScala.mkString("<br />\n")

3

Según las otras respuestas y los comentarios sobre esta pregunta, parece que la mayoría de las personas que vienen aquí están realmente buscando una solución general que proporcione una representación de texto sin formato con un formato agradable de un documento HTML. Sé que lo estaba.

Afortunadamente, JSoup ya proporciona un ejemplo bastante completo de cómo lograr esto: HtmlToPlainText.java

El ejemplo FormattingVisitorse puede modificar fácilmente según sus preferencias y se ocupa de la mayoría de los elementos de bloque y del ajuste de línea.

Para evitar la descomposición del enlace, aquí está la solución de Jonathan Hedley completa:

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, jonathan@hedley.net
 */
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();
        }
    }
}

3

Esta es mi versión de traducir html a texto (la versión modificada de la respuesta de user121196, en realidad).

Esto no solo conserva los saltos de línea, sino que también formatea el texto y elimina los saltos de línea excesivos, los símbolos de escape HTML, y obtendrá un resultado mucho mejor de su HTML (en mi caso, lo recibo por correo).

Está escrito originalmente en Scala, pero puede cambiarlo a Java fácilmente

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()      
}

También debe anteponer una nueva línea a las etiquetas <div>. De lo contrario, si un div sigue a las etiquetas <a> o <span>, no estará en una nueva línea.
Andrei Volgin

2

Prueba esto:

public String noTags(String str){
    Document d = Jsoup.parse(str);
    TextNode tn = new TextNode(d.body().html(), "");
    return tn.getWholeText();
}

1
<p> <b> hola mundo </b> </p> <p> <br /> <b> yo </b> <a href=" google.com"> googlez </a> </ p > pero necesito hola mundo yo googlez (sin etiquetas html)
Billy

Esta respuesta no devuelve texto sin formato; devuelve HTML con nuevas líneas insertadas.
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;
}

Se usa al llamar con el html en cuestión, que contiene el br, junto con cualquier cadena que desee usar como marcador de posición de nueva línea temporal. Por ejemplo:

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

La recursividad asegurará que la cadena que utiliza como marcador de posición de salto de línea / salto de línea nunca estará realmente en el html de origen, ya que seguirá agregando un "1" hasta que no se encuentre la cadena de marcador de posición del separador de enlaces en el html. No tendrá el problema de formato que los métodos Jsoup.clean parecen encontrar con caracteres especiales.


Buena, pero no necesita recursividad, simplemente agregue esta línea: while (dirtyHTML.contains (linebreakerString)) linebreakerString = linebreakerString + "1";
Dr NotSoKind

Ah, sí. Completamente cierto. Supongo que mi mente quedó atrapada por una vez en poder usar la recursividad :)
Chris6647

1

Según la respuesta de user121196 y Green Beret con selects y <pre>s, la única solución que me funciona es:

org.jsoup.nodes.Element elementWithHtml = ....
elementWithHtml.select("br").append("<pre>\n</pre>");
elementWithHtml.select("p").prepend("<pre>\n\n</pre>");
elementWithHtml.text();
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.