Як зберегти розриви рядків при використанні jsoup для перетворення HTML у звичайний текст?


101

У мене є такий код:

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

І я маю результат:

hello world yo googlez

Але я хочу перервати лінію:

hello world
yo googlez

Я переглянув TextNode # getWholeText () jsoup, але не можу зрозуміти, як ним користуватися.

Якщо <br>в розмітці я розбираю, як я можу отримати розрив рядка в отриманому результаті?


редагуйте текст - у вашому запитанні не з’являється розрив рядків. Загалом, прочитайте попередній перегляд свого питання, перш ніж його публікувати, щоб переконатися, що все відображається правильно.
Робін Грін

Я задав те саме питання (без вимоги jsoup), але все ще не маю гарного рішення: stackoverflow.com/questions/2513707/…
Eduardo

див. відповідь @zeenosaur.
Ян-Хо Бе

Відповіді:


102

Справжнє рішення, що зберігає перерви в лінії, має бути таким:

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

Він задовольняє наступним вимогам:

  1. якщо оригінальний html містить новий рядок (\ n), він зберігається
  2. якщо оригінальний html містить теги br або p, вони будуть переведені в новий рядок (\ n).

5
Це має бути обрана відповідь
1313

2
br2nl - не найкорисніша або точна назва методу
DD.

2
Це найкраща відповідь. Але як щодо for (Element e : document.select("br")) e.after(new TextNode("\n", ""));додавання реального нового рядка, а не послідовності \ n? Для різниці див. Вузол :: after () та Elements :: append () . У replaceAll()цьому випадку це не потрібно. Подібно до p та інших елементів блоку.
користувач2043553

1
@ user121196 відповідь повинна бути обраною відповіддю. Якщо у вас все-таки є HTML-об'єкти після очищення вхідного HTML, застосуйте StringEscapeUtils.unescapeHtml (...) Apache commons до виводу з очищення Jsoup.
karth500

6
Див. Github.com/jhy/jsoup/blob/master/src/main/java/org/jsoup/… для вичерпної відповіді на цю проблему.
Малькольм Сміт

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

Ми використовуємо цей метод тут:

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

Передаючи його, Whitelist.none()ми гарантуємо, що всі HTML видалені.

За допомогою прошивки new OutputSettings().prettyPrint(false)ми переконуємося, що вихід не буде переформатований та збережені розриви рядків.


Це має бути єдиною правильною відповіддю. Усі інші припускають, що лише brтеги створюють нові рядки. Як щодо будь-якого іншого блочного елемента в HTML , такі як div, p, і ulт.д.? Усі вони також вводять нові рядки.
adarshr

7
За допомогою цього рішення створено html "<html> <body> <div> рядок 1 </div> <div> рядок 2 </div> <div> рядок 3 </div> </body> </html>" вихід: "рядок 1 рядок 2 рядок 3" без нових рядків.
JohnC

2
Це не працює для мене; <br> не створюють розривів рядків.
ДжошуаD

43

З

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

у вас є вихід

"A B" 

і ні

A

B

Для цього я використовую:

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

2
Дійсно, це легко паліативне, але IMHO цим має повністю займатися сама бібліотека Jsoup (яка на даний момент має кілька тривожних форм поведінки, як ця - інакше це чудова бібліотека!).
СРГ

5
Хіба JSoup не дає вам DOM? Чому б просто не замінити всі <br>елементи текстовими вузлами, що містять нові рядки, а потім зателефонувати .text()замість того, щоб перетворити регулярний вираз, який спричинить неправильний вихід для деяких рядків, таких як<div title=<br>'not an attribute'></div>
Майк Самуель,

5
Добре, але звідки береться цей "опис"?
Стів Вотерс

"опис" представляє змінну, до якої присвоюється звичайний текст
enigma969

23

Спробуйте це за допомогою 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));
}

приємно, це працює на мене з невеликою зміною new Document.OutputSettings().prettyPrint(true)
Ашу,

Це рішення залишає "& nbsp;" як текст замість їх розбору в пробілі.
Андрій Волгін

13

На Jsoup v1.11.2 тепер ми можемо використовувати Element.wholeText().

Приклад коду:

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

user121196's відповідь все ще працює. Але wholeText()зберігає вирівнювання текстів.


Супер-приємна функція!
Денис Кулагін

8

Для більш складного HTML жодне з перерахованих вище рішень не працювало цілком правильно; Мені вдалося успішно здійснити перетворення, зберігаючи розриви рядків із:

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

(версія 1.10.3)


1
Найкращі відповіді! Дякую Енді Рез!
Бхарат Надукатла

6

Ви можете пройти певний елемент

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

А для вашого коду

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

Я думаю , ви повинні перевірити , якщо isBlockв tail(node, depth)замість цього, і Append \nпри виході з блоку , а не при вході його? Я роблю це (тобто використовую tail), і це прекрасно працює. Однак якщо я використовую так, headяк ви, то це: <p>line one<p>line twoзакінчується як один рядок.
KajMagnus

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

працює, якщо сам html не містить "br2n"

Так,

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

працює надійніше і простіше.


4

Спробуйте це за допомогою 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

Використовуйте textNodes()для отримання списку текстових вузлів. Потім з'єднайте їх \nяк роздільник. Ось який-небудь код scala, який я використовую для цього, порт Java повинен бути простим:

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

3

Виходячи з інших відповідей та коментарів до цього питання, здається, що більшість людей, які приїжджають сюди, дійсно шукають загальне рішення, яке забезпечить чітко відформатоване представлення простого тексту HTML-документа. Я знаю, що був.

На щастя, JSoup вже надає досить вичерпний приклад того, як цього досягти: HtmlToPlainText.java

Приклад FormattingVisitorможна легко підганяти до ваших уподобань і стосується більшості елементів блоку та обгортання ліній.

Щоб уникнути гниття посилань, ось рішення Джонатана Хедлі повністю:

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

Це моя версія перекладу HTML у текст (фактично модифікована версія відповіді user121196).

Це не лише зберігає розриви рядків, але також форматує текст і видаляє надмірні розриви рядків, символи втечі HTML, і ви отримаєте набагато кращий результат від свого HTML (у моєму випадку я отримую його з пошти).

Він спочатку написаний на Scala, але ви можете легко змінити його на Java

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

Вам також потрібно додати новий рядок до тегів <div>. В іншому випадку, якщо div слід за тегами <a> або <span>, він не буде в новому рядку.
Андрій Волгін

2

Спробуйте це:

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

1
<p> <b> привіт світ </b> </p> <p> <br /> <b> йо </b> <a href=" google.com"> googlez </a> </p > але мені потрібен привіт світ yo googlez (без html-тегів)
Біллі,

Ця відповідь не повертає звичайний текст; він повертає HTML із вставленими новими рядками.
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;
}

Використовується при виклику з відповідним html-файлом, що містить br, а також будь-яку рядок, який ви хочете використовувати як тимчасовий заповнювач нового рядка. Наприклад:

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

Рекурсія забезпечить, що рядок, який ви використовуєте як заповнювач нового рядка / рядка, завжди не буде у вихідному HTML-коді, оскільки він буде постійно додавати "1", доки рядок заповнювача заповнення linkbreaker не знайдеться в html. Не буде проблеми з форматуванням, що, схоже, методи Jsoup.clean стикаються зі спеціальними символами.


Хороший, але вам не потрібна рекурсія, просто додайте цей рядок: while (dirtyHTML.contains (linebreakerString)) linebreakerString = linebreakerString + "1";
Д-р NotSoKind

Ага, так. Цілком вірно. Здогадуюсь, мій розум
зациклювався

1

Виходячи з відповіді користувача121196 та Green Beret з selects і <pre>s, єдине рішення, яке працює для мене, це:

org.jsoup.nodes.Element elementWithHtml = ....
elementWithHtml.select("br").append("<pre>\n</pre>");
elementWithHtml.select("p").prepend("<pre>\n\n</pre>");
elementWithHtml.text();
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.