diff --git a/crawl_project/QQ_1774775619275.png b/crawl_project/QQ_1774775619275.png new file mode 100644 index 0000000..6914ef0 Binary files /dev/null and b/crawl_project/QQ_1774775619275.png differ diff --git a/crawl_project/douban_top250.csv b/crawl_project/douban_top250.csv new file mode 100644 index 0000000..4b75194 --- /dev/null +++ b/crawl_project/douban_top250.csv @@ -0,0 +1,251 @@ +电影名称,导演,上映年份,豆瓣评分,评价人数 +肖申克的救赎,弗兰克·德拉邦特,1994,9.7,3272365 +霸王别姬,陈凯歌,1993,9.6,2413917 +泰坦尼克号,詹姆斯·卡梅隆,1997,9.5,2488514 +阿甘正传,罗伯特·泽米吉斯,1994,9.5,2421965 +千与千寻,宫崎骏,2001,9.4,2526875 +美丽人生,罗伯托·贝尼尼,1997,9.5,1477086 +星际穿越,克里斯托弗·诺兰,2014,9.4,2170922 +这个杀手不太冷,吕克·贝松,1994,9.4,2544491 +盗梦空间,克里斯托弗·诺兰,2010,9.4,2316943 +楚门的世界,彼得·威尔,1998,9.4,2009502 +辛德勒的名单,史蒂文·斯皮尔伯格,1993,9.5,1243119 +忠犬八公的故事,莱塞·霍尔斯道姆,2009,9.4,1538386 +海上钢琴师,朱塞佩·托纳多雷,1998,9.3,1873244 +疯狂动物城,拜伦·霍华德,2016,9.3,2308656 +三傻大闹宝莱坞,拉库马·希拉尼,2009,9.2,2071634 +机器人总动员,安德鲁·斯坦顿,2008,9.3,1485974 +放牛班的春天,克里斯托夫·巴拉蒂,2004,9.3,1468523 +无间道,刘伟强,2002,9.3,1558535 +控方证人,比利·怀尔德,1957,9.6,738018 +寻梦环游记,李·昂克里奇,2017,9.1,1978541 +大话西游之大圣娶亲,刘镇伟,1995,9.2,1704325 +熔炉,黄东赫,2011,9.3,1033176 +触不可及,奥利维·那卡什,2011,9.3,1290538 +教父,弗朗西斯·福特·科波拉,1972,9.3,1102884 +末代皇帝,贝纳尔多·贝托鲁奇,1987,9.3,1024956 +哈利·波特与魔法石,Chris,2001,9.2,1416752 +当幸福来敲门,加布里尔·穆奇诺,2006,9.1,1682759 +龙猫,宫崎骏,1988,9.2,1406668 +活着,张艺谋,1994,9.3,966837 +怦然心动,罗伯·莱纳,2010,9.1,2045109 +蝙蝠侠:黑暗骑士,克里斯托弗·诺兰,2008,9.2,1189296 +指环王3:王者无敌,彼得·杰克逊,2003,9.3,907182 +我不是药神,文牧野,2018,9.0,2343826 +乱世佳人,维克多·弗莱明,1939,9.3,792038 +飞屋环游记,彼特·道格特,2009,9.1,1495284 +让子弹飞,姜文,2010,9.0,1914828 +哈尔的移动城堡,宫崎骏,2004,9.1,1285485 +十二怒汉,西德尼·吕美特,1957,9.4,576682 +海蒂和爷爷,阿兰·葛斯彭纳,2015,9.3,776727 +素媛,李濬益,2013,9.3,773789 +猫鼠游戏,史蒂文·斯皮尔伯格,2002,9.1,1206094 +天空之城,宫崎骏,1986,9.2,1005901 +鬼子来了,姜文,2000,9.3,712825 +摔跤吧!爸爸,涅提·蒂瓦里,2016,9.0,1740491 +少年派的奇幻漂流,李安,2012,9.1,1486215 +钢琴家,罗曼·波兰斯基,2002,9.3,747958 +指环王2:双塔奇兵,彼得·杰克逊,2002,9.2,854299 +死亡诗社,彼得·威尔,1989,9.2,877838 +大话西游之月光宝盒,刘镇伟,1995,9.0,1362051 +绿皮书,彼得·法雷里,2018,8.9,1886503 +何以为家,娜丁·拉巴基,2018,9.1,1159929 +闻香识女人,马丁·布莱斯,1992,9.1,1019820 +大闹天宫,万籁鸣,1961,9.4,507985 +黑客帝国,安迪·沃卓斯基,1999,9.1,947353 +指环王1:护戒使者,彼得·杰克逊,2001,9.1,956965 +罗马假日,威廉·惠勒,1953,9.1,1046011 +教父2,弗朗西斯·福特·科波拉,1974,9.3,637710 +狮子王,Roger,1994,9.1,958011 +天堂电影院,朱塞佩·托纳多雷,1988,9.2,739200 +饮食男女,李安,1994,9.2,722844 +辩护人,杨宇硕,2013,9.2,656544 +本杰明·巴顿奇事,大卫·芬奇,2008,9.0,1102393 +搏击俱乐部,大卫·芬奇,1999,9.0,971489 +美丽心灵,朗·霍华德,2001,9.1,851834 +穿条纹睡衣的男孩,马克·赫尔曼,2008,9.2,640270 +情书,岩井俊二,1995,8.9,1321952 +哈利·波特与死亡圣器(下),大卫·叶茨,2011,9.0,968253 +两杆大烟枪,盖·里奇,1998,9.1,676165 +窃听风暴,弗洛里安·亨克尔·冯·多纳斯马尔克,2006,9.2,630626 +音乐之声,罗伯特·怀斯,1965,9.1,682653 +功夫,周星驰,2004,8.9,1332309 +哈利·波特与阿兹卡班的囚徒,阿方索·卡隆,2004,9.0,888522 +西西里的美丽传说,朱塞佩·托纳多雷,2000,8.9,1091124 +阿凡达,詹姆斯·卡梅隆,2009,8.8,1582985 +看不见的客人,奥里奥尔·保罗,2016,8.8,1437350 +拯救大兵瑞恩,史蒂文·斯皮尔伯格,1998,9.1,723281 +沉默的羔羊,乔纳森·戴米,1991,8.9,1014000 +小鞋子,马基德·马基迪,1997,9.2,464572 +布达佩斯大饭店,韦斯·安德森,2014,8.9,1083213 +蝴蝶效应,埃里克·布雷斯,2004,8.9,1065735 +飞越疯人院,米洛斯·福尔曼,1975,9.1,602515 +禁闭岛,Martin,2010,8.9,1110434 +还有明天,宝拉·柯特莱西,2023,9.3,367094 +心灵捕手,格斯·范·桑特,1997,9.0,813722 +致命魔术,克里斯托弗·诺兰,2006,8.9,962744 +低俗小说,昆汀·塔伦蒂诺,1994,8.9,957630 +超脱,托尼·凯耶,2011,9.0,721605 +哈利·波特与密室,Chris,2002,8.9,914825 +一一,杨德昌,2000,9.1,508083 +喜剧之王,周星驰,1999,8.8,1092967 +摩登时代,查理·卓别林,1936,9.3,346350 +杀人回忆,奉俊昊,2003,8.9,835763 +致命ID,詹姆斯·曼高德,2003,8.9,947109 +春光乍泄,王家卫,1997,9.0,708613 +加勒比海盗,戈尔·维宾斯基,2003,8.8,967274 +海豚湾,路易·西霍尤斯,2009,9.3,382141 +美国往事,赛尔乔·莱翁内,1984,9.1,471106 +红辣椒,今敏,2006,9.0,568894 +七宗罪,大卫·芬奇,1995,8.8,1060350 +唐伯虎点秋香,李力持,1993,8.8,1223163 +狩猎,托马斯·温特伯格,2012,9.1,469664 +幽灵公主,宫崎骏,1997,8.9,668862 +甜蜜蜜,陈可辛,1996,8.9,672256 +寄生虫,奉俊昊,2019,8.8,1546942 +天书奇谭,王树忱,1983,9.2,334500 +蝙蝠侠:黑暗骑士崛起,克里斯托弗·诺兰,2012,8.9,813632 +超能陆战队,唐·霍尔,2014,8.8,1144394 +7号房的礼物,李焕庆,2013,8.9,621665 +第六感,M·奈特·沙马兰,1999,8.9,637165 +茶馆,谢添,1982,9.5,208393 +爱在黎明破晓前,理查德·林克莱特,1995,8.8,799980 +爱在日落黄昏时,理查德·林克莱特,2004,8.9,653121 +被嫌弃的松子的一生,中岛哲也,2006,8.8,774911 +哈利·波特与火焰杯,迈克·内威尔,2005,8.8,801220 +头脑特工队,彼特·道格特,2015,8.8,805466 +未麻的部屋,今敏,1997,9.1,425166 +重庆森林,王家卫,1994,8.8,922463 +借东西的小人阿莉埃蒂,米林宏昌,2010,8.9,627662 +菊次郎的夏天,北野武,1999,8.9,673415 +入殓师,泷田洋二郎,2008,8.9,742875 +断背山,李安,2005,8.8,782696 +剪刀手爱德华,蒂姆·波顿,1990,8.7,1120361 +勇敢的心,梅尔·吉布森,1995,8.9,605637 +时空恋旅人,理查德·柯蒂斯,2013,8.8,792845 +驯龙高手,迪恩·德布洛斯,2010,8.8,865342 +消失的爱人,大卫·芬奇,2014,8.7,1073380 +无人知晓,是枝裕和,2004,9.1,372454 +倩女幽魂,程小东,1987,8.8,835764 +傲慢与偏见,乔·怀特,2005,8.7,934590 +新世界,朴勋政,2013,8.9,527605 +花样年华,王家卫,2000,8.8,805039 +玩具总动员3,李·昂克里奇,2010,8.9,592801 +一个叫欧维的男人决定去死,汉内斯·赫尔姆,2015,8.9,569180 +完美的世界,克林特·伊斯特伍德,1993,9.1,360255 +色,戒,李安,2007,8.7,961264 +阳光灿烂的日子,姜文,1994,8.8,695820 +怪兽电力公司,彼特·道格特,2001,8.8,770500 +小森林 夏秋篇,森淳一,2014,9.0,473506 +天使爱美丽,让-皮埃尔·热内,2001,8.7,1021186 +教父3,弗朗西斯·福特·科波拉,1990,9.0,432529 +侧耳倾听,近藤喜文,1995,8.9,520711 +哪吒闹海,王树忱,1979,9.2,308367 +九品芝麻官,王晶,1994,8.8,794428 +被解救的姜戈,昆汀·塔伦蒂诺,2012,8.8,692686 +请以你的名字呼唤我,卢卡·瓜达尼诺,2017,8.8,824872 +幸福终点站,史蒂文·斯皮尔伯格,2004,8.8,642514 +釜山行,延尚昊,2016,8.6,1350551 +神偷奶爸,皮艾尔·柯芬,2010,8.7,1047113 +小森林 冬春篇,森淳一,2015,9.0,420799 +喜宴,李安,1993,9.0,431991 +萤火之森,大森贵弘,2011,8.8,611321 +告白,中岛哲也,2010,8.8,744270 +玛丽和麦克斯,亚当·艾略特,2009,9.0,471015 +七武士,黑泽明,1954,9.3,235798 +头号玩家,史蒂文·斯皮尔伯格,2018,8.6,1517576 +模仿游戏,莫滕·泰杜姆,2014,8.8,720496 +惊魂记,阿尔弗雷德·希区柯克,1960,9.0,356731 +大鱼,蒂姆·波顿,2003,8.8,627367 +心灵奇旅,彼特·道格特,2020,8.7,1143487 +射雕英雄传之东成西就,刘镇伟,1993,8.7,718409 +血战钢锯岭,梅尔·吉布森,2016,8.7,860215 +背靠背,脸对脸,黄建新,1994,9.5,180092 +机器人之梦,巴勃罗·贝格尔,2023,9.1,438062 +你的名字。,新海诚,2016,8.5,1593889 +我是山姆,杰茜·尼尔森,2001,9.0,376937 +阳光姐妹淘,姜炯哲,2011,8.8,638981 +恐怖直播,金秉祐,2013,8.7,728886 +黑客帝国3:矩阵革命,拉娜·沃卓斯基,2003,8.8,497783 +末路狂花,雷德利·斯科特,1991,9.0,348194 +小丑,托德·菲利普斯,2019,8.7,1126163 +三块广告牌,马丁·麦克唐纳,2017,8.7,920888 +谍影重重3,保罗·格林格拉斯,2007,8.9,471417 +电锯惊魂,詹姆斯·温,2004,8.7,613720 +高山下的花环,谢晋,1984,9.5,159270 +无间道2,刘伟强,2003,8.8,567777 +达拉斯买家俱乐部,让-马克·瓦雷,2013,8.8,506578 +疯狂原始人,科克·德·米科,2013,8.7,922005 +绿里奇迹,弗兰克·德拉邦特,1999,8.9,388809 +爱在午夜降临前,理查德·林克莱特,2013,8.9,474113 +疯狂的石头,宁浩,2006,8.6,926939 +雨中曲,斯坦利·多南,1952,9.1,269175 +2001太空漫游,斯坦利·库布里克,1968,8.9,397084 +海街日记,是枝裕和,2015,8.8,521773 +风之谷,宫崎骏,1984,8.9,393947 +上帝之城,费尔南多·梅里尔斯,2002,9.0,333771 +心迷宫,忻钰坤,2014,8.7,618292 +英雄本色,吴宇森,1986,8.6,606772 +记忆碎片,克里斯托弗·诺兰,2000,8.7,669589 +纵横四海,吴宇森,1991,8.8,474152 +无敌破坏王,瑞奇·莫尔,2012,8.7,612810 +卢旺达饭店,特瑞·乔治,2004,8.9,367601 +东京教父,今敏,2003,9.0,291738 +小偷家族,是枝裕和,2018,8.7,896062 +恐怖游轮,克里斯托弗·史密斯,2009,8.5,991130 +牯岭街少年杀人事件,杨德昌,1991,8.9,369903 +冰川时代,卡洛斯·沙尔丹哈,2002,8.7,686605 +魔女宅急便,宫崎骏,1989,8.7,525424 +芙蓉镇,谢晋,1987,9.3,190295 +忠犬八公物语,神山征二郎,1987,9.2,218977 +岁月神偷,罗启锐,2010,8.7,613107 +遗愿清单,罗伯·莱纳,2007,8.7,539323 +荒蛮故事,达米安·斯兹弗隆,2014,8.7,518520 +大佛普拉斯,黄信尧,2017,8.7,554931 +源代码,邓肯·琼斯,2011,8.6,927255 +花束般的恋爱,土井裕泰,2021,8.6,829328 +白日梦想家,本·斯蒂勒,2013,8.6,649369 +疯狂的麦克斯4:狂暴之路,乔治·米勒,2015,8.7,619438 +可可西里,陆川,2004,8.9,347072 +你看起来好像很好吃,藤森雅也,2010,8.9,376354 +爱乐之城,达米恩·查泽雷,2016,8.4,1118392 +贫民窟的百万富翁,丹尼·鲍尔,2008,8.6,807611 +波西米亚狂想曲,布莱恩·辛格,2018,8.6,691074 +城市之光,查理·卓别林,1931,9.3,173264 +爆裂鼓手,达米恩·查泽雷,2014,8.6,692181 +青蛇,徐克,1993,8.6,601993 +东邪西毒,王家卫,1994,8.6,636097 +哈利·波特与死亡圣器(上),大卫·叶茨,2010,8.6,697106 +无耻混蛋,昆汀·塔伦蒂诺,2009,8.7,563546 +终结者2:审判日,詹姆斯·卡梅隆,1991,8.8,386576 +大红灯笼高高挂,张艺谋,1991,8.8,372958 +黑天鹅,达伦·阿罗诺夫斯基,2010,8.6,847736 +新龙门客栈,李惠民,1992,8.7,510948 +初恋这件小事,普特鹏·普罗萨卡·那·萨克那卡林,2010,8.5,1052760 +人工智能,史蒂文·斯皮尔伯格,2001,8.7,503417 +千钧一发,安德鲁·尼科尔,1997,8.8,351248 +崖上的波妞,宫崎骏,2008,8.6,576363 +雨人,巴瑞·莱文森,1988,8.7,443647 +虎口脱险,杰拉尔·乌里,1966,8.9,296796 +哈利·波特与凤凰社,大卫·叶茨,2007,8.6,696912 +彗星来的那一夜,詹姆斯·沃德·布柯特,2013,8.6,686001 +罗生门,黑泽明,1950,8.8,358979 +海边的曼彻斯特,肯尼斯·罗纳根,2016,8.6,654079 +恋恋笔记本,尼克·卡索维茨,2004,8.5,749531 +真爱至上,理查德·柯蒂斯,2003,8.5,809354 +火星救援,雷德利·斯科特,2015,8.5,835422 +黑客帝国2:重装上阵,拉娜·沃卓斯基,2003,8.7,456272 +步履不停,是枝裕和,2008,8.8,328549 +冰雪奇缘,克里斯·巴克,2013,8.5,831127 +奇迹男孩,斯蒂芬·卓博斯基,2017,8.6,601482 +千年女优,今敏,2001,8.8,318761 +战争之王,安德鲁·尼科尔,2005,8.7,416175 +谍影重重2,保罗·格林格拉斯,2004,8.7,400470 +蜘蛛侠:平行宇宙,鲍勃·佩尔西凯蒂,2018,8.6,758179 +攻壳机动队,押井守,1995,9.0,218659 +血钻,爱德华·兹威克,2006,8.7,422529 +小姐,朴赞郁,2016,8.5,577359 +隐藏人物,特奥多尔·梅尔菲,2016,8.9,273022 +魂断蓝桥,茂文·勒鲁瓦,1940,8.8,311143 +血观音,杨雅喆,2017,8.6,562335 +房间,伦尼·阿伯拉罕森,2015,8.7,400485 diff --git a/crawl_project/pom.xml b/crawl_project/pom.xml new file mode 100644 index 0000000..44da696 --- /dev/null +++ b/crawl_project/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + org.example + crawl_project + 1.0-SNAPSHOT + jar + + crawl_project + http://maven.apache.org + + + UTF-8 + + + + + junit + junit + 3.8.1 + test + + + org.jsoup + jsoup + 1.17.2 + + + com.opencsv + opencsv + 5.9 + + + org.knowm.xchart + xchart + 3.8.7 + + + diff --git a/crawl_project/src/main/java/com/example/crawl/ChartGenerator.java b/crawl_project/src/main/java/com/example/crawl/ChartGenerator.java new file mode 100644 index 0000000..88c6c77 --- /dev/null +++ b/crawl_project/src/main/java/com/example/crawl/ChartGenerator.java @@ -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 movies) { + Map yearMap = movies.stream() + .filter(m -> m.getYear() > 1980) + .collect(Collectors.groupingBy(Movie::getYear, Collectors.counting())); + + List> sortedList = new ArrayList<>(yearMap.entrySet()); + sortedList.sort(Entry.comparingByKey()); + + if (sortedList.size() > 15) { + sortedList = sortedList.subList(0, 15); + } + + List xData = new ArrayList<>(); + List yData = new ArrayList<>(); + for (Entry 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 movies) { + Map avgRatingMap = movies.stream() + .filter(m -> m.getYear() > 1980) + .collect(Collectors.groupingBy(Movie::getYear, Collectors.averagingDouble(Movie::getRating))); + + List> sortedList = new ArrayList<>(avgRatingMap.entrySet()); + sortedList.sort(Entry.comparingByKey()); + + if (sortedList.size() > 15) { + sortedList = sortedList.subList(0, 15); + } + + // ✅ 修复:X轴使用数字类型 Integer,不再用字符串 + List xData = new ArrayList<>(); + List yData = new ArrayList<>(); + for (Entry 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 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(); + } + } +} \ No newline at end of file diff --git a/crawl_project/src/main/java/com/example/crawl/CsvExporter.java b/crawl_project/src/main/java/com/example/crawl/CsvExporter.java new file mode 100644 index 0000000..3b47eeb --- /dev/null +++ b/crawl_project/src/main/java/com/example/crawl/CsvExporter.java @@ -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 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; + } +} diff --git a/crawl_project/src/main/java/com/example/crawl/DataAnalyzer.java b/crawl_project/src/main/java/com/example/crawl/DataAnalyzer.java new file mode 100644 index 0000000..5984fcb --- /dev/null +++ b/crawl_project/src/main/java/com/example/crawl/DataAnalyzer.java @@ -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 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 movies) { + System.out.println("\n===== 各年份电影数量统计 ====="); + Map 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 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); + } +} diff --git a/crawl_project/src/main/java/com/example/crawl/DoubanCrawler.java b/crawl_project/src/main/java/com/example/crawl/DoubanCrawler.java new file mode 100644 index 0000000..d446d17 --- /dev/null +++ b/crawl_project/src/main/java/com/example/crawl/DoubanCrawler.java @@ -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 crawlTop250() { + List 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; + } +} + diff --git a/crawl_project/src/main/java/com/example/crawl/Main.java b/crawl_project/src/main/java/com/example/crawl/Main.java new file mode 100644 index 0000000..f78df90 --- /dev/null +++ b/crawl_project/src/main/java/com/example/crawl/Main.java @@ -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 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); // 饼图 + } +} diff --git a/crawl_project/src/main/java/com/example/crawl/Movie.java b/crawl_project/src/main/java/com/example/crawl/Movie.java new file mode 100644 index 0000000..6a4a4e6 --- /dev/null +++ b/crawl_project/src/main/java/com/example/crawl/Movie.java @@ -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 + + '}'; + } +} + diff --git a/crawl_project/src/main/java/org/example/App.java b/crawl_project/src/main/java/org/example/App.java new file mode 100644 index 0000000..5f21d2e --- /dev/null +++ b/crawl_project/src/main/java/org/example/App.java @@ -0,0 +1,13 @@ +package org.example; + +/** + * Hello world! + * + */ +public class App +{ + public static void main( String[] args ) + { + System.out.println( "Hello World!" ); + } +} diff --git a/crawl_project/src/test/java/org/example/AppTest.java b/crawl_project/src/test/java/org/example/AppTest.java new file mode 100644 index 0000000..d5f435d --- /dev/null +++ b/crawl_project/src/test/java/org/example/AppTest.java @@ -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 ); + } +} diff --git a/crawl_project/target/classes/com/example/crawl/ChartGenerator.class b/crawl_project/target/classes/com/example/crawl/ChartGenerator.class new file mode 100644 index 0000000..eea9e9e Binary files /dev/null and b/crawl_project/target/classes/com/example/crawl/ChartGenerator.class differ diff --git a/crawl_project/target/classes/com/example/crawl/CsvExporter.class b/crawl_project/target/classes/com/example/crawl/CsvExporter.class new file mode 100644 index 0000000..619f644 Binary files /dev/null and b/crawl_project/target/classes/com/example/crawl/CsvExporter.class differ diff --git a/crawl_project/target/classes/com/example/crawl/DataAnalyzer.class b/crawl_project/target/classes/com/example/crawl/DataAnalyzer.class new file mode 100644 index 0000000..b0bec60 Binary files /dev/null and b/crawl_project/target/classes/com/example/crawl/DataAnalyzer.class differ diff --git a/crawl_project/target/classes/com/example/crawl/DoubanCrawler.class b/crawl_project/target/classes/com/example/crawl/DoubanCrawler.class new file mode 100644 index 0000000..ba80532 Binary files /dev/null and b/crawl_project/target/classes/com/example/crawl/DoubanCrawler.class differ diff --git a/crawl_project/target/classes/com/example/crawl/Main.class b/crawl_project/target/classes/com/example/crawl/Main.class new file mode 100644 index 0000000..d7d3635 Binary files /dev/null and b/crawl_project/target/classes/com/example/crawl/Main.class differ diff --git a/crawl_project/target/classes/com/example/crawl/Movie.class b/crawl_project/target/classes/com/example/crawl/Movie.class new file mode 100644 index 0000000..7d508c2 Binary files /dev/null and b/crawl_project/target/classes/com/example/crawl/Movie.class differ diff --git a/crawl_project/target/classes/org/example/App.class b/crawl_project/target/classes/org/example/App.class new file mode 100644 index 0000000..4240a01 Binary files /dev/null and b/crawl_project/target/classes/org/example/App.class differ diff --git a/crawl_project/历年平均评分_折线图.png b/crawl_project/历年平均评分_折线图.png new file mode 100644 index 0000000..bf7b198 Binary files /dev/null and b/crawl_project/历年平均评分_折线图.png differ diff --git a/crawl_project/年份电影数量_柱状图.png b/crawl_project/年份电影数量_柱状图.png new file mode 100644 index 0000000..f29a402 Binary files /dev/null and b/crawl_project/年份电影数量_柱状图.png differ diff --git a/crawl_project/评分分布_饼图.png b/crawl_project/评分分布_饼图.png new file mode 100644 index 0000000..b471ed8 Binary files /dev/null and b/crawl_project/评分分布_饼图.png differ diff --git a/crawl_project/豆瓣电影Top250数据爬取与可视化分析实验报告.md b/crawl_project/豆瓣电影Top250数据爬取与可视化分析实验报告.md new file mode 100644 index 0000000..1bea806 --- /dev/null +++ b/crawl_project/豆瓣电影Top250数据爬取与可视化分析实验报告.md @@ -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 + + + + + org.jsoup + jsoup + 1.17.2 + + + + com.opencsv + opencsv + 5.8 + + + + org.knowm.xchart + xchart + 3.8.7 + + +``` + +### (三)模块开发实现 + +#### 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`结构化数据列表。 + +#### 3. 数据分析类(DataAnalyzer.java) + +基于Java Stream API实现三大核心分析功能: + +1. 数据总览:统计电影总数、计算平均评分; + +2. 评分Top10:按评分降序排序,获取评分最高的10部电影; + +3. 年份分布:按上映年份分组,统计各年份的电影产出数量,并按年份升序输出。 + +#### 4. CSV导出类(CsvExporter.java) + +1. 定义CSV文件导出路径与表头(电影名称、导演、上映年份、豆瓣评分、评价人数); + +2. 遍历`List`数据,将每部电影的属性按表头顺序写入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`改为`List`(年份数字),符合数据类型要求| +|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 生成) \ No newline at end of file