2010-01-21 21 views
10

Moje pytanie brzmi trochę jak this question ale mam kolejne ograniczenia:Jak wyodrębnić tekst z rozsądnie rozsądnego HTML?

  • wiem dokument są rozsądnie rozsądny
  • są bardzo regularne (wszystkie one pochodzą z tego samego źródła
  • Chcę około 99 % widocznego tekstu
  • około 99% tego, co jest opłacalne w ogóle jest tekst (są one bardziej lub mniej RTF konwertowane do formatu HTML)
  • nie dbam o przerwach formatowania lub nawet ust.

Czy są jakieś narzędzia skonfigurowane do robienia tego lub czy lepiej wyłuskać RegexBuddy i C#?

Jestem otwarty na narzędzia wiersza poleceń lub narzędzia do przetwarzania wsadowego, a także biblioteki C/C#/D.

+3

Wszystko, ale regexes. – jonnii

+0

Gdyby było to coś innego niż ograniczenia, nigdy bym nie pomyślał * o regexie :) – BCS

Odpowiedz

6

Należy użyć HTML Agility Pack.

Prawdopodobnie chcesz znaleźć element za pomocą LINQ i wywołania Descendants, a następnie uzyskać jego InnerText.

+0

Masz na myśli, że muszę się nauczyć LINQ? (zaskakująco, to jest pierwsza rzecz, na którą natknąłem się, to LINQ brzmi jak właściwa droga, ale potem znowu, nie jestem zwykle w tej dziedzinie) – BCS

+0

@BCS: Nie musisz się nauczyć LINQ, ale LINQ znacznie ułatwia używanie. Sądzę, że efektywne użycie LINQ sprawiłoby, że twój kod był co najmniej o 120% krótszy i łatwiejszy do zrozumienia. – SLaks

+2

Wow mój kod to -20 linii kodu! ;) – BCS

2

Jest to stosunkowo proste, jeśli załadujesz HTML do C#, a następnie za pomocą mshtml.dll lub formantu WebBrowser w C#/WinForms, możesz następnie traktować cały dokument HTML jako drzewo, przechodzić przez drzewo przechwytujące obiekty InnerText.

Można też użyć pliku document.all, który pobiera drzewo, spłaszcza go, a następnie można przetworzyć go po drzewie, ponownie przechwytując tekst InnerText.

Oto przykład:

 WebBrowser webBrowser = new WebBrowser(); 
     webBrowser.Url = new Uri("url_of_file"); //can be remote or local 
     webBrowser.DocumentCompleted += delegate 
     { 
      HtmlElementCollection collection = webBrowser.Document.All; 
      List<string> contents = new List<string>(); 

      /* 
      * Adds all inner-text of a tag, including inner-text of sub-tags 
      * ie. <html><body><a>test</a><b>test 2</b></body></html> would do: 
      * "test test 2" when collection[i] == <html> 
      * "test test 2" when collection[i] == <body> 
      * "test" when collection[i] == <a> 
      * "test 2" when collection[i] == <b> 
      */ 
      for (int i = 0; i < collection.Count; i++) 
      { 
       if (!string.IsNullOrEmpty(collection[i].InnerText)) 
       { 
        contents.Add(collection[i].InnerText); 
       } 
      } 

      /* 
      * <html><body><a>test</a><b>test 2</b></body></html> 
      * outputs: test test 2|test test 2|test|test 2 
      */ 
      string contentString = string.Join("|", contents.ToArray()); 
      MessageBox.Show(contentString); 
     }; 

nadzieję, że pomoże!

+0

Googleing dla mshtml.dll daje większość raportów o stronie lub błędach, naprawia błędy i błędy. --- Czy masz link do jakiejś dokumentacji? – BCS

+0

Właśnie edytowałem swój post na przykładzie za pomocą kontrolki WebBrowser. – AlishahNovin

+0

dobry przykład, działa dobrze dla mnie .. + 1 – ridoy

1

Tutaj możesz pobrać narzędzie i jego źródło, które konwertuje do iz powrotem HTML i XAML: XAML/HTML converter.

Zawiera parser HTML (coś takiego musi oczywiście być bardziej tolerancyjne niż standardowy analizator składni XML) i można przechodzić przez HTML podobny do XML.

1

z linii poleceń, można użyć przeglądarki Lynx tekstu like this:

Jeśli chcesz pobrać stronę internetową w sformatowanym wyjściu (czyli bez znaczników HTML, ale zamiast jak wydaje się w Lynx), a następnie wpisać:

lynx -dump URL > filename 

Jeśli są jakieś linki na stronie, adresy URL dla tych linków zostaną włączone na końcu pobranego stronie.

Możesz disable the list of links z -nolist. Na przykład:

lynx -dump -nolist http://stackoverflow.com/a/10469619/724176 > filename 
14

Ten kod I włamał się dzisiaj z HTML Agility Pack będzie wyodrębnić tekst niesformatowany przycięte.

public static string ExtractText(string html) 
{ 
    if (html == null) 
    { 
     throw new ArgumentNullException("html"); 
    } 

    HtmlDocument doc = new HtmlDocument(); 
    doc.LoadHtml(html); 

    var chunks = new List<string>(); 

    foreach (var item in doc.DocumentNode.DescendantNodesAndSelf()) 
    { 
     if (item.NodeType == HtmlNodeType.Text) 
     { 
      if (item.InnerText.Trim() != "") 
      { 
       chunks.Add(item.InnerText.Trim()); 
      } 
     } 
    } 
    return String.Join(" ", chunks); 
} 

Jeśli chcesz utrzymać pewien poziom formatowania można zbudować na the sample wyposażonego źródła.

public string Convert(string path) 
{ 
    HtmlDocument doc = new HtmlDocument(); 
    doc.Load(path); 

    StringWriter sw = new StringWriter(); 
    ConvertTo(doc.DocumentNode, sw); 
    sw.Flush(); 
    return sw.ToString(); 
} 

public string ConvertHtml(string html) 
{ 
    HtmlDocument doc = new HtmlDocument(); 
    doc.LoadHtml(html); 

    StringWriter sw = new StringWriter(); 
    ConvertTo(doc.DocumentNode, sw); 
    sw.Flush(); 
    return sw.ToString(); 
} 

public void ConvertTo(HtmlNode node, TextWriter outText) 
{ 
    string html; 
    switch (node.NodeType) 
    { 
     case HtmlNodeType.Comment: 
      // don't output comments 
      break; 

     case HtmlNodeType.Document: 
      ConvertContentTo(node, outText); 
      break; 

     case HtmlNodeType.Text: 
      // script and style must not be output 
      string parentName = node.ParentNode.Name; 
      if ((parentName == "script") || (parentName == "style")) 
       break; 

      // get text 
      html = ((HtmlTextNode) node).Text; 

      // is it in fact a special closing node output as text? 
      if (HtmlNode.IsOverlappedClosingElement(html)) 
       break; 

      // check the text is meaningful and not a bunch of whitespaces 
      if (html.Trim().Length > 0) 
      { 
       outText.Write(HtmlEntity.DeEntitize(html)); 
      } 
      break; 

     case HtmlNodeType.Element: 
      switch (node.Name) 
      { 
       case "p": 
        // treat paragraphs as crlf 
        outText.Write("\r\n"); 
        break; 
      } 

      if (node.HasChildNodes) 
      { 
       ConvertContentTo(node, outText); 
      } 
      break; 
    } 
} 


private void ConvertContentTo(HtmlNode node, TextWriter outText) 
{ 
    foreach (HtmlNode subnode in node.ChildNodes) 
    { 
     ConvertTo(subnode, outText); 
    } 
} 
0

Oto najlepszy sposób:

public static string StripHTML(string HTMLText) 
    { 
     Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); 
     return reg.Replace(HTMLText, ""); 
    } 
+1

'wybierz łącze z google gdzie zapytanie =" Limit Html RegEx "1" -> http://stackoverflow.com/questions/1732348 – BCS

2

Oto kod używam:

using System.Web; 
public static string ExtractText(string html) 
{ 
    Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); 
    string s =reg.Replace(html, " "); 
    s = HttpUtility.HtmlDecode(s); 
    return s; 
} 
+1

Może to być dopuszczalne w pewnych okolicznościach. Należy jednak pamiętać, że każdy nawias prostokątny występujący w komentarzu lub bloku CDATA przerwałby to wyrażenie regularne, nie wspominając już o tym, że regex może zmienić zawartość znaczników '

2

Można użyć NUglify który obsługuje ekstrakcji tekstu z HTML:

var result = Uglify.HtmlToText("<div> <p>This is <em> a text </em></p> </div>"); 
Console.WriteLine(result.Code); // prints: This is a text 

Ponieważ używa HTML5 cu stom parser, powinien być całkiem niezawodny (szczególnie, jeśli dokument nie zawiera żadnych błędów) i jest bardzo szybki (nie dotyczy to wyrażenia regularnego, ale czysty parser rekurencyjny)

+0

FWIW, działa to znakomicie przy minimum fuzz. Dzięki! – Frans

0

Oto klasa, którą opracowałem, aby osiągnąć to samo . Wszystkie dostępne biblioteki parsujące HTML były zbyt wolne, regex również było zbyt wolne. Funkcjonalność wyjaśniono w komentarzach do kodu. Z moich testów wynika, że ​​ten kod jest nieco ponad 10 razy szybszy niż równoważny kod HTML Agility Pack podczas testowania na stronie docelowej Amazon (uwzględnionej poniżej).

/// <summary> 
/// The fast HTML text extractor class is designed to, as quickly and as ignorantly as possible, 
/// extract text data from a given HTML character array. The class searches for and deletes 
/// script and style tags in a first and second pass, with an optional third pass to do the same 
/// to HTML comments, and then copies remaining non-whitespace character data to an ouput array. 
/// All whitespace encountered is replaced with a single whitespace in to avoid multiple 
/// whitespace in the output. 
/// 
/// Note that the returned text content still may have named character and numbered character 
/// references within that, when decoded, may produce multiple whitespace. 
/// </summary> 
public class FastHtmlTextExtractor 
{ 

    private readonly char[] SCRIPT_OPEN_TAG = new char[7] { '<', 's', 'c', 'r', 'i', 'p', 't' }; 
    private readonly char[] SCRIPT_CLOSE_TAG = new char[9] { '<', '/', 's', 'c', 'r', 'i', 'p', 't', '>' }; 

    private readonly char[] STYLE_OPEN_TAG = new char[6] { '<', 's', 't', 'y', 'l', 'e' }; 
    private readonly char[] STYLE_CLOSE_TAG = new char[8] { '<', '/', 's', 't', 'y', 'l', 'e', '>' }; 

    private readonly char[] COMMENT_OPEN_TAG = new char[3] { '<', '!', '-' }; 
    private readonly char[] COMMENT_CLOSE_TAG = new char[3] { '-', '-', '>' }; 

    private int[] m_deletionDictionary; 

    public string Extract(char[] input, bool stripComments = false) 
    { 
     var len = input.Length; 
     int next = 0; 

     m_deletionDictionary = new int[len]; 

     // Whipe out all text content between style and script tags. 
     FindAndWipe(SCRIPT_OPEN_TAG, SCRIPT_CLOSE_TAG, input); 
     FindAndWipe(STYLE_OPEN_TAG, STYLE_CLOSE_TAG, input); 

     if(stripComments) 
     { 
      // Whipe out everything between HTML comments. 
      FindAndWipe(COMMENT_OPEN_TAG, COMMENT_CLOSE_TAG, input); 
     } 

     // Whipe text between all other tags now. 
     while(next < len) 
     { 
      next = SkipUntil(next, '<', input); 

      if(next < len) 
      { 
       var closeNext = SkipUntil(next, '>', input); 

       if(closeNext < len) 
       { 
        m_deletionDictionary[next] = (closeNext + 1) - next; 
        WipeRange(next, closeNext + 1, input); 
       } 

       next = closeNext + 1; 
      } 
     } 

     // Collect all non-whitespace and non-null chars into a new 
     // char array. All whitespace characters are skipped and replaced 
     // with a single space char. Multiple whitespace is ignored. 
     var lastSpace = true; 
     var extractedPos = 0; 
     var extracted = new char[len]; 

     for(next = 0; next < len; ++next) 
     { 
      if(m_deletionDictionary[next] > 0) 
      { 
       next += m_deletionDictionary[next]; 
       continue; 
      } 

      if(char.IsWhiteSpace(input[next]) || input[next] == '\0') 
      { 
       if(lastSpace) 
       { 
        continue; 
       } 

       extracted[extractedPos++] = ' '; 
       lastSpace = true; 
      } 
      else 
      { 
       lastSpace = false; 
       extracted[extractedPos++] = input[next]; 
      } 
     } 

     return new string(extracted, 0, extractedPos); 
    } 

    /// <summary> 
    /// Does a search in the input array for the characters in the supplied open and closing tag 
    /// char arrays. Each match where both tag open and tag close are discovered causes the text 
    /// in between the matches to be overwritten by Array.Clear(). 
    /// </summary> 
    /// <param name="openingTag"> 
    /// The opening tag to search for. 
    /// </param> 
    /// <param name="closingTag"> 
    /// The closing tag to search for. 
    /// </param> 
    /// <param name="input"> 
    /// The input to search in. 
    /// </param> 
    private void FindAndWipe(char[] openingTag, char[] closingTag, char[] input) 
    { 
     int len = input.Length; 
     int pos = 0; 

     do 
     { 
      pos = FindNext(pos, openingTag, input); 

      if(pos < len) 
      { 
       var closenext = FindNext(pos, closingTag, input); 

       if(closenext < len) 
       { 
        m_deletionDictionary[pos - openingTag.Length] = closenext - (pos - openingTag.Length); 
        WipeRange(pos - openingTag.Length, closenext, input); 
       } 

       if(closenext > pos) 
       { 
        pos = closenext; 
       } 
       else 
       { 
        ++pos; 
       } 
      } 
     } 
     while(pos < len); 
    } 

    /// <summary> 
    /// Skips as many characters as possible within the input array until the given char is 
    /// found. The position of the first instance of the char is returned, or if not found, a 
    /// position beyond the end of the input array is returned. 
    /// </summary> 
    /// <param name="pos"> 
    /// The starting position to search from within the input array. 
    /// </param> 
    /// <param name="c"> 
    /// The character to find. 
    /// </param> 
    /// <param name="input"> 
    /// The input to search within. 
    /// </param> 
    /// <returns> 
    /// The position of the found character, or an index beyond the end of the input array. 
    /// </returns> 
    private int SkipUntil(int pos, char c, char[] input) 
    { 
     if(pos >= input.Length) 
     { 
      return pos; 
     } 

     do 
     { 
      if(input[pos] == c) 
      { 
       return pos; 
      } 

      ++pos; 
     } 
     while(pos < input.Length); 

     return pos; 
    } 

    /// <summary> 
    /// Clears a given range in the input array. 
    /// </summary> 
    /// <param name="start"> 
    /// The start position from which the array will begin to be cleared. 
    /// </param> 
    /// <param name="end"> 
    /// The end position in the array, the position to clear up-until. 
    /// </param> 
    /// <param name="input"> 
    /// The source array wherin the supplied range will be cleared. 
    /// </param> 
    /// <remarks> 
    /// Note that the second parameter is called end, not lenghth. This parameter is meant to be 
    /// a position in the array, not the amount of entries in the array to clear. 
    /// </remarks> 
    private void WipeRange(int start, int end, char[] input) 
    { 
     Array.Clear(input, start, end - start); 
    } 

    /// <summary> 
    /// Finds the next occurance of the supplied char array within the input array. This search 
    /// ignores whitespace. 
    /// </summary> 
    /// <param name="pos"> 
    /// The position to start searching from. 
    /// </param> 
    /// <param name="what"> 
    /// The sequence of characters to find. 
    /// </param> 
    /// <param name="input"> 
    /// The input array to perform the search on. 
    /// </param> 
    /// <returns> 
    /// The position of the end of the first matching occurance. That is, the returned position 
    /// points to the very end of the search criteria within the input array, not the start. If 
    /// no match could be found, a position beyond the end of the input array will be returned. 
    /// </returns> 
    public int FindNext(int pos, char[] what, char[] input) 
    { 
     do 
     { 
      if(Next(ref pos, what, input)) 
      { 
       return pos; 
      } 
      ++pos; 
     } 
     while(pos < input.Length); 

     return pos; 
    } 

    /// <summary> 
    /// Probes the input array at the given position to determine if the next N characters 
    /// matches the supplied character sequence. This check ignores whitespace. 
    /// </summary> 
    /// <param name="pos"> 
    /// The position at which to check within the input array for a match to the supplied 
    /// character sequence. 
    /// </param> 
    /// <param name="what"> 
    /// The character sequence to attempt to match. Note that whitespace between characters 
    /// within the input array is accebtale. 
    /// </param> 
    /// <param name="input"> 
    /// The input array to check within. 
    /// </param> 
    /// <returns> 
    /// True if the next N characters within the input array matches the supplied search 
    /// character sequence. Returns false otherwise. 
    /// </returns> 
    public bool Next(ref int pos, char[] what, char[] input) 
    { 
     int z = 0; 

     do 
     { 
      if(char.IsWhiteSpace(input[pos]) || input[pos] == '\0') 
      { 
       ++pos; 
       continue; 
      } 

      if(input[pos] == what[z]) 
      { 
       ++z; 
       ++pos; 
       continue; 
      } 

      return false; 
     } 
     while(pos < input.Length && z < what.Length); 

     return z == what.Length; 
    } 
} 

Odpowiednik w HtmlAgilityPack:

// Where m_whitespaceRegex is a Regex with [\s]. 
// Where sampleHtmlText is a raw HTML string. 

var extractedSampleText = new StringBuilder(); 
HtmlDocument doc = new HtmlDocument(); 
doc.LoadHtml(sampleHtmlText); 

if(doc != null && doc.DocumentNode != null) 
{ 
    foreach(var script in doc.DocumentNode.Descendants("script").ToArray()) 
    { 
     script.Remove(); 
    } 

    foreach(var style in doc.DocumentNode.Descendants("style").ToArray()) 
    { 
     style.Remove(); 
    } 

    var allTextNodes = doc.DocumentNode.SelectNodes("//text()"); 
    if(allTextNodes != null && allTextNodes.Count > 0) 
    { 
     foreach(HtmlNode node in allTextNodes) 
     { 
      extractedSampleText.Append(node.InnerText); 
     } 
    } 

    var finalText = m_whitespaceRegex.Replace(extractedSampleText.ToString(), " "); 
}