云计算与大数据期末项目 电商大数据离线计算

本文涉及的产品
云原生大数据计算服务 MaxCompute,5000CU*H 100GB 3个月
云原生大数据计算服务MaxCompute,500CU*H 100GB 3个月
简介: 云计算与大数据期末项目 电商大数据离线计算

一、项目介绍

       电子商务中会产生海量的数据,蕴含着不可估量的数据价值,可通过数据分析来挖掘这些潜在价值,以提升平台的销量。本次实践项目利用 Hadoop 的分布式计算框架 MapReduce 来分析用户行为数据,计算得出商品点击排行、商品分类占比等统计指标,使得更加熟练掌握 MapReduce 程序的设计。

       相关理论知识有,Map 和 Reduce 流程图如下:输入数据(INPUT)首先分割(SPLIT)成若干份,然后每份分别进行 MAP 过程;中间再经过 COMBINE 任务(可选)和 PARTTION 对 MAP 所得的结果进行聚合——把同 key 的结果放到同一组,最后再进行 REDUCE 过程。

二、项目内容

电商大数据离线计算,主要有六个部分,分别为:

第1部分:统计用户流失情况;

第2部分:统计所有商品点击量排行;

第3部分:统计各个商品类别中点击量最高的商品;

第4部分:统计五种商品类别占比;

第5部分:统计各类商品种类的购买次数;

第6部分:统计五类商品中各自点击量最高的商品的购买次数。

三、功能模块

四、功能分析

统计用户流失情况:就是统计出用户4种不同用户行为的数量,即点击浏览(pv)的数量,购买(buy)的数量等。

统计所有商品点击量排行:即统计出每个商品id中用户行为是pv(点击浏览)的数量,reduce的输出最后是按点击量的大小从大到小排序

统计各个商品类别中点击量最高的商品:根据用户行为数据,编写 MapReduce 程序来统计各个商品类别中点击量最高的商品。

统计五种商品类别占比:根据用户行为数据,编写 MapReduce 程序来统计出五种商品分类占比数据。

统计出各个商品类别的数量,在把一个商品类别的数量除以所有商品类别的数量即可得到该商品类别的占比。

统计各类商品种类的购买次数:根据用户行为数据,编写 MapReduce 程序来统计出各类商品种类的购买次数。

统计五类商品中各自点击量最高的商品的购买次数:根据用户行为数据,编写 MapReduce 程序来统计出五类商品中各自点击量最高的商品的购买次数。

五、项目步骤

(一)统计用户流失情况

1、项目需求分析及任务

任务:就是统计出用户4种不同用户行为的数量,即点击浏览(pv)的数量,购买(buy)的数量等。

编程要求:

main 方法已给出,其中 Job 和输入输出路径已配置完成,无需更改,

map 和 reduce 的输入输出 key、value 已给出。

预期输出格式

buy,总数

cart,总数

fav,总数

pv,总数

2、技术方法

这是编程中用到的电商数据数据,为 CSV 格式,文件名user_behavior.csv,大小9948行,每一行数据(4列)分别表示: 用户id, 商品id, 商品类别, 用户行为;商品类别有 手机、平板电脑、笔记本、智能手表、耳机,总共5大类别;用户行为中pv代表点击浏览,cart代表加入购物车,fav代表添加到喜欢,buy代表购买。

用户流失情况就是统计出用户4种不同用户行为的数量,即点击浏览(pv)的数量,购买(buy)的数量等。

3、部分截图与相关代码

1. 1.     public static class ThisMap extends Mapper<Object, Text, Text, IntWritable> {
2. 2. 
3. 3.         //私有变量1,可重复使用
4. 4.         private static IntWritable one = new IntWritable(1);
5. 5. 
6. 6.         @Override
7. 7.         protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
8. 8. 
9. 9.             /*** 在这编写map内容 ****/
10. 10.             /********** Begin **********/
11. 11.             //分割每行数据
12. 12.             String[] atts = value.toString().split(",");
13. 13.             //得到行为属性
14. 14.             String behavior = atts[3];
15. 15.             //行为属性作key,1作value的map输出
16. 16.             context.write(new Text(behavior), one);
17. 17. 
18. 18.             /********** End **********/
19. 19.         }
20. 20.     }
21. 21. 
22. 22.     public static class ThisReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
23. 23. 
24. 24.         @Override
25. 25.         protected void reduce(Text key, Iterable<IntWritable> values, Context context)
26. 26.                 throws IOException, InterruptedException {
27. 27. 
28. 28.             /*** 在这编写reduce内容 ****/
29. 29.             /********** Begin **********/
30. 30.             //统计同key的values总数
31. 31.             int sum = 0;
32. 32.             for(IntWritable one : values){
33. 33.                 sum += one.get();
34. 34.             }
35. 35.             //写入到reduce输出
36. 36.             context.write(key, new IntWritable(sum));
37. 37. 
38. 38.             /********** End **********/
39. 39.         }
40. 40.     }

(二)统计所有商品点击量排行

1、项目需求分析及任务

任务:即统计出每个商品id中用户行为是pv(点击浏览)的数量,reduce的输出最后是按点击量的大小从大到小排序。

编程要求:

main 方法已给出,其中 Job 和输入输出路径已配置完成,无需更改,

map 和 reduce 的输入输出 key、value 已给出。

预期输出格式(按点击量从大到小):

商品id,点击量

商品id,点击量

···

2、技术方法

cleanup()方法

编程中可能会用到 cleanup() 方法,cleanup 方法是 mapper/reduce 对象执行完所有的 map/reduce 方法之后最后执行的方法,可用于清理资源释放或清理工作;默认继承的父类方法为空。

3、部分截图与相关代码

1. 1.     public static class ThisMap extends Mapper<Object, Text, Text, IntWritable> {
2. 2. 
3. 3.         private static IntWritable one = new IntWritable(1);
4. 4. 
5. 5.         @Override
6. 6.         protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
7. 7. 
8. 8.             /*** 在这编写map内容 ****/
9. 9.             /********** Begin **********/
10. 10.             //1. 分割每行数据
11. 11.             String[] atts = value.toString().split(",");
12. 12.             //2. 得到商品id
13. 13.             String item = atts[1];
14. 14.             //3. 得到行为属性
15. 15.             String behavior = atts[3];
16. 16.             //4. 如果行为属性是 'pv',则写入到map输出
17. 17.             if (behavior.equals("pv")) {
18. 18.                 context.write(new Text(item), one);
19. 19.             }
20. 20. 
21. 21.             /********** End **********/
22. 22. 
23. 23.         }
24. 24.     }
25. 25. 
26. 26.     public static class ThisReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
27. 27. 
28. 28.         //对象实例,用来保存reduce方法中处理的数据
29. 29.         List<Object[]> list = new LinkedList<>();
30. 30. 
31. 31.         @Override
32. 32.         protected void reduce(Text key, Iterable<IntWritable> values, Context context)
33. 33.                 throws IOException, InterruptedException {
34. 34.             /*** 在这编写reduce内容 ****/
35. 35.             /********** Begin **********/
36. 36.             // 统计同key总数, 把key和sum写入到list中
37. 37.             int sum = 0;
38. 38.             for (IntWritable one : values) {
39. 39.                 sum += one.get();
40. 40.             }
41. 41.             list.add(new Object[] { key.toString(), Integer.valueOf(sum) });
42. 42. 
43. 43.             /********** End **********/
44. 44.         }
45. 45. 
46. 46. 
47. 47. 
48. 48.         //cleanup方法,即reduce对象执行完所有的reduce方法后最后执行的方法
49. 49.         @Override
50. 50.         protected void cleanup(Reducer<Text, IntWritable, Text, IntWritable>.Context context)
51. 51.                 throws IOException, InterruptedException {
52. 52. 
53. 53.             // 按照sum的大小对list进行排序,得到的结果是从小到大
54. 54.             list = list.stream().sorted((o1, o2) -> { return ((int)o1[1] - (int)o2[1]);}).collect(Collectors.toList());
55. 55.             // 从后向前遍历,即从大到小
56. 56.             for(int i=list.size()-1; i>=0; i--){
57. 57.                 Object[] o = list.get(i);
58. 58.                 //写入到reduce输出
59. 59.                 context.write(new Text((String) o[0]), new IntWritable((int) o[1]));
60. 60.             }
61. 61.         }
62. 62.     }

(三)统计各个商品类别中点击量最高的商品

1、项目需求分析及任务

任务:根据用户行为数据,编写 MapReduce 程序来统计各个商品类别中点击量最高的商品。

编程要求:

main 方法已给出,其中 Job 和输入输出路径已配置完成,无需更改,

map 和 reduce 的输入输出 key、value 已给出。

预期输出格式

商品类型,点击量最高的商品id

商品类型,点击量最高的商品id

2、技术方法

这是编程中用到的电商数据数据,为 CSV 格式,文件名user_behavior.csv,大小9948行,每一行数据(4列)分别表示: 用户id, 商品id, 商品类别, 用户行为;商品类别有 手机、平板电脑、笔记本、智能手表、耳机,总共5大类别;用户行为中pv代表点击浏览,cart代表加入购物车,fav代表添加到喜欢,buy代表购买。

3、部分截图与相关代码

1. 1.     public static class ThisMap extends Mapper<Object, Text, Text, Text> {
2. 2. 
3. 3.         @Override
4. 4.         protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
5. 5. 
6. 6.             /*** 在这编写map内容 ****/
7. 7.             /********** Begin **********/
8. 8.             // 作用跟前几关一样,不再描述
9. 9.             String[] atts = value.toString().split(",");
10. 10.             String item = atts[1];
11. 11.             String type = atts[2];
12. 12.             String behavior = atts[3];
13. 13.             if (behavior.equals("pv")) {
14. 14.                 context.write(new Text(type), new Text(item));
15. 15.             }
16. 16.             /********** End **********/
17. 17. 
18. 18.         }
19. 19.     }
20. 20. 
21. 21.     public static class ThisReduce extends Reducer<Text, Text, Text, Text> {
22. 22. 
23. 23.         @Override
24. 24.         protected void reduce(Text key, Iterable<Text> values, Context context)
25. 25.                 throws IOException, InterruptedException {
26. 26. 
27. 27.             /*** 在这编写reduce内容 ****/
28. 28.             /********** Begin **********/
29. 29. 
30. 30.             // 提示: 先得出所有商品id的数量,再从这些数量中找出最大值
31. 31. 
32. 32.             // 1. 一个map,用来保存各个商品id的数量
33. 33.             Map<String, Integer> map = new HashMap<>();
34. 34.             // 2. 统计values中各个value的数量
35. 35.             for (Text value : values) {
36. 36.                 String item = value.toString();
37. 37.                 Integer count = !map.containsKey(item) ? 1 : map.get(item) + 1;
38. 38.                 map.put(item, count);
39. 39.             }
40. 40.             // 3. 找出map中value最大的键值对
41. 41.             Map.Entry<String, Integer> itemMax = Collections.max(map.entrySet(), (entry1, entry2) -> {
42. 42.                 return entry1.getValue() - entry2.getValue();
43. 43.             });
44. 44.             // 4. 结果写入reduce输出
45. 45.             context.write(key, new Text(itemMax.getKey()));
46. 46. 
47. 47.             /********** End **********/
48. 48.         }
49. 49. 
50. 50.     }

(四)统计五种商品类别占比

1、项目需求分析及任务

任务:根据用户行为数据,编写 MapReduce 程序来统计出五种商品分类占比数据。统计出各个商品类别的数量,在把一个商品类别的数量除以所有商品类别的数量即可得到该商品类别的占比。

编程要求:

main 方法已给出,其中 Job 和输入输出路径已配置完成,无需更改,

map 和 reduce 的输入输出 key、value 已给出。

预期输出格式:

商品类别,占总数比例

商品类别,占总数比例

···

2、技术方法

cleanup()方法

编程中可能会用到 cleanup() 方法,cleanup 方法是 mapper/reduce 对象执行完所有的 map/reduce 方法之后最后执行的方法,可用于清理资源释放或清理工作;默认继承的父类方法为空。

3、部分截图与相关代码

1. 1.     public static class ThisMap extends Mapper<Object, Text, Text, IntWritable> {
2. 2. 
3. 3.         private static IntWritable one = new IntWritable(1);
4. 4. 
5. 5.         @Override
6. 6.         protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
7. 7.             /*** 在这编写map内容 ****/
8. 8.             /********** Begin **********/
9. 9. 
10. 10.             String[] atts = value.toString().split(",");
11. 11.             String type = atts[2];
12. 12.             context.write(new Text(type), one);
13. 13. 
14. 14.             /********** End **********/
15. 15.         }
16. 16.     }
17. 17. 
18. 18.     public static class ThisReduce extends Reducer<Text, IntWritable, Text, DoubleWritable> {
19. 19. 
20. 20.         // 保存reduce方法的处理结果
21. 21.         Map<String,Integer> map = new HashMap<>();
22. 22. 
23. 23.         @Override
24. 24.         protected void reduce(Text key, Iterable<IntWritable> values, Context context)
25. 25.                 throws IOException, InterruptedException {
26. 26.             /*** 在这编写reduce内容 ****/
27. 27.             /********** Begin **********/
28. 28.             int count = 0;
29. 29.             for (IntWritable one : values) {
30. 30.                 count += one.get();
31. 31.             }
32. 32.             map.put(key.toString(), count);
33. 33.             /********** End **********/
34. 34. 
35. 35. 
36. 36.         }
37. 37. 
38. 38.         // 需要重写 cleanup方法
39. 39.         @Override
40. 40.         protected void cleanup(Reducer<Text, IntWritable, Text, DoubleWritable>.Context context)
41. 41.                 throws IOException, InterruptedException {
42. 42. 
43. 43.             // 得到所有商品类别数量的总和
44. 44.             int sum = 0;
45. 45.             for (int v : map.values()) {
46. 46.                 sum += v;
47. 47.             }
48. 48.             // 得出每个商品类别的占比
49. 49.             for (String key : map.keySet()) {
50. 50.                 int value = map.get(key);
51. 51.                 double ratio = ((double) value) / sum;
52. 52.                 context.write(new Text(key), new DoubleWritable(ratio));
53. 53.             }
54. 54.         }
55. 55. 
56. 56.     }

(五)统计各类商品种类的购买次数

1、项目需求分析及任务

任务:根据用户行为数据,编写 MapReduce 程序来统计出各类商品种类的购买次数。

编程要求:

main 方法已给出,其中 Job 和输入输出路径已配置完成,无需更改,

map 和 reduce 的输入输出 key、value 已给出。

预期输出格式:

商品类型,购买次数

商品类型,购买次数

···

2、技术方法

这是编程中用到的电商数据数据,为 CSV 格式,文件名user_behavior.csv,大小9948行,每一行数据(4列)分别表示: 用户id, 商品id, 商品类别, 用户行为;商品类别有 手机、平板电脑、笔记本、智能手表、耳机,总共5大类别;用户行为中pv代表点击浏览,cart代表加入购物车,fav代表添加到喜欢,buy代表购买。

3、部分截图与相关代码

1. 1.     public static class ThisMap extends Mapper<Object, Text, Text, IntWritable> {
2. 2. 
3. 3.         private static IntWritable one = new IntWritable(1);
4. 4. 
5. 5.         @Override
6. 6.         protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
7. 7.             /*** 在这编写map内容 ****/
8. 8.             /********** Begin **********/
9. 9.             String[] atts = value.toString().split(",");
10. 10.             String type = atts[2];
11. 11.             if (atts[3].equals("buy")) {
12. 12.                 context.write(new Text(type), one);
13. 13.             }
14. 14.             /********** End **********/
15. 15. 
16. 16.         }
17. 17.     }
18. 18. 
19. 19.     public static class ThisReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
20. 20. 
21. 21.         @Override
22. 22.         protected void reduce(Text key, Iterable<IntWritable> values, Context context)
23. 23.                 throws IOException, InterruptedException {
24. 24. 
25. 25.             /*** 在这编写reduce内容 ****/
26. 26.             /********** Begin **********/
27. 27.             int count = 0;
28. 28.             for (IntWritable one : values) {
29. 29.                 count += one.get();
30. 30.             }
31. 31.             context.write(key, new IntWritable(count));
32. 32.             /********** End **********/
33. 33. 
34. 34.         }
35. 35.     }

(六)统计五类商品中各自点击量最高的商品的购买次数

1、项目需求分析及任务

任务:根据用户行为数据,编写 MapReduce 程序来统计出五类商品中各自点击量最高的商品的购买次数。

编程要求:

main 方法已给出,其中 Job 和输入输出路径已配置完成,无需更改,

map 和 reduce 的输入输出 key、value 已给出。

预期输出格式:

商品类型,本类型中点击量最高的id,购买次数

商品类型,本类型中点击量最高的id,购买次数

···

2、技术方法

这是编程中用到的电商数据数据,为 CSV 格式,文件名user_behavior.csv,大小9948行,每一行数据(4列)分别表示: 用户id, 商品id, 商品类别, 用户行为;商品类别有 手机、平板电脑、笔记本、智能手表、耳机,总共5大类别;用户行为中pv代表点击浏览,cart代表加入购物车,fav代表添加到喜欢,buy代表购买。

3、部分截图与相关代码

1. 1.     public static class ThisMap extends Mapper<Object, Text, Text, Text> {
2. 2. 
3. 3.         @Override
4. 4.         protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
5. 5.             /*** 在这编写map内容 ****/
6. 6.             /********** Begin **********/
7. 7.             String[] atts = value.toString().split(",");
8. 8.             String type = atts[2];
9. 9.             //把value作为map的输出值,因为到时还需要用到一些属性
10. 10.             context.write(new Text(type), value);
11. 11.             /********** End **********/
12. 12. 
13. 13.         }
14. 14.     }
15. 15. 
16. 16.     public static class ThisReduce extends Reducer<Text, Text, Text, Text> {
17. 17. 
18. 18.         @Override
19. 19.         protected void reduce(Text key, Iterable<Text> values, Context context)
20. 20.                 throws IOException, InterruptedException {
21. 21. 
22. 22.             /*** 在这编写reduce内容 ****/
23. 23.             /********** Begin **********/
24. 24. 
25. 25.             Map<String, Integer> map = new HashMap<>();
26. 26.             List<String> value_list = new ArrayList<>();
27. 27.             // 1. 因为需要遍历多次values里的值,把values可迭代对象转化为list
28. 28.             for (Text v : values) {
29. 29.                 value_list.add(v.toString());
30. 30.             }
31. 31.             // 2. 统计所有商品的数量
32. 32.             for (String v : value_list) {
33. 33.                 String[] atts = v.toString().split(",");
34. 34.                 String item = atts[1];
35. 35.                 Integer count = !map.containsKey(item) ? 1 : map.get(item) + 1;
36. 36.                 map.put(item, count);
37. 37.             }
38. 38.             // 3. 找出点击数量最大的商品
39. 39.             String itemClickMax = Collections.max(map.entrySet(), (entry1, entry2) -> {
40. 40.                 return entry1.getValue() - entry2.getValue();
41. 41.             }).getKey();
42. 42.             // 4. 统计点击量最大的商品的购买次数
43. 43.             int buyCount = 0;
44. 44.             for (String v : value_list) {
45. 45.                 String[] atts = v.toString().split(",");
46. 46.                 if (atts[1].equals(itemClickMax) && atts[3].equals("buy")) {
47. 47.                     buyCount++;
48. 48.                 }
49. 49.             }
50. 50.             // 5. 把商品类别、点击量最大的商品id、购买次数写入reducer输出
51. 51.             context.write(key, new Text(itemClickMax + "\t" + buyCount));
52. 52. 
53. 53.             /********** End **********/
54. 54. 
55. 55.         }
56. 56. 
57. 57.     }

六、项目心得

进行作业的过程中的需要仔细阅读题目,步骤的缺漏会影响最终的结果。编写代码时注意括号和分号的使用,遇到困难应该多思考多尝试。

相关实践学习
基于MaxCompute的热门话题分析
本实验围绕社交用户发布的文章做了详尽的分析,通过分析能得到用户群体年龄分布,性别分布,地理位置分布,以及热门话题的热度。
SaaS 模式云数据仓库必修课
本课程由阿里云开发者社区和阿里云大数据团队共同出品,是SaaS模式云原生数据仓库领导者MaxCompute核心课程。本课程由阿里云资深产品和技术专家们从概念到方法,从场景到实践,体系化的将阿里巴巴飞天大数据平台10多年的经过验证的方法与实践深入浅出的讲给开发者们。帮助大数据开发者快速了解并掌握SaaS模式的云原生的数据仓库,助力开发者学习了解先进的技术栈,并能在实际业务中敏捷的进行大数据分析,赋能企业业务。 通过本课程可以了解SaaS模式云原生数据仓库领导者MaxCompute核心功能及典型适用场景,可应用MaxCompute实现数仓搭建,快速进行大数据分析。适合大数据工程师、大数据分析师 大量数据需要处理、存储和管理,需要搭建数据仓库?学它! 没有足够人员和经验来运维大数据平台,不想自建IDC买机器,需要免运维的大数据平台?会SQL就等于会大数据?学它! 想知道大数据用得对不对,想用更少的钱得到持续演进的数仓能力?获得极致弹性的计算资源和更好的性能,以及持续保护数据安全的生产环境?学它! 想要获得灵活的分析能力,快速洞察数据规律特征?想要兼得数据湖的灵活性与数据仓库的成长性?学它! 出品人:阿里云大数据产品及研发团队专家 产品 MaxCompute 官网 https://www.aliyun.com/product/odps&nbsp;
目录
相关文章
|
1月前
|
存储 负载均衡 算法
大数据散列分区计算哈希值
大数据散列分区计算哈希值
47 4
|
2月前
|
分布式计算 大数据 Java
大数据-87 Spark 集群 案例学习 Spark Scala 案例 手写计算圆周率、计算共同好友
大数据-87 Spark 集群 案例学习 Spark Scala 案例 手写计算圆周率、计算共同好友
75 5
|
2月前
|
分布式计算 关系型数据库 MySQL
大数据-88 Spark 集群 案例学习 Spark Scala 案例 SuperWordCount 计算结果数据写入MySQL
大数据-88 Spark 集群 案例学习 Spark Scala 案例 SuperWordCount 计算结果数据写入MySQL
56 3
|
2月前
|
存储 分布式计算 算法
大数据-106 Spark Graph X 计算学习 案例:1图的基本计算、2连通图算法、3寻找相同的用户
大数据-106 Spark Graph X 计算学习 案例:1图的基本计算、2连通图算法、3寻找相同的用户
73 0
|
1月前
|
机器学习/深度学习 存储 大数据
云计算与大数据技术的融合应用
云计算与大数据技术的融合应用
|
1月前
|
存储 弹性计算 分布式计算
云计算在大数据处理中的优势与挑战
云计算在大数据处理中的优势与挑战
|
1月前
|
存储 人工智能 大数据
物联网、大数据、云计算、人工智能之间的关系
物联网、大数据、云计算、人工智能之间的关系是紧密相连、相互促进的。这四者既有各自独立的技术特征,又能在不同层面上相互融合,共同推动信息技术的发展和应用。
588 0
|
1月前
|
分布式计算 Java MaxCompute
ODPS MR节点跑graph连通分量计算代码报错java heap space如何解决
任务启动命令:jar -resources odps-graph-connect-family-2.0-SNAPSHOT.jar -classpath ./odps-graph-connect-family-2.0-SNAPSHOT.jar ConnectFamily 若是设置参数该如何设置
|
2月前
|
算法 大数据 数据库
云计算与大数据平台的数据库迁移与同步
本文详细介绍了云计算与大数据平台的数据库迁移与同步的核心概念、算法原理、具体操作步骤、数学模型公式、代码实例及未来发展趋势与挑战。涵盖全量与增量迁移、一致性与异步复制等内容,旨在帮助读者全面了解并应对相关技术挑战。
58 3
|
2月前
|
机器学习/深度学习 监控 搜索推荐
电商平台如何精准抓住你的心?揭秘大数据背后的神秘推荐系统!
【10月更文挑战第12天】在信息爆炸时代,数据驱动决策成为企业优化决策的关键方法。本文以某大型电商平台的商品推荐系统为例,介绍其通过收集用户行为数据,经过预处理、特征工程、模型选择与训练、评估优化及部署监控等步骤,实现个性化商品推荐,提升用户体验和销售额的过程。
104 1

热门文章

最新文章