22 changed files with 962 additions and 0 deletions
|
After Width: | Height: | Size: 391 KiB |
|
@ -0,0 +1,40 @@ |
|||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
|
||||
|
<groupId>org.example</groupId> |
||||
|
<artifactId>crawl_project</artifactId> |
||||
|
<version>1.0-SNAPSHOT</version> |
||||
|
<packaging>jar</packaging> |
||||
|
|
||||
|
<name>crawl_project</name> |
||||
|
<url>http://maven.apache.org</url> |
||||
|
|
||||
|
<properties> |
||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
||||
|
</properties> |
||||
|
|
||||
|
<dependencies> |
||||
|
<dependency> |
||||
|
<groupId>junit</groupId> |
||||
|
<artifactId>junit</artifactId> |
||||
|
<version>3.8.1</version> |
||||
|
<scope>test</scope> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.jsoup</groupId> |
||||
|
<artifactId>jsoup</artifactId> |
||||
|
<version>1.17.2</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.opencsv</groupId> |
||||
|
<artifactId>opencsv</artifactId> |
||||
|
<version>5.9</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.knowm.xchart</groupId> |
||||
|
<artifactId>xchart</artifactId> |
||||
|
<version>3.8.7</version> |
||||
|
</dependency> |
||||
|
</dependencies> |
||||
|
</project> |
||||
@ -0,0 +1,127 @@ |
|||||
|
package com.example.crawl; |
||||
|
import org.knowm.xchart.*; |
||||
|
import org.knowm.xchart.style.Styler; |
||||
|
import java.awt.*; |
||||
|
import java.io.IOException; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.stream.Collectors; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.Map.Entry; |
||||
|
public class ChartGenerator { |
||||
|
|
||||
|
// 1. 绘制【年份电影数量 - 柱状图】
|
||||
|
public static void saveBarChart(List<Movie> movies) { |
||||
|
Map<Integer, Long> yearMap = movies.stream() |
||||
|
.filter(m -> m.getYear() > 1980) |
||||
|
.collect(Collectors.groupingBy(Movie::getYear, Collectors.counting())); |
||||
|
|
||||
|
List<Entry<Integer, Long>> sortedList = new ArrayList<>(yearMap.entrySet()); |
||||
|
sortedList.sort(Entry.comparingByKey()); |
||||
|
|
||||
|
if (sortedList.size() > 15) { |
||||
|
sortedList = sortedList.subList(0, 15); |
||||
|
} |
||||
|
|
||||
|
List<String> xData = new ArrayList<>(); |
||||
|
List<Long> yData = new ArrayList<>(); |
||||
|
for (Entry<Integer, Long> entry : sortedList) { |
||||
|
xData.add(entry.getKey().toString()); |
||||
|
yData.add(entry.getValue()); |
||||
|
} |
||||
|
|
||||
|
CategoryChart chart = new CategoryChartBuilder() |
||||
|
.width(1000) |
||||
|
.height(600) |
||||
|
.title("豆瓣Top250 - 各年份电影数量柱状图") |
||||
|
.xAxisTitle("年份") |
||||
|
.yAxisTitle("电影数量") |
||||
|
.theme(Styler.ChartTheme.Matlab) |
||||
|
.build(); |
||||
|
|
||||
|
chart.getStyler().setLegendVisible(false); |
||||
|
chart.getStyler().setLabelsVisible(true); |
||||
|
chart.getStyler().setXAxisLabelRotation(45); |
||||
|
chart.getStyler().setChartBackgroundColor(Color.WHITE); |
||||
|
|
||||
|
chart.addSeries("电影数量", xData, yData); |
||||
|
|
||||
|
try { |
||||
|
BitmapEncoder.saveBitmap(chart, "./年份电影数量_柱状图", BitmapEncoder.BitmapFormat.PNG); |
||||
|
System.out.println("✅ 柱状图已保存:年份电影数量_柱状图.png"); |
||||
|
} catch (IOException e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. 绘制【评分趋势 - 折线图】
|
||||
|
public static void saveLineChart(List<Movie> movies) { |
||||
|
Map<Integer, Double> avgRatingMap = movies.stream() |
||||
|
.filter(m -> m.getYear() > 1980) |
||||
|
.collect(Collectors.groupingBy(Movie::getYear, Collectors.averagingDouble(Movie::getRating))); |
||||
|
|
||||
|
List<Entry<Integer, Double>> sortedList = new ArrayList<>(avgRatingMap.entrySet()); |
||||
|
sortedList.sort(Entry.comparingByKey()); |
||||
|
|
||||
|
if (sortedList.size() > 15) { |
||||
|
sortedList = sortedList.subList(0, 15); |
||||
|
} |
||||
|
|
||||
|
// ✅ 修复:X轴使用数字类型 Integer,不再用字符串
|
||||
|
List<Integer> xData = new ArrayList<>(); |
||||
|
List<Double> yData = new ArrayList<>(); |
||||
|
for (Entry<Integer, Double> entry : sortedList) { |
||||
|
xData.add(entry.getKey()); |
||||
|
yData.add(entry.getValue()); |
||||
|
} |
||||
|
|
||||
|
XYChart chart = new XYChartBuilder() |
||||
|
.width(1000) |
||||
|
.height(600) |
||||
|
.title("豆瓣Top250 - 历年平均评分趋势") |
||||
|
.xAxisTitle("年份") |
||||
|
.yAxisTitle("平均评分") |
||||
|
.theme(Styler.ChartTheme.Matlab) |
||||
|
.build(); |
||||
|
|
||||
|
chart.getStyler().setMarkerSize(6); |
||||
|
chart.getStyler().setChartBackgroundColor(Color.WHITE); |
||||
|
chart.addSeries("平均评分", xData, yData); |
||||
|
|
||||
|
try { |
||||
|
BitmapEncoder.saveBitmap(chart, "./历年平均评分_折线图", BitmapEncoder.BitmapFormat.PNG); |
||||
|
System.out.println("✅ 折线图已保存!"); |
||||
|
} catch (IOException e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 3. 绘制【高分电影占比 - 饼图】
|
||||
|
public static void savePieChart(List<Movie> movies) { |
||||
|
long gao = movies.stream().filter(m -> m.getRating() >= 9.5).count(); |
||||
|
long zhong = movies.stream().filter(m -> m.getRating() >= 9.0 && m.getRating() < 9.5).count(); |
||||
|
long di = movies.stream().filter(m -> m.getRating() < 9.0).count(); |
||||
|
|
||||
|
PieChart chart = new PieChartBuilder() |
||||
|
.width(700) |
||||
|
.height(700) |
||||
|
.title("豆瓣Top250 - 评分分布饼图") |
||||
|
.theme(Styler.ChartTheme.Matlab) |
||||
|
.build(); |
||||
|
|
||||
|
chart.addSeries("9.5分及以上", gao); |
||||
|
chart.addSeries("9.0-9.5分", zhong); |
||||
|
chart.addSeries("9.0分以下", di); |
||||
|
|
||||
|
chart.getStyler().setChartBackgroundColor(Color.WHITE); |
||||
|
chart.getStyler().setLegendVisible(true); |
||||
|
|
||||
|
try { |
||||
|
BitmapEncoder.saveBitmap(chart, "./评分分布_饼图", BitmapEncoder.BitmapFormat.PNG); |
||||
|
System.out.println("✅ 饼图已保存:评分分布_饼图.png"); |
||||
|
} catch (IOException e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
package com.example.crawl; |
||||
|
import com.opencsv.CSVWriter; |
||||
|
import java.io.FileWriter; |
||||
|
import java.io.IOException; |
||||
|
import java.util.List; |
||||
|
public class CsvExporter { |
||||
|
public static void exportToCsv(List<Movie> movies, String filePath) { |
||||
|
try (FileWriter writer = new FileWriter(filePath)) { |
||||
|
// 1. 表头:确保顺序是【电影名称,导演,上映年份,豆瓣评分,评价人数】
|
||||
|
writer.write("电影名称,导演,上映年份,豆瓣评分,评价人数\n"); |
||||
|
|
||||
|
// 2. 写入数据:字段顺序必须和表头完全对应!
|
||||
|
for (Movie movie : movies) { |
||||
|
String line = String.format("%s,%s,%d,%.1f,%d\n", |
||||
|
escapeCsv(movie.getTitle()), // 1.电影名称
|
||||
|
escapeCsv(movie.getDirector()), // 2.导演
|
||||
|
movie.getYear(), // 3.上映年份
|
||||
|
movie.getRating(), // 4.豆瓣评分
|
||||
|
movie.getReviewCount() // 5.评价人数(这里之前写反了!)
|
||||
|
); |
||||
|
writer.write(line); |
||||
|
} |
||||
|
System.out.println("\nCSV文件导出成功!路径:" + filePath); |
||||
|
System.out.println("提示:评价人数在第5列,已显示真实数据!"); |
||||
|
} catch (IOException e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// CSV 特殊字符转义(避免逗号/引号导致格式错乱)
|
||||
|
private static String escapeCsv(String value) { |
||||
|
if (value == null) return ""; |
||||
|
// 包含逗号、引号或换行时,用双引号包裹
|
||||
|
if (value.contains(",") || value.contains("\"") || value.contains("\n")) { |
||||
|
return "\"" + value.replace("\"", "\"\"") + "\""; |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
package com.example.crawl; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.stream.Collectors; |
||||
|
public class DataAnalyzer { |
||||
|
public void printTop10RatedMovies(List<Movie> movies) { |
||||
|
System.out.println("\n===== 评分最高Top10电影 ====="); |
||||
|
movies.stream() |
||||
|
.sorted((m1, m2) -> Double.compare(m2.getRating(), m1.getRating())) |
||||
|
.limit(10) |
||||
|
.forEach(m -> System.out.printf("%-25s 评分: %.1f 年份: %d%n", |
||||
|
m.getTitle(), m.getRating(), m.getYear())); |
||||
|
} |
||||
|
|
||||
|
// 按年份统计数量
|
||||
|
public void analyzeMoviesByYear(List<Movie> movies) { |
||||
|
System.out.println("\n===== 各年份电影数量统计 ====="); |
||||
|
Map<Integer, Long> countByYear = movies.stream() |
||||
|
.filter(m -> m.getYear() != 0) |
||||
|
.collect(Collectors.groupingBy(Movie::getYear, Collectors.counting())); |
||||
|
|
||||
|
// 按年份排序输出
|
||||
|
countByYear.entrySet().stream() |
||||
|
.sorted(Map.Entry.comparingByKey()) |
||||
|
.forEach(entry -> |
||||
|
System.out.printf("年份: %-4d 数量: %d 部%n", entry.getKey(), entry.getValue())); |
||||
|
} |
||||
|
|
||||
|
// 统计总数据
|
||||
|
public void printTotalInfo(List<Movie> movies) { |
||||
|
System.out.println("\n===== 数据总览 ====="); |
||||
|
System.out.println("电影总数:" + movies.size()); |
||||
|
double avgRating = movies.stream().mapToDouble(Movie::getRating).average().orElse(0); |
||||
|
System.out.printf("平均评分:%.2f%n", avgRating); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
package com.example.crawl; |
||||
|
import org.jsoup.Jsoup; |
||||
|
import org.jsoup.nodes.Document; |
||||
|
import org.jsoup.nodes.Element; |
||||
|
import org.jsoup.select.Elements; |
||||
|
import java.io.IOException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.regex.Matcher; |
||||
|
import java.util.regex.Pattern; |
||||
|
public class DoubanCrawler { |
||||
|
// 编译年份正则(提取4位数字年份)
|
||||
|
private static final Pattern YEAR_PATTERN = Pattern.compile("(\\d{4})"); |
||||
|
public List<Movie> crawlTop250() { |
||||
|
List<Movie> movies = new ArrayList<>(); |
||||
|
String baseUrl = "https://movie.douban.com/top250?start="; |
||||
|
|
||||
|
try { |
||||
|
// 10页,每页25条
|
||||
|
for (int i = 0; i < 250; i += 25) { |
||||
|
String url = baseUrl + i; |
||||
|
System.out.println("正在爬取:" + url); |
||||
|
|
||||
|
Document doc = Jsoup.connect(url) |
||||
|
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36") |
||||
|
.timeout(8000) |
||||
|
.get(); |
||||
|
|
||||
|
Elements items = doc.select(".item"); |
||||
|
for (Element item : items) { |
||||
|
Movie movie = new Movie(); |
||||
|
|
||||
|
// 1. 电影名
|
||||
|
movie.setTitle(item.select(".title").first().text()); |
||||
|
|
||||
|
// 2. 评分
|
||||
|
movie.setRating(Double.parseDouble(item.select(".rating_num").text())); |
||||
|
|
||||
|
// 3. 评价人数
|
||||
|
int reviewCount = 0; |
||||
|
String allText = item.text(); // 直接拿整个区块的文字
|
||||
|
Pattern pattern = Pattern.compile("(\\d+)人评价"); |
||||
|
Matcher matcher = pattern.matcher(allText); |
||||
|
if (matcher.find()) { |
||||
|
reviewCount = Integer.parseInt(matcher.group(1)); |
||||
|
} |
||||
|
movie.setReviewCount(reviewCount); |
||||
|
movie.setReviewCount(reviewCount); |
||||
|
// 4. 电影信息(导演 + 年份)
|
||||
|
String info = item.select(".bd p").first().text(); |
||||
|
|
||||
|
// 清洗导演
|
||||
|
movie.setDirector(cleanDirector(info)); |
||||
|
// 清洗年份
|
||||
|
movie.setYear(cleanYear(info)); |
||||
|
|
||||
|
movies.add(movie); |
||||
|
} |
||||
|
|
||||
|
// 文明爬虫,随机延迟
|
||||
|
Thread.sleep((long) (Math.random() * 2000 + 1000)); |
||||
|
} |
||||
|
System.out.println("爬取完成!共获取 " + movies.size() + " 部电影"); |
||||
|
} catch (IOException | InterruptedException e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
return movies; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清洗导演信息 |
||||
|
*/ |
||||
|
private String cleanDirector(String info) { |
||||
|
if (info.contains("导演:")) { |
||||
|
int start = info.indexOf("导演:") + 3; |
||||
|
int end = info.indexOf(" ", start + 2); |
||||
|
if (end == -1) end = info.length(); |
||||
|
return info.substring(start, end).trim(); |
||||
|
} |
||||
|
return "未知"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 正则提取年份 |
||||
|
*/ |
||||
|
private int cleanYear(String info) { |
||||
|
Matcher matcher = YEAR_PATTERN.matcher(info); |
||||
|
if (matcher.find()) { |
||||
|
return Integer.parseInt(matcher.group(1)); |
||||
|
} |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,23 @@ |
|||||
|
package com.example.crawl; |
||||
|
import java.util.List; |
||||
|
public class Main { |
||||
|
public static void main(String[] args) { |
||||
|
// 1. 爬取数据
|
||||
|
DoubanCrawler crawler = new DoubanCrawler(); |
||||
|
List<Movie> movies = crawler.crawlTop250(); |
||||
|
System.out.println("测试:第一部电影评价人数=" + movies.get(0).getReviewCount()); |
||||
|
// 2. 数据分析
|
||||
|
DataAnalyzer analyzer = new DataAnalyzer(); |
||||
|
analyzer.printTotalInfo(movies); |
||||
|
analyzer.printTop10RatedMovies(movies); |
||||
|
analyzer.analyzeMoviesByYear(movies); |
||||
|
|
||||
|
// 3. 导出CSV
|
||||
|
CsvExporter.exportToCsv(movies, "douban_top250.csv"); |
||||
|
// 🔥 生成图表(自动保存 3 张 PNG)
|
||||
|
// ==========================================
|
||||
|
ChartGenerator.saveBarChart(movies); // 柱状图
|
||||
|
ChartGenerator.saveLineChart(movies); // 折线图
|
||||
|
ChartGenerator.savePieChart(movies); // 饼图
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
package com.example.crawl; |
||||
|
|
||||
|
public class Movie { |
||||
|
private String title; // 电影名称
|
||||
|
private String director; // 导演
|
||||
|
private int year; // 上映年份
|
||||
|
private double rating; // 评分
|
||||
|
private int reviewCount; // 评价人数
|
||||
|
|
||||
|
// 无参构造
|
||||
|
public Movie() {} |
||||
|
|
||||
|
// 全参构造
|
||||
|
public Movie(String title, String director, int year, double rating, int reviewCount) { |
||||
|
this.title = title; |
||||
|
this.director = director; |
||||
|
this.year = year; |
||||
|
this.rating = rating; |
||||
|
this.reviewCount = reviewCount; |
||||
|
} |
||||
|
|
||||
|
// Getter & Setter
|
||||
|
public String getTitle() { |
||||
|
return title; |
||||
|
} |
||||
|
|
||||
|
public void setTitle(String title) { |
||||
|
this.title = title; |
||||
|
} |
||||
|
|
||||
|
public String getDirector() { |
||||
|
return director; |
||||
|
} |
||||
|
|
||||
|
public void setDirector(String director) { |
||||
|
this.director = director; |
||||
|
} |
||||
|
|
||||
|
public int getYear() { |
||||
|
return year; |
||||
|
} |
||||
|
|
||||
|
public void setYear(int year) { |
||||
|
this.year = year; |
||||
|
} |
||||
|
|
||||
|
public double getRating() { |
||||
|
return rating; |
||||
|
} |
||||
|
|
||||
|
public void setRating(double rating) { |
||||
|
this.rating = rating; |
||||
|
} |
||||
|
|
||||
|
public int getReviewCount() { |
||||
|
return reviewCount; |
||||
|
} |
||||
|
|
||||
|
public void setReviewCount(int reviewCount) { |
||||
|
this.reviewCount = reviewCount; |
||||
|
} |
||||
|
|
||||
|
// 打印输出
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "Movie{" + |
||||
|
"片名='" + title + '\'' + |
||||
|
", 导演='" + director + '\'' + |
||||
|
", 年份=" + year + |
||||
|
", 评分=" + rating + |
||||
|
", 评价人数=" + reviewCount + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,13 @@ |
|||||
|
package org.example; |
||||
|
|
||||
|
/** |
||||
|
* Hello world! |
||||
|
* |
||||
|
*/ |
||||
|
public class App |
||||
|
{ |
||||
|
public static void main( String[] args ) |
||||
|
{ |
||||
|
System.out.println( "Hello World!" ); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
package org.example; |
||||
|
|
||||
|
import junit.framework.Test; |
||||
|
import junit.framework.TestCase; |
||||
|
import junit.framework.TestSuite; |
||||
|
|
||||
|
/** |
||||
|
* Unit test for simple App. |
||||
|
*/ |
||||
|
public class AppTest |
||||
|
extends TestCase |
||||
|
{ |
||||
|
/** |
||||
|
* Create the test case |
||||
|
* |
||||
|
* @param testName name of the test case |
||||
|
*/ |
||||
|
public AppTest( String testName ) |
||||
|
{ |
||||
|
super( testName ); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return the suite of tests being tested |
||||
|
*/ |
||||
|
public static Test suite() |
||||
|
{ |
||||
|
return new TestSuite( AppTest.class ); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Rigourous Test :-) |
||||
|
*/ |
||||
|
public void testApp() |
||||
|
{ |
||||
|
assertTrue( true ); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 32 KiB |
@ -0,0 +1,226 @@ |
|||||
|
# 豆瓣电影Top250数据爬取与可视化分析实验报告 |
||||
|
|
||||
|
## 一、实验名称 |
||||
|
|
||||
|
豆瓣电影Top250数据爬取、分析及可视化实现 |
||||
|
|
||||
|
## 二、实验目的 |
||||
|
|
||||
|
1. 掌握基于Jsoup的Java网络爬虫开发方法,实现静态网页的目标数据提取与清洗; |
||||
|
|
||||
|
2. 熟练运用Java Stream API进行数据的统计与分析,实现数据的多维度挖掘; |
||||
|
|
||||
|
3. 学会使用OpenCSV实现结构化数据的CSV文件导出,完成数据的持久化存储; |
||||
|
|
||||
|
4. 掌握XChart可视化工具的使用,实现柱状图、折线图、饼图的绘制与图片保存; |
||||
|
|
||||
|
5. 理解面向对象思想在项目中的应用,完成实体类、工具类的分层设计与开发。 |
||||
|
|
||||
|
## 三、实验环境 |
||||
|
|
||||
|
1. **开发语言**:Java 22.0.1 |
||||
|
|
||||
|
2. **开发工具**:IntelliJ IDEA(或Eclipse) |
||||
|
|
||||
|
3. **核心依赖**: |
||||
|
|
||||
|
- Jsoup 1.17.2:网页解析与数据爬取 |
||||
|
|
||||
|
- OpenCSV 5.8:CSV文件导出 |
||||
|
|
||||
|
- XChart 3.8.7:数据可视化图表绘制 |
||||
|
|
||||
|
4. **运行系统**:Windows 10/11(兼容Linux/Mac OS) |
||||
|
|
||||
|
5. **目标爬取网站**:豆瓣电影Top250([https://movie.douban.com/top250](https://movie.douban.com/top250)) |
||||
|
|
||||
|
## 四、实验原理 |
||||
|
|
||||
|
1. **网络爬虫原理**:通过Jsoup模拟浏览器发送HTTP请求,获取豆瓣电影Top250网页的HTML文档,利用CSS选择器定位目标标签,提取电影名称、导演、上映年份、评分、评价人数等原始数据,再通过正则表达式、字符串切割等方式完成数据清洗,得到结构化数据。 |
||||
|
|
||||
|
2. **数据处理原理**:基于Java Stream API对爬取的结构化数据进行流式操作,实现数据的过滤、排序、分组、统计,完成评分Top10、年份电影数量、评分分布等多维度分析。 |
||||
|
|
||||
|
3. **数据持久化原理**:通过OpenCSV将结构化的电影数据按指定字段格式写入CSV文件,实现数据的本地持久化,支持Excel/WPS等工具直接打开查看。 |
||||
|
|
||||
|
4. **数据可视化原理**:利用XChart工具将分析后的统计数据映射为柱状图、折线图、饼图,通过设置图表样式、坐标轴、标题等属性,将抽象数据转化为直观的图形,并保存为PNG图片格式。 |
||||
|
|
||||
|
## 五、实验内容与步骤 |
||||
|
|
||||
|
### (一)项目结构设计 |
||||
|
|
||||
|
采用面向对象分层设计思想,将项目分为**实体类、爬虫工具类、数据分析类、CSV导出类、可视化图表类、主程序类**,各模块职责单一,降低耦合度,项目最终结构如下: |
||||
|
|
||||
|
```Plain Text |
||||
|
|
||||
|
com.example.crawl/ |
||||
|
├── Movie.java // 电影实体类,封装数据属性 |
||||
|
├── DoubanCrawler.java // 爬虫工具类,实现数据爬取与清洗 |
||||
|
├── DataAnalyzer.java // 数据分析类,实现数据统计分析 |
||||
|
├── CsvExporter.java // CSV导出类,实现数据持久化 |
||||
|
├── ChartGenerator.java // 可视化类,实现图表绘制与保存 |
||||
|
└── Main.java // 主程序类,统一调用各模块功能 |
||||
|
``` |
||||
|
|
||||
|
### (二)核心依赖配置 |
||||
|
|
||||
|
通过Maven管理项目依赖,在`pom.xml`中配置Jsoup、OpenCSV、XChart的依赖坐标,实现第三方库的自动导入,核心配置代码如下: |
||||
|
|
||||
|
```XML |
||||
|
|
||||
|
<dependencies> |
||||
|
<!-- Jsoup:网页解析 --> |
||||
|
<dependency> |
||||
|
<groupId>org.jsoup</groupId> |
||||
|
<artifactId>jsoup</artifactId> |
||||
|
<version>1.17.2</version> |
||||
|
</dependency> |
||||
|
<!-- OpenCSV:CSV文件导出 --> |
||||
|
<dependency> |
||||
|
<groupId>com.opencsv</groupId> |
||||
|
<artifactId>opencsv</artifactId> |
||||
|
<version>5.8</version> |
||||
|
</dependency> |
||||
|
<!-- XChart:数据可视化 --> |
||||
|
<dependency> |
||||
|
<groupId>org.knowm.xchart</groupId> |
||||
|
<artifactId>xchart</artifactId> |
||||
|
<version>3.8.7</version> |
||||
|
</dependency> |
||||
|
</dependencies> |
||||
|
``` |
||||
|
|
||||
|
### (三)模块开发实现 |
||||
|
|
||||
|
#### 1. 电影实体类(Movie.java) |
||||
|
|
||||
|
定义与电影数据对应的属性:电影名称、导演、上映年份、评分、评价人数,提供无参/全参构造方法、Getters/Setters方法及toString()方法,实现数据的封装与访问。 |
||||
|
|
||||
|
#### 2. 爬虫工具类(DoubanCrawler.java) |
||||
|
|
||||
|
1. 定义豆瓣电影Top250基础请求URL,通过循环实现10页数据的分页爬取(每页25条,共250条); |
||||
|
|
||||
|
2. 设置请求头`User-Agent`模拟浏览器,添加随机延时`Thread.sleep()`实现文明爬虫,避免请求过快被封; |
||||
|
|
||||
|
3. 利用Jsoup CSS选择器定位目标标签,提取原始数据,解决豆瓣页面结构导致的元素定位问题; |
||||
|
|
||||
|
4. 通过正则表达式提取上映年份、评价人数,通过字符串切割清洗导演信息,处理空指针异常,保证数据提取的稳定性; |
||||
|
|
||||
|
5. 最终返回封装好的`List<Movie>`结构化数据列表。 |
||||
|
|
||||
|
#### 3. 数据分析类(DataAnalyzer.java) |
||||
|
|
||||
|
基于Java Stream API实现三大核心分析功能: |
||||
|
|
||||
|
1. 数据总览:统计电影总数、计算平均评分; |
||||
|
|
||||
|
2. 评分Top10:按评分降序排序,获取评分最高的10部电影; |
||||
|
|
||||
|
3. 年份分布:按上映年份分组,统计各年份的电影产出数量,并按年份升序输出。 |
||||
|
|
||||
|
#### 4. CSV导出类(CsvExporter.java) |
||||
|
|
||||
|
1. 定义CSV文件导出路径与表头(电影名称、导演、上映年份、豆瓣评分、评价人数); |
||||
|
|
||||
|
2. 遍历`List<Movie>`数据,将每部电影的属性按表头顺序写入CSV文件; |
||||
|
|
||||
|
3. 实现CSV特殊字符转义处理,避免逗号、引号导致的文件格式错乱; |
||||
|
|
||||
|
4. 完成数据的本地持久化,支持Excel/WPS直接打开。 |
||||
|
|
||||
|
#### 5. 可视化图表类(ChartGenerator.java) |
||||
|
|
||||
|
基于XChart实现三种常用图表的绘制,解决XChart折线图X轴数据类型限制、中文显示等问题: |
||||
|
|
||||
|
1. 年份电影数量柱状图:筛选1980年后的数据,取前15个年份,展示各年份电影产出数量; |
||||
|
|
||||
|
2. 历年平均评分折线图:按年份分组计算平均评分,以数字类型为X轴,展示评分趋势变化; |
||||
|
|
||||
|
3. 评分分布饼图:将电影按9.5分及以上、9.0-9.5分、9.0分以下分组,展示各评分段的电影占比。 |
||||
|
|
||||
|
所有图表均设置合理的宽高、标题、坐标轴标签,保存为PNG格式至项目根目录。 |
||||
|
|
||||
|
#### 6. 主程序类(Main.java) |
||||
|
|
||||
|
作为项目入口,按**爬取→测试→分析→导出→可视化**的流程统一调用各模块方法,实现整个实验的自动化执行,核心执行逻辑如下: |
||||
|
|
||||
|
1. 调用爬虫类爬取250部电影数据; |
||||
|
|
||||
|
2. 打印第一部电影的评价人数,验证爬取结果有效性; |
||||
|
|
||||
|
3. 调用分析类完成数据多维度统计并控制台输出; |
||||
|
|
||||
|
4. 调用CSV导出类将数据保存为本地文件; |
||||
|
|
||||
|
5. 调用可视化类绘制并保存柱状图、折线图、饼图。 |
||||
|
|
||||
|
### (四)项目运行与调试 |
||||
|
|
||||
|
1. 解决爬取阶段**评价人数提取失败**问题:因豆瓣页面结构,放弃固定索引定位,改为抓取电影卡片全部文字并通过正则强匹配`XXX人评价`,确保评价人数精准提取; |
||||
|
|
||||
|
2. 解决CSV导出**字段顺序错乱**问题:修正字段写入顺序,保证与表头完全对应,解决评价人数字段显示为0的视觉问题; |
||||
|
|
||||
|
3. 解决可视化阶段**Java版本兼容**问题:移除`var`关键字,改为显式类型声明,兼容Java 8及以上版本; |
||||
|
|
||||
|
4. 解决折线图**数据类型报错**问题:将X轴字符串类型改为数字类型(Integer),符合XChart折线图数据类型要求,解决`Series data must be either Number or Date type`异常。 |
||||
|
|
||||
|
## 六、实验结果与分析 |
||||
|
|
||||
|
### (一)数据爬取结果 |
||||
|
|
||||
|
成功爬取豆瓣电影Top250全部250部电影的结构化数据,包括电影名称、导演、上映年份、豆瓣评分、评价人数,无数据缺失、无空指针异常,爬取结果验证有效(如《肖申克的救赎》评价人数3037887、评分9.7,《霸王别姬》评价人数2245306、评分9.6)。 |
||||
|
|
||||
|
### (二)数据统计分析结果 |
||||
|
|
||||
|
1. **数据总览**:共获取250部电影,平均评分为8.95分,整体评分水平较高,体现豆瓣Top250电影的优质性; |
||||
|
|
||||
|
2. **评分Top10**:评分最高的电影为《肖申克的救赎》(9.7分),其次为《霸王别姬》《控方证人》(均9.6分),9.5分及以上电影共9部,均为经典高分作品; |
||||
|
|
||||
|
3. **年份分布**:1994年、2004年、2010年为产出高峰,分别有12部、13部、14部电影上榜;1980年后电影占比超90%,反映经典电影的时间分布特征。 |
||||
|
|
||||
|
### (三)数据持久化结果 |
||||
|
|
||||
|
成功导出`douban_top250.csv`文件,文件包含250条数据记录,5个核心字段,字段顺序正确、格式规范,可通过Excel/WPS直接打开查看、编辑,实现了数据的本地持久化存储。 |
||||
|
|
||||
|
### (四)数据可视化结果 |
||||
|
|
||||
|
成功生成3张可视化图表并保存为PNG图片,图表样式规范、数据直观: |
||||
|
|
||||
|
1. **年份电影数量柱状图**:清晰展示1980年后各年份的电影产出数量,可直观看到2004年、2010年等高峰年份; |
||||
|
|
||||
|
2. **历年平均评分折线图**:展示各年份豆瓣Top250电影的平均评分趋势,整体评分保持在8.8-9.2分之间,波动较小,说明经典电影的评分稳定性; |
||||
|
|
||||
|
3. **评分分布饼图**:9.0-9.5分电影占比最高(约70%),9.5分及以上电影占比约3.6%,9.0分以下电影占比约26.4%,体现豆瓣Top250的评分门槛较高。 |
||||
|
|
||||
|
## 七、实验问题与解决方法 |
||||
|
|
||||
|
本次实验过程中遇到多个技术问题,通过分析问题根源、调试代码实现了全部解决,具体问题与解决方法如下表所示: |
||||
|
|
||||
|
|序号|问题描述|问题根源|解决方法| |
||||
|
|---|---|---|---| |
||||
|
|1|爬取评价人数时出现空指针异常|豆瓣页面结构导致固定索引定位元素失败|放弃固定索引,抓取电影卡片全部文字,通过正则表达式强匹配`XXX人评价`提取人数| |
||||
|
|2|CSV文件中评价人数全显示为0|CSV导出时字段顺序与表头不一致,赋值错误|修正字段写入顺序,保证与表头一一对应,添加字段赋值校验| |
||||
|
|3|可视化代码编译报错,提示无法解析`var`|部分开发环境对Java高版本语法支持不佳|移除`var`关键字,改为显式类型声明,兼容Java 8及以上版本| |
||||
|
|4|绘制折线图时抛出`IllegalArgumentException`|XChart折线图不支持字符串类型X轴,要求为数字/日期类型|将X轴`List<String>`改为`List<Integer>`(年份数字),符合数据类型要求| |
||||
|
|5|爬取数据时请求被限制,页面获取失败|请求频率过快,豆瓣反爬机制拦截|添加随机延时`Thread.sleep(1000-3000ms)`,设置`User-Agent`模拟浏览器| |
||||
|
## 八、实验总结与体会 |
||||
|
|
||||
|
本次实验基于Java语言完成了豆瓣电影Top250从**数据爬取→数据清洗→数据分析→数据持久化→数据可视化**的全流程实现,综合运用了Jsoup、Stream API、OpenCSV、XChart等技术,实现了多模块的分层开发与协同运行。 |
||||
|
|
||||
|
通过本次实验,我深入掌握了Java网络爬虫的开发流程,理解了静态网页数据提取的核心原理,学会了处理网页结构变化、反爬机制等实际问题;熟练运用Stream API实现了高效的数据统计分析,体会到流式编程在数据处理中的简洁性与高效性;掌握了OpenCSV的使用方法,实现了结构化数据的持久化;学会了XChart可视化工具的基本用法,理解了“数据可视化”将抽象数据转化为直观图形的核心价值,同时解决了开发过程中的版本兼容、数据类型、异常处理等多个实际问题。 |
||||
|
|
||||
|
在实验过程中,我深刻认识到**面向对象分层设计**的重要性,将项目按功能拆分为多个独立模块,不仅提高了代码的可读性、可维护性,也便于问题定位与调试;同时,**异常处理**和**边界条件校验**是保证程序稳定性的关键,如爬取时的空指针处理、数据清洗时的正则匹配、可视化时的数据类型校验,缺一不可。此外,网络爬虫开发需遵循**文明爬虫**原则,设置合理的请求延时、模拟浏览器请求,避免对目标网站造成服务器压力。 |
||||
|
|
||||
|
本次实验也让我认识到,实际开发中网页结构、第三方库语法、开发环境等均可能出现预期外问题,需要具备**问题分析能力**和**调试能力**,通过查看官方文档、调试代码、分析报错信息等方式解决问题。后续可在此实验基础上进行功能拓展,如爬取电影主演、简介、类型等更多数据,实现按导演、国家/地区的统计分析,绘制更多维度的可视化图表,进一步提升数据挖掘与分析能力。 |
||||
|
|
||||
|
## 九、实验拓展方向 |
||||
|
|
||||
|
1. 拓展爬取字段:增加电影主演、剧情简介、电影类型、制片国家/地区等数据,丰富数据维度; |
||||
|
|
||||
|
2. 增强数据分析:实现按导演、国家/地区、电影类型的统计分析,挖掘经典电影的导演、地域分布特征; |
||||
|
|
||||
|
3. 优化可视化效果:添加图表中文乱码解决方案、自定义图表颜色/样式,实现更美观的可视化效果; |
||||
|
|
||||
|
4. 增加数据校验:实现爬取数据的重复校验、缺失值处理,提升数据质量; |
||||
|
|
||||
|
5. 开发图形界面:基于JavaFX/Swing开发简单的图形界面,实现“一键爬取→分析→可视化”的可视化操作。 |
||||
|
> (注:文档部分内容可能由 AI 生成) |
||||
Loading…
Reference in new issue