MapReduce TotalOrderPartitioner 全局排序<转>

本文介绍Hadoop中的TotalOrderPartitioner如何实现全局排序,包括其工作原理、依赖的partition file生成方式及示例程序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们知道Mapreduce框架在feed数据给reducer之前会对map output key排序,这种排序机制保证了每一个reducer局部有序,Hadoop 默认的partitioner是HashPartitioner,它依赖于output key的hashcode,使得相同key会去相同reducer,但是不保证全局有序,如果想要获得全局排序结果(比如获取top N, bottom N),就需要用到TotalOrderPartitioner了,它保证了相同key去相同reducer的同时也保证了全局有序。

public class HashPartitioner<K, V> extends Partitioner<K, V> {
  /** Use {@link Object#hashCode()} to partition. */
  public int getPartition(K key, V value,
                          int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }
}

/**
 * Partitioner effecting a total order by reading split points from
 * an externally generated source.
 */
@InterfaceAudience.Public
@InterfaceStability.Stable
public class TotalOrderPartitioner<K extends WritableComparable<?>,V>
    extends Partitioner<K,V> implements Configurable {
  // by construction, we know if our keytype
  @SuppressWarnings("unchecked") // is memcmp-able and uses the trie
  public int getPartition(K key, V value, int numPartitions) {
    return partitions.findPartition(key);
  }
}

TotalOrderPartitioner依赖于一个partition file来distribute keys,partition file是一个实现计算好的sequence file,如果我们设置的reducer number是N,那么这个文件包含(N-1)个key分割点,并且是基于key comparator排好序的。TotalOrderPartitioner会检查每一个key属于哪一个reducer的范围内,然后决定分发给哪一个reducer。

InputSampler类的writePartitionFile方法会对input files取样并创建partition file。有三种取样方法:

1. RandomSampler  随机取样
2. IntervalSampler  从s个split里面按照一定间隔取样,通常适用于有序数据
3. SplitSampler  从s个split中选取前n条记录取样

paritition file可以通过TotalOrderPartitioner.setPartitionFile(conf, partitionFile)来设置,在TotalOrderPartitioner instance创建的时候会调用setConf函数,这时会读入partition file中key值,如果key是BinaryComparable(可以认为是字符串类型)的话会构建trie,时间复杂度是O(n), n是树的深度。如果是非BinaryComparable类型就构建BinarySearchNode,用二分查找,时间复杂度O(log(n)),n是reduce数

      boolean natOrder =
        conf.getBoolean(NATURAL_ORDER, true);
      if (natOrder && BinaryComparable.class.isAssignableFrom(keyClass)) {
        partitions = buildTrie((BinaryComparable[])splitPoints, 0,
            splitPoints.length, new byte[0],
            // Now that blocks of identical splitless trie nodes are 
            // represented reentrantly, and we develop a leaf for any trie
            // node with only one split point, the only reason for a depth
            // limit is to refute stack overflow or bloat in the pathological
            // case where the split points are long and mostly look like bytes 
            // iii...iixii...iii  .  Therefore, we make the default depth
            // limit large but not huge.
            conf.getInt(MAX_TRIE_DEPTH, 200));
      } else {
        partitions = new BinarySearchNode(splitPoints, comparator);
      }

示例程序

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.partition.InputSampler;
import org.apache.hadoop.mapreduce.lib.partition.InputSampler.RandomSampler;
import org.apache.hadoop.mapreduce.lib.partition.TotalOrderPartitioner;

public class TotalSortMR {
 
 public static int runTotalSortJob(String[] args) throws Exception {
  Path inputPath = new Path(args[0]);
  Path outputPath = new Path(args[1]);
  Path partitionFile = new Path(args[2]);
  int reduceNumber = Integer.parseInt(args[3]);
  
  // RandomSampler第一个参数表示key会被选中的概率,第二个参数是一个选取samples数,第三个参数是最大读取input splits数
  RandomSampler<Text, Text> sampler = new InputSampler.RandomSampler<Text, Text>(0.1, 10000, 10);
  
  Configuration conf = new Configuration();
  // 设置partition file全路径到conf
  TotalOrderPartitioner.setPartitionFile(conf, partitionFile);
  
  Job job = new Job(conf);
  job.setJobName("Total-Sort");
  job.setJarByClass(TotalSortMR.class);
  job.setInputFormatClass(KeyValueTextInputFormat.class);
  job.setMapOutputKeyClass(Text.class);
  job.setMapOutputValueClass(Text.class);
  job.setNumReduceTasks(reduceNumber);
  
  // partitioner class设置成TotalOrderPartitioner
  job.setPartitionerClass(TotalOrderPartitioner.class);
  
  FileInputFormat.setInputPaths(job, inputPath);
  FileOutputFormat.setOutputPath(job, outputPath);
  outputPath.getFileSystem(conf).delete(outputPath, true);
  
  // 写partition file到mapreduce.totalorderpartitioner.path
  InputSampler.writePartitionFile(job, sampler);
  
  return job.waitForCompletion(true)? 0 : 1;
  
 }
 
 public static void main(String[] args) throws Exception{
  System.exit(runTotalSortJob(args));
 }
}

上面的例子是采用InputSampler来创建partition file,其实还可以使用mapreduce来创建,可以自定义一个inputformat来取样,将output key输出到一个reducer

ps:hive 0.12实现了parallel ORDER BY(https://issues.apache.org/jira/browse/HIVE-1402),也是基于TotalOrderPartitioner,非常靠谱的new feature啊

<think>好的,我现在需要帮助用户解决如何使用MapReduce分析HDFS上的CSV文件的问题。用户的要求包括统计多少驴友来过,排名、攻略数量、景点名称、点评数量、简介、纬度、经度、评分这些内容。首先,我得理清用户的具体需求,然后逐步思考如何通过MapReduce实现这些分析。 首先,用户提到的“多少驴友来过”可能是指每个景点的访问人数,或者总的独立用户数。需要明确用户是想统计每个景点的访问人数还是整个数据集中的总用户数。假设是每个景点的访问人数,这可能涉及到按景点名称分组统计用户数量。 接下来,排名可能指的是根据某个指标如访问人数、点评数量或评分进行排名。需要确定排名的依据,比如按访问次数从高到低排序。 攻略数量、景点名称、点评数量这些字段可能需要直接从数据中提取或进行统计。简介、纬度、经度、评分这些信息可能需要保留每个景点的详细信息,可能在处理过程中需要去重,确保每个景点只出现一次,并附带这些属性。 接下来,我需要考虑MapReduce的处理流程。通常,MapReduce分为Map阶段和Reduce阶段。在Map阶段,处理每一行数据,提取所需的字段。在Reduce阶段,进行聚合和统计。 首先,需要解析CSV文件。可能需要处理CSV中的逗号分隔值,注意字段中的逗号是否被正确义,比如字段内容用引号括起来的情况。如果CSV结构复杂,可能需要使用CSV解析库,但MapReduce默认情况下是按行处理,用逗号分割可能需要自定义解析逻辑。 假设CSV的每一行对应一个景点的记录,包含字段如用户ID、攻略数量、景点名称、点评数量、简介、纬度、经度、评分等。或者可能存在每个用户的访问记录,每条记录代表一个用户访问某个景点,这种情况下需要统计每个景点的访问用户数。 如果数据是每个用户的访问记录,比如每行代表一个用户的一次访问,那么统计每个景点的访问人数就是按景点名称分组,统计用户数(可能需要去重用户ID)。而如果每行是景点信息,包含访问过的用户列表,那么可能需要解析用户列表,统计数量。 这里可能需要用户的数据结构信息,但用户没有提供,所以需要做一些假设。假设CSV的每一行是一个景点的信息,每个景点有多个属性,包括来过的驴友(用户ID列表)、攻略数量、点评数量等。或者可能每行是一次用户访问记录,包含用户ID、景点名称、评分等信息。 如果每行是用户访问记录,那么处理方式不同。例如: 用户ID,景点名称,评分,点评内容,攻略数量,纬度,经度,... 这种情况下,统计每个景点的访问用户数(去重用户ID),然后根据访问次数排名,同时可能需要提取每个景点的其他信息,如攻略数量、点评数量等,并确保每个景点的这些信息正确聚合。 例如,在Map阶段,对于每条记录,输出键为景点名称,值为一个包含用户ID和其他字段的对象。在Reduce阶段,统计每个景点的独立用户数(使用Set来去重用户ID),同时可能需要处理攻略数量、点评数量等字段的汇总或取平均值等。 但攻略数量和点评数量可能每个景点记录中已经存在,比如每个景点有多个记录,每个记录可能有不同的攻略数量和点评数量?或者每个景点的攻略数量和点评数量是固定值?需要明确如何处理这些字段。如果每个景点的这些数值是固定的,可能只需要在Reduce阶段取其中一个即可。但如果这些数值需要累加,比如总攻略数量,则需要不同的处理方式。 例如,假设每个记录对应一个用户对某景点的点评,那么攻略数量可能每个记录都是1,需要统计每个景点的总点评数量,即每出现一次记录就加1。而用户数则是统计不同用户ID的数量。 这里可能需要更详细的数据结构,但用户未提供,所以需要基于常规情况假设。 接下来,步骤可能如下: 1. 编写Mapper类,解析CSV的每一行,提取景点名称、用户ID、攻略数量、点评数量、纬度、经度、评分等字段。 假设CSV结构为: 用户ID,景点名称,攻略数量,点评数量,简介,纬度,经度,评分,... 那么Mapper需要读取这些字段。例如,对于每一行,分割后得到各个字段,然后以景点名称作为key,其他字段组合为value。 例如,Mapper的输出是: 键:景点名称 值:一个对象包含用户ID、攻略数量、点评数量、简介、纬度、经度、评分等。 但需要注意,在MapReduce中,每个Mapper输出的是键值对,其中值可以是自定义的Writable类型,包含多个字段。 2. Reducer需要处理同一景点名称下的所有记录,统计独立用户数(通过收集所有用户ID并去重),攻略数量可能需要累加或取最大值,点评数量同样处理,而简介、纬度、经度、评分可能每个景点的这些信息是相同的,因此只需取其中一个即可。 例如,在Reducer中,对于每个景点名称: - 收集所有用户ID,存入Set中,统计数量即为驴友数量。 - 攻略数量可能每个记录的数值不同,但如果是每条记录代表一次攻略,则累加攻略数量;或者如果攻略数量是每个景点的属性,则只需取其中一个值。 - 点评数量同理,可能累加每个记录的点评数量,或取固定值。 - 简介、纬度、经度、评分这些字段可能在每个记录中相同,所以取第一个即可。 因此,Reducer的输出应包括景点名称、驴友数量(Set的大小)、攻略总数、点评总数、简介、纬度、经度、平均评分或最高评分等,具体取决于需求。 3. 排名处理:MapReduce默认会对键进行排序,但这里的排名可能需要根据驴友数量或评分等进行全局排序。由于每个Reducer处理一个景点,输出的结果可能需要在另一个MapReduce任务中进行排序,或者使用二次排序技术。 例如,第一个MapReduce任务统计每个景点的各个指标,输出结果。第二个MapReduce任务读取这些结果,按驴友数量或评分进行排序,生成排名。 或者,在第一个Reduce阶段输出时,以驴友数量作为键,景点名称和其他信息作为值,这样在shuffle阶段会按键排序,然后使用一个Reducer来输出排名,但需要处理相同数量的情况。 不过,全局排序可能需要将所有数据发送到一个Reducer,这可能影响性能。另一种方法是使用TotalOrderPartitioner进行分区,但可能需要采样数据。 这里可能需要分两个步骤:首先统计各景点的指标,然后进行排序。 综上,步骤可能分为两个MapReduce作业: 作业1:统计每个景点的各项指标。 Mapper1:解析每行数据,输出键为景点名称,值为包含用户ID、攻略数、点评数、简介、纬度、经度、评分的对象。 Reducer1:对于每个景点名称,统计独立用户数,总攻略数,总点评数,并保留简介、纬度、经度、平均评分(或其他统计方式)。 作业2:对作业1的输出进行排序,例如按驴友数量降序排列,生成排名。 Mapper2:读取作业1的输出,将驴友数量作为键,景点名称和其他信息作为值,以便排序。 Reducer2:收集排序后的结果,分配排名,输出最终结果。 但需要确保在作业2中,所有数据被正确排序。可能需要在Mapper2中将键设为驴友数量,并设置为降序排列,然后使用一个Reducer来输出排序后的结果,并添加排名。 不过,在MapReduce中,默认按键排序是升序,如果要降序,可以自定义排序比较器。 接下来,具体实现步骤: 1. 编写第一个MapReduce作业: - Mapper类:读取CSV文件,解析每行,提取景点名称作为键,其他字段(用户ID、攻略数、点评数等)作为值。需要注意处理可能的CSV格式问题,比如字段中包含逗号的情况,可能需要使用CSV解析库,如OpenCSV,或者在代码中处理。 例如,使用Java的String.split(",")可能不够,因为字段内容可能包含逗号。这里可能需要更复杂的解析方法,或者假设数据格式简单,没有包含逗号的字段。 假设数据格式简单,可以直接用split分割。例如: String[] columns = value.toString().split(","); 然后,假设各列的位置固定,如: 用户ID -> columns[0] 景点名称 -> columns[1] 攻略数量 -> columns[2] 点评数量 -> columns[3] 简介 -> columns[4] 纬度 -> columns[5] 经度 -> columns[6] 评分 -> columns[7] 则Mapper的输出键是景点名称,值包括用户ID、攻略数量、点评数量、简介、纬度、经度、评分。 在Map阶段,可能需要构造一个自定义的Writable类型来传递这些值。例如,定义一个TouristValue类,包含用户ID、攻略数量、点评数量、简介、纬度、经度、评分等字段,并实现Writable接口。 不过,考虑到在Reducer中需要统计独立用户数,可能需要将用户ID单独传递,而其他字段可能需要处理,比如攻略数量可能需要累加或取最大值。或者,可能每个记录的攻略数量是该用户贡献的攻略数量,总攻略数量需要累加所有记录的攻略数? 这需要明确业务逻辑。例如,如果攻略数量字段是该用户为该景点写的攻略数量,那么总攻略数应该累加所有记录的攻略数量。如果该字段是景点的总攻略数量,每个记录相同,则只需取其中一个值。 假设攻略数量和点评数量是每个用户对该景点的贡献,那么需要累加;而简介、纬度、经度、评分是景点的固有属性,每个记录相同,所以只需取第一个出现的值。 因此,在Reducer中,需要: - 收集所有用户ID到Set中,统计数量。 - 累加攻略数量和点评数量。 - 取简介、纬度、经度、评分中的第一个出现的值(假设所有记录中这些值相同)。 因此,自定义的Writable可能需要包含用户ID、攻略数量、点评数量,而其他字段可以单独处理,或者在Reducer中从第一个记录获取。 但为了传递这些信息,可能需要在Mapper输出的值中包含用户ID、攻略数、点评数,以及简介、纬度、经度、评分。例如,定义一个自定义Writable: public class TouristDataWritable implements Writable { private String userId; private int strategyCount; private int reviewCount; private String intro; private double latitude; private double longitude; private double rating; // 构造函数、getter、setter、write、readFields方法 } 然后在Mapper中,解析每行数据,填充这些字段,并作为值输出。 在Reducer中,对于每个景点,遍历所有值: - 将每个userId添加到HashSet中,统计独立用户数。 - 累加strategyCount和reviewCount。 - 从第一个值中获取intro、latitude、longitude、rating。 因此,Reducer的输出是景点名称,以及统计后的独立用户数、总攻略数、总点评数,以及简介、纬度、经度、评分。 接下来,第二个MapReduce作业用于排序: Mapper读取第一个作业的输出,键是景点名称,值是统计后的数据。在Mapper中,将驴友数量作为新的键,将其他信息作为值,以便根据驴友数量排序。 例如,Mapper输出的键是IntWritable类型的驴友数量,值是Text类型,包含景点名称和其他字段的组合。 然后,在MapReduce中,设置SortComparator为降序排列,这样在进入Reducer之前,数据会按驴友数量从高到低排序。 Reducer接收到按驴友数量降序排列的数据,然后为每个记录分配排名。由于可能多个景点有相同的驴友数量,排名处理需要考虑并列情况。 例如,在Reducer中维护一个计数器,记录当前排名。每次处理一个键(驴友数量),如果当前键与前一个不同,则更新排名计数器。然后输出排名、景点名称及其他信息。 但Reducer需要处理所有记录,因此可能需要将数据发送到单个Reducer,或者在多个Reducer中处理,但排名可能不准确。因此,可能需要使用一个Reducer来处理全局排序。 综上,具体步骤: 作业1: Mapper1: 输入:CSV文件的每一行 处理: 解析出景点名称、用户ID、攻略数量、点评数量、简介、纬度、经度、评分。 输出键:景点名称(Text) 输出值:自定义Writable包含用户ID、攻略数、点评数、简介、纬度、经度、评分。 Reducer1: 输入键:景点名称 输入值:Iterable<TouristDataWritable> 处理: 初始化HashSet存储用户ID,累加攻略数和点评数,记录简介等字段。 输出键:景点名称 输出值:独立用户数、总攻略数、总点评数、简介、纬度、经度、评分。 作业2: Mapper2: 输入:作业1的输出 处理: 读取每条记录,提取独立用户数作为键,其他信息(景点名称、总攻略数等)作为值。 为了降序排序,可以将键设为负数,或者自定义比较器。 例如,将键设为驴友数量,但使用自定义的DescendingComparator。 在Job2中设置: job.setSortComparatorClass(DescIntComparator.class); DescIntComparator类实现RawComparator,对整数进行降序排序。 Reducer2: 输入键:驴友数量(降序排列) 输入值:景点名称和其他信息 处理: 维护一个排名变量,遍历所有记录,为每个景点分配排名。注意处理相同数量的情况(并列排名)。 输出最终结果:排名、景点名称、独立用户数、总攻略数、总点评数、简介、纬度、经度、评分。 最后,输出结果可能需要包含用户所需的所有字段:驴友数量、排名、攻略数量、景点名称、点评数量、简介、纬度、经度、评分。 需要注意的是,在Reducer1中,如何获取简介、纬度、经度、评分。假设每个景点的这些信息在所有记录中相同,Reducer1只需要取第一个出现的值即可。如果存在不一致的情况,可能需要处理,比如取平均值(对于评分)或多数值,但用户需求中评分可能需要平均或其他统计方式。需要根据用户需求调整。例如,评分可能需要计算平均值,即累加所有记录的评分,然后除以记录数量。或者,每个记录的评分是该用户对景点的评分,平均评分是该景点的平均分。因此,在Reducer1中,可能需要累加评分总和和记录数,然后计算平均分。 这可能需要调整自定义Writable和Reducer的处理逻辑。例如,在TouristDataWritable中,除了用户ID、攻略数、点评数,还需要评分。然后在Reducer中,累加评分总和,并统计记录数,最后计算平均分。 因此,在Reducer1的处理中: - 独立用户数:用户ID的Set大小 - 总攻略数:各记录的攻略数之和 - 总点评数:各记录的点评数之和 - 平均评分:各记录的评分总和 / 记录数 - 简介、纬度、经度:取第一个记录的值(假设一致) 这样,Reducer1的输出包括平均评分。 综上,整个流程可能需要调整数据处理逻辑,特别是对于评分的处理。用户需求中的“评分”可能需要平均分,所以需要计算总和和记录数,再求平均。 因此,在自定义Writable中,可能需要传递用户ID、攻略数、点评数、评分,而简介、纬度、经度作为固定字段。 Reducer1的处理步骤: 1. 遍历所有值: a. 将用户ID添加到HashSet中。 b. 累加攻略数、点评数、评分总和。 c. 记录处理的总记录数。 d. 保存第一个值的简介、纬度、经度。 2. 计算平均评分:评分总和 / 记录数 3. 输出键为景点名称,值为独立用户数、总攻略数、总点评数、简介、纬度、经度、平均评分。 这样,在作业1的输出中,每个景点有这些统计后的指标。 作业2进行排序,按独立用户数降序排列,生成排名。 Reducer2处理时,需要为每个景点分配排名。例如,排名1是用户数最多的景点,依次类推。如果多个景点用户数相同,它们的排名相同,下一个景点跳过相应数量的位置。例如,两个景点并列第一,则下一个是第三名。 但具体排名方式需根据用户需求确定。用户可能希望标准排名方式,例如并列后下一个排名是当前位置+并列数。 在Reducer2中,因为所有记录已经按用户数降序排列,可以维护一个变量跟踪当前排名和上一个用户数。例如: 初始化 rank = 0,prevCount = -1,position = 0 对于每个输入记录: position += 1 if 当前用户数 != prevCount: rank = position prevCount = 当前用户数 输出排名为 rank 这样,如果多个记录用户数相同,它们的排名相同,但下一个不同用户数的排名会是当前位置。例如: 用户数:10,10,9,8,8,8 则排名为1,1,3,4,4,4 这可能符合常见的排名方式。 但用户可能需要不同的处理方式,这里需要假设用户希望这样的排名。 综上,代码实现的大致思路: 作业1的Mapper读取CSV,提取景点名称作为键,其他字段作为值。 作业1的Reducer统计用户数、攻略数、点评数、平均评分,并提取简介等字段。 作业2的Mapper将用户数作为键,其他信息作为值,以便排序。 作业2的Reducer遍历排序后的记录,分配排名,并输出结果。 现在,我需要考虑如何将这些步骤化为具体的MapReduce代码,以及如何处理各个字段的解析和计算。 另外,需要注意输入输出格式。作业1的输入是TextInputFormat,输出可以是TextOutputFormat,或者SequenceFileOutputFormat。作业2的输入是作业1的输出,同样使用TextInputFormat或其他。 在编写代码时,需要定义自定义的Writable类型,用于在Map和Reduce之间传递多个字段的值。例如,对于作业1的Mapper输出值,需要包括用户ID、攻略数、点评数、评分。而Reducer需要收集这些值进行统计。 可能还需要处理数据类型,如攻略数、点评数为整数,纬度和经度为浮点数,评分为浮点数,简介为字符串等。 此外,异常处理也很重要,例如CSV行可能损坏,字段数量不符,需要跳过这些错误行。 综上,实现步骤总结: 1. 编写第一个MapReduce作业: a. Mapper解析CSV行,输出(景点名称, TouristDataWritable)。 b. Reducer统计每个景点的独立用户数、总攻略数、总点评数、平均评分,并保留简介、纬度、经度。 2. 编写第二个MapReduce作业: a. Mapper读取作业1的输出,将驴友数量作为键,其余信息作为值。 b. 使用自定义比较器对键进行降序排序。 c. Reducer分配排名,输出结果。 现在,具体代码可能如下(简化的伪代码): 作业1的Mapper: public class StatsMapper extends Mapper<LongWritable, Text, Text, TouristDataWritable> { protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] columns = value.toString().split(","); // 假设列顺序:用户ID,景点名称,攻略数,点评数,简介,纬度,经度,评分 String userId = columns[0]; String spotName = columns[1]; int strategyCount = Integer.parseInt(columns[2]); int reviewCount = Integer.parseInt(columns[3]); String intro = columns[4]; double lat = Double.parseDouble(columns[5]); double lng = Double.parseDouble(columns[6]); double rating = Double.parseDouble(columns[7]); TouristDataWritable data = new TouristDataWritable(userId, strategyCount, reviewCount, rating, intro, lat, lng); context.write(new Text(spotName), data); } } 作业1的Reducer: public class StatsReducer extends Reducer<Text, TouristDataWritable, Text, Text> { protected void reduce(Text key, Iterable<TouristDataWritable> values, Context context) throws IOException, InterruptedException { Set<String> userIds = new HashSet<>(); int totalStrategy = 0; int totalReview = 0; double totalRating = 0; int recordCount = 0; String intro = null; Double lat = null; Double lng = null; for (TouristDataWritable data : values) { userIds.add(data.getUserId()); totalStrategy += data.getStrategyCount(); totalReview += data.getReviewCount(); totalRating += data.getRating(); recordCount++; if (intro == null) { intro = data.getIntro(); lat = data.getLat(); lng = data.getLng(); } } double avgRating = totalRating / recordCount; String outputValue = String.format("%d,%d,%d,%s,%f,%f,%f", userIds.size(), totalStrategy, totalReview, intro, lat, lng, avgRating); context.write(key, new Text(outputValue)); } } 作业2的Mapper: public class SortMapper extends Mapper<LongWritable, Text, IntWritable, Text> { protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] parts = value.toString().split("\t"); String spotName = parts[0]; String[] stats = parts[1].split(","); int userCount = Integer.parseInt(stats[0]); String otherData = parts[1]; // 传递其余数据 context.write(new IntWritable(userCount), new Text(spotName + "," + otherData)); } } 自定义排序比较器(降序): public class DescIntComparator extends WritableComparator { protected DescIntComparator() { super(IntWritable.class, true); } @Override public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) { Integer v1 = ByteBuffer.wrap(b1, s1, l1).getInt(); Integer v2 = ByteBuffer.wrap(b2, s2, l2).getInt(); return -v1.compareTo(v2); } } 作业2的Reducer: public class RankReducer extends Reducer<IntWritable, Text, Text, Text> { private int currentRank = 0; private int previousCount = -1; private int position = 0; protected void reduce(IntWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException { int userCount = key.get(); for (Text value : values) { position++; if (userCount != previousCount) { currentRank = position; previousCount = userCount; } String[] parts = value.toString().split(",", 2); String spotName = parts[0]; String stats = parts[1]; String output = String.format("%d,%s", currentRank, stats); context.write(new Text(spotName), new Text(output)); } } } 最后,用户需要运行这两个作业,将结果输出到HDFS的指定目录。 可能的命令行步骤: 作业1: hadoop jar MyJar.jar com.example.StatsJob /input/qunar /output/stats 作业2: hadoop jar MyJar.jar com.example.SortJob /output/stats /output/final_rank 需要注意作业之间的依赖关系,即作业2在作业1完成后执行。 总结来说,用户的问题需要通过两个MapReduce作业来实现:第一个统计每个景点的各项指标,第二个进行排序并生成排名。需要处理CSV解析、自定义Writable类型、降序排序和排名分配等关键点。</think>我们将通过以下步骤使用MapReduce分析Qunar旅游数据: $$流程结构图$$ 输入 -> [统计指标MR作业] -> 中间结果 -> [排序排名MR作业] -> 最终输出 一、数据预处理阶段 1. 原始数据结构假设(需根据实际结构调整): $$字段顺序$$ 用户ID,景点名称,攻略数量,点评数量,简介,纬度,经度,评分 二、第一阶段MR实现指标统计 ```java // Mapper伪代码 map(LongWritable key, Text value) { parse CSV行; emit(景点名称, [用户ID, 攻略数, 点评数, 评分, 简介, 纬度, 经度]); } // Reducer伪代码 reduce(景点名称, values) { Set<String> 用户集合 = 收集所有用户ID; int 总攻略数 = sum(各记录攻略数); int 总点评数 = sum(各记录点评数); double 平均评分 = sum(各评分)/记录数; String 简介 = 取第一个记录的简介; double 纬度 = 取第一个记录的纬度; double 经度 = 取第一个记录的经度; emit(景点名称, 统计结果); } ``` 三、第二阶段MR实现排序排名 ```java // 排序Mapper map(LongWritable key, Text value) { parse 景点统计结果; emit(驴友数量, [景点名称+其他指标]); // 使用负数实现降序 } // 自定义比较器 class DescComparator { compare(a,b) { return b - a; } // 降序排列 } // 排名Reducer reduce(驴友数量, values) { int 当前排名 = 1; for (每个景点) { if (数量变化) 更新排名; emit(排名++, 景点完整信息); } } ``` 四、最终输出格式 $$结果示例$$ | 排名 | 景点名称 | 驴友数量 | 攻略数 | 点评数 | 平均分 | 纬度 | 经度 | |------|----------|----------|--------|--------|--------|------|------| | 1 | 故宫 | 15234 | 2345 | 8765 | 4.8 | 39.92| 116.40| 五、执行命令示例 ```bash # 执行统计作业 hadoop jar analysis.jar StatsJob /input/qunar /tmp/stats # 执行排序作业 hadoop jar analysis.jar SortJob /tmp/stats /output/rank_result ``` 注意事项: 1. CSV解析需处理字段中的特殊字符(如包含逗号需用OpenCSV解析) 2. 纬经度精度建议保留6位小数 3. 评分聚合建议使用加权平均(若存在不同用户的多次评分) 4. 最终结果建议存储为CSV格式便于后续使用 通过以上两个MapReduce作业的级联处理,即可完成旅游数据的多维分析需求。实际开发时建议使用Hive或Spark SQL实现会更高效,但本方案保持原生MapReduce实现逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值