diff --git a/project/.idea/.gitignore b/project/.idea/.gitignore
deleted file mode 100644
index b6b1ecf..0000000
--- a/project/.idea/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-# 默认忽略的文件
-/shelf/
-/workspace.xml
-# 已忽略包含查询文件的默认文件夹
-/queries/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
-# 基于编辑器的 HTTP 客户端请求
-/httpRequests/
diff --git a/project/.idea/compiler.xml b/project/.idea/compiler.xml
deleted file mode 100644
index 90212ab..0000000
--- a/project/.idea/compiler.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/project/.idea/encodings.xml b/project/.idea/encodings.xml
deleted file mode 100644
index aa00ffa..0000000
--- a/project/.idea/encodings.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/project/.idea/jarRepositories.xml b/project/.idea/jarRepositories.xml
deleted file mode 100644
index 712ab9d..0000000
--- a/project/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/project/.idea/misc.xml b/project/.idea/misc.xml
deleted file mode 100644
index 9dc782b..0000000
--- a/project/.idea/misc.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/project/.idea/vcs.xml b/project/.idea/vcs.xml
deleted file mode 100644
index 6c0b863..0000000
--- a/project/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/project/202506050211-靖佳颖-期末实验报告.docx b/project/202506050211-靖佳颖-期末实验报告.docx
deleted file mode 100644
index 5e5c363..0000000
Binary files a/project/202506050211-靖佳颖-期末实验报告.docx and /dev/null differ
diff --git a/project/charts/news_time_trend.png b/project/charts/news_time_trend.png
deleted file mode 100644
index caa1bf0..0000000
Binary files a/project/charts/news_time_trend.png and /dev/null differ
diff --git a/project/charts/news_top_words.png b/project/charts/news_top_words.png
deleted file mode 100644
index efa2fda..0000000
Binary files a/project/charts/news_top_words.png and /dev/null differ
diff --git a/project/charts/price_histogram.png b/project/charts/price_histogram.png
deleted file mode 100644
index 2e8a277..0000000
Binary files a/project/charts/price_histogram.png and /dev/null differ
diff --git a/project/charts/province_bar.png b/project/charts/province_bar.png
deleted file mode 100644
index 08ad8b7..0000000
Binary files a/project/charts/province_bar.png and /dev/null differ
diff --git a/project/charts/rating_pie.png b/project/charts/rating_pie.png
deleted file mode 100644
index 20e8426..0000000
Binary files a/project/charts/rating_pie.png and /dev/null differ
diff --git a/project/charts/temperature_comparison.png b/project/charts/temperature_comparison.png
deleted file mode 100644
index d006d51..0000000
Binary files a/project/charts/temperature_comparison.png and /dev/null differ
diff --git a/project/charts/temperature_上海.png b/project/charts/temperature_上海.png
deleted file mode 100644
index e26c047..0000000
Binary files a/project/charts/temperature_上海.png and /dev/null differ
diff --git a/project/charts/temperature_北京.png b/project/charts/temperature_北京.png
deleted file mode 100644
index ea333a1..0000000
Binary files a/project/charts/temperature_北京.png and /dev/null differ
diff --git a/project/charts/temperature_广州.png b/project/charts/temperature_广州.png
deleted file mode 100644
index e2e9759..0000000
Binary files a/project/charts/temperature_广州.png and /dev/null differ
diff --git a/project/dependency-reduced-pom.xml b/project/dependency-reduced-pom.xml
deleted file mode 100644
index 6997e2b..0000000
--- a/project/dependency-reduced-pom.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
- 4.0.0
- com.example
- crawler-project
- crawler-project
- 1.0.0
- Java爬虫项目 - MVC + Command + Strategy模式
-
-
-
- maven-compiler-plugin
- 3.11.0
-
- ${java.version}
- ${java.version}
- ${project.build.sourceEncoding}
-
-
-
- maven-shade-plugin
- 3.5.0
-
-
- package
-
- shade
-
-
-
-
- com.example.crawler.Main
-
-
-
-
-
-
-
-
-
-
- junit
- junit
- 4.13.2
- test
-
-
- hamcrest-core
- org.hamcrest
-
-
-
-
-
- 11
- 11
- 1.17.2
- 1.5.3
- 11
- 2.10.1
- UTF-8
-
-
diff --git a/project/output/books_20260530_190333.json b/project/output/books_20260530_190333.json
deleted file mode 100644
index e715ee6..0000000
--- a/project/output/books_20260530_190333.json
+++ /dev/null
@@ -1,3602 +0,0 @@
-[
- {
- "title": "A Light in the Attic",
- "price": "£51.77",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Tipping the Velvet",
- "price": "£53.74",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Soumission",
- "price": "£50.10",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Sharp Objects",
- "price": "£47.82",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Sapiens: A Brief History of Humankind",
- "price": "£54.23",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Requiem Red",
- "price": "£22.65",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Dirty Little Secrets of Getting Your Dream Job",
- "price": "£33.34",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Coming Woman: A Novel Based on the Life of the Infamous Feminist, Victoria Woodhull",
- "price": "£17.93",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Boys in the Boat: Nine Americans and Their Epic Quest for Gold at the 1936 Berlin Olympics",
- "price": "£22.60",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Black Maria",
- "price": "£52.15",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Starving Hearts (Triangular Trade Trilogy, #1)",
- "price": "£13.99",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Shakespeare's Sonnets",
- "price": "£20.66",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Set Me Free",
- "price": "£17.46",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Scott Pilgrim's Precious Little Life (Scott Pilgrim #1)",
- "price": "£52.29",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Rip it Up and Start Again",
- "price": "£35.02",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Our Band Could Be Your Life: Scenes from the American Indie Underground, 1981-1991",
- "price": "£57.25",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Olio",
- "price": "£23.88",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Mesaerion: The Best Science Fiction Stories 1800-1849",
- "price": "£37.59",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Libertarianism for Beginners",
- "price": "£51.33",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "It's Only the Himalayas",
- "price": "£45.17",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "In Her Wake",
- "price": "£12.84",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "How Music Works",
- "price": "£37.32",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Foolproof Preserving: A Guide to Small Batch Jams, Jellies, Pickles, Condiments, and More: A Foolproof Guide to Making Small Batch Jams, Jellies, Pickles, Condiments, and More",
- "price": "£30.52",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Chase Me (Paris Nights #2)",
- "price": "£25.27",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Black Dust",
- "price": "£34.53",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Birdsong: A Story in Pictures",
- "price": "£54.64",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "America's Cradle of Quarterbacks: Western Pennsylvania's Football Factory from Johnny Unitas to Joe Montana",
- "price": "£22.50",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Aladdin and His Wonderful Lamp",
- "price": "£53.13",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Worlds Elsewhere: Journeys Around Shakespeare’s Globe",
- "price": "£40.30",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Wall and Piece",
- "price": "£44.18",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Four Agreements: A Practical Guide to Personal Freedom",
- "price": "£17.66",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Five Love Languages: How to Express Heartfelt Commitment to Your Mate",
- "price": "£31.05",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Elephant Tree",
- "price": "£23.82",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Bear and the Piano",
- "price": "£36.89",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Sophie's World",
- "price": "£15.94",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Penny Maybe",
- "price": "£33.29",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Maude (1883-1993):She Grew Up with the country",
- "price": "£18.02",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "In a Dark, Dark Wood",
- "price": "£19.63",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Behind Closed Doors",
- "price": "£52.22",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "You can't bury them all: Poems",
- "price": "£33.63",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Slow States of Collapse: Poems",
- "price": "£57.31",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Reasons to Stay Alive",
- "price": "£26.41",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Private Paris (Private #10)",
- "price": "£47.61",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "#HigherSelfie: Wake Up Your Life. Free Your Soul. Find Your Tribe.",
- "price": "£23.11",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Without Borders (Wanderlove #1)",
- "price": "£45.07",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "When We Collided",
- "price": "£31.77",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "We Love You, Charlie Freeman",
- "price": "£50.27",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Untitled Collection: Sabbath Poems 2014",
- "price": "£14.27",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Unseen City: The Majesty of Pigeons, the Discreet Charm of Snails & Other Wonders of the Urban Wilderness",
- "price": "£44.18",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Unicorn Tracks",
- "price": "£18.78",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Unbound: How Eight Technologies Made Us Human, Transformed Society, and Brought Our World to the Brink",
- "price": "£25.52",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Tsubasa: WoRLD CHRoNiCLE 2 (Tsubasa WoRLD CHRoNiCLE #2)",
- "price": "£16.28",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Throwing Rocks at the Google Bus: How Growth Became the Enemy of Prosperity",
- "price": "£31.12",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "This One Summer",
- "price": "£19.49",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Thirst",
- "price": "£17.27",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Torch Is Passed: A Harding Family Story",
- "price": "£19.09",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Secret of Dreadwillow Carse",
- "price": "£56.13",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Pioneer Woman Cooks: Dinnertime: Comfort Classics, Freezer Food, 16-Minute Meals, and Other Delicious Ways to Solve Supper!",
- "price": "£56.41",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Past Never Ends",
- "price": "£56.50",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Natural History of Us (The Fine Art of Pretending #2)",
- "price": "£45.22",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Nameless City (The Nameless City #1)",
- "price": "£38.16",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Murder That Never Was (Forensic Instincts #5)",
- "price": "£54.11",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Most Perfect Thing: Inside (and Outside) a Bird's Egg",
- "price": "£42.96",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Mindfulness and Acceptance Workbook for Anxiety: A Guide to Breaking Free from Anxiety, Phobias, and Worry Using Acceptance and Commitment Therapy",
- "price": "£23.89",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Life-Changing Magic of Tidying Up: The Japanese Art of Decluttering and Organizing",
- "price": "£16.77",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Inefficiency Assassin: Time Management Tactics for Working Smarter, Not Longer",
- "price": "£20.59",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Gutsy Girl: Escapades for Your Life of Epic Adventure",
- "price": "£37.13",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Electric Pencil: Drawings from Inside State Hospital No. 3",
- "price": "£56.06",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Death of Humanity: and the Case for Life",
- "price": "£58.11",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Bulletproof Diet: Lose up to a Pound a Day, Reclaim Energy and Focus, Upgrade Your Life",
- "price": "£49.05",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Art Forger",
- "price": "£40.76",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Age of Genius: The Seventeenth Century and the Birth of the Modern Mind",
- "price": "£19.73",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Activist's Tao Te Ching: Ancient Advice for a Modern Revolution",
- "price": "£32.24",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Spark Joy: An Illustrated Master Class on the Art of Organizing and Tidying Up",
- "price": "£41.83",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Soul Reader",
- "price": "£39.58",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Security",
- "price": "£39.25",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Saga, Volume 6 (Saga (Collected Editions) #6)",
- "price": "£25.02",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Saga, Volume 5 (Saga (Collected Editions) #5)",
- "price": "£51.04",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Reskilling America: Learning to Labor in the Twenty-First Century",
- "price": "£19.83",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Rat Queens, Vol. 3: Demons (Rat Queens (Collected Editions) #11-15)",
- "price": "£50.40",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Princess Jellyfish 2-in-1 Omnibus, Vol. 01 (Princess Jellyfish 2-in-1 Omnibus #1)",
- "price": "£13.61",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Princess Between Worlds (Wide-Awake Princess #5)",
- "price": "£13.34",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Pop Gun War, Volume 1: Gift",
- "price": "£18.97",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Political Suicide: Missteps, Peccadilloes, Bad Calls, Backroom Hijinx, Sordid Pasts, Rotten Breaks, and Just Plain Dumb Mistakes in the Annals of American Politics",
- "price": "£36.28",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Patience",
- "price": "£10.16",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Outcast, Vol. 1: A Darkness Surrounds Him (Outcast #1)",
- "price": "£15.44",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "orange: The Complete Collection 1 (orange: The Complete Collection #1)",
- "price": "£48.41",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Online Marketing for Busy Authors: A Step-By-Step Guide",
- "price": "£46.35",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "On a Midnight Clear",
- "price": "£14.07",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Obsidian (Lux #1)",
- "price": "£14.86",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "My Paris Kitchen: Recipes and Stories",
- "price": "£33.37",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Masks and Shadows",
- "price": "£56.40",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Mama Tried: Traditional Italian Cooking for the Screwed, Crude, Vegan, and Tattooed",
- "price": "£14.02",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Lumberjanes, Vol. 2: Friendship to the Max (Lumberjanes #5-8)",
- "price": "£46.91",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Lumberjanes, Vol. 1: Beware the Kitten Holy (Lumberjanes #1-4)",
- "price": "£45.61",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Lumberjanes Vol. 3: A Terrible Plan (Lumberjanes #9-12)",
- "price": "£19.92",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Layered: Baking, Building, and Styling Spectacular Cakes",
- "price": "£40.11",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Judo: Seven Steps to Black Belt (an Introductory Guide for Beginners)",
- "price": "£53.90",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Join",
- "price": "£35.67",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "In the Country We Love: My Family Divided",
- "price": "£22.00",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Immunity: How Elie Metchnikoff Changed the Course of Modern Medicine",
- "price": "£57.36",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "I Hate Fairyland, Vol. 1: Madly Ever After (I Hate Fairyland (Compilations) #1-5)",
- "price": "£29.17",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "I am a Hero Omnibus Volume 1",
- "price": "£54.63",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "How to Be Miserable: 40 Strategies You Already Use",
- "price": "£46.03",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Her Backup Boyfriend (The Sorensen Family #1)",
- "price": "£33.97",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Giant Days, Vol. 2 (Giant Days #5-8)",
- "price": "£22.11",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Forever and Forever: The Courtship of Henry Longfellow and Fanny Appleton",
- "price": "£29.69",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "First and First (Five Boroughs #3)",
- "price": "£15.97",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Fifty Shades Darker (Fifty Shades #2)",
- "price": "£21.96",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Everydata: The Misinformation Hidden in the Little Data You Consume Every Day",
- "price": "£54.35",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Don't Be a Jerk: And Other Practical Advice from Dogen, Japan's Greatest Zen Master",
- "price": "£37.97",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Danganronpa Volume 1",
- "price": "£51.99",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Crown of Midnight (Throne of Glass #2)",
- "price": "£43.29",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Codename Baboushka, Volume 1: The Conclave of Death",
- "price": "£36.72",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Camp Midnight",
- "price": "£17.08",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Call the Nurse: True Stories of a Country Nurse on a Scottish Isle",
- "price": "£29.14",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Burning",
- "price": "£28.81",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Bossypants",
- "price": "£49.46",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Bitch Planet, Vol. 1: Extraordinary Machine (Bitch Planet (Collected Editions))",
- "price": "£37.92",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Avatar: The Last Airbender: Smoke and Shadow, Part 3 (Smoke and Shadow #3)",
- "price": "£28.09",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Algorithms to Live By: The Computer Science of Human Decisions",
- "price": "£30.81",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "A World of Flavor: Your Gluten Free Passport",
- "price": "£42.95",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "A Piece of Sky, a Grain of Rice: A Memoir in Four Meditations",
- "price": "£56.76",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "A Murder in Time",
- "price": "£16.64",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "A Flight of Arrows (The Pathfinders #2)",
- "price": "£55.53",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "A Fierce and Subtle Poison",
- "price": "£28.13",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "A Court of Thorns and Roses (A Court of Thorns and Roses #1)",
- "price": "£52.37",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "(Un)Qualified: How God Uses Broken People to Do Big Things",
- "price": "£54.00",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "You Are What You Love: The Spiritual Power of Habit",
- "price": "£21.87",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "William Shakespeare's Star Wars: Verily, A New Hope (William Shakespeare's Star Wars #4)",
- "price": "£43.30",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Tuesday Nights in 1980",
- "price": "£21.04",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Tracing Numbers on a Train",
- "price": "£41.60",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Throne of Glass (Throne of Glass #1)",
- "price": "£35.07",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Thomas Jefferson and the Tripoli Pirates: The Forgotten War That Changed American History",
- "price": "£59.64",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Thirteen Reasons Why",
- "price": "£52.72",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The White Cat and the Monk: A Retelling of the Poem “Pangur Bán”",
- "price": "£58.08",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Wedding Dress",
- "price": "£24.12",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Vacationers",
- "price": "£42.15",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Third Wave: An Entrepreneur’s Vision of the Future",
- "price": "£12.61",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Stranger",
- "price": "£17.44",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Shadow Hero (The Shadow Hero)",
- "price": "£33.14",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Secret (The Secret #1)",
- "price": "£27.37",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Regional Office Is Under Attack!",
- "price": "£51.36",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Psychopath Test: A Journey Through the Madness Industry",
- "price": "£36.00",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Project",
- "price": "£10.65",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Power of Now: A Guide to Spiritual Enlightenment",
- "price": "£43.54",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Omnivore's Dilemma: A Natural History of Four Meals",
- "price": "£38.21",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Nerdy Nummies Cookbook: Sweet Treats for the Geek in All of Us",
- "price": "£37.34",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Murder of Roger Ackroyd (Hercule Poirot #4)",
- "price": "£44.10",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Mistake (Off-Campus #2)",
- "price": "£43.29",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Matchmaker's Playbook (Wingmen Inc. #1)",
- "price": "£55.85",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Love and Lemons Cookbook: An Apple-to-Zucchini Celebration of Impromptu Cooking",
- "price": "£37.60",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Long Shadow of Small Ghosts: Murder and Memory in an American City",
- "price": "£10.97",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Kite Runner",
- "price": "£41.82",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The House by the Lake",
- "price": "£36.95",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Glittering Court (The Glittering Court #1)",
- "price": "£44.28",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Girl on the Train",
- "price": "£55.02",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Genius of Birds",
- "price": "£17.24",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Emerald Mystery",
- "price": "£23.15",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Cookies & Cups Cookbook: 125+ sweet & savory recipes reminding you to Always Eat Dessert First",
- "price": "£41.25",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Bridge to Consciousness: I'm Writing the Bridge Between Science and Our Old and New Beliefs.",
- "price": "£32.00",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Artist's Way: A Spiritual Path to Higher Creativity",
- "price": "£38.49",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Art of War",
- "price": "£33.34",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Argonauts",
- "price": "£10.93",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The 10% Entrepreneur: Live Your Startup Dream Without Quitting Your Day Job",
- "price": "£27.55",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Suddenly in Love (Lake Haven #1)",
- "price": "£55.99",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Something More Than This",
- "price": "£16.24",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Soft Apocalypse",
- "price": "£26.12",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "So You've Been Publicly Shamed",
- "price": "£12.23",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Shoe Dog: A Memoir by the Creator of NIKE",
- "price": "£23.99",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Shobu Samurai, Project Aryoku (#3)",
- "price": "£29.06",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Secrets and Lace (Fatal Hearts #1)",
- "price": "£20.27",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Scarlett Epstein Hates It Here",
- "price": "£43.55",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Romero and Juliet: A Tragic Tale of Love and Zombies",
- "price": "£36.94",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Redeeming Love",
- "price": "£20.47",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Poses for Artists Volume 1 - Dynamic and Sitting Poses: An Essential Reference for Figure Drawing and the Human Form",
- "price": "£41.06",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Poems That Make Grown Women Cry",
- "price": "£14.19",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Nightingale, Sing",
- "price": "£38.28",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Night Sky with Exit Wounds",
- "price": "£41.05",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Mrs. Houdini",
- "price": "£30.25",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Modern Romance",
- "price": "£28.26",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Miss Peregrine’s Home for Peculiar Children (Miss Peregrine’s Peculiar Children #1)",
- "price": "£10.76",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Louisa: The Extraordinary Life of Mrs. Adams",
- "price": "£16.85",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Little Red",
- "price": "£13.47",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Library of Souls (Miss Peregrine’s Peculiar Children #3)",
- "price": "£48.56",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Large Print Heart of the Pride",
- "price": "£19.15",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "I Had a Nice Time And Other Lies...: How to find love & sh*t like that",
- "price": "£57.36",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Hollow City (Miss Peregrine’s Peculiar Children #2)",
- "price": "£42.98",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Grumbles",
- "price": "£22.16",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Full Moon over Noah’s Ark: An Odyssey to Mount Ararat and Beyond",
- "price": "£49.43",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Frostbite (Vampire Academy #2)",
- "price": "£29.99",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Follow You Home",
- "price": "£21.36",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "First Steps for New Christians (Print Edition)",
- "price": "£29.00",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Finders Keepers (Bill Hodges Trilogy #2)",
- "price": "£53.53",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Fables, Vol. 1: Legends in Exile (Fables #1)",
- "price": "£41.62",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Eureka Trivia 6.0",
- "price": "£54.59",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Drive: The Surprising Truth About What Motivates Us",
- "price": "£34.95",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Done Rubbed Out (Reightman & Bailey #1)",
- "price": "£37.72",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Doing It Over (Most Likely To #1)",
- "price": "£35.61",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Deliciously Ella Every Day: Quick and Easy Recipes for Gluten-Free Snacks, Packed Lunches, and Simple Meals",
- "price": "£42.16",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Dark Notes",
- "price": "£19.19",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Daring Greatly: How the Courage to Be Vulnerable Transforms the Way We Live, Love, Parent, and Lead",
- "price": "£19.43",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Close to You",
- "price": "£49.46",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Chasing Heaven: What Dying Taught Me About Living",
- "price": "£37.80",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Big Magic: Creative Living Beyond Fear",
- "price": "£30.80",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Becoming Wise: An Inquiry into the Mystery and Art of Living",
- "price": "£27.43",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Beauty Restored (Riley Family Legacy Novellas #3)",
- "price": "£11.11",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Batman: The Long Halloween (Batman)",
- "price": "£36.50",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Batman: The Dark Knight Returns (Batman)",
- "price": "£15.38",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Ayumi's Violin",
- "price": "£15.48",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Anonymous",
- "price": "£46.82",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Amy Meets the Saints and Sages",
- "price": "£18.46",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Amid the Chaos",
- "price": "£36.58",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Amatus",
- "price": "£50.54",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Agnostic: A Spirited Manifesto",
- "price": "£12.51",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Zealot: The Life and Times of Jesus of Nazareth",
- "price": "£24.70",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "You (You #1)",
- "price": "£43.61",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Wonder Woman: Earth One, Volume One (Wonder Woman: Earth One #1)",
- "price": "£37.34",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Wild Swans",
- "price": "£14.36",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Why the Right Went Wrong: Conservatism--From Goldwater to the Tea Party and Beyond",
- "price": "£52.65",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Whole Lotta Creativity Going On: 60 Fun and Unusual Exercises to Awaken and Strengthen Your Creativity",
- "price": "£38.20",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "What's It Like in Space?: Stories from Astronauts Who've Been There",
- "price": "£19.60",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "We Are Robin, Vol. 1: The Vigilante Business (We Are Robin #1)",
- "price": "£53.90",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Walt Disney's Alice in Wonderland",
- "price": "£12.96",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "V for Vendetta (V for Vendetta Complete)",
- "price": "£37.10",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Until Friday Night (The Field Party #1)",
- "price": "£46.31",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Unbroken: A World War II Story of Survival, Resilience, and Redemption",
- "price": "£45.95",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Twenty Yawns",
- "price": "£22.08",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Through the Woods",
- "price": "£25.38",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "This Is Where It Ends",
- "price": "£27.12",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Year of Magical Thinking",
- "price": "£43.04",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Wright Brothers",
- "price": "£56.80",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The White Queen (The Cousins' War #1)",
- "price": "£25.91",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Wedding Pact (The O'Malleys #2)",
- "price": "£32.61",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Time Keeper",
- "price": "£27.88",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Testament of Mary",
- "price": "£52.67",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Star-Touched Queen",
- "price": "£46.02",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Songs of the Gods",
- "price": "£44.48",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Song of Achilles",
- "price": "£37.40",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Rosie Project (Don Tillman #1)",
- "price": "£54.04",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Power of Habit: Why We Do What We Do in Life and Business",
- "price": "£16.88",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Marriage of Opposites",
- "price": "£28.08",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Lucifer Effect: Understanding How Good People Turn Evil",
- "price": "£10.40",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Long Haul (Diary of a Wimpy Kid #9)",
- "price": "£44.07",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Loney",
- "price": "£23.40",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Literature Book (Big Ideas Simply Explained)",
- "price": "£17.43",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Last Mile (Amos Decker #2)",
- "price": "£54.21",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Immortal Life of Henrietta Lacks",
- "price": "£40.67",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Hidden Oracle (The Trials of Apollo #1)",
- "price": "£52.26",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Help Yourself Cookbook for Kids: 60 Easy Plant-Based Recipes Kids Can Make to Stay Healthy and Save the Earth",
- "price": "£28.77",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Guilty (Will Robie #4)",
- "price": "£13.82",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The First Hostage (J.B. Collins #2)",
- "price": "£25.85",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Dovekeepers",
- "price": "£48.78",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Darkest Lie",
- "price": "£35.35",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Bane Chronicles (The Bane Chronicles #1-11)",
- "price": "£44.73",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Bad-Ass Librarians of Timbuktu: And Their Race to Save the World’s Most Precious Manuscripts",
- "price": "£15.77",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The 14th Colony (Cotton Malone #11)",
- "price": "£39.24",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "That Darkness (Gardiner and Renner #1)",
- "price": "£13.92",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Tastes Like Fear (DI Marnie Rome #3)",
- "price": "£10.69",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Take Me with You",
- "price": "£45.21",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Swell: A Year of Waves",
- "price": "£45.58",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Superman Vol. 1: Before Truth (Superman by Gene Luen Yang #1)",
- "price": "£11.89",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Still Life with Bread Crumbs",
- "price": "£26.41",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Steve Jobs",
- "price": "£39.50",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Sorting the Beef from the Bull: The Science of Food Fraud Forensics",
- "price": "£44.74",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Someone Like You (The Harrisons #2)",
- "price": "£52.79",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "So Cute It Hurts!!, Vol. 6 (So Cute It Hurts!! #6)",
- "price": "£35.43",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Shtum",
- "price": "£55.84",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "See America: A Celebration of Our National Parks & Treasured Sites",
- "price": "£48.87",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "salt.",
- "price": "£46.78",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Robin War",
- "price": "£47.82",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Red Hood/Arsenal, Vol. 1: Open for Business (Red Hood/Arsenal #1)",
- "price": "£25.48",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Rain Fish",
- "price": "£23.57",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Quarter Life Poetry: Poems for the Young, Broke and Hangry",
- "price": "£50.89",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Pet Sematary",
- "price": "£10.56",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Overload: How to Unplug, Unwind, and Unleash Yourself from the Pressure of Stress",
- "price": "£52.15",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Once Was a Time",
- "price": "£18.28",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Old School (Diary of a Wimpy Kid #10)",
- "price": "£11.83",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "No Dream Is Too High: Life Lessons From a Man Who Walked on the Moon",
- "price": "£21.95",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Naruto (3-in-1 Edition), Vol. 14: Includes Vols. 40, 41 & 42 (Naruto: Omnibus #14)",
- "price": "£38.39",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "My Name Is Lucy Barton",
- "price": "£41.56",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "My Mrs. Brown",
- "price": "£24.48",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "My Kind of Crazy",
- "price": "£40.36",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Mr. Mercedes (Bill Hodges Trilogy #1)",
- "price": "£28.90",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "More Than Music (Chasing the Dream #1)",
- "price": "£37.61",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Made to Stick: Why Some Ideas Survive and Others Die",
- "price": "£38.85",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Luis Paints the World",
- "price": "£53.95",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Luckiest Girl Alive",
- "price": "£49.83",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Lowriders to the Center of the Earth (Lowriders in Space #2)",
- "price": "£51.51",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Love Is a Mix Tape (Music #1)",
- "price": "£18.03",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Looking for Lovely: Collecting the Moments that Matter",
- "price": "£29.14",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Living Leadership by Insight: A Good Leader Achieves, a Great Leader Builds Monuments",
- "price": "£46.91",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Let It Out: A Journey Through Journaling",
- "price": "£26.79",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Lady Midnight (The Dark Artifices #1)",
- "price": "£16.28",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "It's All Easy: Healthy, Delicious Weeknight Meals in under 30 Minutes",
- "price": "£19.55",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Island of Dragons (Unwanteds #7)",
- "price": "£29.65",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "I Know What I'm Doing -- and Other Lies I Tell Myself: Dispatches from a Life Under Construction",
- "price": "£25.98",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "I Am Pilgrim (Pilgrim #1)",
- "price": "£10.60",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Hyperbole and a Half: Unfortunate Situations, Flawed Coping Mechanisms, Mayhem, and Other Things That Happened",
- "price": "£14.75",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Hush, Hush (Hush, Hush #1)",
- "price": "£47.02",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Hold Your Breath (Search and Rescue #1)",
- "price": "£28.82",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Hamilton: The Revolution",
- "price": "£58.79",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Greek Mythic History",
- "price": "£10.23",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "God: The Most Unpleasant Character in All Fiction",
- "price": "£30.03",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Glory over Everything: Beyond The Kitchen House",
- "price": "£45.84",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Feathers: Displays of Brilliant Plumage",
- "price": "£49.05",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Far & Away: Places on the Brink of Change: Seven Continents, Twenty-Five Years",
- "price": "£15.06",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Every Last Word",
- "price": "£46.47",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Eligible (The Austen Project #4)",
- "price": "£27.09",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "El Deafo",
- "price": "£57.62",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Eight Hundred Grapes",
- "price": "£14.39",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Eaternity: More than 150 Deliciously Easy Vegan Recipes for a Long, Healthy, Satisfied, Joyful Life",
- "price": "£51.75",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Eat Fat, Get Thin",
- "price": "£54.07",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Don't Get Caught",
- "price": "£55.35",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Doctor Sleep (The Shining #2)",
- "price": "£40.12",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Demigods & Magicians: Percy and Annabeth Meet the Kanes (Percy Jackson & Kane Chronicles Crossover #1-3)",
- "price": "£37.51",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Dear Mr. Knightley",
- "price": "£11.21",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Daily Fantasy Sports",
- "price": "£36.58",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Crazy Love: Overwhelmed by a Relentless God",
- "price": "£47.72",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Cometh the Hour (The Clifton Chronicles #6)",
- "price": "£25.01",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Code Name Verity (Code Name Verity #1)",
- "price": "£22.13",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Clockwork Angel (The Infernal Devices #1)",
- "price": "£44.14",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "City of Glass (The Mortal Instruments #3)",
- "price": "£56.02",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "City of Fallen Angels (The Mortal Instruments #4)",
- "price": "£11.23",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "City of Bones (The Mortal Instruments #1)",
- "price": "£43.28",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "City of Ashes (The Mortal Instruments #2)",
- "price": "£47.27",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Cell",
- "price": "£20.29",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Catching Jordan (Hundred Oaks)",
- "price": "£50.83",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Carry On, Warrior: Thoughts on Life Unarmed",
- "price": "£31.85",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Carrie",
- "price": "£46.23",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Buying In: The Secret Dialogue Between What We Buy and Who We Are",
- "price": "£37.80",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Brain on Fire: My Month of Madness",
- "price": "£49.32",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Batman: Europa",
- "price": "£32.01",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Barefoot Contessa Back to Basics",
- "price": "£28.01",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Barefoot Contessa at Home: Everyday Recipes You'll Make Over and Over Again",
- "price": "£50.62",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Balloon Animals",
- "price": "£17.03",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Art Ops Vol. 1",
- "price": "£48.80",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Aristotle and Dante Discover the Secrets of the Universe (Aristotle and Dante Discover the Secrets of the Universe #1)",
- "price": "£58.14",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Angels Walking (Angels Walking #1)",
- "price": "£34.20",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Angels & Demons (Robert Langdon #1)",
- "price": "£51.48",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "All the Light We Cannot See",
- "price": "£29.87",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Adulthood Is a Myth: A \"Sarah's Scribbles\" Collection",
- "price": "£10.90",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Abstract City",
- "price": "£56.37",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "A Time of Torment (Charlie Parker #14)",
- "price": "£48.35",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "A Study in Scarlet (Sherlock Holmes #1)",
- "price": "£16.73",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "A Series of Catastrophes and Miracles: A True Story of Love, Science, and Cancer",
- "price": "£56.48",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "A People's History of the United States",
- "price": "£40.79",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "A Man Called Ove",
- "price": "£39.72",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "A Distant Mirror: The Calamitous 14th Century",
- "price": "£14.58",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "A Brush of Wings (Angels Walking #3)",
- "price": "£55.51",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "1491: New Revelations of the Americas Before Columbus",
- "price": "£21.80",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Three Searches, Meaning, and the Story",
- "price": "£13.33",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Searching for Meaning in Gailana",
- "price": "£38.73",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Rook",
- "price": "£37.86",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "My Kitchen Year: 136 Recipes That Saved My Life",
- "price": "£11.53",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "13 Hours: The Inside Account of What Really Happened In Benghazi",
- "price": "£27.06",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Will You Won't You Want Me?",
- "price": "£13.86",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Tipping Point for Planet Earth: How Close Are We to the Edge?",
- "price": "£37.55",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Star-Touched Queen",
- "price": "£32.30",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Silent Sister (Riley MacPherson #1)",
- "price": "£46.29",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Midnight Watch: A Novel of the Titanic and the Californian",
- "price": "£26.20",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Lonely City: Adventures in the Art of Being Alone",
- "price": "£33.26",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Gray Rhino: How to Recognize and Act on the Obvious Dangers We Ignore",
- "price": "£59.15",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Golden Condom: And Other Essays on Love Lost and Found",
- "price": "£39.43",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Epidemic (The Program 0.6)",
- "price": "£14.44",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Dinner Party",
- "price": "£56.54",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Diary of a Young Girl",
- "price": "£59.90",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Children",
- "price": "£11.88",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Stars Above (The Lunar Chronicles #4.5)",
- "price": "£48.05",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Snatched: How A Drug Queen Went Undercover for the DEA and Was Kidnapped By Colombian Guerillas",
- "price": "£21.21",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Raspberry Pi Electronics Projects for the Evil Genius",
- "price": "£49.67",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Quench Your Own Thirst: Business Lessons Learned Over a Beer or Two",
- "price": "£43.14",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Psycho: Sanitarium (Psycho #1.5)",
- "price": "£36.97",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Poisonous (Max Revere Novels #3)",
- "price": "£26.80",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "One with You (Crossfire #5)",
- "price": "£15.71",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "No Love Allowed (Dodge Cove #1)",
- "price": "£54.65",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Murder at the 42nd Street Library (Raymond Ambler #1)",
- "price": "£54.36",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Most Wanted",
- "price": "£35.28",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Love, Lies and Spies",
- "price": "£20.55",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "How to Speak Golf: An Illustrated Guide to Links Lingo",
- "price": "£58.32",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Hide Away (Eve Duncan #20)",
- "price": "£11.84",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Furiously Happy: A Funny Book About Horrible Things",
- "price": "£41.46",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Everyday Italian: 125 Simple and Delicious Recipes",
- "price": "£20.10",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Equal Is Unfair: America's Misguided Fight Against Income Inequality",
- "price": "£56.86",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Eleanor & Park",
- "price": "£56.51",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Dirty (Dive Bar #1)",
- "price": "£40.83",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Can You Keep a Secret? (Fear Street Relaunch #4)",
- "price": "£48.64",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Boar Island (Anna Pigeon #19)",
- "price": "£59.48",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "A Paris Apartment",
- "price": "£39.01",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "A la Mode: 120 Recipes in 60 Pairings: Pies, Tarts, Cakes, Crisps, and More Topped with Ice Cream, Gelato, Frozen Custard, and More",
- "price": "£38.77",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Troublemaker: Surviving Hollywood and Scientology",
- "price": "£48.39",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Widow",
- "price": "£27.26",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Sleep Revolution: Transforming Your Life, One Night at a Time",
- "price": "£11.68",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Improbability of Love",
- "price": "£59.45",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Art of Startup Fundraising",
- "price": "£21.00",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Take Me Home Tonight (Rock Star Romance #3)",
- "price": "£53.98",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Sleeping Giants (Themis Files #1)",
- "price": "£48.74",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Setting the World on Fire: The Brief, Astonishing Life of St. Catherine of Siena",
- "price": "£21.15",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Playing with Fire",
- "price": "£13.71",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Off the Hook (Fishing for Trouble #1)",
- "price": "£47.67",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Mothering Sunday",
- "price": "£13.34",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Mother, Can You Not?",
- "price": "£16.89",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "M Train",
- "price": "£27.18",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Lilac Girls",
- "price": "£17.28",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Lies and Other Acts of Love",
- "price": "£45.14",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Lab Girl",
- "price": "£40.85",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Keep Me Posted",
- "price": "£20.46",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "It Didn't Start with You: How Inherited Family Trauma Shapes Who We Are and How to End the Cycle",
- "price": "£56.27",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Grey (Fifty Shades #4)",
- "price": "£48.49",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Exit, Pursued by a Bear",
- "price": "£51.34",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Daredevils",
- "price": "£16.34",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Cravings: Recipes for What You Want to Eat",
- "price": "£20.50",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Born for This: How to Find the Work You Were Meant to Do",
- "price": "£21.59",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Arena",
- "price": "£21.36",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Adultery",
- "price": "£20.88",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "A Mother's Reckoning: Living in the Aftermath of Tragedy",
- "price": "£19.53",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "A Gentleman's Position (Society of Gentlemen #3)",
- "price": "£14.75",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "11/22/63",
- "price": "£48.48",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "10% Happier: How I Tamed the Voice in My Head, Reduced Stress Without Losing My Edge, and Found Self-Help That Actually Works",
- "price": "£24.57",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "10-Day Green Smoothie Cleanse: Lose Up to 15 Pounds in 10 Days!",
- "price": "£49.71",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Without Shame",
- "price": "£48.27",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Watchmen",
- "price": "£58.05",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Unlimited Intuition Now",
- "price": "£58.87",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Underlying Notes",
- "price": "£11.82",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Shack",
- "price": "£28.03",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The New Brand You: Your New Image Makes the Sale for You",
- "price": "£44.05",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Moosewood Cookbook: Recipes from Moosewood Restaurant, Ithaca, New York",
- "price": "£12.34",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Flowers Lied",
- "price": "£16.68",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Fabric of the Cosmos: Space, Time, and the Texture of Reality",
- "price": "£55.91",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Book of Mormon",
- "price": "£24.57",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Art and Science of Low Carbohydrate Living",
- "price": "£52.98",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Alien Club",
- "price": "£54.40",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Suzie Snowflake: One beautiful flake (a self-esteem story)",
- "price": "£54.81",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Nap-a-Roo",
- "price": "£25.08",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "NaNo What Now? Finding your editing process, revising your NaNoWriMo book and building a writing career through publishing and beyond",
- "price": "£10.41",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Modern Day Fables",
- "price": "£47.44",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "If I Gave You God's Phone Number....: Searching for Spirituality in America",
- "price": "£20.91",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Fruits Basket, Vol. 9 (Fruits Basket #9)",
- "price": "£33.95",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Dress Your Family in Corduroy and Denim",
- "price": "£43.68",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Don't Forget Steven",
- "price": "£33.23",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Chernobyl 01:23:40: The Incredible True Story of the World's Worst Nuclear Disaster",
- "price": "£35.92",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Art and Fear: Observations on the Perils (and Rewards) of Artmaking",
- "price": "£48.63",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "A Shard of Ice (The Black Symphony Saga #1)",
- "price": "£56.63",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "A Hero's Curse (The Unseen Chronicles #1)",
- "price": "£50.49",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "23 Degrees South: A Tropical Tale of Changing Whether...",
- "price": "£35.79",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Zero to One: Notes on Startups, or How to Build the Future",
- "price": "£34.06",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Why Not Me?",
- "price": "£17.76",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "When Breath Becomes Air",
- "price": "£39.36",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Vagabonding: An Uncommon Guide to the Art of Long-Term World Travel",
- "price": "£36.94",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Unlikely Pilgrimage of Harold Fry (Harold Fry #1)",
- "price": "£43.62",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The New Drawing on the Right Side of the Brain",
- "price": "£43.02",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Midnight Assassin: Panic, Scandal, and the Hunt for America's First Serial Killer",
- "price": "£28.45",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Martian (The Martian #1)",
- "price": "£41.39",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The High Mountains of Portugal",
- "price": "£51.15",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Grownup",
- "price": "£35.88",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The E-Myth Revisited: Why Most Small Businesses Don't Work and What to Do About It",
- "price": "£36.91",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "South of Sunshine",
- "price": "£28.93",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Smarter Faster Better: The Secrets of Being Productive in Life and Business",
- "price": "£38.89",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Silence in the Dark (Logan Point #4)",
- "price": "£58.33",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Shadows of the Past (Logan Point #1)",
- "price": "£39.67",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Roller Girl",
- "price": "£14.10",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Rising Strong",
- "price": "£21.82",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Proofs of God: Classical Arguments from Tertullian to Barth",
- "price": "£54.21",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Please Kill Me: The Uncensored Oral History of Punk",
- "price": "£31.19",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Out of Print: City Lights Spotlight No. 14",
- "price": "£53.64",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "My Life Next Door (My Life Next Door )",
- "price": "£36.39",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Miller's Valley",
- "price": "£58.54",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Man's Search for Meaning",
- "price": "£29.48",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Love That Boy: What Two Presidents, Eight Road Trips, and My Son Taught Me About a Parent's Expectations",
- "price": "£25.06",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Living Forward: A Proven Plan to Stop Drifting and Get the Life You Want",
- "price": "£12.55",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Les Fleurs du Mal",
- "price": "£29.04",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Left Behind (Left Behind #1)",
- "price": "£40.72",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Kill 'Em and Leave: Searching for James Brown and the American Soul",
- "price": "£45.05",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Kierkegaard: A Christian Missionary to Christians",
- "price": "£47.13",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "John Vassos: Industrial Design for Modern Life",
- "price": "£20.22",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "I'll Give You the Sun",
- "price": "£56.48",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "I Will Find You",
- "price": "£44.21",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Hystopia: A Novel",
- "price": "£21.96",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Howl and Other Poems",
- "price": "£40.45",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "History of Beauty",
- "price": "£10.29",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Heaven is for Real: A Little Boy's Astounding Story of His Trip to Heaven and Back",
- "price": "£52.86",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Future Shock (Future Shock #1)",
- "price": "£55.65",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Ender's Game (The Ender Quintet #1)",
- "price": "£43.64",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Diary of a Citizen Scientist: Chasing Tiger Beetles and Other New Ways of Engaging the World",
- "price": "£28.41",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Death by Leisure: A Cautionary Tale",
- "price": "£37.51",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Brilliant Beacons: A History of the American Lighthouse",
- "price": "£11.45",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Brazen: The Courage to Find the You That's Been Hiding",
- "price": "£19.22",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Between the World and Me",
- "price": "£56.91",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Being Mortal: Medicine and What Matters in the End",
- "price": "£55.06",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "A Murder Over a Girl: Justice, Gender, Junior High",
- "price": "£13.20",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "32 Yolks",
- "price": "£53.63",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "\"Most Blessed of the Patriarchs\": Thomas Jefferson and the Empire of the Imagination",
- "price": "£44.48",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "You Are a Badass: How to Stop Doubting Your Greatness and Start Living an Awesome Life",
- "price": "£12.08",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Wildlife of New York: A Five-Borough Coloring Book",
- "price": "£22.14",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "What Happened on Beale Street (Secrets of the South Mysteries #2)",
- "price": "£25.37",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Unreasonable Hope: Finding Faith in the God Who Brings Purpose to Your Pain",
- "price": "£46.33",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Under the Tuscan Sun",
- "price": "£37.33",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Toddlers Are A**holes: It's Not Your Fault",
- "price": "£25.55",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Year of Living Biblically: One Man's Humble Quest to Follow the Bible as Literally as Possible",
- "price": "£34.72",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Whale",
- "price": "£35.96",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Story of Art",
- "price": "£41.14",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Origin of Species",
- "price": "£10.01",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Great Gatsby",
- "price": "£36.05",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Good Girl",
- "price": "£49.03",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Glass Castle",
- "price": "£16.24",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Faith of Christopher Hitchens: The Restless Soul of the World's Most Notorious Atheist",
- "price": "£39.55",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Drowning Girls",
- "price": "£35.67",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Constant Princess (The Tudor Court #1)",
- "price": "£16.62",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Bourne Identity (Jason Bourne #1)",
- "price": "£42.78",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Bachelor Girl's Guide to Murder (Herringford and Watts Mysteries #1)",
- "price": "£52.30",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Art Book",
- "price": "£32.34",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The 7 Habits of Highly Effective People: Powerful Lessons in Personal Change",
- "price": "£33.17",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Team of Rivals: The Political Genius of Abraham Lincoln",
- "price": "£20.12",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Steal Like an Artist: 10 Things Nobody Told You About Being Creative",
- "price": "£20.90",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Sit, Stay, Love",
- "price": "£20.90",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Sister Dear",
- "price": "£40.20",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Shrunken Treasures: Literary Classics, Short, Sweet, and Silly",
- "price": "£52.87",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Rich Dad, Poor Dad",
- "price": "£51.74",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Raymie Nightingale",
- "price": "£34.41",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Playing from the Heart",
- "price": "£32.38",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Nightstruck: A Novel",
- "price": "£50.35",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Naturally Lean: 125 Nourishing Gluten-Free, Plant-Based Recipes--All Under 300 Calories",
- "price": "£11.38",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Meternity",
- "price": "£43.58",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Memoirs of a Geisha",
- "price": "£49.67",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Like Never Before (Walker Family #2)",
- "price": "£28.77",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Life of Pi",
- "price": "£13.22",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Leave This Song Behind: Teen Poetry at Its Best",
- "price": "£51.17",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "King's Folly (The Kinsman Chronicles #1)",
- "price": "£39.61",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "John Adams",
- "price": "£57.43",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "How to Cook Everything Vegetarian: Simple Meatless Recipes for Great Food (How to Cook Everything)",
- "price": "£46.01",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "How to Be a Domestic Goddess: Baking and the Art of Comfort Cooking",
- "price": "£28.25",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Good in Bed (Cannie Shapiro #1)",
- "price": "£37.05",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Fruits Basket, Vol. 7 (Fruits Basket #7)",
- "price": "£19.57",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "For the Love: Fighting for Grace in a World of Impossible Standards",
- "price": "£45.13",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Finding God in the Ruins: How God Redeems Pain",
- "price": "£46.64",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Every Heart a Doorway (Every Heart A Doorway #1)",
- "price": "£12.16",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Delivering the Truth (Quaker Midwife Mystery #1)",
- "price": "£20.89",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Counted With the Stars (Out from Egypt #1)",
- "price": "£17.97",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Chronicles, Vol. 1",
- "price": "£52.60",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Blue Like Jazz: Nonreligious Thoughts on Christian Spirituality",
- "price": "£25.77",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Benjamin Franklin: An American Life",
- "price": "£48.19",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "At The Existentialist Café: Freedom, Being, and apricot cocktails with: Jean-Paul Sartre, Simone de Beauvoir, Albert Camus, Martin Heidegger, Edmund Husserl, Karl Jaspers, Maurice Merleau-Ponty and others",
- "price": "£29.93",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "A Summer In Europe",
- "price": "£44.34",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "A Short History of Nearly Everything",
- "price": "£52.40",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "A Gathering of Shadows (Shades of Magic #2)",
- "price": "£44.81",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Sound Of Love",
- "price": "£57.84",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Rise and Fall of the Third Reich: A History of Nazi Germany",
- "price": "£39.67",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Perks of Being a Wallflower",
- "price": "£55.02",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Mysterious Affair at Styles (Hercule Poirot #1)",
- "price": "£24.80",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Man Who Mistook His Wife for a Hat and Other Clinical Tales",
- "price": "£59.45",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Makings of a Fatherless Child",
- "price": "£31.58",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Joy of Cooking",
- "price": "£43.27",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Invention of Wings",
- "price": "£37.34",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Hobbit (Middle-Earth Universe)",
- "price": "£17.80",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Great Railway Bazaar",
- "price": "£30.54",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Golden Compass (His Dark Materials #1)",
- "price": "£18.77",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The God Delusion",
- "price": "£46.85",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Girl You Left Behind (The Girl You Left Behind #1)",
- "price": "£15.79",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Fellowship of the Ring (The Lord of the Rings #1)",
- "price": "£10.27",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Collected Poems of W.B. Yeats (The Collected Works of W.B. Yeats #1)",
- "price": "£15.42",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Barefoot Contessa Cookbook",
- "price": "£59.92",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Tell the Wolves I'm Home",
- "price": "£50.96",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Ship Leaves Harbor: Essays on Travel by a Recovering Journeyman",
- "price": "£30.60",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Pride and Prejudice",
- "price": "£19.27",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Musicophilia: Tales of Music and the Brain",
- "price": "£46.58",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "Mere Christianity",
- "price": "£48.51",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Me Before You (Me Before You #1)",
- "price": "£19.02",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "In the Woods (Dublin Murder Squad #1)",
- "price": "£38.38",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "In Cold Blood",
- "price": "£49.98",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "How to Stop Worrying and Start Living",
- "price": "£46.49",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "Give It Back",
- "price": "£18.32",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Girl, Interrupted",
- "price": "£42.14",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Fun Home: A Family Tragicomic",
- "price": "£56.59",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Fruits Basket, Vol. 6 (Fruits Basket #6)",
- "price": "£20.96",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Deception Point",
- "price": "£40.32",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Death Note, Vol. 6: Give-and-Take (Death Note #6)",
- "price": "£36.39",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "Catherine the Great: Portrait of a Woman",
- "price": "£58.55",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Better Homes and Gardens New Cook Book",
- "price": "£39.61",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "An Unquiet Mind: A Memoir of Moods and Madness",
- "price": "£21.30",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "A Year in Provence (Provence #1)",
- "price": "£56.88",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "World Without End (The Pillars of the Earth #2)",
- "price": "£32.97",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Will Grayson, Will Grayson (Will Grayson, Will Grayson)",
- "price": "£47.31",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Why Save the Bankers?: And Other Essays on Our Economic and Political Crisis",
- "price": "£48.67",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "Where She Went (If I Stay #2)",
- "price": "£41.73",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "What If?: Serious Scientific Answers to Absurd Hypothetical Questions",
- "price": "£53.68",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "Two Summers",
- "price": "£14.64",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "This Is Your Brain on Music: The Science of a Human Obsession",
- "price": "£38.40",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Secret Garden",
- "price": "£15.08",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Raven King (The Raven Cycle #4)",
- "price": "£30.57",
- "availability": "In stock",
- "rating": "2星"
- },
- {
- "title": "The Raven Boys (The Raven Cycle #1)",
- "price": "£57.74",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Power Greens Cookbook: 140 Delicious Superfood Recipes",
- "price": "£11.05",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Metamorphosis",
- "price": "£28.58",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The Mathews Men: Seven Brothers and the War Against Hitler's U-boats",
- "price": "£42.91",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Little Paris Bookshop",
- "price": "£24.73",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Hiding Place",
- "price": "£55.91",
- "availability": "In stock",
- "rating": "4星"
- },
- {
- "title": "The Grand Design",
- "price": "£13.76",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Firm",
- "price": "£45.56",
- "availability": "In stock",
- "rating": "3星"
- },
- {
- "title": "The Fault in Our Stars",
- "price": "£47.22",
- "availability": "In stock",
- "rating": "1星"
- },
- {
- "title": "The False Prince (The Ascendance Trilogy #1)",
- "price": "£56.00",
- "availability": "In stock",
- "rating": "5星"
- },
- {
- "title": "The Expatriates",
- "price": "£44.58",
- "availability": "In stock",
- "rating": "2星"
- }
-]
\ No newline at end of file
diff --git a/project/output/news_20260530_190333.json b/project/output/news_20260530_190333.json
deleted file mode 100644
index f10e931..0000000
--- a/project/output/news_20260530_190333.json
+++ /dev/null
@@ -1,82 +0,0 @@
-[
- {
- "title": "专栏",
- "publishTime": "",
- "url": "http://zhuanlan.sina.com.cn/"
- },
- {
- "title": "导航",
- "publishTime": "",
- "url": "http://news.sina.com.cn/guide/"
- },
- {
- "title": "新浪财经",
- "publishTime": "",
- "url": "https://finance.sina.com.cn/mobile/comfinanceweb.shtml"
- },
- {
- "title": "新浪博客",
- "publishTime": "",
- "url": "https://blog.sina.com.cn/lm/z/app/"
- },
- {
- "title": "我的收藏",
- "publishTime": "",
- "url": "http://my.sina.com.cn/#location=fav"
- },
- {
- "title": "注册",
- "publishTime": "",
- "url": "https://login.sina.com.cn/signup/signup?entry=news"
- },
- {
- "title": "新闻中心",
- "publishTime": "",
- "url": "http://news.sina.com.cn/"
- },
- {
- "title": "新闻排行",
- "publishTime": "",
- "url": "http://news.sina.com.cn/hotnews/"
- },
- {
- "title": "联系我们",
- "publishTime": "",
- "url": "http://www.sina.com.cn/contactus.html"
- },
- {
- "title": "广告服务",
- "publishTime": "",
- "url": "http://emarketing.sina.com.cn/"
- },
- {
- "title": "通行证注册",
- "publishTime": "",
- "url": "http://login.sina.com.cn/signup/signup"
- },
- {
- "title": "产品答疑",
- "publishTime": "",
- "url": "http://help.sina.com.cn/"
- },
- {
- "title": "招聘信息",
- "publishTime": "",
- "url": "http://career.sina.com.cn/"
- },
- {
- "title": "网站律师",
- "publishTime": "",
- "url": "http://corp.sina.com.cn/lawfirm/sina.htm"
- },
- {
- "title": "版权所有",
- "publishTime": "",
- "url": "https://corp.sina.com.cn/chn/copyright.html"
- },
- {
- "title": "意见反馈",
- "publishTime": "",
- "url": "http://news.sina.com.cn/feedback/post.html"
- }
-]
\ No newline at end of file
diff --git a/project/output/university_ranking_20260530_190333.json b/project/output/university_ranking_20260530_190333.json
deleted file mode 100644
index a6f98a4..0000000
--- a/project/output/university_ranking_20260530_190333.json
+++ /dev/null
@@ -1,212 +0,0 @@
-[
- {
- "rank": 1,
- "universityName": "清华大学 Tsinghua University 双一流/985/211",
- "totalScore": "综合",
- "province": "北京",
- "category": ""
- },
- {
- "rank": 2,
- "universityName": "北京大学 Peking University 双一流/985/211",
- "totalScore": "综合",
- "province": "北京",
- "category": ""
- },
- {
- "rank": 3,
- "universityName": "浙江大学 Zhejiang University 双一流/985/211",
- "totalScore": "综合",
- "province": "浙江",
- "category": ""
- },
- {
- "rank": 4,
- "universityName": "上海交通大学 Shanghai Jiao Tong University 双一流/985/211",
- "totalScore": "综合",
- "province": "上海",
- "category": ""
- },
- {
- "rank": 5,
- "universityName": "复旦大学 Fudan University 双一流/985/211",
- "totalScore": "综合",
- "province": "上海",
- "category": ""
- },
- {
- "rank": 6,
- "universityName": "南京大学 Nanjing University 双一流/985/211",
- "totalScore": "综合",
- "province": "江苏",
- "category": ""
- },
- {
- "rank": 7,
- "universityName": "中国科学技术大学 University of Science and Technology of China 双一流/985/211",
- "totalScore": "理工",
- "province": "安徽",
- "category": ""
- },
- {
- "rank": 8,
- "universityName": "武汉大学 Wuhan University 双一流/985/211",
- "totalScore": "综合",
- "province": "湖北",
- "category": ""
- },
- {
- "rank": 9,
- "universityName": "华中科技大学 Huazhong University of Science and Technology 双一流/985/211",
- "totalScore": "综合",
- "province": "湖北",
- "category": ""
- },
- {
- "rank": 10,
- "universityName": "西安交通大学 Xi'an Jiaotong University 双一流/985/211",
- "totalScore": "综合",
- "province": "陕西",
- "category": ""
- },
- {
- "rank": 11,
- "universityName": "北京航空航天大学 Beihang University 双一流/985/211",
- "totalScore": "理工",
- "province": "北京",
- "category": ""
- },
- {
- "rank": 12,
- "universityName": "中山大学 Sun Yat-sen University 双一流/985/211",
- "totalScore": "综合",
- "province": "广东",
- "category": ""
- },
- {
- "rank": 13,
- "universityName": "北京理工大学 Beijing Institute of Technology 双一流/985/211",
- "totalScore": "理工",
- "province": "北京",
- "category": ""
- },
- {
- "rank": 14,
- "universityName": "哈尔滨工业大学 Harbin Institute of Technology 双一流/985/211",
- "totalScore": "理工",
- "province": "黑龙江",
- "category": ""
- },
- {
- "rank": 15,
- "universityName": "四川大学 Sichuan University 双一流/985/211",
- "totalScore": "综合",
- "province": "四川",
- "category": ""
- },
- {
- "rank": 16,
- "universityName": "东南大学 Southeast University 双一流/985/211",
- "totalScore": "综合",
- "province": "江苏",
- "category": ""
- },
- {
- "rank": 17,
- "universityName": "中国人民大学 Renmin University of China 双一流/985/211",
- "totalScore": "综合",
- "province": "北京",
- "category": ""
- },
- {
- "rank": 18,
- "universityName": "同济大学 Tongji University 双一流/985/211",
- "totalScore": "综合",
- "province": "上海",
- "category": ""
- },
- {
- "rank": 19,
- "universityName": "北京师范大学 Beijing Normal University 双一流/985/211",
- "totalScore": "师范",
- "province": "北京",
- "category": ""
- },
- {
- "rank": 20,
- "universityName": "天津大学 Tianjin University 双一流/985/211",
- "totalScore": "理工",
- "province": "天津",
- "category": ""
- },
- {
- "rank": 21,
- "universityName": "西北工业大学 Northwestern Polytechnical University 双一流/985/211",
- "totalScore": "理工",
- "province": "陕西",
- "category": ""
- },
- {
- "rank": 22,
- "universityName": "山东大学 Shandong University 双一流/985/211",
- "totalScore": "综合",
- "province": "山东",
- "category": ""
- },
- {
- "rank": 23,
- "universityName": "南开大学 Nankai University 双一流/985/211",
- "totalScore": "综合",
- "province": "天津",
- "category": ""
- },
- {
- "rank": 24,
- "universityName": "厦门大学 Xiamen University 双一流/985/211",
- "totalScore": "综合",
- "province": "福建",
- "category": ""
- },
- {
- "rank": 25,
- "universityName": "中国农业大学 China Agricultural University 双一流/985/211",
- "totalScore": "农业",
- "province": "北京",
- "category": ""
- },
- {
- "rank": 26,
- "universityName": "吉林大学 Jilin University 双一流/985/211",
- "totalScore": "综合",
- "province": "吉林",
- "category": ""
- },
- {
- "rank": 27,
- "universityName": "中南大学 Central South University 双一流/985/211",
- "totalScore": "综合",
- "province": "湖南",
- "category": ""
- },
- {
- "rank": 28,
- "universityName": "大连理工大学 Dalian University of Technology 双一流/985/211",
- "totalScore": "理工",
- "province": "辽宁",
- "category": ""
- },
- {
- "rank": 29,
- "universityName": "湖南大学 Hunan University 双一流/985/211",
- "totalScore": "综合",
- "province": "湖南",
- "category": ""
- },
- {
- "rank": 30,
- "universityName": "华东师范大学 East China Normal University 双一流/985/211",
- "totalScore": "师范",
- "province": "上海",
- "category": ""
- }
-]
\ No newline at end of file
diff --git a/project/output/weather_20260530_190333.json b/project/output/weather_20260530_190333.json
deleted file mode 100644
index 537f65e..0000000
--- a/project/output/weather_20260530_190333.json
+++ /dev/null
@@ -1,335 +0,0 @@
-[
- {
- "cityName": "上海",
- "temperature": 22.7,
- "humidity": 83.0,
- "windSpeed": 7.8,
- "weatherCode": "3",
- "hourlyTimes": [
- "00:00",
- "01:00",
- "02:00",
- "03:00",
- "04:00",
- "05:00",
- "06:00",
- "07:00",
- "08:00",
- "09:00",
- "10:00",
- "11:00",
- "12:00",
- "13:00",
- "14:00",
- "15:00",
- "16:00",
- "17:00",
- "18:00",
- "19:00",
- "20:00",
- "21:00",
- "22:00",
- "23:00"
- ],
- "hourlyTemperatures": [
- 19.2,
- 19.0,
- 18.9,
- 18.3,
- 18.1,
- 17.8,
- 18.7,
- 20.9,
- 23.5,
- 24.9,
- 26.2,
- 27.0,
- 27.5,
- 28.1,
- 28.2,
- 27.4,
- 26.7,
- 25.0,
- 23.8,
- 22.7,
- 22.0,
- 20.6,
- 19.9,
- 19.4
- ],
- "hourlyHumidities": [
- 83,
- 84,
- 85,
- 87,
- 89,
- 92,
- 90,
- 79,
- 55,
- 43,
- 38,
- 34,
- 33,
- 31,
- 30,
- 32,
- 35,
- 45,
- 54,
- 63,
- 67,
- 73,
- 76,
- 78
- ],
- "hourlyWindSpeeds": [
- 3.8,
- 3.3,
- 2.6,
- 1.9,
- 1.0,
- 0.6,
- 2.3,
- 0.6,
- 1.8,
- 2.7,
- 3.0,
- 3.5,
- 5.4,
- 5.4,
- 6.0,
- 7.8,
- 9.2,
- 9.0,
- 8.1,
- 7.8,
- 7.2,
- 7.1,
- 7.1,
- 7.1
- ]
- },
- {
- "cityName": "广州",
- "temperature": 25.9,
- "humidity": 85.0,
- "windSpeed": 5.3,
- "weatherCode": "81",
- "hourlyTimes": [
- "00:00",
- "01:00",
- "02:00",
- "03:00",
- "04:00",
- "05:00",
- "06:00",
- "07:00",
- "08:00",
- "09:00",
- "10:00",
- "11:00",
- "12:00",
- "13:00",
- "14:00",
- "15:00",
- "16:00",
- "17:00",
- "18:00",
- "19:00",
- "20:00",
- "21:00",
- "22:00",
- "23:00"
- ],
- "hourlyTemperatures": [
- 27.7,
- 27.2,
- 26.0,
- 25.5,
- 25.4,
- 25.0,
- 25.0,
- 26.0,
- 28.1,
- 29.3,
- 30.6,
- 31.9,
- 33.0,
- 33.8,
- 33.9,
- 33.6,
- 34.2,
- 30.5,
- 29.4,
- 25.9,
- 26.4,
- 26.5,
- 26.3,
- 26.2
- ],
- "hourlyHumidities": [
- 85,
- 87,
- 82,
- 84,
- 85,
- 90,
- 92,
- 87,
- 76,
- 70,
- 63,
- 57,
- 54,
- 53,
- 53,
- 54,
- 51,
- 69,
- 72,
- 95,
- 97,
- 96,
- 98,
- 98
- ],
- "hourlyWindSpeeds": [
- 5.8,
- 4.9,
- 4.4,
- 3.3,
- 3.4,
- 3.8,
- 4.1,
- 5.6,
- 4.0,
- 3.8,
- 4.0,
- 2.8,
- 1.3,
- 3.3,
- 5.1,
- 5.2,
- 5.1,
- 12.3,
- 3.1,
- 5.3,
- 3.6,
- 1.7,
- 2.0,
- 1.4
- ]
- },
- {
- "cityName": "北京",
- "temperature": 32.3,
- "humidity": 56.0,
- "windSpeed": 17.1,
- "weatherCode": "0",
- "hourlyTimes": [
- "00:00",
- "01:00",
- "02:00",
- "03:00",
- "04:00",
- "05:00",
- "06:00",
- "07:00",
- "08:00",
- "09:00",
- "10:00",
- "11:00",
- "12:00",
- "13:00",
- "14:00",
- "15:00",
- "16:00",
- "17:00",
- "18:00",
- "19:00",
- "20:00",
- "21:00",
- "22:00",
- "23:00"
- ],
- "hourlyTemperatures": [
- 22.8,
- 21.9,
- 21.2,
- 20.1,
- 19.6,
- 18.8,
- 19.2,
- 20.7,
- 23.7,
- 27.0,
- 29.9,
- 32.5,
- 34.5,
- 35.8,
- 36.3,
- 36.6,
- 36.2,
- 35.7,
- 34.2,
- 32.3,
- 30.9,
- 29.9,
- 29.1,
- 28.6
- ],
- "hourlyHumidities": [
- 56,
- 60,
- 63,
- 69,
- 71,
- 75,
- 74,
- 67,
- 57,
- 45,
- 37,
- 28,
- 21,
- 18,
- 20,
- 21,
- 26,
- 26,
- 30,
- 33,
- 35,
- 36,
- 35,
- 34
- ],
- "hourlyWindSpeeds": [
- 11.6,
- 10.6,
- 7.6,
- 4.5,
- 3.9,
- 2.3,
- 2.3,
- 0.6,
- 0.8,
- 2.2,
- 2.4,
- 4.9,
- 7.6,
- 10.4,
- 12.2,
- 13.4,
- 14.7,
- 15.1,
- 14.5,
- 17.1,
- 16.9,
- 18.1,
- 19.7,
- 20.1
- ]
- }
-]
\ No newline at end of file
diff --git a/project/pom.xml b/project/pom.xml
deleted file mode 100644
index 32ea206..0000000
--- a/project/pom.xml
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
- 4.0.0
-
- com.example
- crawler-project
- 1.0.0
- crawler-project
- Java爬虫项目 - MVC + Command + Strategy模式
-
-
- 11
- 1.17.2
- 2.10.1
- 1.5.3
- 1.4.14
- 11
- 11
- UTF-8
-
-
-
-
-
- org.jsoup
- jsoup
- ${jsoup.version}
-
-
-
-
- com.google.code.gson
- gson
- ${gson.version}
-
-
-
-
- org.jfree
- jfreechart
- ${jfreechart.version}
-
-
-
-
- ch.qos.logback
- logback-classic
- ${logback.version}
-
-
-
-
- junit
- junit
- 4.13.2
- test
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.11.0
-
- ${java.version}
- ${java.version}
- ${project.build.sourceEncoding}
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.5.0
-
-
- package
-
- shade
-
-
-
-
- com.example.crawler.Main
-
-
-
-
-
-
-
-
-
diff --git a/project/reports/book_analysis_report.txt b/project/reports/book_analysis_report.txt
deleted file mode 100644
index 943ced3..0000000
--- a/project/reports/book_analysis_report.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-========== 书籍数据分析报告 ==========
-生成时间: 2026-05-30T17:47:42.026682900
-分析书籍总数: 600
-
-【价格统计】
-最高价: £59.92
-最低价: £10.01
-平均价: £35.29
-
-【库存统计】
-有库存: 600 本
-缺货: 0 本
-
-报告生成完成
diff --git a/project/reports/news_analysis_report.txt b/project/reports/news_analysis_report.txt
deleted file mode 100644
index d1794d6..0000000
--- a/project/reports/news_analysis_report.txt
+++ /dev/null
@@ -1,31 +0,0 @@
-========== 新闻数据分析报告 ==========
-生成时间: 2026-05-30T17:47:42.145591
-分析新闻总数: 16
-
-【发布时间分布】
- 00:00 - 01:00: 0 条
- 01:00 - 02:00: 0 条
- 02:00 - 03:00: 0 条
- 03:00 - 04:00: 0 条
- 04:00 - 05:00: 0 条
- 05:00 - 06:00: 0 条
- 06:00 - 07:00: 0 条
- 07:00 - 08:00: 0 条
- 08:00 - 09:00: 0 条
- 09:00 - 10:00: 0 条
- 10:00 - 11:00: 0 条
- 11:00 - 12:00: 0 条
- 12:00 - 13:00: 0 条
- 13:00 - 14:00: 0 条
- 14:00 - 15:00: 0 条
- 15:00 - 16:00: 0 条
- 16:00 - 17:00: 0 条
- 17:00 - 18:00: 16 条
- 18:00 - 19:00: 0 条
- 19:00 - 20:00: 0 条
- 20:00 - 21:00: 0 条
- 21:00 - 22:00: 0 条
- 22:00 - 23:00: 0 条
- 23:00 - 00:00: 0 条
-
-报告生成完成
diff --git a/project/reports/ranking_analysis_report.txt b/project/reports/ranking_analysis_report.txt
deleted file mode 100644
index 7636b13..0000000
--- a/project/reports/ranking_analysis_report.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-========== 大学排名数据分析报告 ==========
-生成时间: 2026-05-30T17:47:42.272388
-分析大学总数: 30
-
-【省份排行榜 TOP 10】
- 北京: 7 所大学
- 上海: 4 所大学
- 湖北: 2 所大学
- 湖南: 2 所大学
- 天津: 2 所大学
- 陕西: 2 所大学
- 江苏: 2 所大学
- 山东: 1 所大学
- 福建: 1 所大学
- 吉林: 1 所大学
-
-报告生成完成
diff --git a/project/reports/weather_analysis_report.txt b/project/reports/weather_analysis_report.txt
deleted file mode 100644
index 247e060..0000000
--- a/project/reports/weather_analysis_report.txt
+++ /dev/null
@@ -1,29 +0,0 @@
-========== 天气数据分析报告 ==========
-生成时间: 2026-05-30T17:47:42.585539200
-分析城市数量: 3
-数据来源: Open-Meteo API (CC BY 4.0)
-
-【多城市天气对比】
-
-城市: 上海
- 当前温度: 24.0°C
- 当前湿度: 83%
- 风速: 8.3 km/h
- 天气: 多云
- 24小时平均温度: 22.7°C
-
-城市: 广州
- 当前温度: 29.8°C
- 当前湿度: 85%
- 风速: 2.4 km/h
- 天气: 小毛毛雨
- 24小时平均温度: 28.6°C
-
-城市: 北京
- 当前温度: 34.6°C
- 当前湿度: 56%
- 风速: 14.4 km/h
- 天气: 晴
- 24小时平均温度: 28.2°C
-
-报告生成完成
diff --git a/project/src/main/java/com/example/crawler/Main.java b/project/src/main/java/com/example/crawler/Main.java
deleted file mode 100644
index 09b9ddd..0000000
--- a/project/src/main/java/com/example/crawler/Main.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.example.crawler;
-
-import com.example.crawler.controller.CrawlerController;
-
-/**
- * 爬虫项目主入口类
- */
-public class Main {
-
- public static void main(String[] args) {
- // 创建控制器并启动CLI界面
- CrawlerController controller = new CrawlerController();
- controller.start();
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/chart/ChartGenerator.java b/project/src/main/java/com/example/crawler/chart/ChartGenerator.java
deleted file mode 100644
index 3985f59..0000000
--- a/project/src/main/java/com/example/crawler/chart/ChartGenerator.java
+++ /dev/null
@@ -1,229 +0,0 @@
-package com.example.crawler.chart;
-
-import java.awt.Color;
-import java.awt.Font;
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import com.example.crawler.constant.CrawlerConstants;
-import org.jfree.chart.ChartFactory;
-import org.jfree.chart.ChartUtils;
-import org.jfree.chart.JFreeChart;
-import org.jfree.chart.axis.CategoryAxis;
-import org.jfree.chart.axis.NumberAxis;
-import org.jfree.chart.plot.CategoryPlot;
-import org.jfree.chart.plot.PiePlot;
-import org.jfree.chart.plot.XYPlot;
-import org.jfree.chart.renderer.category.BarRenderer;
-import org.jfree.chart.renderer.category.LineAndShapeRenderer;
-import org.jfree.data.category.DefaultCategoryDataset;
-import org.jfree.data.general.DefaultPieDataset;
-import org.jfree.data.xy.XYDataset;
-import org.jfree.data.xy.XYSeries;
-import org.jfree.data.xy.XYSeriesCollection;
-
-public class ChartGenerator {
-
- static {
- File dir = new File(CrawlerConstants.CHARTS_DIR);
- if (!dir.exists()) {
- dir.mkdirs();
- }
- }
-
- public static void generatePriceHistogram(Map priceDistribution, String fileName) {
- DefaultCategoryDataset dataset = createCategoryDataset(priceDistribution);
- JFreeChart chart = ChartFactory.createBarChart(
- "书籍价格分布",
- "价格区间(£)",
- "书籍数量",
- dataset
- );
- customizeBarChart(chart);
- saveChart(chart, fileName);
- }
-
- public static void generateRatingPieChart(Map ratingDistribution, String fileName) {
- DefaultPieDataset dataset = new DefaultPieDataset<>();
- for (Map.Entry entry : ratingDistribution.entrySet()) {
- dataset.setValue(entry.getKey(), entry.getValue());
- }
- JFreeChart chart = ChartFactory.createPieChart(
- "书籍评分分布",
- dataset,
- true,
- true,
- false
- );
- customizePieChart(chart);
- saveChart(chart, fileName);
- }
-
- public static void generateNewsTimeTrend(Map hourDistribution, String fileName) {
- DefaultCategoryDataset dataset = new DefaultCategoryDataset();
- for (int i = 0; i < 24; i++) {
- int count = hourDistribution.getOrDefault(i, 0);
- dataset.addValue(count, "新闻数量", String.format("%02d:00", i));
- }
- JFreeChart chart = ChartFactory.createLineChart(
- "新闻发布时间分布",
- "小时",
- "新闻数量",
- dataset
- );
- customizeLineChart(chart);
- saveChart(chart, fileName);
- }
-
- public static void generateWordFrequencyBarChart(Map wordFrequency, String fileName) {
- Map top10 = wordFrequency.entrySet().stream()
- .sorted(Map.Entry.comparingByValue().reversed())
- .limit(10)
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- DefaultCategoryDataset dataset = new DefaultCategoryDataset();
- for (Map.Entry entry : top10.entrySet()) {
- dataset.addValue(entry.getValue(), "词频", entry.getKey());
- }
- JFreeChart chart = ChartFactory.createBarChart(
- "新闻高频词 TOP 10",
- "关键词",
- "出现次数",
- dataset
- );
- customizeBarChart(chart);
- saveChart(chart, fileName);
- }
-
- public static void generateProvinceBarChart(Map provinceDistribution, String fileName) {
- Map top10 = provinceDistribution.entrySet().stream()
- .sorted(Map.Entry.comparingByValue().reversed())
- .limit(10)
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- DefaultCategoryDataset dataset = createCategoryDataset(top10);
- JFreeChart chart = ChartFactory.createBarChart(
- "各省上榜大学数量 TOP 10",
- "省份",
- "大学数量",
- dataset
- );
- customizeBarChart(chart);
- saveChart(chart, fileName);
- }
-
- public static void generateScoreHistogram(Map scoreDistribution, String fileName) {
- DefaultCategoryDataset dataset = createCategoryDataset(scoreDistribution);
- JFreeChart chart = ChartFactory.createBarChart(
- "大学总分分布",
- "分数区间",
- "大学数量",
- dataset
- );
- customizeBarChart(chart);
- saveChart(chart, fileName);
- }
-
- public static void generateTemperatureTrend(List times, List temperatures, String cityName, String fileName) {
- XYSeries series = new XYSeries(cityName);
- for (int i = 0; i < Math.min(times.size(), temperatures.size()); i++) {
- series.add(i, temperatures.get(i));
- }
- XYDataset dataset = new XYSeriesCollection(series);
- JFreeChart chart = ChartFactory.createXYLineChart(
- cityName + " 未来24小时温度变化",
- "小时",
- "温度(°C)",
- dataset
- );
- customizeXYLineChart(chart);
- saveChart(chart, fileName);
- }
-
- public static void generateMultiCityTemperatureComparison(Map> cityTemperatures, String fileName) {
- XYSeriesCollection dataset = new XYSeriesCollection();
- for (Map.Entry> entry : cityTemperatures.entrySet()) {
- XYSeries series = new XYSeries(entry.getKey());
- List temps = entry.getValue();
- for (int i = 0; i < Math.min(temps.size(), 24); i++) {
- series.add(i, temps.get(i));
- }
- dataset.addSeries(series);
- }
- JFreeChart chart = ChartFactory.createXYLineChart(
- "多城市未来24小时温度对比",
- "小时",
- "温度(°C)",
- dataset
- );
- customizeXYLineChart(chart);
- saveChart(chart, fileName);
- }
-
- private static DefaultCategoryDataset createCategoryDataset(Map data) {
- DefaultCategoryDataset dataset = new DefaultCategoryDataset();
- for (Map.Entry entry : data.entrySet()) {
- dataset.addValue(entry.getValue(), "数值", entry.getKey());
- }
- return dataset;
- }
-
- private static void customizeBarChart(JFreeChart chart) {
- chart.getTitle().setFont(new Font("Microsoft YaHei", Font.BOLD, 16));
- chart.getLegend().setItemFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
-
- CategoryPlot plot = chart.getCategoryPlot();
- CategoryAxis domainAxis = plot.getDomainAxis();
- domainAxis.setLabelFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
- domainAxis.setTickLabelFont(new Font("Microsoft YaHei", Font.PLAIN, 10));
-
- NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
- rangeAxis.setLabelFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
-
- BarRenderer renderer = (BarRenderer) plot.getRenderer();
- renderer.setSeriesPaint(0, new Color(79, 129, 189));
- }
-
- private static void customizePieChart(JFreeChart chart) {
- chart.getTitle().setFont(new Font("Microsoft YaHei", Font.BOLD, 16));
- chart.getLegend().setItemFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
-
- PiePlot plot = (PiePlot) chart.getPlot();
- plot.setLabelFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
- }
-
- private static void customizeLineChart(JFreeChart chart) {
- chart.getTitle().setFont(new Font("Microsoft YaHei", Font.BOLD, 16));
- chart.getLegend().setItemFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
-
- CategoryPlot plot = chart.getCategoryPlot();
- LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
- renderer.setSeriesPaint(0, new Color(79, 129, 189));
- }
-
- private static void customizeXYLineChart(JFreeChart chart) {
- chart.getTitle().setFont(new Font("Microsoft YaHei", Font.BOLD, 16));
- chart.getLegend().setItemFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
-
- XYPlot plot = chart.getXYPlot();
-
- NumberAxis xAxis = (NumberAxis) plot.getDomainAxis();
- xAxis.setLabelFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
-
- NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
- yAxis.setLabelFont(new Font("Microsoft YaHei", Font.PLAIN, 12));
- }
-
- private static void saveChart(JFreeChart chart, String fileName) {
- try {
- File file = new File(CrawlerConstants.CHARTS_DIR, fileName);
- ChartUtils.saveChartAsPNG(file, chart, 800, 500);
- System.out.println("图表已保存: " + file.getAbsolutePath());
- } catch (IOException e) {
- System.err.println("保存图表失败: " + e.getMessage());
- }
- }
-}
diff --git a/project/src/main/java/com/example/crawler/command/BaseCrawlCommand.java b/project/src/main/java/com/example/crawler/command/BaseCrawlCommand.java
deleted file mode 100644
index 743d5b7..0000000
--- a/project/src/main/java/com/example/crawler/command/BaseCrawlCommand.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.example.crawler.command;
-
-import com.example.crawler.constant.CrawlerConstants;
-import com.example.crawler.exception.CrawlException;
-import com.example.crawler.exception.NetworkException;
-import com.example.crawler.repository.DataRepository;
-import com.example.crawler.strategy.CrawlStrategy;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public abstract class BaseCrawlCommand implements Command {
-
- protected static final Logger logger = LoggerFactory.getLogger(BaseCrawlCommand.class);
-
- protected DataRepository repository;
- protected int maxRetries;
- protected long retryDelayMs;
-
- public BaseCrawlCommand(DataRepository repository) {
- this.repository = repository;
- this.maxRetries = CrawlerConstants.MAX_RETRIES;
- this.retryDelayMs = 2000;
- }
-
- protected abstract CrawlStrategy> getStrategy();
-
- protected abstract void saveToRepository(Object data);
-
- @Override
- public void execute() {
- try {
- Object data = crawlWithRetry();
- saveToRepository(data);
- logger.info("Crawling completed and saved to repository");
- } catch (Exception e) {
- logger.error("Crawling failed", e);
- System.err.println("爬取失败: " + e.getMessage());
- }
- }
-
- protected Object crawlWithRetry() throws Exception {
- int attempts = 0;
- while (attempts < maxRetries) {
- try {
- CrawlStrategy> strategy = getStrategy();
- return strategy.crawl();
- } catch (NetworkException e) {
- attempts++;
- if (attempts < maxRetries) {
- logger.warn("Network error, retrying in {}ms (attempt {}/{})", retryDelayMs, attempts, maxRetries);
- Thread.sleep(retryDelayMs);
- } else {
- logger.error("Max retries reached, giving up");
- throw e;
- }
- }
- }
- throw new CrawlException("Max retries exceeded");
- }
-}
diff --git a/project/src/main/java/com/example/crawler/command/BookCommand.java b/project/src/main/java/com/example/crawler/command/BookCommand.java
deleted file mode 100644
index 5d6df8a..0000000
--- a/project/src/main/java/com/example/crawler/command/BookCommand.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.example.crawler.command;
-
-import com.example.crawler.model.Book;
-import com.example.crawler.repository.DataRepository;
-import com.example.crawler.strategy.BookCrawlStrategy;
-import com.example.crawler.strategy.CrawlStrategy;
-
-import java.util.List;
-
-public class BookCommand extends BaseCrawlCommand {
-
- public BookCommand(DataRepository repository) {
- super(repository);
- }
-
- @Override
- protected CrawlStrategy> getStrategy() {
- return new BookCrawlStrategy();
- }
-
- @Override
- @SuppressWarnings("unchecked")
- protected void saveToRepository(Object data) {
- repository.saveBooks((List) data);
- System.out.println("成功爬取 " + ((List) data).size() + " 本书籍信息");
- }
-
- @Override
- public String getName() {
- return "爬取书籍信息";
- }
-}
diff --git a/project/src/main/java/com/example/crawler/command/Command.java b/project/src/main/java/com/example/crawler/command/Command.java
deleted file mode 100644
index 4c804a7..0000000
--- a/project/src/main/java/com/example/crawler/command/Command.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.example.crawler.command;
-
-/**
- * 命令接口
- * 定义命令执行的标准方法,实现Command模式
- */
-public interface Command {
-
- /**
- * 执行命令
- */
- void execute();
-
- /**
- * 获取命令名称
- *
- * @return 命令名称
- */
- String getName();
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/command/CrawlAllCommand.java b/project/src/main/java/com/example/crawler/command/CrawlAllCommand.java
deleted file mode 100644
index d54f6b8..0000000
--- a/project/src/main/java/com/example/crawler/command/CrawlAllCommand.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.example.crawler.command;
-
-import com.example.crawler.controller.CrawlerController;
-import com.example.crawler.repository.DataRepository;
-
-public class CrawlAllCommand implements Command {
-
- private final DataRepository repository;
- private final CrawlerController controller;
-
- public CrawlAllCommand(CrawlerController controller) {
- this.controller = controller;
- this.repository = controller.getRepository();
- }
-
- @Override
- public void execute() {
- System.out.println("\n=== 开始爬取全部数据源 ===");
-
- Command[] commands = {
- new BookCommand(repository),
- new NewsCommand(repository),
- new CrawlRankingCommand(repository),
- new WeatherCommand(repository)
- };
-
- for (Command command : commands) {
- command.execute();
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
-
- new SaveCommand(controller).execute();
-
- System.out.println("\n=== 全部数据爬取完成 ===");
- }
-
- @Override
- public String getName() {
- return "爬取全部数据并保存";
- }
-}
diff --git a/project/src/main/java/com/example/crawler/command/CrawlAndAnalyzeAllCommand.java b/project/src/main/java/com/example/crawler/command/CrawlAndAnalyzeAllCommand.java
deleted file mode 100644
index 0174398..0000000
--- a/project/src/main/java/com/example/crawler/command/CrawlAndAnalyzeAllCommand.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package com.example.crawler.command;
-
-import com.example.crawler.controller.CrawlerController;
-import com.example.crawler.repository.DataRepository;
-import com.example.crawler.service.BookAnalysisService;
-import com.example.crawler.service.NewsAnalysisService;
-import com.example.crawler.service.RankingAnalysisService;
-import com.example.crawler.service.WeatherAnalysisService;
-
-public class CrawlAndAnalyzeAllCommand implements Command {
-
- private final DataRepository repository;
- private final CrawlerController controller;
-
- public CrawlAndAnalyzeAllCommand(CrawlerController controller) {
- this.controller = controller;
- this.repository = controller.getRepository();
- }
-
- @Override
- public void execute() {
- System.out.println("\n========== 爬取全部数据并生成分析 ==========\n");
-
- System.out.println("第1步:爬取书籍信息...");
- try {
- BookCommand bookCommand = new BookCommand(repository);
- bookCommand.execute();
- } catch (Exception e) {
- System.err.println("书籍爬取失败: " + e.getMessage());
- }
-
- System.out.println("\n第2步:爬取新闻信息...");
- try {
- NewsCommand newsCommand = new NewsCommand(repository);
- newsCommand.execute();
- } catch (Exception e) {
- System.err.println("新闻爬取失败: " + e.getMessage());
- }
-
- System.out.println("\n第3步:爬取大学排名...");
- try {
- CrawlRankingCommand rankingCommand = new CrawlRankingCommand(repository);
- rankingCommand.execute();
- } catch (Exception e) {
- System.err.println("大学排名爬取失败: " + e.getMessage());
- }
-
- System.out.println("\n第4步:爬取天气数据...");
- try {
- WeatherCommand weatherCommand = new WeatherCommand(repository);
- weatherCommand.execute();
- } catch (Exception e) {
- System.err.println("天气数据爬取失败: " + e.getMessage());
- }
-
- System.out.println("\n========== 数据爬取完成,开始分析 ==========\n");
-
- try {
- BookAnalysisService bookService = new BookAnalysisService();
- if (!repository.getBooks().isEmpty()) {
- bookService.analyze(repository.getBooks());
- }
- } catch (Exception e) {
- System.err.println("书籍分析失败: " + e.getMessage());
- }
-
- try {
- NewsAnalysisService newsService = new NewsAnalysisService();
- if (!repository.getNewsList().isEmpty()) {
- newsService.analyze(repository.getNewsList());
- }
- } catch (Exception e) {
- System.err.println("新闻分析失败: " + e.getMessage());
- }
-
- try {
- RankingAnalysisService rankingService = new RankingAnalysisService();
- if (!repository.getRankings().isEmpty()) {
- rankingService.analyze(repository.getRankings());
- }
- } catch (Exception e) {
- System.err.println("大学排名分析失败: " + e.getMessage());
- }
-
- try {
- WeatherAnalysisService weatherService = new WeatherAnalysisService();
- if (!repository.getWeatherList().isEmpty()) {
- weatherService.analyze(repository.getWeatherList());
- }
- } catch (Exception e) {
- System.err.println("天气分析失败: " + e.getMessage());
- }
-
- System.out.println("\n========== 全部完成 ==========");
- System.out.println("原始数据已保存到 output/ 目录");
- System.out.println("分析报告已保存到 reports/ 目录");
- System.out.println("图表已保存到 charts/ 目录");
- }
-
- @Override
- public String getName() {
- return "爬取并分析全部数据";
- }
-}
diff --git a/project/src/main/java/com/example/crawler/command/CrawlRankingCommand.java b/project/src/main/java/com/example/crawler/command/CrawlRankingCommand.java
deleted file mode 100644
index f9cfc73..0000000
--- a/project/src/main/java/com/example/crawler/command/CrawlRankingCommand.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.example.crawler.command;
-
-import com.example.crawler.model.UniversityRank;
-import com.example.crawler.repository.DataRepository;
-import com.example.crawler.strategy.CrawlStrategy;
-import com.example.crawler.strategy.UniversityRankCrawlStrategy;
-
-import java.util.List;
-
-public class CrawlRankingCommand extends BaseCrawlCommand {
-
- public CrawlRankingCommand(DataRepository repository) {
- super(repository);
- }
-
- @Override
- protected CrawlStrategy> getStrategy() {
- return new UniversityRankCrawlStrategy();
- }
-
- @Override
- @SuppressWarnings("unchecked")
- protected void saveToRepository(Object data) {
- repository.saveRankings((List) data);
- System.out.println("成功爬取 " + ((List) data).size() + " 条大学排名数据");
- }
-
- @Override
- public String getName() {
- return "爬取软科中国大学排名";
- }
-}
diff --git a/project/src/main/java/com/example/crawler/command/ExitCommand.java b/project/src/main/java/com/example/crawler/command/ExitCommand.java
deleted file mode 100644
index 8085f2b..0000000
--- a/project/src/main/java/com/example/crawler/command/ExitCommand.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.example.crawler.command;
-
-/**
- * 退出命令
- * // Command模式:退出命令
- */
-public class ExitCommand implements Command {
-
- @Override
- public void execute() {
- System.out.println("\n=== 感谢使用数据爬取系统 ===");
- System.exit(0);
- }
-
- @Override
- public String getName() {
- return "退出";
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/command/GenerateAllAnalysisCommand.java b/project/src/main/java/com/example/crawler/command/GenerateAllAnalysisCommand.java
deleted file mode 100644
index 0ed7164..0000000
--- a/project/src/main/java/com/example/crawler/command/GenerateAllAnalysisCommand.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.example.crawler.command;
-
-import com.example.crawler.controller.CrawlerController;
-import com.example.crawler.repository.DataRepository;
-import com.example.crawler.service.BookAnalysisService;
-import com.example.crawler.service.NewsAnalysisService;
-import com.example.crawler.service.RankingAnalysisService;
-import com.example.crawler.service.WeatherAnalysisService;
-
-public class GenerateAllAnalysisCommand implements Command {
-
- private final DataRepository repository;
- private final CrawlerController controller;
-
- public GenerateAllAnalysisCommand(CrawlerController controller) {
- this.controller = controller;
- this.repository = controller.getRepository();
- }
-
- @Override
- public void execute() {
- System.out.println("\n========== 生成所有数据源分析报告 ==========\n");
-
- try {
- BookAnalysisService bookService = new BookAnalysisService();
- if (!repository.getBooks().isEmpty()) {
- bookService.analyze(repository.getBooks());
- } else {
- System.out.println("没有书籍数据,跳过书籍分析");
- }
- } catch (Exception e) {
- System.err.println("书籍分析失败: " + e.getMessage());
- }
-
- try {
- NewsAnalysisService newsService = new NewsAnalysisService();
- if (!repository.getNewsList().isEmpty()) {
- newsService.analyze(repository.getNewsList());
- } else {
- System.out.println("没有新闻数据,跳过新闻分析");
- }
- } catch (Exception e) {
- System.err.println("新闻分析失败: " + e.getMessage());
- }
-
- try {
- RankingAnalysisService rankingService = new RankingAnalysisService();
- if (!repository.getRankings().isEmpty()) {
- rankingService.analyze(repository.getRankings());
- } else {
- System.out.println("没有大学排名数据,跳过排名分析");
- }
- } catch (Exception e) {
- System.err.println("大学排名分析失败: " + e.getMessage());
- }
-
- try {
- WeatherAnalysisService weatherService = new WeatherAnalysisService();
- if (!repository.getWeatherList().isEmpty()) {
- weatherService.analyze(repository.getWeatherList());
- } else {
- System.out.println("没有天气数据,跳过天气分析");
- }
- } catch (Exception e) {
- System.err.println("天气分析失败: " + e.getMessage());
- }
-
- System.out.println("\n========== 分析完成 ==========");
- System.out.println("报告已保存到 reports/ 目录");
- System.out.println("图表已保存到 charts/ 目录");
- }
-
- @Override
- public String getName() {
- return "生成所有分析报告";
- }
-}
diff --git a/project/src/main/java/com/example/crawler/command/NewsCommand.java b/project/src/main/java/com/example/crawler/command/NewsCommand.java
deleted file mode 100644
index 1250d1a..0000000
--- a/project/src/main/java/com/example/crawler/command/NewsCommand.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.example.crawler.command;
-
-import com.example.crawler.model.News;
-import com.example.crawler.repository.DataRepository;
-import com.example.crawler.strategy.CrawlStrategy;
-import com.example.crawler.strategy.NewsCrawlStrategy;
-
-import java.util.List;
-
-public class NewsCommand extends BaseCrawlCommand {
-
- public NewsCommand(DataRepository repository) {
- super(repository);
- }
-
- @Override
- protected CrawlStrategy> getStrategy() {
- return new NewsCrawlStrategy();
- }
-
- @Override
- @SuppressWarnings("unchecked")
- protected void saveToRepository(Object data) {
- repository.saveNewsList((List) data);
- System.out.println("成功爬取 " + ((List) data).size() + " 条新闻");
- }
-
- @Override
- public String getName() {
- return "爬取新浪国内新闻";
- }
-}
diff --git a/project/src/main/java/com/example/crawler/command/SaveCommand.java b/project/src/main/java/com/example/crawler/command/SaveCommand.java
deleted file mode 100644
index f6e606a..0000000
--- a/project/src/main/java/com/example/crawler/command/SaveCommand.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.example.crawler.command;
-
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.List;
-
-import com.example.crawler.constant.CrawlerConstants;
-import com.example.crawler.controller.CrawlerController;
-import com.example.crawler.model.Book;
-import com.example.crawler.model.News;
-import com.example.crawler.model.UniversityRank;
-import com.example.crawler.model.Weather;
-import com.example.crawler.util.JsonUtil;
-
-public class SaveCommand implements Command {
-
- private final CrawlerController controller;
-
- public SaveCommand(CrawlerController controller) {
- this.controller = controller;
- }
-
- @Override
- public void execute() {
- System.out.println("\n=== 开始保存数据 ===");
-
- try {
- String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
-
- // 保存书籍数据
- List books = controller.getBooks();
- if (books != null && !books.isEmpty()) {
- String bookFileName = CrawlerConstants.OUTPUT_DIR + "/books_" + timestamp + ".json";
- JsonUtil.saveListToJsonFile(books, bookFileName);
- System.out.println("书籍数据已保存到: " + bookFileName);
- }
-
- // 保存新闻数据
- List newsList = controller.getNewsList();
- if (newsList != null && !newsList.isEmpty()) {
- String newsFileName = CrawlerConstants.OUTPUT_DIR + "/news_" + timestamp + ".json";
- JsonUtil.saveListToJsonFile(newsList, newsFileName);
- System.out.println("新闻数据已保存到: " + newsFileName);
- }
-
- // 保存大学排名数据
- List universityRankList = controller.getUniversityRankList();
- if (universityRankList != null && !universityRankList.isEmpty()) {
- String rankingFileName = CrawlerConstants.OUTPUT_DIR + "/university_ranking_" + timestamp + ".json";
- JsonUtil.saveListToJsonFile(universityRankList, rankingFileName);
- System.out.println("大学排名数据已保存到: " + rankingFileName);
- }
-
- // 保存天气数据
- List weatherList = controller.getWeatherList();
- if (weatherList != null && !weatherList.isEmpty()) {
- String weatherFileName = CrawlerConstants.OUTPUT_DIR + "/weather_" + timestamp + ".json";
- JsonUtil.saveListToJsonFile(weatherList, weatherFileName);
- System.out.println("天气数据已保存到: " + weatherFileName);
- }
-
- System.out.println("\n=== 数据保存完成 ===");
-
- } catch (Exception e) {
- System.err.println("保存数据失败: " + e.getMessage());
- e.printStackTrace();
- }
- }
-
- @Override
- public String getName() {
- return "保存当前数据到文件";
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/command/WeatherCommand.java b/project/src/main/java/com/example/crawler/command/WeatherCommand.java
deleted file mode 100644
index 7a3f7a6..0000000
--- a/project/src/main/java/com/example/crawler/command/WeatherCommand.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.example.crawler.command;
-
-import com.example.crawler.model.Weather;
-import com.example.crawler.repository.DataRepository;
-import com.example.crawler.strategy.CrawlStrategy;
-import com.example.crawler.strategy.WeatherCrawlStrategy;
-
-import java.util.List;
-
-public class WeatherCommand extends BaseCrawlCommand {
-
- public WeatherCommand(DataRepository repository) {
- super(repository);
- }
-
- @Override
- protected CrawlStrategy> getStrategy() {
- return new WeatherCrawlStrategy();
- }
-
- @Override
- @SuppressWarnings("unchecked")
- protected void saveToRepository(Object data) {
- repository.saveWeatherList((List) data);
- System.out.println("成功爬取 " + ((List) data).size() + " 个城市的天气信息");
- }
-
- @Override
- public String getName() {
- return "爬取天气数据";
- }
-}
diff --git a/project/src/main/java/com/example/crawler/constant/CrawlerConstants.java b/project/src/main/java/com/example/crawler/constant/CrawlerConstants.java
deleted file mode 100644
index 841ce75..0000000
--- a/project/src/main/java/com/example/crawler/constant/CrawlerConstants.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.example.crawler.constant;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class CrawlerConstants {
-
- public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
- public static final String REFERER = "https://www.baidu.com";
-
- public static final int TIMEOUT_MS = 10000;
- public static final int MAX_RETRIES = 3;
- public static final long DELAY_MS = 3000;
-
- public static final String URL_BOOKS = "https://books.toscrape.com/";
- public static final String URL_NEWS = "https://news.sina.com.cn/china/";
- public static final String URL_RANKING = "https://www.shanghairanking.cn/rankings/bcur/202310";
- public static final String URL_WEATHER_API = "https://api.open-meteo.com/v1/forecast";
-
- public static final String OUTPUT_DIR = "output";
- public static final String REPORTS_DIR = "reports";
- public static final String CHARTS_DIR = "charts";
-
- public static final Map CITY_COORDINATES;
- static {
- CITY_COORDINATES = new HashMap<>();
- CITY_COORDINATES.put("北京", new double[]{39.9042, 116.4074});
- CITY_COORDINATES.put("上海", new double[]{31.2304, 121.4737});
- CITY_COORDINATES.put("广州", new double[]{23.1291, 113.2644});
- }
-}
diff --git a/project/src/main/java/com/example/crawler/controller/CrawlerController.java b/project/src/main/java/com/example/crawler/controller/CrawlerController.java
deleted file mode 100644
index cedc475..0000000
--- a/project/src/main/java/com/example/crawler/controller/CrawlerController.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package com.example.crawler.controller;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Scanner;
-
-import com.example.crawler.command.BookCommand;
-import com.example.crawler.command.Command;
-import com.example.crawler.command.CrawlAllCommand;
-import com.example.crawler.command.CrawlAndAnalyzeAllCommand;
-import com.example.crawler.command.CrawlRankingCommand;
-import com.example.crawler.command.ExitCommand;
-import com.example.crawler.command.GenerateAllAnalysisCommand;
-import com.example.crawler.command.NewsCommand;
-import com.example.crawler.command.SaveCommand;
-import com.example.crawler.command.WeatherCommand;
-import com.example.crawler.model.Book;
-import com.example.crawler.model.News;
-import com.example.crawler.model.UniversityRank;
-import com.example.crawler.model.Weather;
-import com.example.crawler.repository.DataRepository;
-import com.example.crawler.view.CrawlerView;
-
-public class CrawlerController {
-
- private final CrawlerView view;
- private final Map commandMap;
- private final DataRepository repository;
-
- public CrawlerController() {
- this.view = new CrawlerView();
- this.repository = DataRepository.getInstance();
- this.commandMap = new HashMap<>();
- initCommands();
- }
-
- private void initCommands() {
- commandMap.put(1, new BookCommand(repository));
- commandMap.put(2, new NewsCommand(repository));
- commandMap.put(3, new CrawlRankingCommand(repository));
- commandMap.put(4, new WeatherCommand(repository));
- commandMap.put(5, new CrawlAllCommand(this));
- commandMap.put(6, new SaveCommand(this));
- commandMap.put(7, new GenerateAllAnalysisCommand(this));
- commandMap.put(8, new CrawlAndAnalyzeAllCommand(this));
- commandMap.put(9, new ExitCommand());
- }
-
- public void start() {
- Scanner scanner = new Scanner(System.in);
-
- while (true) {
- view.showMenu();
-
- int choice = view.getInput(scanner);
-
- Command command = commandMap.get(choice);
- if (command != null) {
- command.execute();
- } else {
- view.showError("无效的选择,请输入1-9之间的数字");
- }
-
- if (choice != 9) {
- view.pause(scanner);
- }
- }
- }
-
- public List getBooks() {
- return repository.getBooks();
- }
-
- public List getNewsList() {
- return repository.getNewsList();
- }
-
- public List getUniversityRankList() {
- return repository.getRankings();
- }
-
- public List getWeatherList() {
- return repository.getWeatherList();
- }
-
- public DataRepository getRepository() {
- return repository;
- }
-}
diff --git a/project/src/main/java/com/example/crawler/exception/CrawlException.java b/project/src/main/java/com/example/crawler/exception/CrawlException.java
deleted file mode 100644
index dad0237..0000000
--- a/project/src/main/java/com/example/crawler/exception/CrawlException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.example.crawler.exception;
-
-/**
- * 爬虫异常基类
- * 所有爬虫相关异常都继承此类
- */
-public class CrawlException extends Exception {
-
- public CrawlException(String message) {
- super(message);
- }
-
- public CrawlException(String message, Throwable cause) {
- super(message, cause);
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/exception/DataSaveException.java b/project/src/main/java/com/example/crawler/exception/DataSaveException.java
deleted file mode 100644
index 6d26120..0000000
--- a/project/src/main/java/com/example/crawler/exception/DataSaveException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.example.crawler.exception;
-
-/**
- * 数据保存异常
- * 用于处理文件写入失败、JSON序列化失败等数据保存相关错误
- */
-public class DataSaveException extends CrawlException {
-
- public DataSaveException(String message) {
- super(message);
- }
-
- public DataSaveException(String message, Throwable cause) {
- super(message, cause);
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/exception/NetworkException.java b/project/src/main/java/com/example/crawler/exception/NetworkException.java
deleted file mode 100644
index ecc898e..0000000
--- a/project/src/main/java/com/example/crawler/exception/NetworkException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.example.crawler.exception;
-
-/**
- * 网络异常
- * 用于处理HTTP请求失败、连接超时等网络相关错误
- */
-public class NetworkException extends CrawlException {
-
- public NetworkException(String message) {
- super(message);
- }
-
- public NetworkException(String message, Throwable cause) {
- super(message, cause);
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/exception/ParseException.java b/project/src/main/java/com/example/crawler/exception/ParseException.java
deleted file mode 100644
index ecae303..0000000
--- a/project/src/main/java/com/example/crawler/exception/ParseException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.example.crawler.exception;
-
-/**
- * 解析异常
- * 用于处理HTML解析失败、JSON解析失败等数据解析相关错误
- */
-public class ParseException extends CrawlException {
-
- public ParseException(String message) {
- super(message);
- }
-
- public ParseException(String message, Throwable cause) {
- super(message, cause);
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/model/Book.java b/project/src/main/java/com/example/crawler/model/Book.java
deleted file mode 100644
index c58fe68..0000000
--- a/project/src/main/java/com/example/crawler/model/Book.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.example.crawler.model;
-
-/**
- * 书籍数据模型
- * 存储toscrape.com网站的书籍信息
- */
-public class Book {
-
- private String title;
- private String price;
- private String availability;
- private String rating;
-
- public Book() {
- }
-
- public Book(String title, String price, String availability, String rating) {
- this.title = title;
- this.price = price;
- this.availability = availability;
- this.rating = rating;
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public String getPrice() {
- return price;
- }
-
- public void setPrice(String price) {
- this.price = price;
- }
-
- public String getAvailability() {
- return availability;
- }
-
- public void setAvailability(String availability) {
- this.availability = availability;
- }
-
- public String getRating() {
- return rating;
- }
-
- public void setRating(String rating) {
- this.rating = rating;
- }
-
- @Override
- public String toString() {
- return "Book{" +
- "title='" + title + '\'' +
- ", price='" + price + '\'' +
- ", availability='" + availability + '\'' +
- ", rating='" + rating + '\'' +
- '}';
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/model/News.java b/project/src/main/java/com/example/crawler/model/News.java
deleted file mode 100644
index d5a5c21..0000000
--- a/project/src/main/java/com/example/crawler/model/News.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.example.crawler.model;
-
-/**
- * 新闻数据模型
- * 存储新浪新闻的国内新闻信息
- */
-public class News {
-
- private String title;
- private String publishTime;
- private String url;
-
- public News() {
- }
-
- public News(String title, String publishTime, String url) {
- this.title = title;
- this.publishTime = publishTime;
- this.url = url;
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public String getPublishTime() {
- return publishTime;
- }
-
- public void setPublishTime(String publishTime) {
- this.publishTime = publishTime;
- }
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
-
- @Override
- public String toString() {
- return "News{" +
- "title='" + title + '\'' +
- ", publishTime='" + publishTime + '\'' +
- ", url='" + url + '\'' +
- '}';
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/model/UniversityRank.java b/project/src/main/java/com/example/crawler/model/UniversityRank.java
deleted file mode 100644
index bde80a7..0000000
--- a/project/src/main/java/com/example/crawler/model/UniversityRank.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package com.example.crawler.model;
-
-/**
- * 大学排名数据模型
- * 存储软科中国大学排名信息
- */
-public class UniversityRank {
-
- private Integer rank;
- private String universityName;
- private String totalScore;
- private String province;
- private String category;
-
- public UniversityRank() {
- }
-
- public UniversityRank(Integer rank, String universityName, String totalScore, String province, String category) {
- this.rank = rank;
- this.universityName = universityName;
- this.totalScore = totalScore;
- this.province = province;
- this.category = category;
- }
-
- public Integer getRank() {
- return rank;
- }
-
- public void setRank(Integer rank) {
- this.rank = rank;
- }
-
- public String getUniversityName() {
- return universityName;
- }
-
- public void setUniversityName(String universityName) {
- this.universityName = universityName;
- }
-
- public String getTotalScore() {
- return totalScore;
- }
-
- public void setTotalScore(String totalScore) {
- this.totalScore = totalScore;
- }
-
- public String getProvince() {
- return province;
- }
-
- public void setProvince(String province) {
- this.province = province;
- }
-
- public String getCategory() {
- return category;
- }
-
- public void setCategory(String category) {
- this.category = category;
- }
-
- @Override
- public String toString() {
- return "UniversityRank{" +
- "rank=" + rank +
- ", universityName='" + universityName + '\'' +
- ", totalScore='" + totalScore + '\'' +
- ", province='" + province + '\'' +
- ", category='" + category + '\'' +
- '}';
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/model/Weather.java b/project/src/main/java/com/example/crawler/model/Weather.java
deleted file mode 100644
index dbc6ccc..0000000
--- a/project/src/main/java/com/example/crawler/model/Weather.java
+++ /dev/null
@@ -1,140 +0,0 @@
-package com.example.crawler.model;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * 天气数据模型
- * 存储 Open-Meteo API 的城市天气信息
- * 数据来源:Open-Meteo (CC BY 4.0)
- */
-public class Weather {
-
- private String cityName;
- private double temperature;
- private double humidity;
- private double windSpeed;
- private String weatherCode;
- private List hourlyTimes;
- private List hourlyTemperatures;
- private List hourlyHumidities;
- private List hourlyWindSpeeds;
-
- public Weather() {
- this.hourlyTimes = new ArrayList<>();
- this.hourlyTemperatures = new ArrayList<>();
- this.hourlyHumidities = new ArrayList<>();
- this.hourlyWindSpeeds = new ArrayList<>();
- }
-
- public Weather(String cityName, double temperature, double humidity, double windSpeed, String weatherCode) {
- this.cityName = cityName;
- this.temperature = temperature;
- this.humidity = humidity;
- this.windSpeed = windSpeed;
- this.weatherCode = weatherCode;
- this.hourlyTimes = new ArrayList<>();
- this.hourlyTemperatures = new ArrayList<>();
- this.hourlyHumidities = new ArrayList<>();
- this.hourlyWindSpeeds = new ArrayList<>();
- }
-
- public String getCityName() {
- return cityName;
- }
-
- public void setCityName(String cityName) {
- this.cityName = cityName;
- }
-
- public double getTemperature() {
- return temperature;
- }
-
- public void setTemperature(double temperature) {
- this.temperature = temperature;
- }
-
- public double getHumidity() {
- return humidity;
- }
-
- public void setHumidity(double humidity) {
- this.humidity = humidity;
- }
-
- public double getWindSpeed() {
- return windSpeed;
- }
-
- public void setWindSpeed(double windSpeed) {
- this.windSpeed = windSpeed;
- }
-
- public String getWeatherCode() {
- return weatherCode;
- }
-
- public void setWeatherCode(String weatherCode) {
- this.weatherCode = weatherCode;
- }
-
- public List getHourlyTimes() {
- return hourlyTimes;
- }
-
- public void setHourlyTimes(List hourlyTimes) {
- this.hourlyTimes = hourlyTimes;
- }
-
- public List getHourlyTemperatures() {
- return hourlyTemperatures;
- }
-
- public void setHourlyTemperatures(List hourlyTemperatures) {
- this.hourlyTemperatures = hourlyTemperatures;
- }
-
- public List getHourlyHumidities() {
- return hourlyHumidities;
- }
-
- public void setHourlyHumidities(List hourlyHumidities) {
- this.hourlyHumidities = hourlyHumidities;
- }
-
- public List getHourlyWindSpeeds() {
- return hourlyWindSpeeds;
- }
-
- public void setHourlyWindSpeeds(List hourlyWindSpeeds) {
- this.hourlyWindSpeeds = hourlyWindSpeeds;
- }
-
- public String getWeatherDescription() {
- if (weatherCode == null) return "未知";
- switch (weatherCode) {
- case "0": return "晴";
- case "1": case "2": case "3": return "多云";
- case "45": case "48": return "雾";
- case "51": case "53": case "55": return "小毛毛雨";
- case "61": case "63": case "65": return "小雨";
- case "80": case "81": case "82": return "阵雨";
- case "95": return "雷暴";
- case "96": case "99": return "雷暴加冰雹";
- default: return "未知";
- }
- }
-
- @Override
- public String toString() {
- return "Weather{" +
- "cityName='" + cityName + '\'' +
- ", temperature=" + temperature +
- ", humidity=" + humidity +
- ", windSpeed=" + windSpeed +
- ", weatherCode='" + weatherCode + '\'' +
- ", weather='" + getWeatherDescription() + '\'' +
- '}';
- }
-}
diff --git a/project/src/main/java/com/example/crawler/repository/DataRepository.java b/project/src/main/java/com/example/crawler/repository/DataRepository.java
deleted file mode 100644
index e76e5bc..0000000
--- a/project/src/main/java/com/example/crawler/repository/DataRepository.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.example.crawler.repository;
-
-import com.example.crawler.model.Book;
-import com.example.crawler.model.News;
-import com.example.crawler.model.UniversityRank;
-import com.example.crawler.model.Weather;
-import java.util.ArrayList;
-import java.util.List;
-
-public class DataRepository {
-
- private static DataRepository instance;
-
- private List books;
- private List newsList;
- private List rankings;
- private List weatherList;
-
- private DataRepository() {
- this.books = new ArrayList<>();
- this.newsList = new ArrayList<>();
- this.rankings = new ArrayList<>();
- this.weatherList = new ArrayList<>();
- }
-
- public static synchronized DataRepository getInstance() {
- if (instance == null) {
- instance = new DataRepository();
- }
- return instance;
- }
-
- public List getBooks() {
- return new ArrayList<>(books);
- }
-
- public void saveBooks(List books) {
- this.books.clear();
- this.books.addAll(books);
- }
-
- public List getNewsList() {
- return new ArrayList<>(newsList);
- }
-
- public void saveNewsList(List newsList) {
- this.newsList.clear();
- this.newsList.addAll(newsList);
- }
-
- public List getRankings() {
- return new ArrayList<>(rankings);
- }
-
- public void saveRankings(List rankings) {
- this.rankings.clear();
- this.rankings.addAll(rankings);
- }
-
- public List getWeatherList() {
- return new ArrayList<>(weatherList);
- }
-
- public void saveWeatherList(List weatherList) {
- this.weatherList.clear();
- this.weatherList.addAll(weatherList);
- }
-
- public void clearAll() {
- books.clear();
- newsList.clear();
- rankings.clear();
- weatherList.clear();
- }
-}
diff --git a/project/src/main/java/com/example/crawler/service/BookAnalysisService.java b/project/src/main/java/com/example/crawler/service/BookAnalysisService.java
deleted file mode 100644
index e1aebaf..0000000
--- a/project/src/main/java/com/example/crawler/service/BookAnalysisService.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package com.example.crawler.service;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import com.example.crawler.chart.ChartGenerator;
-import com.example.crawler.constant.CrawlerConstants;
-import com.example.crawler.model.Book;
-import com.example.crawler.util.DataCleaner;
-
-public class BookAnalysisService {
-
- static {
- File dir = new File(CrawlerConstants.REPORTS_DIR);
- if (!dir.exists()) {
- dir.mkdirs();
- }
- }
-
- public void analyze(List books) {
- if (books == null || books.isEmpty()) {
- System.out.println("没有书籍数据可分析");
- return;
- }
-
- System.out.println("\n========== 书籍数据分析 ==========");
- System.out.println("共分析 " + books.size() + " 本书\n");
-
- analyzePriceDistribution(books);
- analyzeRatingDistribution(books);
- analyzeStockStatus(books);
-
- generateReport(books);
- }
-
- private void analyzePriceDistribution(List books) {
- System.out.println("【价格分析】");
- List prices = new ArrayList<>();
- for (Book book : books) {
- double price = DataCleaner.cleanPrice(book.getPrice());
- if (price > 0) {
- prices.add(price);
- }
- }
-
- if (prices.isEmpty()) {
- System.out.println("无法获取有效价格数据");
- return;
- }
-
- double maxPrice = prices.stream().mapToDouble(Double::doubleValue).max().orElse(0);
- double minPrice = prices.stream().mapToDouble(Double::doubleValue).min().orElse(0);
- double avgPrice = prices.stream().mapToDouble(Double::doubleValue).average().orElse(0);
-
- System.out.println("最高价: £" + String.format("%.2f", maxPrice));
- System.out.println("最低价: £" + String.format("%.2f", minPrice));
- System.out.println("平均价: £" + String.format("%.2f", avgPrice));
-
- Map priceRanges = new HashMap<>();
- String[] ranges = {"0-10", "10-20", "20-30", "30-40", "40-50", "50+"};
- for (String range : ranges) {
- priceRanges.put(range, 0);
- }
-
- for (Double price : prices) {
- if (price < 10) priceRanges.put("0-10", priceRanges.get("0-10") + 1);
- else if (price < 20) priceRanges.put("10-20", priceRanges.get("10-20") + 1);
- else if (price < 30) priceRanges.put("20-30", priceRanges.get("20-30") + 1);
- else if (price < 40) priceRanges.put("30-40", priceRanges.get("30-40") + 1);
- else if (price < 50) priceRanges.put("40-50", priceRanges.get("40-50") + 1);
- else priceRanges.put("50+", priceRanges.get("50+") + 1);
- }
-
- System.out.println("\n价格区间分布:");
- for (Map.Entry entry : priceRanges.entrySet()) {
- System.out.println(" " + entry.getKey() + ": " + entry.getValue() + " 本");
- }
-
- ChartGenerator.generatePriceHistogram(priceRanges, "price_histogram.png");
- }
-
- private void analyzeRatingDistribution(List books) {
- System.out.println("\n【评分分析】");
- Map ratingCounts = new HashMap<>();
- ratingCounts.put("5星", 0);
- ratingCounts.put("4星", 0);
- ratingCounts.put("3星", 0);
- ratingCounts.put("2星", 0);
- ratingCounts.put("1星", 0);
- ratingCounts.put("未知", 0);
-
- for (Book book : books) {
- int rating = DataCleaner.cleanRating(book.getRating());
- switch (rating) {
- case 5: ratingCounts.put("5星", ratingCounts.get("5星") + 1); break;
- case 4: ratingCounts.put("4星", ratingCounts.get("4星") + 1); break;
- case 3: ratingCounts.put("3星", ratingCounts.get("3星") + 1); break;
- case 2: ratingCounts.put("2星", ratingCounts.get("2星") + 1); break;
- case 1: ratingCounts.put("1星", ratingCounts.get("1星") + 1); break;
- default: ratingCounts.put("未知", ratingCounts.get("未知") + 1);
- }
- }
-
- int total = books.size();
- System.out.println("评分分布:");
- for (Map.Entry entry : ratingCounts.entrySet()) {
- double percentage = (entry.getValue() * 100.0) / total;
- System.out.println(" " + entry.getKey() + ": " + entry.getValue() + " 本 (" + String.format("%.1f", percentage) + "%)");
- }
-
- ChartGenerator.generateRatingPieChart(ratingCounts, "rating_pie.png");
- }
-
- private void analyzeStockStatus(List books) {
- System.out.println("\n【库存分析】");
- int inStock = 0;
- int outOfStock = 0;
-
- for (Book book : books) {
- String availability = book.getAvailability();
- if (availability != null && availability.toLowerCase().contains("in stock")) {
- inStock++;
- } else {
- outOfStock++;
- }
- }
-
- System.out.println("有库存: " + inStock + " 本");
- System.out.println("缺货: " + outOfStock + " 本");
- }
-
- private void generateReport(List books) {
- String fileName = CrawlerConstants.REPORTS_DIR + "/book_analysis_report.txt";
- try (PrintWriter writer = new PrintWriter(new FileWriter(fileName))) {
- writer.println("========== 书籍数据分析报告 ==========");
- writer.println("生成时间: " + java.time.LocalDateTime.now());
- writer.println("分析书籍总数: " + books.size());
- writer.println();
-
- List prices = books.stream()
- .map(b -> DataCleaner.cleanPrice(b.getPrice()))
- .filter(p -> p > 0)
- .collect(Collectors.toList());
-
- if (!prices.isEmpty()) {
- writer.println("【价格统计】");
- writer.println("最高价: £" + String.format("%.2f", prices.stream().mapToDouble(Double::doubleValue).max().orElse(0)));
- writer.println("最低价: £" + String.format("%.2f", prices.stream().mapToDouble(Double::doubleValue).min().orElse(0)));
- writer.println("平均价: £" + String.format("%.2f", prices.stream().mapToDouble(Double::doubleValue).average().orElse(0)));
- writer.println();
- }
-
- writer.println("【库存统计】");
- long inStock = books.stream().filter(b -> b.getAvailability() != null && b.getAvailability().toLowerCase().contains("in stock")).count();
- writer.println("有库存: " + inStock + " 本");
- writer.println("缺货: " + (books.size() - inStock) + " 本");
-
- writer.println("\n报告生成完成");
- System.out.println("\n报告已保存: " + fileName);
- } catch (IOException e) {
- System.err.println("生成报告失败: " + e.getMessage());
- }
- }
-}
diff --git a/project/src/main/java/com/example/crawler/service/NewsAnalysisService.java b/project/src/main/java/com/example/crawler/service/NewsAnalysisService.java
deleted file mode 100644
index 13b68cf..0000000
--- a/project/src/main/java/com/example/crawler/service/NewsAnalysisService.java
+++ /dev/null
@@ -1,138 +0,0 @@
-package com.example.crawler.service;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import com.example.crawler.chart.ChartGenerator;
-import com.example.crawler.constant.CrawlerConstants;
-import com.example.crawler.model.News;
-import com.example.crawler.util.DataCleaner;
-
-public class NewsAnalysisService {
-
- static {
- File dir = new File(CrawlerConstants.REPORTS_DIR);
- if (!dir.exists()) {
- dir.mkdirs();
- }
- }
-
- public void analyze(List newsList) {
- if (newsList == null || newsList.isEmpty()) {
- System.out.println("没有新闻数据可分析");
- return;
- }
-
- System.out.println("\n========== 新闻数据分析 ==========");
- System.out.println("共分析 " + newsList.size() + " 条新闻\n");
-
- analyzeTimeDistribution(newsList);
- analyzeKeywords(newsList);
-
- generateReport(newsList);
- }
-
- private void analyzeTimeDistribution(List newsList) {
- System.out.println("【发布时间分布】");
- Map hourDistribution = new HashMap<>();
- for (int i = 0; i < 24; i++) {
- hourDistribution.put(i, 0);
- }
-
- for (News news : newsList) {
- try {
- java.time.LocalDateTime dateTime = DataCleaner.cleanNewsTime(news.getPublishTime());
- int hour = DataCleaner.extractHour(dateTime);
- hourDistribution.put(hour, hourDistribution.get(hour) + 1);
- } catch (Exception e) {
- // 忽略解析失败的数据
- }
- }
-
- System.out.println("\n按小时统计:");
- for (int i = 0; i < 24; i++) {
- int count = hourDistribution.get(i);
- String bar = "*".repeat(Math.max(1, count));
- System.out.printf(" %02d:00 - %02d:00: %3d %s%n", i, (i + 1) % 24, count, bar);
- }
-
- int peakHour = 0;
- int peakCount = 0;
- for (Map.Entry entry : hourDistribution.entrySet()) {
- if (entry.getValue() > peakCount) {
- peakCount = entry.getValue();
- peakHour = entry.getKey();
- }
- }
- System.out.println("\n高峰时段: " + String.format("%02d:00", peakHour) + " (发布 " + peakCount + " 条新闻)");
-
- ChartGenerator.generateNewsTimeTrend(hourDistribution, "news_time_trend.png");
- }
-
- private void analyzeKeywords(List newsList) {
- System.out.println("\n【关键词分析】");
- Map allWords = new HashMap<>();
-
- for (News news : newsList) {
- String title = DataCleaner.cleanTitle(news.getTitle());
- String[] words = DataCleaner.extractWords(title);
- Map wordFreq = DataCleaner.countWordFrequency(words);
- for (Map.Entry entry : wordFreq.entrySet()) {
- allWords.put(entry.getKey(), allWords.getOrDefault(entry.getKey(), 0) + entry.getValue());
- }
- }
-
- List> sortedWords = allWords.entrySet().stream()
- .sorted(Map.Entry.comparingByValue().reversed())
- .limit(20)
- .collect(Collectors.toList());
-
- System.out.println("\n高频词 TOP 10:");
- for (int i = 0; i < Math.min(10, sortedWords.size()); i++) {
- Map.Entry entry = sortedWords.get(i);
- System.out.printf(" %2d. %s: %d%n", i + 1, entry.getKey(), entry.getValue());
- }
-
- Map top10 = sortedWords.stream()
- .limit(10)
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- ChartGenerator.generateWordFrequencyBarChart(top10, "news_top_words.png");
- }
-
- private void generateReport(List newsList) {
- String fileName = CrawlerConstants.REPORTS_DIR + "/news_analysis_report.txt";
- try (PrintWriter writer = new PrintWriter(new FileWriter(fileName))) {
- writer.println("========== 新闻数据分析报告 ==========");
- writer.println("生成时间: " + java.time.LocalDateTime.now());
- writer.println("分析新闻总数: " + newsList.size());
- writer.println();
-
- Map hourDistribution = new HashMap<>();
- for (int i = 0; i < 24; i++) hourDistribution.put(i, 0);
- for (News news : newsList) {
- try {
- int hour = DataCleaner.extractHour(DataCleaner.cleanNewsTime(news.getPublishTime()));
- hourDistribution.put(hour, hourDistribution.get(hour) + 1);
- } catch (Exception e) {}
- }
-
- writer.println("【发布时间分布】");
- for (int i = 0; i < 24; i++) {
- writer.println(String.format(" %02d:00 - %02d:00: %d 条", i, (i + 1) % 24, hourDistribution.get(i)));
- }
-
- writer.println("\n报告生成完成");
- System.out.println("\n报告已保存: " + fileName);
- } catch (IOException e) {
- System.err.println("生成报告失败: " + e.getMessage());
- }
- }
-}
diff --git a/project/src/main/java/com/example/crawler/service/RankingAnalysisService.java b/project/src/main/java/com/example/crawler/service/RankingAnalysisService.java
deleted file mode 100644
index 1259e40..0000000
--- a/project/src/main/java/com/example/crawler/service/RankingAnalysisService.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package com.example.crawler.service;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import com.example.crawler.chart.ChartGenerator;
-import com.example.crawler.constant.CrawlerConstants;
-import com.example.crawler.model.UniversityRank;
-import com.example.crawler.util.DataCleaner;
-
-public class RankingAnalysisService {
-
- static {
- File dir = new File(CrawlerConstants.REPORTS_DIR);
- if (!dir.exists()) {
- dir.mkdirs();
- }
- }
-
- public void analyze(List ranks) {
- if (ranks == null || ranks.isEmpty()) {
- System.out.println("没有大学排名数据可分析");
- return;
- }
-
- System.out.println("\n========== 大学排名数据分析 ==========");
- System.out.println("共分析 " + ranks.size() + " 所大学\n");
-
- analyzeProvinceDistribution(ranks);
- analyzeScoreDistribution(ranks);
- analyzeCategoryDistribution(ranks);
-
- generateReport(ranks);
- }
-
- private void analyzeProvinceDistribution(List ranks) {
- System.out.println("【各省份上榜大学数量】");
- Map provinceCounts = new HashMap<>();
-
- for (UniversityRank rank : ranks) {
- String province = rank.getProvince();
- if (province != null && !province.isEmpty()) {
- provinceCounts.put(province, provinceCounts.getOrDefault(province, 0) + 1);
- }
- }
-
- List> sorted = provinceCounts.entrySet().stream()
- .sorted(Map.Entry.comparingByValue().reversed())
- .collect(Collectors.toList());
-
- System.out.println("\n省份排行榜 TOP 10:");
- int rankNum = 1;
- for (Map.Entry entry : sorted) {
- if (rankNum > 10) break;
- System.out.printf(" %2d. %s: %d 所大学%n", rankNum++, entry.getKey(), entry.getValue());
- }
-
- Map top10 = sorted.stream()
- .limit(10)
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- ChartGenerator.generateProvinceBarChart(top10, "province_bar.png");
- }
-
- private void analyzeScoreDistribution(List ranks) {
- System.out.println("\n【总分分析】");
- List scores = new ArrayList<>();
-
- for (UniversityRank rank : ranks) {
- double score = DataCleaner.cleanScore(rank.getTotalScore());
- if (score > 0) {
- scores.add(score);
- }
- }
-
- if (scores.isEmpty()) {
- System.out.println("无法获取有效分数数据");
- return;
- }
-
- double maxScore = scores.stream().mapToDouble(Double::doubleValue).max().orElse(0);
- double minScore = scores.stream().mapToDouble(Double::doubleValue).min().orElse(0);
- double avgScore = scores.stream().mapToDouble(Double::doubleValue).average().orElse(0);
-
- List sortedScores = scores.stream().sorted().collect(Collectors.toList());
- double median = sortedScores.get(sortedScores.size() / 2);
-
- System.out.println("最高分: " + String.format("%.2f", maxScore));
- System.out.println("最低分: " + String.format("%.2f", minScore));
- System.out.println("平均分: " + String.format("%.2f", avgScore));
- System.out.println("中位数: " + String.format("%.2f", median));
-
- Map scoreRanges = new HashMap<>();
- String[] ranges = {"0-20", "20-40", "40-60", "60-80", "80-100"};
- for (String range : ranges) {
- scoreRanges.put(range, 0);
- }
-
- for (Double score : scores) {
- if (score < 20) scoreRanges.put("0-20", scoreRanges.get("0-20") + 1);
- else if (score < 40) scoreRanges.put("20-40", scoreRanges.get("20-40") + 1);
- else if (score < 60) scoreRanges.put("40-60", scoreRanges.get("40-60") + 1);
- else if (score < 80) scoreRanges.put("60-80", scoreRanges.get("60-80") + 1);
- else scoreRanges.put("80-100", scoreRanges.get("80-100") + 1);
- }
-
- System.out.println("\n分数区间分布:");
- for (Map.Entry entry : scoreRanges.entrySet()) {
- System.out.println(" " + entry.getKey() + ": " + entry.getValue() + " 所");
- }
-
- ChartGenerator.generateScoreHistogram(scoreRanges, "score_boxplot.png");
- }
-
- private void analyzeCategoryDistribution(List ranks) {
- System.out.println("\n【办学层次统计】");
- Map categoryCounts = new HashMap<>();
-
- for (UniversityRank rank : ranks) {
- String category = rank.getCategory();
- if (category != null && !category.isEmpty()) {
- categoryCounts.put(category, categoryCounts.getOrDefault(category, 0) + 1);
- }
- }
-
- if (categoryCounts.isEmpty()) {
- System.out.println("没有办学层次数据");
- return;
- }
-
- List> sorted = categoryCounts.entrySet().stream()
- .sorted(Map.Entry.comparingByValue().reversed())
- .collect(Collectors.toList());
-
- System.out.println("\n办学层次分布:");
- for (Map.Entry entry : sorted) {
- System.out.printf(" %s: %d 所%n", entry.getKey(), entry.getValue());
- }
- }
-
- private void generateReport(List ranks) {
- String fileName = CrawlerConstants.REPORTS_DIR + "/ranking_analysis_report.txt";
- try (PrintWriter writer = new PrintWriter(new FileWriter(fileName))) {
- writer.println("========== 大学排名数据分析报告 ==========");
- writer.println("生成时间: " + java.time.LocalDateTime.now());
- writer.println("分析大学总数: " + ranks.size());
- writer.println();
-
- Map provinceCounts = new HashMap<>();
- for (UniversityRank rank : ranks) {
- String province = rank.getProvince();
- if (province != null && !province.isEmpty()) {
- provinceCounts.put(province, provinceCounts.getOrDefault(province, 0) + 1);
- }
- }
-
- writer.println("【省份排行榜 TOP 10】");
- provinceCounts.entrySet().stream()
- .sorted(Map.Entry.comparingByValue().reversed())
- .limit(10)
- .forEach(e -> writer.println(" " + e.getKey() + ": " + e.getValue() + " 所大学"));
-
- List scores = ranks.stream()
- .map(r -> DataCleaner.cleanScore(r.getTotalScore()))
- .filter(s -> s > 0)
- .collect(Collectors.toList());
-
- if (!scores.isEmpty()) {
- writer.println();
- writer.println("【分数统计】");
- writer.println("最高分: " + String.format("%.2f", scores.stream().mapToDouble(Double::doubleValue).max().orElse(0)));
- writer.println("最低分: " + String.format("%.2f", scores.stream().mapToDouble(Double::doubleValue).min().orElse(0)));
- writer.println("平均分: " + String.format("%.2f", scores.stream().mapToDouble(Double::doubleValue).average().orElse(0)));
- }
-
- writer.println("\n报告生成完成");
- System.out.println("\n报告已保存: " + fileName);
- } catch (IOException e) {
- System.err.println("生成报告失败: " + e.getMessage());
- }
- }
-}
diff --git a/project/src/main/java/com/example/crawler/service/WeatherAnalysisService.java b/project/src/main/java/com/example/crawler/service/WeatherAnalysisService.java
deleted file mode 100644
index e91a0a8..0000000
--- a/project/src/main/java/com/example/crawler/service/WeatherAnalysisService.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package com.example.crawler.service;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import com.example.crawler.chart.ChartGenerator;
-import com.example.crawler.constant.CrawlerConstants;
-import com.example.crawler.model.Weather;
-
-public class WeatherAnalysisService {
-
- static {
- File dir = new File(CrawlerConstants.REPORTS_DIR);
- if (!dir.exists()) {
- dir.mkdirs();
- }
- }
-
- public void analyze(List weatherList) {
- if (weatherList == null || weatherList.isEmpty()) {
- System.out.println("没有天气数据可分析");
- return;
- }
-
- System.out.println("\n========== 天气数据分析 ==========");
- System.out.println("共分析 " + weatherList.size() + " 个城市\n");
-
- analyzeCurrentWeather(weatherList);
- analyzeTemperatureTrend(weatherList);
- analyzeHumidityTrend(weatherList);
- analyzeComfortIndex(weatherList);
-
- generateReport(weatherList);
- }
-
- private void analyzeCurrentWeather(List weatherList) {
- System.out.println("【当前天气对比】");
- System.out.println("┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐");
- System.out.println("│ 城市名称 │ 温度(°C)│ 湿度(%) │ 风速(km/h)│ 天气状况 │ 舒适度 │");
- System.out.println("├──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤");
-
- for (Weather weather : weatherList) {
- double comfort = calculateComfortIndex(weather.getTemperature(), weather.getHumidity());
- String comfortDesc = getComfortDescription(comfort);
- System.out.printf("│ %-8s │ %8.1f │ %8.0f │ %8.1f │ %-8s │ %-8s │%n",
- weather.getCityName(),
- weather.getTemperature(),
- weather.getHumidity(),
- weather.getWindSpeed(),
- weather.getWeatherDescription(),
- comfortDesc);
- }
- System.out.println("└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘");
- }
-
- private void analyzeTemperatureTrend(List weatherList) {
- System.out.println("\n【未来24小时温度分析】");
-
- Map> cityTemperatures = new HashMap<>();
- for (Weather weather : weatherList) {
- cityTemperatures.put(weather.getCityName(), weather.getHourlyTemperatures());
-
- List temps = weather.getHourlyTemperatures();
- if (!temps.isEmpty()) {
- double maxTemp = temps.stream().mapToDouble(Double::doubleValue).max().orElse(0);
- double minTemp = temps.stream().mapToDouble(Double::doubleValue).min().orElse(0);
- double avgTemp = temps.stream().mapToDouble(Double::doubleValue).average().orElse(0);
-
- int maxIndex = temps.indexOf(maxTemp);
- int minIndex = temps.indexOf(minTemp);
-
- String maxTime = maxIndex < weather.getHourlyTimes().size() ? weather.getHourlyTimes().get(maxIndex) : "";
- String minTime = minIndex < weather.getHourlyTimes().size() ? weather.getHourlyTimes().get(minIndex) : "";
-
- System.out.printf(" %s: 最高 %.1f°C(%s) 最低 %.1f°C(%s) 平均 %.1f°C%n",
- weather.getCityName(), maxTemp, maxTime, minTemp, minTime, avgTemp);
- }
-
- ChartGenerator.generateTemperatureTrend(
- weather.getHourlyTimes(),
- weather.getHourlyTemperatures(),
- weather.getCityName(),
- "temperature_" + weather.getCityName() + ".png"
- );
- }
-
- ChartGenerator.generateMultiCityTemperatureComparison(cityTemperatures, "temperature_comparison.png");
- }
-
- private void analyzeHumidityTrend(List weatherList) {
- System.out.println("\n【未来24小时湿度分析】");
- for (Weather weather : weatherList) {
- List humidities = weather.getHourlyHumidities();
- if (!humidities.isEmpty()) {
- double avgHumidity = humidities.stream().mapToInt(Integer::intValue).average().orElse(0);
- System.out.printf(" %s: 平均湿度 %.0f%%%n", weather.getCityName(), avgHumidity);
- }
- }
- }
-
- private void analyzeComfortIndex(List weatherList) {
- System.out.println("\n【舒适度指数分析】");
- System.out.println("(基于温度和湿度的体感舒适度计算,0-100分制)");
-
- for (Weather weather : weatherList) {
- double comfort = calculateComfortIndex(weather.getTemperature(), weather.getHumidity());
- String description = getComfortDescription(comfort);
- System.out.printf(" %s: %.1f分 (%s)%n", weather.getCityName(), comfort, description);
- }
- }
-
- private double calculateComfortIndex(double temperature, double humidity) {
- double tempDiff = Math.abs(temperature - 22);
- double humDiff = Math.abs(humidity - 50);
-
- double comfort = 100 - (tempDiff * 3 + humDiff * 0.5);
- return Math.max(0, Math.min(100, comfort));
- }
-
- private String getComfortDescription(double comfort) {
- if (comfort >= 80) return "非常舒适";
- if (comfort >= 60) return "舒适";
- if (comfort >= 40) return "一般";
- if (comfort >= 20) return "不舒适";
- return "极不舒适";
- }
-
- private void generateReport(List weatherList) {
- String fileName = CrawlerConstants.REPORTS_DIR + "/weather_analysis_report.txt";
- try (PrintWriter writer = new PrintWriter(new FileWriter(fileName))) {
- writer.println("========== 天气数据分析报告 ==========");
- writer.println("生成时间: " + java.time.LocalDateTime.now());
- writer.println("分析城市数量: " + weatherList.size());
- writer.println("数据来源: Open-Meteo API (CC BY 4.0)");
- writer.println();
-
- writer.println("【多城市天气对比】");
- for (Weather weather : weatherList) {
- writer.println("\n城市: " + weather.getCityName());
- writer.println(" 当前温度: " + String.format("%.1f°C", weather.getTemperature()));
- writer.println(" 当前湿度: " + String.format("%.0f%%", weather.getHumidity()));
- writer.println(" 风速: " + String.format("%.1f km/h", weather.getWindSpeed()));
- writer.println(" 天气: " + weather.getWeatherDescription());
-
- List temps = weather.getHourlyTemperatures();
- if (!temps.isEmpty()) {
- writer.println(" 24小时平均温度: " + String.format("%.1f°C", temps.stream().mapToDouble(Double::doubleValue).average().orElse(0)));
- }
- }
-
- writer.println("\n报告生成完成");
- System.out.println("\n报告已保存: " + fileName);
- } catch (IOException e) {
- System.err.println("生成报告失败: " + e.getMessage());
- }
- }
-}
diff --git a/project/src/main/java/com/example/crawler/strategy/BookCrawlStrategy.java b/project/src/main/java/com/example/crawler/strategy/BookCrawlStrategy.java
deleted file mode 100644
index 746b57e..0000000
--- a/project/src/main/java/com/example/crawler/strategy/BookCrawlStrategy.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package com.example.crawler.strategy;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.select.Elements;
-
-import com.example.crawler.exception.CrawlException;
-import com.example.crawler.exception.NetworkException;
-import com.example.crawler.exception.ParseException;
-import com.example.crawler.model.Book;
-import com.example.crawler.util.HttpUtil;
-
-/**
- * 书籍爬取策略
- * // 策略模式:书籍信息爬取策略
- */
-public class BookCrawlStrategy implements CrawlStrategy {
-
- private static final String BASE_URL = "https://books.toscrape.com/";
- private static final String PAGE_URL_FORMAT = "https://books.toscrape.com/catalogue/page-%d.html";
- private static final int MAX_PAGES = 30; // 最大爬取页数
-
- @Override
- public List crawl() throws CrawlException {
- List books = new ArrayList<>();
- int pageNum = 1;
-
- try {
- while (true) {
- // 达到最大页数限制时停止
- if (pageNum > MAX_PAGES) {
- System.out.println("已达到最大爬取页数限制(" + MAX_PAGES + "页),停止爬取");
- break;
- }
-
- String url = pageNum == 1 ? BASE_URL : String.format(PAGE_URL_FORMAT, pageNum);
-
- // 设置请求头
- Map headers = Map.of(
- "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- "Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
- );
-
- String html = HttpUtil.get(url, headers);
- Document doc = Jsoup.parse(html);
-
- Elements bookElements = doc.select(".product_pod");
-
- // 如果没有书籍元素,说明已到达最后一页
- if (bookElements.isEmpty()) {
- System.out.println("第 " + pageNum + " 页没有书籍数据,停止爬取");
- break;
- }
-
- for (Element bookElement : bookElements) {
- Book book = parseBook(bookElement);
- books.add(book);
- }
-
- System.out.println("已爬取第 " + pageNum + " 页,共 " + books.size() + " 本书");
-
- // 设置请求间隔
- HttpUtil.sleep(1);
-
- pageNum++;
- }
-
- return books;
- } catch (NetworkException e) {
- // 如果是404错误且已经爬取了一些数据,返回已获取的数据
- if (e.getMessage().contains("404") && !books.isEmpty()) {
- System.out.println("第 " + pageNum + " 页不存在(404),返回已爬取的 " + books.size() + " 本书");
- return books;
- }
- throw new NetworkException("爬取书籍信息时网络异常: " + e.getMessage(), e);
- } catch (ParseException e) {
- throw new ParseException("解析书籍信息时异常: " + e.getMessage(), e);
- } catch (Exception e) {
- throw new CrawlException("爬取书籍信息时发生未知异常: " + e.getMessage(), e);
- }
- }
-
- /**
- * 解析书籍元素
- */
- private Book parseBook(Element bookElement) throws ParseException {
- try {
- // 获取书名
- Element titleElement = bookElement.selectFirst("h3 a");
- String title = titleElement != null ? titleElement.attr("title") : "未知书名";
-
- // 获取价格
- Element priceElement = bookElement.selectFirst(".price_color");
- String price = priceElement != null ? priceElement.text() : "未知价格";
-
- // 获取库存状态
- Element availabilityElement = bookElement.selectFirst(".instock.availability");
- String availability = availabilityElement != null ? availabilityElement.text().trim() : "未知库存";
-
- // 获取星级评分
- Element ratingElement = bookElement.selectFirst(".star-rating");
- String rating = "未知";
- if (ratingElement != null) {
- String classAttr = ratingElement.attr("class");
- if (classAttr.contains("One")) rating = "1星";
- else if (classAttr.contains("Two")) rating = "2星";
- else if (classAttr.contains("Three")) rating = "3星";
- else if (classAttr.contains("Four")) rating = "4星";
- else if (classAttr.contains("Five")) rating = "5星";
- }
-
- return new Book(title, price, availability, rating);
- } catch (Exception e) {
- throw new ParseException("解析书籍信息失败: " + e.getMessage(), e);
- }
- }
-
- @Override
- public String getDataSourceName() {
- return "toscrape.com书籍信息";
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/strategy/CrawlStrategy.java b/project/src/main/java/com/example/crawler/strategy/CrawlStrategy.java
deleted file mode 100644
index 39f8d22..0000000
--- a/project/src/main/java/com/example/crawler/strategy/CrawlStrategy.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.example.crawler.strategy;
-
-import com.example.crawler.exception.CrawlException;
-
-import java.util.List;
-
-/**
- * 爬取策略接口
- * 定义爬取操作的标准方法,实现策略模式
- */
-public interface CrawlStrategy {
-
- /**
- * 执行爬取操作
- *
- * @return 爬取到的数据列表
- * @throws CrawlException 爬虫异常
- */
- List crawl() throws CrawlException;
-
- /**
- * 获取数据源名称
- *
- * @return 数据源名称
- */
- String getDataSourceName();
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/strategy/NewsCrawlStrategy.java b/project/src/main/java/com/example/crawler/strategy/NewsCrawlStrategy.java
deleted file mode 100644
index ac30f8e..0000000
--- a/project/src/main/java/com/example/crawler/strategy/NewsCrawlStrategy.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package com.example.crawler.strategy;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.select.Elements;
-
-import com.example.crawler.exception.CrawlException;
-import com.example.crawler.exception.NetworkException;
-import com.example.crawler.exception.ParseException;
-import com.example.crawler.model.News;
-import com.example.crawler.util.HttpUtil;
-
-/**
- * 新浪新闻爬取策略
- * // 策略模式:新浪新闻爬取策略
- */
-public class NewsCrawlStrategy implements CrawlStrategy {
-
- private static final String NEWS_URL = "https://news.sina.com.cn/china/";
- private static final int MAX_NEWS_COUNT = 20;
-
- @Override
- public List crawl() throws CrawlException {
- List newsList = new ArrayList<>();
-
- try {
- // 设置请求头
- Map headers = Map.of(
- "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- "Referer", "https://news.sina.com.cn/",
- "Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
- );
-
- String html = HttpUtil.get(NEWS_URL, headers);
- Document doc = Jsoup.parse(html);
-
- // 新浪新闻页面结构可能变化,使用多种选择器尝试
- Elements newsElements = doc.select(".news-item, .news-list li, .list-item, .feed-card-item");
-
- // 如果上述选择器都没找到,尝试更通用的选择器
- if (newsElements.isEmpty()) {
- newsElements = doc.select("a[href*=sina.com.cn]");
- }
-
- int count = 0;
- for (Element element : newsElements) {
- if (count >= MAX_NEWS_COUNT) {
- break;
- }
-
- try {
- News news = parseNews(element);
- if (news != null && news.getTitle() != null && !news.getTitle().isEmpty()) {
- newsList.add(news);
- count++;
- }
- } catch (ParseException e) {
- // 跳过解析失败的新闻,继续处理下一个
- continue;
- }
- }
-
- // 如果使用通用选择器获取的结果不够,尝试另一种方式
- if (newsList.size() < MAX_NEWS_COUNT) {
- Elements titleElements = doc.select("h2 a, h3 a, .title a, .news-title a");
- for (Element element : titleElements) {
- if (count >= MAX_NEWS_COUNT) {
- break;
- }
- try {
- News news = parseNewsFromTitleElement(element);
- if (news != null && news.getTitle() != null && !news.getTitle().isEmpty()) {
- newsList.add(news);
- count++;
- }
- } catch (ParseException e) {
- continue;
- }
- }
- }
-
- System.out.println("已爬取 " + newsList.size() + " 条新浪新闻");
- return newsList;
-
- } catch (NetworkException e) {
- throw new NetworkException("爬取新浪新闻时网络异常: " + e.getMessage(), e);
- } catch (Exception e) {
- throw new CrawlException("爬取新浪新闻时发生未知异常: " + e.getMessage(), e);
- }
- }
-
- /**
- * 解析新闻元素
- */
- private News parseNews(Element element) throws ParseException {
- try {
- String title = "";
- String url = "";
- String publishTime = "";
-
- // 尝试获取标题和链接
- Element linkElement = element.selectFirst("a");
- if (linkElement != null) {
- title = linkElement.text().trim();
- url = linkElement.attr("abs:href");
- }
-
- // 尝试获取发布时间
- Element timeElement = element.selectFirst(".time, .pubtime, span[class*=time]");
- if (timeElement != null) {
- publishTime = timeElement.text().trim();
- }
-
- if (title.isEmpty() || url.isEmpty()) {
- return null;
- }
-
- return new News(title, publishTime, url);
- } catch (Exception e) {
- throw new ParseException("解析新闻信息失败: " + e.getMessage(), e);
- }
- }
-
- /**
- * 从标题元素解析新闻
- */
- private News parseNewsFromTitleElement(Element element) throws ParseException {
- try {
- String title = element.text().trim();
- String url = element.attr("abs:href");
-
- if (title.isEmpty() || url.isEmpty()) {
- return null;
- }
-
- return new News(title, "", url);
- } catch (Exception e) {
- throw new ParseException("解析新闻标题失败: " + e.getMessage(), e);
- }
- }
-
- @Override
- public String getDataSourceName() {
- return "新浪国内新闻";
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/strategy/StrategyFactory.java b/project/src/main/java/com/example/crawler/strategy/StrategyFactory.java
deleted file mode 100644
index 9b56383..0000000
--- a/project/src/main/java/com/example/crawler/strategy/StrategyFactory.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.example.crawler.strategy;
-
-import com.example.crawler.strategy.BookCrawlStrategy;
-import com.example.crawler.strategy.NewsCrawlStrategy;
-import com.example.crawler.strategy.UniversityRankCrawlStrategy;
-import com.example.crawler.strategy.WeatherCrawlStrategy;
-
-public class StrategyFactory {
-
- public static CrawlStrategy> getStrategy(int choice) {
- switch (choice) {
- case 1:
- return new BookCrawlStrategy();
- case 2:
- return new NewsCrawlStrategy();
- case 3:
- return new UniversityRankCrawlStrategy();
- case 4:
- return new WeatherCrawlStrategy();
- default:
- throw new IllegalArgumentException("Invalid choice: " + choice);
- }
- }
-}
diff --git a/project/src/main/java/com/example/crawler/strategy/UniversityRankCrawlStrategy.java b/project/src/main/java/com/example/crawler/strategy/UniversityRankCrawlStrategy.java
deleted file mode 100644
index f4b5f81..0000000
--- a/project/src/main/java/com/example/crawler/strategy/UniversityRankCrawlStrategy.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package com.example.crawler.strategy;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.select.Elements;
-
-import com.example.crawler.exception.CrawlException;
-import com.example.crawler.exception.NetworkException;
-import com.example.crawler.exception.ParseException;
-import com.example.crawler.model.UniversityRank;
-import com.example.crawler.util.HttpUtil;
-
-/**
- * 软科中国大学排名爬取策略
- * // 策略模式:软科中国大学排名爬取策略
- */
-public class UniversityRankCrawlStrategy implements CrawlStrategy {
-
- private static final String RANKING_URL = "https://www.shanghairanking.cn/rankings/bcur/2025";
-
- @Override
- public List crawl() throws CrawlException {
- List rankings = new ArrayList<>();
-
- try {
- // 设置请求头
- Map headers = Map.of(
- "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- "Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
- "Referer", "https://www.shanghairanking.cn/"
- );
-
- // 设置请求延迟
- HttpUtil.sleep(3);
-
- String html = HttpUtil.get(RANKING_URL, headers);
- Document doc = Jsoup.parse(html);
-
- // 提取表格数据
- Elements rows = doc.select("table tbody tr");
-
- if (rows.isEmpty()) {
- // 如果第一个选择器失败,尝试其他可能的选择器
- rows = doc.select(".rk-table tbody tr");
- }
-
- if (rows.isEmpty()) {
- // 尝试更通用的选择器
- rows = doc.select("tr");
- }
-
- int count = 0;
- for (Element row : rows) {
- try {
- UniversityRank ranking = parseRow(row);
- if (ranking != null && ranking.getRank() != null) {
- rankings.add(ranking);
- count++;
-
- // 最多爬取200条数据
- if (count >= 200) {
- break;
- }
- }
- } catch (ParseException e) {
- // 跳过解析失败的行
- continue;
- }
- }
-
- System.out.println("已爬取 " + rankings.size() + " 条大学排名数据");
- return rankings;
-
- } catch (NetworkException e) {
- throw new NetworkException("爬取软科大学排名时网络异常: " + e.getMessage(), e);
- } catch (Exception e) {
- throw new CrawlException("爬取软科大学排名时发生未知异常: " + e.getMessage(), e);
- }
- }
-
- /**
- * 解析表格行数据
- */
- private UniversityRank parseRow(Element row) throws ParseException {
- try {
- Elements cells = row.select("td");
-
- if (cells.size() < 4) {
- return null;
- }
-
- // 第1列:排名
- String rankStr = cells.get(0).text().trim();
- Integer rank = null;
- try {
- rank = Integer.parseInt(rankStr);
- } catch (NumberFormatException e) {
- // 如果排名不是数字(如"1-3"这样的范围),尝试提取第一个数字
- String numPart = rankStr.replaceAll("[^0-9]", "");
- if (!numPart.isEmpty()) {
- rank = Integer.parseInt(numPart);
- }
- }
-
- if (rank == null) {
- return null;
- }
-
- // 第2列:学校名称
- String universityName = cells.get(1).text().trim();
-
- // 第4列:总分
- String totalScore = "";
- if (cells.size() > 3) {
- totalScore = cells.get(3).text().trim();
- }
-
- // 尝试提取省份和办学层次(第3列可能包含这些信息)
- String province = "";
- String category = "";
- if (cells.size() > 2) {
- String thirdColumn = cells.get(2).text().trim();
- // 尝试解析省份和办学层次
- String[] parts = thirdColumn.split("\\s+");
- if (parts.length >= 1) {
- province = parts[0];
- }
- if (parts.length >= 2) {
- category = parts[1];
- }
- }
-
- return new UniversityRank(rank, universityName, totalScore, province, category);
- } catch (Exception e) {
- throw new ParseException("解析大学排名行数据失败: " + e.getMessage(), e);
- }
- }
-
- @Override
- public String getDataSourceName() {
- return "软科中国大学排名";
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/strategy/WeatherCrawlStrategy.java b/project/src/main/java/com/example/crawler/strategy/WeatherCrawlStrategy.java
deleted file mode 100644
index 6ade78c..0000000
--- a/project/src/main/java/com/example/crawler/strategy/WeatherCrawlStrategy.java
+++ /dev/null
@@ -1,177 +0,0 @@
-package com.example.crawler.strategy;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import com.example.crawler.constant.CrawlerConstants;
-import com.example.crawler.exception.CrawlException;
-import com.example.crawler.exception.NetworkException;
-import com.example.crawler.exception.ParseException;
-import com.example.crawler.model.Weather;
-import com.example.crawler.util.HttpUtil;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-
-public class WeatherCrawlStrategy implements CrawlStrategy {
-
- @Override
- public List crawl() throws CrawlException {
- List weatherList = new ArrayList<>();
-
- try {
- for (Map.Entry entry : CrawlerConstants.CITY_COORDINATES.entrySet()) {
- String cityName = entry.getKey();
- double[] coords = entry.getValue();
- double latitude = coords[0];
- double longitude = coords[1];
-
- String weatherUrl = buildApiUrl(latitude, longitude);
- Map headers = Map.of(
- "User-Agent", CrawlerConstants.USER_AGENT
- );
-
- String response = HttpUtil.get(weatherUrl, headers);
- Weather weather = parseWeatherData(cityName, response);
- weatherList.add(weather);
-
- System.out.println("已获取 " + cityName + " 的天气信息");
-
- HttpUtil.sleep(2);
- }
-
- return weatherList;
-
- } catch (NetworkException e) {
- throw new NetworkException("爬取天气数据时网络异常: " + e.getMessage(), e);
- } catch (ParseException e) {
- throw new ParseException("解析天气数据时异常: " + e.getMessage(), e);
- } catch (Exception e) {
- throw new CrawlException("爬取天气数据时发生未知异常: " + e.getMessage(), e);
- }
- }
-
- private String buildApiUrl(double latitude, double longitude) {
- return CrawlerConstants.URL_WEATHER_API + "?latitude=" + latitude +
- "&longitude=" + longitude +
- "¤t_weather=true" +
- "&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m" +
- "&forecast_days=1" +
- "&timezone=Asia/Shanghai";
- }
-
- private Weather parseWeatherData(String cityName, String jsonData) throws ParseException {
- try {
- JsonObject obj = JsonParser.parseString(jsonData).getAsJsonObject();
-
- Weather weather = new Weather();
- weather.setCityName(cityName);
-
- JsonObject currentWeather = obj.getAsJsonObject("current_weather");
- if (currentWeather != null) {
- weather.setTemperature(cleanTemperature(getJsonDouble(currentWeather, "temperature", 0)));
- weather.setWindSpeed(cleanWindSpeed(getJsonDouble(currentWeather, "windspeed", 0)));
- weather.setWeatherCode(String.valueOf(getJsonInt(currentWeather, "weathercode", -1)));
- }
-
- JsonObject hourly = obj.getAsJsonObject("hourly");
- if (hourly != null) {
- JsonArray times = hourly.getAsJsonArray("time");
- JsonArray temps = hourly.getAsJsonArray("temperature_2m");
- JsonArray humidities = hourly.getAsJsonArray("relative_humidity_2m");
- JsonArray windSpeeds = hourly.getAsJsonArray("wind_speed_10m");
-
- if (times != null && temps != null) {
- int count = Math.min(times.size(), 24);
- for (int i = 0; i < count; i++) {
- weather.getHourlyTimes().add(cleanTimeString(getJsonString(times, i, "")));
- weather.getHourlyTemperatures().add(cleanTemperature(getJsonDouble(temps, i, 0)));
- }
- }
-
- if (humidities != null) {
- int count = Math.min(humidities.size(), 24);
- for (int i = 0; i < count; i++) {
- weather.getHourlyHumidities().add(cleanHumidity(getJsonInt(humidities, i, 50)));
- }
- }
-
- if (windSpeeds != null) {
- int count = Math.min(windSpeeds.size(), 24);
- for (int i = 0; i < count; i++) {
- weather.getHourlyWindSpeeds().add(cleanWindSpeed(getJsonDouble(windSpeeds, i, 0)));
- }
- }
-
- if (!weather.getHourlyHumidities().isEmpty()) {
- weather.setHumidity(weather.getHourlyHumidities().get(0));
- }
- }
-
- return weather;
- } catch (Exception e) {
- throw new ParseException("解析天气JSON数据失败: " + e.getMessage(), e);
- }
- }
-
- private String getJsonString(JsonArray arr, int index, String defaultValue) {
- if (arr == null || index >= arr.size()) return defaultValue;
- JsonElement element = arr.get(index);
- return element.isJsonNull() ? defaultValue : element.getAsString();
- }
-
- private double getJsonDouble(JsonObject obj, String key, double defaultValue) {
- JsonElement element = obj.get(key);
- if (element == null || element.isJsonNull()) return defaultValue;
- return element.getAsDouble();
- }
-
- private int getJsonInt(JsonObject obj, String key, int defaultValue) {
- JsonElement element = obj.get(key);
- if (element == null || element.isJsonNull()) return defaultValue;
- return element.getAsInt();
- }
-
- private double getJsonDouble(JsonArray arr, int index, double defaultValue) {
- if (arr == null || index >= arr.size()) return defaultValue;
- JsonElement element = arr.get(index);
- if (element == null || element.isJsonNull()) return defaultValue;
- return element.getAsDouble();
- }
-
- private int getJsonInt(JsonArray arr, int index, int defaultValue) {
- if (arr == null || index >= arr.size()) return defaultValue;
- JsonElement element = arr.get(index);
- if (element == null || element.isJsonNull()) return defaultValue;
- return element.getAsInt();
- }
-
- private double cleanTemperature(double temp) {
- return Math.round(temp * 10.0) / 10.0;
- }
-
- private double cleanWindSpeed(double speed) {
- return Math.round(speed * 10.0) / 10.0;
- }
-
- private int cleanHumidity(int humidity) {
- if (humidity < 0) return 50;
- if (humidity > 100) return 100;
- return humidity;
- }
-
- private String cleanTimeString(String time) {
- if (time == null || time.isEmpty()) return "";
- if (time.contains("T")) {
- return time.substring(time.indexOf("T") + 1, time.indexOf("T") + 6);
- }
- return time;
- }
-
- @Override
- public String getDataSourceName() {
- return "Open-Meteo 实时天气";
- }
-}
diff --git a/project/src/main/java/com/example/crawler/util/DataCleaner.java b/project/src/main/java/com/example/crawler/util/DataCleaner.java
deleted file mode 100644
index 32991fd..0000000
--- a/project/src/main/java/com/example/crawler/util/DataCleaner.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package com.example.crawler.util;
-
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * 数据清洗工具类
- * 提供各类数据的清洗方法
- */
-public class DataCleaner {
-
- private static final Map STOP_WORDS = new HashMap<>();
- static {
- STOP_WORDS.put("的", "的");
- STOP_WORDS.put("了", "了");
- STOP_WORDS.put("是", "是");
- STOP_WORDS.put("在", "在");
- STOP_WORDS.put("和", "和");
- STOP_WORDS.put("与", "与");
- STOP_WORDS.put("对", "对");
- STOP_WORDS.put("为", "为");
- STOP_WORDS.put("有", "有");
- STOP_WORDS.put("我", "我");
- STOP_WORDS.put("你", "你");
- STOP_WORDS.put("他", "他");
- STOP_WORDS.put("她", "她");
- STOP_WORDS.put("它", "它");
- STOP_WORDS.put("这", "这");
- STOP_WORDS.put("那", "那");
- STOP_WORDS.put("就", "就");
- STOP_WORDS.put("也", "也");
- STOP_WORDS.put("都", "都");
- STOP_WORDS.put("要", "要");
- STOP_WORDS.put("会", "会");
- STOP_WORDS.put("能", "能");
- STOP_WORDS.put("可", "可");
- STOP_WORDS.put("以", "以");
- STOP_WORDS.put("说", "说");
- STOP_WORDS.put("到", "到");
- STOP_WORDS.put("来", "来");
- STOP_WORDS.put("去", "去");
- STOP_WORDS.put("着", "着");
- STOP_WORDS.put("过", "过");
- }
-
- public static double cleanPrice(String price) {
- if (price == null || price.isEmpty()) return 0.0;
- String cleaned = price.replaceAll("[^0-9.]", "");
- try {
- return Double.parseDouble(cleaned);
- } catch (NumberFormatException e) {
- return 0.0;
- }
- }
-
- public static int cleanRating(String ratingClass) {
- if (ratingClass == null) return 0;
- if (ratingClass.contains("Five")) return 5;
- if (ratingClass.contains("Four")) return 4;
- if (ratingClass.contains("Three")) return 3;
- if (ratingClass.contains("Two")) return 2;
- if (ratingClass.contains("One")) return 1;
- return 0;
- }
-
- public static LocalDateTime cleanNewsTime(String timeStr) {
- if (timeStr == null || timeStr.isEmpty()) return LocalDateTime.now();
- try {
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
- return LocalDateTime.parse(timeStr, formatter);
- } catch (Exception e) {
- try {
- DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm");
- return LocalDateTime.parse(timeStr, formatter2);
- } catch (Exception e2) {
- return LocalDateTime.now();
- }
- }
- }
-
- public static String cleanTitle(String title) {
- if (title == null) return "";
- return title.trim().replaceAll("\\s+", " ");
- }
-
- public static double cleanScore(String score) {
- if (score == null || score.isEmpty()) return 0.0;
- String cleaned = score.replaceAll("[^0-9.]", "");
- try {
- return Double.parseDouble(cleaned);
- } catch (NumberFormatException e) {
- return 0.0;
- }
- }
-
- public static String[] extractWords(String text) {
- if (text == null || text.isEmpty()) return new String[0];
- String cleaned = text.replaceAll("[^\u4e00-\u9fa5a-zA-Z0-9]", " ");
- return cleaned.split("\\s+");
- }
-
- public static boolean isStopWord(String word) {
- return word == null || word.length() < 2 || STOP_WORDS.containsKey(word);
- }
-
- public static Map countWordFrequency(String[] words) {
- Map frequency = new HashMap<>();
- for (String word : words) {
- if (isStopWord(word)) continue;
- frequency.put(word, frequency.getOrDefault(word, 0) + 1);
- }
- return frequency;
- }
-
- public static int extractHour(LocalDateTime dateTime) {
- return dateTime.getHour();
- }
-}
diff --git a/project/src/main/java/com/example/crawler/util/HttpUtil.java b/project/src/main/java/com/example/crawler/util/HttpUtil.java
deleted file mode 100644
index 774d2e6..0000000
--- a/project/src/main/java/com/example/crawler/util/HttpUtil.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package com.example.crawler.util;
-
-import com.example.crawler.exception.NetworkException;
-
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.time.Duration;
-import java.util.Map;
-
-/**
- * HTTP工具类
- * 封装HTTP请求操作,使用Java 11内置HttpClient
- */
-public class HttpUtil {
-
- private static final HttpClient httpClient = HttpClient.newBuilder()
- .connectTimeout(Duration.ofSeconds(30))
- .followRedirects(HttpClient.Redirect.NORMAL)
- .build();
-
- private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
-
- /**
- * 发送GET请求
- *
- * @param url 请求URL
- * @return 响应内容
- * @throws NetworkException 网络异常
- */
- public static String get(String url) throws NetworkException {
- return get(url, Map.of());
- }
-
- /**
- * 发送GET请求(带请求头)
- *
- * @param url 请求URL
- * @param headers 请求头
- * @return 响应内容
- * @throws NetworkException 网络异常
- */
- public static String get(String url, Map headers) throws NetworkException {
- try {
- HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .timeout(Duration.ofSeconds(30))
- .GET();
-
- // 添加默认User-Agent
- if (!headers.containsKey("User-Agent")) {
- requestBuilder.header("User-Agent", DEFAULT_USER_AGENT);
- }
-
- // 添加自定义请求头
- headers.forEach(requestBuilder::header);
-
- HttpRequest request = requestBuilder.build();
- HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
-
- if (response.statusCode() != 200) {
- throw new NetworkException("HTTP请求失败,状态码: " + response.statusCode());
- }
-
- return response.body();
- } catch (NetworkException e) {
- throw e;
- } catch (Exception e) {
- throw new NetworkException("网络请求失败: " + e.getMessage(), e);
- }
- }
-
- /**
- * 发送POST请求
- *
- * @param url 请求URL
- * @param body 请求体
- * @param headers 请求头
- * @return 响应内容
- * @throws NetworkException 网络异常
- */
- public static String post(String url, String body, Map headers) throws NetworkException {
- try {
- HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
- .uri(URI.create(url))
- .timeout(Duration.ofSeconds(30))
- .header("Content-Type", "application/json")
- .POST(HttpRequest.BodyPublishers.ofString(body));
-
- // 添加默认User-Agent
- if (!headers.containsKey("User-Agent")) {
- requestBuilder.header("User-Agent", DEFAULT_USER_AGENT);
- }
-
- // 添加自定义请求头
- headers.forEach(requestBuilder::header);
-
- HttpRequest request = requestBuilder.build();
- HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
-
- if (response.statusCode() != 200) {
- throw new NetworkException("HTTP请求失败,状态码: " + response.statusCode());
- }
-
- return response.body();
- } catch (NetworkException e) {
- throw e;
- } catch (Exception e) {
- throw new NetworkException("网络请求失败: " + e.getMessage(), e);
- }
- }
-
- /**
- * 设置请求间隔,避免对服务器造成压力
- *
- * @param seconds 间隔秒数
- */
- public static void sleep(int seconds) {
- try {
- Thread.sleep(seconds * 1000L);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/util/JsonUtil.java b/project/src/main/java/com/example/crawler/util/JsonUtil.java
deleted file mode 100644
index 5b2e929..0000000
--- a/project/src/main/java/com/example/crawler/util/JsonUtil.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.example.crawler.util;
-
-import com.example.crawler.exception.DataSaveException;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-import java.io.FileWriter;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.List;
-
-/**
- * JSON工具类
- * 封装JSON序列化和文件读写操作
- */
-public class JsonUtil {
-
- private static final Gson gson = new GsonBuilder()
- .setPrettyPrinting()
- .disableHtmlEscaping()
- .create();
-
- /**
- * 将对象序列化为JSON字符串
- *
- * @param obj 对象
- * @return JSON字符串
- */
- public static String toJson(Object obj) {
- return gson.toJson(obj);
- }
-
- /**
- * 将JSON字符串反序列化为对象
- *
- * @param json JSON字符串
- * @param classOfT 目标类
- * @param 泛型类型
- * @return 反序列化后的对象
- */
- public static T fromJson(String json, Class classOfT) {
- return gson.fromJson(json, classOfT);
- }
-
- /**
- * 将对象保存为JSON文件
- *
- * @param obj 对象
- * @param filePath 文件路径
- * @throws DataSaveException 数据保存异常
- */
- public static void saveToJsonFile(Object obj, String filePath) throws DataSaveException {
- try {
- // 确保目录存在
- Path path = Paths.get(filePath);
- Path parentDir = path.getParent();
- if (parentDir != null && !Files.exists(parentDir)) {
- Files.createDirectories(parentDir);
- }
-
- try (FileWriter writer = new FileWriter(filePath)) {
- gson.toJson(obj, writer);
- }
- } catch (IOException e) {
- throw new DataSaveException("保存JSON文件失败: " + e.getMessage(), e);
- }
- }
-
- /**
- * 将列表保存为JSON文件
- *
- * @param list 列表
- * @param filePath 文件路径
- * @param 泛型类型
- * @throws DataSaveException 数据保存异常
- */
- public static void saveListToJsonFile(List list, String filePath) throws DataSaveException {
- try {
- // 确保目录存在
- Path path = Paths.get(filePath);
- Path parentDir = path.getParent();
- if (parentDir != null && !Files.exists(parentDir)) {
- Files.createDirectories(parentDir);
- }
-
- try (FileWriter writer = new FileWriter(filePath)) {
- gson.toJson(list, writer);
- }
- } catch (IOException e) {
- throw new DataSaveException("保存JSON文件失败: " + e.getMessage(), e);
- }
- }
-}
\ No newline at end of file
diff --git a/project/src/main/java/com/example/crawler/view/CrawlerView.java b/project/src/main/java/com/example/crawler/view/CrawlerView.java
deleted file mode 100644
index 5e72292..0000000
--- a/project/src/main/java/com/example/crawler/view/CrawlerView.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.example.crawler.view;
-
-import java.util.Scanner;
-
-/**
- * 爬虫视图类
- * // MVC模式:View层,负责CLI界面显示和用户交互
- */
-public class CrawlerView {
-
- /**
- * 显示主菜单
- */
- public void showMenu() {
- System.out.println("\n=== 数据爬取与分析系统 ===");
- System.out.println("1. 爬取书籍信息(toscrape.com)");
- System.out.println("2. 爬取新浪国内新闻");
- System.out.println("3. 爬取软科中国大学排名");
- System.out.println("4. 爬取Open-Meteo实时天气");
- System.out.println("5. 爬取全部数据并保存");
- System.out.println("6. 保存当前数据到文件");
- System.out.println("7. 生成所有数据源的分析报告与图表");
- System.out.println("8. 爬取并分析所有数据(一键完成)");
- System.out.println("9. 退出");
- System.out.print("请选择操作:");
- }
-
- /**
- * 获取用户输入
- *
- * @param scanner 输入扫描器
- * @return 用户选择的数字
- */
- public int getInput(Scanner scanner) {
- try {
- String input = scanner.nextLine().trim();
- return Integer.parseInt(input);
- } catch (NumberFormatException e) {
- return -1; // 返回无效值
- }
- }
-
- /**
- * 显示错误信息
- *
- * @param message 错误信息
- */
- public void showError(String message) {
- System.err.println("错误: " + message);
- }
-
- /**
- * 显示成功信息
- *
- * @param message 成功信息
- */
- public void showSuccess(String message) {
- System.out.println("成功: " + message);
- }
-
- /**
- * 暂停并等待用户按回车键继续
- *
- * @param scanner 输入扫描器
- */
- public void pause(Scanner scanner) {
- System.out.print("\n按回车键继续...");
- scanner.nextLine();
- System.out.print("\033[H\033[2J");
- System.out.flush();
- }
-}
diff --git a/project/target/classes/com/example/crawler/Main.class b/project/target/classes/com/example/crawler/Main.class
deleted file mode 100644
index 1497663..0000000
Binary files a/project/target/classes/com/example/crawler/Main.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/chart/ChartGenerator.class b/project/target/classes/com/example/crawler/chart/ChartGenerator.class
deleted file mode 100644
index dd49de6..0000000
Binary files a/project/target/classes/com/example/crawler/chart/ChartGenerator.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/BaseCrawlCommand.class b/project/target/classes/com/example/crawler/command/BaseCrawlCommand.class
deleted file mode 100644
index 216241a..0000000
Binary files a/project/target/classes/com/example/crawler/command/BaseCrawlCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/BookCommand.class b/project/target/classes/com/example/crawler/command/BookCommand.class
deleted file mode 100644
index 3d0baf1..0000000
Binary files a/project/target/classes/com/example/crawler/command/BookCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/Command.class b/project/target/classes/com/example/crawler/command/Command.class
deleted file mode 100644
index 7bc1ab3..0000000
Binary files a/project/target/classes/com/example/crawler/command/Command.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/CrawlAllCommand.class b/project/target/classes/com/example/crawler/command/CrawlAllCommand.class
deleted file mode 100644
index eeb9144..0000000
Binary files a/project/target/classes/com/example/crawler/command/CrawlAllCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/CrawlAndAnalyzeAllCommand.class b/project/target/classes/com/example/crawler/command/CrawlAndAnalyzeAllCommand.class
deleted file mode 100644
index a2db69d..0000000
Binary files a/project/target/classes/com/example/crawler/command/CrawlAndAnalyzeAllCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/CrawlRankingCommand.class b/project/target/classes/com/example/crawler/command/CrawlRankingCommand.class
deleted file mode 100644
index e65a8c4..0000000
Binary files a/project/target/classes/com/example/crawler/command/CrawlRankingCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/ExitCommand.class b/project/target/classes/com/example/crawler/command/ExitCommand.class
deleted file mode 100644
index 903b3a7..0000000
Binary files a/project/target/classes/com/example/crawler/command/ExitCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/GenerateAllAnalysisCommand.class b/project/target/classes/com/example/crawler/command/GenerateAllAnalysisCommand.class
deleted file mode 100644
index 60ef93b..0000000
Binary files a/project/target/classes/com/example/crawler/command/GenerateAllAnalysisCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/NewsCommand.class b/project/target/classes/com/example/crawler/command/NewsCommand.class
deleted file mode 100644
index b7c5106..0000000
Binary files a/project/target/classes/com/example/crawler/command/NewsCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/SaveCommand.class b/project/target/classes/com/example/crawler/command/SaveCommand.class
deleted file mode 100644
index 291183a..0000000
Binary files a/project/target/classes/com/example/crawler/command/SaveCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/command/WeatherCommand.class b/project/target/classes/com/example/crawler/command/WeatherCommand.class
deleted file mode 100644
index be29d68..0000000
Binary files a/project/target/classes/com/example/crawler/command/WeatherCommand.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/constant/CrawlerConstants.class b/project/target/classes/com/example/crawler/constant/CrawlerConstants.class
deleted file mode 100644
index d201e06..0000000
Binary files a/project/target/classes/com/example/crawler/constant/CrawlerConstants.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/controller/CrawlerController.class b/project/target/classes/com/example/crawler/controller/CrawlerController.class
deleted file mode 100644
index 6fb5ba5..0000000
Binary files a/project/target/classes/com/example/crawler/controller/CrawlerController.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/exception/CrawlException.class b/project/target/classes/com/example/crawler/exception/CrawlException.class
deleted file mode 100644
index e48a793..0000000
Binary files a/project/target/classes/com/example/crawler/exception/CrawlException.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/exception/DataSaveException.class b/project/target/classes/com/example/crawler/exception/DataSaveException.class
deleted file mode 100644
index dc59472..0000000
Binary files a/project/target/classes/com/example/crawler/exception/DataSaveException.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/exception/NetworkException.class b/project/target/classes/com/example/crawler/exception/NetworkException.class
deleted file mode 100644
index 2327b22..0000000
Binary files a/project/target/classes/com/example/crawler/exception/NetworkException.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/exception/ParseException.class b/project/target/classes/com/example/crawler/exception/ParseException.class
deleted file mode 100644
index 180dbca..0000000
Binary files a/project/target/classes/com/example/crawler/exception/ParseException.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/model/Book.class b/project/target/classes/com/example/crawler/model/Book.class
deleted file mode 100644
index de5aa2b..0000000
Binary files a/project/target/classes/com/example/crawler/model/Book.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/model/News.class b/project/target/classes/com/example/crawler/model/News.class
deleted file mode 100644
index 2f396b6..0000000
Binary files a/project/target/classes/com/example/crawler/model/News.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/model/UniversityRank.class b/project/target/classes/com/example/crawler/model/UniversityRank.class
deleted file mode 100644
index 39bd8fa..0000000
Binary files a/project/target/classes/com/example/crawler/model/UniversityRank.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/model/Weather.class b/project/target/classes/com/example/crawler/model/Weather.class
deleted file mode 100644
index 223a2d8..0000000
Binary files a/project/target/classes/com/example/crawler/model/Weather.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/repository/DataRepository.class b/project/target/classes/com/example/crawler/repository/DataRepository.class
deleted file mode 100644
index 9325a6b..0000000
Binary files a/project/target/classes/com/example/crawler/repository/DataRepository.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/service/BookAnalysisService.class b/project/target/classes/com/example/crawler/service/BookAnalysisService.class
deleted file mode 100644
index 3aac772..0000000
Binary files a/project/target/classes/com/example/crawler/service/BookAnalysisService.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/service/NewsAnalysisService.class b/project/target/classes/com/example/crawler/service/NewsAnalysisService.class
deleted file mode 100644
index 347efc9..0000000
Binary files a/project/target/classes/com/example/crawler/service/NewsAnalysisService.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/service/RankingAnalysisService.class b/project/target/classes/com/example/crawler/service/RankingAnalysisService.class
deleted file mode 100644
index fcbac49..0000000
Binary files a/project/target/classes/com/example/crawler/service/RankingAnalysisService.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/service/WeatherAnalysisService.class b/project/target/classes/com/example/crawler/service/WeatherAnalysisService.class
deleted file mode 100644
index c6cd28c..0000000
Binary files a/project/target/classes/com/example/crawler/service/WeatherAnalysisService.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/strategy/BookCrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/BookCrawlStrategy.class
deleted file mode 100644
index c00eb38..0000000
Binary files a/project/target/classes/com/example/crawler/strategy/BookCrawlStrategy.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/strategy/CrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/CrawlStrategy.class
deleted file mode 100644
index dd7a055..0000000
Binary files a/project/target/classes/com/example/crawler/strategy/CrawlStrategy.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/strategy/NewsCrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/NewsCrawlStrategy.class
deleted file mode 100644
index d87e85e..0000000
Binary files a/project/target/classes/com/example/crawler/strategy/NewsCrawlStrategy.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/strategy/StrategyFactory.class b/project/target/classes/com/example/crawler/strategy/StrategyFactory.class
deleted file mode 100644
index 7484a28..0000000
Binary files a/project/target/classes/com/example/crawler/strategy/StrategyFactory.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/strategy/UniversityRankCrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/UniversityRankCrawlStrategy.class
deleted file mode 100644
index 73618c1..0000000
Binary files a/project/target/classes/com/example/crawler/strategy/UniversityRankCrawlStrategy.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/strategy/WeatherCrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/WeatherCrawlStrategy.class
deleted file mode 100644
index aaad47c..0000000
Binary files a/project/target/classes/com/example/crawler/strategy/WeatherCrawlStrategy.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/util/DataCleaner.class b/project/target/classes/com/example/crawler/util/DataCleaner.class
deleted file mode 100644
index 4fedbe3..0000000
Binary files a/project/target/classes/com/example/crawler/util/DataCleaner.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/util/HttpUtil.class b/project/target/classes/com/example/crawler/util/HttpUtil.class
deleted file mode 100644
index a0e5905..0000000
Binary files a/project/target/classes/com/example/crawler/util/HttpUtil.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/util/JsonUtil.class b/project/target/classes/com/example/crawler/util/JsonUtil.class
deleted file mode 100644
index b8af89d..0000000
Binary files a/project/target/classes/com/example/crawler/util/JsonUtil.class and /dev/null differ
diff --git a/project/target/classes/com/example/crawler/view/CrawlerView.class b/project/target/classes/com/example/crawler/view/CrawlerView.class
deleted file mode 100644
index 4d8e326..0000000
Binary files a/project/target/classes/com/example/crawler/view/CrawlerView.class and /dev/null differ
diff --git a/project/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/project/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
deleted file mode 100644
index 784a2d4..0000000
--- a/project/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
+++ /dev/null
@@ -1,27 +0,0 @@
-com\example\crawler\constant\CrawlerConstants.class
-com\example\crawler\model\Weather.class
-com\example\crawler\util\DataCleaner.class
-com\example\crawler\strategy\NewsCrawlStrategy.class
-com\example\crawler\util\HttpUtil.class
-com\example\crawler\model\Book.class
-com\example\crawler\service\BookAnalysisService.class
-com\example\crawler\model\UniversityRank.class
-com\example\crawler\strategy\WeatherCrawlStrategy.class
-com\example\crawler\strategy\CrawlStrategy.class
-com\example\crawler\exception\NetworkException.class
-com\example\crawler\model\News.class
-com\example\crawler\command\Command.class
-com\example\crawler\strategy\StrategyFactory.class
-com\example\crawler\command\ExitCommand.class
-com\example\crawler\strategy\BookCrawlStrategy.class
-com\example\crawler\exception\CrawlException.class
-com\example\crawler\util\JsonUtil.class
-com\example\crawler\strategy\UniversityRankCrawlStrategy.class
-com\example\crawler\service\NewsAnalysisService.class
-com\example\crawler\service\WeatherAnalysisService.class
-com\example\crawler\exception\DataSaveException.class
-com\example\crawler\repository\DataRepository.class
-com\example\crawler\exception\ParseException.class
-com\example\crawler\view\CrawlerView.class
-com\example\crawler\chart\ChartGenerator.class
-com\example\crawler\service\RankingAnalysisService.class
diff --git a/project/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/project/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
deleted file mode 100644
index f0e9336..0000000
--- a/project/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
+++ /dev/null
@@ -1,38 +0,0 @@
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\exception\NetworkException.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\CrawlAllCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\service\BookAnalysisService.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\util\DataCleaner.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\BookCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\repository\DataRepository.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\strategy\StrategyFactory.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\util\HttpUtil.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\service\RankingAnalysisService.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\service\WeatherAnalysisService.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\constant\CrawlerConstants.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\strategy\BookCrawlStrategy.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\model\UniversityRank.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\NewsCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\Command.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\model\Weather.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\exception\ParseException.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\GenerateAllAnalysisCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\strategy\UniversityRankCrawlStrategy.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\util\JsonUtil.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\CrawlAndAnalyzeAllCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\service\NewsAnalysisService.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\CrawlRankingCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\exception\DataSaveException.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\strategy\WeatherCrawlStrategy.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\SaveCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\model\News.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\BaseCrawlCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\controller\CrawlerController.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\model\Book.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\exception\CrawlException.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\ExitCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\command\WeatherCommand.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\chart\ChartGenerator.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\strategy\CrawlStrategy.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\Main.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\strategy\NewsCrawlStrategy.java
-C:\Users\Lenovo\Desktop\java\project\src\main\java\com\example\crawler\view\CrawlerView.java