From a52792f019da9b32c3bf67a09bb9b8760ee400d7 Mon Sep 17 00:00:00 2001 From: Chengwuyi <3394813085@qq.com> Date: Sat, 30 May 2026 21:44:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E9=A1=B9=E7=9B=AE=E6=BA=90?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | Bin 0 -> 40 bytes .vscode/launch.json | 45 ++ pom.xml | 132 +++++ result.jsonl | 20 + src/main/java/com/ski/crawler/Main.java | 40 ++ .../java/com/ski/crawler/command/Command.java | 10 + .../com/ski/crawler/command/CrawlCommand.java | 253 ++++++++++ .../ski/crawler/command/ExportCommand.java | 77 +++ .../ski/crawler/command/FilterCommand.java | 18 + .../com/ski/crawler/command/HelpCommand.java | 38 ++ .../com/ski/crawler/command/ListCommand.java | 53 ++ .../ski/crawler/command/ResumeCommand.java | 140 ++++++ .../com/ski/crawler/command/SitesCommand.java | 19 + .../com/ski/crawler/command/StatsCommand.java | 66 +++ .../crawler/controller/CrawlerContext.java | 30 ++ .../crawler/controller/CrawlerController.java | 72 +++ .../crawler/exception/CrawlerException.java | 12 + .../crawler/exception/NetworkException.java | 12 + .../ski/crawler/exception/ParseException.java | 12 + .../ski/crawler/factory/StrategyFactory.java | 31 ++ .../java/com/ski/crawler/model/SkiLift.java | 83 +++ .../java/com/ski/crawler/model/SkiResort.java | 252 ++++++++++ .../java/com/ski/crawler/model/SkiReview.java | 76 +++ .../java/com/ski/crawler/model/SkiTicket.java | 74 +++ .../java/com/ski/crawler/model/SkiTrail.java | 92 ++++ .../crawler/parser/ResortDetailParser.java | 471 ++++++++++++++++++ .../com/ski/crawler/parser/ResortParser.java | 34 ++ .../repository/SkiResortRepository.java | 74 +++ .../ski/crawler/service/ScraperService.java | 243 +++++++++ .../com/ski/crawler/site/CrawlerSite.java | 22 + .../com/ski/crawler/site/SkimapOrgSite.java | 194 ++++++++ .../ski/crawler/site/SkiresortInfoSite.java | 33 ++ .../com/ski/crawler/site/WikipediaSite.java | 204 ++++++++ .../ski/crawler/spider/ResortListSpider.java | 98 ++++ .../ski/crawler/strategy/CrawlStrategy.java | 19 + .../strategy/SkiResortInfoStrategy.java | 81 +++ .../ski/crawler/strategy/SkimapStrategy.java | 199 ++++++++ .../crawler/strategy/WikipediaStrategy.java | 244 +++++++++ .../java/com/ski/crawler/util/CliArgs.java | 74 +++ .../java/com/ski/crawler/util/ExcelUtil.java | 179 +++++++ .../java/com/ski/crawler/util/JsonUtil.java | 43 ++ .../java/com/ski/crawler/util/RetryUtil.java | 33 ++ .../com/ski/crawler/util/ValidationUtil.java | 60 +++ .../com/ski/crawler/utils/CrawlerHttp.java | 52 ++ .../com/ski/crawler/utils/HttpClientUtil.java | 27 + .../com/ski/crawler/view/ConsoleView.java | 336 +++++++++++++ src/main/resources/logback.xml | 13 + target/classes/com/ski/crawler/Main.class | Bin 0 -> 2635 bytes .../com/ski/crawler/command/Command.class | Bin 0 -> 291 bytes .../ski/crawler/command/CrawlCommand.class | Bin 0 -> 13648 bytes .../ski/crawler/command/ExportCommand.class | Bin 0 -> 5764 bytes .../ski/crawler/command/FilterCommand.class | Bin 0 -> 849 bytes .../com/ski/crawler/command/HelpCommand.class | Bin 0 -> 2244 bytes .../com/ski/crawler/command/ListCommand.class | Bin 0 -> 2979 bytes .../ski/crawler/command/ResumeCommand$1.class | Bin 0 -> 793 bytes .../ski/crawler/command/ResumeCommand.class | Bin 0 -> 6949 bytes .../ski/crawler/command/SitesCommand.class | Bin 0 -> 1042 bytes .../ski/crawler/command/StatsCommand.class | Bin 0 -> 4718 bytes .../crawler/controller/CrawlerContext.class | Bin 0 -> 1107 bytes .../controller/CrawlerController.class | Bin 0 -> 2595 bytes .../crawler/exception/CrawlerException.class | Bin 0 -> 566 bytes .../crawler/exception/NetworkException.class | Bin 0 -> 589 bytes .../crawler/exception/ParseException.class | Bin 0 -> 583 bytes .../ski/crawler/factory/StrategyFactory.class | Bin 0 -> 1729 bytes .../com/ski/crawler/model/SkiLift.class | Bin 0 -> 2081 bytes .../com/ski/crawler/model/SkiResort.class | Bin 0 -> 6689 bytes .../com/ski/crawler/model/SkiReview.class | Bin 0 -> 2065 bytes .../com/ski/crawler/model/SkiTicket.class | Bin 0 -> 1934 bytes .../com/ski/crawler/model/SkiTrail.class | Bin 0 -> 2420 bytes .../parser/ResortDetailParser$Price.class | Bin 0 -> 624 bytes .../crawler/parser/ResortDetailParser.class | Bin 0 -> 15117 bytes .../com/ski/crawler/parser/ResortParser.class | Bin 0 -> 1681 bytes .../repository/SkiResortRepository.class | Bin 0 -> 4858 bytes .../service/ScraperService$CrawlReport.class | Bin 0 -> 775 bytes .../ski/crawler/service/ScraperService.class | Bin 0 -> 15891 bytes .../com/ski/crawler/site/CrawlerSite.class | Bin 0 -> 558 bytes .../com/ski/crawler/site/SkimapOrgSite.class | Bin 0 -> 7185 bytes .../ski/crawler/site/SkiresortInfoSite.class | Bin 0 -> 1816 bytes .../com/ski/crawler/site/WikipediaSite.class | Bin 0 -> 7149 bytes .../ski/crawler/spider/ResortListSpider.class | Bin 0 -> 3965 bytes .../ski/crawler/strategy/CrawlStrategy.class | Bin 0 -> 635 bytes .../strategy/SkiResortInfoStrategy.class | Bin 0 -> 4359 bytes .../ski/crawler/strategy/SkimapStrategy.class | Bin 0 -> 8010 bytes .../crawler/strategy/WikipediaStrategy.class | Bin 0 -> 9152 bytes .../com/ski/crawler/util/CliArgs.class | Bin 0 -> 2605 bytes .../com/ski/crawler/util/ExcelUtil.class | Bin 0 -> 8630 bytes .../com/ski/crawler/util/JsonUtil.class | Bin 0 -> 2375 bytes .../com/ski/crawler/util/RetryUtil.class | Bin 0 -> 1603 bytes .../com/ski/crawler/util/ValidationUtil.class | Bin 0 -> 2359 bytes .../com/ski/crawler/utils/CrawlerHttp.class | Bin 0 -> 2333 bytes .../ski/crawler/utils/HttpClientUtil.class | Bin 0 -> 2031 bytes .../ski/crawler/view/ConsoleView$Ansi.class | Bin 0 -> 1097 bytes .../ski/crawler/view/ConsoleView$Col.class | Bin 0 -> 624 bytes .../view/ConsoleView$TablePrinter.class | Bin 0 -> 7816 bytes .../com/ski/crawler/view/ConsoleView.class | Bin 0 -> 5568 bytes target/classes/logback.xml | 13 + 96 files changed, 4403 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 pom.xml create mode 100644 result.jsonl create mode 100644 src/main/java/com/ski/crawler/Main.java create mode 100644 src/main/java/com/ski/crawler/command/Command.java create mode 100644 src/main/java/com/ski/crawler/command/CrawlCommand.java create mode 100644 src/main/java/com/ski/crawler/command/ExportCommand.java create mode 100644 src/main/java/com/ski/crawler/command/FilterCommand.java create mode 100644 src/main/java/com/ski/crawler/command/HelpCommand.java create mode 100644 src/main/java/com/ski/crawler/command/ListCommand.java create mode 100644 src/main/java/com/ski/crawler/command/ResumeCommand.java create mode 100644 src/main/java/com/ski/crawler/command/SitesCommand.java create mode 100644 src/main/java/com/ski/crawler/command/StatsCommand.java create mode 100644 src/main/java/com/ski/crawler/controller/CrawlerContext.java create mode 100644 src/main/java/com/ski/crawler/controller/CrawlerController.java create mode 100644 src/main/java/com/ski/crawler/exception/CrawlerException.java create mode 100644 src/main/java/com/ski/crawler/exception/NetworkException.java create mode 100644 src/main/java/com/ski/crawler/exception/ParseException.java create mode 100644 src/main/java/com/ski/crawler/factory/StrategyFactory.java create mode 100644 src/main/java/com/ski/crawler/model/SkiLift.java create mode 100644 src/main/java/com/ski/crawler/model/SkiResort.java create mode 100644 src/main/java/com/ski/crawler/model/SkiReview.java create mode 100644 src/main/java/com/ski/crawler/model/SkiTicket.java create mode 100644 src/main/java/com/ski/crawler/model/SkiTrail.java create mode 100644 src/main/java/com/ski/crawler/parser/ResortDetailParser.java create mode 100644 src/main/java/com/ski/crawler/parser/ResortParser.java create mode 100644 src/main/java/com/ski/crawler/repository/SkiResortRepository.java create mode 100644 src/main/java/com/ski/crawler/service/ScraperService.java create mode 100644 src/main/java/com/ski/crawler/site/CrawlerSite.java create mode 100644 src/main/java/com/ski/crawler/site/SkimapOrgSite.java create mode 100644 src/main/java/com/ski/crawler/site/SkiresortInfoSite.java create mode 100644 src/main/java/com/ski/crawler/site/WikipediaSite.java create mode 100644 src/main/java/com/ski/crawler/spider/ResortListSpider.java create mode 100644 src/main/java/com/ski/crawler/strategy/CrawlStrategy.java create mode 100644 src/main/java/com/ski/crawler/strategy/SkiResortInfoStrategy.java create mode 100644 src/main/java/com/ski/crawler/strategy/SkimapStrategy.java create mode 100644 src/main/java/com/ski/crawler/strategy/WikipediaStrategy.java create mode 100644 src/main/java/com/ski/crawler/util/CliArgs.java create mode 100644 src/main/java/com/ski/crawler/util/ExcelUtil.java create mode 100644 src/main/java/com/ski/crawler/util/JsonUtil.java create mode 100644 src/main/java/com/ski/crawler/util/RetryUtil.java create mode 100644 src/main/java/com/ski/crawler/util/ValidationUtil.java create mode 100644 src/main/java/com/ski/crawler/utils/CrawlerHttp.java create mode 100644 src/main/java/com/ski/crawler/utils/HttpClientUtil.java create mode 100644 src/main/java/com/ski/crawler/view/ConsoleView.java create mode 100644 src/main/resources/logback.xml create mode 100644 target/classes/com/ski/crawler/Main.class create mode 100644 target/classes/com/ski/crawler/command/Command.class create mode 100644 target/classes/com/ski/crawler/command/CrawlCommand.class create mode 100644 target/classes/com/ski/crawler/command/ExportCommand.class create mode 100644 target/classes/com/ski/crawler/command/FilterCommand.class create mode 100644 target/classes/com/ski/crawler/command/HelpCommand.class create mode 100644 target/classes/com/ski/crawler/command/ListCommand.class create mode 100644 target/classes/com/ski/crawler/command/ResumeCommand$1.class create mode 100644 target/classes/com/ski/crawler/command/ResumeCommand.class create mode 100644 target/classes/com/ski/crawler/command/SitesCommand.class create mode 100644 target/classes/com/ski/crawler/command/StatsCommand.class create mode 100644 target/classes/com/ski/crawler/controller/CrawlerContext.class create mode 100644 target/classes/com/ski/crawler/controller/CrawlerController.class create mode 100644 target/classes/com/ski/crawler/exception/CrawlerException.class create mode 100644 target/classes/com/ski/crawler/exception/NetworkException.class create mode 100644 target/classes/com/ski/crawler/exception/ParseException.class create mode 100644 target/classes/com/ski/crawler/factory/StrategyFactory.class create mode 100644 target/classes/com/ski/crawler/model/SkiLift.class create mode 100644 target/classes/com/ski/crawler/model/SkiResort.class create mode 100644 target/classes/com/ski/crawler/model/SkiReview.class create mode 100644 target/classes/com/ski/crawler/model/SkiTicket.class create mode 100644 target/classes/com/ski/crawler/model/SkiTrail.class create mode 100644 target/classes/com/ski/crawler/parser/ResortDetailParser$Price.class create mode 100644 target/classes/com/ski/crawler/parser/ResortDetailParser.class create mode 100644 target/classes/com/ski/crawler/parser/ResortParser.class create mode 100644 target/classes/com/ski/crawler/repository/SkiResortRepository.class create mode 100644 target/classes/com/ski/crawler/service/ScraperService$CrawlReport.class create mode 100644 target/classes/com/ski/crawler/service/ScraperService.class create mode 100644 target/classes/com/ski/crawler/site/CrawlerSite.class create mode 100644 target/classes/com/ski/crawler/site/SkimapOrgSite.class create mode 100644 target/classes/com/ski/crawler/site/SkiresortInfoSite.class create mode 100644 target/classes/com/ski/crawler/site/WikipediaSite.class create mode 100644 target/classes/com/ski/crawler/spider/ResortListSpider.class create mode 100644 target/classes/com/ski/crawler/strategy/CrawlStrategy.class create mode 100644 target/classes/com/ski/crawler/strategy/SkiResortInfoStrategy.class create mode 100644 target/classes/com/ski/crawler/strategy/SkimapStrategy.class create mode 100644 target/classes/com/ski/crawler/strategy/WikipediaStrategy.class create mode 100644 target/classes/com/ski/crawler/util/CliArgs.class create mode 100644 target/classes/com/ski/crawler/util/ExcelUtil.class create mode 100644 target/classes/com/ski/crawler/util/JsonUtil.class create mode 100644 target/classes/com/ski/crawler/util/RetryUtil.class create mode 100644 target/classes/com/ski/crawler/util/ValidationUtil.class create mode 100644 target/classes/com/ski/crawler/utils/CrawlerHttp.class create mode 100644 target/classes/com/ski/crawler/utils/HttpClientUtil.class create mode 100644 target/classes/com/ski/crawler/view/ConsoleView$Ansi.class create mode 100644 target/classes/com/ski/crawler/view/ConsoleView$Col.class create mode 100644 target/classes/com/ski/crawler/view/ConsoleView$TablePrinter.class create mode 100644 target/classes/com/ski/crawler/view/ConsoleView.class create mode 100644 target/classes/logback.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8b21d0dc1b5d31ff801532044c68d90de8e7279a GIT binary patch literal 40 qcmezWuY@6yp@<=!A(f$oL7#z_fr~+pp^TxJA( + + 4.0.0 + + com.ski + crawler + 1.0.0 + jar + + Web Crawler + A Java web crawler project + + + 11 + 11 + UTF-8 + 1.15.3 + 4.5.13 + 2.15.2 + 5.2.5 + + + + + + org.jsoup + jsoup + ${jsoup.version} + + + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + + org.projectlombok + lombok + 1.18.28 + provided + + + + + org.slf4j + slf4j-api + 1.7.36 + + + + ch.qos.logback + logback-classic + 1.2.12 + + + + + junit + junit + 4.13.2 + test + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 11 + 11 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + + + com.ski.crawler.Main + + + + + + + + + diff --git a/result.jsonl b/result.jsonl new file mode 100644 index 0000000..a1550e6 --- /dev/null +++ b/result.jsonl @@ -0,0 +1,20 @@ +{"id":null,"name":"Thredbo","country":"Australia","region":"Oceania","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/1121#ski-map-42368","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Valle Nevado","country":"Chile","region":"Americas","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/1144#ski-map-42367","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Las Leñas","country":"Argentina","region":"Americas","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/1129#ski-map-42366","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Damüls-Mellau Au, Damüls, Mellau","country":"Vorarlberg","region":"Austria","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/2700#ski-map-37810","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Appalachian Ski Mtn.","country":"North Carolina","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/285#ski-map-34865","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Las Leñas","country":"Argentina","region":"Americas","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/1129#ski-map-42365","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Blue Mountain","country":"Ontario","region":"Canada","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/113#ski-map-39542","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Smugglers' Notch Resort","country":"Vermont","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/209#ski-map-6815","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Granlibakken Ski Resort","country":"California","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/535#ski-map-40733","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Magic Mountain","country":"Vermont","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/201#ski-map-7492","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Bromley Mountain","country":"Vermont","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/217#ski-map-4224","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Magic Mountain","country":"Vermont","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/201#ski-map-6965","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Wurmberg","country":"Central Uplands","region":"Germany","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/4190#ski-map-7596","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Vail","country":"Colorado","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/507#ski-map-2580","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"King Pine Ski Area","country":"New Hampshire","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/354#ski-map-11664","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Pigeon Mountain","country":"Alberta","region":"Canada","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/2131#ski-map-23689","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"The Pines","country":"New York","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/4872#ski-map-10199","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Ski Cooper","country":"Colorado","region":"United States","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/512#ski-map-6863","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Staller Sattel","country":"Tyrol","region":"Austria","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/12393#ski-map-17983","sourceSite":"skimap","crawlTime":null} +{"id":null,"name":"Val Neigette","country":"Quebec","region":"Canada","latitude":null,"longitude":null,"altitudeMin":null,"altitudeMax":null,"totalKm":null,"slopeCount":null,"liftCount":null,"ticketPriceMin":null,"ticketPriceMax":null,"currency":null,"openTime":null,"snowDepthCm":null,"temperatureC":null,"nearbyHotels":null,"rentalShops":null,"url":"https://skimap.org/skiareas/view/2205#ski-map-2834","sourceSite":"skimap","crawlTime":null} diff --git a/src/main/java/com/ski/crawler/Main.java b/src/main/java/com/ski/crawler/Main.java new file mode 100644 index 0000000..99c4ea1 --- /dev/null +++ b/src/main/java/com/ski/crawler/Main.java @@ -0,0 +1,40 @@ +package com.ski.crawler; + +import com.ski.crawler.command.CrawlCommand; +import com.ski.crawler.command.ExportCommand; +import com.ski.crawler.command.FilterCommand; +import com.ski.crawler.command.HelpCommand; +import com.ski.crawler.command.ListCommand; +import com.ski.crawler.command.ResumeCommand; +import com.ski.crawler.command.SitesCommand; +import com.ski.crawler.command.StatsCommand; +import com.ski.crawler.controller.CrawlerContext; +import com.ski.crawler.controller.CrawlerController; +import com.ski.crawler.factory.StrategyFactory; +import com.ski.crawler.repository.SkiResortRepository; +import com.ski.crawler.service.ScraperService; + +public class Main { + public static void main(String[] args) { + try { + SkiResortRepository repo = new SkiResortRepository(); + StrategyFactory factory = new StrategyFactory(); + ScraperService service = new ScraperService(); + CrawlerContext context = new CrawlerContext(repo, factory, service); + + CrawlerController controller = new CrawlerController( + new CrawlCommand(), + new ListCommand(), + new FilterCommand(), + new ExportCommand(), + new ResumeCommand(), + new StatsCommand(), + new SitesCommand(), + new HelpCommand() + ); + controller.run(args, context); + } catch (Exception e) { + System.err.println("Crawler failed: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/ski/crawler/command/Command.java b/src/main/java/com/ski/crawler/command/Command.java new file mode 100644 index 0000000..699ea81 --- /dev/null +++ b/src/main/java/com/ski/crawler/command/Command.java @@ -0,0 +1,10 @@ +package com.ski.crawler.command; + +import com.ski.crawler.controller.CrawlerContext; + +public interface Command { + String name(); + + void execute(String[] args, CrawlerContext context) throws Exception; +} + diff --git a/src/main/java/com/ski/crawler/command/CrawlCommand.java b/src/main/java/com/ski/crawler/command/CrawlCommand.java new file mode 100644 index 0000000..5b122e9 --- /dev/null +++ b/src/main/java/com/ski/crawler/command/CrawlCommand.java @@ -0,0 +1,253 @@ +package com.ski.crawler.command; + +import com.ski.crawler.controller.CrawlerContext; +import com.ski.crawler.exception.NetworkException; +import com.ski.crawler.factory.StrategyFactory; +import com.ski.crawler.repository.SkiResortRepository; +import com.ski.crawler.service.ScraperService; +import com.ski.crawler.strategy.CrawlStrategy; +import com.ski.crawler.util.CliArgs; +import com.ski.crawler.util.ExcelUtil; +import com.ski.crawler.utils.CrawlerHttp; +import com.ski.crawler.view.ConsoleView; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class CrawlCommand implements Command { + @Override + public String name() { + return "crawl"; + } + + @Override + public void execute(String[] args, CrawlerContext context) throws Exception { + Map opts = CliArgs.parseOptions(args, 1); + + String siteId = normalizeSite(opts.getOrDefault("site", "skiresort")); + int limit = parseLimit(opts.get("limit"), 100); + int threads = CliArgs.parseInt(opts.get("threads"), 3); + int timeoutMs = CliArgs.parseInt(opts.get("timeout"), 20000); + int retry = CliArgs.parseInt(opts.get("retry"), 3); + long retrySleep = CliArgs.parseInt(opts.get("retry-sleep"), 1000); + boolean dryRun = CliArgs.parseBoolean(opts.get("dry-run")); + boolean full = CliArgs.parseBoolean(opts.get("full")); + boolean incremental = !full; + boolean noProxy = CliArgs.parseBoolean(opts.get("no-proxy")); + boolean color = CliArgs.parseBoolean(opts.get("color")); + boolean showFailures = CliArgs.parseBoolean(opts.get("show-failures")); + Integer widthArg = CliArgs.parseNullableInt(opts.get("width")); + + String country = opts.get("country"); + String startUrl = opts.get("start-url"); + String outRaw = opts.get("out"); + String out = (outRaw == null || outRaw.trim().isEmpty()) ? null : outRaw.trim(); + String outJsonl = out; + String outXlsx = null; + if (out != null && out.toLowerCase(Locale.ROOT).endsWith(".xlsx")) { + outXlsx = out; + outJsonl = null; + } + + String userAgent = opts.getOrDefault("ua", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"); + + String proxyHost = opts.getOrDefault("proxy-host", "127.0.0.1"); + int proxyPort = CliArgs.parseInt(opts.get("proxy-port"), 7890); + boolean proxyEnabled = !noProxy; + String proxy = opts.get("proxy"); + if (proxy != null && !proxy.isEmpty()) { + String p = proxy.trim(); + if (p.equalsIgnoreCase("none") || p.equalsIgnoreCase("off") || p.equalsIgnoreCase("false")) { + proxyEnabled = false; + } else { + int idx = p.lastIndexOf(':'); + if (idx > 0 && idx < p.length() - 1) { + proxyHost = p.substring(0, idx); + proxyPort = CliArgs.parseInt(p.substring(idx + 1), proxyPort); + } else { + proxyHost = p; + } + } + } + + CrawlerHttp http = new CrawlerHttp(userAgent, proxyHost, proxyPort, proxyEnabled, timeoutMs); + int width = resolveWidth(widthArg); + ConsoleView view = new ConsoleView(width, color); + + StrategyFactory factory = context.strategies(); + SkiResortRepository repo = context.repository(); + ScraperService svc = context.scraper(); + + ScraperService.CrawlReport report; + if (siteId.equals("all")) { + if (outJsonl != null) { + System.err.println("When --site all, JSONL --out is not supported. Use --out result.xlsx or omit --out."); + return; + } + report = crawlAll(factory, svc, startUrl, limit, threads, country, http, repo, incremental, view, showFailures, dryRun, retry, retrySleep); + } else { + CrawlStrategy strategy = factory.create(siteId); + try { + report = svc.crawl(strategy, startUrl, limit, threads, country, http, repo, incremental, outJsonl, view, showFailures, dryRun, retry, retrySleep); + } catch (NetworkException e) { + throw e; + } + } + + if (outXlsx != null) { + if (dryRun) { + System.err.println("dry-run is enabled, skip writing: " + outXlsx); + } else { + ExcelUtil.exportResortsBySiteToXlsx(repo.getAll(), outXlsx); + System.err.println("Excel exported: " + repo.getAll().size() + " -> " + outXlsx); + } + } + + Map summary = new LinkedHashMap<>(); + summary.put("site", report.site); + summary.put("total", report.total); + summary.put("success", report.success); + summary.put("filteredOut", report.filteredOut); + summary.put("skipped", report.skipped); + summary.put("failed", report.failed); + if (outXlsx != null && !dryRun) { + summary.put("out", outXlsx); + } else if (outJsonl != null && !dryRun) { + summary.put("out", outJsonl); + } + + view.printSummary(summary, sortByValueDesc(report.byCountry), showFailures ? report.failures : null); + } + + private String normalizeSite(String raw) { + if (raw == null) { + return "skiresort"; + } + String t = raw.trim().toLowerCase(Locale.ROOT); + if (t.equals("wiki")) { + return "wikipedia"; + } + return t; + } + + private ScraperService.CrawlReport crawlAll( + StrategyFactory factory, + ScraperService svc, + String startUrl, + int limit, + int threads, + String countryFilter, + CrawlerHttp http, + SkiResortRepository repo, + boolean incremental, + ConsoleView view, + boolean showFailures, + boolean dryRun, + int retryAttempts, + long retrySleepMs + ) throws Exception { + List sites = Arrays.asList("skiresort", "wikipedia", "skimap"); + Map byCountry = new LinkedHashMap<>(); + List failures = new java.util.ArrayList<>(); + int total = 0; + int success = 0; + int filteredOut = 0; + int skipped = 0; + int failed = 0; + + for (String s : sites) { + CrawlStrategy strategy = factory.create(s); + try { + ScraperService.CrawlReport r = svc.crawl(strategy, null, limit, threads, countryFilter, http, repo, incremental, null, view, showFailures, dryRun, retryAttempts, retrySleepMs); + total += r.total; + success += r.success; + filteredOut += r.filteredOut; + skipped += r.skipped; + failed += r.failed; + mergeByCountry(byCountry, r.byCountry); + if (showFailures && r.failures != null) { + for (String f : r.failures) { + if (failures.size() >= 200) { + break; + } + failures.add(f); + } + } + } catch (Exception e) { + failed += 1; + if (showFailures && failures.size() < 200) { + failures.add("site=" + s + " [" + e.getClass().getSimpleName() + "] " + (e.getMessage() == null ? "" : e.getMessage())); + } + } + } + + ScraperService.CrawlReport out = new ScraperService.CrawlReport(); + out.site = "all"; + out.total = total; + out.success = success; + out.filteredOut = filteredOut; + out.skipped = skipped; + out.failed = failed; + out.byCountry = byCountry; + out.failures = failures; + return out; + } + + private void mergeByCountry(Map acc, Map add) { + if (acc == null || add == null || add.isEmpty()) { + return; + } + for (Map.Entry e : add.entrySet()) { + if (e.getKey() == null) { + continue; + } + long v = e.getValue() == null ? 0L : e.getValue(); + acc.put(e.getKey(), acc.getOrDefault(e.getKey(), 0L) + v); + } + } + + private int parseLimit(String v, int def) { + if (v == null || v.trim().isEmpty()) { + return def; + } + String t = v.trim(); + if (t.equalsIgnoreCase("all")) { + return -1; + } + try { + int n = Integer.parseInt(t); + return n <= 0 ? def : n; + } catch (Exception e) { + return def; + } + } + + private int resolveWidth(Integer widthArg) { + if (widthArg != null && widthArg > 20) { + return widthArg; + } + String cols = System.getenv("COLUMNS"); + if (cols != null) { + try { + int n = Integer.parseInt(cols.trim()); + if (n > 20) { + return n; + } + } catch (Exception ignored) { + } + } + return 120; + } + + private Map sortByValueDesc(Map m) { + if (m == null || m.isEmpty()) { + return m; + } + return m.entrySet().stream() + .sorted((a, b) -> Long.compare(b.getValue(), a.getValue())) + .collect(LinkedHashMap::new, (acc, e) -> acc.put(e.getKey(), e.getValue()), Map::putAll); + } +} diff --git a/src/main/java/com/ski/crawler/command/ExportCommand.java b/src/main/java/com/ski/crawler/command/ExportCommand.java new file mode 100644 index 0000000..a625cf9 --- /dev/null +++ b/src/main/java/com/ski/crawler/command/ExportCommand.java @@ -0,0 +1,77 @@ +package com.ski.crawler.command; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ski.crawler.controller.CrawlerContext; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.repository.SkiResortRepository; +import com.ski.crawler.util.CliArgs; +import com.ski.crawler.util.ExcelUtil; +import com.ski.crawler.util.JsonUtil; + +import java.io.BufferedWriter; +import java.util.Locale; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ExportCommand implements Command { + @Override + public String name() { + return "export"; + } + + @Override + public void execute(String[] args, CrawlerContext context) throws Exception { + Map opts = CliArgs.parseOptions(args, 1); + String out = opts.get("out"); + if (out == null || out.trim().isEmpty()) { + System.err.println("Missing --out "); + return; + } + + SkiResortRepository repo = context.repository(); + List all = repo.getAll(); + String path = out.trim(); + if (path.toLowerCase(Locale.ROOT).endsWith(".xlsx")) { + ExcelUtil.exportResortsBySiteToXlsx(all, path); + System.err.println("Exported: " + all.size() + " -> " + path); + return; + } + + ObjectMapper mapper = JsonUtil.mapper(); + try (BufferedWriter w = JsonUtil.openJsonlWriter(path)) { + for (SkiResort r : all) { + w.write(mapper.writeValueAsString(toJson(r))); + w.newLine(); + } + } + System.err.println("Exported: " + all.size() + " -> " + path); + } + + private Map toJson(SkiResort r) { + Map obj = new LinkedHashMap<>(); + obj.put("id", r.getId()); + obj.put("name", r.getName()); + obj.put("country", r.getCountry()); + obj.put("region", r.getRegion()); + obj.put("latitude", r.getLatitude()); + obj.put("longitude", r.getLongitude()); + obj.put("altitudeMin", r.getAltitudeMin()); + obj.put("altitudeMax", r.getAltitudeMax()); + obj.put("totalKm", r.getTotalKm()); + obj.put("slopeCount", r.getSlopeCount()); + obj.put("liftCount", r.getLiftCount()); + obj.put("ticketPriceMin", r.getTicketPriceMin()); + obj.put("ticketPriceMax", r.getTicketPriceMax()); + obj.put("currency", r.getCurrency()); + obj.put("openTime", r.getOpenTime()); + obj.put("snowDepthCm", r.getSnowDepthCm()); + obj.put("temperatureC", r.getTemperatureC()); + obj.put("nearbyHotels", r.getNearbyHotels()); + obj.put("rentalShops", r.getRentalShops()); + obj.put("url", r.getSourceUrl()); + obj.put("sourceSite", r.getSourceSite()); + obj.put("crawlTime", r.getCrawledAt() == null ? null : r.getCrawledAt().toString()); + return obj; + } +} diff --git a/src/main/java/com/ski/crawler/command/FilterCommand.java b/src/main/java/com/ski/crawler/command/FilterCommand.java new file mode 100644 index 0000000..3dfe9d5 --- /dev/null +++ b/src/main/java/com/ski/crawler/command/FilterCommand.java @@ -0,0 +1,18 @@ +package com.ski.crawler.command; + +import com.ski.crawler.controller.CrawlerContext; + +public class FilterCommand implements Command { + private final ListCommand delegate = new ListCommand(); + + @Override + public String name() { + return "filter"; + } + + @Override + public void execute(String[] args, CrawlerContext context) { + delegate.execute(args, context); + } +} + diff --git a/src/main/java/com/ski/crawler/command/HelpCommand.java b/src/main/java/com/ski/crawler/command/HelpCommand.java new file mode 100644 index 0000000..ebcae3f --- /dev/null +++ b/src/main/java/com/ski/crawler/command/HelpCommand.java @@ -0,0 +1,38 @@ +package com.ski.crawler.command; + +import com.ski.crawler.controller.CrawlerContext; + +public class HelpCommand implements Command { + @Override + public String name() { + return "help"; + } + + @Override + public void execute(String[] args, CrawlerContext context) { + System.out.println("命令:"); + System.out.println(" crawl --site --limit [--country <关键词>] [--out ] [--dry-run] [--no-proxy]"); + System.out.println(" list [--country <关键词>]"); + System.out.println(" export --out "); + System.out.println(" resume --in "); + System.out.println(" stats"); + System.out.println(" sites"); + System.out.println(" help"); + System.out.println(); + System.out.println("crawl 参数:"); + System.out.println(" --threads 默认 3"); + System.out.println(" --start-url 覆盖站点入口"); + System.out.println(" --timeout 默认 20000"); + System.out.println(" --ua 覆盖 UA"); + System.out.println(" --proxy 代理配置"); + System.out.println(" --proxy-host / --proxy-port "); + System.out.println(" --no-proxy 禁用代理"); + System.out.println(" --width 表格宽度"); + System.out.println(" --color 表头上色(可选)"); + System.out.println(" --show-failures 结束时输出失败列表(可选)"); + System.out.println(" --full 全量抓取(忽略去重,仍然不会往仓库写重复 URL)"); + System.out.println(" --retry 默认 3"); + System.out.println(" --retry-sleep 默认 1000"); + System.out.println(" --dry-run 不写入仓库/不导出文件(仅展示)"); + } +} diff --git a/src/main/java/com/ski/crawler/command/ListCommand.java b/src/main/java/com/ski/crawler/command/ListCommand.java new file mode 100644 index 0000000..2981502 --- /dev/null +++ b/src/main/java/com/ski/crawler/command/ListCommand.java @@ -0,0 +1,53 @@ +package com.ski.crawler.command; + +import com.ski.crawler.controller.CrawlerContext; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.repository.SkiResortRepository; +import com.ski.crawler.util.CliArgs; +import com.ski.crawler.view.ConsoleView; + +import java.util.List; +import java.util.Map; + +public class ListCommand implements Command { + @Override + public String name() { + return "list"; + } + + @Override + public void execute(String[] args, CrawlerContext context) { + Map opts = CliArgs.parseOptions(args, 1); + String country = opts.get("country"); + boolean color = CliArgs.parseBoolean(opts.get("color")); + Integer widthArg = CliArgs.parseNullableInt(opts.get("width")); + int width = resolveWidth(widthArg); + + SkiResortRepository repo = context.repository(); + List list = (country == null || country.trim().isEmpty()) ? repo.getAll() : repo.filterByCountry(country); + + ConsoleView view = new ConsoleView(width, color); + view.printHeader(); + for (SkiResort r : list) { + view.printResort(r); + } + } + + private int resolveWidth(Integer widthArg) { + if (widthArg != null && widthArg > 20) { + return widthArg; + } + String cols = System.getenv("COLUMNS"); + if (cols != null) { + try { + int n = Integer.parseInt(cols.trim()); + if (n > 20) { + return n; + } + } catch (Exception ignored) { + } + } + return 120; + } +} + diff --git a/src/main/java/com/ski/crawler/command/ResumeCommand.java b/src/main/java/com/ski/crawler/command/ResumeCommand.java new file mode 100644 index 0000000..319ce28 --- /dev/null +++ b/src/main/java/com/ski/crawler/command/ResumeCommand.java @@ -0,0 +1,140 @@ +package com.ski.crawler.command; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ski.crawler.controller.CrawlerContext; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.repository.SkiResortRepository; +import com.ski.crawler.util.CliArgs; +import com.ski.crawler.util.JsonUtil; + +import java.io.BufferedReader; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +public class ResumeCommand implements Command { + @Override + public String name() { + return "resume"; + } + + @Override + public void execute(String[] args, CrawlerContext context) throws Exception { + Map opts = CliArgs.parseOptions(args, 1); + String in = opts.get("in"); + if (in == null || in.trim().isEmpty()) { + System.err.println("Missing --in "); + return; + } + + SkiResortRepository repo = context.repository(); + ObjectMapper mapper = JsonUtil.mapper(); + int loaded = 0; + int skipped = 0; + try (BufferedReader br = JsonUtil.openJsonlReader(in.trim())) { + String line; + while ((line = br.readLine()) != null) { + String t = line.trim(); + if (t.isEmpty()) { + continue; + } + Map obj = mapper.readValue(t, new TypeReference>() {}); + SkiResort r = fromJson(obj); + if (r.getSourceUrl() == null && obj.get("url") != null) { + r.setSourceUrl(String.valueOf(obj.get("url"))); + } + if (repo.add(r)) { + loaded++; + } else { + skipped++; + } + } + } + System.err.println("Resumed: loaded=" + loaded + " skipped=" + skipped + " totalInRepo=" + repo.getAll().size()); + } + + private SkiResort fromJson(Map obj) { + SkiResort r = new SkiResort(); + r.setName(asString(obj.get("name"))); + r.setCountry(asString(obj.get("country"))); + r.setRegion(asString(obj.get("region"))); + r.setLatitude(asDouble(obj.get("latitude"))); + r.setLongitude(asDouble(obj.get("longitude"))); + r.setAltitudeMin(asInt(obj.get("altitudeMin"))); + r.setAltitudeMax(asInt(obj.get("altitudeMax"))); + r.setTotalKm(asDouble(obj.get("totalKm"))); + r.setSlopeCount(asInt(obj.get("slopeCount"))); + r.setLiftCount(asInt(obj.get("liftCount"))); + r.setTicketPriceMin(asDouble(obj.get("ticketPriceMin"))); + r.setTicketPriceMax(asDouble(obj.get("ticketPriceMax"))); + r.setCurrency(asString(obj.get("currency"))); + r.setOpenTime(asString(obj.get("openTime"))); + r.setSnowDepthCm(asInt(obj.get("snowDepthCm"))); + r.setTemperatureC(asDouble(obj.get("temperatureC"))); + r.setSourceSite(asString(obj.get("sourceSite"))); + r.setSourceUrl(asString(obj.get("url"))); + String crawlTime = asString(obj.get("crawlTime")); + if (crawlTime != null) { + try { + r.setCrawledAt(LocalDateTime.parse(crawlTime)); + } catch (Exception ignored) { + } + } + + Object hotels = obj.get("nearbyHotels"); + if (hotels instanceof List) { + r.setNearbyHotels((List) hotels); + } + Object shops = obj.get("rentalShops"); + if (shops instanceof List) { + r.setRentalShops((List) shops); + } + return r; + } + + private String asString(Object v) { + if (v == null) { + return null; + } + String s = String.valueOf(v).replace('\u00A0', ' ').trim(); + return s.isEmpty() ? null : s; + } + + private Integer asInt(Object v) { + try { + if (v == null) { + return null; + } + if (v instanceof Number) { + return ((Number) v).intValue(); + } + String s = String.valueOf(v).trim(); + if (s.isEmpty()) { + return null; + } + return Integer.parseInt(s); + } catch (Exception e) { + return null; + } + } + + private Double asDouble(Object v) { + try { + if (v == null) { + return null; + } + if (v instanceof Number) { + return ((Number) v).doubleValue(); + } + String s = String.valueOf(v).trim().replace(",", "."); + if (s.isEmpty()) { + return null; + } + return Double.parseDouble(s); + } catch (Exception e) { + return null; + } + } +} + diff --git a/src/main/java/com/ski/crawler/command/SitesCommand.java b/src/main/java/com/ski/crawler/command/SitesCommand.java new file mode 100644 index 0000000..12f5d91 --- /dev/null +++ b/src/main/java/com/ski/crawler/command/SitesCommand.java @@ -0,0 +1,19 @@ +package com.ski.crawler.command; + +import com.ski.crawler.controller.CrawlerContext; + +public class SitesCommand implements Command { + @Override + public String name() { + return "sites"; + } + + @Override + public void execute(String[] args, CrawlerContext context) { + System.out.println("sites:"); + System.out.println(" skiresort https://www.skiresort.info"); + System.out.println(" wikipedia https://en.wikipedia.org/wiki/List_of_ski_areas_and_resorts"); + System.out.println(" skimap https://skimap.org"); + } +} + diff --git a/src/main/java/com/ski/crawler/command/StatsCommand.java b/src/main/java/com/ski/crawler/command/StatsCommand.java new file mode 100644 index 0000000..c65cf8f --- /dev/null +++ b/src/main/java/com/ski/crawler/command/StatsCommand.java @@ -0,0 +1,66 @@ +package com.ski.crawler.command; + +import com.ski.crawler.controller.CrawlerContext; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.repository.SkiResortRepository; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class StatsCommand implements Command { + @Override + public String name() { + return "stats"; + } + + @Override + public void execute(String[] args, CrawlerContext context) { + SkiResortRepository repo = context.repository(); + List all = repo.getAll(); + System.out.println("total=" + all.size()); + + Map byCountry = repo.countByCountry(); + List> top = byCountry.entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .limit(20) + .collect(Collectors.toList()); + if (!top.isEmpty()) { + System.out.println("byCountry(top20):"); + long max = top.get(0).getValue() == null ? 0 : top.get(0).getValue(); + for (Map.Entry e : top) { + long v = e.getValue() == null ? 0 : e.getValue(); + System.out.println(" " + e.getKey() + ": " + v + " " + bar(v, max, 30)); + } + } + + double sum = 0; + int cnt = 0; + for (SkiResort r : all) { + Double p = r.getTicketPriceMin(); + if (p != null && p >= 0) { + sum += p; + cnt++; + } + } + if (cnt > 0) { + System.out.println("avgTicketPriceMin=" + (sum / cnt) + " samples=" + cnt); + } + } + + private String bar(long v, long max, int width) { + if (max <= 0 || width <= 0) { + return ""; + } + int n = (int) Math.round((double) v * width / (double) max); + if (n <= 0) { + return ""; + } + StringBuilder sb = new StringBuilder(n); + for (int i = 0; i < n; i++) { + sb.append('#'); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/ski/crawler/controller/CrawlerContext.java b/src/main/java/com/ski/crawler/controller/CrawlerContext.java new file mode 100644 index 0000000..8f86520 --- /dev/null +++ b/src/main/java/com/ski/crawler/controller/CrawlerContext.java @@ -0,0 +1,30 @@ +package com.ski.crawler.controller; + +import com.ski.crawler.factory.StrategyFactory; +import com.ski.crawler.repository.SkiResortRepository; +import com.ski.crawler.service.ScraperService; + +public class CrawlerContext { + private final SkiResortRepository repository; + private final StrategyFactory strategyFactory; + private final ScraperService scraperService; + + public CrawlerContext(SkiResortRepository repository, StrategyFactory strategyFactory, ScraperService scraperService) { + this.repository = repository; + this.strategyFactory = strategyFactory; + this.scraperService = scraperService; + } + + public SkiResortRepository repository() { + return repository; + } + + public StrategyFactory strategies() { + return strategyFactory; + } + + public ScraperService scraper() { + return scraperService; + } +} + diff --git a/src/main/java/com/ski/crawler/controller/CrawlerController.java b/src/main/java/com/ski/crawler/controller/CrawlerController.java new file mode 100644 index 0000000..f161ca1 --- /dev/null +++ b/src/main/java/com/ski/crawler/controller/CrawlerController.java @@ -0,0 +1,72 @@ +package com.ski.crawler.controller; + +import com.ski.crawler.command.Command; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class CrawlerController { + private final Map commands = new HashMap<>(); + + public CrawlerController(Command... cmds) { + if (cmds != null) { + for (Command c : cmds) { + if (c != null && c.name() != null) { + commands.put(c.name().toLowerCase(Locale.ROOT), c); + } + } + } + } + + public void run(String[] args, CrawlerContext context) throws Exception { + String cmd = firstArg(args); + if (cmd.isEmpty()) { + execute("help", args, context); + return; + } + + if (isLegacyLimit(cmd)) { + execute("crawl", new String[]{"crawl", "--limit", cmd}, context); + return; + } + if ("all".equalsIgnoreCase(cmd)) { + execute("crawl", new String[]{"crawl", "--limit", "all"}, context); + return; + } + + execute(cmd, args, context); + } + + private void execute(String cmd, String[] args, CrawlerContext context) throws Exception { + Command c = commands.get(cmd.toLowerCase(Locale.ROOT)); + if (c == null) { + Command help = commands.get("help"); + if (help != null) { + help.execute(args, context); + } + return; + } + c.execute(args, context); + } + + private String firstArg(String[] args) { + if (args == null || args.length == 0 || args[0] == null) { + return ""; + } + return args[0].trim(); + } + + private boolean isLegacyLimit(String s) { + try { + if (s == null) { + return false; + } + Integer.parseInt(s.trim()); + return true; + } catch (Exception e) { + return false; + } + } +} + diff --git a/src/main/java/com/ski/crawler/exception/CrawlerException.java b/src/main/java/com/ski/crawler/exception/CrawlerException.java new file mode 100644 index 0000000..34b5db7 --- /dev/null +++ b/src/main/java/com/ski/crawler/exception/CrawlerException.java @@ -0,0 +1,12 @@ +package com.ski.crawler.exception; + +public class CrawlerException extends Exception { + public CrawlerException(String message) { + super(message); + } + + public CrawlerException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/src/main/java/com/ski/crawler/exception/NetworkException.java b/src/main/java/com/ski/crawler/exception/NetworkException.java new file mode 100644 index 0000000..5d767ec --- /dev/null +++ b/src/main/java/com/ski/crawler/exception/NetworkException.java @@ -0,0 +1,12 @@ +package com.ski.crawler.exception; + +public class NetworkException extends CrawlerException { + public NetworkException(String message) { + super(message); + } + + public NetworkException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/src/main/java/com/ski/crawler/exception/ParseException.java b/src/main/java/com/ski/crawler/exception/ParseException.java new file mode 100644 index 0000000..acdfd84 --- /dev/null +++ b/src/main/java/com/ski/crawler/exception/ParseException.java @@ -0,0 +1,12 @@ +package com.ski.crawler.exception; + +public class ParseException extends CrawlerException { + public ParseException(String message) { + super(message); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/src/main/java/com/ski/crawler/factory/StrategyFactory.java b/src/main/java/com/ski/crawler/factory/StrategyFactory.java new file mode 100644 index 0000000..4163ef5 --- /dev/null +++ b/src/main/java/com/ski/crawler/factory/StrategyFactory.java @@ -0,0 +1,31 @@ +package com.ski.crawler.factory; + +import com.ski.crawler.strategy.CrawlStrategy; +import com.ski.crawler.strategy.SkiResortInfoStrategy; +import com.ski.crawler.strategy.SkimapStrategy; +import com.ski.crawler.strategy.WikipediaStrategy; + +import java.util.Locale; + +public class StrategyFactory { + public CrawlStrategy create(String id) { + if (id == null) { + return new SkiResortInfoStrategy(); + } + String t = id.trim().toLowerCase(Locale.ROOT); + if (t.equals("wiki")) { + t = "wikipedia"; + } + switch (t) { + case "skiresort": + return new SkiResortInfoStrategy(); + case "wikipedia": + return new WikipediaStrategy(); + case "skimap": + return new SkimapStrategy(); + default: + throw new IllegalArgumentException("Unknown site: " + id); + } + } +} + diff --git a/src/main/java/com/ski/crawler/model/SkiLift.java b/src/main/java/com/ski/crawler/model/SkiLift.java new file mode 100644 index 0000000..9c40e14 --- /dev/null +++ b/src/main/java/com/ski/crawler/model/SkiLift.java @@ -0,0 +1,83 @@ +package com.ski.crawler.model; + +public class SkiLift { + private Long id; + private Long resortId; + private Integer totalLifts; + private Integer gondolas; + private Integer chairlifts; + private Integer surfaceLifts; + private Integer cableCars; + private Integer travelators; + + public SkiLift() { + } + + public SkiLift(Long resortId) { + this.resortId = resortId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getResortId() { + return resortId; + } + + public void setResortId(Long resortId) { + this.resortId = resortId; + } + + public Integer getTotalLifts() { + return totalLifts; + } + + public void setTotalLifts(Integer totalLifts) { + this.totalLifts = totalLifts; + } + + public Integer getGondolas() { + return gondolas; + } + + public void setGondolas(Integer gondolas) { + this.gondolas = gondolas; + } + + public Integer getChairlifts() { + return chairlifts; + } + + public void setChairlifts(Integer chairlifts) { + this.chairlifts = chairlifts; + } + + public Integer getSurfaceLifts() { + return surfaceLifts; + } + + public void setSurfaceLifts(Integer surfaceLifts) { + this.surfaceLifts = surfaceLifts; + } + + public Integer getCableCars() { + return cableCars; + } + + public void setCableCars(Integer cableCars) { + this.cableCars = cableCars; + } + + public Integer getTravelators() { + return travelators; + } + + public void setTravelators(Integer travelators) { + this.travelators = travelators; + } +} diff --git a/src/main/java/com/ski/crawler/model/SkiResort.java b/src/main/java/com/ski/crawler/model/SkiResort.java new file mode 100644 index 0000000..f870969 --- /dev/null +++ b/src/main/java/com/ski/crawler/model/SkiResort.java @@ -0,0 +1,252 @@ +package com.ski.crawler.model; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +public class SkiResort { + private Long id; + private String name; + private String country; + private String region; + private Double latitude; + private Double longitude; + private Integer altitudeMin; + private Integer altitudeMax; + private Double totalKm; + private BigDecimal overallScore; + private SkiTrail skiTrail; + private SkiLift skiLift; + private SkiTicket skiTicket; + private String sourceUrl; + private String sourceSite; + private LocalDateTime crawledAt; + + private Integer slopeCount; + private Integer liftCount; + private Double ticketPriceMin; + private Double ticketPriceMax; + private String currency; + private String openTime; + private Double temperatureC; + private Integer snowDepthCm; + private List nearbyHotels; + private List rentalShops; + + public SkiResort() { + } + + public SkiResort(String name, String country, String sourceUrl) { + this.name = name; + this.country = country; + this.sourceUrl = sourceUrl; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + this.region = region; + } + + public Double getLatitude() { + return latitude; + } + + public void setLatitude(Double latitude) { + this.latitude = latitude; + } + + public Double getLongitude() { + return longitude; + } + + public void setLongitude(Double longitude) { + this.longitude = longitude; + } + + public Integer getAltitudeMin() { + return altitudeMin; + } + + public void setAltitudeMin(Integer altitudeMin) { + this.altitudeMin = altitudeMin; + } + + public Integer getAltitudeMax() { + return altitudeMax; + } + + public void setAltitudeMax(Integer altitudeMax) { + this.altitudeMax = altitudeMax; + } + + public Double getTotalKm() { + return totalKm; + } + + public void setTotalKm(Double totalKm) { + this.totalKm = totalKm; + } + + public BigDecimal getOverallScore() { + return overallScore; + } + + public void setOverallScore(BigDecimal overallScore) { + this.overallScore = overallScore; + } + + public SkiTrail getSkiTrail() { + return skiTrail; + } + + public void setSkiTrail(SkiTrail skiTrail) { + this.skiTrail = skiTrail; + } + + public SkiLift getSkiLift() { + return skiLift; + } + + public void setSkiLift(SkiLift skiLift) { + this.skiLift = skiLift; + } + + public SkiTicket getSkiTicket() { + return skiTicket; + } + + public void setSkiTicket(SkiTicket skiTicket) { + this.skiTicket = skiTicket; + } + + public String getSourceUrl() { + return sourceUrl; + } + + public void setSourceUrl(String sourceUrl) { + this.sourceUrl = sourceUrl; + } + + public String getSourceSite() { + return sourceSite; + } + + public void setSourceSite(String sourceSite) { + this.sourceSite = sourceSite; + } + + public LocalDateTime getCrawledAt() { + return crawledAt; + } + + public void setCrawledAt(LocalDateTime crawledAt) { + this.crawledAt = crawledAt; + } + + public Integer getSlopeCount() { + return slopeCount; + } + + public void setSlopeCount(Integer slopeCount) { + this.slopeCount = slopeCount; + } + + public Integer getLiftCount() { + return liftCount; + } + + public void setLiftCount(Integer liftCount) { + this.liftCount = liftCount; + } + + public Double getTicketPriceMin() { + return ticketPriceMin; + } + + public void setTicketPriceMin(Double ticketPriceMin) { + this.ticketPriceMin = ticketPriceMin; + } + + public Double getTicketPriceMax() { + return ticketPriceMax; + } + + public void setTicketPriceMax(Double ticketPriceMax) { + this.ticketPriceMax = ticketPriceMax; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public String getOpenTime() { + return openTime; + } + + public void setOpenTime(String openTime) { + this.openTime = openTime; + } + + public Double getTemperatureC() { + return temperatureC; + } + + public void setTemperatureC(Double temperatureC) { + this.temperatureC = temperatureC; + } + + public Integer getSnowDepthCm() { + return snowDepthCm; + } + + public void setSnowDepthCm(Integer snowDepthCm) { + this.snowDepthCm = snowDepthCm; + } + + public List getNearbyHotels() { + return nearbyHotels; + } + + public void setNearbyHotels(List nearbyHotels) { + this.nearbyHotels = nearbyHotels; + } + + public List getRentalShops() { + return rentalShops; + } + + public void setRentalShops(List rentalShops) { + this.rentalShops = rentalShops; + } +} diff --git a/src/main/java/com/ski/crawler/model/SkiReview.java b/src/main/java/com/ski/crawler/model/SkiReview.java new file mode 100644 index 0000000..09de6bb --- /dev/null +++ b/src/main/java/com/ski/crawler/model/SkiReview.java @@ -0,0 +1,76 @@ +package com.ski.crawler.model; + +import java.time.LocalDateTime; + +public class SkiReview { + private Long id; + private Long resortId; + private Double overallScore; + private Double snowScore; + private Double facilitiesScore; + private Integer totalReviews; + private LocalDateTime crawledAt; + + public SkiReview() { + } + + public SkiReview(Long resortId) { + this.resortId = resortId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getResortId() { + return resortId; + } + + public void setResortId(Long resortId) { + this.resortId = resortId; + } + + public Double getOverallScore() { + return overallScore; + } + + public void setOverallScore(Double overallScore) { + this.overallScore = overallScore; + } + + public Double getSnowScore() { + return snowScore; + } + + public void setSnowScore(Double snowScore) { + this.snowScore = snowScore; + } + + public Double getFacilitiesScore() { + return facilitiesScore; + } + + public void setFacilitiesScore(Double facilitiesScore) { + this.facilitiesScore = facilitiesScore; + } + + public Integer getTotalReviews() { + return totalReviews; + } + + public void setTotalReviews(Integer totalReviews) { + this.totalReviews = totalReviews; + } + + public LocalDateTime getCrawledAt() { + return crawledAt; + } + + public void setCrawledAt(LocalDateTime crawledAt) { + this.crawledAt = crawledAt; + } +} diff --git a/src/main/java/com/ski/crawler/model/SkiTicket.java b/src/main/java/com/ski/crawler/model/SkiTicket.java new file mode 100644 index 0000000..b682501 --- /dev/null +++ b/src/main/java/com/ski/crawler/model/SkiTicket.java @@ -0,0 +1,74 @@ +package com.ski.crawler.model; + +public class SkiTicket { + private Long id; + private Long resortId; + private String ticketType; + private Double priceAdult; + private Double priceChild; + private String currency; + private String season; + + public SkiTicket() { + } + + public SkiTicket(Long resortId) { + this.resortId = resortId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getResortId() { + return resortId; + } + + public void setResortId(Long resortId) { + this.resortId = resortId; + } + + public String getTicketType() { + return ticketType; + } + + public void setTicketType(String ticketType) { + this.ticketType = ticketType; + } + + public Double getPriceAdult() { + return priceAdult; + } + + public void setPriceAdult(Double priceAdult) { + this.priceAdult = priceAdult; + } + + public Double getPriceChild() { + return priceChild; + } + + public void setPriceChild(Double priceChild) { + this.priceChild = priceChild; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public String getSeason() { + return season; + } + + public void setSeason(String season) { + this.season = season; + } +} diff --git a/src/main/java/com/ski/crawler/model/SkiTrail.java b/src/main/java/com/ski/crawler/model/SkiTrail.java new file mode 100644 index 0000000..b29d641 --- /dev/null +++ b/src/main/java/com/ski/crawler/model/SkiTrail.java @@ -0,0 +1,92 @@ +package com.ski.crawler.model; + +public class SkiTrail { + private Long id; + private Long resortId; + private Double totalKm; + private Double beginnerKm; + private Double intermediateKm; + private Double expertKm; + private Integer totalRuns; + private Boolean snowMaking; + private Integer snowDepthCm; + + public SkiTrail() { + } + + public SkiTrail(Long resortId) { + this.resortId = resortId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getResortId() { + return resortId; + } + + public void setResortId(Long resortId) { + this.resortId = resortId; + } + + public Double getTotalKm() { + return totalKm; + } + + public void setTotalKm(Double totalKm) { + this.totalKm = totalKm; + } + + public Double getBeginnerKm() { + return beginnerKm; + } + + public void setBeginnerKm(Double beginnerKm) { + this.beginnerKm = beginnerKm; + } + + public Double getIntermediateKm() { + return intermediateKm; + } + + public void setIntermediateKm(Double intermediateKm) { + this.intermediateKm = intermediateKm; + } + + public Double getExpertKm() { + return expertKm; + } + + public void setExpertKm(Double expertKm) { + this.expertKm = expertKm; + } + + public Integer getTotalRuns() { + return totalRuns; + } + + public void setTotalRuns(Integer totalRuns) { + this.totalRuns = totalRuns; + } + + public Boolean getSnowMaking() { + return snowMaking; + } + + public void setSnowMaking(Boolean snowMaking) { + this.snowMaking = snowMaking; + } + + public Integer getSnowDepthCm() { + return snowDepthCm; + } + + public void setSnowDepthCm(Integer snowDepthCm) { + this.snowDepthCm = snowDepthCm; + } +} diff --git a/src/main/java/com/ski/crawler/parser/ResortDetailParser.java b/src/main/java/com/ski/crawler/parser/ResortDetailParser.java new file mode 100644 index 0000000..f3cada9 --- /dev/null +++ b/src/main/java/com/ski/crawler/parser/ResortDetailParser.java @@ -0,0 +1,471 @@ +package com.ski.crawler.parser; + +import com.ski.crawler.model.SkiLift; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.model.SkiTicket; +import com.ski.crawler.model.SkiTrail; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ResortDetailParser { + private static final Pattern INT_M_PATTERN = Pattern.compile("(\\d{2,5})\\s*m\\b", Pattern.CASE_INSENSITIVE); + private static final Pattern ALT_RANGE_PATTERN = Pattern.compile("(\\d{2,5})\\s*m\\s*(?:-|–|to)\\s*(\\d{2,5})\\s*m\\b", Pattern.CASE_INSENSITIVE); + private static final Pattern KM_PATTERN = Pattern.compile("(\\d+(?:[\\.,]\\d+)?)\\s*km\\b", Pattern.CASE_INSENSITIVE); + private static final Pattern PERCENT_PATTERN = Pattern.compile("(\\d{1,3})\\s*%\\b"); + private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+(?:[\\.,]\\d+)?)"); + private static final Pattern CURRENCY_FIRST_PATTERN = Pattern.compile("(?:(SFr\\.)|CHF|€|\\$|£)\\s*(\\d+(?:[\\.,]\\d+)?)"); + private static final Pattern CURRENCY_LAST_PATTERN = Pattern.compile("(\\d+(?:[\\.,]\\d+)?)\\s*(€|\\$|£|CHF|SFr\\.)"); +//解析滑雪场详情页的 HTML 内容,提取出滑雪场的详细信息。 +//它使用 Jsoup 解析 HTML 内容,然后根据 HTML 结构提取出滑雪场的名称、国家、区域、海拔、总距离、总轨迹、总 lift、总票、总评分等信息。 +//最后,它将这些信息封装到 SkiResort 对象中。 + public SkiResort parse(String html) { + SkiResort resort = new SkiResort(); + resort.setSkiTrail(new SkiTrail()); + resort.setSkiLift(new SkiLift()); + resort.setSkiTicket(new SkiTicket()); + + if (html == null || html.isEmpty()) { + return resort; + } + + Document doc; + try { + doc = Jsoup.parse(html); + } catch (Exception e) { + return resort; + } + + tryFillName(doc, resort); + tryFillCountryRegionFromBreadcrumb(doc, resort); + tryFillAltitude(doc, resort); + tryFillTotalKmAndTrailBreakdown(doc, resort); + tryFillLifts(doc, resort.getSkiLift()); + tryFillTickets(doc, resort.getSkiTicket()); + tryFillOverallScore(doc, resort); + + return resort; + } + + private void tryFillName(Document doc, SkiResort resort) { + try { + Element nameEl = doc.selectFirst(".resort-name"); + if (nameEl == null) { + nameEl = doc.selectFirst("h1"); + } + if (nameEl != null) { + String name = cleanText(nameEl.text()); + if (!name.isEmpty()) { + resort.setName(name); + } + } + } catch (Exception ignored) { + } + } + + private void tryFillCountryRegionFromBreadcrumb(Document doc, SkiResort resort) { + try { + Elements crumbs = doc.select(".breadcrumb a, nav.breadcrumb a, ol.breadcrumb a, ul.breadcrumb a, .breadcrumb li, nav.breadcrumb li, ol.breadcrumb li, ul.breadcrumb li"); + List items = new ArrayList<>(); + for (Element el : crumbs) { + String t = cleanText(el.text()); + if (t.isEmpty()) { + continue; + } + String lower = t.toLowerCase(Locale.ROOT); + if (lower.equals("ski resorts") || lower.equals("ski-resorts") || lower.equals("home") || lower.equals("worldwide")) { + continue; + } + items.add(t); + } + + if (items.size() >= 3) { + resort.setCountry(items.get(items.size() - 3)); + resort.setRegion(items.get(items.size() - 2)); + } else if (items.size() == 2) { + resort.setCountry(items.get(0)); + resort.setRegion(items.get(1)); + } else if (items.size() == 1) { + resort.setCountry(items.get(0)); + } + } catch (Exception ignored) { + } + } + + private void tryFillAltitude(Document doc, SkiResort resort) { + try { + String text = doc.text(); + + Integer min = null; + Integer max = null; + + Matcher range = ALT_RANGE_PATTERN.matcher(text); + if (range.find()) { + min = safeParseInt(range.group(1)); + max = safeParseInt(range.group(2)); + } else { + List ms = new ArrayList<>(); + Matcher m = INT_M_PATTERN.matcher(text); + while (m.find() && ms.size() < 3) { + Integer v = safeParseInt(m.group(1)); + if (v != null) { + ms.add(v); + } + } + if (ms.size() >= 2) { + min = ms.get(ms.size() - 2); + max = ms.get(ms.size() - 1); + } + } + + resort.setAltitudeMin(min); + resort.setAltitudeMax(max); + } catch (Exception ignored) { + } + } + + private void tryFillTotalKmAndTrailBreakdown(Document doc, SkiResort resort) { + try { + Double totalKm = null; + + Element kmEl = firstElementContaining(doc, "km", "slope", "slopes", "piste"); + if (kmEl != null) { + totalKm = firstDoubleFrom(KM_PATTERN, kmEl.text()); + } + if (totalKm == null) { + totalKm = firstDoubleFrom(KM_PATTERN, doc.text()); + } + resort.setTotalKm(totalKm); + + SkiTrail trail = resort.getSkiTrail(); + if (trail == null) { + trail = new SkiTrail(); + resort.setSkiTrail(trail); + } + trail.setTotalKm(totalKm); + + Integer beginnerPct = percentNearKeyword(doc, "beginner", "easy"); + Integer intermediatePct = percentNearKeyword(doc, "intermediate", "medium"); + Integer expertPct = percentNearKeyword(doc, "expert", "advanced", "difficult"); + + if (totalKm != null) { + if (beginnerPct != null) { + trail.setBeginnerKm(roundKm(totalKm * beginnerPct / 100.0)); + } + if (intermediatePct != null) { + trail.setIntermediateKm(roundKm(totalKm * intermediatePct / 100.0)); + } + if (expertPct != null) { + trail.setExpertKm(roundKm(totalKm * expertPct / 100.0)); + } + } + } catch (Exception ignored) { + } + } + + private void tryFillLifts(Document doc, SkiLift lift) { + if (lift == null) { + return; + } + try { + String text = doc.text(); + lift.setTotalLifts(intNear(text, "lift", "lifts")); + lift.setGondolas(intNear(text, "gondola", "gondolas")); + lift.setChairlifts(intNear(text, "chairlift", "chairlifts")); + lift.setSurfaceLifts(intNear(text, "surface lift", "surface lifts", "t-bar", "drag lift", "platter lift")); + lift.setCableCars(intNear(text, "cable car", "cable cars")); + lift.setTravelators(intNear(text, "travelator", "travelators", "moving carpet")); + } catch (Exception ignored) { + } + } + + private void tryFillTickets(Document doc, SkiTicket ticket) { + if (ticket == null) { + return; + } + try { + Element adultEl = firstElementContaining(doc, "adult", "adults"); + Element childEl = firstElementContaining(doc, "child", "children", "kid", "kids"); + + Price adult = (adultEl != null) ? extractPrice(adultEl.text()) : null; + Price child = (childEl != null) ? extractPrice(childEl.text()) : null; + + if (adult == null || child == null) { + List prices = extractAllPrices(doc.text(), 4); + if (adult == null && !prices.isEmpty()) { + adult = prices.get(0); + } + if (child == null && prices.size() >= 2) { + child = prices.get(1); + } + } + + if (adult != null) { + ticket.setPriceAdult(adult.amount); + ticket.setCurrency(adult.currency); + } + if (child != null) { + ticket.setPriceChild(child.amount); + if (ticket.getCurrency() == null) { + ticket.setCurrency(child.currency); + } + } + } catch (Exception ignored) { + } + } + + private void tryFillOverallScore(Document doc, SkiResort resort) { + try { + Element scoreEl = firstElementContaining(doc, "score", "rating", "stars"); + BigDecimal score = null; + if (scoreEl != null) { + score = firstBigDecimal(scoreEl.text()); + } + if (score == null) { + score = firstBigDecimal(doc.text()); + } + if (score != null) { + if (score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(new BigDecimal("10")) > 0) { + return; + } + resort.setOverallScore(score); + } + } catch (Exception ignored) { + } + } + + private Element firstElementContaining(Document doc, String... keywords) { + Elements candidates = doc.select("div, span, p, li, td, th"); + for (Element el : candidates) { + String t = el.text(); + if (t == null || t.isEmpty()) { + continue; + } + String lower = t.toLowerCase(Locale.ROOT); + for (String k : keywords) { + if (k != null && !k.isEmpty() && lower.contains(k.toLowerCase(Locale.ROOT))) { + return el; + } + } + } + return null; + } + + private Integer percentNearKeyword(Document doc, String... keywords) { + Elements candidates = doc.select("div, span, p, li, td, th"); + for (Element el : candidates) { + String t = el.text(); + if (t == null || t.isEmpty()) { + continue; + } + String lower = t.toLowerCase(Locale.ROOT); + boolean hit = false; + for (String k : keywords) { + if (k != null && !k.isEmpty() && lower.contains(k.toLowerCase(Locale.ROOT))) { + hit = true; + break; + } + } + if (!hit) { + continue; + } + Matcher m = PERCENT_PATTERN.matcher(t); + if (m.find()) { + Integer pct = safeParseInt(m.group(1)); + if (pct != null && pct >= 0 && pct <= 100) { + return pct; + } + } + } + return null; + } + + private Integer intNear(String text, String... keywords) { + if (text == null || text.isEmpty()) { + return null; + } + String lower = text.toLowerCase(Locale.ROOT); + int bestIndex = -1; + for (String k : keywords) { + if (k == null || k.isEmpty()) { + continue; + } + int idx = lower.indexOf(k.toLowerCase(Locale.ROOT)); + if (idx >= 0) { + bestIndex = idx; + break; + } + } + if (bestIndex < 0) { + return null; + } + int start = Math.max(0, bestIndex - 40); + int end = Math.min(text.length(), bestIndex + 40); + String window = text.substring(start, end); + Matcher m = Pattern.compile("(\\d{1,4})").matcher(window); + if (m.find()) { + return safeParseInt(m.group(1)); + } + return null; + } + + private Double roundKm(double v) { + return new BigDecimal(v).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + + private Double firstDoubleFrom(Pattern pattern, String text) { + if (text == null) { + return null; + } + Matcher m = pattern.matcher(text); + if (m.find()) { + return safeParseDouble(m.group(1)); + } + return null; + } + + private BigDecimal firstBigDecimal(String text) { + if (text == null) { + return null; + } + Matcher m = NUMBER_PATTERN.matcher(text); + if (m.find()) { + Double d = safeParseDouble(m.group(1)); + if (d == null) { + return null; + } + return BigDecimal.valueOf(d).setScale(2, RoundingMode.HALF_UP); + } + return null; + } + + private Price extractPrice(String text) { + if (text == null) { + return null; + } + Matcher m1 = CURRENCY_FIRST_PATTERN.matcher(text); + if (m1.find()) { + String cur = normalizeCurrency(m1.group(1), text.substring(m1.start(), Math.min(text.length(), m1.end()))); + Double amount = safeParseDouble(m1.group(2)); + if (amount != null) { + return new Price(cur, amount); + } + } + Matcher m2 = CURRENCY_LAST_PATTERN.matcher(text); + if (m2.find()) { + Double amount = safeParseDouble(m2.group(1)); + String cur = normalizeCurrency(null, m2.group(2)); + if (amount != null) { + return new Price(cur, amount); + } + } + return null; + } + + private List extractAllPrices(String text, int limit) { + List out = new ArrayList<>(); + if (text == null || text.isEmpty()) { + return out; + } + Matcher m1 = CURRENCY_FIRST_PATTERN.matcher(text); + while (m1.find() && out.size() < limit) { + Double amount = safeParseDouble(m1.group(2)); + if (amount == null) { + continue; + } + String cur = normalizeCurrency(m1.group(1), text.substring(m1.start(), Math.min(text.length(), m1.end()))); + out.add(new Price(cur, amount)); + } + Matcher m2 = CURRENCY_LAST_PATTERN.matcher(text); + while (m2.find() && out.size() < limit) { + Double amount = safeParseDouble(m2.group(1)); + if (amount == null) { + continue; + } + String cur = normalizeCurrency(null, m2.group(2)); + out.add(new Price(cur, amount)); + } + return out; + } + + private String normalizeCurrency(String group1, String raw) { + String src = (group1 != null && !group1.isEmpty()) ? group1 : raw; + if (src == null) { + return null; + } + String s = src.trim(); + if (s.startsWith("SFr")) { + return "SFr."; + } + if (s.equalsIgnoreCase("CHF")) { + return "CHF"; + } + if (s.contains("€")) { + return "€"; + } + if (s.contains("$")) { + return "$"; + } + if (s.contains("£")) { + return "£"; + } + return s.isEmpty() ? null : s; + } + + private String cleanText(String s) { + if (s == null) { + return ""; + } + return s.replace('\u00A0', ' ').trim(); + } + + private Integer safeParseInt(String s) { + try { + if (s == null) { + return null; + } + String t = s.replaceAll("[^0-9]", ""); + if (t.isEmpty()) { + return null; + } + return Integer.parseInt(t); + } catch (Exception e) { + return null; + } + } + + private Double safeParseDouble(String s) { + try { + if (s == null) { + return null; + } + String t = s.trim().replace(",", "."); + t = t.replaceAll("[^0-9.]", ""); + if (t.isEmpty()) { + return null; + } + return Double.parseDouble(t); + } catch (Exception e) { + return null; + } + } + + private static class Price { + private final String currency; + private final Double amount; + + private Price(String currency, Double amount) { + this.currency = currency; + this.amount = amount; + } + } +} diff --git a/src/main/java/com/ski/crawler/parser/ResortParser.java b/src/main/java/com/ski/crawler/parser/ResortParser.java new file mode 100644 index 0000000..5f65855 --- /dev/null +++ b/src/main/java/com/ski/crawler/parser/ResortParser.java @@ -0,0 +1,34 @@ +package com.ski.crawler.parser; + +import com.ski.crawler.model.SkiResort; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.time.LocalDateTime; +//ResortParser 类是解析器,负责解析滑雪场的 HTML 页内容。 +//它使用 Jsoup 解析 HTML 内容,然后根据 HTML 结构提取出滑雪场的名称、国家、区域、海拔、总距离、总轨迹、总 lift、总票、总评分等信息。 +//最后,它将这些信息封装到 SkiResort 对象中。 +public class ResortParser { + public SkiResort parseResort(String html, String sourceUrl) { + Document doc = Jsoup.parse(html); + SkiResort resort = new SkiResort(); + + String title = doc.title(); + resort.setName((title == null || title.isEmpty()) ? "UNKNOWN" : title); + resort.setSourceUrl(sourceUrl); + resort.setCrawledAt(LocalDateTime.now()); + + Element countryMeta = doc.selectFirst("meta[name=country]"); + if (countryMeta != null) { + resort.setCountry(countryMeta.attr("content")); + } + + Element regionMeta = doc.selectFirst("meta[name=region]"); + if (regionMeta != null) { + resort.setRegion(regionMeta.attr("content")); + } + + return resort; + } +} diff --git a/src/main/java/com/ski/crawler/repository/SkiResortRepository.java b/src/main/java/com/ski/crawler/repository/SkiResortRepository.java new file mode 100644 index 0000000..1255533 --- /dev/null +++ b/src/main/java/com/ski/crawler/repository/SkiResortRepository.java @@ -0,0 +1,74 @@ +package com.ski.crawler.repository; + +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.util.ValidationUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.LongAdder; + +public class SkiResortRepository { + private final Map byUrl = new LinkedHashMap<>(); + + public synchronized boolean containsUrl(String url) { + if (url == null || url.trim().isEmpty()) { + return false; + } + return byUrl.containsKey(url.trim()); + } + + public synchronized boolean add(SkiResort resort) { + if (resort == null) { + return false; + } + SkiResort cleaned = ValidationUtil.clean(resort); + ValidationUtil.validate(cleaned); + String url = cleaned.getSourceUrl().trim(); + if (byUrl.containsKey(url)) { + return false; + } + byUrl.put(url, cleaned); + return true; + } + + public synchronized List getAll() { + return Collections.unmodifiableList(new ArrayList<>(byUrl.values())); + } + + public synchronized List filterByCountry(String keyword) { + String k = ValidationUtil.normalizeCountryKey(keyword); + if (k.isEmpty()) { + return getAll(); + } + List out = new ArrayList<>(); + for (SkiResort r : byUrl.values()) { + String c = ValidationUtil.normalizeCountryKey(r.getCountry()); + if (!c.isEmpty() && (c.equals(k) || c.contains(k))) { + out.add(r); + } + } + return Collections.unmodifiableList(out); + } + + public Map countByCountry() { + Map tmp = new ConcurrentHashMap<>(); + for (SkiResort r : getAll()) { + String c = r.getCountry(); + if (c == null || c.trim().isEmpty()) { + continue; + } + String key = c.replace('\u00A0', ' ').trim(); + tmp.computeIfAbsent(key, x -> new LongAdder()).increment(); + } + Map out = new LinkedHashMap<>(); + for (Map.Entry e : tmp.entrySet()) { + out.put(e.getKey(), e.getValue().sum()); + } + return out; + } +} + diff --git a/src/main/java/com/ski/crawler/service/ScraperService.java b/src/main/java/com/ski/crawler/service/ScraperService.java new file mode 100644 index 0000000..e77c9d7 --- /dev/null +++ b/src/main/java/com/ski/crawler/service/ScraperService.java @@ -0,0 +1,243 @@ +package com.ski.crawler.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ski.crawler.exception.NetworkException; +import com.ski.crawler.exception.ParseException; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.repository.SkiResortRepository; +import com.ski.crawler.strategy.CrawlStrategy; +import com.ski.crawler.util.JsonUtil; +import com.ski.crawler.util.RetryUtil; +import com.ski.crawler.util.ValidationUtil; +import com.ski.crawler.utils.CrawlerHttp; +import com.ski.crawler.view.ConsoleView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.LongAdder; + +public class ScraperService { + private static final Logger log = LoggerFactory.getLogger(ScraperService.class); + + public CrawlReport crawl( + CrawlStrategy strategy, + String startUrl, + int limit, + int threads, + String countryFilter, + CrawlerHttp http, + SkiResortRepository repo, + boolean incremental, + String outPath, + ConsoleView view, + boolean showFailures, + boolean dryRun, + int retryAttempts, + long retrySleepMs + ) throws NetworkException { + String actualStartUrl = (startUrl == null || startUrl.isEmpty()) ? strategy.defaultStartUrl() : startUrl; + + List urls; + try { + urls = RetryUtil.retry(() -> strategy.collectDetailUrls(actualStartUrl, limit, http), retryAttempts, retrySleepMs); + } catch (NetworkException e) { + throw e; + } catch (Exception e) { + throw new NetworkException("Collect urls failed: " + e.getMessage(), e); + } + + int total = urls.size(); + Queue queue = new ConcurrentLinkedQueue<>(urls); + AtomicInteger done = new AtomicInteger(0); + AtomicInteger success = new AtomicInteger(0); + AtomicInteger skipped = new AtomicInteger(0); + AtomicInteger failed = new AtomicInteger(0); + AtomicInteger filteredOut = new AtomicInteger(0); + + Map byCountry = new ConcurrentHashMap<>(); + List failures = Collections.synchronizedList(new ArrayList<>()); + Map seenThisRun = new ConcurrentHashMap<>(); + Object outLock = new Object(); + + BufferedWriter outWriter = null; + ObjectMapper mapper = null; + if (outPath != null && !outPath.trim().isEmpty()) { + try { + outWriter = JsonUtil.openJsonlWriter(outPath.trim()); + mapper = JsonUtil.mapper(); + } catch (Exception e) { + throw new NetworkException("Open out file failed: " + e.getMessage(), e); + } + } + final BufferedWriter outWriterFinal = outWriter; + final ObjectMapper mapperFinal = mapper; + + view.printHeader(); + + int workerCount = Math.max(1, threads); + ExecutorService pool = Executors.newFixedThreadPool(workerCount); + for (int i = 0; i < workerCount; i++) { + pool.submit(() -> { + while (true) { + String url = queue.poll(); + if (url == null) { + return; + } + + try { + if (incremental) { + if (repo.containsUrl(url)) { + skipped.incrementAndGet(); + continue; + } + if (dryRun) { + if (seenThisRun.putIfAbsent(url, Boolean.TRUE) != null) { + skipped.incrementAndGet(); + continue; + } + } + } + + String html = RetryUtil.retry(() -> http.getHtml(url), retryAttempts, retrySleepMs); + SkiResort resort = strategy.parseDetail(url, html); + resort.setSourceSite(strategy.id()); + resort.setSourceUrl(url); + SkiResort cleaned = ValidationUtil.clean(resort); + ValidationUtil.validate(cleaned); + success.incrementAndGet(); + + String country = cleaned.getCountry(); + if (country != null && !country.trim().isEmpty()) { + String key = country.replace('\u00A0', ' ').trim(); + byCountry.computeIfAbsent(key, k -> new LongAdder()).increment(); + } + + String filter = ValidationUtil.normalizeCountryKey(countryFilter); + if (!filter.isEmpty()) { + String c = ValidationUtil.normalizeCountryKey(cleaned.getCountry()); + if (c.isEmpty() || (!c.equals(filter) && !c.contains(filter))) { + filteredOut.incrementAndGet(); + if (!dryRun) { + repo.add(cleaned); + } + continue; + } + } + + synchronized (outLock) { + view.printResort(cleaned); + if (!dryRun && outWriterFinal != null && mapperFinal != null) { + outWriterFinal.write(mapperFinal.writeValueAsString(toJson(cleaned))); + outWriterFinal.newLine(); + } + } + + if (!dryRun) { + repo.add(cleaned); + } + } catch (ParseException e) { + failed.incrementAndGet(); + if (showFailures && failures.size() < 200) { + failures.add(url + " [ParseException] " + safeMsg(e.getMessage())); + } + log.error("Parse failed: {}", url, e); + } catch (Exception e) { + failed.incrementAndGet(); + if (showFailures && failures.size() < 200) { + failures.add(url + " [" + e.getClass().getSimpleName() + "] " + safeMsg(e.getMessage())); + } + log.error("Crawl failed: {}", url, e); + } finally { + int finished = done.incrementAndGet(); + log.info("{}/{} success={} skipped={} failed={}", finished, total, success.get(), skipped.get(), failed.get()); + } + } + }); + } + + pool.shutdown(); + try { + pool.awaitTermination(7, TimeUnit.DAYS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + if (outWriterFinal != null) { + try (BufferedWriter w = outWriterFinal) { + w.flush(); + } catch (Exception ignored) { + } + } + + Map byCountryOut = new LinkedHashMap<>(); + for (Map.Entry e : byCountry.entrySet()) { + byCountryOut.put(e.getKey(), e.getValue().sum()); + } + + CrawlReport report = new CrawlReport(); + report.site = strategy.id(); + report.total = total; + report.success = success.get(); + report.filteredOut = filteredOut.get(); + report.skipped = skipped.get(); + report.failed = failed.get(); + report.byCountry = byCountryOut; + report.failures = new ArrayList<>(failures); + return report; + } + + private Map toJson(SkiResort r) { + Map obj = new LinkedHashMap<>(); + obj.put("id", r.getId()); + obj.put("name", r.getName()); + obj.put("country", r.getCountry()); + obj.put("region", r.getRegion()); + obj.put("latitude", r.getLatitude()); + obj.put("longitude", r.getLongitude()); + obj.put("altitudeMin", r.getAltitudeMin()); + obj.put("altitudeMax", r.getAltitudeMax()); + obj.put("totalKm", r.getTotalKm()); + obj.put("slopeCount", r.getSlopeCount()); + obj.put("liftCount", r.getLiftCount()); + obj.put("ticketPriceMin", r.getTicketPriceMin()); + obj.put("ticketPriceMax", r.getTicketPriceMax()); + obj.put("currency", r.getCurrency()); + obj.put("openTime", r.getOpenTime()); + obj.put("snowDepthCm", r.getSnowDepthCm()); + obj.put("temperatureC", r.getTemperatureC()); + obj.put("nearbyHotels", r.getNearbyHotels()); + obj.put("rentalShops", r.getRentalShops()); + obj.put("url", r.getSourceUrl()); + obj.put("sourceSite", r.getSourceSite()); + obj.put("crawlTime", r.getCrawledAt() == null ? null : r.getCrawledAt().toString()); + return obj; + } + + private String safeMsg(String s) { + return s == null ? "" : s.replace('\n', ' ').replace('\r', ' ').trim(); + } + + public static class CrawlReport { + public String site; + public int total; + public int success; + public int filteredOut; + public int skipped; + public int failed; + public Map byCountry; + public List failures; + } +} diff --git a/src/main/java/com/ski/crawler/site/CrawlerSite.java b/src/main/java/com/ski/crawler/site/CrawlerSite.java new file mode 100644 index 0000000..738970b --- /dev/null +++ b/src/main/java/com/ski/crawler/site/CrawlerSite.java @@ -0,0 +1,22 @@ +//站点抽象接口 :每个站点实现“列表采集 + 详情解析”两件事 +//每个站点需要实现以下方法: +//id():返回站点的唯一标识符,用于在命令行中指定要采集的站点。 +//defaultStartUrl():返回站点的默认采集起始 URL。 +//collectDetailUrls():采集站点的详情页 URL 列表,返回一个字符串列表。 +//parseDetail():解析详情页 HTML,返回一个 SkiResort 实例。 +package com.ski.crawler.site; + +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.utils.CrawlerHttp; + +import java.util.List; + +public interface CrawlerSite { + String id();// + + String defaultStartUrl();// + + List collectDetailUrls(String startUrl, int limit, CrawlerHttp http) throws Exception; + + SkiResort parseDetail(String sourceUrl, String html) throws Exception; +} diff --git a/src/main/java/com/ski/crawler/site/SkimapOrgSite.java b/src/main/java/com/ski/crawler/site/SkimapOrgSite.java new file mode 100644 index 0000000..5871ef5 --- /dev/null +++ b/src/main/java/com/ski/crawler/site/SkimapOrgSite.java @@ -0,0 +1,194 @@ +package com.ski.crawler.site; + +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.utils.CrawlerHttp; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SkimapOrgSite implements CrawlerSite { + private static final Pattern LAT_LON_PATTERN = Pattern.compile("(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)"); + + @Override + public String id() { + return "skimap"; + } + + @Override + public String defaultStartUrl() { + return "https://skimap.org"; + } + + @Override + public List collectDetailUrls(String startUrl, int limit, CrawlerHttp http) { + Set out = new LinkedHashSet<>(); + Set visited = new LinkedHashSet<>(); + + String page = startUrl; + while (page != null && !page.isEmpty() && !visited.contains(page)) { + visited.add(page); + if (page.toLowerCase(Locale.ROOT).contains("/skiareas/view/")) { + out.add(page); + break; + } + + Document doc = http.getDocument(page); + for (Element a : doc.select("a[href]")) { + String href = a.attr("href"); + if (href == null || href.isEmpty()) { + continue; + } + String abs = a.absUrl("href"); + if (abs == null || abs.isEmpty()) { + continue; + } + String lower = abs.toLowerCase(Locale.ROOT); + if (!lower.contains("/skiareas/view/")) { + continue; + } + out.add(abs); + if (limit > 0 && out.size() >= limit) { + return new ArrayList<>(out); + } + } + + String next = findNext(doc); + page = (next != null && !visited.contains(next)) ? next : null; + } + + return new ArrayList<>(out); + } + + @Override + public SkiResort parseDetail(String sourceUrl, String html) { + Document doc = org.jsoup.Jsoup.parse(html, sourceUrl); + SkiResort resort = new SkiResort(); + + String name = null; + Element h1 = doc.selectFirst("h1"); + if (h1 != null) { + name = clean(h1.text()); + } + if (name == null || name.isEmpty()) { + Element ogTitle = doc.selectFirst("meta[property=og:title]"); + if (ogTitle != null) { + name = clean(ogTitle.attr("content")); + } + } + if (name != null && !name.isEmpty()) { + resort.setName(name); + } + + List crumbs = new ArrayList<>(); + for (Element a : doc.select(".breadcrumb a, nav.breadcrumb a, ol.breadcrumb a, ul.breadcrumb a")) { + String t = clean(a.text()); + if (!t.isEmpty()) { + crumbs.add(t); + } + } + if (crumbs.size() >= 1) { + resort.setCountry(crumbs.get(crumbs.size() - 1)); + } + if (crumbs.size() >= 2) { + resort.setRegion(crumbs.get(crumbs.size() - 2)); + } + + Double[] latLon = extractLatLon(doc); + if (latLon != null) { + resort.setLatitude(latLon[0]); + resort.setLongitude(latLon[1]); + } + + return resort; + } + + private String findNext(Document doc) { + Element e = doc.selectFirst("a[rel=next], a.next, li.pagination-next a, a[aria-label=Next]"); + if (e != null) { + String abs = e.absUrl("href"); + return abs == null || abs.isEmpty() ? null : abs; + } + for (Element a : doc.select("a[href]")) { + String t = clean(a.text()).toLowerCase(Locale.ROOT); + if (t.equals("next") || t.equals("next ›") || t.contains("next")) { + String abs = a.absUrl("href"); + if (abs != null && !abs.isEmpty()) { + return abs; + } + } + } + return null; + } + + private Double[] extractLatLon(Document doc) { + Element metaLat = doc.selectFirst("meta[property=place:location:latitude], meta[name=geo.position]"); + Element metaLon = doc.selectFirst("meta[property=place:location:longitude]"); + if (metaLat != null && metaLon != null) { + Double lat = safeParseDouble(metaLat.attr("content")); + Double lon = safeParseDouble(metaLon.attr("content")); + if (lat != null && lon != null) { + return new Double[]{lat, lon}; + } + } + if (metaLat != null) { + Double[] ll = parseLatLon(metaLat.attr("content")); + if (ll != null) { + return ll; + } + } + Double[] ll = parseLatLon(doc.text()); + if (ll != null) { + return ll; + } + return null; + } + + private Double[] parseLatLon(String text) { + if (text == null || text.isEmpty()) { + return null; + } + Matcher m = LAT_LON_PATTERN.matcher(text); + while (m.find()) { + Double lat = safeParseDouble(m.group(1)); + Double lon = safeParseDouble(m.group(2)); + if (lat == null || lon == null) { + continue; + } + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { + continue; + } + return new Double[]{lat, lon}; + } + return null; + } + + private String clean(String s) { + if (s == null) { + return ""; + } + return s.replace('\u00A0', ' ').trim(); + } + + private Double safeParseDouble(String s) { + try { + if (s == null) { + return null; + } + String t = s.trim().replace(",", "."); + t = t.replaceAll("[^0-9.\\-]", ""); + if (t.isEmpty()) { + return null; + } + return Double.parseDouble(t); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/ski/crawler/site/SkiresortInfoSite.java b/src/main/java/com/ski/crawler/site/SkiresortInfoSite.java new file mode 100644 index 0000000..7b6cbef --- /dev/null +++ b/src/main/java/com/ski/crawler/site/SkiresortInfoSite.java @@ -0,0 +1,33 @@ +package com.ski.crawler.site; + +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.parser.ResortDetailParser; +import com.ski.crawler.spider.ResortListSpider; +import com.ski.crawler.utils.CrawlerHttp; + +import java.util.List; + +public class SkiresortInfoSite implements CrawlerSite { + private final ResortDetailParser detailParser = new ResortDetailParser(); + + @Override + public String id() { + return "skiresort"; + } + + @Override + public String defaultStartUrl() { + return "https://www.skiresort.info/ski-resorts/"; + } + + @Override + public List collectDetailUrls(String startUrl, int limit, CrawlerHttp http) throws Exception { + ResortListSpider listSpider = new ResortListSpider(http); + return limit > 0 ? listSpider.fetchFirst(startUrl, limit) : listSpider.fetchAll(startUrl); + } + + @Override + public SkiResort parseDetail(String sourceUrl, String html) throws Exception { + return detailParser.parse(html); + } +} diff --git a/src/main/java/com/ski/crawler/site/WikipediaSite.java b/src/main/java/com/ski/crawler/site/WikipediaSite.java new file mode 100644 index 0000000..1971bd1 --- /dev/null +++ b/src/main/java/com/ski/crawler/site/WikipediaSite.java @@ -0,0 +1,204 @@ +package com.ski.crawler.site; + +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.utils.CrawlerHttp; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class WikipediaSite implements CrawlerSite { + private static final Pattern GEO_SEMI_PATTERN = Pattern.compile("(-?\\d+(?:\\.\\d+)?)\\s*;\\s*(-?\\d+(?:\\.\\d+)?)"); + private static final Pattern GEO_COMMA_PATTERN = Pattern.compile("(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)"); + + @Override + public String id() { + return "wikipedia"; + } + + @Override + public String defaultStartUrl() { + return "https://en.wikipedia.org/wiki/List_of_ski_areas_and_resorts"; + } + + @Override + public List collectDetailUrls(String startUrl, int limit, CrawlerHttp http) { + Document doc = http.getDocument(startUrl); + Element content = doc.selectFirst("#mw-content-text"); + if (content == null) { + content = doc.body(); + } + + Set out = new LinkedHashSet<>(); + for (Element a : content.select("a[href]")) { + String href = a.attr("href"); + if (href == null || href.isEmpty()) { + continue; + } + if (!href.startsWith("/wiki/")) { + continue; + } + if (href.contains(":")) { + continue; + } + if (href.contains("#")) { + href = href.substring(0, href.indexOf('#')); + } + String abs = a.absUrl("href"); + if (abs == null || abs.isEmpty()) { + continue; + } + String lower = abs.toLowerCase(Locale.ROOT); + if (lower.contains("list_of_")) { + continue; + } + out.add(abs); + if (limit > 0 && out.size() >= limit) { + break; + } + } + return new ArrayList<>(out); + } + + @Override + public SkiResort parseDetail(String sourceUrl, String html) { + Document doc = org.jsoup.Jsoup.parse(html, sourceUrl); + SkiResort resort = new SkiResort(); + + Element h1 = doc.selectFirst("#firstHeading"); + if (h1 == null) { + h1 = doc.selectFirst("h1"); + } + if (h1 != null) { + String name = clean(h1.text()); + if (!name.isEmpty()) { + resort.setName(name); + } + } + + Element infobox = doc.selectFirst("table.infobox"); + if (infobox != null) { + String country = extractInfoboxValue(infobox, "Country"); + if (country != null && !country.isEmpty()) { + resort.setCountry(country); + } + String region = extractInfoboxValue(infobox, "Location"); + if (region != null && !region.isEmpty()) { + resort.setRegion(region); + } + } + + Double[] latLon = extractLatLon(doc); + if (latLon != null) { + resort.setLatitude(latLon[0]); + resort.setLongitude(latLon[1]); + } + + return resort; + } + + private Double[] extractLatLon(Document doc) { + Element geoDec = doc.selectFirst("span.geo-dec"); + if (geoDec != null) { + Double[] ll = parseLatLon(geoDec.text()); + if (ll != null) { + return ll; + } + } + Element geo = doc.selectFirst("span.geo"); + if (geo != null) { + Double[] ll = parseLatLon(geo.text()); + if (ll != null) { + return ll; + } + } + Element metaLat = doc.selectFirst("meta[property=place:location:latitude], meta[name=geo.position]"); + Element metaLon = doc.selectFirst("meta[property=place:location:longitude]"); + if (metaLat != null && metaLon != null) { + Double lat = safeParseDouble(metaLat.attr("content")); + Double lon = safeParseDouble(metaLon.attr("content")); + if (lat != null && lon != null) { + return new Double[]{lat, lon}; + } + } + if (metaLat != null) { + Double[] ll = parseLatLon(metaLat.attr("content")); + if (ll != null) { + return ll; + } + } + return null; + } + + private Double[] parseLatLon(String text) { + if (text == null || text.isEmpty()) { + return null; + } + Matcher m1 = GEO_SEMI_PATTERN.matcher(text); + if (m1.find()) { + Double lat = safeParseDouble(m1.group(1)); + Double lon = safeParseDouble(m1.group(2)); + if (lat != null && lon != null) { + return new Double[]{lat, lon}; + } + } + Matcher m2 = GEO_COMMA_PATTERN.matcher(text); + if (m2.find()) { + Double lat = safeParseDouble(m2.group(1)); + Double lon = safeParseDouble(m2.group(2)); + if (lat != null && lon != null) { + return new Double[]{lat, lon}; + } + } + return null; + } + + private String extractInfoboxValue(Element infobox, String header) { + for (Element row : infobox.select("tr")) { + Element th = row.selectFirst("th"); + Element td = row.selectFirst("td"); + if (th == null || td == null) { + continue; + } + String key = clean(th.text()); + if (!header.equalsIgnoreCase(key)) { + continue; + } + String value = clean(td.text()); + if (value.isEmpty()) { + return null; + } + return value; + } + return null; + } + + private String clean(String s) { + if (s == null) { + return ""; + } + return s.replace('\u00A0', ' ').trim(); + } + + private Double safeParseDouble(String s) { + try { + if (s == null) { + return null; + } + String t = s.trim().replace(",", "."); + t = t.replaceAll("[^0-9.\\-]", ""); + if (t.isEmpty()) { + return null; + } + return Double.parseDouble(t); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/ski/crawler/spider/ResortListSpider.java b/src/main/java/com/ski/crawler/spider/ResortListSpider.java new file mode 100644 index 0000000..7559362 --- /dev/null +++ b/src/main/java/com/ski/crawler/spider/ResortListSpider.java @@ -0,0 +1,98 @@ +//负责收集所有滑雪场的详情页地址 +package com.ski.crawler.spider; + +import com.ski.crawler.utils.CrawlerHttp; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.IOException; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.Set; +//ResortListSpider 类是爬虫的列表采集器,负责采集所有滑雪场的列表页。 +//它使用一个队列来存储待采集的 URL,每次从队列中取出一个 URL,然后使用 Jsoup 连接该 URL 并获取 HTML 内容。 +//最后,它解析 HTML 内容,提取出所有滑雪场的详情页 URL,并将它们添加到队列中。 +public class ResortListSpider { + private final CrawlerHttp http; + private final Random random = new Random(); + private final LinkedList queue = new LinkedList<>();// + + public ResortListSpider(CrawlerHttp http) { + this.http = http; + } + + public List fetchAll(String startUrl) throws IOException, InterruptedException { + return fetchFirst(startUrl, -1); + } + + public List fetchFirst(String startUrl, int limit) throws IOException, InterruptedException { + Set visitedPages = new HashSet<>(); + Set detailUrls = new HashSet<>(); + String page = startUrl; + + while (page != null && !visitedPages.contains(page)) { + visitedPages.add(page); + + Document doc = http.getDocument(page); + + Elements links = doc.select("a[href]"); + for (Element a : links) { + String href = a.attr("href"); + if (href == null || href.isEmpty()) { + continue; + } + if (href.startsWith("/ski-resort/") || href.startsWith("https://www.skiresort.info/ski-resort/")) { + String abs = a.absUrl("href"); + if (!abs.isEmpty()) { + detailUrls.add(abs); + if (limit > 0 && detailUrls.size() >= limit) { + break; + } + } + } + } + + if (limit > 0 && detailUrls.size() >= limit) { + break; + } + + String next = findNext(doc); + if (next != null && !visitedPages.contains(next)) { + page = next; + } else { + page = null; + } + + Thread.sleep(2000 + random.nextInt(2001)); + } + + queue.clear(); + queue.addAll(detailUrls); + return new LinkedList<>(queue); + } + + private String findNext(Document doc) { + Element e = doc.selectFirst("a[rel=next], a.next, li.pagination-next a"); + if (e != null) { + return e.absUrl("href"); + } + + for (Element a : doc.select("a[href]")) { + String t = a.text().toLowerCase(); + if (t.contains("next") || t.contains("下一页") || t.contains("weiter")) { + String abs = a.absUrl("href"); + if (!abs.isEmpty()) { + return abs; + } + } + } + return null; + } + + public LinkedList getQueue() { + return new LinkedList<>(queue); + } +} diff --git a/src/main/java/com/ski/crawler/strategy/CrawlStrategy.java b/src/main/java/com/ski/crawler/strategy/CrawlStrategy.java new file mode 100644 index 0000000..967214c --- /dev/null +++ b/src/main/java/com/ski/crawler/strategy/CrawlStrategy.java @@ -0,0 +1,19 @@ +package com.ski.crawler.strategy; + +import com.ski.crawler.exception.NetworkException; +import com.ski.crawler.exception.ParseException; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.utils.CrawlerHttp; + +import java.util.List; + +public interface CrawlStrategy { + String id(); + + String defaultStartUrl(); + + List collectDetailUrls(String startUrl, int limit, CrawlerHttp http) throws NetworkException; + + SkiResort parseDetail(String sourceUrl, String html) throws ParseException; +} + diff --git a/src/main/java/com/ski/crawler/strategy/SkiResortInfoStrategy.java b/src/main/java/com/ski/crawler/strategy/SkiResortInfoStrategy.java new file mode 100644 index 0000000..0e786a0 --- /dev/null +++ b/src/main/java/com/ski/crawler/strategy/SkiResortInfoStrategy.java @@ -0,0 +1,81 @@ +package com.ski.crawler.strategy; + +import com.ski.crawler.exception.NetworkException; +import com.ski.crawler.exception.ParseException; +import com.ski.crawler.model.SkiLift; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.model.SkiTicket; +import com.ski.crawler.model.SkiTrail; +import com.ski.crawler.parser.ResortDetailParser; +import com.ski.crawler.spider.ResortListSpider; +import com.ski.crawler.utils.CrawlerHttp; + +import java.util.List; + +public class SkiResortInfoStrategy implements CrawlStrategy { + private final ResortDetailParser detailParser = new ResortDetailParser(); + + @Override + public String id() { + return "skiresort"; + } + + @Override + public String defaultStartUrl() { + return "https://www.skiresort.info/ski-resorts/"; + } + + @Override + public List collectDetailUrls(String startUrl, int limit, CrawlerHttp http) throws NetworkException { + try { + ResortListSpider listSpider = new ResortListSpider(http); + return limit > 0 ? listSpider.fetchFirst(startUrl, limit) : listSpider.fetchAll(startUrl); + } catch (Exception e) { + throw new NetworkException("Collect urls failed: " + e.getMessage(), e); + } + } + + @Override + public SkiResort parseDetail(String sourceUrl, String html) throws ParseException { + try { + SkiResort resort = detailParser.parse(html); + resort.setSourceUrl(sourceUrl); + resort.setSourceSite(id()); + + SkiTrail trail = resort.getSkiTrail(); + if (trail != null && trail.getTotalRuns() != null) { + resort.setSlopeCount(trail.getTotalRuns()); + } + + SkiLift lift = resort.getSkiLift(); + if (lift != null && lift.getTotalLifts() != null) { + resort.setLiftCount(lift.getTotalLifts()); + } + + SkiTicket ticket = resort.getSkiTicket(); + if (ticket != null) { + Double a = ticket.getPriceAdult(); + Double c = ticket.getPriceChild(); + if (ticket.getCurrency() != null && resort.getCurrency() == null) { + resort.setCurrency(ticket.getCurrency()); + } + Double min = null; + Double max = null; + if (a != null) { + min = a; + max = a; + } + if (c != null) { + min = min == null ? c : Math.min(min, c); + max = max == null ? c : Math.max(max, c); + } + resort.setTicketPriceMin(min); + resort.setTicketPriceMax(max); + } + + return resort; + } catch (Exception e) { + throw new ParseException("Parse detail failed: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/ski/crawler/strategy/SkimapStrategy.java b/src/main/java/com/ski/crawler/strategy/SkimapStrategy.java new file mode 100644 index 0000000..8acb633 --- /dev/null +++ b/src/main/java/com/ski/crawler/strategy/SkimapStrategy.java @@ -0,0 +1,199 @@ +package com.ski.crawler.strategy; + +import com.ski.crawler.exception.NetworkException; +import com.ski.crawler.exception.ParseException; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.utils.CrawlerHttp; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SkimapStrategy implements CrawlStrategy { + private static final Pattern LAT_LON_PATTERN = Pattern.compile("(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)"); + + @Override + public String id() { + return "skimap"; + } + + @Override + public String defaultStartUrl() { + return "https://skimap.org"; + } + + @Override + public List collectDetailUrls(String startUrl, int limit, CrawlerHttp http) throws NetworkException { + try { + Set out = new LinkedHashSet<>(); + Set visited = new LinkedHashSet<>(); + + String page = startUrl; + while (page != null && !page.isEmpty() && !visited.contains(page)) { + visited.add(page); + if (page.toLowerCase(Locale.ROOT).contains("/skiareas/view/")) { + out.add(page); + break; + } + + Document doc = http.getDocument(page); + for (Element a : doc.select("a[href]")) { + String abs = a.absUrl("href"); + if (abs == null || abs.isEmpty()) { + continue; + } + String lower = abs.toLowerCase(Locale.ROOT); + if (!lower.contains("/skiareas/view/")) { + continue; + } + out.add(abs); + if (limit > 0 && out.size() >= limit) { + return new ArrayList<>(out); + } + } + + String next = findNext(doc); + page = (next != null && !visited.contains(next)) ? next : null; + } + + return new ArrayList<>(out); + } catch (Exception e) { + throw new NetworkException("Collect urls failed: " + e.getMessage(), e); + } + } + + @Override + public SkiResort parseDetail(String sourceUrl, String html) throws ParseException { + try { + Document doc = org.jsoup.Jsoup.parse(html, sourceUrl); + SkiResort resort = new SkiResort(); + resort.setSourceUrl(sourceUrl); + resort.setSourceSite(id()); + + String name = null; + Element h1 = doc.selectFirst("h1"); + if (h1 != null) { + name = clean(h1.text()); + } + if (name == null || name.isEmpty()) { + Element ogTitle = doc.selectFirst("meta[property=og:title]"); + if (ogTitle != null) { + name = clean(ogTitle.attr("content")); + } + } + if (name != null && !name.isEmpty()) { + resort.setName(name); + } + + List crumbs = new ArrayList<>(); + for (Element a : doc.select(".breadcrumb a, nav.breadcrumb a, ol.breadcrumb a, ul.breadcrumb a")) { + String t = clean(a.text()); + if (!t.isEmpty()) { + crumbs.add(t); + } + } + if (crumbs.size() >= 1) { + resort.setCountry(crumbs.get(crumbs.size() - 1)); + } + if (crumbs.size() >= 2) { + resort.setRegion(crumbs.get(crumbs.size() - 2)); + } + + Double[] latLon = extractLatLon(doc); + if (latLon != null) { + resort.setLatitude(latLon[0]); + resort.setLongitude(latLon[1]); + } + + return resort; + } catch (Exception e) { + throw new ParseException("Parse detail failed: " + e.getMessage(), e); + } + } + + private String findNext(Document doc) { + Element e = doc.selectFirst("a[rel=next], a.next, li.pagination-next a, a[aria-label=Next]"); + if (e != null) { + String abs = e.absUrl("href"); + return abs == null || abs.isEmpty() ? null : abs; + } + for (Element a : doc.select("a[href]")) { + String t = clean(a.text()).toLowerCase(Locale.ROOT); + if (t.equals("next") || t.equals("next ›") || t.contains("next")) { + String abs = a.absUrl("href"); + if (abs != null && !abs.isEmpty()) { + return abs; + } + } + } + return null; + } + + private Double[] extractLatLon(Document doc) { + Element metaLat = doc.selectFirst("meta[property=place:location:latitude], meta[name=geo.position]"); + Element metaLon = doc.selectFirst("meta[property=place:location:longitude]"); + if (metaLat != null && metaLon != null) { + Double lat = safeParseDouble(metaLat.attr("content")); + Double lon = safeParseDouble(metaLon.attr("content")); + if (lat != null && lon != null) { + return new Double[]{lat, lon}; + } + } + if (metaLat != null) { + Double[] ll = parseLatLon(metaLat.attr("content")); + if (ll != null) { + return ll; + } + } + return parseLatLon(doc.text()); + } + + private Double[] parseLatLon(String text) { + if (text == null || text.isEmpty()) { + return null; + } + Matcher m = LAT_LON_PATTERN.matcher(text); + while (m.find()) { + Double lat = safeParseDouble(m.group(1)); + Double lon = safeParseDouble(m.group(2)); + if (lat == null || lon == null) { + continue; + } + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { + continue; + } + return new Double[]{lat, lon}; + } + return null; + } + + private String clean(String s) { + if (s == null) { + return ""; + } + return s.replace('\u00A0', ' ').trim(); + } + + private Double safeParseDouble(String s) { + try { + if (s == null) { + return null; + } + String t = s.trim().replace(",", "."); + t = t.replaceAll("[^0-9.\\-]", ""); + if (t.isEmpty()) { + return null; + } + return Double.parseDouble(t); + } catch (Exception e) { + return null; + } + } +} + diff --git a/src/main/java/com/ski/crawler/strategy/WikipediaStrategy.java b/src/main/java/com/ski/crawler/strategy/WikipediaStrategy.java new file mode 100644 index 0000000..3848c98 --- /dev/null +++ b/src/main/java/com/ski/crawler/strategy/WikipediaStrategy.java @@ -0,0 +1,244 @@ +package com.ski.crawler.strategy; + +import com.ski.crawler.exception.NetworkException; +import com.ski.crawler.exception.ParseException; +import com.ski.crawler.model.SkiResort; +import com.ski.crawler.utils.CrawlerHttp; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class WikipediaStrategy implements CrawlStrategy { + private static final Pattern GEO_SEMI_PATTERN = Pattern.compile("(-?\\d+(?:\\.\\d+)?)\\s*;\\s*(-?\\d+(?:\\.\\d+)?)"); + private static final Pattern GEO_COMMA_PATTERN = Pattern.compile("(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)"); + private static final Pattern INT_M_PATTERN = Pattern.compile("(\\d{2,5})\\s*m\\b", Pattern.CASE_INSENSITIVE); + + @Override + public String id() { + return "wikipedia"; + } + + @Override + public String defaultStartUrl() { + return "https://en.wikipedia.org/wiki/List_of_ski_areas_and_resorts"; + } + + @Override + public List collectDetailUrls(String startUrl, int limit, CrawlerHttp http) throws NetworkException { + try { + Document doc = http.getDocument(startUrl); + Element content = doc.selectFirst("#mw-content-text"); + if (content == null) { + content = doc.body(); + } + + Set out = new LinkedHashSet<>(); + for (Element a : content.select("a[href]")) { + String href = a.attr("href"); + if (href == null || href.isEmpty()) { + continue; + } + if (!href.startsWith("/wiki/")) { + continue; + } + if (href.contains(":")) { + continue; + } + if (href.contains("#")) { + href = href.substring(0, href.indexOf('#')); + } + String abs = a.absUrl("href"); + if (abs == null || abs.isEmpty()) { + continue; + } + String lower = abs.toLowerCase(Locale.ROOT); + if (lower.contains("list_of_")) { + continue; + } + out.add(abs); + if (limit > 0 && out.size() >= limit) { + break; + } + } + return new ArrayList<>(out); + } catch (Exception e) { + throw new NetworkException("Collect urls failed: " + e.getMessage(), e); + } + } + + @Override + public SkiResort parseDetail(String sourceUrl, String html) throws ParseException { + try { + Document doc = org.jsoup.Jsoup.parse(html, sourceUrl); + SkiResort resort = new SkiResort(); + resort.setSourceUrl(sourceUrl); + resort.setSourceSite(id()); + + Element h1 = doc.selectFirst("#firstHeading"); + if (h1 == null) { + h1 = doc.selectFirst("h1"); + } + if (h1 != null) { + String name = clean(h1.text()); + if (!name.isEmpty()) { + resort.setName(name); + } + } + + Element infobox = doc.selectFirst("table.infobox"); + if (infobox != null) { + String country = extractInfoboxValue(infobox, "Country"); + if (country != null && !country.isEmpty()) { + resort.setCountry(country); + } + String location = extractInfoboxValue(infobox, "Location"); + if (location != null && !location.isEmpty()) { + resort.setRegion(location); + } + + Integer top = extractElevation(infobox, "Top elevation", "Highest elevation"); + Integer base = extractElevation(infobox, "Base elevation", "Lowest elevation"); + if (base != null) { + resort.setAltitudeMin(base); + } + if (top != null) { + resort.setAltitudeMax(top); + } + } + + Double[] latLon = extractLatLon(doc); + if (latLon != null) { + resort.setLatitude(latLon[0]); + resort.setLongitude(latLon[1]); + } + + return resort; + } catch (Exception e) { + throw new ParseException("Parse detail failed: " + e.getMessage(), e); + } + } + + private Double[] extractLatLon(Document doc) { + Element geoDec = doc.selectFirst("span.geo-dec"); + if (geoDec != null) { + Double[] ll = parseLatLon(geoDec.text()); + if (ll != null) { + return ll; + } + } + Element geo = doc.selectFirst("span.geo"); + if (geo != null) { + Double[] ll = parseLatLon(geo.text()); + if (ll != null) { + return ll; + } + } + Element metaLat = doc.selectFirst("meta[property=place:location:latitude], meta[name=geo.position]"); + Element metaLon = doc.selectFirst("meta[property=place:location:longitude]"); + if (metaLat != null && metaLon != null) { + Double lat = safeParseDouble(metaLat.attr("content")); + Double lon = safeParseDouble(metaLon.attr("content")); + if (lat != null && lon != null) { + return new Double[]{lat, lon}; + } + } + if (metaLat != null) { + Double[] ll = parseLatLon(metaLat.attr("content")); + if (ll != null) { + return ll; + } + } + return null; + } + + private Double[] parseLatLon(String text) { + if (text == null || text.isEmpty()) { + return null; + } + Matcher m1 = GEO_SEMI_PATTERN.matcher(text); + if (m1.find()) { + Double lat = safeParseDouble(m1.group(1)); + Double lon = safeParseDouble(m1.group(2)); + if (lat != null && lon != null) { + return new Double[]{lat, lon}; + } + } + Matcher m2 = GEO_COMMA_PATTERN.matcher(text); + if (m2.find()) { + Double lat = safeParseDouble(m2.group(1)); + Double lon = safeParseDouble(m2.group(2)); + if (lat != null && lon != null) { + return new Double[]{lat, lon}; + } + } + return null; + } + + private Integer extractElevation(Element infobox, String... headers) { + for (String h : headers) { + String v = extractInfoboxValue(infobox, h); + if (v == null || v.isEmpty()) { + continue; + } + Matcher m = INT_M_PATTERN.matcher(v); + if (m.find()) { + try { + return Integer.parseInt(m.group(1)); + } catch (Exception ignored) { + } + } + } + return null; + } + + private String extractInfoboxValue(Element infobox, String header) { + for (Element row : infobox.select("tr")) { + Element th = row.selectFirst("th"); + Element td = row.selectFirst("td"); + if (th == null || td == null) { + continue; + } + String key = clean(th.text()); + if (!header.equalsIgnoreCase(key)) { + continue; + } + String value = clean(td.text()); + if (value.isEmpty()) { + return null; + } + return value; + } + return null; + } + + private String clean(String s) { + if (s == null) { + return ""; + } + return s.replace('\u00A0', ' ').trim(); + } + + private Double safeParseDouble(String s) { + try { + if (s == null) { + return null; + } + String t = s.trim().replace(",", "."); + t = t.replaceAll("[^0-9.\\-]", ""); + if (t.isEmpty()) { + return null; + } + return Double.parseDouble(t); + } catch (Exception e) { + return null; + } + } +} + diff --git a/src/main/java/com/ski/crawler/util/CliArgs.java b/src/main/java/com/ski/crawler/util/CliArgs.java new file mode 100644 index 0000000..bca51d0 --- /dev/null +++ b/src/main/java/com/ski/crawler/util/CliArgs.java @@ -0,0 +1,74 @@ +package com.ski.crawler.util; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class CliArgs { + public static Map parseOptions(String[] args, int startIndex) { + Map out = new HashMap<>(); + if (args == null) { + return out; + } + for (int i = startIndex; i < args.length; i++) { + String a = args[i]; + if (a == null) { + continue; + } + String t = a.trim(); + if (!t.startsWith("--")) { + continue; + } + String body = t.substring(2); + String key; + String value; + int eq = body.indexOf('='); + if (eq >= 0) { + key = body.substring(0, eq).trim().toLowerCase(Locale.ROOT); + value = body.substring(eq + 1).trim(); + } else { + key = body.trim().toLowerCase(Locale.ROOT); + if (i + 1 < args.length && args[i + 1] != null && !args[i + 1].trim().startsWith("--")) { + value = args[++i].trim(); + } else { + value = "true"; + } + } + if (!key.isEmpty()) { + out.put(key, value); + } + } + return out; + } + + public static int parseInt(String v, int def) { + try { + if (v == null || v.trim().isEmpty()) { + return def; + } + return Integer.parseInt(v.trim()); + } catch (Exception e) { + return def; + } + } + + public static Integer parseNullableInt(String v) { + try { + if (v == null || v.trim().isEmpty()) { + return null; + } + return Integer.parseInt(v.trim()); + } catch (Exception e) { + return null; + } + } + + public static boolean parseBoolean(String v) { + if (v == null) { + return false; + } + String t = v.trim().toLowerCase(Locale.ROOT); + return t.equals("true") || t.equals("1") || t.equals("yes") || t.equals("y") || t.equals("on"); + } +} + diff --git a/src/main/java/com/ski/crawler/util/ExcelUtil.java b/src/main/java/com/ski/crawler/util/ExcelUtil.java new file mode 100644 index 0000000..de233ca --- /dev/null +++ b/src/main/java/com/ski/crawler/util/ExcelUtil.java @@ -0,0 +1,179 @@ +package com.ski.crawler.util; + +import com.ski.crawler.model.SkiResort; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.FileOutputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class ExcelUtil { + private static final List DEFAULT_SHEETS = Arrays.asList("skiresort", "wikipedia", "skimap"); + + public static void exportResortsBySiteToXlsx(List resorts, String path) throws Exception { + Map> bySite = new LinkedHashMap<>(); + for (String s : DEFAULT_SHEETS) { + bySite.put(s, new ArrayList<>()); + } + List other = new ArrayList<>(); + if (resorts != null) { + for (SkiResort r : resorts) { + String site = normalizeSite(r == null ? null : r.getSourceSite()); + if (bySite.containsKey(site)) { + bySite.get(site).add(r); + } else { + other.add(r); + } + } + } + + try (Workbook wb = new XSSFWorkbook()) { + CellStyle headerStyle = createHeaderStyle(wb); + for (Map.Entry> e : bySite.entrySet()) { + writeSheet(wb, headerStyle, e.getKey(), e.getValue()); + } + if (!other.isEmpty()) { + writeSheet(wb, headerStyle, "other", other); + } + try (FileOutputStream out = new FileOutputStream(path)) { + wb.write(out); + } + } + } + + private static void writeSheet(Workbook wb, CellStyle headerStyle, String sheetName, List rows) { + Sheet sheet = wb.createSheet(safeSheetName(sheetName)); + sheet.createFreezePane(0, 1); + + int r = 0; + Row header = sheet.createRow(r++); + String[] cols = new String[]{ + "sourceSite", "name", "country", "region", + "latitude", "longitude", + "altitudeMin", "altitudeMax", + "totalKm", "slopeCount", "liftCount", + "ticketPriceMin", "ticketPriceMax", "currency", + "overallScore", + "url", + "crawlTime" + }; + for (int i = 0; i < cols.length; i++) { + Cell c = header.createCell(i); + c.setCellValue(cols[i]); + c.setCellStyle(headerStyle); + } + + if (rows != null) { + for (SkiResort sr : rows) { + if (sr == null) { + continue; + } + Row row = sheet.createRow(r++); + int i = 0; + setCell(row, i++, sr.getSourceSite()); + setCell(row, i++, sr.getName()); + setCell(row, i++, sr.getCountry()); + setCell(row, i++, sr.getRegion()); + setCell(row, i++, sr.getLatitude()); + setCell(row, i++, sr.getLongitude()); + setCell(row, i++, sr.getAltitudeMin()); + setCell(row, i++, sr.getAltitudeMax()); + setCell(row, i++, sr.getTotalKm()); + setCell(row, i++, sr.getSlopeCount()); + setCell(row, i++, sr.getLiftCount()); + setCell(row, i++, sr.getTicketPriceMin()); + setCell(row, i++, sr.getTicketPriceMax()); + setCell(row, i++, sr.getCurrency()); + setCell(row, i++, sr.getOverallScore()); + setCell(row, i++, sr.getSourceUrl()); + setCell(row, i, sr.getCrawledAt() == null ? null : sr.getCrawledAt().toString()); + } + } + + int[] widths = new int[]{ + 12, 28, 14, 18, + 12, 12, + 12, 12, + 10, 10, 10, + 14, 14, 10, + 12, + 40, + 22 + }; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, Math.min(255, Math.max(8, widths[i])) * 256); + } + } + + private static CellStyle createHeaderStyle(Workbook wb) { + CellStyle style = wb.createCellStyle(); + Font font = wb.createFont(); + font.setBold(true); + style.setFont(font); + return style; + } + + private static void setCell(Row row, int col, Object v) { + Cell cell = row.createCell(col); + if (v == null) { + return; + } + if (v instanceof Integer) { + cell.setCellValue(((Integer) v).doubleValue()); + return; + } + if (v instanceof Long) { + cell.setCellValue(((Long) v).doubleValue()); + return; + } + if (v instanceof Double) { + cell.setCellValue((Double) v); + return; + } + if (v instanceof Float) { + cell.setCellValue(((Float) v).doubleValue()); + return; + } + if (v instanceof BigDecimal) { + cell.setCellValue(((BigDecimal) v).doubleValue()); + return; + } + cell.setCellValue(String.valueOf(v)); + } + + private static String normalizeSite(String s) { + if (s == null) { + return ""; + } + return s.trim().toLowerCase(Locale.ROOT); + } + + private static String safeSheetName(String name) { + String n = name == null ? "sheet" : name.trim(); + if (n.isEmpty()) { + n = "sheet"; + } + n = n.replace(':', '-') + .replace('\\', '-') + .replace('/', '-') + .replace('?', '-') + .replace('*', '-') + .replace('[', '(') + .replace(']', ')'); + if (n.length() > 31) { + n = n.substring(0, 31); + } + return n; + } +} diff --git a/src/main/java/com/ski/crawler/util/JsonUtil.java b/src/main/java/com/ski/crawler/util/JsonUtil.java new file mode 100644 index 0000000..1a7508b --- /dev/null +++ b/src/main/java/com/ski/crawler/util/JsonUtil.java @@ -0,0 +1,43 @@ +package com.ski.crawler.util; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class JsonUtil { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static ObjectMapper mapper() { + return MAPPER; + } + + public static BufferedWriter openJsonlWriter(String path) throws Exception { + return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(path), StandardCharsets.UTF_8)); + } + + public static BufferedReader openJsonlReader(String path) throws Exception { + return new BufferedReader(new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8)); + } + + public static List readAllLines(String path) throws Exception { + List out = new ArrayList<>(); + try (BufferedReader br = openJsonlReader(path)) { + String line; + while ((line = br.readLine()) != null) { + if (!line.trim().isEmpty()) { + out.add(line); + } + } + } + return out; + } +} + diff --git a/src/main/java/com/ski/crawler/util/RetryUtil.java b/src/main/java/com/ski/crawler/util/RetryUtil.java new file mode 100644 index 0000000..fa0be2f --- /dev/null +++ b/src/main/java/com/ski/crawler/util/RetryUtil.java @@ -0,0 +1,33 @@ +package com.ski.crawler.util; + +import com.ski.crawler.exception.NetworkException; + +import java.util.concurrent.Callable; + +public class RetryUtil { + public static T retry(Callable task, int maxAttempts, long baseSleepMs) throws Exception { + Exception last = null; + int attempts = Math.max(1, maxAttempts); + for (int i = 1; i <= attempts; i++) { + try { + return task.call(); + } catch (Exception e) { + last = e; + if (i == attempts) { + throw e; + } + long sleep = baseSleepMs <= 0 ? 0 : baseSleepMs; + if (sleep > 0) { + try { + Thread.sleep(sleep); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new NetworkException("Retry interrupted", ie); + } + } + } + } + throw last == null ? new NetworkException("Retry failed") : last; + } +} + diff --git a/src/main/java/com/ski/crawler/util/ValidationUtil.java b/src/main/java/com/ski/crawler/util/ValidationUtil.java new file mode 100644 index 0000000..bf82e71 --- /dev/null +++ b/src/main/java/com/ski/crawler/util/ValidationUtil.java @@ -0,0 +1,60 @@ +package com.ski.crawler.util; + +import com.ski.crawler.model.SkiResort; + +import java.util.Locale; + +public class ValidationUtil { + public static SkiResort clean(SkiResort r) { + if (r == null) { + return null; + } + r.setName(trimToNull(r.getName())); + r.setCountry(trimToNull(r.getCountry())); + r.setRegion(trimToNull(r.getRegion())); + r.setSourceUrl(trimToNull(r.getSourceUrl())); + r.setSourceSite(trimToNull(r.getSourceSite())); + + if (r.getLatitude() != null && (r.getLatitude() < -90 || r.getLatitude() > 90)) { + r.setLatitude(null); + } + if (r.getLongitude() != null && (r.getLongitude() < -180 || r.getLongitude() > 180)) { + r.setLongitude(null); + } + if (r.getTicketPriceMin() != null && r.getTicketPriceMin() < 0) { + r.setTicketPriceMin(null); + } + if (r.getTicketPriceMax() != null && r.getTicketPriceMax() < 0) { + r.setTicketPriceMax(null); + } + return r; + } + + public static void validate(SkiResort r) { + if (r == null) { + throw new IllegalArgumentException("SkiResort is null"); + } + if (r.getSourceUrl() == null || r.getSourceUrl().isEmpty()) { + throw new IllegalArgumentException("sourceUrl is empty"); + } + if (r.getName() == null || r.getName().isEmpty()) { + throw new IllegalArgumentException("name is empty"); + } + } + + public static String normalizeCountryKey(String country) { + if (country == null) { + return ""; + } + return country.replace('\u00A0', ' ').trim().toLowerCase(Locale.ROOT); + } + + private static String trimToNull(String s) { + if (s == null) { + return null; + } + String t = s.replace('\u00A0', ' ').trim(); + return t.isEmpty() ? null : t; + } +} + diff --git a/src/main/java/com/ski/crawler/utils/CrawlerHttp.java b/src/main/java/com/ski/crawler/utils/CrawlerHttp.java new file mode 100644 index 0000000..a0f77fd --- /dev/null +++ b/src/main/java/com/ski/crawler/utils/CrawlerHttp.java @@ -0,0 +1,52 @@ +//统一 HTTP 配置 :UA/代理/超时集中管理,避免各处硬编码 +package com.ski.crawler.utils; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +import java.net.InetSocketAddress; +import java.net.Socket; + +public class CrawlerHttp { + private final String userAgent; + private final String proxyHost; + private final int proxyPort; + private final boolean proxyEnabled; + private final int timeoutMs; + + public CrawlerHttp(String userAgent, String proxyHost, int proxyPort, boolean proxyEnabled, int timeoutMs) { + this.userAgent = userAgent; + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.proxyEnabled = proxyEnabled; + this.timeoutMs = timeoutMs; + } + + public Document getDocument(String url) { + org.jsoup.Connection conn = Jsoup.connect(url) + .userAgent(userAgent) + .timeout(timeoutMs) + .followRedirects(true); + if (proxyEnabled && proxyHost != null && !proxyHost.isEmpty() && proxyPort > 0 && isProxyReachable(proxyHost, proxyPort, 300)) { + conn = conn.proxy(proxyHost, proxyPort); + } + try { + return conn.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getHtml(String url) { + return getDocument(url).outerHtml(); + } + + private boolean isProxyReachable(String host, int port, int timeoutMs) { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(host, port), timeoutMs); + return true; + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/ski/crawler/utils/HttpClientUtil.java b/src/main/java/com/ski/crawler/utils/HttpClientUtil.java new file mode 100644 index 0000000..671e91f --- /dev/null +++ b/src/main/java/com/ski/crawler/utils/HttpClientUtil.java @@ -0,0 +1,27 @@ +//网络请求工具 +package com.ski.crawler.utils; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class HttpClientUtil { + public String get(String url) throws IOException { + HttpGet request = new HttpGet(url); + + try (CloseableHttpClient client = HttpClients.createDefault(); + CloseableHttpResponse response = client.execute(request)) { + HttpEntity entity = response.getEntity(); + if (entity == null) { + return ""; + } + return EntityUtils.toString(entity, StandardCharsets.UTF_8); + } + } +} diff --git a/src/main/java/com/ski/crawler/view/ConsoleView.java b/src/main/java/com/ski/crawler/view/ConsoleView.java new file mode 100644 index 0000000..a1f288c --- /dev/null +++ b/src/main/java/com/ski/crawler/view/ConsoleView.java @@ -0,0 +1,336 @@ +package com.ski.crawler.view; + +import com.ski.crawler.model.SkiResort; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ConsoleView { + private final boolean color; + private final int width; + private final TablePrinter table; + + public ConsoleView(int width, boolean color) { + this.width = Math.max(60, width); + this.color = color; + this.table = new TablePrinter(this.width, this.color); + } + + public void printHeader() { + table.printHeader(); + } + + public void printResort(SkiResort r) { + table.printRow(r); + } + + public void printSummary(Map summary, Map byCountry, List failures) { + System.err.println("---- summary ----"); + for (Map.Entry e : summary.entrySet()) { + System.err.println(e.getKey() + "=" + (e.getValue() == null ? "" : e.getValue())); + } + if (byCountry != null && !byCountry.isEmpty()) { + System.err.println("by country:"); + for (Map.Entry e : byCountry.entrySet()) { + System.err.println(" " + e.getKey() + ": " + e.getValue()); + } + } + if (failures != null && !failures.isEmpty()) { + System.err.println("failures:"); + for (String f : failures) { + System.err.println(" " + f); + } + } + } + + private static class TablePrinter { + private final int width; + private final boolean color; + private boolean headerPrinted; + + private TablePrinter(int width, boolean color) { + this.width = width; + this.color = color; + } + + private void printHeader() { + if (headerPrinted) { + return; + } + headerPrinted = true; + List cols = columns(); + String line = formatRow(cols, new String[]{"SITE", "NAME", "COUNTRY", "REGION", "COORD", "ALT", "KM", "LIFTS", "PRICE", "SCORE", "URL"}, true); + System.out.println(line); + System.out.println(repeat("-", displayWidth(stripAnsi(line)))); + } + + private void printRow(SkiResort r) { + List cols = columns(); + String coord = formatCoord(r.getLatitude(), r.getLongitude()); + String alt = formatAlt(r.getAltitudeMin(), r.getAltitudeMax()); + String km = r.getTotalKm() == null ? "" : String.valueOf(r.getTotalKm()); + String lifts = r.getLiftCount() == null ? "" : String.valueOf(r.getLiftCount()); + String price = formatPrice(r.getTicketPriceMin(), r.getTicketPriceMax(), r.getCurrency()); + String score = r.getOverallScore() == null ? "" : r.getOverallScore().toPlainString(); + String line = formatRow(cols, new String[]{ + safe0(r.getSourceSite()), + safe0(r.getName()), + safe0(r.getCountry()), + safe0(r.getRegion()), + coord, + alt, + km, + lifts, + price, + score, + safe0(r.getSourceUrl()) + }, false); + System.out.println(line); + } + + private List columns() { + List cols = new ArrayList<>(); + cols.add(new Col("SITE", 6, 10, 1)); + cols.add(new Col("NAME", 10, 26, 3)); + cols.add(new Col("COUNTRY", 6, 14, 2)); + cols.add(new Col("REGION", 6, 16, 2)); + cols.add(new Col("COORD", 10, 22, 2)); + cols.add(new Col("ALT", 5, 12, 1)); + cols.add(new Col("KM", 2, 8, 1)); + cols.add(new Col("LIFTS", 4, 8, 1)); + cols.add(new Col("PRICE", 4, 14, 1)); + cols.add(new Col("SCORE", 4, 8, 1)); + cols.add(new Col("URL", 10, 200, 4)); + allocate(cols, width); + return cols; + } + + private String formatRow(List cols, String[] values, boolean header) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < cols.size() && i < values.length; i++) { + Col c = cols.get(i); + String v = values[i] == null ? "" : values[i]; + String cell = padRight(truncate(v, c.width), c.width); + if (header && color) { + cell = Ansi.cyan(cell); + } + sb.append(cell); + if (i != cols.size() - 1) { + sb.append(" "); + } + } + return sb.toString(); + } + + private void allocate(List cols, int totalWidth) { + int gaps = (cols.size() - 1) * 2; + int available = Math.max(20, totalWidth - gaps); + int minSum = 0; + for (Col c : cols) { + c.width = c.min; + minSum += c.width; + } + int remaining = available - minSum; + if (remaining <= 0) { + return; + } + + int totalWeight = 0; + for (Col c : cols) { + totalWeight += c.weight; + } + for (int loop = 0; loop < 3 && remaining > 0; loop++) { + boolean any = false; + for (Col c : cols) { + if (remaining <= 0) { + break; + } + int maxAdd = c.max - c.width; + if (maxAdd <= 0) { + continue; + } + int add = Math.max(1, remaining * c.weight / Math.max(1, totalWeight)); + add = Math.min(add, maxAdd); + c.width += add; + remaining -= add; + any = true; + } + if (!any) { + break; + } + } + int idx = cols.size() - 1; + while (remaining > 0 && idx >= 0) { + Col c = cols.get(idx); + int maxAdd = c.max - c.width; + if (maxAdd > 0) { + int add = Math.min(maxAdd, remaining); + c.width += add; + remaining -= add; + } + idx--; + } + } + + private String formatCoord(Double lat, Double lon) { + if (lat == null || lon == null) { + return ""; + } + return String.format("%.5f,%.5f", lat, lon); + } + + private String formatAlt(Integer min, Integer max) { + if (min == null && max == null) { + return ""; + } + if (min != null && max != null) { + return min + "-" + max + "m"; + } + if (min != null) { + return min + "m"; + } + return max + "m"; + } + + private String formatPrice(Double min, Double max, String currency) { + if (min == null && max == null) { + return ""; + } + String cur = currency == null ? "" : currency.trim(); + String range; + if (min != null && max != null) { + range = stripTrailingZeros(min) + "-" + stripTrailingZeros(max); + } else if (min != null) { + range = stripTrailingZeros(min); + } else { + range = stripTrailingZeros(max); + } + return cur.isEmpty() ? range : cur + " " + range; + } + + private String stripTrailingZeros(Double v) { + try { + return BigDecimal.valueOf(v).stripTrailingZeros().toPlainString(); + } catch (Exception e) { + return String.valueOf(v); + } + } + } + + private static class Col { + private final int min; + private final int max; + private final int weight; + private int width; + + private Col(String name, int min, int max, int weight) { + this.min = min; + this.max = max; + this.weight = weight; + } + } + + private static class Ansi { + private static String wrap(String code, String s) { + return "\u001B[" + code + "m" + s + "\u001B[0m"; + } + + private static String cyan(String s) { + return wrap("36", s); + } + } + + private static String safe0(String s) { + if (s == null) { + return ""; + } + return s.replace('\t', ' ').trim(); + } + + private static String padRight(String s, int width) { + int w = displayWidth(s); + if (w >= width) { + return s; + } + StringBuilder sb = new StringBuilder(s); + for (int i = 0; i < (width - w); i++) { + sb.append(' '); + } + return sb.toString(); + } + + private static String truncate(String s, int width) { + if (s == null) { + return ""; + } + if (displayWidth(s) <= width) { + return s; + } + String ell = "..."; + int target = Math.max(0, width - displayWidth(ell)); + StringBuilder sb = new StringBuilder(); + int w = 0; + for (int i = 0; i < s.length(); ) { + int cp = s.codePointAt(i); + String ch = new String(Character.toChars(cp)); + int cw = displayWidth(ch); + if (w + cw > target) { + break; + } + sb.append(ch); + w += cw; + i += Character.charCount(cp); + } + sb.append(ell); + return sb.toString(); + } + + private static int displayWidth(String s) { + if (s == null || s.isEmpty()) { + return 0; + } + int w = 0; + for (int i = 0; i < s.length(); ) { + int cp = s.codePointAt(i); + if (cp == 27) { + int m = s.indexOf('m', i); + if (m > i) { + i = m + 1; + continue; + } + } + if (cp <= 0x1F || (cp >= 0x7F && cp <= 0x9F)) { + i += Character.charCount(cp); + continue; + } + if (cp <= 0x7F) { + w += 1; + } else { + w += 2; + } + i += Character.charCount(cp); + } + return w; + } + + private static String repeat(String s, int n) { + if (n <= 0) { + return ""; + } + StringBuilder sb = new StringBuilder(n * s.length()); + for (int i = 0; i < n; i++) { + sb.append(s); + } + return sb.toString(); + } + + private static String stripAnsi(String s) { + if (s == null) { + return ""; + } + return s.replaceAll("\\u001B\\[[;\\d]*m", ""); + } +} + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..7b62fdf --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + + + + diff --git a/target/classes/com/ski/crawler/Main.class b/target/classes/com/ski/crawler/Main.class new file mode 100644 index 0000000000000000000000000000000000000000..9a4feb9b211d41c2fc84e1fd486ca5f7f432d17f GIT binary patch literal 2635 zcma)8%W@M(6g}Oxxh9 zR8}mhf+f4K%bF~36{_R|@f7Dt+@5>q-qZ8fKd=7;FpGxg){tmiP?ZPd&)( zJcFe|*DqwI?ituGFte8$%5o67DhRjIR?)p9qaajw8fzw!h#MFbIK7v+YCAQS3T-6| zTMM&Udll$B*@)utwfHQ|uJ)Lw!U_$mxMo!x9L%UQ0-N6N$~ zj_BMS6?mL#N=N6IGEMk4p)>ULQF`Tu^I61A979GI*ky*mc5?PK zJLkLK6DvS7t|agYK4q7O8*Ia?t=h1Kr#3ZCpK)BnX9luwXo|HJDOpNj9t%84hM~Yv zt=-)qeVe_m*zwXXWlSvMhJot>13PLk|HzSLr`>tA}6)D?S)TC3XW@Za#Chp*_0qehKdh?MJ_zILqqpYLw zn8Kk}UbTSQVS$GZw9q+j>h>2n-<0bAkp&)H7XnIA6S@ zy5?|^3h2pbA{PY3jdG^Buy(DyLA<+FmM!4BtE6@#XLgrn-9^^+Jd3K4>F?T-`JGS;8OOoaX5Rr_wD^g@ ze}l6$N5Qf8%r;^#tJ^03^)|2{@9;lS1905OSsd@8pDQE_oBw@Wdr?6$zs)a<3I;e1 zEmbhgapYzNqxtEx=eBWpuBU=y`DE-5jOV*6IKk}+E6>GATBogiZv`n@XRLf*1=F<7 zS$U&^8CvJ9e5`^CwBEP!Np4@H`5{g2&(gePZDVe(ceMA+XwNHr{2K;xzvJrkD}4SM zOTa69`LY_p6FjAUbwkH@16qUz5yKF_M}{$igP7naKpKZ|35Suz5ps{>J~i0J7(9&Q zA(QwEF^_Q)Pl$eoN&Je_)f_EWllg-9ujw~NOdj9x)Fgh!1ANP*k3iu&e9!YIu!bM- zBYKF6(fcxA$1~>C!3r6Y{5ZP)#!~}}{e-V_p$i3)v||CEzW_gRX3`4jb`zuj0(lbp AY5)KL literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/command/Command.class b/target/classes/com/ski/crawler/command/Command.class new file mode 100644 index 0000000000000000000000000000000000000000..e994cbb5e039582e572219e226e76e2deca6e900 GIT binary patch literal 291 zcmZXPy>0?A5QJyRf%#XYq^G@WaG8P%okE2~fs`hd<5(wO?(E1IxM!ot1LUC)dm;)2 z#cH&>pENVS=hF{>7fb~@gqilW++^w72v=<|KR*MxP*Gj0h?E-cwj Aa{vGU literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/command/CrawlCommand.class b/target/classes/com/ski/crawler/command/CrawlCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..d3892af94961b0f02e4f0c79c0a72a925f826324 GIT binary patch literal 13648 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/command/ExportCommand.class b/target/classes/com/ski/crawler/command/ExportCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..835542f9a299e505c4bd7c759d2f680f880cea08 GIT binary patch literal 5764 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/command/FilterCommand.class b/target/classes/com/ski/crawler/command/FilterCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..e10bac2767740e6f8c5e77e68aba0d0988420e28 GIT binary patch literal 849 zcmb7C&2G~`7@UnwoESGr)AIk*Qm9B$z()>2NDzsW1&5+Yh||VdQWwXTHk-DO1yV~T zkaz$d3Nag7l!``zgLgH%-^_g9tbhFc`W?ViY5zCTvvK$; zNsZEj0(o!+TKn=qhN;XZ;hWLEij9Y=z{WCxk}6Oet5i*-QFM!zRY%Fpl#ISW^;wc7 z=DC2==?(?lK|WSK9IOPW!}rh>xc?7IJ{nl5L!ceNLrtI=C7F6XpN^D%D@Q3iYDal2 z)1lOf#pR)E_L7;v!=>Gx3y1x>nVeE=z0-}(LfSPt$tL|8ZgQXxi#P>5b)@3C&EiGp z{olnGIA%uYsePG2@j`<@bLcHr^A9Ad;K48L7QtEo literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/command/HelpCommand.class b/target/classes/com/ski/crawler/command/HelpCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..f4089dbe0a7e07d07271739c769e96c1a4318728 GIT binary patch literal 2244 zcmb7F`%e^C6#lNLJ5qcU)M|ZQi`qrIu8&%+ZnsjbwUwe3rA>@Y$7O)g*_qAEtUy8< zYYM0-(gwk5(^mvy(gYth)Upfy-oImqU4G&}XnO9<6_%n+x(WBY_j1m6&;8DsbN~8# zYy!Z3d=|n2hRTGMj5-(fXu?)o4b6_yI;onC(PNsCstM*HEM!=5LA|6#4b^Opo;ZI& zOSmB{X4v#1cj?NoIHsGr8)qm9S2Zv!tg#xkQk3AOGK8QoEU(o~tuCEBui2;7^M=My zUTY;(qd~QGv6h<)-4@+p*joFd1B>E4M8}wFlBiaOt7>QCu6J$SY(Au*f(%;deheY4 zO-rO*GOZ4uop1V_(@fX43?WoaAXtrMx>}oisH$NJUcuTB*38kWzTI)PWEoz?I+DOj zy9}#>b95_ulHzeGDossNR2vwUc;AiZGK16EJEho!%^`4x3jYFgt;_HlDk=JuATUhg z49^d~szJe4hJMcd+;Yw%5l4459;1BQnq%2+N2`8OPic+1+Ch6sHPxXSMx5*nJ*m4q zRu`<#Mj{C-O*e1nF|YIYshcB(`#tePL-VW0;HQ|%X+|7U6HPzXZA0JUr&9FmI`{og09r(m~bY|DjPE3rXZ;j-C zmh4$gH}(QTQpBj<%*?@j)a^+v|Mv0()tFzgfIBrLBTcGqq^VAdDe{@V$sd27yfs>w z=<~YAyupWs$3x!LTSPgR={Um|f~hHO7{vrLbFcH>RCn*>^**n6K)5~~&-V{`H!@S* zJ=585a+#j|*Q2>7J-O^1Z{l(;)8~!#d0n^ZzusUEr|R(I{aDtRt@)qczbkB}2B^kP zFoZ=wzG=iUG%dwr#U&%lWw&1~pD-L3*7DPwiF#&2p;0(gi$Q2nvA&-rNhSxnbD2@P zQ7+TzJ?zg9j!~r4PF1^^+H=KodMJHgiJ(VP9sQ4n%1~NwrR{`vR2Q$pilPR;Q?x{E z26a^WTSU*9KzdGSTtcfTAtPM0{STBrq{#x5(YQF+#8R4<$@wxYrxnrTWg3NZgitt_ zl$SpsD0WqfTuboGbtP5>uIwCHmyplXM_66HeiR!8Zjrc6Vp!sKiEl{UDREbTqu3+7 z-;}#=NjxC&pv0KOxWvN}-;;Pm;!%mmBz`2ZR$`sRlZ3O1HDN7XXDJE0j1*dqH8gTk zDvWaMLInu?F{@g+8(3mb8RI_dy<|3rx!rp7vg kN{nI)Ca~3)l?T#ESxH2kLcO1fr#Ou>BrPL;k&sXS0o$&ZwEzGB literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/command/ListCommand.class b/target/classes/com/ski/crawler/command/ListCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..8d139e1f4234bd33dfaa06080b9196a0e8aca482 GIT binary patch literal 2979 zcmb7GTXz#x7~Q8yJ83ed7urIE%B7_=y^uXVclnvG-=u{mJi9yIp@3VZ-19FfBpOG9{>*H zmVyd_)`XP|XJ(CX!qyf{-41h|)KXL7VI$*2bMFeg0(F#Q?6HxR;Ju&A{bg1LP zdeIHMr5xKbB~j5FVNomP=!;GyH15YX1Ql!-*jWy8&N0kz)HH_d>5Pg;utPviYj#E- zO*@8_l7VgHK9)!Cs*h-CLT@4Fgf&MLO9AY{9tFDvYTOtLQY!YMm3&X@46wbUB&uBE zBBh+KlrtlB z!zz057=5^5$LCB_CO4KM#QhuTm!PJQKCRmkKLq+zJdQ(zVCxynoY&7u`vT3SCJOSQ zn4e|-q=F{|Lgg7rFl`$qQpEsRKz>_KTN%T#>?MJNETeJ*#q;pktZ|xfZ0Gc<4#BqJ zDHTUh&st;MCFRC5Du!^B88kA-lWB*R*%T9g9K#6(#{~}jKPM`l#YuJ;6Fp>_Y@9;1 zyOkgtf-sKZITg=in4NaXFdf}Kx)jaTk3d@~ZH3I`m@k@85Es~8j%?o07s9N4WoOM>(q0!os z#K~D92kE-mCmZe_qgh$u0=?_7Jf!m3<0VMkDx@)4F9Lhl87r?N z!WPW@o^^ud6tgO0O_!QR_Tw6Pj zmoko?RPh-;&o#WBnlBsfYx~!|sPkLW^Oq{V!q@cd&R;^>WNyb4e8Z|JqCdWv(A`Ul zN#sqyLKB?%{xNILPUt5Ld12Q{vZA?fg#%I(dpN2+{8jQx!^>}zOXVIurLw1?;b%TdRclLBJ*%pXXv(RIzllQ{ zzuo0sMsvKUVds53T7f^&HsI}PXfG_5(a~So)6iX99~h|W3|3`vuwUuUqJO{_^gh6r zpf`)bES|oFK+u;(^bY(%o@5alsQwKj=bMzK%6k|c@CW@_oDNps$60{^^$wbY>U~@g zbgT^og8pD2i-g=_EB>(Zk0iwF64LLYZ6q;G*otPf@(%68c0~B~FoYeLL<=lD3Wpy7 z*RdNn`23LDFd-JRxrinBi9n&(D+u5X`fpzK@FqRvah1PgfWoVAe1*}}Y$C#Ag|k`VfJFm6@p~YbWBksQmp*#C z9-zJ}i}&gpe}YfG+->-18J~=FKR_dsIJk^2jl6vZ#a`_^R^QAwo|&J&zW)I53J(J`1fC=&m&I8s6RSRDWF`B#(o=az zMVV8)@_IV~S_02Jc%}+Rc9~~#p^~%0Xb!O?-RE3XyL77j;9;r#jwQY_} zqOxOUQ}16qwtT??+x4h>J6v!$HlZ20qjMKSRuC&ZLt1{Pa;qJ(% zkflpAxv$PI+)U}Y96Oup`EUiOUOr3~l(^v@AO7JtI>}7IkbQEeW*XqOz~fp|h;SG8 z1Oi%8QaUdBb^qiqnm@$<=hDua%h#M4bno{rvLx| literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/command/ResumeCommand.class b/target/classes/com/ski/crawler/command/ResumeCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..bdcb6de9dcd30cd5a001283fd624ebc92c0b3ec6 GIT binary patch literal 6949 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/command/SitesCommand.class b/target/classes/com/ski/crawler/command/SitesCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..4292ed964768f03c02c8fbfff81e60a0d94cc825 GIT binary patch literal 1042 zcmb7DU2hUW6g|V&Ql!N~TkB^Rtrc2jd^0J=#E_Vn5H-Q@;sY6$DV=tAhwKhg|CRcn zi4Xn&f0XeKyKQOOK1jlxdoTCgd*)F z_mq)wCvhC)7;YYk&w~3R9P{_XBk36j1%|Eb_$ig4@KS}!ykf{Untg^`M~`F)S==bY zL6M=-RiS)04TduMD2BddsCKm{{Jw~k_0wR^9IBXMr+Zz)MZsn6bKyM` zsc_sbqQ;Pq?F|_mc_O{3q1@MvPnUDA=ogwu`*v2HWM&;58hK*c&HfT@;kJX#3)AX- ziH!`(xP!Yi1U)qj>q#G_`3DkYNR|`<$+}PDn#@@Bt?#|#bErTs=S){${t_t2P0ywCY;Hfx>5TPi%zMLc16>$)>_tR`}##6q?V zTW8^xj>g=2ysKg}&<6vmGZ1uzct9^_khF@6XfjX<`Y{j_GU#*KJc-#tv?vjcX$`e2 zW=}WtlG!rs{l_90N~lAjS;a<*FiO0Y;RE>N>LrLJK;rE1E~5~uzh8`XP1 z@WA4BiaRMb2+uWb%;hhW^cB)j!6uz9X|WpJq*&UB%3>FLb8U4zC7l_vTXTv09~fi( AG5`Po literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/command/StatsCommand.class b/target/classes/com/ski/crawler/command/StatsCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..40ad25d3783279214fe813b2020132d5240138c2 GIT binary patch literal 4718 zcmb7Id3+RS8GgQOva`oDgpds^XSmvgBg=Gs8y@TC^Ueo~2a-p<3~vRmA(guUhZg+TYK=)cAZen`DRGSWJGI zjP>>((v!9Vzrd22_zSE8{;fvFu(t{L8k_bDRJNG|dI&y*RVWAu%<3{SdQU#xujlq^ z{V83bw#!Uvsr_2ckne@XN_)t#1g`9wNnx?U3F^scY09c=Z0edeywA=VnZZN=a|Eg^ z>7jt4AJLO}o3z^-Us_6AMqkFxnJF2oHaBK%X2#Z!*omh7AgBSeIehi6E9^rGgb65cG{&ww_jTC9Yy_&AcrTal>brvFjLTo6**_bRvu< ztX8l}pxVjAlw1{Su$Gy!jGGvJ!lR>>$c(92hxGz;)7r4k&?YtefME~ui3!Wdj71td z$|N;}jksFD3k2pkYP?pc*o4hYc2*je%19xlxt0lBge@xKAcCrsWeB2+pt4HyN8z>Dt6*U%!lPfPhf#Z ztGmkTEQGcLyA^a2=hOPz%yd@EX+)BWE_4g1IekRWS$c17K+g%x^9Y=#rbWGYv4ZO+ zBj_ZG-mKUm-B7UyeMA7!Me!`7*3tc1D$mR{7eeXK7s)Fm>C)ve_Thj6_JX<7Mtl5H z@e&-AAt1!{0fF_Gp}->`QAHrhpE{}R!ctBtmq-LS4HE*V~>A_rEDj|E;*J5z#tkykN-!vrnq z3W@bL&jPIi>U1mF83tWuW-x>!c(sbx;I#t56c-8tXj-SV;bs-D#~UcyuyqHS0`;DV zbrja=;;ne2f}`w1)8U`oRq-ZiVP%HhR)Y056>pXcvUly)M-{xq+o7CpAA-Q!RNRiY zGY0({3t(5tPIw%5_f&iU_X~sv z9Lnj8wAZ2nACi84Sf-)9Tqqc1EjLKi%qcSx9Sl1oPhfS)oJw*UmYU&8b} z!;P8K5f75grN|VvN1#nbGXznPy{=z0#en&#Pq{^%Qf_ z{;fqS``q(Q(Tc3jgyh;?qRSI2x5rVRAVqI|UB}a9#i* zJb|w%_^PM5bZhpIiVOHUyLXOrn8Tp5vAwBN!8azyhtq^y$6ND8iX$$Bi};p2U4C0q zcgU%J5#LquJ$#?NLd#}#DQ970TXBo^NMKoA#1B>c2(#I=Y}4JO4Tp^Zdx)uKIaXMl z5z(ShdnJ&F%|6s;=5tAXry*nGthXms6i}*F6*M)Z`%IR9=TC1@10xR|RTU;4U8KQ@XXH%QT1c*+hxG#Va1h zy~JtprmH+r+z$#9p*V~M5p68jJ~sQrEI)o7z#rH$Y}3|KEh2zFF{BL*BHkbx`1Tip zMcT-qx7oFb21`q4X?Kg@`K2S1(Q|Dn&9Zb$!P5dOOF~o5Sq0BX_9lj|IF zu#{84$3J<5sN|g=DZB>x7UP{jMb)a)2tCY)3aGsLokbN_@L6>UP>or9V|p>0SINAV zcggIltv$tCG3%@%R^JqB9U?BP?-teug^Rsw#!%n0b_@&Sm5*Rqv~nDc0%y?-j9~-U zuZdShtH!ZaU>vOiXRzxGdd9K00(H24953^6>Be!qvI6JO6!%B{<2WQZaPFbB+6-kx z{l0Nz<#z8l*#4e?5aM9@Yb_^#kVOa{ghh5Cuv}~pykeZC>k2W z-ElRlK92cObqw#TzUyAp%u}4_{DI!VbD zs1m*SC_W~?mqiCYj!)3q<&^bFe2SL#iYh$BH9sx65vTAl(fS~^;SqeApgT{?KEt&z zZPM^rf<&QZH{dj35Tt#}36rx3(aME*6rW=hKF_rvb&Wu=vH8YQJsN6L9av7MJdZ>BAS#E zoY9C0mSMQ82|iiiw4r%|!3xeqBI}h0NVgvGG~*&mx@T$djuQw(s_uio_A95B9FJ5T z8(wjFCAPBLw{UeEe_ODNUzxJ$?4k>_W)ekwl3AC{rpLjA z&w2VWvtC76>+usl`6+2Jeu|%wcOw!1bFKxPeAb{UT=6f|i+`g=!9^keSMv)>sK77r lE8gq){(DM)%DMj0x&9e{<&Hd8{tbV}vrd(?;2-|re*yh8)ouU) literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/controller/CrawlerContext.class b/target/classes/com/ski/crawler/controller/CrawlerContext.class new file mode 100644 index 0000000000000000000000000000000000000000..1b6ff29fdf0af5a2d68870524009d4db76d83261 GIT binary patch literal 1107 zcmah{O>fgc5Pcgv2}w*6Mo74h|iAw57QG7YKFEF=W!x+tgaaL#>;&FZvEg?9*QJZ`tUE} zEkifaifeIvwa>$hWLO|M%C7uiv6RrAgc?5=DiG>YhJs;G04Ld%A1qW7w((BRq<+uv ztq8iK?Se>EhlRq5ZiW*EdlY{XZCL0!SV51$8Vn~bF!UYN(J;}PgC_2oXv0AZZL)7H zXX4=EY%0`8K8?tM{x}YKG~r5`bDp&IXPGeU7W%1tZ!+lhp!gJb7(?@rzo!&NuY{q> zDu!nMm<(PCuEIG|4B`tFihXJR-d&v9S7uY4%d4*$NCNBYuKc9gSbE#n=mbO=?&&X*X4M?>knnDafQ6Y0^!pO_NlRQ!_lmW6GGe{|CFv;LTF- T6Fe;euP=kQOTo|5{&xNV2Q>^M literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/controller/CrawlerController.class b/target/classes/com/ski/crawler/controller/CrawlerController.class new file mode 100644 index 0000000000000000000000000000000000000000..5aaac4815402a4871d14aaf5bfd2a8131ac3cd20 GIT binary patch literal 2595 zcmbtWU2_v<7(H)xlTFj5DJd1=TD|+YP_o2Li$r7-|#XRf91R1=ld|G6xdSID#m!&DR*13W9eD8nd~q`<2>K9-Q(*1ft> zs!bOwRmbw3uNqE~p6F@AoM5uSu^0l$HdffFa*Ru(HScB1-OI`g^r?=|aEphE7J)Z! zUh8dXEmPxe$j)NO5}XKw&V*FOHgMZ(BJ76c2$pe2gT2|MJiKYLFItX{yC~5eD~40E zxVA$sr|Ej8La?^IE!J&1jW;M#sMa0Rx@gP0*|)9dk4t}&8O8Cl!3@Zz$fw3x0AbF) zWJ{Gr?&QiJJQd<|F7Xt4;wMBGo?`d+h$S~Lm0UO-e12S2CLbXa4-QPsq&6^;dWJm$ z8|Y7bhnPS47X9lu!0}-P*S=Q7K(G4E%io_6RY8is!;A{(P#Kl28a?P@|BK=nx^ao) zE7*fHPaNiAVM01hIb&mUyKG`Qdyv}D0Ls_ zCo_PPd_(2zl&AJjYE`+ilt`{4dpW(1v#XRH%Oz9mxbP&wh3QocBw{n6pE0zc#AXT$ zYHYT!5KI@m6R|5iQdq_QHb-Jt{4T9s*lq}{piC0zaRU_agZE` zc=v~K10%SJ!!T)3nU93ewV974%;OrhkUu`b0@a1+;_G}*@iN2j<=^PjFf=s6J$8z1 z`hVj|S?!!GUed4IGu#Z|7xbQ1r-5e37wY&++ zN>W&gw9_o$IAtAW+9z<1rIT_d{m@N0Igh%Olk6bJM5X#V6>HDDxj9 zNlCBIf4NC7-pID}oQgnHg0m+!f_=rO#OO)BKu}quotAz;@tV3eqpW#AD$RY0io3k0 zAGY7iz$D?$ax}%gbKK2gHx+JOfSx9W9+X*Jb`v62Q01>A!+!%q(;C4ARyYmd-hU4= BOx^$h literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/exception/CrawlerException.class b/target/classes/com/ski/crawler/exception/CrawlerException.class new file mode 100644 index 0000000000000000000000000000000000000000..81ec85a415ae97f59d42b3defefe262a43f72da2 GIT binary patch literal 566 zcma)&!A`%_}N#h2>eyEc&T3R!0#ouy8;=&K`ql|Cb ziUwKO#dPkQJLlauAD?gU04{NC!(iz8@xo0WrRyuc425#Vi!Yv18AtA@s9b*+Y?utK z2mZ|6kVk>LK47R^$w;O{hW45FV;HAOM#12G%3zM-d*NUQ)fy~R80wyk#9g+S2{qxf zP%t#T*yrJtE2(u^G}F0E82aA-V-6VXg-8+};f5o26RlL+0D7lW@)R7wu zmd~?LAN!#*kU;{bJHXjY?S-BJhL!|wL_D;#dZs&vVLhMg3)HOwwybQLWeQ7f8* HdZE literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/exception/NetworkException.class b/target/classes/com/ski/crawler/exception/NetworkException.class new file mode 100644 index 0000000000000000000000000000000000000000..2b4cd3d12438106b489d018400d873ca87f52739 GIT binary patch literal 589 zcma)&!A`u?jnq0pXq3B*$(Rpbpsk}5sF{Vq7L z|Md5B!p6@~ypfSidJOeT|B*j)FXYkG8zx#t)9%%n!S1UE;bIS^G72a%RD2nU!E!MX zdc-H8V5s>j;Nh5SY4o~iCo>r{wEh35?=mxrtme zk*f@afG=ZW)^~at+@V_PK-@_)O?~@OJ0=J&z=&v;MPKw1v^%7Fq%As&&DIM0ui1Z> zb~mdQGUoy5M(#oxhos1lBUFfCSk*P_o;sKwPP5fs;dmQXqDw9e>^!$^V9pvwS8;+< LYDII9&qCu1r9g%> literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/exception/ParseException.class b/target/classes/com/ski/crawler/exception/ParseException.class new file mode 100644 index 0000000000000000000000000000000000000000..17b52f5548942a03c10985f14f526baac7e34699 GIT binary patch literal 583 zcmZ{g%}&BV6h^k>CXF$HupjCKM*Cx?MSLw+BrbdaAIf;A zR3ZvpOy^F&d+wQje!YJHxWusygP|QHk(WM6FHn3Q3gwB{Ks;wMiM@NSQgQuL+9)x& z+tdAJ|0>U5UCCHxJ%(o6f8;OR3wb>82APuaqWcDIRmY4`l^Fr literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/factory/StrategyFactory.class b/target/classes/com/ski/crawler/factory/StrategyFactory.class new file mode 100644 index 0000000000000000000000000000000000000000..490814eff47a93cf61c88e974e766c46f1454454 GIT binary patch literal 1729 zcma)7T~ixX7=BKmY_eGj4ImQ-?N3M-{Pipy?wnbJuXFUf2Dm^(91A*o8RHO&7GRQzH?iclA&+e zaxD2CLr*Th$slcfzsumAC=QS)~ks-Ou{oUZ=Ll&$i@j6)&r@XLpNxVtH z>Db^+1#dCT{%g0lI?=nGUP4O2rBiqszaom<^|%;#Tw$2(l48}iMUC56y;`dw9C`h& zDVox9oeT}avW9m-{2guZx+u7g$>o+M36A4S?nvrKr>i0F+c!-7>PC-LDlX%Lfc00O zYhlNQ#v@@>pI&J^90Q|S>Mu)f)=Rt@4TV~ETb?OatYB=0I|!qJTnq{z`do8e=@Yk` zB_Vg+s;}Z>6cv2JFx@TDa_+cw5zI8vvkfu|6QD0K{1CtM$hsZy26w8q@TZHeTW>Xu zlelA=XB;d0O>v6(7cu(|xnim)M1k9O*^Xvt3oH8BqMod_6W&e!_O+M z!Dm24npUlCK(e1s3}-PwD><_v3fW7#`*VLHeT>2FBV-=V9b)J=j2+_D(tKv>5a)kH zUnXCg-(EY!+%IGed_}*Wd*46!ndH>tjc)_3{^ehlBa+5KMklAAFmZsTh0Gh_e)0e_ z3z>YlKYM`jPOf7tQO>s?9^)d(cSv5|o_hoXIKumX1X&0JFdMEK)!I)e2IyTIL>A{T zg&{OCg1e--hcSGI@epw~Y}Ne;G2z2C6bSPuzQKpIOW-<19ie}RC&A-6`~*2#)9JLL fDCD|A9~1ZpH%KPQZH<2G_>4{}?KV)sCdU2&)OgE? literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/model/SkiLift.class b/target/classes/com/ski/crawler/model/SkiLift.class new file mode 100644 index 0000000000000000000000000000000000000000..75c329f0c63a8d0e86892e4050f852bb26b655ef GIT binary patch literal 2081 zcmaKsUsKaS6vgkBQlMQ56hT2Nf}m{?A|N7vXOs~Kr|Jj{?;(T|2+d3y@N4Oejx#>^ z0sK&o=Wf#`WH!F!cC%;C{_fdr{`~#*n~2tEr;jp%%9eL!v@RXP^35-9R>n z&}dA$-wf=A?X$Y!HBUX)gdlI7n~v|c)v|T%pP7~&E*`MV6W87~edr2-Z{FCh8F)O( zZ8=RR*b&rIsU8c;?t(o}Jv1t5h=v6X)t#pO@%rk-_K(;@P^s=&rh9Dq4v)i^+2GuP z+f@C(Spayn66-^?Ts+dUN4ElMhM zHi~ADGL(n06JzbNB+1$#c*f@hN%rj=C`(^T8f2-2v^ycF2;EV1*sR=;RAgn5JB_Ar zdk(#8-?H}{K4KZ(-9^5gG>vsVxbOXx;qdV*!)K)`VpA2}tBUSa1-YugQWcw`mf(b2 zh35!HE2sjsxAv1t-<0eQo_W|4`pJTH48LSsMB{W9BR05)CmZZR$=W;{)aHNCMC4Lb z$ir|M!PX_^G~QN+PP`Mh!4UW9LFj3yF++W)@>iH=h@n_UFwp@`b%Az5pd7=LBbate z6G(3|29oQ7y0>U?o6RBuHs!5OqEy%ncq?!a-0?2X-2w!amd8P$fNP?_tK@2^}LK0*h zAR8$l{NG}b=UR}ZB*?ZFWJM3MlmvMVkT)qH{O4wn7g~_jB*cFS6YzuB*+IX$fh1-Jqhv|AO|TR{CQ)LE%m|J{s(f-4mAJ( literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/model/SkiResort.class b/target/classes/com/ski/crawler/model/SkiResort.class new file mode 100644 index 0000000000000000000000000000000000000000..e89f08e6b176b70efc03690e3116abf7c0348b13 GIT binary patch literal 6689 zcma)7$!qOecX59uG5FxSYe*yYu#NQEEi{1*int#NjHRkL&rI2 zQF|0bj=!^Fksa)JL&x_^&$?`N51k$RX!Af+9X&5Gjm3p;vZQD4T9@+&aqjFm$VB>{JJQ zRM{h97`c@hL@}y|?jS;4s|E*#+?i-!0RkIXnhIv%vu(3i^`Zfb=JfWBA=3ttsa-UO3O22wvn@KQ z=vCd}dS%=VM;)XOXCcRA%n3cdj&HU`Qy#p{vIjczvHr&16S=<2oxZV7vS__c?bN|% z7ueLP&n~p-H0rQu?xY*-Km{%8?aMe+HN*x3HqE6rsMWOE;w*^L;s~_E$*?N7?j|>l zf`hqMSdw)RKW7(RaG}zItFjB~=|v}=jHoJQpdB`KF?T0a^j+)@G>U2YHY3CZ%%;OO z%_E!hC9B>96l*h(%}$e=XY-Ox^V!^H^W=d=3!t|p+YgpBg&^q&?BQ~o7SKsBfrloO zo@`JO3x;TN?i^$%*VuIO2~N0HID(_P&9I(h(?Z7OSjM-Jnb6tY)PPP+Xw4SHKK9zQ zh!(>PKAO#z#>A!-L-Tf^ZO7ef1s3?0{>4lAHg(eyy*UlzmA&6)o8%5`bOT&I+j^=6 z@^Ya~J#>nWPHMVhmerJHk{1jZchHryEf@DtUhtqzr_xfLJsI;=vn&}~r;9e)FsERt zaXFs@o;GG)?Pi-!=PH?>D(aTaNj<3MtLoaPCz~(yZFY03O=r;eG<3%7jCk_}D~44# zY0GCnciHrP&RK<%K8b5AnFp+RpH0hHQPo)Tz@mk~*3n321uOr?rZYLi7jlLhlf<;p zwewq>mP-~~KAg3K@w8#@zqe@xhmgBPa$jrhjt3EI{lTV{iB@u7m)U4$MzQ9fY+A)@ zb1h|`=^`&RMi?vq*`^;PMV@)2Jr6exxU_MnGYs=$b+WPPO>waCQqSA8n$FT(&7PB9 zpZeIuyL(o=`RQHJGi&xV54}S($>8IzHqscVO!10M{Y=5FG4luosuO#g$76H>Do4-6 zwfwz@&BP4GH{@y5!ea_&Ej;Oz#GF$S(?UtKekIYvl|(0263tXeG)pDX@RUS1QW9-K zNmQ?rn3_tWzLZ3PD2W7D68WnnQd3DJoRUZ;C6O*lA{ms#J}Ze$z}xhy^FF)>!jZXN zk8^xm;CK!+&V#fd=k~urYmX_DAL7^rGeVZNB6)sqasg7KzeX3m4D}Xb^fq;Kt%&_&`blg&0BwH7Hb%av0jgj8T;QIkJoIR- z6s}EW2QR#Zw#L4)AnQz!^&-e14M`yDOppt)+@>55PEQ88SOnQ%f((ct+hmXpCdgKR z4Ca9FCNaoQMUd?#$Tkt=5*cK>333TQcIJR^filQWD&ol5ncbRQCdjY|vYYnA#!Wf0 z%LEw#NGS(|I|PILOavJ*LB>RoQ5j^!1i2C*SLJ|ki(-&VX$(g*BA1yU*NPxl(3P=q zQ$#K^LB;`6&H>>b$skvWAXl3pdqt3IWss{)kbMA|&H>>j%^=r_TXVe$QWZhQ<*m8i z1PK9B%K_mj!60Rtz|q_q*96%wf=tp>Y}~Xpt_gA@KyJzb;W5P^9_u}a=-++Uj#Ws zH%cG}Oppfw@=y*4e|!IAh*%&v2jz?95F$T0_6D|5dK(WkUPYN zcBcvQq6l)g+|cecL0$pKt2rS2wa6g%h#>cxAg_xc_sbynnjmiicr@~o)GOw=z#)K_OmJ!YbQ3DmE0Q1uUP7PUV{eGT9(bo?8Ax->fg literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/model/SkiReview.class b/target/classes/com/ski/crawler/model/SkiReview.class new file mode 100644 index 0000000000000000000000000000000000000000..24768126e3bed1d807af2dc699d87867cb0df646 GIT binary patch literal 2065 zcmai!ZBG+H6ov1WQlP99T0nUb1O(fnU8M^0VvRzYpp7Jy@O>#`UD$S$?E-&G6OD-< z`~m(buOcIfR0B{W)YI4z@dZ5a*Myt9RCw46&} z8})1JRNPwPZiJE=OA>dvij zP1~+F99L+R4T+DOn+sb&KGtbFcYz@D*=$(0#evUyuzMN6O{2c7Aji|>=jkKNV@LaxD8RY_|i=Do=%A$2d%(%2oDag zjgJ$*5`6ly#W~0pVagUU$rihmEm+V_!T~oF-z<7%v>Mvs;!nzbld>6n)37DLCs8ts zU%D@%`}6=kHkiYg4XRMGHq8db%^x)1cbSl=Q*g-wB`2L`eRbf(D=|JpEYick(~y0J z`c6wy3#gRnTCz?Jt%h%|#uZ0i>%x_=3l)b~jtj#aR&v{&=yJ6te8GLH$bB3}r)*e`;qogw@V$81KS1 zgdt9LSw=Ck=`(`{DJy6g+v6Li;;fOLV`AfvvFy8zqx9K$*`OQ^g~ss>UB;2V%UHFH z9o7sox*Vcra@~hfKUueF&@h+43EZy>%0g9Dt!B-(K_jfmLKCjyhE%3SFQUy|i7X`(DFUR_1B7oBCsKqW zyf5+Al#?JUT96fbsRJn|LDm4W-UEcMG=r>aLChq`rWRyf4`L=kwgIx!1BAaR26?4z z%|;SrPYd!|-^Z?)q;0$6?UCv$v#%i)HKtJ zK0qI;>72_h?CxB8vv=>_|M}nFIrrTC^Y_ECv}W!@{hBpQ34H1$ey{3-sMjyu5ms@6-^cvzm=UuHMLDJ!L zf+0<2kX1+1@7^mns@8N%3cB86iiVO1NlNV2yegt);84|6GcUnNI#ZLtU&NflFajwV z5|ET&yrOFCtlO?L<3d4PNDnppUqZmF|kswZaxQ>$4&t!;@I!^emRXiAsIJVB3w9jRbNlQuD z-zAXkqIf zhJ3`s;3Jlqk61!JVlDWH~nlh;%o~?3CGdwPni0~W%uwI#%%!QeoXs5 zzR5iiJb-DOk--c;$)Jo%(uT<(yZQrW2QD#=9!Hl1A|<&~s;9P`XeUaCAm-qa<(cDi zg8B~gU(wtL1Vu7{ln`jK50tln0t7QZfa#?)ja8g+fCTy=0bXAMSrUTGdO_xdAdkf$ zj;i!P7IC|!As}`|p1`u8A}d~yWg$ogvR1IGA}ijCWD#U_2ncl;Rb*8NvgQR@7lN#d zLDsw=n+UQs1cX|gKypHm4KK)!5M)yfvf%~UN05UdAautFWJ?IL?FGpTL3YF-+g^|Y zf)s~<&?P63U3h^{x{1zPv*!gV3qcOx&0oMPWrsUXYp)q%5vT$qVukK|T!uq5mv_yy1VYxBmc(vr*^( literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/parser/ResortDetailParser$Price.class b/target/classes/com/ski/crawler/parser/ResortDetailParser$Price.class new file mode 100644 index 0000000000000000000000000000000000000000..0a76a7a9077e5cddfc15227cd434c4fdc012a91b GIT binary patch literal 624 zcmb7B$w~u35Pg+Ivp56oLkofXB|*#Ez30>7G&iEDu5O;0Jhg z`5m!(3@CWfOIN*DUG-{t{&;)>u!)5{G6c_$I-a>wp0CAiAhmZbv|)21O{C*J84DHE zQ);oUl`r$i5r*61MtFe;Ti$WAE&Z5K@Dr_N=-(0C+IRRg)+%gm5&A_ZN<#klGrAWg z%|P1dwhC3eLs(k=o&1C8B~%-P+-`IsOUPo-K@mfQ>~ghH1UKiP5B=729pq6Ulxr%K zM@gqC^_gHn+eGAxpdqxfxJTyVOJxWfwSTUJ`!AhFiT35bvc)HV?{3|0gy0;8q13y9 zFh-g@W(n0lYZ3a=1MsSkq=^qA6QjaIX8D*IxOQgtTstvaVy=i1+W|)BjO@Si$^)F0 z)h;`0U5wl_mBA>#E^?gn!nb`zk=}Tn?-*mg@|A{hOfcdCsSKr_6|!X~8D%hqX|`67 MFoQYv=2L#=6J7b0Bme*a literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/parser/ResortDetailParser.class b/target/classes/com/ski/crawler/parser/ResortDetailParser.class new file mode 100644 index 0000000000000000000000000000000000000000..e93b3a23fdb0295b624a5199730211d0e57639f8 GIT binary patch literal 15117 zcmbVT3w%`Nl|Sds_Zw7v>HDs@|`r8T0CDD_d>g0()nZr|_iZtM23-LAV^twpo{^W8g-goJL(Pv?I3 zd!EPtobR0PyLsuor=KOFW!fH{G^XrIVAXaCdM|uqGOe_0(+KxjWpQVw%v>ws}YEj!li5H#fJpG5I&BK7*-9v?dYm z3Gb`f6ilVUiC8_;Wr_pZ+yJZV#Nds8ze%uYA4ZAZ6Fd2GgX&a{3@Arq!N)>Pkl@IxH9k%BxI6{ z+y+gT(IW^*R+L>@S9Rc*AH4lQDlRmQY~CrOXPTtT=-HSE&6OB&<@V~pRVbEUDr5S< zh;*D|k|7;0wE0{VSfcoxzkR2$GGAC(z*IE`1Lvo|%p{LYT)|XyE(Nl5z$CBqs1^<{ ztt;zTlh|HeexPaHnghSQ>G*-|^A5c5pv~SW`epP&lL}?CSRYjzpA z)FhMK;CDkL7H%8t-x*G94(=2+6>W%j2cuhqiHN-0mF^V^7hZDEa+8+PL|`M;8%Z)% zZuo!O1z_z`M#FURn4%m#Q$Nrbt?B5CSTrjMYnx zv|M^Oo3w`3G8H7lDJ-TMnJz3lhqEj_s!M(OR-IY^vvHT%5ZRUT(mHFE4JNe`tln#_ zB5h1_&$Ed1P-|>DAJdy7-F;!Kve8xKB-z|QklL^O!!|EnMLTr58Yqek(II2h_qz!ao=#H8KQeUjb1F`9~`218+Ki<;CgZL{sR&GA$) zy1u_L7E-v7iTXnEy)nU7+@t}z7FzF7*s`=fr>uN@s+HPhnWRZ68iXqCWuyTHx8m=T zg$*VlX|GB92r4u!k}UNL-p@mnjS(U1dXo;&4NMa=+$)HIu)j}9dFW&MfJq+|$rjsl zZ`>131f$W8?sy`s(@ki|?W)cDy2Ar0C=afvH`N~%&y{Pn_ZKF2Lha9R^5cu#1`1Fe zigz;3^@Ao7Rr(V?>3<$73}V74Gt);snf@prjCbpgFXQjR9n<9LhBwerlRiri!g;_YS?25;YcRqMw}_z` zn)IKefedq?RyrB<1*Ti7cV=z2G#Dt21^0}qiAP6O434hIZ;wXKnM0~ZP9ilUXNg8c za}S&JMLG^1l3~#tQ_WaR=J93qtFakNVsra)2Hz?oY)mAA`!__A@aQ5qZ3YSPnmO2j+{D6B}f z-b2sOb2>en*(lU_6*h!TdY-IZq8a{UOSw8Cwbyk6VcG#NGM!D|4iT2={xXqIT49AO!}TI^h32X_6Cz}keH17 zu}MFnpJLu*Lgf#9ky&uy-k`{>(7m$TdhHu?k)PW zPH&HqYS>*&dPl_X+!cw1r1x2q-lg}zPEP`H$T?!Rw*`1LIA3N)US}qQyTU4QYl%S) z7v+;_ch5&5Iw4g{)a&d-mX>pPc0Dx3Zj*I3fE;Kl6F0O*uuqlb&z+e_!jlH}np|+s znBYF3rC&xC=LtJAP}-!2D}f{vI6F6;%!mKr>uZJyMHMU zZ7&D)^z}=O;!0VHE*(Fvw9-UOpHc572@EFX3tZKBEFq(8a1aYb5$PL@NYo`IY;eqB zlXo%9NXKA-0aIPRGqJ5^OhYQ>G&ZUMJpnFtIDx9WJ%T z9uR`^YS8KZBI>H0LDT5$Y$(jp;C@C zO1ovQe-J7&uZqf#nEX*0Dyegmv^3!~&)|=vBUHU7jFsXT_(`yzdq;;)f#&}B9(XyK zc|el!b$o}(cgke42LJ~4Xh>@I_;3+I;dVa_hJzv4mBB|~fU1aP?nQNXZzKxqyB}&% z#Y8w}@B`Qg`XZ?QtY9+|Qh51ClRw9w2Q!FC62b1&rbMJ0rq?_MXwLOhPMm}lsQxZ=JmYeeGPnrCc5z{n@ z&}GE4CO;?nk~sRz2vCr+&sffrVA@G8OiO7~+=s3dD5XW`AmXp^FK&52^a)uvowJw! zjl!&#o&H5&D=A5~!7pKQWDh{I!7od73RWNrf7RsI_}iI1cU7ckb+|jy9|QvxW9Zv< z)QJ9iYjS<-J^VU7tKlu&f6KJ{MA@dJ~8$UhQ`#_4h} z5#Ah^Gv2ZLm$w-F6NEC07Rtz<$?@fbvd#a(eoW!;aGQk3lQXP>HN(yse$`Ots&T;Pr zAMw4=oi+iw(&*Kjvcl7mUR#0tP}D^3~^x^Z+4;$kPYIielZ_Md{9hBt24EF z>qIOO={GdQybfHMNXwr)dmwgiMC95IQ@ci7 zhoN>L#LH5-Q$U7=Vbu9EOEfKsu%TgD4j3AuQ?8cDBSO_~rq%h18{zHpKro^{e|{TJ zhK3zRSCm&@C8iZKwYY}NzCh8KF`p6VJsEVa27?{(LF7)jN&uA?SO>lNceT~xI}j$d zNcPnf4JMP~TU{!Cz#47vPpx6O9ZS+29ECcNoai)7ZY5EgCijc#iR(l4#BHB?;!aOJ zaXY7;xNF0+1lNocXd>pBgtruF_$69VROmWMQy!zKMboUpjG|dqp|oi3Nt*WRD5?NIjQ4it!pH3b&49go}`tnfm76oLQ|Sn zqqq*m%hI$S#f>Olo~HK7)8ws8)7Hb}3Jg`Ar0s3TNUw7ShAOH8X$q>}u=?#)zkTXA zmZpR%>`Bvgs__265NN0cMGbT#s1Wwo&}96ULcnvVnC3zp7g9ORr=_$2JuA^SKr5+= zR?$LS$StBaT8xXbCAijFiVLTUDT-bRx!1HC>>ham8Jta>xu zg5On`$11Hv{XfvHCCMu)V=(`qlwkYnFTFYj+TcYTf8JF1H>2wkOBYh6sl>#cC2k#b7Gstn4 zigdbr7~%y(44z~~JZ}^j;(pCyPP-N(KoJ*I79C5|Lv5$%OTfS*bxv?KROdcMA;0@6 zN;5rAch|Wp{4SeW-LETrpvNk|euBP|rWfiAzfp8LO)nmyYLs49rFkg5s!G#P`Zh|3 z34XFjM}|M`cRocwg7-U0OCiI|Tj(kJnTGW{)AY++wJWdsSI4N; z?(KSj=An~goiRNj!+^?o!<)%9 zYB*8lFae3#>MZ~2%M@08ONl*k?HU8drkvuy5D1C%}`TL6znOa4S&}zx{gv6 z`dRfEaIphcovC!%h(o0dut_t1U1|1YF+->5*(_$Z!K?{4(=NCo1L!EAI86aUX3(`T z^aPw)68=2}S2qZA-$NT{FVM3Oh}lm)c)t#Jw;!e(aPRkidW3GGC*aynLA%evt;r6t z%aU6LH?R*IHz--SaZwth4?@BQJ%$`$0&q}3Lr57WfT}5Uj3;p+9Oz1l^JJh3pGeRy zE<&9FN+xkJ`gyWgQoVQzdWq9qN%}Cz&j&-N&JOtx2*<(x6d()5X+?jgrNi(|W1s*x z^A-xUYEU6g=mKu5sMMdK&MrqW`#QRu#e6|Wm#dil9bN8Xp5Bq>nc`+oQ(?skDit?- zSDNRDN1h}V0wQ@2RHb=-AkF1I zAKBRMH?WIjMj9g-AxE)}+6Rznl zSnl0$JD&zvjsS-D0z&tpbUzIK0f6Z!pz>Mz0d|m|!j=3l9m5UH7r2PN$R)VQ6p(GV z)RF-iUyuPAN*2g8F61k?3m}_M|H0cZj}t3D&sXwxfb2uKI=>1qa%0tfd^JkqY_8-T zC>aV!OGtm0mg(FGXEW(6&C;pyJ-XJ05<6xL)9kUZgU>B&57LY&ognp@z!`Ers@zDR zqAH`>Ym~BfLQSDG_vSV9sfK>5A)eQePz@=oVGkM%Tkrc-!}Ut>irbNG2BF!1Z-HQ6dpnGPXo!+Br?^8T|M#<%<^gsl0nkHJo5kCkf;n~G< zCso*HYt^~^uG3`r-DhZRg(wfkajZ@ktDOd4%wI%YK!twY-p~0sCPCln89#kg8tvU3 zjcPU@to(8Qa+)7ig~!wUq!?#u!0%SoPs1}jbA}4lyR_ys@CLTmk(>VWkm*;Ufv+Pl z{{|wcGuZlHgyVXNV%XOA)63YbUxBr~2IPO6PSAItpYOu1zDM7LWxa|k=kH_d{~7(5 z-oWqgaIY+yQky(u#)luBKjSWe0`T)5;l5c7U@~&j%0$*T$YV0ZW3=BnbkM^-kI#E__;p!I#nvC^A<&X9ySKo=+ccB@O!MIP7tvoJ zn|Fbi_khk}gam{ju+01+**1w- z85jG1AX=ae1GHnzj8D{XVQV|QwGhk%i09SOs(O4JRo$CY)onRdIcg1P%Y3%Bz)|Zx zMhpF>&%3xzg`ji%9;Ge6gra%c@;GY|u}(PXWuMnqaCG=JpK(M0P=A6hk~Q9x;VNV; zukd@EcI4~zdr#9;zZbE>J#c{XEDSve)q97s3SXC}aFxHXK?ZV~naDimA-7n>CN1TO zNP8#Y^0g3OqE4m+Hvd5`##UVj`IOMj*ye9#Ki$F8=w6P`sK2#j9CRoX&#cb6HT_p9RH18x)OnBmXD=5un(D@4E#a9=ZdcTT4Xhp)b%h z{x5_LUS*7TN-pzvK*moK#)AwFSS-wxE6jMt?`OrnV}lA;zc#2!04hmsj^`bw@KB|t z{5GxD39J~;*6NO0=zE#p@VOUDxO$q(BuKUVz|1N?(jm#M)BI+w$LG0+ygpB@Qy}AJ zpWCM&9e&a0yq8=&znfsgK zM66UOmn{vQ1DW|$zzb*smywUlX)0GxfGe@%1ZV|U(`8(PyS9bYffJ^!XuF1&QW!?n z!^;4rOW+HZ13q=wwd(Ov;tG108}cA?JPVoQDkg!$!B=^fAMqh>l~mS&IQI_Ct?BS1 z0eqV;pl~7KH8lfu9(bra&4ZE)@lZhXqU1(YJXb3~Kez)>ZKX5fzYme8fQND3W@#4b z3j9oGR_o$UMz%<+g$PNTN490KqM%|WzS#-Io2?qv9}`ihN{#RbD)?B!!U#BVf=3)T z${u*plbY!|p-oC_zO?2)sZA4)Z5Vh&C%<-P0gA;x&lU*5YpsTJXiyW%#p^_3#$0YEdh+ zoSj{CwubExi(v_+JPli2u@bgJ*{6pz_gN|tjy1WAwCQ@xrmMD6(RG3<5f{AK3i3eN z+=_rrnIv=|TT-c$+J&}E^2t@FXnZ-yZ3nrV@kb6@pzE#p`;kt#jw@(BGLfaoKyp!W zZI<*}8*LM*OcAVs1)sJ+7}*Gwu%7ETI6nJuoF-5@vbVr^GU5{=mgFda~i75Y6!<2TxE@_ zw4-Mk+qoYm8lxn~ z;pqo(-JVo}vVzh>SwS7j3TmIW7(Q4-a5Y_9qRjGrR4BLl0NEB=q%B3OgL;4#t1d~C zRX>-)f(Z01t${$#Qmusue2+kyCa1xgR;x(=8N@0%$1C=UPFtSWtb%})oa^BDE<^s1 z5v0oQ;XP|nC(uD$X^UP8Ioa2_tCZZ39zbsPNe~8zIaH~GpczMpAGWnnhY$@Dx@1Dp z7GkqAvL(;0V81J)4BeVg_q%fwqHV;4&sK>6TPKL^IBy}|iye3$>|{T5b3M)C12l(k zpelYp^z;F`m~Vs|y@`5Je;pr0h>sMT57DRiX1LH>=m_76GngzIe@yAjIt6+q ztFu?KuJjdc8Qh_SWZ%#(L4x4GN$?{|k4^;2^-5DNoLLqrO}UY(U#9fv!e{Llsa7`* z1gus)dkUmxLvmqRc{zE{QZdp8K1&5kzs1UC7Ed1eK5H`cj0;TLh%N+Kl1J=?jwb^6 zh+_3hg!_T4-D}ImNQ$);X|3TfVc%LcB(JN}+8Xt`TD`WYfETST>JC&*JpDdZ4g0t!+tb%X94Dlno$z z2u|Q(0PVOU&)O|~S@L{}09J%aWGvC{{vqHRu=#O literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/parser/ResortParser.class b/target/classes/com/ski/crawler/parser/ResortParser.class new file mode 100644 index 0000000000000000000000000000000000000000..1503f6cabba41f89630cef21ea7ad2707472add9 GIT binary patch literal 1681 zcma)6QFGHq5dO}wEk_ZA7;vH_Ed&aU0Y`0Vp^bqybrM=|92zI4K>DC4nxMpzJkq%g zKc&B*ukDLr+L=D`z;9wozmuG%joqO$_UUeS@7w)$_xAOlzx@H=CZ1@>2wZeR---`B z%Zcn~Z8x$yb`x-f6o6#d_%2?Uo&R>f9G*AokX<`gq#0LVJ7q9po`8=uH;{q;IwAHxZSbx}1fy^5y;|gW5D;u^? zKWB$gZYkSq1}e(dplvhkMvl7`wRKqdSi?sGlSyf$=et%?_+?wVn>-o#1lQ?42%ibe z9I_a+V^vD{%)kxYOnuanCf!|P^Ud~y8gi@LD&Pw?o9{~dNk9s>ov<6o==l#S$5jKj zaGQM;yKT1dsu#tCs|@2fP}Y^Un^d)cFPR`G3?yr;V+l14Uz6|g!u9P9EMtWa#FjD= zxIEO=Ko=|ScMW`_@~bnywUk2v_XH-6s73Cc7Y1ZjLfyccvL2=NMk1!qr_yh$hc|$) z><&xDTI`07!1Ms)zZ;p)JxXSRz}!1u6SLb-`5J2=U=H9OW|G`e2C3nRrnH@dHM`TF z4SL#DtDfrjiGL2I1w{yRVCWoYK!!U`9j+Qr7Qceu+?5w7{FEWv$Rx8DCl|nPMRH5}1_s#4t^f>|*=Zz$$ zlO#0GxF$G(ihO9NahEwq@cZAu?1rR5`nX7Kng>YA7K literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/repository/SkiResortRepository.class b/target/classes/com/ski/crawler/repository/SkiResortRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..dda3fc14e483773bc5b9b8187339f64092d33009 GIT binary patch literal 4858 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 B>oouX literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/service/ScraperService$CrawlReport.class b/target/classes/com/ski/crawler/service/ScraperService$CrawlReport.class new file mode 100644 index 0000000000000000000000000000000000000000..4c336157efeb71c842c7d5213aee06a76d3547fa GIT binary patch literal 775 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 U!SV-i=Xh&gzPe3z1)MMaFTK@JzyJUM literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/site/CrawlerSite.class b/target/classes/com/ski/crawler/site/CrawlerSite.class new file mode 100644 index 0000000000000000000000000000000000000000..feffd1e1b03cf47205559d410a8b5606f7f9bd87 GIT binary patch literal 558 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 R4v9SO@hwC5%XE{$%4di&!~XyP literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/site/WikipediaSite.class b/target/classes/com/ski/crawler/site/WikipediaSite.class new file mode 100644 index 0000000000000000000000000000000000000000..33819012b77b5d9d4ee2aee386e37ee6bf3c0611 GIT binary patch literal 7149 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/spider/ResortListSpider.class b/target/classes/com/ski/crawler/spider/ResortListSpider.class new file mode 100644 index 0000000000000000000000000000000000000000..66e989db6da60e500ad80434d5aa530452bbede1 GIT binary patch literal 3965 zcma)9TW}NC8UBteY30>AAc1Wb2-t-L-vxw{L?$%EKoB+NB8(FgC~Il4*On!-yT(8- zp_lZQ-qRayOGBHMUQ^P7z@hE*iD~=PzV)Fq?R4_er@r)|51F{%Ig)JI*aI`5J$wFt z|LgaE=lIX>z5ROtPvW;40s>nyh4F+p<|Hz%HI=j7gg4=2`8Z~K1=sI&JU^{AGz0}! zj#}rfM9#_&Ck_vd+8JLUIO6*g0-Jgl+nMy8oR{b+Sv^3dZh^3C<+Fuxfq1X7By|$U z)E=dwbCdR@EznRU>~->Ec2*R;KcT2y!oV?@j z7I?hnA>wR3!L)h`SzAX9Vg~9E7pQ4zJ)vQBr3O`z>sWk=$w}x`eFWy_oSh*9H>&W-=X3!sTJb~8xl31o` zOg(be_A?`Uayfy`E%#TabwL?bmGylWGWLY;6!M;iEdr}lE>0nlIy@)Q(8}QF(U8jf zw(Cw#_;z+qN8qW4aJXd0be5*WKqtBcbd|t<$MuM|$Cj{|S|W#t=UcAd?=tH$)T$vN z@btsjSu!*YPYA^3+k3$BM$)#gV>@C|?8H+Vo)oB?SD?_q(?}9RRyHe8U+uayMcu7~ zI(A{VhGzttA2>(`K7eNhqQkbow~(0}xAUw_SGCzP^jmui?r>t%D@;x#^2D>3sOWb` z@SK2Vog8uPv!`_I#eNO@1lBC~fr007fP>`OIW_i)2e>QgCHCcP8J^dz;~;u9d{Drg zcdY7ZtG#MnGT~l7>57#k&b<;jDpSjBo-Jn7skV=gR{4R3e~(Q3fZe*h?l7 zQ&Ur2v{f?I<>b#67Mkuy&cL`#D{KvUoDvD#f`JK~W2T;SiTp#pOUXKW27DFph~@RM zq;fRR8<@fc$t_MkD>oJV7T0qQYvmqR6@eYxiAx4PEXN{}Ddc_2$$OGWKWgA(c$xK) z?IWu&o|aT=O2^0XNe!P^P|)LSl9e^^DZIkN!^_z=529ep!PXO!X+LA&vy!+&nVfC8 za_92~Ud0z=(%GycM6=vYMdm$)oP;)MPmSXmd`U9@mkHSAvDswxWV=pvG+AA8 z(b1H1x|pBC!KA$oQ?}bD z=hb)aT>kg9TM^tOdZuhiP^>uHC+Ivv9zI&SxvZ(MjY8D?$1OQx zj=T%hFTRX*Nyb4lN3n)ixf;afErP4Kynv`D?-!Itxeh?yG1xmi!rv9#$?}T;pG|GI zv9j$hR=s!|^|#Q#w55B!HR0}#!3v7IUH=qo>y!S-b6I{w*dhGy{o00!D_;!!hHM0YZ5hU0sR z=t*j(RzxaUdmWBhJA=ale?WgSVn*TvMZB2QP5mAYn|cwat{{9Jdrdu_DZ-vc9aj!l zSFp9DJeG`_QR+=&qZu@#C6DW`B4!3>kQZ2gO&X|^UWljZ+1KuqJ>O=AiHDL?d(TW{t;~C!0)3pOrgZo=4{T}@{%z3yv=iw?ne4nz{5q%Gh8g{CG zr)LRfU4zhYn*Yn28um(u8qZA3(noY@1r7aF;g`UwSqAU`JGJr;iX|N(_AWNiPT+rt ztzhgtkvSFwzu3z+jZvR#kH22T4-R$QgWe&*J9eX8L0rN+cpV2M3`_9t>n!5OMf@xo zGDGoS7V+jZ8o7E)t?Ib?4Odst(5cMGSy8a&^M*8{(0fxzFx4n9b*WMBV;G0IIznPf zvq8t%ntmL^3ASYbLu`o6{Q_Q60NAZ4>|6x^IH%gls{lrEi%ksBLQ=J}hIY20sM6*9JDHwf alE$J5swS>8DjB1sS58LYZGIKN-Twm>Jimtk literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/strategy/CrawlStrategy.class b/target/classes/com/ski/crawler/strategy/CrawlStrategy.class new file mode 100644 index 0000000000000000000000000000000000000000..7c93ae1f5ed29e8af3f94f6598e642406391e06d GIT binary patch literal 635 zcmb_a%TB{E5S$IAp)Jq{JYlp*vxMeg}v zSygJKGIL2n_pP1w@|GFE2GUs9uzKRKftFWZ;0fs8kf*lcyEC#ROR|4?;1_G=&OQ4% z-#Pc@wYRUm0^nWvyMj7_-jp>HbEb`0%GTyg-Hth~t-1Q-d~9UeIH5b1?G9!ptdYX5 zf`CBlS#4H}nObHtcJ$0yJ>@D03f!}*rhch4}+X6e2m4aE167#JO zlrbj+f_+BDaQ6$;cXgi>2=rTNJ&bxZt7t@6L5o25E$a(o9hw^<(56B`NT4NYWc14(nI|>dkl%&PfIDS40=ud_Upskpzs^VtZ0zbzmQu*X7@5gL2zLlH zvJ$qJbo!FkC$yaDj<}lbK4_aE+$C`Dl=Bsruc>l#+ZQLt5@r!wjK zGCek|yK|O3T@+xLBgSM#b8|KmeCq$?Q(tvx`x6T8Va_UDbFxO7NcZt2;~DWb!q|Z> z38QX;x$9Qaa#D)yRB<1o%*cf9rlt-Xw&M!ys1;RJ8l`wl#V))@Pw5-?n3f?cU&U3oh2?~-c z=AoZ51;YZJm9VSoD}g3&)cGwc@W9%cES95E@eF66S(?{G{Rx4tO0H5vdXu*pryPQ# zDjo#O)8P3du(yUM)k&?i8^#zus9;=ROQrD&RuvE96i)(2cSo$8ozgj>9Os#t3ZXtXk($=&(zdl+@|oz=7rwLYohsTBhKQ-+zAlnW}JmV-}%pg(8ZdL}h5!?++Hr<#7+}~EDsF5$Z>zUS-E&PkttpyG%45s!2F>?h01(2%sSqF)L+ zd>H*$#ZPKUY3JJ5P5Z()jy`$YDD@tP?F`!`xP1HQT)k`jbCct-ftNzrV;!sW`W?nu zZW2@k!s?t7b<`Pj3kX}ofdalbyS0_aNFfI#q}W3KWI1feYup{guJ3pGZMIhqb5{PK zWw{Q6$qwo6l$B;j{0^@wc!fuC>5MWmv(~g8^QW+XcplbLu4T{5Q}y=(&y}}zf!kMj zM9ZX2-PxYBtm#}fQFhuCW_sDq3c=C&tX_lt;WB>z2EKen^PpHz(XW}N9FB>WAbuOd zpE!8>{X1btj@QAC2^LRJ$2&v_e<95~6*~O8d5Ol(AIxNQyWi9tN9SGdue{t;C8buF z3SQ$xU7MBl_H)Af1p9`+2L9UQWt`U)x#p#w&nA8y^yolq&jQq*MXVcNKPD(js$kL=h1T+ja=o? zdnF!NMCW)UkjL(8{O=ngfdwR9_579U_t1b$LL1eA-(i~DLQ|dC>bbNZL20BAja=== zdp+sBcpnZhez|uL{nQgC*8mPtQzvB);{mRYklum%H?dj4jzbCtZ*n7`Kq&YCe?xMy zhx(4<7BEOEy5bM%h%M#w`c?s+9%VT6X%8(w4_T=%QJRXVWaV8#!1eUN-hBvVU zHR>($FiFeC8zK#o){uwm(c~hJdD1=>4@QDZcsdfyuz?JpyDpW< z?>s&);*BC6jwnlLiYW0${=$*a5)MQfuX87q#}_Z7HPXnp_i+_py^3#=FI-Lzm&o6d z>YZohF0gjbux`(?VwWhDXTAQxYW)j? z_&1WYI)pcZm;nxJ| zD9_KA@G_BfnrCeu3vAR$YQ4%;h#oz{^KXGV&f^qMu?6Dj4dPMar{2TQDKx!>-3rDi z_9pIBkb4`u6g;lrBLCh(o4;6}tmj|gCNZP<03*hHDThT*!SBGm@ b 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/util/CliArgs.class b/target/classes/com/ski/crawler/util/CliArgs.class new file mode 100644 index 0000000000000000000000000000000000000000..fa1568945d8eca45ece8ed51353a852a190f5306 GIT binary patch literal 2605 zcma)7?N3uz9DYvUZj09nMXHsTp_>8)?ZEj$dD#R}t3@_(GI1|n$`!7Zwzjt@*@x|e z$v&EWR6lJ?=9XZ>wyf%wm?cXzoBtA-d(OGHQUY5{;NEl3@9jCy@A;kn{m*Yt0UX1U z29H2XCcl^{Em(<6(YTp4i;1#rWfP-W>qK#`q`@apf8AIz5?LcRmzbEjZf0zOz@U}0 z>|p_KXV)bG-)Mf;tb-RJ9exA^cBHMGdA__jV-_zOGg(t0lFnz0>?NaU$!~YlXU|(D zfwuJjVHgn53r4YIP84h_pQHc&&MWD4Op|ue%FPX=x|}@)f7~cAEHr7&&eT@XAsj?P!=dT{6$7fdrjEDK%S;z0CN9d3 zuQTM>2W0xgI^Mw%fo*m^oxf=oM~xEObHF`UO}2q8`8lQtEK_Y!^y^51eb%hf*kZw6 zR&t!)h9SJGf#Tf04uI{^@g7dlrBJ4TkJs|(sCuA=S$C%vfxE@>1_wi+aSIJro0XEJ z3WyZToK&2p<2n|2|Cu++MwTkrFppK$Ob5XM7rjB`d7fpn z6a?EQmV;n~C!eECCiCTD#yn-ot5J6^+e4BQ>;+?ZKZ7?Z8;<~=8ov^J3%MJAgy2tH zcu>cuG=mP?_|zQ-zPIx$gfMo{tDb8Q8e|gBEK|#<7r!HN`4M)G$DhOBv)VlT0Kddr z`;xwx?>Pbw@LkMTK~vHn^GBL1XuT)*?!^553ic)gu|WM$1^bVNnnL%{5es;mLKSq; z|7Y}6(3jL=fu@k}Dt%>+-~)`r0yWlF-_`ijZ_yR!qVE~@syFR!rH1%zuW0RaL$%1! zWH6>xaQq&^Juxj7e1btQrle2&={GOOdscZIpE}ev-mmi5L4N5ZBup9_`P@w!T1i7Y z@8=Hg^kOHkpeP3EKZZRxi)MU+7A(*!hc`X=S{5g&|;%cH;`Wv`f7mnMTPJaELuj(9%mwAA5FKb)elzOa9K% zhXwGd>OhRyKEqX)iy*vVaav>M5Dd(?puXf=)@<7dO02q|9wW1w7OTo9%*+zk9`&xp z)lXbWS_WwuqJD-IuBemQ#nr+uiAJc-C^TmsG;>s#L($jtk!VKbFnSd+&LpL~BS$Nk zzlW~wNLDS{yCXTZ*wq~=s71ItQdA2@`c|05LCB79J<8`vYU~s>cDe@Lh>Na)oJu`9 S=tA%W*3j=X-DbE4<$nPP{3tg7 literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/util/ExcelUtil.class b/target/classes/com/ski/crawler/util/ExcelUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..1d17da12f5857dbfb45547a88993103bd090f3aa GIT binary patch literal 8630 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/util/RetryUtil.class b/target/classes/com/ski/crawler/util/RetryUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..01d95cf8436df6c4405f6db143fa158f9646e07f GIT binary patch literal 1603 zcmah}TT>fl7=FI6$sP!VLj#R1khW+ORMtZanjj^`qM-@Sm>9Cgm59>}ky?aJo1{mJG7RrdvAOHR}Amjwng*_wd1 z+S*bU24Kb!MNA-3a++$RySu5ps@!yyz*wnOmu^jZPH;bd)chT%Bakj#%pxxk^#V!) zw=<=XO-QreYSz1+r<%UKDqS}K&KK9SJ&iMbX>Z+oSXXV|X*D}K#stPsW9mx;%!;$! zlz!JE;lffi_kX12m-t_*R`bgN3GNmqa9PKs!0@TJvh;W2NZ}RcZdX2JWSL?yTO7ny zOzW5un7S~&IA)M$!kJ4~;8NzCU@ct7td1Mc6|TDDDY+HLt9XqrJFZe~qGZ;ywLm=+ zM;3DeL&xdtX}^fydu3SH@kShP26Z-_rmsA&+xB(X0&_3cQJrMn-cbHt%WL$LZQyNz z(Xi3deGs)}VIJ=WO>~QB$GHwGS@Aumxt%{#5z=Yxg*DIC3@ix5duU%t$L#^V7XZC4 zkUR${Thqb5m_Dsc@gx+r)n~~Z`Lfdxn0ZOt2@JBD1>aY@ZJ#YIihS9W9aRZZT@GH> z3FgWUrzd$PioPq7G32SeLko6c#Nm=YqkkStnoc}FUXR)*BQ|?6@m{~O@)cj!8)ex( zo=mIK>Uwpx##FL>``?54AYHfyHZMOSK!hg_JXf7Jo4ep1ojZW>b11gB24YZQkZZiB zzpv6UHH~?PD@$9pdJ%c>NE|e=+t}$~wS1N8iLm zVEp5=UsBp%NQ9!>2Uy%kG^HJp#s~*^lBDuj!76g36NGa&q-&D4#or)rL)04POeZkK z+bk_^GLTygBTs}CO7Gz^?o;D)>Pt+AVSUN)861}I0r%jvj5}d^y0j_K{{TFCTMhj- z@FAs9`l=G`4wIUq{u-q^J>B477hq5_hL7+uc@>G+OLpL4owxh^=U_d>h>lSq*0}zQ l1XpuKdq&=dj;lfoWn5Se9l3un9(jVv0w2h%M1%;+{{fY=cTE5Q literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/util/ValidationUtil.class b/target/classes/com/ski/crawler/util/ValidationUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..54fcd487e88c38f82d50ae58341441bd6a214d53 GIT binary patch literal 2359 zcma)7>vI!T6#v~e%{JktByDNsT}pv8RU)Xo3Rr22Ac0~Z5%9%!yM%2w*}B<4!56-t zd~-$|9fvQDU(pYN(NXwRXZ)9R9DjFrlcuyY@FBV9p7T4;d(PRv|NHGv0Q+%MLzBRE z!zpIVb5_=H^{ck&W-Fd$XY;yk&FG%xl%{yq5D`eu>R0rvt(OYfvFTaU@C2fVt&-&( z6=+WP=LI6e&WssHGhz}ghzhLDStavKr8sT6llru63bf}OL$~v~YpHw4jChx=vcQhq zV}vp&&|=u8UJ@8duTUs506RM|XN{X>$MpvLABl(~g$@nv0$U&U{*W<=POKBq3Z{2P zFEW?TbboHCCKH}(l?sD#2y{z4f%O7$vWk<=nTl-->`bo&ud1KIMu|<>OjqTat97d> z=T+=mC3;j?afUVQR7#$EO|g3=wks2HnrL9}kl3lvF`~!K0-slb(-Qp(93yy%J0S6- z!b)N%oQi9hQ?9MxyCt#;E(u>I*dwu5X(Z|z6PCw@Na0zD=afpKu2PfimpFif0&A#~ zW5aosnH8lSaVl&UmUa+FG#p;mih)exD4wU086U~!tiI-H6suVe=RPdvWKDbmf9TLj)S&5gGX;qv$R0$>|CY6m8 zZA@Cmoavo)EyFx*`A?9SctxqDsJ2piUgE-Py842${F=n;tLf?s0>Ty8_Q)aN!%TR( zF?U*@4@a`~O0WP}#=gfGv%H2Gf!?KQIBDBvLAPIU3zec-@LS5(}VN%{Aw3-C*in>EYp3XLJ^COT43cJ)&kj zhWBto!~3zyKXZ}KqxRc3Lk-UelDmEx` zi%)J-WS>v&Qe?&_pHk%0KKTr(eT(Q*M-_)UF?8#Dr0Tb0-~Nc!jHqIStP=t5gpWHF z;)2`h8g7iZ@kMm1gSfU^KLq+yWSk55=NkF1X6`VpWBgm*FJ zy_6k6J1(LFCf4C|bm1ny2Dh;uKVc*85%~wUG?eT<-oTr*cags}(4zm~e55zh-OU>6 zm=0=)Z|GV*z$j~&_#MXicJnR@_Ze(v5eaovVSN{hoKTbuM9BuxTS}xJFvw(!|GK?= zpzVx*2e$I-(2q0@@qYvZ4N;CZM5&>rEg4J37-5-d1uZfaM!10yM%%qRnG|j(d#bon z#r0qDZWSLb6|jw1U^m@n8Ni+fHyiL7O9sCf%vfWE`6Ufql&RK4VNcjd4IXj>ev+E_ z8Ige{N9}Y0et9C;$Co<5H#z7#>k91P3*T7>-|tIO!grfMbM#+yYnUD4kIb)Vq6uIB E599YIZ2$lO literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/utils/CrawlerHttp.class b/target/classes/com/ski/crawler/utils/CrawlerHttp.class new file mode 100644 index 0000000000000000000000000000000000000000..c9bfb44c7065e3bba5ff0963d0bf72b013fda857 GIT binary patch literal 2333 zcmah}-%}G;6#j15WF;(>9|8u&YPFhdfM{E_2DL>*i3NXvrILc}vRPpv*-dvhqW?!< z>_gu=eeuE0lu+tSeX}$Dd-~Mr*nW36lBn3u?A&wiy=Tw)&Uel|f4unhHvng`p&}-5 z#B?h;f8EZRp0QE3yj(4?%YJSm+D!#PRfQtZwPxHma%IC=$=z69v&=xCz2;lq_=@FF z@0o7i&jp_Ctc=m1>bVadPPx7`4)vR^M_m*Iv~WM^7|Ug=#G@ij0=r_lwP3~}5= zjZ~%5#kc&I#2I|7;_NG}7PXRwbI3Eg?N3&!!Nahu#U#shUc&{jNxE$RrfjuY%P?1? zdJXKtZ=sMW%5*-}Fpf(C@vsL4vb)*71*ZJ}vW7`qAq-ZOJ(F%VeXVXPrr2N2w=r9D zB!`m^Osg8O1W8=QHQB763G8o$aeLKsH)JYfnRyj60=-QqJAV^G%`3C-W%1eDE&bE1 zB3nQG#+)P#Y^Kz7UByHNAO+}3;0q6 zpYr}@JH1MXhD9v#sq>jzUKC~@`D^CZaH`@fwoiEMSV3;iHP@{miM!}lvHX%zc~jsL z-Hw+^p5^;V7$`|bnREXx8E{ZYKP&Rxtjg*|!JAPUe#RPxYZ}(&D2bb8*XMAlz*RxA zTk5e>D-Bgy$WEhFnyXc-5{?{h%4#?Qm1;Qs5`LJBK=mm9Fy6LBf{(9+YAJ;yl(2sX9z!lI5&p>=dFNz*!BPEZ^ll>)1C;~Y7>PmI#;F)) zxKBO8d5XU0I4wmTr@up%+Q9cXrpptlcZ=aV#vY+(_~pqqK54_u@HQp{o?L&5s|zDr zC_H2IC738-niyr?I#B|H#BzZA9b~pcTnD)x!gXf4M8qW=g~h=vInX22I~+K)k{k@- z7H;4sIk1@PEzHvY9PKs4{>G9D{ff$%0=MybgMkaQm5jCP**fOyxbuXYs2CBcnJ{2~ zWV92oy%8`bM`GkkoFKWfe2XHlhwF@WcnV`7dvFWpP#yPDiRajpO4MO1 zkMl}%mr|bP<0<8LBvbLc@*}!aN-F*XcNBG$<+alGm@)Pek_2SBZ-on#jthS=;H)@1iUZu tT+*wA^o~G%fwYQB_}v|+_zNAEC^|d;p{1fis5m1y8zcT}bQ;6`e*y0o{(S%d literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/utils/HttpClientUtil.class b/target/classes/com/ski/crawler/utils/HttpClientUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..178dcdf5808a730dd906386a459ac7b677f5d3f3 GIT binary patch literal 2031 zcmbVN-%}e^6#gzGxk*R?NvUkXwkV}ZDXg`>G?dmhg%%8;1ekHg8M|z*WZRI9yBk}d z{3rYuI=(s82ioe4eQRfY_OI!K^}D+WAxRx|2JYQ+?>XN+_q*RY`Tfsdegkj?Wr-ev z3ESN<{riq-d)9-R^2}!7)O_mRYmv+ve)lJ!J<1iK|ZC z39bqB=JJ~Yv7%d1Y4jqMK^zHzfs#{KE6trP<*i#=H6<`qa&4=&X?c#e_ZDM8)$s); zOaCU9S)R77c=%+lbdXUw@SOVgY`$|zAU*%kR*k@M>%PR0KsF3>Tyt@Ce@Egd5nXTF zv>KLORc4h0n0Cm3xub%rTZu@yt%5YNIHqs-w!m<=BsSA9Fe-6e;7mKDv(u>kje9_{ zpTQX35jbLd$_msi^_A7E@s1aA`O-fj6l<=pG?NEVvuT{fxWp*|^FM}W@GjmHkm{kb zn*m>IJJ)e~7fLofXH9+GRDMAI+mCGXKa}vA@*8{XeFIwLD)1W;w9x z70as>Bb)Db5toRuvA*!-hXThV{k2BydGcAV;3Ek-ooc_ME;%#GOjjF03!u!Cfg8zM zV5##VT5j{^L%Y;N8gsZV!J>`oAS~$hs^>n?I?CW<+@KDvN~PRvG(6?|svfG*gy}z*jG79S|(?z74frAko+m!PIJ`TKtK98W+Dg9M`#JI z4iIFJ-!WQmX&CVb!IND1@mFL&pL&AfWw9a(v8Na*#Etm5U7RQ+jKnic2o$6tUt-Ki zyg*tnofR?(Xn-rFDJ!c_B9a5-B6L@&g8@jo2fkIPT)= z0py!~rk^wCQRHDU^P5--=PWLv5!##KY;n|fKd$5ZaQ$vLTO76B3(eO|$kZUKeu#ZN zOVp#7z%a25#vLR22=NWv<-EmN^LmXeld(Ns_18@l$!H3Xa0~O~IE(MFfZKeoQIv59 zi;S3HYF0q`8V7E*+DS9hGStsFNH15XA{ zU&(<})qbcwdBXR#SAM80qzMb>qAwgz_-D@B=DBnYVQx?P%Dg1Z6iX+BbXA{98#9>C zVit3RTuu4%sMl%A;8-+0Nyyi7scA;PIVI~ z|Ew~ZDNUyGtibgw^2iaEI-)I^uq(_vWm*w+BYeaAq+O;HtvoS=K%@7e@Ch4JwwN+A zgJnYM=`$OILKfGe*>+v`0$1*f6ysPQBLJgoeo;O4HJ(p^a5MwD5qicDa=D z`?@WiSb&(xzHp5Wh7rX_g!jn}$LP4>f$&c~8Lri|Zuh!V#7l7ht?+o*l~dr21U@!N zB`L+YMOAoST^U))&Eb9q8-)2ngE~}b>~sj(H@+`}swcuwh89YM)qnYd|5a?$$L#0{ zVWv^FuuWL`JCyw$>q5vJ$ghUll9+6^@}kEH*k~R8I4%`P=2sxbtVDABjb*Q8|#tT`oxB9#%$8*<0LV9h)5GN9~Fz^N=k(VV29-8KH~6 E01C?up8x;= literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/view/ConsoleView$Col.class b/target/classes/com/ski/crawler/view/ConsoleView$Col.class new file mode 100644 index 0000000000000000000000000000000000000000..9a7afd8577592bd642bb33508ac67005140f2eaa GIT binary patch literal 624 zcma)3%SyvQ6g{_1Ta8g`wZ66WfoK9AruMYEv4o0Rlg&n zGtmhoq0$O{5wu05O#RiglAekQ8?FDO++#0V^fRqrW(oFbcoq5bP?_7xKVE3t3=kEU<|LA!W(9IZ|740vzX(G pr7({LmN(2;$z{x#F$SyVA6O@kMXqvK!ZM@H^Mo2!n5}00+6Vt8e>?yH literal 0 HcmV?d00001 diff --git a/target/classes/com/ski/crawler/view/ConsoleView$TablePrinter.class b/target/classes/com/ski/crawler/view/ConsoleView$TablePrinter.class new file mode 100644 index 0000000000000000000000000000000000000000..4fc519b25cc55fcd6b487693baec40b9258121c9 GIT binary patch literal 7816 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 + + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + + + +