Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/link extraction #1

Merged
merged 3 commits into from Apr 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion build.sbt
Expand Up @@ -7,11 +7,13 @@ scalaVersion := "2.12.8"
lazy val akkaVersion = "2.5.21"
lazy val akkaHttpVersion = "10.1.8"
lazy val akkaStreamVersion = "2.5.21"
lazy val jSoupVersion = "1.11.3"

libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % akkaVersion,
"com.typesafe.akka" %% "akka-testkit" % akkaVersion,
"org.scalatest" %% "scalatest" % "3.0.5" % "test",
"com.typesafe.akka" %% "akka-http" % akkaHttpVersion,
"com.typesafe.akka" %% "akka-http" % akkaHttpVersion,
"com.typesafe.akka" %% "akka-stream" % akkaStreamVersion,
"org.jsoup" % "jsoup" % jSoupVersion
)
13 changes: 13 additions & 0 deletions src/main/scala/com/bsgenerator/extractor/LinkExtractor.scala
@@ -0,0 +1,13 @@
package com.bsgenerator.extractor

import org.jsoup.Jsoup

import scala.collection.JavaConverters._

class LinkExtractor() {
def extract(content: String, context: String): Set[String] = {
val document = Jsoup.parse(content, context)
val linkElements = document.getElementsByTag("a").asScala.toArray
linkElements.map(_.attr("abs:href")).toSet
}
}
@@ -0,0 +1,5 @@
package com.bsgenerator.extractor.article

trait ArticleExtractor {
def extract(content: String): Option[String]
}
@@ -0,0 +1,86 @@
package com.bsgenerator.extractor.article

import org.jsoup.Jsoup
import org.jsoup.nodes.Element

import scala.collection.JavaConverters._
import scala.collection.mutable

object HeuristicExtractor {

val unlikelyClassCandidates: Seq[String] = Seq("-ad-", "ai2html", "banner", "breadcrumbs", "combx", "comment", "community", "cover-wrap", "disqus", "extra", "foot", "gdpr", "header", "legends", "menu", "related", "remark", "replies", "rss", "shoutbox", "sidebar", "skyscraper", "social", "sponsor", "supplemental", "ad-break", "agegate", "pagination", "pager", "popup", "yom-remote")
zemiret marked this conversation as resolved.
Show resolved Hide resolved
val likelyClassCandidates: Seq[String] = Seq("article", "body", "column", "main")
val minCharTextLength = 100

val contentWrappers: Array[String] = Array("section", "div", "header", "img")

val scorers: Seq[String => Int] = Seq(
_.toCharArray.count(_ == ','),
e => Math.min(3, e.length / 100)
)

val cleaningLadies: Seq[Element => Unit] = Seq(
e => contentWrappers.map(e.getElementsByTag).filter(_.text().isBlank).foreach(_.remove)
)

}

class HeuristicExtractor extends ArticleExtractor {

import HeuristicExtractor._


private def cleanContent(el: Element): Element = {
cleaningLadies.foreach(_.apply(el))
el
}

private def scoreParagraph(paragraph: String): Int = scorers.map(_.apply(paragraph)).sum

private def scoreDivisionForLevel(level: Int): Int = {
if (level > 1) {
return level * 3
}
Math.max(1, level)
}

private def propagateScoring(paragraph: Element, scores: mutable.Map[Element, Int] = mutable.Map.empty): Unit = {
val score = scoreParagraph(paragraph.text())
paragraph.parents().asScala.zipWithIndex.foreach { case (p, i) =>
scores.update(p, scores.getOrElse(p, 0) + (score / scoreDivisionForLevel(i)))
}
}

private def canContainContent(el: Element): Boolean = {
(
(likelyClassCandidates.exists(el.className.contains(_)) || !unlikelyClassCandidates.exists(el.className.contains(_)))
&& el.text.trim.length > minCharTextLength)
}

private def getNonParagraphTextScoring(el: Element, depth: Int = 0): Int = {
if (depth == 3) return 0
val nonParagraphSiblings = el.siblingElements().asScala.filter(e => !(e.tagName().equalsIgnoreCase("p") || e.tagName().equalsIgnoreCase("pre")))

(el.textNodes().asScala.map(e => scoreParagraph(e.text())).sum + nonParagraphSiblings.map(e => {
getNonParagraphTextScoring(e, depth + 1)
}).sum) / scoreDivisionForLevel(depth - 1)
}

override def extract(content: String): Option[String] = {
val soup = Jsoup.parse(content)
if (!soup.select("p, pre").asScala.map(_.parent()).toSet.exists(canContainContent)) {
return null
}
val candidates = soup.select("p, pre").asScala.filter(canContainContent).map(cleanContent)
val candidateScores: mutable.Map[Element, Int] = mutable.Map.empty
candidates.foreach(e => propagateScoring(e, candidateScores))
candidateScores.map {
case (key, value) => value + getNonParagraphTextScoring(key)
}

candidateScores.toSeq.sortBy(_._2).lastOption match {
case None => Option.empty
case Some(e) => Option(e._1.wholeText().strip())
}
}
}
@@ -0,0 +1,3 @@
Ponad 377 tysięcy uczniów ma przystąpić w poniedziałek do egzaminu ósmoklasisty. Od godziny 9. uczniowie będą rozwiązywali zadania z języka polskiego.Ministerstwo Edukacji Narodowej Anna Zalewska zapewniła w piątek, że mimo trwającego od poniedziałku strajku nauczycieli egzamin się odbędzie. - Mamy jednoznaczne informacje od kuratorów i wojewodów, że jesteśmy do tych egzaminów przygotowani - mówiła Anna Zalewska.- Nie mamy informacji, aby gdziekolwiek w Polsce w czasie egzaminu ósmoklasisty miało dojść do problemów - stwierdził w niedzielę w rozmowie z RMF FM dyrektor Centralnej Komisji Egzaminacyjnej dr Marcin Smolik.
Egzamin ósmoklasisty ma zostać przeprowadzony po raz pierwszy. Rozłożony jest na trzy dni, w pierwszym uczniowie zmierzą się z częścią z języka polskiego, w drugim z matematyką, zaś trzeciego dnia odbędzie się egzamin z języka obcego. Niemal 95 proc. uczniów wybrało język angielski.W tym tygodniu (w środę, czwartek i piątek) odbywał się egzamin gimnazjalny. Z powodu niekompletnych składów zespołów nadzorujących egzamin z historii i wiedzy o społeczeństwie nie odbył się w sumie w trzech szkołach, z j. polskiego - w jednej.Rekrutacje do szkół ponadgimnazjalnychWiosną 2019 r. o przyjęcie do szkół średnich ubiegać się będą dwa roczniki uczniów: pierwszy rocznik uczących się w 8-letnich szkołach podstawowych i ostatni rocznik uczący się w gimnazjach. Będą dla nich przeprowadzone dwie odrębne rekrutacje.
Absolwenci gimnazjów i absolwenci 8-letniej szkoły podstawowej, choć uczyć się będą w tym samych szkołach, to w odrębnych klasach. Związane jest to z tym, że będą uczyć się według dwóch różnych podstaw programowych: absolwenci gimnazjów zgodnie z tą starą (dla nich nauka w liceach będzie 3-letnia, a w technikach 4-letnia), absolwenci 8-letniej szkoły podstawowej zgodnie z nową (dla nich nauka w liceach będzie 4-letnia, a w technikach 5-letnia).