Scalaで未来と俳優で英語を学ぶ

ここで私は英語を引き上げることにしました。 特に、語彙を大幅に増やしたいと思いました。 遊び心のある方法でこれを行うのに役立つプログラムがたくさんあることを知っています。 キャッチは、私はゲーミフィケーションが好きではないということです。 昔ながらの方法が好きです。 言葉、転写、翻訳を含む表である紙片。 そして彼に教えます。 そして、例えば、翻訳欄を閉じることにより、知識をテストします。 一般的に、私が大学で教えたように。



OxfordDictionaryサイトで選択された3000の最も頻繁に使用される単語があると聞きました。 この単語のリストは次のとおりです。印刷して学習できる形式ではなく、 その結果、すべてをプログラムするというアイデアが生まれました。 しかし、これを順次アルゴリズムとしてではなく、すべてを並列化するために。 すべての単語のポンピングと構文解析にかかる時間(3000単語* 2サイト)/ 60秒= 100分。 これは、ページをポンプアウトして解析し、翻訳とトランスクリプションを抽出するために1秒を与えた場合です(実際には、接続が開くまで、閉じている間など、3倍長いと思います)。



画像





タスクを一度に2つの大きなブロックに分割しました。 最初のブロックは、I / O操作のブロック-サイトからページをポンピングすることです。 2番目のブロックは、ブロックせずにCPUをロードする計算操作です。ページを解析して翻訳とトランスクリプションを抽出し、解析結果を辞書に追加します。



ScalaのFutureを使用して、スレッドプールでブロック操作を行うことにしました。 計算タスク、私はAkkaを3人の俳優に分散させることにしました。 TDD手法を適用して、私は最初に、将来のアプリケーション用に構成要素のテストを作成しました。



class Test extends FlatSpec with Matchers { "Table Of Content extractor" should "download and extract content from Oxford Site" in { val content:List[String] = OxfordSite.getTableOfContent content.size should be (10) content.find(_ == "AB") should be (Some("AB")) content.find(_ == "UZ") should be (Some("UZ")) } "Words list extractor" should "download words from page" in { val future: Future[Try[Option[List[String]]]] = OxfordSite.getWordsFromPage("AB", 1) val wordsTry:Try[Option[List[String]]] = Await.result(future,60 seconds) wordsTry should be a 'success val words = wordsTry.get words.get.find(_ == "abandon") should be (Some("abandon")) } "Words list extractor" should "return None from empty page" in { val future: Future[Try[Option[List[String]]]] = OxfordSite.getWordsFromPage("AB", 999) val wordsTry:Try[Option[List[String]]] = Await.result(future,60 seconds) wordsTry should be a 'success val words = wordsTry.get words should be(None) } "Russian Translation" should "download translation and parse" in { val page: Future[Try[String]] = LingvoSite.getPage("test") val pageResultTry: Try[String]= Await.result(page,60 seconds) pageResultTry should be a 'success val pageResult = pageResultTry.get pageResult.contains("") should be(true) LingvoSite.parseTranslation(pageResult).get should be("") } "English Translation" should "download translation and parse" in { val page: Future[Try[String]] = OxfordSite.getPage("test") val pageResultTry: Try[String] = Await.result(page,60 seconds) pageResultTry should be a 'success val pageResult = pageResultTry.get pageResult.contains("examination") should be(true) OxfordSite.parseTranslation(pageResult).get should be(("test", "an examination of somebody's knowledge or ability, consisting of questions for them to answer or activities for them to perform")) } }
      
      







注意してください。 計算結果を返すことができる関数には、Try [...]があります。 それらは、成功結果または失敗と実行のいずれかです。 頻繁に呼び出され、I / Oブロッキング操作を持つ関数は、Future [Try [...]]のような結果になります。 関数を呼び出すと、I / O操作が長いFutureがすぐに返されます。 さらに、それらはTry内に入り、エラーで終了する場合があります(たとえば、接続が切断されます)。



アプリケーション自体はTop3000WordsApp.scalaで初期化されます。 俳優のシステムは上昇しています。 アクターが作成されています。 単語リストの解析が開始されます。これにより、英語とロシア語のページの転写と翻訳が並行して開始されます。 ページのジャンプが成功した場合、ページのコンテンツは、解析、翻訳の抽出、トランスクリプションのアクターによってトリガーされます。 アクターは、翻訳の結果を最終的なアクター辞書に転送します。これにより、すべての結果が1か所に集約されます。 Enterキーを押すと、アクターシステムがシャットダウンします。 そして、これに関するシグナルを受信するDictionaryActorアクターは、組み立てられた辞書をdictionaty.txtファイルに保存します



 object Top3000WordsApp extends App { val system = ActorSystem("Top3000Words") val dictionatyActor = system.actorOf(Props[DictionaryActor], "dictionatyActor") val englishTranslationActor = system.actorOf(Props(classOf[EnglishTranslationActor], dictionatyActor), "englishTranslationActor") val russianTranslationActor = system.actorOf(Props(classOf[RussianTranslationActor], dictionatyActor), "russianTranslationActor") val mapGetPageThreadExecutionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(16)) val mapGetWordsThreadExecutionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(16)) start() scala.io.StdIn.readLine() system.terminate() def start() = { import concurrent.ExecutionContext.Implicits.global Future { OxfordSite.getTableOfContent.par.foreach(letterGroup => { getWords(letterGroup, 1) }) } } def getWords(letterGroup: String, pageNum: Int): Unit = { implicit val executor = mapGetWordsThreadExecutionContext OxfordSite.getWordsFromPage(letterGroup, pageNum).map(tryWords => { tryWords match { case Success(Some(words)) => words.par.foreach(word => { parse(word,letterGroup,pageNum) }) case Success(None) => Unit case Failure(ex) => println(ex.getMessage) } }) } def parse(word: String, letterGroup: String, pageNum: Int)= { implicit val executor = mapGetPageThreadExecutionContext OxfordSite.getPage(word).map(tryEnglishPage => { tryEnglishPage match { case Success(englishPage) => { englishTranslationActor ! (word, englishPage) getWords(letterGroup, pageNum + 1) } case Failure(ex) => println(ex.getMessage) } }) LingvoSite.getPage(word).map(_ match { case Success(russianPage) => { russianTranslationActor !(word, russianPage) } case Failure(ex) => println(ex.getMessage) }) } }
      
      







アルゴリズムは、start、getWords、parse関数に分かれていることに注意してください。 これは、タスクの各フェーズに独自のスレッドプールが必要であり、ThreadExecutionContextとして暗黙的に渡されるためです。 最初は、再帰呼び出しのためにgetWords関数が1つしかありませんでした。 しかし、アルゴリズムの最上位では並列化がスレッドのプール全体で消費され、最下部では動作するようにフリースレッドを提供するという永遠の期待があったため、すべてが非常に遅くなりました。 そして、一番下には操作の最大数があります。



以下は、サイトからのダウンロードと解析の実装です。



 object OxfordSite { val getPageThreadExecutionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(16)) def parseTranslation(content: String): Try[(String, String)] = { Try { val browser = new Browser val doc = browser.parseString(content) val spanElement: Element = doc >> element(".phon") val str = Jsoup.parse(spanElement.toString).text() val transcription = str.stripPrefix("BrE//").stripSuffix("//").trim val translation = doc >> text(".def") (transcription,translation) } } def getPage(word: String): Future[Try[String]] = { implicit val executor = getPageThreadExecutionContext Future { Try { val html = Source.fromURL("http://www.oxfordlearnersdictionaries.com/definition/english/" + (word.replace(' ','-')) + "_1") html.mkString } } } def getWordsFromPage(letterGroup: String, pageNum: Int): Future[Try[Option[List[String]]]] = { import ExecutionContext.Implicits.global Future { Try { val html = Source.fromURL("http://www.oxfordlearnersdictionaries.com" + "/wordlist/english/oxford3000/Oxford3000_" + letterGroup + "/?page=" + pageNum) val page = html.mkString val browser = new Browser val doc = browser.parseString(page) val ulElement: Element = doc >> element(".wordlist-oxford3000") val liElements: List[Element] = ulElement >> elementList("li") if (liElements.size > 0) Some(liElements.map(_ >> text("a"))) else None } } } def getTableOfContent: List[String] = { val html = Source.fromURL("http://www.oxfordlearnersdictionaries.com/wordlist/english/oxford3000/Oxford3000_A-B/") val page = html.mkString val browser = new Browser val doc = browser.parseString(page) val ulElement: Element = doc >> element(".hide_phone") val liElements: List[Element] = ulElement >> elementList("li") List(liElements.head >> text("span")) ++ liElements.tail.map(_ >> text("a")) } } object LingvoSite { val getPageThreadExecutionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(16)) def parseTranslation(content: String): Try[String] = { Try { val browser = new Browser val doc = browser.parseString(content) val spanElement: Element = doc >> element(".r_rs") spanElement >> text("a") } } def getPage(word: String): Future[Try[String]] = { implicit val executor = getPageThreadExecutionContext Future { Try { val html = Source.fromURL("http://www.translate.ru/dictionary/en-ru/" + java.net.URLEncoder.encode(word,"UTF-8")) html.mkString } } } }
      
      







アクターが使用するデータ構造。



 case class Word (word: String, transcription: Option[String] = None, russianTranslation:Option[String] = None, englishTranslation: Option[String] = None) case class RussianTranslation(word:String, translation: String) case class EnglishTranslation(word:String, translation: String) case class Transcription(word:String, transcription: String)
      
      







入り口で解析するためにダウンロードされたページを受け入れ、翻訳と転写をDictionaryActorアクターに転送するアクター



 class EnglishTranslationActor (dictionaryActor: ActorRef) extends Actor { println("EnglishTranslationActor") def receive = { case (word: String, englishPage: String) => { OxfordSite.parseTranslation(englishPage) match { case Success((transcription, translation)) => { dictionaryActor ! EnglishTranslation(word,translation) dictionaryActor ! Transcription(word,transcription) } case Failure(ex) => { println(ex.getMessage) } } } } } class RussianTranslationActor (dictionaryActor: ActorRef) extends Actor { println("RussianTranslationActor") def receive = { case (word: String, russianPage: String) => { LingvoSite.parseTranslation(russianPage) match { case Success(translation) => { dictionaryActor ! RussianTranslation(word, translation) } case Failure(ex) => { println(ex.getMessage) } } } } }
      
      







翻訳とトランスクリプションを含む辞書を蓄積し、俳優のシステムがシャットダウンした後、辞書全体をdictionary.txtに書き込む俳優



 class DictionaryActor extends Actor { println("DictionaryActor") override def postStop(): Unit = { println("DictionaryActor postStop") val fileText = DictionaryActor.words.map{case (_, someWord)=> { val transcription = someWord.transcription.getOrElse(" ") val russianTranslation = someWord.russianTranslation.getOrElse(" ") val englishTranslation = someWord.englishTranslation.getOrElse(" ") List(someWord.word, transcription , russianTranslation , englishTranslation).mkString("|") }}.mkString("\n") scala.tools.nsc.io.File("dictionary.txt").writeAll(fileText) println("dictionary.txt saved") System.exit(0) } def receive = { case Transcription(wordName, transcription) => { val newElement = DictionaryActor.words.get(wordName) match { case Some(word) => word.copy(transcription = Some(transcription)) case None => Word(wordName,transcription = Some(transcription)) } DictionaryActor.words += wordName -> newElement println(newElement) } case RussianTranslation(wordName, translation) => { val newElement = DictionaryActor.words.get(wordName) match { case Some(word) => word.copy(russianTranslation = Some(translation)) case None => Word(wordName,russianTranslation = Some(translation)) } DictionaryActor.words += wordName -> newElement println(newElement) } case EnglishTranslation(wordName, translation) => { val newElement = DictionaryActor.words.get(wordName) match { case Some(word) => word.copy(englishTranslation = Some(translation)) case None => Word(wordName,englishTranslation = Some(translation)) } DictionaryActor.words += wordName -> newElement println(newElement) } } } object DictionaryActor { var words = scala.collection.mutable.Map[String, Word]() }
      
      







調査結果は何ですか? 私のMac Book Proでは、この記事を書いている間、このスクリプトは約1時間実行されました。 Enterキーを押して中断しました。結果は次のとおりです。



 bash-3.2$ cat ./dictionary.txt |wc -l 1809
      
      







その後、スクリプトを再度実行し、数時間放置しました。 彼が戻ったとき、プロセッサに100%の負荷がかかり、コンソールにガベージコレクターに関するエラーがありました。Enterキーを押すと、プログラムがその作業の結果をファイルに保存できませんでした。 診断は、Futureとpar.mapまたはpar.foreachに書き込むことです。もちろん美しくて便利ですが、スレッドのレベルで実際にどのように機能するか、ボトルの細い首がどこにあるかを理解するのは非常に困難です。 最後に、俳優のすべてを書き直す予定です。 さらに、アクターのプールを使用します。 たとえば、4人のアクターが単語リストを含むページをポンプアウトおよび解析し、18人のアクターが翻訳を含むページをポンプアウトし、4人のアクターが翻訳と文字起こしを含むページを解析し、1人のアクターがすべてを辞書に入れます。



brunch v0.1での現在の実装github.com/evgenyigumnov/top3000words/tree/v0.1すべてがプールを持つアクターに書き換えられるバージョンは、brunch v0.2で、 まもなくマスターになります。 たぶん、誰かが私が現在のバージョンで間違っていたと思っているのでしょうか? さて、多分新しいバージョンのヒント?



利用可能なGithubプロジェクト: github.com/evgenyigumnov/top3000words



プロジェクトテストの実行:sbt test

アプリケーションの起動:sbt run

さて、どれだけ待つのがうんざりしているのか、Enterキーを押して、現在のフォルダー内のdictionary.txtの内容を把握します。



PS

その結果、彼は30スレッドで10分で解析する最終バージョンv0.2を作成しました。 github.com/evgenyigumnov/top3000words/tree/v0.2

作業の終了時に、Enterキーを押す必要はありません。 すべては俳優で行われます。 将来的には、重いI / Oブロッカーのみがラップされます。



All Articles