博客数据清洗篇

一、我们要做什么

csdn有了自己的统一标签之后,就要着眼于对目前所有的数据进行清洗,即打上统一标签,这里先清洗博客数据。如果立即对所有的数据打标签,数据量太大,采用一个折中的方案,先对近一个月有更新的博客数据进行清洗,然后提供接口对有更新的博客进行清洗。博客打标签依赖于多标签分类器,目前已经支持106个类别。

数据清洗还要一个目的,将博客数据挂到我们的技能树上面,即清洗的同时完成对博客数据的知识结构化,这里可以简单理解为归类。同时也会做的一个操作就是,更新技能树上面的样本数据,每个技能树节点挂载的样本数据有一个限值,超过限值的话根据质量得分进行替换。

二、清洗历史数据

目前所有数据都在阿里云的ODPS(Open Data Processing Service,后更名为MaxComputer)里面,近一个月有更新的数据有1500W,当时不知道在ODPS里面查询数据是收费的,采用了分页多次查询的方式,导致消耗了很多费用,正确的方式应该是:查询需要的数据到临时表中,然后再把临时表拉到本地进行处理,然后再将数据写回ODPS中。

3.1 从ODPS拉数据

这里主要介绍利用maxcompute客户端的tunnel命令拉取数据,参考maxcomputer的tunnel操作。通过tunnel拉取数据主要是价格要便宜,能一次性将表中所有数据拉到本地,但是有一个问题就是如果表中有特殊字符将会导致拉取中断,且不能断点续传,字段与字段之间还需要指定特殊的字符进行分割。于是想到采用对字段进行加密的方式,例如base64,刚好odps的sql也支持,使用时再进行解密。

将tags、content、title字段进行base64加密。

CREATE TABLE csdn_dev.tmp_blog_tags_traindata4 as 
SELECT
articleid, 
base64(cast(tags as binary)) as tags, 
base64(cast(content as binary)) as content, 
base64(cast(title as binary)) as title 
FROM tmp_blog_tags_traindata3;

下载MaxCompute客户端安装包(Github)。MaxCompute客户端从v0.28.0版开始支持JDK 1.9,v0.28.0以下版本只支持JDK 1.8。

解压下载的安装包文件,得到bin、conf、lib和plugins文件夹。

进入conf文件夹,配置odps_config.ini文件,配置requried fields。

###################################### Required fields ############################################
project_name=*********
access_id=*********
access_key=**********
end_point=**********
tunnel_endpoint=**********
###################################### Optional fields ############################################
log_view_host=http://logview.odps.aliyun.com
https_check=true
# confirm threshold for query input size(unit: GB)
data_size_confirm=100.0
# this url is for odpscmd update
update_url=
# download sql results by instance tunnel
use_instance_tunnel=
# the max records when download sql results by instance tunnel
instance_tunnel_max_record=
debug=true
# IMPORTANT:
#   If leaving tunnel_endpoint untouched, console will try to automatically get one from odps service, which might charge networking fees in some cases.
#   Please refer to https://help.aliyun.com/document_detail/34951.html
# tunnel_endpoint=

# use set.<key>=
# e.g. set.odps.sql.select.output.format=

拉取数据:

tunnel download tmp_blog_tags_traindata3 ./blog_tag.txt -fd " " -rd "\n";

上传数据:

扫描二维码关注公众号,回复: 16174083 查看本文章

tunnel upload ./blog_tag.txt tmp_blog_tags_traindata3 -fd " " -rd "\n";

注:-fd表示列之间间隔符,-rd表示行之间间隔符。

3.2 清洗数据

博客打统一标签。为了加快处理速度,这里使用分批推理,在有GPU的机器上面推理效果更好。

    def batch_predict_weights(self, batch_input, topN=3):
        """批推理"""
        # 判断是否初始化
        if self.model is None:
            if not self.load_model():
                return
        input_x = tf.convert_to_tensor(batch_input)
        predictions = self.model(input_x)
        predictions = predictions.numpy()
        result_list = []
        for i in range(len(batch_input)):
            res = dict()
            for j in range(len(predictions[i])):
                if predictions[i][j][0] > 0.1:
                    res[self._id2tag_dict[j]] = predictions[i][j][0]
            result_list.append(res)
        return result_list

并且引入对标题和用户自定义标签进行标签抽取。

    def predict_batch(self, batch_list, topn=3):
        batch_result_list = []
        batch_input = [input_x for _, input_x, _ in batch_list]
        result_list = self.model.batch_predict_weights(batch_input)
        for index, tags in enumerate(result_list):
            tags_list = self.tag_distance_predict.predict(batch_list[index][0])
            results = self._merge_result(tags_list, tags, topn)
            batch_result_list.append(results)
        return batch_result_list

调用技能树提供的match_node_while_update()方法,返回数据在技能树中的挂载信息,同时更新技能树节点样本数据。之后将数据写入文本,通过maxcompute客户端的tunnel命令上传到ODPS中。最后表中的数据如下:

article_id tag tree_name node_id score username
439355 java java java-4-413 31 xuehouniao
458839 eclipse java java-6-125 32 yesky12
463767 java java java-6-125 32 xujianhua815926
463767 eclipse java java-6-125 32 xujianhua815926

三、清洗增量数据

对以后的新增博客以及有更新的博客都会调用增量数据清洗接口进行清洗,接口主要做下面几件事情:打标签,匹配技能树,写MQ, 写ODPS。接口由媒资调用触发,除了打标签,匹配技能树, 写ODPS外,增加了写MQ操作,媒资需要打标签的结果,但是接口是异步的,所以这里将结果写入MQ等待其去消费。

接收数据。接口收到的数据首先会写到队列中,写入成功返回success,否则返回failed。

    def blog_tag_clean(self, article_id, title, content, username, user_tags, createtime):
        if not self.data_pool.full():
            self.data_pool.put((article_id, title, content,
                               username, user_tags, createtime))
            return {'err': ErrorCode.SUCCESS}

        logger.warning("Queue is full, waiting for a moment and try again.")
        return {
            'err': ErrorCode.FAILED
        }

打标签。打标签的操作同上面的历史数据清洗。

匹配技能树。匹配技能树同历史数据清洗,但是增加了一些参数,供后面技能树使用。这里有一点需要注意,匹配技能树的接口可以根据配置文件决定是调用本项目的service接口,还是调用http接口,这两个接口返回的结果并不是一样的,需要做一下处理或者统一返回结果。

                input_dict = {
                    'sample_id': article_id,
                    'title': title,
                    'tags': ",".join(tags),
                    'body': content,
                    'tree_name': name,
                    'category': 'blog',
                    'strategy': 'quality',
                    'user_name': username,
                    "write_flag": 1 if write_flag else 0,
                    'date_created': createtime
                }
                ret = self.skill_tree_service.match_node_while_update(
                    input_dict)
                if isinstance(ret, tuple):
                    ret = ret[0]

写MQ。

MQ使用的时阿里的rocketmq,推荐使用阿里云官方提供的SDK,否则出问题了没有人可以咨询。python仅支持http,参考http sdk。有一个地方需要注意,instance_id如果存在一定要填写,否则不能写入数据。

   def open(self):
        self.rocketmq_client = MQClient(
            #设置HTTP接入域名(此处以公共云生产环境为例)
            self._rocketmq_addr,
            #AccessKey 阿里云身份验证,在阿里云服务器管理控制台创建
            self._rocketmq_key,
            #SecretKey 阿里云身份验证,在阿里云服务器管理控制台创建
            self._rocketmq_secret
            )
        #Topic所属实例ID,默认实例为空None
        self.producer = self.rocketmq_client.get_producer(self._instance_id, self._rocketmq_topic)
        self.consumer = self.rocketmq_client.get_consumer(self._instance_id, self._rocketmq_topic, self._rocketmq_group_id, self._rocketmq_group_id)
        return {
            'err': ErrorCode.SUCCESS
        }
    
    def close(self):
        pass
    
    def put(self, data:dict):
        try:
            msg = TopicMessage(json.dumps(data), self._rocketmq_group_id)
            re_msg = self.producer.publish_message(msg)
            # logger.info("Publish Message Succeed. MessageID:%s, BodyMD5:%s" % (re_msg.message_id, re_msg.message_body_md5))
            return {
                'err': ErrorCode.SUCCESS
            }
        except MQExceptionBase as e:
            return {
                'err': ErrorCode.FAILED,
                'msg': str(e)
            }

写ODPS。利用pyodps即可。

    @exception_reconnect
    def write_table(self, table_name, data_list, partition=None):
        """插入数据"""
        ret = self.check_open()
        if ret['err'] != ErrorCode.SUCCESS:
            return ret

        tb = self.odps.get_table(name=table_name)
        if tb is None:
            return {
                'err': ErrorCode.FAILED,
                'msg': "Table:{} is not exists.".format(table_name)
            }
        if partition is not None:
            with tb.open_writer(partition=partition, create_partition=True) as writer:
                writer.write(data_list)
        else:
            with tb.open_writer() as writer:
                writer.write(data_list)

        return {
            'err': ErrorCode.SUCCESS
        }

四、结束语

目前博客新增和更改数据大约每天3W,单机的部署方式也还能处理的过来,接口的性能主要消耗在技能树匹配数据和分类器打标签上面,后续数据量增大时,要考虑优化处理速度以及部署方式。

猜你喜欢

转载自blog.csdn.net/zxm2015/article/details/119839604
今日推荐