From 996755ef50562a6564ca4bd26702351f19980685 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Mon, 16 Dec 2013 21:42:11 +0100 Subject: [PATCH] extract images from article in Reader --- .../{ => Models}/PocketArticle.cs | 9 + .../Models/PocketArticleImage.cs | 36 ++++ PocketSharp.Reader/PocketReader.cs | 19 ++ PocketSharp.Reader/PocketSharp.Reader.csproj | 3 +- PocketSharp.Tests/ReadTests.cs | 18 ++ PocketSharp/PocketSharp.csproj | 1 - .../NReadability/NReadabilityTranscoder.cs | 199 +++++++++--------- 7 files changed, 184 insertions(+), 101 deletions(-) rename PocketSharp.Reader/{ => Models}/PocketArticle.cs (76%) create mode 100644 PocketSharp.Reader/Models/PocketArticleImage.cs diff --git a/PocketSharp.Reader/PocketArticle.cs b/PocketSharp.Reader/Models/PocketArticle.cs similarity index 76% rename from PocketSharp.Reader/PocketArticle.cs rename to PocketSharp.Reader/Models/PocketArticle.cs index 90716c8..8d19aa4 100644 --- a/PocketSharp.Reader/PocketArticle.cs +++ b/PocketSharp.Reader/Models/PocketArticle.cs @@ -1,5 +1,6 @@ using PropertyChanged; using System; +using System.Collections.Generic; namespace PocketSharp.Models { @@ -17,6 +18,14 @@ namespace PocketSharp.Models /// public string Content { get; set; } + /// + /// Gets or sets the images. + /// + /// + /// The images. + /// + public List Images { get; set; } + /// /// Gets or sets the title. /// diff --git a/PocketSharp.Reader/Models/PocketArticleImage.cs b/PocketSharp.Reader/Models/PocketArticleImage.cs new file mode 100644 index 0000000..fcaff25 --- /dev/null +++ b/PocketSharp.Reader/Models/PocketArticleImage.cs @@ -0,0 +1,36 @@ +using PropertyChanged; +using System; + +namespace PocketSharp.Models +{ + /// + /// Article image + /// + [ImplementPropertyChanged] + public class PocketArticleImage + { + /// + /// Gets or sets the URI. + /// + /// + /// The URI. + /// + public Uri Uri { get; set; } + + /// + /// Gets or sets the title. + /// + /// + /// The title. + /// + public string Title { get; set; } + + /// + /// Gets or sets the alternative text. + /// + /// + /// The alternative text. + /// + public string AlternativeText { get; set; } + } +} diff --git a/PocketSharp.Reader/PocketReader.cs b/PocketSharp.Reader/PocketReader.cs index a1f2e1b..b8bff56 100644 --- a/PocketSharp.Reader/PocketReader.cs +++ b/PocketSharp.Reader/PocketReader.cs @@ -1,9 +1,12 @@ using PocketSharp.Models; using PocketSharp.Ports.NReadability; using System; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using System.Xml.Linq; namespace PocketSharp { @@ -107,9 +110,25 @@ namespace PocketSharp // process/transcode HTML TranscodingResult transcodingResult = transcoder.Transcode(transcodingInput); + // get images from article + List images = transcodingResult.RawDocument.GetElementsByTagName("img").Select(image => + { + Uri imageUri; + Uri.TryCreate(image.GetAttributeValue("src", null), UriKind.Absolute, out imageUri); + + return new PocketArticleImage() + { + Uri = imageUri, + Title = image.GetAttributeValue("title", null), + AlternativeText = image.GetAttributeValue("alt", null) + }; + }).ToList(); + + // create article return new PocketArticle() { Content = transcodingResult.ExtractedContent, + Images = images, Title = transcodingResult.ExtractedTitle, NextPage = transcodingResult.NextPageUrl != null ? new Uri(transcodingResult.NextPageUrl, UriKind.Absolute) : null }; diff --git a/PocketSharp.Reader/PocketSharp.Reader.csproj b/PocketSharp.Reader/PocketSharp.Reader.csproj index 611e575..254c0b0 100644 --- a/PocketSharp.Reader/PocketSharp.Reader.csproj +++ b/PocketSharp.Reader/PocketSharp.Reader.csproj @@ -42,7 +42,8 @@ - + + diff --git a/PocketSharp.Tests/ReadTests.cs b/PocketSharp.Tests/ReadTests.cs index cdfa158..a6b1e66 100644 --- a/PocketSharp.Tests/ReadTests.cs +++ b/PocketSharp.Tests/ReadTests.cs @@ -39,6 +39,24 @@ namespace PocketSharp.Tests } + [Fact] + public async Task ReadArticleWithImagesTest() + { + PocketArticle result = await reader.Read(new Uri("https://hacks.mozilla.org/2013/12/application-layout-with-css3-flexible-box-module/")); + Assert.True(result.Images.Count >= 3); + Assert.True(result.Images[0].Uri.ToString().StartsWith("https://hacks.mozilla.org")); + Assert.True(result.Images[1].Uri.ToString().EndsWith(".gif")); + } + + + [Fact] + public async Task ReadArticleWithNoImagesTest() + { + PocketArticle result = await reader.Read(new Uri("http://getpocket.com/hits/awards/2013/")); + Assert.True(result.Images == null || result.Images.Count < 1); + } + + [Fact] public async Task ReadArticleWithInvalidUriTest() { diff --git a/PocketSharp/PocketSharp.csproj b/PocketSharp/PocketSharp.csproj index 09d1d04..e0d02d2 100644 --- a/PocketSharp/PocketSharp.csproj +++ b/PocketSharp/PocketSharp.csproj @@ -44,7 +44,6 @@ - diff --git a/PortablePorts/NReadability/NReadabilityTranscoder.cs b/PortablePorts/NReadability/NReadabilityTranscoder.cs index 734d1eb..19a6924 100644 --- a/PortablePorts/NReadability/NReadabilityTranscoder.cs +++ b/PortablePorts/NReadability/NReadabilityTranscoder.cs @@ -22,9 +22,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using System.Reflection; using System.Xml; using System.Xml.Linq; @@ -36,15 +36,15 @@ namespace PocketSharp.Ports.NReadability public class NReadabilityTranscoder { #region Nested types - + /// /// Used in FindNextPageLink /// private class LinkData { - public float Score; - public string LinkText; - public string LinkHref; + public float Score; + public string LinkText; + public string LinkHref; } #endregion @@ -102,7 +102,7 @@ namespace PocketSharp.Ports.NReadability private static readonly Regex _UnlikelyCandidatesRegex = new Regex("combx|comment|community|disqus|extra|foot|header|menu|remark|rss|shoutbox|sidebar|side|sponsor|ad-break|agegate|pagination|pager|popup|tweet|twitter", RegexOptions.IgnoreCase); private static readonly Regex _OkMaybeItsACandidateRegex = new Regex("and|article|body|column|main|shadow", RegexOptions.IgnoreCase); - private static readonly Regex _PositiveWeightRegex = new Regex("article|body|content|entry|hentry|main|page|pagination|post|text|blog|story", RegexOptions.IgnoreCase); + private static readonly Regex _PositiveWeightRegex = new Regex("article|body|content|entry|hentry|main|page|pagination|post|text|blog|story", RegexOptions.IgnoreCase); private static readonly Regex _NegativeWeightRegex = new Regex("combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|shoutbox|sidebar|side|sponsor|shopping|tags|tool|widget", RegexOptions.IgnoreCase); private static readonly Regex _NegativeLinkParentRegex = new Regex("(stories|articles|news|documents|posts|notes|series|historie|artykuly|artykuły|wpisy|dokumenty|serie|geschichten|erzählungen|erzahlungen)", RegexOptions.IgnoreCase); private static readonly Regex _Extraneous = new Regex("print|archive|comment|discuss|e[-]?mail|share|reply|all|login|sign|single|also", RegexOptions.IgnoreCase); @@ -118,7 +118,7 @@ namespace PocketSharp.Ports.NReadability private static readonly Regex _ArticleTitleDashRegex2 = new Regex("(.*)[\\|\\-] .*"); private static readonly Regex _ArticleTitleDashRegex3 = new Regex("[^\\|\\-]*[\\|\\-](.*)"); private static readonly Regex _ArticleTitleColonRegex1 = new Regex(".*:(.*)"); - private static readonly Regex _ArticleTitleColonRegex2 = new Regex("[^:]*[:](.*)"); + private static readonly Regex _ArticleTitleColonRegex2 = new Regex("[^:]*[:](.*)"); private static readonly Regex _NextLink = new Regex(@"(next|weiter|continue|dalej|następna|nastepna>([^\|]|$)|�([^\|]|$))", RegexOptions.IgnoreCase); private static readonly Regex _NextStoryLink = new Regex("(story|article|news|document|post|note|series|historia|artykul|artykuł|wpis|dokument|seria|geschichte|erzählung|erzahlung|artikel|serie)", RegexOptions.IgnoreCase); private static readonly Regex _PrevLink = new Regex("(prev|earl|[^b]old|new|wstecz|poprzednia|<|�)", RegexOptions.IgnoreCase); @@ -148,7 +148,7 @@ namespace PocketSharp.Ports.NReadability private readonly ReadingStyle _readingStyle; private readonly ReadingSize _readingSize; private readonly ReadingMargin _readingMargin; - + #endregion #region Helper instance fields @@ -190,7 +190,7 @@ namespace PocketSharp.Ports.NReadability _sgmlDomBuilder = new SgmlDomBuilder(); _sgmlDomSerializer = new SgmlDomSerializer(); - _elementsScores = new Dictionary(); + _elementsScores = new Dictionary(); } /// @@ -374,7 +374,7 @@ namespace PocketSharp.Ports.NReadability _dontStripUnlikelys = true; return TranscodeToXml(htmlContent, url, out mainContentExtracted, out extractedTitle, out nextPageUrl); - } + } finally { _dontStripUnlikelys = false; @@ -444,7 +444,7 @@ namespace PocketSharp.Ports.NReadability /* If the leftovers of the URL after removing the base URL don't contain any digits, it's certainly not a next page link. */ string linkHrefLeftover = linkHref.Replace(articleBaseUrl, ""); - + if (!Regex.IsMatch(linkHrefLeftover, @"\d")) { continue; @@ -453,7 +453,7 @@ namespace PocketSharp.Ports.NReadability if (!possiblePagesByLink.Keys.Contains(linkHref)) { possiblePagesByLink[linkHref] = new LinkData { Score = 0, LinkHref = linkHref, LinkText = linkText }; - } + } else { possiblePagesByLink[linkHref].LinkText += " | " + linkText; @@ -541,7 +541,7 @@ namespace PocketSharp.Ports.NReadability linkObj.Score += 12.5f; positiveDescendantMatch = true; } - + if (!negativeDescendantMatch && _PrevLink.IsMatch(descendantData)) { linkObj.Score -= 100; @@ -604,7 +604,7 @@ namespace PocketSharp.Ports.NReadability if (topPage != null) { - string nextHref = Regex.Replace(topPage.LinkHref, @"\/$", ""); + string nextHref = Regex.Replace(topPage.LinkHref, @"\/$", ""); var nextHrefUri = new Uri(new Uri(articleBaseUrl), nextHref); return nextHrefUri.OriginalString; @@ -612,7 +612,7 @@ namespace PocketSharp.Ports.NReadability return null; } - + /// /// Find a cleaned up version of the current URL, to use for comparing links for possible next-pageyness. /// @@ -626,13 +626,13 @@ namespace PocketSharp.Ports.NReadability } string protocol = urlUri.Scheme; - string hostname = urlUri.Host; + string hostname = urlUri.Host; string noUrlParams = urlUri.AbsolutePath + "/"; List urlSlashes = noUrlParams.Split('/').Reverse().ToList(); var cleanedSegments = new List(); int slashLen = urlSlashes.Count(); - for (int i = 0; i < slashLen; i++) + for (int i = 0; i < slashLen; i++) { string segment = urlSlashes[i]; @@ -656,10 +656,10 @@ namespace PocketSharp.Ports.NReadability { segment = segment.Replace(",00", ""); } - + /* If our first or second segment has anything looking like a page number, remove it. */ var pageNumRegex = new Regex("((_|-)?p[a-z]*|(_|-))[0-9]{1,2}$", RegexOptions.IgnoreCase); - + if (pageNumRegex.IsMatch(segment) && ((i == 1) || (i == 0))) { segment = pageNumRegex.Replace(segment, ""); @@ -752,7 +752,7 @@ namespace PocketSharp.Ports.NReadability /* Remove all anchors. */ elementsToRemove.Clear(); - + IEnumerable anchorElements = rootElement.GetElementsByTagName("a") .Where(aElement => aElement.Attribute("name") != null && aElement.Attribute("href") == null); @@ -929,63 +929,63 @@ namespace PocketSharp.Ports.NReadability new ElementsTraverser( element => + { + string elementName = GetElementName(element); + + /* Remove unlikely candidates. */ + string unlikelyMatchString = element.GetClass() + " " + element.GetId(); + + if (unlikelyMatchString.Length > 0 + && !"body".Equals(elementName, StringComparison.OrdinalIgnoreCase) + && !"a".Equals(elementName, StringComparison.OrdinalIgnoreCase) + && _UnlikelyCandidatesRegex.IsMatch(unlikelyMatchString) + && !_OkMaybeItsACandidateRegex.IsMatch(unlikelyMatchString)) { - string elementName = GetElementName(element); + XElement parentElement = element.Parent; - /* Remove unlikely candidates. */ - string unlikelyMatchString = element.GetClass() + " " + element.GetId(); - - if (unlikelyMatchString.Length > 0 - && !"body".Equals(elementName, StringComparison.OrdinalIgnoreCase) - && !"a".Equals(elementName, StringComparison.OrdinalIgnoreCase) - && _UnlikelyCandidatesRegex.IsMatch(unlikelyMatchString) - && !_OkMaybeItsACandidateRegex.IsMatch(unlikelyMatchString)) + if (parentElement != null) { - XElement parentElement = element.Parent; - - if (parentElement != null) - { - element.Remove(); - } - - // element has been removed - we can go to the next one - return; + element.Remove(); } - /* Turn all divs that don't have children block level elements into p's or replace text nodes within the div with p's. */ - if ("div".Equals(elementName, StringComparison.OrdinalIgnoreCase)) + // element has been removed - we can go to the next one + return; + } + + /* Turn all divs that don't have children block level elements into p's or replace text nodes within the div with p's. */ + if ("div".Equals(elementName, StringComparison.OrdinalIgnoreCase)) + { + if (!_DivToPElementsRegex.IsMatch(element.GetInnerHtml())) { - if (!_DivToPElementsRegex.IsMatch(element.GetInnerHtml())) - { - // no block elements inside - change to p - SetElementName(element, "p"); - } - else - { - // replace text nodes with p's (experimental) - new ChildNodesTraverser( - childNode => - { - if (childNode.NodeType != XmlNodeType.Text - || GetInnerText(childNode).Length == 0) - { - return; - } - - XElement paraElement = new XElement("p"); - - // note that we're not using GetInnerText() here; instead we're getting raw InnerText to preserve whitespaces - paraElement.SetInnerHtml(((XText)childNode).Value); - - paraElement.SetClass(ReadabilityStyledCssClass); - paraElement.SetStyle("display: inline;"); - - childNode.ReplaceWith(paraElement); - } - ).Traverse(element); - } + // no block elements inside - change to p + SetElementName(element, "p"); } - }).Traverse(rootElement); + else + { + // replace text nodes with p's (experimental) + new ChildNodesTraverser( + childNode => + { + if (childNode.NodeType != XmlNodeType.Text + || GetInnerText(childNode).Length == 0) + { + return; + } + + XElement paraElement = new XElement("p"); + + // note that we're not using GetInnerText() here; instead we're getting raw InnerText to preserve whitespaces + paraElement.SetInnerHtml(((XText)childNode).Value); + + paraElement.SetClass(ReadabilityStyledCssClass); + paraElement.SetStyle("display: inline;"); + + childNode.ReplaceWith(paraElement); + } + ).Traverse(element); + } + } + }).Traverse(rootElement); } internal void CollapseRedundantParagraphDivs(XDocument document) @@ -994,31 +994,31 @@ namespace PocketSharp.Ports.NReadability new ElementsTraverser( element => + { + string elementName = GetElementName(element); + + if ("div".Equals(elementName, StringComparison.OrdinalIgnoreCase)) { - string elementName = GetElementName(element); + XNode childNode = element.Nodes().SingleOrNone(); - if ("div".Equals(elementName, StringComparison.OrdinalIgnoreCase)) + if (childNode != null) { - XNode childNode = element.Nodes().SingleOrNone(); + XElement childElement = childNode as XElement; - if (childNode != null) + if (childElement != null && "p".Equals(GetElementName(childElement), StringComparison.OrdinalIgnoreCase)) { - XElement childElement = childNode as XElement; + // we have a div with a single child element that is a paragraph - let's remove the div and attach the paragraph to the div's parent + XElement parentElement = element.Parent; - if (childElement != null && "p".Equals(GetElementName(childElement), StringComparison.OrdinalIgnoreCase)) + if (parentElement != null) { - // we have a div with a single child element that is a paragraph - let's remove the div and attach the paragraph to the div's parent - XElement parentElement = element.Parent; - - if (parentElement != null) - { - element.AddBeforeSelf(childElement); - element.Remove(); - } + element.AddBeforeSelf(childElement); + element.Remove(); } } } - }).Traverse(rootElement); + } + }).Traverse(rootElement); } internal IEnumerable FindCandidatesForArticleContent(XDocument document, string articleContentElementHint = null) @@ -1137,7 +1137,7 @@ namespace PocketSharp.Ports.NReadability Math.Max( _MaxSiblingScoreTreshold, _SiblingScoreTresholdCoefficient * topCandidateElementScore); - + string topCandidateClass = topCandidateElement.GetClass(); // iterate through the sibling elements and decide whether append them @@ -1224,7 +1224,8 @@ namespace PocketSharp.Ports.NReadability Clean(articleContentElement, "form"); Clean(articleContentElement, "object"); - if (articleContentElement.GetElementsByTagName("h1").Count() == 1) { + if (articleContentElement.GetElementsByTagName("h1").Count() == 1) + { Clean(articleContentElement, "h1"); } @@ -1516,17 +1517,17 @@ namespace PocketSharp.Ports.NReadability { new ElementsTraverser( element => + { + string elementClass = element.GetClass(); + + if (elementClass.Contains(ReadabilityStyledCssClass)) { - string elementClass = element.GetClass(); + // don't remove the style if that's we who have styled this element + return; + } - if (elementClass.Contains(ReadabilityStyledCssClass)) - { - // don't remove the style if that's we who have styled this element - return; - } - - element.SetStyle(null); - }).Traverse(rootElement); + element.SetStyle(null); + }).Traverse(rootElement); } internal string GetUserStyleClass(string prefix, String enumStr) @@ -1650,7 +1651,7 @@ namespace PocketSharp.Ports.NReadability return url; } - Uri baseUri; + Uri baseUri; if (!Uri.TryCreate(articleUrl, UriKind.Absolute, out baseUri)) { @@ -1659,8 +1660,8 @@ namespace PocketSharp.Ports.NReadability /* If the link is simply a query string, then simply attach it to the original URL */ if (url.StartsWith("?")) - { - return baseUri.Scheme + "://" + baseUri.Host + baseUri.AbsolutePath + url; + { + return baseUri.Scheme + "://" + baseUri.Host + baseUri.AbsolutePath + url; } Uri absoluteUri;