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