diff --git a/java-cli/src/main/java/com/example/datacollect/command/HistoryCommand.java b/java-cli/src/main/java/com/example/datacollect/command/HistoryCommand.java new file mode 100644 index 0000000..e3846a1 --- /dev/null +++ b/java-cli/src/main/java/com/example/datacollect/command/HistoryCommand.java @@ -0,0 +1,4 @@ +package com.example.datacollect.command; + +public class HistoryCommand { +} diff --git a/project/.idea/.gitignore b/project/.idea/.gitignore new file mode 100644 index 0000000..b6b1ecf --- /dev/null +++ b/project/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/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 new file mode 100644 index 0000000..90212ab --- /dev/null +++ b/project/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/project/.idea/encodings.xml b/project/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/project/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/project/.idea/jarRepositories.xml b/project/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/project/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/project/.idea/misc.xml b/project/.idea/misc.xml new file mode 100644 index 0000000..9dc782b --- /dev/null +++ b/project/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/project/.idea/vcs.xml b/project/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/project/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/project/202506050211-靖佳颖-期末实验报告.docx b/project/202506050211-靖佳颖-期末实验报告.docx new file mode 100644 index 0000000..5e5c363 Binary files /dev/null and b/project/202506050211-靖佳颖-期末实验报告.docx differ diff --git a/project/charts/news_time_trend.png b/project/charts/news_time_trend.png new file mode 100644 index 0000000..caa1bf0 Binary files /dev/null and b/project/charts/news_time_trend.png differ diff --git a/project/charts/news_top_words.png b/project/charts/news_top_words.png new file mode 100644 index 0000000..efa2fda Binary files /dev/null and b/project/charts/news_top_words.png differ diff --git a/project/charts/price_histogram.png b/project/charts/price_histogram.png new file mode 100644 index 0000000..2e8a277 Binary files /dev/null and b/project/charts/price_histogram.png differ diff --git a/project/charts/province_bar.png b/project/charts/province_bar.png new file mode 100644 index 0000000..08ad8b7 Binary files /dev/null and b/project/charts/province_bar.png differ diff --git a/project/charts/province_distribution_2022.png b/project/charts/province_distribution_2022.png deleted file mode 100644 index 21ca053..0000000 Binary files a/project/charts/province_distribution_2022.png and /dev/null differ diff --git a/project/charts/province_distribution_2023.png b/project/charts/province_distribution_2023.png deleted file mode 100644 index 783ca23..0000000 Binary files a/project/charts/province_distribution_2023.png and /dev/null differ diff --git a/project/charts/province_distribution_2024.png b/project/charts/province_distribution_2024.png deleted file mode 100644 index b68155d..0000000 Binary files a/project/charts/province_distribution_2024.png and /dev/null differ diff --git a/project/charts/rank_trend_上海交通大学.png b/project/charts/rank_trend_上海交通大学.png deleted file mode 100644 index 2bc6b2c..0000000 Binary files a/project/charts/rank_trend_上海交通大学.png and /dev/null differ diff --git a/project/charts/rank_trend_北京大学.png b/project/charts/rank_trend_北京大学.png deleted file mode 100644 index bdf6fa1..0000000 Binary files a/project/charts/rank_trend_北京大学.png and /dev/null differ diff --git a/project/charts/rank_trend_复旦大学.png b/project/charts/rank_trend_复旦大学.png deleted file mode 100644 index e25a446..0000000 Binary files a/project/charts/rank_trend_复旦大学.png and /dev/null differ diff --git a/project/charts/rank_trend_浙江大学.png b/project/charts/rank_trend_浙江大学.png deleted file mode 100644 index ee484af..0000000 Binary files a/project/charts/rank_trend_浙江大学.png and /dev/null differ diff --git a/project/charts/rank_trend_清华大学.png b/project/charts/rank_trend_清华大学.png deleted file mode 100644 index aae460e..0000000 Binary files a/project/charts/rank_trend_清华大学.png and /dev/null differ diff --git a/project/charts/rating_pie.png b/project/charts/rating_pie.png new file mode 100644 index 0000000..20e8426 Binary files /dev/null and b/project/charts/rating_pie.png differ diff --git a/project/charts/temperature_comparison.png b/project/charts/temperature_comparison.png new file mode 100644 index 0000000..d006d51 Binary files /dev/null and b/project/charts/temperature_comparison.png differ diff --git a/project/charts/temperature_上海.png b/project/charts/temperature_上海.png new file mode 100644 index 0000000..e26c047 Binary files /dev/null and b/project/charts/temperature_上海.png differ diff --git a/project/charts/temperature_北京.png b/project/charts/temperature_北京.png new file mode 100644 index 0000000..ea333a1 Binary files /dev/null and b/project/charts/temperature_北京.png differ diff --git a/project/charts/temperature_广州.png b/project/charts/temperature_广州.png new file mode 100644 index 0000000..e2e9759 Binary files /dev/null and b/project/charts/temperature_广州.png differ diff --git a/project/charts/top10_2022.png b/project/charts/top10_2022.png deleted file mode 100644 index 793e19e..0000000 Binary files a/project/charts/top10_2022.png and /dev/null differ diff --git a/project/charts/top10_2023.png b/project/charts/top10_2023.png deleted file mode 100644 index 1f08206..0000000 Binary files a/project/charts/top10_2023.png and /dev/null differ diff --git a/project/charts/top10_2024.png b/project/charts/top10_2024.png deleted file mode 100644 index 8309920..0000000 Binary files a/project/charts/top10_2024.png and /dev/null differ diff --git a/project/data/university_rank_2022.csv b/project/data/university_rank_2022.csv deleted file mode 100644 index 95b84ec..0000000 --- a/project/data/university_rank_2022.csv +++ /dev/null @@ -1,21 +0,0 @@ -"排名","学校名称","省份","总分","年份" -"1","清华大学","北京","852.5","2022" -"2","北京大学","北京","848.2","2022" -"3","浙江大学","浙江","822.5","2022" -"4","上海交通大学","上海","815.3","2022" -"5","复旦大学","上海","805.1","2022" -"6","南京大学","江苏","785.6","2022" -"7","中国科学技术大学","安徽","782.4","2022" -"8","华中科技大学","湖北","765.8","2022" -"9","武汉大学","湖北","758.2","2022" -"10","西安交通大学","陕西","752.6","2022" -"11","中山大学","广东","745.3","2022" -"12","四川大学","四川","738.9","2022" -"13","哈尔滨工业大学","黑龙江","732.5","2022" -"14","北京航空航天大学","北京","725.8","2022" -"15","东南大学","江苏","718.4","2022" -"16","北京理工大学","北京","712.6","2022" -"17","同济大学","上海","705.3","2022" -"18","中国人民大学","北京","698.5","2022" -"19","北京师范大学","北京","692.1","2022" -"20","南开大学","天津","685.7","2022" diff --git a/project/data/university_rank_2023.csv b/project/data/university_rank_2023.csv deleted file mode 100644 index aa9f005..0000000 --- a/project/data/university_rank_2023.csv +++ /dev/null @@ -1,21 +0,0 @@ -"排名","学校名称","省份","总分","年份" -"1","清华大学","北京","853.0","2023" -"2","北京大学","北京","848.7","2023" -"3","浙江大学","浙江","823.0","2023" -"4","上海交通大学","上海","815.8","2023" -"5","复旦大学","上海","805.6","2023" -"6","南京大学","江苏","786.1","2023" -"7","中国科学技术大学","安徽","782.9","2023" -"8","华中科技大学","湖北","766.3","2023" -"9","武汉大学","湖北","758.7","2023" -"10","西安交通大学","陕西","753.1","2023" -"11","中山大学","广东","745.8","2023" -"12","四川大学","四川","739.4","2023" -"13","哈尔滨工业大学","黑龙江","733.0","2023" -"14","北京航空航天大学","北京","726.3","2023" -"15","东南大学","江苏","718.9","2023" -"16","北京理工大学","北京","713.1","2023" -"17","同济大学","上海","705.8","2023" -"18","中国人民大学","北京","699.0","2023" -"19","北京师范大学","北京","692.6","2023" -"20","南开大学","天津","686.2","2023" diff --git a/project/data/university_rank_2024.csv b/project/data/university_rank_2024.csv deleted file mode 100644 index 266b4a3..0000000 --- a/project/data/university_rank_2024.csv +++ /dev/null @@ -1,21 +0,0 @@ -"排名","学校名称","省份","总分","年份" -"1","清华大学","北京","853.5","2024" -"2","北京大学","北京","849.2","2024" -"3","浙江大学","浙江","823.5","2024" -"4","上海交通大学","上海","816.3","2024" -"5","复旦大学","上海","806.1","2024" -"6","南京大学","江苏","786.6","2024" -"7","中国科学技术大学","安徽","783.4","2024" -"8","华中科技大学","湖北","766.8","2024" -"9","武汉大学","湖北","759.2","2024" -"10","西安交通大学","陕西","753.6","2024" -"11","中山大学","广东","746.3","2024" -"12","四川大学","四川","739.9","2024" -"13","哈尔滨工业大学","黑龙江","733.5","2024" -"14","北京航空航天大学","北京","726.8","2024" -"15","东南大学","江苏","719.4","2024" -"16","北京理工大学","北京","713.6","2024" -"17","同济大学","上海","706.3","2024" -"18","中国人民大学","北京","699.5","2024" -"19","北京师范大学","北京","693.1","2024" -"20","南开大学","天津","686.7","2024" diff --git a/project/dependency-reduced-pom.xml b/project/dependency-reduced-pom.xml index 0bbd124..6997e2b 100644 --- a/project/dependency-reduced-pom.xml +++ b/project/dependency-reduced-pom.xml @@ -1,22 +1,25 @@ 4.0.0 - com.university - university-rank-crawler - 1.0-SNAPSHOT + com.example + crawler-project + crawler-project + 1.0.0 + Java爬虫项目 - MVC + Command + Strategy模式 maven-compiler-plugin 3.11.0 - 11 - 11 + ${java.version} + ${java.version} + ${project.build.sourceEncoding} maven-shade-plugin - 3.5.1 + 3.5.0 package @@ -26,26 +29,36 @@ - com.university.Main + com.example.crawler.Main - - org.codehaus.mojo - exec-maven-plugin - 3.1.1 - - com.university.Main - - + + + junit + junit + 4.13.2 + test + + + hamcrest-core + org.hamcrest + + + + - 11 + 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 new file mode 100644 index 0000000..e715ee6 --- /dev/null +++ b/project/output/books_20260530_190333.json @@ -0,0 +1,3602 @@ +[ + { + "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 new file mode 100644 index 0000000..f10e931 --- /dev/null +++ b/project/output/news_20260530_190333.json @@ -0,0 +1,82 @@ +[ + { + "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 new file mode 100644 index 0000000..a6f98a4 --- /dev/null +++ b/project/output/university_ranking_20260530_190333.json @@ -0,0 +1,212 @@ +[ + { + "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 new file mode 100644 index 0000000..537f65e --- /dev/null +++ b/project/output/weather_20260530_190333.json @@ -0,0 +1,335 @@ +[ + { + "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 index 5634cbb..32ea206 100644 --- a/project/pom.xml +++ b/project/pom.xml @@ -1,72 +1,80 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.university - university-rank-crawler - 1.0-SNAPSHOT - jar + 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 - 1.16.2 + ${jsoup.version} - + + + com.google.code.gson + gson + ${gson.version} + + + org.jfree jfreechart - 1.5.3 + ${jfreechart.version} - + - com.opencsv - opencsv - 5.8 + ch.qos.logback + logback-classic + ${logback.version} - + - org.slf4j - slf4j-simple - 2.0.9 + junit + junit + 4.13.2 + test - org.apache.maven.plugins maven-compiler-plugin 3.11.0 - 11 - 11 + ${java.version} + ${java.version} + ${project.build.sourceEncoding} - - org.apache.maven.plugins maven-shade-plugin - 3.5.1 + 3.5.0 package @@ -76,23 +84,13 @@ - com.university.Main + com.example.crawler.Main - - - - org.codehaus.mojo - exec-maven-plugin - 3.1.1 - - com.university.Main - - diff --git a/project/reports/book_analysis_report.txt b/project/reports/book_analysis_report.txt new file mode 100644 index 0000000..943ced3 --- /dev/null +++ b/project/reports/book_analysis_report.txt @@ -0,0 +1,14 @@ +========== 书籍数据分析报告 ========== +生成时间: 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 new file mode 100644 index 0000000..d1794d6 --- /dev/null +++ b/project/reports/news_analysis_report.txt @@ -0,0 +1,31 @@ +========== 新闻数据分析报告 ========== +生成时间: 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 new file mode 100644 index 0000000..7636b13 --- /dev/null +++ b/project/reports/ranking_analysis_report.txt @@ -0,0 +1,17 @@ +========== 大学排名数据分析报告 ========== +生成时间: 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 new file mode 100644 index 0000000..247e060 --- /dev/null +++ b/project/reports/weather_analysis_report.txt @@ -0,0 +1,29 @@ +========== 天气数据分析报告 ========== +生成时间: 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 new file mode 100644 index 0000000..09b9ddd --- /dev/null +++ b/project/src/main/java/com/example/crawler/Main.java @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..3985f59 --- /dev/null +++ b/project/src/main/java/com/example/crawler/chart/ChartGenerator.java @@ -0,0 +1,229 @@ +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 new file mode 100644 index 0000000..743d5b7 --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/BaseCrawlCommand.java @@ -0,0 +1,60 @@ +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 new file mode 100644 index 0000000..5d6df8a --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/BookCommand.java @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000..4c804a7 --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/Command.java @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..d54f6b8 --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/CrawlAllCommand.java @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000..0174398 --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/CrawlAndAnalyzeAllCommand.java @@ -0,0 +1,104 @@ +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 new file mode 100644 index 0000000..f9cfc73 --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/CrawlRankingCommand.java @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000..8085f2b --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/ExitCommand.java @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..0ed7164 --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/GenerateAllAnalysisCommand.java @@ -0,0 +1,77 @@ +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 new file mode 100644 index 0000000..1250d1a --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/NewsCommand.java @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000..f6e606a --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/SaveCommand.java @@ -0,0 +1,74 @@ +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 new file mode 100644 index 0000000..7a3f7a6 --- /dev/null +++ b/project/src/main/java/com/example/crawler/command/WeatherCommand.java @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000..841ce75 --- /dev/null +++ b/project/src/main/java/com/example/crawler/constant/CrawlerConstants.java @@ -0,0 +1,31 @@ +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 new file mode 100644 index 0000000..cedc475 --- /dev/null +++ b/project/src/main/java/com/example/crawler/controller/CrawlerController.java @@ -0,0 +1,90 @@ +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 new file mode 100644 index 0000000..dad0237 --- /dev/null +++ b/project/src/main/java/com/example/crawler/exception/CrawlException.java @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..6d26120 --- /dev/null +++ b/project/src/main/java/com/example/crawler/exception/DataSaveException.java @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..ecc898e --- /dev/null +++ b/project/src/main/java/com/example/crawler/exception/NetworkException.java @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..ecae303 --- /dev/null +++ b/project/src/main/java/com/example/crawler/exception/ParseException.java @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..c58fe68 --- /dev/null +++ b/project/src/main/java/com/example/crawler/model/Book.java @@ -0,0 +1,65 @@ +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 new file mode 100644 index 0000000..d5a5c21 --- /dev/null +++ b/project/src/main/java/com/example/crawler/model/News.java @@ -0,0 +1,54 @@ +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 new file mode 100644 index 0000000..bde80a7 --- /dev/null +++ b/project/src/main/java/com/example/crawler/model/UniversityRank.java @@ -0,0 +1,76 @@ +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 new file mode 100644 index 0000000..dbc6ccc --- /dev/null +++ b/project/src/main/java/com/example/crawler/model/Weather.java @@ -0,0 +1,140 @@ +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 new file mode 100644 index 0000000..e76e5bc --- /dev/null +++ b/project/src/main/java/com/example/crawler/repository/DataRepository.java @@ -0,0 +1,75 @@ +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 new file mode 100644 index 0000000..e1aebaf --- /dev/null +++ b/project/src/main/java/com/example/crawler/service/BookAnalysisService.java @@ -0,0 +1,171 @@ +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 new file mode 100644 index 0000000..13b68cf --- /dev/null +++ b/project/src/main/java/com/example/crawler/service/NewsAnalysisService.java @@ -0,0 +1,138 @@ +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 new file mode 100644 index 0000000..1259e40 --- /dev/null +++ b/project/src/main/java/com/example/crawler/service/RankingAnalysisService.java @@ -0,0 +1,189 @@ +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 new file mode 100644 index 0000000..e91a0a8 --- /dev/null +++ b/project/src/main/java/com/example/crawler/service/WeatherAnalysisService.java @@ -0,0 +1,163 @@ +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 new file mode 100644 index 0000000..746b57e --- /dev/null +++ b/project/src/main/java/com/example/crawler/strategy/BookCrawlStrategy.java @@ -0,0 +1,127 @@ +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 new file mode 100644 index 0000000..39f8d22 --- /dev/null +++ b/project/src/main/java/com/example/crawler/strategy/CrawlStrategy.java @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..ac30f8e --- /dev/null +++ b/project/src/main/java/com/example/crawler/strategy/NewsCrawlStrategy.java @@ -0,0 +1,151 @@ +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 new file mode 100644 index 0000000..9b56383 --- /dev/null +++ b/project/src/main/java/com/example/crawler/strategy/StrategyFactory.java @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..f4b5f81 --- /dev/null +++ b/project/src/main/java/com/example/crawler/strategy/UniversityRankCrawlStrategy.java @@ -0,0 +1,148 @@ +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 new file mode 100644 index 0000000..6ade78c --- /dev/null +++ b/project/src/main/java/com/example/crawler/strategy/WeatherCrawlStrategy.java @@ -0,0 +1,177 @@ +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 new file mode 100644 index 0000000..32991fd --- /dev/null +++ b/project/src/main/java/com/example/crawler/util/DataCleaner.java @@ -0,0 +1,122 @@ +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 new file mode 100644 index 0000000..774d2e6 --- /dev/null +++ b/project/src/main/java/com/example/crawler/util/HttpUtil.java @@ -0,0 +1,126 @@ +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 new file mode 100644 index 0000000..5b2e929 --- /dev/null +++ b/project/src/main/java/com/example/crawler/util/JsonUtil.java @@ -0,0 +1,95 @@ +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 new file mode 100644 index 0000000..5e72292 --- /dev/null +++ b/project/src/main/java/com/example/crawler/view/CrawlerView.java @@ -0,0 +1,72 @@ +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/src/main/java/com/university/Main.java b/project/src/main/java/com/university/Main.java deleted file mode 100644 index 4e39ddd..0000000 --- a/project/src/main/java/com/university/Main.java +++ /dev/null @@ -1,359 +0,0 @@ -package com.university; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Scanner; - -import com.university.analysis.RankAnalyzer; -import com.university.crawler.UniversityRankCrawler; -import com.university.model.RankChange; -import com.university.model.University; -import com.university.model.UniversityComparison; -import com.university.storage.DataStorage; -import com.university.visualization.ChartGenerator; -import com.university.visualization.ConsoleReporter; - -/** - * 主程序入口 - * 整合所有模块,提供交互式菜单 - */ -public class Main { - - // 核心组件 - private final UniversityRankCrawler crawler; - private final DataStorage storage; - private final RankAnalyzer analyzer; - private final ChartGenerator chartGenerator; - private final ConsoleReporter reporter; - - // 数据缓存 - private Map> dataCache; - private Scanner scanner; - - public Main() { - this.crawler = new UniversityRankCrawler(); - this.storage = new DataStorage(); - this.analyzer = new RankAnalyzer(); - this.chartGenerator = new ChartGenerator(); - this.reporter = new ConsoleReporter(); - this.dataCache = new HashMap<>(); - this.scanner = new Scanner(System.in); - } - - public static void main(String[] args) { - Main app = new Main(); - app.run(); - } - - /** - * 运行主程序 - */ - public void run() { - // 打印欢迎信息 - reporter.printWelcome(); - - // 初始化数据 - initializeData(); - - // 主循环 - boolean running = true; - while (running) { - reporter.printMenu(); - String choice = scanner.nextLine().trim(); - - switch (choice) { - case "1": - showTopN(); - break; - case "2": - showByProvince(); - break; - case "3": - searchUniversity(); - break; - case "4": - showProvinceStatistics(); - break; - case "5": - showScoreStatistics(); - break; - case "6": - showRankChanges(); - break; - case "7": - compareUniversities(); - break; - case "8": - showYearlyTrend(); - break; - case "9": - generateAllCharts(); - break; - case "0": - running = false; - System.out.println("感谢使用,再见!"); - break; - default: - System.out.println("无效选择,请重新输入!"); - } - } - - scanner.close(); - } - - /** - * 初始化数据 - */ - private void initializeData() { - System.out.println("正在初始化数据..."); - - // 爬取2022-2024年的数据 - int[] years = {2022, 2023, 2024}; - - for (int year : years) { - List data; - - // 先尝试从文件读取 - if (storage.dataExists(year)) { - System.out.println("从文件加载 " + year + " 年数据..."); - data = storage.readRawData(year); - } else { - // 文件不存在则爬取 - System.out.println("爬取 " + year + " 年数据..."); - data = crawler.crawlRankings(year); - // 保存到文件 - storage.saveRawData(data, year); - } - - dataCache.put(year, data); - } - - System.out.println("数据初始化完成!\n"); - } - - /** - * 显示Top N - */ - private void showTopN() { - System.out.print("请输入要查看的年份(2022-2024): "); - int year = Integer.parseInt(scanner.nextLine().trim()); - - System.out.print("请输入要查看的数量: "); - int n = Integer.parseInt(scanner.nextLine().trim()); - - List data = dataCache.get(year); - if (data == null) { - System.out.println("该年份数据不存在!"); - return; - } - - List topN = analyzer.getTopN(data, n); - reporter.printUniversityList(topN, year + "年 Top " + n + " 高校"); - - // 生成图表 - chartGenerator.generateTopNBarChart(data, year, n); - } - - /** - * 按省份查看 - */ - private void showByProvince() { - System.out.print("请输入要查看的年份(2022-2024): "); - int year = Integer.parseInt(scanner.nextLine().trim()); - - System.out.print("请输入省份名称: "); - String province = scanner.nextLine().trim(); - - List data = dataCache.get(year); - if (data == null) { - System.out.println("该年份数据不存在!"); - return; - } - - List result = analyzer.getByProvince(data, province); - if (result.isEmpty()) { - System.out.println("该省份没有高校数据!"); - } else { - reporter.printUniversityList(result, year + "年 " + province + " 高校"); - } - } - - /** - * 搜索高校 - */ - private void searchUniversity() { - System.out.print("请输入要查看的年份(2022-2024): "); - int year = Integer.parseInt(scanner.nextLine().trim()); - - System.out.print("请输入搜索关键词: "); - String keyword = scanner.nextLine().trim(); - - List data = dataCache.get(year); - if (data == null) { - System.out.println("该年份数据不存在!"); - return; - } - - List result = analyzer.searchUniversity(data, keyword); - if (result.isEmpty()) { - System.out.println("未找到匹配的高校!"); - } else { - reporter.printUniversityList(result, "搜索结果"); - } - } - - /** - * 显示省份统计 - */ - private void showProvinceStatistics() { - System.out.print("请输入要查看的年份(2022-2024): "); - int year = Integer.parseInt(scanner.nextLine().trim()); - - List data = dataCache.get(year); - if (data == null) { - System.out.println("该年份数据不存在!"); - return; - } - - Map provinceCount = analyzer.countByProvince(data); - reporter.printProvinceStatistics(provinceCount, year + "年 省份分布统计"); - - // 生成图表 - chartGenerator.generateProvincePieChart(provinceCount, year); - } - - /** - * 显示分数统计 - */ - private void showScoreStatistics() { - System.out.print("请输入要查看的年份(2022-2024): "); - int year = Integer.parseInt(scanner.nextLine().trim()); - - List data = dataCache.get(year); - if (data == null) { - System.out.println("该年份数据不存在!"); - return; - } - - RankAnalyzer.ScoreStatistics stats = analyzer.getScoreStatistics(data); - reporter.printScoreStatistics(stats, year + "年 分数统计"); - } - - /** - * 显示排名变化 - */ - private void showRankChanges() { - List changes = analyzer.calculateRankChanges(dataCache); - - // 显示上升最快 - List rising = analyzer.getFastestRising(changes, 5); - reporter.printRankChanges(rising, "排名上升最快 Top 5"); - - // 显示下降最快 - List falling = analyzer.getFastestFalling(changes, 5); - reporter.printRankChanges(falling, "排名下降最快 Top 5"); - - // 生成图表 - if (!rising.isEmpty()) { - chartGenerator.generateRankChangeChart(rising, "排名上升最快", "rank_rising.png"); - } - if (!falling.isEmpty()) { - chartGenerator.generateRankChangeChart(falling, "排名下降最快", "rank_falling.png"); - } - } - - /** - * 对比两所高校 - */ - private void compareUniversities() { - System.out.print("请输入要查看的年份(2022-2024): "); - int year = Integer.parseInt(scanner.nextLine().trim()); - - System.out.print("请输入第一所高校名称: "); - String name1 = scanner.nextLine().trim(); - - System.out.print("请输入第二所高校名称: "); - String name2 = scanner.nextLine().trim(); - - List data = dataCache.get(year); - if (data == null) { - System.out.println("该年份数据不存在!"); - return; - } - - Optional u1 = data.stream() - .filter(u -> u.getName().equals(name1)) - .findFirst(); - Optional u2 = data.stream() - .filter(u -> u.getName().equals(name2)) - .findFirst(); - - if (u1.isPresent() && u2.isPresent()) { - UniversityComparison comparison = analyzer.compareUniversities(u1.get(), u2.get()); - reporter.printComparison(comparison); - } else { - System.out.println("未找到指定的高校!"); - } - } - - /** - * 显示某高校历年趋势 - */ - private void showYearlyTrend() { - System.out.print("请输入高校名称: "); - String name = scanner.nextLine().trim(); - - List history = analyzer.getUniversityHistory(dataCache, name); - - if (history.isEmpty()) { - System.out.println("未找到该高校的数据!"); - } else { - reporter.printYearlyTrend(history, name); - chartGenerator.generateRankTrendLineChart(history, name); - } - } - - /** - * 生成所有图表 - */ - private void generateAllCharts() { - System.out.println("正在生成所有图表..."); - - for (Map.Entry> entry : dataCache.entrySet()) { - int year = entry.getKey(); - List data = entry.getValue(); - - // Top 10 柱状图 - chartGenerator.generateTopNBarChart(data, year, 10); - - // 省份分布饼图 - Map provinceCount = analyzer.countByProvince(data); - chartGenerator.generateProvincePieChart(provinceCount, year); - } - - // 排名变化图 - List changes = analyzer.calculateRankChanges(dataCache); - List rising = analyzer.getFastestRising(changes, 10); - List falling = analyzer.getFastestFalling(changes, 10); - - if (!rising.isEmpty()) { - chartGenerator.generateRankChangeChart(rising, "排名上升最快", "rank_rising.png"); - } - if (!falling.isEmpty()) { - chartGenerator.generateRankChangeChart(falling, "排名下降最快", "rank_falling.png"); - } - - // 为Top 5高校生成历年趋势折线图 - List topUniversities = analyzer.getTopN(dataCache.get(2024), 5); - for (University u : topUniversities) { - List history = analyzer.getUniversityHistory(dataCache, u.getName()); - if (!history.isEmpty()) { - chartGenerator.generateRankTrendLineChart(history, u.getName()); - } - } - - System.out.println("所有图表生成完成!\n"); - } -} diff --git a/project/src/main/java/com/university/analysis/RankAnalyzer.java b/project/src/main/java/com/university/analysis/RankAnalyzer.java deleted file mode 100644 index 3627576..0000000 --- a/project/src/main/java/com/university/analysis/RankAnalyzer.java +++ /dev/null @@ -1,250 +0,0 @@ -package com.university.analysis; - -import com.university.model.RankChange; -import com.university.model.University; -import com.university.model.UniversityComparison; - -import java.util.*; -import java.util.stream.Collectors; - -/** - * 排名分析类 - * 提供各种数据分析功能 - */ -public class RankAnalyzer { - - /** - * 获取Top N高校 - * - * @param universities 高校列表 - * @param n 数量 - * @return Top N高校列表 - */ - public List getTopN(List universities, int n) { - return universities.stream() - .sorted(Comparator.comparingInt(University::getRank)) - .limit(n) - .collect(Collectors.toList()); - } - - /** - * 按省份统计高校数量 - * - * @param universities 高校列表 - * @return 省份-数量映射 - */ - public Map countByProvince(List universities) { - return universities.stream() - .collect(Collectors.groupingBy( - University::getProvince, - Collectors.counting() - )); - } - - /** - * 按省份统计平均分 - * - * @param universities 高校列表 - * @return 省份-平均分映射 - */ - public Map averageScoreByProvince(List universities) { - return universities.stream() - .collect(Collectors.groupingBy( - University::getProvince, - Collectors.averagingDouble(University::getScore) - )); - } - - /** - * 获取指定省份的高校 - * - * @param universities 高校列表 - * @param province 省份 - * @return 该省份的高校列表 - */ - public List getByProvince(List universities, String province) { - return universities.stream() - .filter(u -> u.getProvince().equals(province)) - .sorted(Comparator.comparingInt(University::getRank)) - .collect(Collectors.toList()); - } - - /** - * 搜索高校 - * - * @param universities 高校列表 - * @param keyword 关键词 - * @return 匹配的高校列表 - */ - public List searchUniversity(List universities, String keyword) { - return universities.stream() - .filter(u -> u.getName().contains(keyword)) - .collect(Collectors.toList()); - } - - /** - * 获取分数统计信息 - * - * @param universities 高校列表 - * @return 统计信息 - */ - public ScoreStatistics getScoreStatistics(List universities) { - DoubleSummaryStatistics stats = universities.stream() - .mapToDouble(University::getScore) - .summaryStatistics(); - - return new ScoreStatistics( - stats.getCount(), - stats.getSum(), - stats.getAverage(), - stats.getMax(), - stats.getMin() - ); - } - - /** - * 计算历年排名变化 - * - * @param dataMap 多年数据映射(年份->高校列表) - * @return 排名变化列表 - */ - public List calculateRankChanges(Map> dataMap) { - List changes = new ArrayList<>(); - - // 获取所有年份并排序 - List years = new ArrayList<>(dataMap.keySet()); - Collections.sort(years); - - if (years.size() < 2) { - return changes; - } - - int startYear = years.get(0); - int endYear = years.get(years.size() - 1); - - List startData = dataMap.get(startYear); - List endData = dataMap.get(endYear); - - // 创建名称到高校的映射 - Map startMap = startData.stream() - .collect(Collectors.toMap(University::getName, u -> u)); - Map endMap = endData.stream() - .collect(Collectors.toMap(University::getName, u -> u)); - - // 计算每所高校的变化 - for (String name : startMap.keySet()) { - if (endMap.containsKey(name)) { - University startUni = startMap.get(name); - University endUni = endMap.get(name); - - RankChange change = new RankChange( - name, - startYear, - endYear, - startUni.getRank(), - endUni.getRank(), - startUni.getScore(), - endUni.getScore() - ); - changes.add(change); - } - } - - return changes; - } - - /** - * 获取排名上升最快的高校 - * - * @param changes 排名变化列表 - * @param n 数量 - * @return 上升最快的高校列表 - */ - public List getFastestRising(List changes, int n) { - return changes.stream() - .filter(c -> c.getRankChange() > 0) // 只取排名上升的 - .sorted(Comparator.comparingInt(RankChange::getRankChange).reversed()) - .limit(n) - .collect(Collectors.toList()); - } - - /** - * 获取排名下降最快的高校 - * - * @param changes 排名变化列表 - * @param n 数量 - * @return 下降最快的高校列表 - */ - public List getFastestFalling(List changes, int n) { - return changes.stream() - .filter(c -> c.getRankChange() < 0) // 只取排名下降的 - .sorted(Comparator.comparingInt(RankChange::getRankChange)) - .limit(n) - .collect(Collectors.toList()); - } - - /** - * 对比两所高校 - * - * @param u1 高校1 - * @param u2 高校2 - * @return 对比结果 - */ - public UniversityComparison compareUniversities(University u1, University u2) { - return new UniversityComparison(u1, u2); - } - - /** - * 获取某高校在多年数据中的信息 - * - * @param dataMap 多年数据映射 - * @param universityName 高校名称 - * @return 该高校历年的信息列表 - */ - public List getUniversityHistory(Map> dataMap, - String universityName) { - List history = new ArrayList<>(); - - for (List yearData : dataMap.values()) { - yearData.stream() - .filter(u -> u.getName().equals(universityName)) - .findFirst() - .ifPresent(history::add); - } - - // 按年份排序 - history.sort(Comparator.comparingInt(University::getYear)); - return history; - } - - /** - * 分数统计信息内部类 - */ - public static class ScoreStatistics { - private final long count; - private final double sum; - private final double average; - private final double max; - private final double min; - - public ScoreStatistics(long count, double sum, double average, double max, double min) { - this.count = count; - this.sum = sum; - this.average = average; - this.max = max; - this.min = min; - } - - public long getCount() { return count; } - public double getSum() { return sum; } - public double getAverage() { return average; } - public double getMax() { return max; } - public double getMin() { return min; } - - @Override - public String toString() { - return String.format("统计信息: 数量=%d, 平均分=%.2f, 最高分=%.2f, 最低分=%.2f", - count, average, max, min); - } - } -} diff --git a/project/src/main/java/com/university/crawler/UniversityRankCrawler.java b/project/src/main/java/com/university/crawler/UniversityRankCrawler.java deleted file mode 100644 index 2257f3a..0000000 --- a/project/src/main/java/com/university/crawler/UniversityRankCrawler.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.university.crawler; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import com.university.model.University; - -/** - * 高校排名爬虫类 - * 负责从网页抓取高校排名数据 - */ -public class UniversityRankCrawler { - - // 请求间隔时间(毫秒),防止请求过快被封 - private static final int REQUEST_DELAY = 1000; - - /** - * 爬取软科中国大学排名数据 - * 分析软科官网HTML结构,提取真实排名数据 - * - * @param year 年份 - * @return 高校列表 - */ - public List crawlRankings(int year) { - List universities = new ArrayList<>(); - - try { - // 软科排名URL - String url = "https://www.shanghairanking.cn/rankings/bcur/" + year; - - System.out.println("正在爬取 " + year + " 年高校排名数据..."); - - // 发送HTTP请求获取网页内容 - Document doc = Jsoup.connect(url) - .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") - .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") - .timeout(15000) - .get(); - - // 分析HTML结构,提取排名数据 - // 找到排名表格 - Elements rows = doc.select("table.rk-table tbody tr"); - - for (Element row : rows) { - Elements cells = row.select("td"); - if (cells.size() >= 5) { - try { - // 提取排名 - String rankText = cells.get(0).text().trim(); - rankText = rankText.replaceAll("[^0-9]", ""); - if (rankText.isEmpty()) continue; - int rank = Integer.parseInt(rankText); - - // 提取学校名称 - String name = cells.get(1).text().trim(); - - // 提取省份 - String province = cells.get(2).text().trim(); - - // 提取总分 - String scoreText = cells.get(4).text().trim(); - scoreText = scoreText.replaceAll("[^0-9.]", ""); - if (scoreText.isEmpty()) continue; - double score = Double.parseDouble(scoreText); - - // 创建高校对象 - University university = new University(rank, name, province, score, year); - universities.add(university); - - // 限制爬取数量,避免请求过多 - if (universities.size() >= 100) break; - } catch (NumberFormatException e) { - // 跳过解析失败的行 - continue; - } - } - } - - // 请求间隔,避免被封 - Thread.sleep(REQUEST_DELAY); - - } catch (IOException e) { - System.err.println("爬取数据失败: " + e.getMessage()); - System.out.println("将使用模拟数据..."); - // 如果爬取失败,使用模拟数据 - universities = generateMockData(year); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - - System.out.println("成功获取 " + universities.size() + " 条数据"); - return universities; - } - - /** - * 爬取多年数据 - * - * @param startYear 开始年份 - * @param endYear 结束年份 - * @return 多年数据集合 - */ - public List> crawlMultipleYears(int startYear, int endYear) { - List> allData = new ArrayList<>(); - - for (int year = startYear; year <= endYear; year++) { - List yearData = crawlRankings(year); - allData.add(yearData); - } - - return allData; - } - - /** - * 生成模拟数据(用于演示) - * 当真实网站无法访问时使用 - */ - private List generateMockData(int year) { - List mockData = new ArrayList<>(); - - // 基础数据,每年的分数略有变化 - double variation = (year - 2022) * 0.5; - - mockData.add(new University(1, "清华大学", "北京", 852.5 + variation, year)); - mockData.add(new University(2, "北京大学", "北京", 848.2 + variation, year)); - mockData.add(new University(3, "浙江大学", "浙江", 822.5 + variation, year)); - mockData.add(new University(4, "上海交通大学", "上海", 815.3 + variation, year)); - mockData.add(new University(5, "复旦大学", "上海", 805.1 + variation, year)); - mockData.add(new University(6, "南京大学", "江苏", 785.6 + variation, year)); - mockData.add(new University(7, "中国科学技术大学", "安徽", 782.4 + variation, year)); - mockData.add(new University(8, "华中科技大学", "湖北", 765.8 + variation, year)); - mockData.add(new University(9, "武汉大学", "湖北", 758.2 + variation, year)); - mockData.add(new University(10, "西安交通大学", "陕西", 752.6 + variation, year)); - mockData.add(new University(11, "中山大学", "广东", 745.3 + variation, year)); - mockData.add(new University(12, "四川大学", "四川", 738.9 + variation, year)); - mockData.add(new University(13, "哈尔滨工业大学", "黑龙江", 732.5 + variation, year)); - mockData.add(new University(14, "北京航空航天大学", "北京", 725.8 + variation, year)); - mockData.add(new University(15, "东南大学", "江苏", 718.4 + variation, year)); - mockData.add(new University(16, "北京理工大学", "北京", 712.6 + variation, year)); - mockData.add(new University(17, "同济大学", "上海", 705.3 + variation, year)); - mockData.add(new University(18, "中国人民大学", "北京", 698.5 + variation, year)); - mockData.add(new University(19, "北京师范大学", "北京", 692.1 + variation, year)); - mockData.add(new University(20, "南开大学", "天津", 685.7 + variation, year)); - - return mockData; - } -} diff --git a/project/src/main/java/com/university/model/RankChange.java b/project/src/main/java/com/university/model/RankChange.java deleted file mode 100644 index f34a83c..0000000 --- a/project/src/main/java/com/university/model/RankChange.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.university.model; - -/** - * 排名变化实体类 - * 用于存储高校历年排名变化信息 - */ -public class RankChange { - - // 学校名称 - private String universityName; - - // 起始年份 - private int startYear; - - // 结束年份 - private int endYear; - - // 起始排名 - private int startRank; - - // 结束排名 - private int endRank; - - // 排名变化(正数表示上升,负数表示下降) - private int rankChange; - - // 起始分数 - private double startScore; - - // 结束分数 - private double endScore; - - // 分数变化 - private double scoreChange; - - public RankChange() { - } - - public RankChange(String universityName, int startYear, int endYear, - int startRank, int endRank, double startScore, double endScore) { - this.universityName = universityName; - this.startYear = startYear; - this.endYear = endYear; - this.startRank = startRank; - this.endRank = endRank; - this.startScore = startScore; - this.endScore = endScore; - - // 计算变化 - this.rankChange = startRank - endRank; // 排名数字变小表示上升 - this.scoreChange = endScore - startScore; - } - - // Getters and Setters - public String getUniversityName() { - return universityName; - } - - public void setUniversityName(String universityName) { - this.universityName = universityName; - } - - public int getStartYear() { - return startYear; - } - - public void setStartYear(int startYear) { - this.startYear = startYear; - } - - public int getEndYear() { - return endYear; - } - - public void setEndYear(int endYear) { - this.endYear = endYear; - } - - public int getStartRank() { - return startRank; - } - - public void setStartRank(int startRank) { - this.startRank = startRank; - } - - public int getEndRank() { - return endRank; - } - - public void setEndRank(int endRank) { - this.endRank = endRank; - } - - public int getRankChange() { - return rankChange; - } - - public void setRankChange(int rankChange) { - this.rankChange = rankChange; - } - - public double getStartScore() { - return startScore; - } - - public void setStartScore(double startScore) { - this.startScore = startScore; - } - - public double getEndScore() { - return endScore; - } - - public void setEndScore(double endScore) { - this.endScore = endScore; - } - - public double getScoreChange() { - return scoreChange; - } - - public void setScoreChange(double scoreChange) { - this.scoreChange = scoreChange; - } - - /** - * 获取变化趋势描述 - */ - public String getTrendDescription() { - if (rankChange > 0) { - return String.format("上升%d位", rankChange); - } else if (rankChange < 0) { - return String.format("下降%d位", Math.abs(rankChange)); - } else { - return "排名不变"; - } - } - - @Override - public String toString() { - return String.format("%s: %d年(第%d名) -> %d年(第%d名), %s", - universityName, startYear, startRank, endYear, endRank, getTrendDescription()); - } -} diff --git a/project/src/main/java/com/university/model/University.java b/project/src/main/java/com/university/model/University.java deleted file mode 100644 index 8f6ea64..0000000 --- a/project/src/main/java/com/university/model/University.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.university.model; - -import java.util.Objects; - -/** - * 高校实体类 (Java Bean) - * 用于封装高校排名数据 - */ -public class University { - - // 排名 - private int rank; - - // 学校名称 - private String name; - - // 所在省份 - private String province; - - // 总分 - private double score; - - // 年份 - private int year; - - // 无参构造方法(必须,用于反射创建对象) - public University() { - } - - // 全参构造方法 - public University(int rank, String name, String province, double score, int year) { - this.rank = rank; - this.name = name; - this.province = province; - this.score = score; - this.year = year; - } - - // Getter和Setter方法 - public int getRank() { - return rank; - } - - public void setRank(int rank) { - this.rank = rank; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getProvince() { - return province; - } - - public void setProvince(String province) { - this.province = province; - } - - public double getScore() { - return score; - } - - public void setScore(double score) { - this.score = score; - } - - public int getYear() { - return year; - } - - public void setYear(int year) { - this.year = year; - } - - /** - * 计算排名变化 - * @param previousRank 往年排名 - * @return 排名变化(正数表示上升,负数表示下降) - */ - public int calculateRankChange(int previousRank) { - return previousRank - this.rank; - } - - /** - * 计算分数变化 - * @param previousScore 往年分数 - * @return 分数变化 - */ - public double calculateScoreChange(double previousScore) { - return this.score - previousScore; - } - - @Override - public String toString() { - return String.format("University{rank=%d, name='%s', province='%s', score=%.2f, year=%d}", - rank, name, province, score, year); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - University that = (University) o; - return rank == that.rank && - Double.compare(that.score, score) == 0 && - year == that.year && - Objects.equals(name, that.name) && - Objects.equals(province, that.province); - } - - @Override - public int hashCode() { - return Objects.hash(rank, name, province, score, year); - } -} diff --git a/project/src/main/java/com/university/model/UniversityComparison.java b/project/src/main/java/com/university/model/UniversityComparison.java deleted file mode 100644 index 022c825..0000000 --- a/project/src/main/java/com/university/model/UniversityComparison.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.university.model; - -/** - * 高校对比实体类 - * 用于存储两所高校的对比信息 - */ -public class UniversityComparison { - - // 第一所高校 - private String universityName1; - - // 第二所高校 - private String universityName2; - - // 年份 - private int year; - - // 高校1排名 - private int rank1; - - // 高校2排名 - private int rank2; - - // 高校1分数 - private double score1; - - // 高校2分数 - private double score2; - - // 高校1省份 - private String province1; - - // 高校2省份 - private String province2; - - // 排名差距 - private int rankGap; - - // 分数差距 - private double scoreGap; - - public UniversityComparison() { - } - - public UniversityComparison(University u1, University u2) { - this.universityName1 = u1.getName(); - this.universityName2 = u2.getName(); - this.year = u1.getYear(); - this.rank1 = u1.getRank(); - this.rank2 = u2.getRank(); - this.score1 = u1.getScore(); - this.score2 = u2.getScore(); - this.province1 = u1.getProvince(); - this.province2 = u2.getProvince(); - - this.rankGap = Math.abs(rank1 - rank2); - this.scoreGap = Math.abs(score1 - score2); - } - - // Getters and Setters - public String getUniversityName1() { - return universityName1; - } - - public void setUniversityName1(String universityName1) { - this.universityName1 = universityName1; - } - - public String getUniversityName2() { - return universityName2; - } - - public void setUniversityName2(String universityName2) { - this.universityName2 = universityName2; - } - - public int getYear() { - return year; - } - - public void setYear(int year) { - this.year = year; - } - - public int getRank1() { - return rank1; - } - - public void setRank1(int rank1) { - this.rank1 = rank1; - } - - public int getRank2() { - return rank2; - } - - public void setRank2(int rank2) { - this.rank2 = rank2; - } - - public double getScore1() { - return score1; - } - - public void setScore1(double score1) { - this.score1 = score1; - } - - public double getScore2() { - return score2; - } - - public void setScore2(double score2) { - this.score2 = score2; - } - - public String getProvince1() { - return province1; - } - - public void setProvince1(String province1) { - this.province1 = province1; - } - - public String getProvince2() { - return province2; - } - - public void setProvince2(String province2) { - this.province2 = province2; - } - - public int getRankGap() { - return rankGap; - } - - public void setRankGap(int rankGap) { - this.rankGap = rankGap; - } - - public double getScoreGap() { - return scoreGap; - } - - public void setScoreGap(double scoreGap) { - this.scoreGap = scoreGap; - } - - /** - * 获取排名较高的高校名称 - */ - public String getHigherRankedUniversity() { - return rank1 < rank2 ? universityName1 : universityName2; - } - - /** - * 获取对比结果描述 - */ - public String getComparisonResult() { - String higherUni = getHigherRankedUniversity(); - return String.format("%d年: %s 排名高于 %s %d位,分数相差 %.2f分", - year, higherUni, - higherUni.equals(universityName1) ? universityName2 : universityName1, - rankGap, scoreGap); - } - - @Override - public String toString() { - return getComparisonResult(); - } -} diff --git a/project/src/main/java/com/university/storage/DataStorage.java b/project/src/main/java/com/university/storage/DataStorage.java deleted file mode 100644 index b7edd2e..0000000 --- a/project/src/main/java/com/university/storage/DataStorage.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.university.storage; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -import com.opencsv.CSVReader; -import com.opencsv.CSVWriter; -import com.opencsv.bean.CsvToBean; -import com.opencsv.bean.CsvToBeanBuilder; -import com.opencsv.bean.StatefulBeanToCsv; -import com.opencsv.bean.StatefulBeanToCsvBuilder; -import com.opencsv.exceptions.CsvDataTypeMismatchException; -import com.opencsv.exceptions.CsvRequiredFieldEmptyException; -import com.opencsv.exceptions.CsvValidationException; -import com.university.model.University; - -/** - * 数据存储类 - * 负责数据的持久化存储(CSV格式) - */ -public class DataStorage { - - // 数据存储目录 - private static final String DATA_DIR = "data"; - - /** - * 构造方法,确保数据目录存在 - */ - public DataStorage() { - File dir = new File(DATA_DIR); - if (!dir.exists()) { - dir.mkdirs(); - } - } - - /** - * 保存高校列表到CSV文件 - * - * @param universities 高校列表 - * @param year 年份 - */ - public void saveToCsv(List universities, int year) { - String filename = DATA_DIR + "/university_rank_" + year + ".csv"; - - try (Writer writer = new OutputStreamWriter( - new FileOutputStream(filename), StandardCharsets.UTF_8)) { - - // 添加BOM,解决Excel中文乱码 - writer.write('\ufeff'); - - // 创建CSV写入器 - StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer) - .withQuotechar('"') - .withSeparator(',') - .withOrderedResults(true) - .build(); - - // 写入数据 - beanToCsv.write(universities); - System.out.println("数据已保存到: " + filename); - - } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) { - System.err.println("保存CSV文件失败: " + e.getMessage()); - } - } - - /** - * 从CSV文件读取高校列表 - * - * @param year 年份 - * @return 高校列表 - */ - public List readFromCsv(int year) { - String filename = DATA_DIR + "/university_rank_" + year + ".csv"; - List universities = new ArrayList<>(); - - try (Reader reader = new InputStreamReader( - new FileInputStream(filename), StandardCharsets.UTF_8)) { - - // 创建CSV读取器 - CsvToBean csvToBean = new CsvToBeanBuilder(reader) - .withType(University.class) - .withIgnoreLeadingWhiteSpace(true) - .build(); - - // 读取数据 - universities = csvToBean.parse(); - System.out.println("从 " + filename + " 读取了 " + universities.size() + " 条数据"); - - } catch (IOException e) { - System.err.println("读取CSV文件失败: " + e.getMessage()); - } - - return universities; - } - - /** - * 保存原始数据(手动控制格式) - * - * @param universities 高校列表 - * @param year 年份 - */ - public void saveRawData(List universities, int year) { - String filename = DATA_DIR + "/university_rank_" + year + ".csv"; - - try (CSVWriter writer = new CSVWriter(new OutputStreamWriter( - new FileOutputStream(filename), StandardCharsets.UTF_8))) { - - // 写入表头 - String[] header = {"排名", "学校名称", "省份", "总分", "年份"}; - writer.writeNext(header); - - // 写入数据 - for (University u : universities) { - String[] row = { - String.valueOf(u.getRank()), - u.getName(), - u.getProvince(), - String.valueOf(u.getScore()), - String.valueOf(u.getYear()) - }; - writer.writeNext(row); - } - - System.out.println("原始数据已保存到: " + filename); - - } catch (IOException e) { - System.err.println("保存原始数据失败: " + e.getMessage()); - } - } - - /** - * 读取原始数据 - * - * @param year 年份 - * @return 高校列表 - */ - public List readRawData(int year) { - String filename = DATA_DIR + "/university_rank_" + year + ".csv"; - List universities = new ArrayList<>(); - - try (CSVReader reader = new CSVReader(new InputStreamReader( - new FileInputStream(filename), StandardCharsets.UTF_8))) { - - // 跳过表头 - reader.readNext(); - - // 读取数据行 - String[] row; - while ((row = reader.readNext()) != null) { - if (row.length >= 5) { - University u = new University(); - u.setRank(Integer.parseInt(row[0].trim())); - u.setName(row[1].trim()); - u.setProvince(row[2].trim()); - u.setScore(Double.parseDouble(row[3].trim())); - u.setYear(Integer.parseInt(row[4].trim())); - universities.add(u); - } - } - - System.out.println("从 " + filename + " 读取了 " + universities.size() + " 条数据"); - - } catch (IOException | CsvValidationException e) { - System.err.println("读取原始数据失败: " + e.getMessage()); - } - - return universities; - } - - /** - * 检查某年份的数据是否存在 - * - * @param year 年份 - * @return 是否存在 - */ - public boolean dataExists(int year) { - File file = new File(DATA_DIR + "/university_rank_" + year + ".csv"); - return file.exists(); - } - - /** - * 删除某年份的数据文件 - * - * @param year 年份 - */ - public void deleteData(int year) { - File file = new File(DATA_DIR + "/university_rank_" + year + ".csv"); - if (file.exists() && file.delete()) { - System.out.println("已删除 " + year + " 年的数据文件"); - } - } -} diff --git a/project/src/main/java/com/university/visualization/ChartGenerator.java b/project/src/main/java/com/university/visualization/ChartGenerator.java deleted file mode 100644 index 439d97f..0000000 --- a/project/src/main/java/com/university/visualization/ChartGenerator.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.university.visualization; - -import com.university.model.RankChange; -import com.university.model.University; -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.PlotOrientation; -import org.jfree.chart.renderer.category.BarRenderer; -import org.jfree.chart.renderer.category.LineAndShapeRenderer; -import org.jfree.data.category.DefaultCategoryDataset; - -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.Map; - -/** - * 图表生成类 - * 使用JFreeChart生成各种统计图表 - */ -public class ChartGenerator { - - // 图表输出目录 - private static final String CHART_DIR = "charts"; - - /** - * 构造方法,确保图表目录存在 - */ - public ChartGenerator() { - File dir = new File(CHART_DIR); - if (!dir.exists()) { - dir.mkdirs(); - } - } - - /** - * 生成Top N高校柱状图 - * - * @param universities 高校列表 - * @param year 年份 - * @param n 数量 - */ - public void generateTopNBarChart(List universities, int year, int n) { - // 创建数据集 - DefaultCategoryDataset dataset = new DefaultCategoryDataset(); - - // 取前N名 - int count = Math.min(n, universities.size()); - for (int i = 0; i < count; i++) { - University u = universities.get(i); - dataset.addValue(u.getScore(), "总分", u.getName()); - } - - // 创建图表 - JFreeChart chart = ChartFactory.createBarChart( - year + "年高校排名Top" + n, // 标题 - "学校", // X轴标签 - "总分", // Y轴标签 - dataset, // 数据集 - PlotOrientation.VERTICAL, // 方向 - true, // 显示图例 - true, // 显示工具提示 - false // 不生成URL - ); - - // 美化图表 - customizeBarChart(chart); - - // 保存图表 - saveChart(chart, "top" + n + "_" + year + ".png"); - } - - /** - * 生成省份分布饼图 - * - * @param provinceCount 省份统计 - * @param year 年份 - */ - public void generateProvincePieChart(Map provinceCount, int year) { - // 创建饼图数据集 - org.jfree.data.general.DefaultPieDataset dataset = - new org.jfree.data.general.DefaultPieDataset<>(); - - // 添加数据 - provinceCount.forEach(dataset::setValue); - - // 创建饼图 - JFreeChart chart = ChartFactory.createPieChart( - year + "年高校省份分布", // 标题 - dataset, // 数据集 - true, // 显示图例 - true, // 显示工具提示 - false // 不生成URL - ); - - // 获取饼图plot并设置标签 - org.jfree.chart.plot.PiePlot plot = (org.jfree.chart.plot.PiePlot) chart.getPlot(); - - // 设置标签格式:省份名称 + 数量 + 百分比 - plot.setLabelGenerator(new org.jfree.chart.labels.StandardPieSectionLabelGenerator( - "{0}: {1}所 ({2})", - java.text.NumberFormat.getIntegerInstance(), - java.text.NumberFormat.getPercentInstance() - )); - - // 设置标签字体 - plot.setLabelFont(new Font("微软雅黑", Font.PLAIN, 12)); - - // 设置标签颜色 - plot.setLabelPaint(Color.BLACK); - - // 设置标签背景 - plot.setLabelBackgroundPaint(new Color(255, 255, 255, 200)); - - // 设置标题字体 - chart.getTitle().setFont(new Font("微软雅黑", Font.BOLD, 16)); - - // 保存图表 - saveChart(chart, "province_distribution_" + year + ".png"); - } - - /** - * 生成历年排名变化折线图 - * - * @param universityHistory 某高校历年数据 - * @param universityName 高校名称 - */ - public void generateRankTrendLineChart(List universityHistory, - String universityName) { - // 创建数据集 - DefaultCategoryDataset dataset = new DefaultCategoryDataset(); - - // 添加数据(注意:排名越小越好,所以取负值让折线图向上表示进步) - for (University u : universityHistory) { - dataset.addValue(u.getRank(), "排名", String.valueOf(u.getYear())); - } - - // 创建图表 - JFreeChart chart = ChartFactory.createLineChart( - universityName + " 历年排名变化", // 标题 - "年份", // X轴标签 - "排名", // Y轴标签 - dataset, // 数据集 - PlotOrientation.VERTICAL, // 方向 - true, // 显示图例 - true, // 显示工具提示 - false // 不生成URL - ); - - // 美化折线图 - customizeLineChart(chart); - - // 保存图表 - saveChart(chart, "rank_trend_" + universityName + ".png"); - } - - /** - * 生成排名变化对比图 - * - * @param changes 排名变化列表 - * @param title 图表标题 - * @param filename 文件名 - */ - public void generateRankChangeChart(List changes, String title, String filename) { - // 创建数据集 - DefaultCategoryDataset dataset = new DefaultCategoryDataset(); - - // 添加数据 - for (RankChange change : changes) { - dataset.addValue(change.getRankChange(), "排名变化", change.getUniversityName()); - } - - // 创建图表 - JFreeChart chart = ChartFactory.createBarChart( - title, - "学校", - "排名变化(位)", - dataset, - PlotOrientation.HORIZONTAL, - true, - true, - false - ); - - // 美化 - customizeBarChart(chart); - - // 保存 - saveChart(chart, filename); - } - - /** - * 生成多高校对比图 - * - * @param universities 高校列表 - * @param year 年份 - */ - public void generateComparisonChart(List universities, int year) { - // 创建数据集 - DefaultCategoryDataset dataset = new DefaultCategoryDataset(); - - // 添加分数数据 - for (University u : universities) { - dataset.addValue(u.getScore(), "总分", u.getName()); - } - - // 创建图表 - JFreeChart chart = ChartFactory.createBarChart( - year + "年高校分数对比", - "学校", - "总分", - dataset, - PlotOrientation.VERTICAL, - true, - true, - false - ); - - customizeBarChart(chart); - saveChart(chart, "comparison_" + year + ".png"); - } - - /** - * 美化柱状图 - */ - private void customizeBarChart(JFreeChart chart) { - CategoryPlot plot = chart.getCategoryPlot(); - - // 设置背景色 - plot.setBackgroundPaint(Color.WHITE); - plot.setRangeGridlinePaint(Color.LIGHT_GRAY); - - // 设置柱状图颜色 - BarRenderer renderer = (BarRenderer) plot.getRenderer(); - renderer.setSeriesPaint(0, new Color(79, 129, 189)); - - // 设置字体 - CategoryAxis domainAxis = plot.getDomainAxis(); - domainAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10)); - domainAxis.setLabelFont(new Font("微软雅黑", Font.BOLD, 12)); - - NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); - rangeAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10)); - rangeAxis.setLabelFont(new Font("微软雅黑", Font.BOLD, 12)); - - // 设置标题字体 - chart.getTitle().setFont(new Font("微软雅黑", Font.BOLD, 16)); - } - - /** - * 美化折线图 - */ - private void customizeLineChart(JFreeChart chart) { - CategoryPlot plot = chart.getCategoryPlot(); - - // 设置背景色 - plot.setBackgroundPaint(Color.WHITE); - plot.setRangeGridlinePaint(Color.LIGHT_GRAY); - - // 设置折线样式 - LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer(); - renderer.setSeriesPaint(0, new Color(79, 129, 189)); - renderer.setSeriesStroke(0, new BasicStroke(2.0f)); - renderer.setSeriesShapesVisible(0, true); - - // 设置字体 - CategoryAxis domainAxis = plot.getDomainAxis(); - domainAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10)); - domainAxis.setLabelFont(new Font("微软雅黑", Font.BOLD, 12)); - - NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); - rangeAxis.setTickLabelFont(new Font("微软雅黑", Font.PLAIN, 10)); - rangeAxis.setLabelFont(new Font("微软雅黑", Font.BOLD, 12)); - - // 设置标题字体 - chart.getTitle().setFont(new Font("微软雅黑", Font.BOLD, 16)); - } - - /** - * 保存图表到文件 - * - * @param chart 图表对象 - * @param filename 文件名 - */ - private void saveChart(JFreeChart chart, String filename) { - try { - File file = new File(CHART_DIR + "/" + filename); - ChartUtils.saveChartAsPNG(file, chart, 800, 600); - System.out.println("图表已保存: " + file.getAbsolutePath()); - } catch (IOException e) { - System.err.println("保存图表失败: " + e.getMessage()); - } - } -} diff --git a/project/src/main/java/com/university/visualization/ConsoleReporter.java b/project/src/main/java/com/university/visualization/ConsoleReporter.java deleted file mode 100644 index d35a9af..0000000 --- a/project/src/main/java/com/university/visualization/ConsoleReporter.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.university.visualization; - -import com.university.analysis.RankAnalyzer; -import com.university.model.RankChange; -import com.university.model.University; -import com.university.model.UniversityComparison; - -import java.util.List; -import java.util.Map; - -/** - * 控制台报表类 - * 格式化输出各种统计结果到控制台 - */ -public class ConsoleReporter { - - /** - * 打印分隔线 - */ - private void printSeparator() { - System.out.println("=".repeat(80)); - } - - /** - * 打印高校列表 - * - * @param universities 高校列表 - * @param title 标题 - */ - public void printUniversityList(List universities, String title) { - printSeparator(); - System.out.println("【" + title + "】"); - printSeparator(); - - // 表头 - System.out.printf("%-6s %-20s %-10s %-10s %-6s%n", - "排名", "学校名称", "省份", "总分", "年份"); - System.out.println("-".repeat(80)); - - // 数据行 - for (University u : universities) { - System.out.printf("%-6d %-20s %-10s %-10.2f %-6d%n", - u.getRank(), - truncate(u.getName(), 20), - u.getProvince(), - u.getScore(), - u.getYear()); - } - - System.out.println(); - } - - /** - * 打印省份统计 - * - * @param provinceCount 省份统计 - * @param title 标题 - */ - public void printProvinceStatistics(Map provinceCount, String title) { - printSeparator(); - System.out.println("【" + title + "】"); - printSeparator(); - - System.out.printf("%-15s %-10s%n", "省份", "高校数量"); - System.out.println("-".repeat(30)); - - // 按数量降序排序 - provinceCount.entrySet().stream() - .sorted(Map.Entry.comparingByValue().reversed()) - .forEach(entry -> System.out.printf("%-15s %-10d%n", - entry.getKey(), entry.getValue())); - - System.out.println(); - } - - /** - * 打印分数统计 - * - * @param statistics 统计信息 - * @param title 标题 - */ - public void printScoreStatistics(RankAnalyzer.ScoreStatistics statistics, String title) { - printSeparator(); - System.out.println("【" + title + "】"); - printSeparator(); - - System.out.printf("高校数量: %d%n", statistics.getCount()); - System.out.printf("平均分数: %.2f%n", statistics.getAverage()); - System.out.printf("最高分数: %.2f%n", statistics.getMax()); - System.out.printf("最低分数: %.2f%n", statistics.getMin()); - System.out.println(); - } - - /** - * 打印排名变化 - * - * @param changes 排名变化列表 - * @param title 标题 - */ - public void printRankChanges(List changes, String title) { - printSeparator(); - System.out.println("【" + title + "】"); - printSeparator(); - - System.out.printf("%-20s %-8s %-8s %-12s %-12s%n", - "学校名称", "起始年", "结束年", "排名变化", "分数变化"); - System.out.println("-".repeat(80)); - - for (RankChange change : changes) { - String rankChangeStr = change.getRankChange() > 0 ? - "↑" + change.getRankChange() : - (change.getRankChange() < 0 ? - "↓" + Math.abs(change.getRankChange()) : - "-"); - - System.out.printf("%-20s %-8d %-8d %-12s %+.2f%n", - truncate(change.getUniversityName(), 20), - change.getStartYear(), - change.getEndYear(), - rankChangeStr, - change.getScoreChange()); - } - - System.out.println(); - } - - /** - * 打印高校对比结果 - * - * @param comparison 对比结果 - */ - public void printComparison(UniversityComparison comparison) { - printSeparator(); - System.out.println("【高校对比分析】"); - printSeparator(); - - System.out.printf("对比年份: %d年%n%n", comparison.getYear()); - - System.out.println("学校信息:"); - System.out.println("-".repeat(50)); - System.out.printf("%-20s %-10s %-10s%n", "学校", "排名", "分数"); - System.out.printf("%-20s %-10d %-10.2f%n", - comparison.getUniversityName1(), - comparison.getRank1(), - comparison.getScore1()); - System.out.printf("%-20s %-10d %-10.2f%n", - comparison.getUniversityName2(), - comparison.getRank2(), - comparison.getScore2()); - - System.out.println(); - System.out.println("对比结果:"); - System.out.println("-".repeat(50)); - System.out.printf("排名领先: %s (领先%d位)%n", - comparison.getHigherRankedUniversity(), - comparison.getRankGap()); - System.out.printf("分数差距: %.2f分%n", comparison.getScoreGap()); - System.out.println(); - } - - /** - * 打印历年趋势 - * - * @param history 历年数据 - * @param name 学校名称 - */ - public void printYearlyTrend(List history, String name) { - printSeparator(); - System.out.println("【" + name + " 历年排名趋势】"); - printSeparator(); - - System.out.printf("%-8s %-8s %-10s%n", "年份", "排名", "分数"); - System.out.println("-".repeat(30)); - - University previous = null; - for (University u : history) { - String trend = ""; - if (previous != null) { - int change = previous.getRank() - u.getRank(); - if (change > 0) { - trend = "↑" + change; - } else if (change < 0) { - trend = "↓" + Math.abs(change); - } else { - trend = "-"; - } - } - - System.out.printf("%-8d %-8d %-10.2f %s%n", - u.getYear(), u.getRank(), u.getScore(), trend); - previous = u; - } - - System.out.println(); - } - - /** - * 打印菜单 - */ - public void printMenu() { - printSeparator(); - System.out.println("【高校排名分析系统】"); - printSeparator(); - System.out.println("1. 查看Top N高校排名"); - System.out.println("2. 按省份查看高校"); - System.out.println("3. 搜索高校"); - System.out.println("4. 查看省份分布统计"); - System.out.println("5. 查看分数统计"); - System.out.println("6. 查看历年排名变化"); - System.out.println("7. 对比两所高校"); - System.out.println("8. 查看某高校历年趋势"); - System.out.println("9. 生成所有图表"); - System.out.println("0. 退出系统"); - printSeparator(); - System.out.print("请选择功能(0-9): "); - } - - /** - * 打印欢迎信息 - */ - public void printWelcome() { - printSeparator(); - System.out.println(" 欢迎使用高校排名分析系统"); - System.out.println(" 本系统提供高校排名数据爬取、分析和可视化功能"); - printSeparator(); - System.out.println(); - } - - /** - * 截断字符串 - * - * @param str 原字符串 - * @param length 最大长度 - * @return 截断后的字符串 - */ - private String truncate(String str, int length) { - if (str == null) return ""; - if (str.length() <= length) return str; - return str.substring(0, length - 3) + "..."; - } -} diff --git a/project/target/classes/com/example/crawler/Main.class b/project/target/classes/com/example/crawler/Main.class new file mode 100644 index 0000000..1497663 Binary files /dev/null and b/project/target/classes/com/example/crawler/Main.class differ diff --git a/project/target/classes/com/example/crawler/chart/ChartGenerator.class b/project/target/classes/com/example/crawler/chart/ChartGenerator.class new file mode 100644 index 0000000..dd49de6 Binary files /dev/null and b/project/target/classes/com/example/crawler/chart/ChartGenerator.class differ diff --git a/project/target/classes/com/example/crawler/command/BaseCrawlCommand.class b/project/target/classes/com/example/crawler/command/BaseCrawlCommand.class new file mode 100644 index 0000000..216241a Binary files /dev/null and b/project/target/classes/com/example/crawler/command/BaseCrawlCommand.class differ diff --git a/project/target/classes/com/example/crawler/command/BookCommand.class b/project/target/classes/com/example/crawler/command/BookCommand.class new file mode 100644 index 0000000..3d0baf1 Binary files /dev/null and b/project/target/classes/com/example/crawler/command/BookCommand.class differ diff --git a/project/target/classes/com/example/crawler/command/Command.class b/project/target/classes/com/example/crawler/command/Command.class new file mode 100644 index 0000000..7bc1ab3 Binary files /dev/null and b/project/target/classes/com/example/crawler/command/Command.class differ diff --git a/project/target/classes/com/example/crawler/command/CrawlAllCommand.class b/project/target/classes/com/example/crawler/command/CrawlAllCommand.class new file mode 100644 index 0000000..eeb9144 Binary files /dev/null and b/project/target/classes/com/example/crawler/command/CrawlAllCommand.class differ diff --git a/project/target/classes/com/example/crawler/command/CrawlAndAnalyzeAllCommand.class b/project/target/classes/com/example/crawler/command/CrawlAndAnalyzeAllCommand.class new file mode 100644 index 0000000..a2db69d Binary files /dev/null and b/project/target/classes/com/example/crawler/command/CrawlAndAnalyzeAllCommand.class differ diff --git a/project/target/classes/com/example/crawler/command/CrawlRankingCommand.class b/project/target/classes/com/example/crawler/command/CrawlRankingCommand.class new file mode 100644 index 0000000..e65a8c4 Binary files /dev/null and b/project/target/classes/com/example/crawler/command/CrawlRankingCommand.class differ diff --git a/project/target/classes/com/example/crawler/command/ExitCommand.class b/project/target/classes/com/example/crawler/command/ExitCommand.class new file mode 100644 index 0000000..903b3a7 Binary files /dev/null and b/project/target/classes/com/example/crawler/command/ExitCommand.class differ diff --git a/project/target/classes/com/example/crawler/command/GenerateAllAnalysisCommand.class b/project/target/classes/com/example/crawler/command/GenerateAllAnalysisCommand.class new file mode 100644 index 0000000..60ef93b Binary files /dev/null and b/project/target/classes/com/example/crawler/command/GenerateAllAnalysisCommand.class differ diff --git a/project/target/classes/com/example/crawler/command/NewsCommand.class b/project/target/classes/com/example/crawler/command/NewsCommand.class new file mode 100644 index 0000000..b7c5106 Binary files /dev/null and b/project/target/classes/com/example/crawler/command/NewsCommand.class differ diff --git a/project/target/classes/com/example/crawler/command/SaveCommand.class b/project/target/classes/com/example/crawler/command/SaveCommand.class new file mode 100644 index 0000000..291183a Binary files /dev/null and b/project/target/classes/com/example/crawler/command/SaveCommand.class differ diff --git a/project/target/classes/com/example/crawler/command/WeatherCommand.class b/project/target/classes/com/example/crawler/command/WeatherCommand.class new file mode 100644 index 0000000..be29d68 Binary files /dev/null and b/project/target/classes/com/example/crawler/command/WeatherCommand.class differ diff --git a/project/target/classes/com/example/crawler/constant/CrawlerConstants.class b/project/target/classes/com/example/crawler/constant/CrawlerConstants.class new file mode 100644 index 0000000..d201e06 Binary files /dev/null and b/project/target/classes/com/example/crawler/constant/CrawlerConstants.class differ diff --git a/project/target/classes/com/example/crawler/controller/CrawlerController.class b/project/target/classes/com/example/crawler/controller/CrawlerController.class new file mode 100644 index 0000000..6fb5ba5 Binary files /dev/null and b/project/target/classes/com/example/crawler/controller/CrawlerController.class differ diff --git a/project/target/classes/com/example/crawler/exception/CrawlException.class b/project/target/classes/com/example/crawler/exception/CrawlException.class new file mode 100644 index 0000000..e48a793 Binary files /dev/null and b/project/target/classes/com/example/crawler/exception/CrawlException.class differ diff --git a/project/target/classes/com/example/crawler/exception/DataSaveException.class b/project/target/classes/com/example/crawler/exception/DataSaveException.class new file mode 100644 index 0000000..dc59472 Binary files /dev/null and b/project/target/classes/com/example/crawler/exception/DataSaveException.class differ diff --git a/project/target/classes/com/example/crawler/exception/NetworkException.class b/project/target/classes/com/example/crawler/exception/NetworkException.class new file mode 100644 index 0000000..2327b22 Binary files /dev/null and b/project/target/classes/com/example/crawler/exception/NetworkException.class differ diff --git a/project/target/classes/com/example/crawler/exception/ParseException.class b/project/target/classes/com/example/crawler/exception/ParseException.class new file mode 100644 index 0000000..180dbca Binary files /dev/null and b/project/target/classes/com/example/crawler/exception/ParseException.class differ diff --git a/project/target/classes/com/example/crawler/model/Book.class b/project/target/classes/com/example/crawler/model/Book.class new file mode 100644 index 0000000..de5aa2b Binary files /dev/null and b/project/target/classes/com/example/crawler/model/Book.class differ diff --git a/project/target/classes/com/example/crawler/model/News.class b/project/target/classes/com/example/crawler/model/News.class new file mode 100644 index 0000000..2f396b6 Binary files /dev/null and b/project/target/classes/com/example/crawler/model/News.class differ diff --git a/project/target/classes/com/example/crawler/model/UniversityRank.class b/project/target/classes/com/example/crawler/model/UniversityRank.class new file mode 100644 index 0000000..39bd8fa Binary files /dev/null and b/project/target/classes/com/example/crawler/model/UniversityRank.class differ diff --git a/project/target/classes/com/example/crawler/model/Weather.class b/project/target/classes/com/example/crawler/model/Weather.class new file mode 100644 index 0000000..223a2d8 Binary files /dev/null and b/project/target/classes/com/example/crawler/model/Weather.class differ diff --git a/project/target/classes/com/example/crawler/repository/DataRepository.class b/project/target/classes/com/example/crawler/repository/DataRepository.class new file mode 100644 index 0000000..9325a6b Binary files /dev/null and b/project/target/classes/com/example/crawler/repository/DataRepository.class differ diff --git a/project/target/classes/com/example/crawler/service/BookAnalysisService.class b/project/target/classes/com/example/crawler/service/BookAnalysisService.class new file mode 100644 index 0000000..3aac772 Binary files /dev/null and b/project/target/classes/com/example/crawler/service/BookAnalysisService.class differ diff --git a/project/target/classes/com/example/crawler/service/NewsAnalysisService.class b/project/target/classes/com/example/crawler/service/NewsAnalysisService.class new file mode 100644 index 0000000..347efc9 Binary files /dev/null and b/project/target/classes/com/example/crawler/service/NewsAnalysisService.class differ diff --git a/project/target/classes/com/example/crawler/service/RankingAnalysisService.class b/project/target/classes/com/example/crawler/service/RankingAnalysisService.class new file mode 100644 index 0000000..fcbac49 Binary files /dev/null and b/project/target/classes/com/example/crawler/service/RankingAnalysisService.class differ diff --git a/project/target/classes/com/example/crawler/service/WeatherAnalysisService.class b/project/target/classes/com/example/crawler/service/WeatherAnalysisService.class new file mode 100644 index 0000000..c6cd28c Binary files /dev/null and b/project/target/classes/com/example/crawler/service/WeatherAnalysisService.class differ diff --git a/project/target/classes/com/example/crawler/strategy/BookCrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/BookCrawlStrategy.class new file mode 100644 index 0000000..c00eb38 Binary files /dev/null and b/project/target/classes/com/example/crawler/strategy/BookCrawlStrategy.class differ diff --git a/project/target/classes/com/example/crawler/strategy/CrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/CrawlStrategy.class new file mode 100644 index 0000000..dd7a055 Binary files /dev/null and b/project/target/classes/com/example/crawler/strategy/CrawlStrategy.class differ diff --git a/project/target/classes/com/example/crawler/strategy/NewsCrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/NewsCrawlStrategy.class new file mode 100644 index 0000000..d87e85e Binary files /dev/null and b/project/target/classes/com/example/crawler/strategy/NewsCrawlStrategy.class differ diff --git a/project/target/classes/com/example/crawler/strategy/StrategyFactory.class b/project/target/classes/com/example/crawler/strategy/StrategyFactory.class new file mode 100644 index 0000000..7484a28 Binary files /dev/null and b/project/target/classes/com/example/crawler/strategy/StrategyFactory.class differ diff --git a/project/target/classes/com/example/crawler/strategy/UniversityRankCrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/UniversityRankCrawlStrategy.class new file mode 100644 index 0000000..73618c1 Binary files /dev/null and b/project/target/classes/com/example/crawler/strategy/UniversityRankCrawlStrategy.class differ diff --git a/project/target/classes/com/example/crawler/strategy/WeatherCrawlStrategy.class b/project/target/classes/com/example/crawler/strategy/WeatherCrawlStrategy.class new file mode 100644 index 0000000..aaad47c Binary files /dev/null and b/project/target/classes/com/example/crawler/strategy/WeatherCrawlStrategy.class differ diff --git a/project/target/classes/com/example/crawler/util/DataCleaner.class b/project/target/classes/com/example/crawler/util/DataCleaner.class new file mode 100644 index 0000000..4fedbe3 Binary files /dev/null and b/project/target/classes/com/example/crawler/util/DataCleaner.class differ diff --git a/project/target/classes/com/example/crawler/util/HttpUtil.class b/project/target/classes/com/example/crawler/util/HttpUtil.class new file mode 100644 index 0000000..a0e5905 Binary files /dev/null and b/project/target/classes/com/example/crawler/util/HttpUtil.class differ diff --git a/project/target/classes/com/example/crawler/util/JsonUtil.class b/project/target/classes/com/example/crawler/util/JsonUtil.class new file mode 100644 index 0000000..b8af89d Binary files /dev/null and b/project/target/classes/com/example/crawler/util/JsonUtil.class differ diff --git a/project/target/classes/com/example/crawler/view/CrawlerView.class b/project/target/classes/com/example/crawler/view/CrawlerView.class new file mode 100644 index 0000000..4d8e326 Binary files /dev/null and b/project/target/classes/com/example/crawler/view/CrawlerView.class differ diff --git a/project/target/classes/com/university/Main.class b/project/target/classes/com/university/Main.class deleted file mode 100644 index 71a146c..0000000 Binary files a/project/target/classes/com/university/Main.class and /dev/null differ diff --git a/project/target/classes/com/university/analysis/RankAnalyzer$ScoreStatistics.class b/project/target/classes/com/university/analysis/RankAnalyzer$ScoreStatistics.class deleted file mode 100644 index cb7600e..0000000 Binary files a/project/target/classes/com/university/analysis/RankAnalyzer$ScoreStatistics.class and /dev/null differ diff --git a/project/target/classes/com/university/analysis/RankAnalyzer.class b/project/target/classes/com/university/analysis/RankAnalyzer.class deleted file mode 100644 index 735ad1b..0000000 Binary files a/project/target/classes/com/university/analysis/RankAnalyzer.class and /dev/null differ diff --git a/project/target/classes/com/university/crawler/UniversityRankCrawler.class b/project/target/classes/com/university/crawler/UniversityRankCrawler.class deleted file mode 100644 index 9828189..0000000 Binary files a/project/target/classes/com/university/crawler/UniversityRankCrawler.class and /dev/null differ diff --git a/project/target/classes/com/university/model/RankChange.class b/project/target/classes/com/university/model/RankChange.class deleted file mode 100644 index b69584d..0000000 Binary files a/project/target/classes/com/university/model/RankChange.class and /dev/null differ diff --git a/project/target/classes/com/university/model/University.class b/project/target/classes/com/university/model/University.class deleted file mode 100644 index 4e29518..0000000 Binary files a/project/target/classes/com/university/model/University.class and /dev/null differ diff --git a/project/target/classes/com/university/model/UniversityComparison.class b/project/target/classes/com/university/model/UniversityComparison.class deleted file mode 100644 index 3dd0bba..0000000 Binary files a/project/target/classes/com/university/model/UniversityComparison.class and /dev/null differ diff --git a/project/target/classes/com/university/storage/DataStorage.class b/project/target/classes/com/university/storage/DataStorage.class deleted file mode 100644 index 26e7b4d..0000000 Binary files a/project/target/classes/com/university/storage/DataStorage.class and /dev/null differ diff --git a/project/target/classes/com/university/visualization/ChartGenerator.class b/project/target/classes/com/university/visualization/ChartGenerator.class deleted file mode 100644 index 82ea901..0000000 Binary files a/project/target/classes/com/university/visualization/ChartGenerator.class and /dev/null differ diff --git a/project/target/classes/com/university/visualization/ConsoleReporter.class b/project/target/classes/com/university/visualization/ConsoleReporter.class deleted file mode 100644 index 1b55633..0000000 Binary files a/project/target/classes/com/university/visualization/ConsoleReporter.class and /dev/null differ diff --git a/project/target/maven-archiver/pom.properties b/project/target/maven-archiver/pom.properties deleted file mode 100644 index bec0a4e..0000000 --- a/project/target/maven-archiver/pom.properties +++ /dev/null @@ -1,3 +0,0 @@ -artifactId=university-rank-crawler -groupId=com.university -version=1.0-SNAPSHOT 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 index 1b4a93f..784a2d4 100644 --- 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 @@ -1,10 +1,27 @@ -com\university\analysis\RankAnalyzer.class -com\university\model\University.class -com\university\storage\DataStorage.class -com\university\Main.class -com\university\model\UniversityComparison.class -com\university\analysis\RankAnalyzer$ScoreStatistics.class -com\university\visualization\ChartGenerator.class -com\university\crawler\UniversityRankCrawler.class -com\university\model\RankChange.class -com\university\visualization\ConsoleReporter.class +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 index 4b2be80..f0e9336 100644 --- 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 @@ -1,9 +1,38 @@ -D:\javatrae\src\main\java\com\university\analysis\RankAnalyzer.java -D:\javatrae\src\main\java\com\university\model\RankChange.java -D:\javatrae\src\main\java\com\university\Main.java -D:\javatrae\src\main\java\com\university\model\UniversityComparison.java -D:\javatrae\src\main\java\com\university\visualization\ConsoleReporter.java -D:\javatrae\src\main\java\com\university\storage\DataStorage.java -D:\javatrae\src\main\java\com\university\model\University.java -D:\javatrae\src\main\java\com\university\visualization\ChartGenerator.java -D:\javatrae\src\main\java\com\university\crawler\UniversityRankCrawler.java +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 diff --git a/project/target/original-university-rank-crawler-1.0-SNAPSHOT.jar b/project/target/original-university-rank-crawler-1.0-SNAPSHOT.jar deleted file mode 100644 index cbf9a18..0000000 Binary files a/project/target/original-university-rank-crawler-1.0-SNAPSHOT.jar and /dev/null differ diff --git a/project/target/university-rank-crawler-1.0-SNAPSHOT-shaded.jar b/project/target/university-rank-crawler-1.0-SNAPSHOT-shaded.jar deleted file mode 100644 index 5829a6c..0000000 Binary files a/project/target/university-rank-crawler-1.0-SNAPSHOT-shaded.jar and /dev/null differ diff --git a/project/target/university-rank-crawler-1.0-SNAPSHOT.jar b/project/target/university-rank-crawler-1.0-SNAPSHOT.jar deleted file mode 100644 index 5829a6c..0000000 Binary files a/project/target/university-rank-crawler-1.0-SNAPSHOT.jar and /dev/null differ