[
  {
    "path": ".gitignore",
    "content": "*.txt\n/TEST/\n.idea/\n/.idea\n/.idea/\n.idea"
  },
  {
    "path": "README.md",
    "content": "# ChatLog\n\n通过QQ导出的QQ群聊天记录进行一定的分析。\n\njust a toy\n\n## 开发日志\n\n2020-08-27 完成数据处理部分代码清洗，优化部分代码。\n\n## 基本功能\n\n* QQ群聊天记录的数据清洗\n* 构建简单用户画像\n* 简单分析与统计\n* 部分数据可视化\n\n## 安装说明\n\npython版本：`3.6.x`\n\n系统平台：`windows`\n\n需要的第三方库：`pymongo,pandas,jieba,seaborn,numpy`\n\n以上均可通过`pip install `安装\n\n需要的软件：`MongoDB`\n\n## 说明\n\n### 1.base\n\n- read_chatlog.py\n\n  对导出的.txt聊天记录文件进行数据清洗。\n\n  `注意：腾讯导出的聊天记录是UTF-8+bom的 需改成 -bom`\n\n  清洗后的数据存入mongo数据库中，具体数据如下：\n\n  | 数据项  | 说明           |\n  | ---- | :----------- |\n  | time | 消息发送时间       |\n  | ID   | QQ号或邮箱       |\n  | name | 发送该消息时所使用的马甲 |\n  | text | 发送消息的内容      |\n\n  \n\n- user_profile.py\n\n  通过清洗好的数据构建用户画像，并保存到mongo数据库中。\n\n  用户基本画像数据如下：\n\n  | 数据项         | 说明                 |\n  | ----------- | ------------------ |\n  | ID          | QQ号或邮箱             |\n  | name_list   | 可统计得到的所有马甲         |\n  | speak_num   | 发言次数               |\n  | word_num    | 发言字数               |\n  | photos_num  | 发送图片数              |\n  | week_online | 记录着周一到周日每天每小时的活跃数据 |\n  | ban_time    | 被禁言时间(有待改进)        |\n\n\n\n\n\n- seg_word.py\n\n  通过[jieba](https://github.com/fxsjy/jieba)分词工具将文本进行分析，统计词频并去停用词后保存。\n\n- chinese_stopword.txt\n\n  停用词典。\n\n  \n\n### 2.analysis\n\n- individual.py\n\n  个人数据统计，分析发言次数最多，发送字数最多，发送图片最多，被禁言时长最长的用户。\n\n- collecticity.py\n\n  总体数据分析，分析群活跃时间。\n\n- interesting.py\n\n  因吹斯听的分析。\n\n  - 马甲最长的聚聚\n  - 改名次数最多的聚聚\n  - 群内队形（+1）次数最多的内容，即使局部打断也可统计。\n\n- content.py\n\n   开发中\n\n\n\n### 3.visualization\n\n- charts.py\n\n  将部分数据可视化。如下：\n\n  用户活跃时间heatmap,横轴为一天0-24时,纵轴为周一到周日。颜色越深的方块活跃程度越高。\n\n  ![heatmap](https://github.com/DingHanyang/chatLog/blob/master/photos/user_time_online.png?raw=true)\n\n  用户发言数TOP10及发表图片所占比例。\n\n  ![photos](https://github.com/DingHanyang/chatLog/blob/master/photos/speak_photo_in_total.png?raw=true)\n\n\n\n- word_img.py\n\n  构建词云。分析群内常用词及部分话题，如下：\n\n  针对所有信息进行词云\n\n  - 构建词长度大于0:\n\n  ![Word1](https://github.com/DingHanyang/chatLog/blob/master/photos/all_wordcloud0.png?raw=true)\n\n  - 词长度大于1:\n\n  ![word2](https://github.com/DingHanyang/chatLog/blob/master/photos/all_wordcloud1.png?raw=true)\n\n  - 词长度大于3：\n\n  ![all_wordcloud3.png](https://github.com/DingHanyang/chatLog/blob/master/photos/all_wordcloud3.png?raw=true)\n\n  针对群聊天记录构建的词云：\n\n  因为测试数据为技（zhuang）术（bi）群，所以本项不具有通用性。具体实现在[此处](https://github.com/DingHanyang/chatLog/blob/master/visualization/Wordcloud.py)\n\n  - 针对经常谈论的公司：\n\n    ![word4](https://github.com/DingHanyang/chatLog/blob/master/photos/company_wordcloud.png?raw=true)\n\n  - 针对谈论的编程语言：\n\n    ![word5](https://github.com/DingHanyang/chatLog/blob/master/photos/PL_wordcloud.png?raw=true)\n\n    \n\n\n## 运行\n\n0.clone本项目到本地。\n\n1.手动从QQ消息管理器中导出消息，注意改为UTF-8-BOM。并将其命名为chatlog.txt放置于run.py同级目录下。\n\n2.开启mongodb服务，运行run.py\n\n3.易于修改的参数有：\n\n- 群等级标签：[DataClean.py](https://github.com/DingHanyang/chatLog/blob/master/base/DataClean.py)    line83:根据不同群等级标签修改。不改无妨，影响用户名称显示\n- 词云样式及背景图片：[Wordcloud](https://github.com/DingHanyang/chatLog/blob/master/visualization/Wordcloud.py) \n- 词云屏蔽词：[Wordcloud](https://github.com/DingHanyang/chatLog/blob/master/visualization/Wordcloud.py) line45:此处已经屏蔽‘图片’，‘表情’，‘说’\n\n\n\n## 最后\n\n填坑中，希望收到改进意见。\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "chatlog/analysis/collectivity.py",
    "content": "\"\"\"\n    总体数据统计分析\n    @author:DingHanyang\n\"\"\"\nimport numpy\nfrom pymongo import MongoClient\n\n\nclass Collectivity(object):\n    def __init__(self):\n        self.client = MongoClient()  # 默认连接 localhost 27017\n        self.db = self.client.chatlog\n        self.post = self.db.vczh\n\n    def get_all_speak_info(self):\n        \"\"\"\n        群总体在线时间分布\n        :return:\n        \"\"\"\n        post = self.db.profile\n        week_online = numpy.zeros((7, 24), dtype=numpy.int)\n        for doc in post.find({}, {'week_online': 1}):\n            week_online += numpy.array(doc['week_online'])\n\n        return week_online.tolist()\n\n    def close(self):\n        self.client.close()\n"
  },
  {
    "path": "chatlog/analysis/content.py",
    "content": "\"\"\"\n    聊天内容分析\n    @author:DingHanyang\n\"\"\"\nfrom pymongo import MongoClient\n\n\nclass ChatText(object):\n    def __init__(self):\n        self.client = MongoClient()  # 默认连接 localhost 27017\n        self.db = self.client.chatlog\n        self.post = self.db.vczh\n\n        # TODO:一个大坑未填\n"
  },
  {
    "path": "chatlog/analysis/individual.py",
    "content": "\"\"\"\n    个体数据统计分析\n    @author:DingHanyang\n\"\"\"\nfrom pymongo import MongoClient\n\n\nclass Individual(object):\n    def __init__(self):\n        self.client = MongoClient()  # 默认连接 localhost 27017\n        self.db = self.client.chatlog\n        self.post = self.db.profile\n\n    def most_speak(self, send_class='speak_num'):\n        \"\"\"\n        发言次数最多、发送字数最多、发送图片最多的用户排行\n        :param send_class:选择 speak_num,word_num,photo_num\n        :return:[(ID1,name1,num),(ID1,name2,num),...]\n        \"\"\"\n        top_list = []\n        for doc in self.post.find({}, {send_class: 1, 'name_list': 1, 'ID': 1}):\n            top_list.append((doc['ID'], doc['name_list'][len(doc['name_list']) - 1], doc[send_class]))\n\n        return sorted(top_list, key=lambda x: x[2], reverse=True)\n\n    def longest_ban(self):\n        \"\"\"\n        被禁言时间最长的人榜单\n        :return:[(name1,time),(name2,time),...]\n        \"\"\"\n        self.post = self.db.profile\n        top_list = []\n        for doc in self.post.find({}, {'ID': 1, 'ban_time': 1, 'name_list': 1}):\n            top_list.append((doc['ID'], doc['name_list'][len(doc['name_list']) - 1], doc['ban_time']))\n\n        return sorted(top_list, key=lambda x: x[2], reverse=True)\n\n    def close(self):\n        self.client.close()\n"
  },
  {
    "path": "chatlog/analysis/interesting.py",
    "content": "\"\"\"\n    因吹斯听 分析及统计\n    @author:DingHanyang\n\"\"\"\nfrom pymongo import MongoClient\n\n\nclass Interesting(object):\n    def __init__(self):\n        self.client = MongoClient()  # 默认连接 localhost 27017\n        self.db = self.client.chatlog\n        self.post = self.db.vczh\n\n    def longest_name(self):\n        \"\"\"\n        取出所有用户的name,并排序。\n        ..note::由于聊天记录时间跨度大,有的聚聚改名频繁,而有些名字由于QQ保存的原因保存成了QQ号而丢失\n        :return:top_list[('name',len(name)),(...,...),...] 按长度从大到小排序\n        \"\"\"\n        res_list = []\n        for doc in self.post.find({}, {'name': 1}):\n            res_list.append(doc['name'])\n        res_list = {}.fromkeys(res_list).keys()\n\n        top_list = []\n        for li in res_list:\n            top_list.append((li, len(li)))\n\n        return sorted(top_list, key=lambda x: x[1], reverse=True)\n\n    def longest_formation(self):\n        \"\"\"\n        所有记录中,跟队形最长的聊天记录。\n        :return:top_list[('text',len(text)),(...,...),...] 按长度从大到小排序\n        \"\"\"\n        res_list = []\n        for doc in self.post.find({}, {'text': 1}):\n            res_list.append(doc['text'])\n\n        top_list = []\n        # text 数据存储形式 [[sentences1],[sentences2],...] 队形大多只有一句 所以只考虑text长度为1的\n        i = 0\n        while i < len(res_list) - 1:\n            if res_list[i][0] == '[图片]':\n                i += 1\n            elif res_list[i][0] == res_list[i + 1][0]:\n                pos = i + 1\n                while pos < len(res_list) - 1:\n                    if res_list[pos][0] == res_list[pos + 1][0]:\n                        pos += 1\n                    else:\n                        if pos - i + 1 > 2:\n                            top_list.append((res_list[i][0], pos - i + 1))\n                        i = pos + 1\n                        break\n            else:\n                i += 1\n\n        # 例如中间有人插话一句将队形打断的话,整合队形\n        k = 0\n        while k < len(top_list) - 1:\n            if top_list[k][0] == top_list[k + 1][0]:\n                top_list.append((top_list[k][0], top_list[k][1] + top_list[k + 1][1]))\n                top_list.pop(k - 1)\n                top_list.pop(k)\n            else:\n                k += 1\n        return sorted(top_list, key=lambda x: x[1], reverse=True)\n\n    def close(self):\n        self.client.close()\n"
  },
  {
    "path": "chatlog/base/constant.py",
    "content": "# re\nJUDGE_TIME_RE = '^(((20[0-3][0-9]-(0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|(20[0-3][0-9]-(0[2469]|11)-(0[1-9]|[12][' \\\n                '0-9]|30))) (20|21|22|23|[0-9]|[0-1][0-9]):[0-5][0-9]:[0-5][0-9])'\nJUDGE_ID_RE = '[(][1-9]\\d{4,}[)]$|[<][A-Za-z\\d]+([-_.][A-Za-z\\d]+)*@([A-Za-z\\d]+[-.])+[A-Za-z\\d]{2,4}[>]$'\n"
  },
  {
    "path": "chatlog/base/read_chatlog.py",
    "content": "import re\nfrom pymongo import MongoClient\n\nfrom chatlog.base import constant\n\n\nclass ReadChatlog(object):\n    def __init__(self, file_path, db_name='chatlog', collection_name='vczh'):\n        self.file_path = file_path\n        self.client = MongoClient()  # 默认连接 localhost 27017\n        self.db = self.client[db_name]\n        self.post = self.db[collection_name]\n\n        # 初始化两个常用正则\n        self.time_pattern = re.compile(constant.JUDGE_TIME_RE)\n        self.ID_pattern = re.compile(constant.JUDGE_ID_RE)\n\n    def _judge_start_line(self, message):\n        \"\"\"\n        判断某行是不是起始行\n        条件1:YYYY-MM-DD HH-MM-SS开头(长度大于19)\n        条件2::(XXXXXXXXX)或者<xxx@xxx.xxx>结尾\n        :return: False or (time,ID)\n        \"\"\"\n        if len(message) > 19 and (self.time_pattern.match(message)) and (self.ID_pattern.search(message)):\n            return self.time_pattern.search(message).group(), self.ID_pattern.search(message).group()\n        return False\n\n    def work(self):\n        \"\"\"\n        腾讯导出的聊天记录是UTF-8+bom的 手动改成-bom\n        进行数据清洗,将原始数据划分成块保存进mongodb中\n        ..note::例子\n            time:YYYY-MM-DD HH-MM-SS\n            ID:(XXXXXXXXX)或者<xxx@xxx.xxx>\n            name:username\n            text:['sentence1','sentence2',...]\n        \"\"\"\n        print('----------正在进行数据清洗-------------')\n\n        with open(self.file_path, 'r', encoding='utf-8') as chatlog_file:\n            chatlog_list = [line.strip() for line in chatlog_file if line.strip() != \"\"]\n\n        now_cursor = 0  # 当前分析位置\n        last = 0  # 上一个行首位置\n        flag = 0\n        first_line_info = self._judge_start_line(str(chatlog_list[now_cursor]))\n        while now_cursor < len(chatlog_list):\n            if self._judge_start_line(str(chatlog_list[now_cursor])):\n                if not flag:\n                    first_line_info = self._judge_start_line(str(chatlog_list[now_cursor]))\n                    last = now_cursor\n                    flag = 1\n                else:\n                    flag = 0\n                    send_time = first_line_info[0]\n                    send_id = first_line_info[1]\n                    # 如果什么消息都没发直接不插入\n                    if not chatlog_list[last + 1:now_cursor]:\n                        continue\n                    # 发送该消息时用户的马甲\n                    name = chatlog_list[last].replace(send_id, \"\").replace(send_time, \"\").lstrip()\n\n                    for extra_char in '()<>':\n                        send_id = send_id.replace(extra_char, \"\")\n\n                    # 由于等级标签有极大部分缺失，所以直接去除\n                    # TODO:消息中大频率出现的标签应该就是等级标签，应自检测\n                    for i in ['【实习】', '【能写代码】', '【专属骚头衔】', '【群地位倒数】', '【实习】', '【管理员】']:\n                        if name[:len(i)] == i:\n                            name = name.replace(i, \"\")\n\n                    # 将时间格式统一\n                    for li in '0123456789':\n                        send_time = send_time.replace(' ' + li + ':', ' 0' + li + ':')\n\n                    self.post.insert_one({'time': send_time, 'ID': send_id, 'name': name,\n                                          'text': chatlog_list[last + 1:now_cursor]})\n\n                    print('time:', send_time, 'ID:', send_id, 'name:', name)\n                    print(chatlog_list[last + 1:now_cursor])\n                    print(\"------------------------------------------------\")\n                    continue\n            now_cursor += 1\n        self.client.close()\n        print('----------数据清洗完成-------------')"
  },
  {
    "path": "chatlog/base/seg_word.py",
    "content": "from collections import Counter\nimport jieba\nfrom pymongo import MongoClient\n\n\nclass SegWord(object):\n    def __init__(self):\n        self.client = MongoClient()  # 默认连接 localhost 27017\n        self.db = self.client.chatlog\n        self.post = self.db.vczh\n\n    def work(self):\n        stopword_list = []\n        fp = open('../base/chinese_stopword.txt', 'r', encoding='utf-8')\n        for line in fp.readlines():\n            stopword_list.append(line.replace('\\n', ''))\n        fp.close()\n\n        word_list = []\n        for doc in self.post.find({}, {'text': 1}):\n            print(len(word_list))\n            word_list.extend(jieba.lcut(doc['text'][0]))\n        word_dict = Counter(word_list)\n        self.post = self.db.word\n        for key in word_dict.keys():\n            if str(key) in stopword_list:\n                print(key)\n                continue\n            self.post.insert({'word': key, 'item': word_dict[key]})\n        self.close()\n\n    def close(self):\n        self.client.close()\n\n"
  },
  {
    "path": "chatlog/base/user_profile.py",
    "content": "\"\"\"\n    构建用户基本画像\n    @author:DingHanyang\n\"\"\"\nfrom datetime import datetime\nfrom pymongo import MongoClient\n\n\nclass UserProfile:\n    def __init__(self, db_name='chatlog', collection_name='vczh'):\n        print(\"正在初始化用户画像模块\")\n        self.client = MongoClient()  # 默认连接 localhost 27017\n        self.db = self.client[db_name]\n        self.post = self.db[collection_name]\n\n        self.res_list = [doc for doc in self.post.find({}, {'_id': 0})]\n\n    def close(self):\n        self.client.close()\n\n    def _get_user_id_list(self):\n        \"\"\"\n        获取记录中所有ID的列表\n        :return:[id1,id2,id3,...]\n        \"\"\"\n        user_id_list = [li['ID'] for li in self.res_list]\n\n        user_id_list = list(set(user_id_list))\n        print('记录中共有', len(user_id_list), '位聚聚发过言')\n\n        return user_id_list\n\n    def _get_all_name(self, user_id):\n        \"\"\"\n        根据ID返回一个用户所有曾用名\n        :param user_id:用户ID\n        :return:{'name1','name2',...}\n        \"\"\"\n        name_list = set()\n        for li in self.res_list:\n            if li['ID'] == user_id:\n                name_list.add(li['name'])\n\n        return list(name_list)\n\n    def _get_speak_infos(self, user_id):\n        \"\"\"\n        返回一个用户的发言次数,发言文字数,发言图片数\n        :param user_id:用户ID\n        :return:[speak_num,word_num,photo_num]\n        \"\"\"\n        speak_num = 0\n        word_num = 0\n        photo_num = 0\n        for li in self.res_list:\n            if li['ID'] == user_id:\n                speak_num += 1\n                for sp in li['text']:\n                    word_num += len(sp)\n                    photo_num += sp.count('[图片]')\n        return speak_num, word_num, photo_num\n\n    def _get_online_time(self, user_id):\n        \"\"\"\n        返回一个用户在那个时段发言数最多(0-24小时)(周1-7)\n        :param user_id:用户ID\n        :return:[[0,0,0,0],[],[],[],...] [周1-7]包含[0-24小时]\n        \"\"\"\n        time_list = []\n        for li in self.res_list:\n            if li['ID'] == user_id:\n                time_list.append(li['time'])\n\n        week_list = [[0 for _ in range(24)] for _ in range(7)]\n\n        for li in time_list:\n            week_list[int(datetime.strptime(li, \"%Y-%m-%d %H:%M:%S\").weekday())][int(li[11:13])] += 1\n\n        return week_list\n\n    def work(self):\n        \"\"\"\n        分析所有用户基本画像并存入数据库\n        ..note::\n            ID:\n            name:[name1,name2,...]\n            speak_num:发言次数\n            word_num:发言字数\n            photo_num:发布图片数\n            week_online:周活跃分布\n        :return:None\n        \"\"\"\n        post = self.db.profile\n        user_id_list = self._get_user_id_list()\n        for li in user_id_list:\n            print('正在构建用户', li, '的用户画像')\n            name_list = self._get_all_name(li)\n            speak_num, word_num, photo_num = self._get_speak_infos(li)\n            week_online = self._get_online_time(li)\n            ban_time = self._ban_time(li)\n            post.insert_one({'ID': li, 'name_list': name_list, 'speak_num': speak_num,\n                             'word_num': word_num, 'photo_num': photo_num,\n                             'week_online': week_online, 'ban_time': ban_time})\n        self.close()\n\n    # TODO 管理员若解禁则扣除时间\n    def _ban_time(self, user_id):\n        \"\"\"\n        统计用户累计禁言时间\n        :return:\n        \"\"\"\n\n        def add_time(add_list):\n            time = 0\n            for times in add_list:\n                for info in [('天', 60 * 24), ('小时', 60), ('分钟', 1)]:\n                    if info[0] in times:\n                        index = times.find(info[0])\n                        if times[index - 2].isdigit():\n                            time += int(times[index - 2:index]) * info[1]\n                        else:\n                            time += int(times[index - 1:index]) * info[1]\n            return time\n\n        name_list = self._get_all_name(user_id)\n        res_list = []\n        for li in self.post.find({'ID': '10000'}, {'text': 1}):\n            if '被管理员禁言' in li['text'][0]:\n                res_list.append(li['text'][0].split(' 被管理员禁言'))\n\n        time_list = []\n        for li in res_list:\n            for name in name_list:\n                if li[0] == name:\n                    time_list.append(li[1])\n\n        return add_time(time_list)\n"
  },
  {
    "path": "chatlog/model/message.py",
    "content": "from mongoengine import *\n\n\nclass Message(Document):\n    meta = {'db_alias': 'chatlog'}\n\n    name = StringField()  # 发言用户昵称\n    ID = StringField()  # 发言用户ID\n    time = DateTimeField()  # 该消息发送时间\n    text = ListField()  # 发送消息的列表\n"
  },
  {
    "path": "chatlog/model/user.py",
    "content": "from mongoengine import *\n\n\nclass User(Document):\n    meta = {'db_alias': 'chatlog'}\n\n    name_now = StringField()  # 用户当前昵称\n    name_list = ListField(StringField)  # 用户历史昵称列表\n    ID = StringField()  # ID用户ID\n    speak_num = IntField()  # 用户发言次数\n    word_num = IntField()  # 用户发送文本分词后的词数\n    photo_num = IntField()  # 用户发送图片的数量\n    ban_num = IntField()  # 用户被禁言的次数\n    ban_time_sum = DateTimeField()  # 用户被禁言时间总和\n"
  },
  {
    "path": "chatlog/run.py",
    "content": "from chatlog.analysis.collectivity import Collectivity\nfrom chatlog.analysis.individual import Individual\nfrom chatlog.analysis.interesting import Interesting\nfrom chatlog.base.read_chatlog import ReadChatlog\nfrom chatlog.base.user_profile import UserProfile\n\nif __name__ == '__main__':\n    RC = ReadChatlog('./chatlog.txt')\n    RC.work()  # 进行聊天记录的清洗并入库\n    UP = UserProfile()\n    UP.work()  # 构建简单的用户画像\n\n    # Collectivity\n    # col = Collectivity()\n    # print(col.get_all_speak_info())  # 群聊天时间分布\n\n    # Individual\n    # ind = Individual()\n    # print(ind.longest_ban())  # 禁言时长的排名\n    # print(ind.most_speak('speak_num'))  # 发言次数的排名\n\n    # Interesting\n    # interest = Interesting()\n    # print(interest.longest_formation())  # 最长队形的排名\n    # print(interest.longest_name())  #  最长的马甲排名\n"
  },
  {
    "path": "chatlog/visualization/charts.py",
    "content": "# -*- coding=utf-8 -*-\n\"\"\"\n    数据可视化模块\n\"\"\"\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom pymongo import MongoClient\n\nfrom chatlog.analysis.individual import Individual\n\n\nclass Charts(object):\n    def __init__(self):\n        self.client = MongoClient()  # 默认连接 localhost 27017\n        self.db = self.client.chatlog\n        self.post = self.db.profile\n\n    def ban_time(self):\n        ind = Individual()\n        res_list = ind.longest_ban()\n        res_list = res_list[0:10]\n        print(res_list)\n        name_list = [i[1] for i in res_list]\n        time_list = [i[2] for i in res_list]\n\n        sns.set(style=\"darkgrid\")\n        plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体\n        f, ax = plt.subplots(figsize=(18.5, 9))\n\n        # Plot the crashes where alcohol was involved\n        sns.set_color_codes(\"muted\")\n        sns.barplot(x=time_list, y=name_list,\n                    label=\"禁言时长\", color=\"y\")\n\n        # Add a legend and informative axis label\n        ax.legend(ncol=2, loc=\"lower right\", frameon=True)\n        ax.set(xlim=(0, time_list[0]), ylabel=\"\",\n               xlabel=\"禁言时间最长\")\n        sns.despine(left=True, bottom=True)\n\n        plt.savefig('../photos/ban_time.png', format='png', dpi=400)\n        plt.close()\n\n    def speak_photo_in_total(self):\n        \"\"\"\n        发言次数前10的用户及发送图片的比例\n        :return:\n        \"\"\"\n        ind = Individual()\n        res_list = ind.most_speak('speak_num')\n        res_list = res_list[0:10]\n        print(res_list)\n        ID_list = [i[0] for i in res_list]\n        name_list = [i[1] for i in res_list]\n        speak_list = [i[2] for i in res_list]\n        photo_num = []\n        for id in ID_list:\n            for doc in self.post.find({'ID': id}, {'photo_num': 1}):\n                photo_num.append(doc['photo_num'])\n        sns.set(style=\"darkgrid\")\n        plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体\n        f, ax = plt.subplots(figsize=(18.5, 9))\n\n        sns.set_color_codes(\"pastel\")\n        sns.barplot(x=speak_list, y=name_list,\n                    label=\"发言次数\", color=\"b\")\n\n        # Plot the crashes where alcohol was involved\n        sns.set_color_codes(\"muted\")\n        sns.barplot(x=photo_num, y=name_list,\n                    label=\"发送图片数\", color=\"b\")\n\n        # Add a legend and informative axis label\n        ax.legend(ncol=2, loc=\"lower right\", frameon=True)\n        ax.set(xlim=(0, speak_list[0]), ylabel=\"\",\n               xlabel=\"群里发言次数排行及发送图片的比例\")\n        sns.despine(left=True, bottom=True)\n\n        plt.savefig('../photos/speak_photo_in_total.png', format='png', dpi=300)\n        plt.close()\n\n    def user_online_time(self, user_ID=None):\n        \"\"\"\n        绘制(全体)用户活跃时间图像\n        :param user_ID:默认为空,指定全体用户,不为空时为指定ID用户\n        \"\"\"\n        res_list = []\n        if user_ID:\n            find_dict = {'ID': user_ID}\n        else:\n            find_dict = {}\n\n        week_online = [[0 for _ in range(24)] for _ in range(7)]\n        for doc in self.post.find(find_dict, {'week_online': 1}):\n            for i in range(0, 7):\n                for j in range(0, 24):\n                    if user_ID:\n                        week_online[i][j] = doc['week_online'][i][j]\n                    else:\n                        week_online[i][j] += doc['week_online'][i][j]\n\n        week_online = np.array([li for li in week_online])\n        columns = [str(i) + '-' + str(i + 1) for i in range(0, 24)]\n        index = ['Mon.', 'Tue.', 'Wed.', 'Thu.', 'Fri.', 'Sat.', 'Sun.']\n\n        week_online = pd.DataFrame(week_online, index=index, columns=columns)\n        plt.figure(figsize=(18.5, 9))\n        plt.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体\n        sns.set()\n\n        # Draw a heatmap with the numeric values in each cell\n        sns.heatmap(week_online, annot=True, fmt=\"d\", cmap=\"YlGnBu\")\n        plt.savefig('../photos/user_time_online.png', format='png', dpi=300)\n        plt.close()\n\n    def close(self):\n        self.client.close()\n\n    def work(self):\n        self.speak_photo_in_total()\n        self.user_online_time()\n        self.close()\n\n\nif __name__ == '__main__':\n    chart = Charts()\n    chart.user_online_time()\n"
  },
  {
    "path": "chatlog/visualization/word_img.py",
    "content": "import sys\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom PIL import Image\nfrom pymongo import MongoClient\nfrom wordcloud import WordCloud, ImageColorGenerator\n\n\nclass WordImg(object):\n    def __init__(self):\n        self.client = MongoClient()  # 默认连接 localhost 27017\n        self.db = self.client.chatlog\n        self.post = self.db.word\n\n    def close(self):\n        self.client.close()\n\n    def draw_wordcloud(self, word_dict, name):\n        cat_mask = np.array(Image.open('../visualization/cat.png'))\n        wc = WordCloud(font_path='../visualization/msyh.ttc',\n                       width=800, height=400,\n                       background_color=\"white\",  # 背景颜色\n                       mask=cat_mask,  # 设置背景图片\n                       min_font_size=6\n                       )\n        wc.fit_words(word_dict)\n\n        image_colors = ImageColorGenerator(cat_mask)\n        # recolor wordcloud and show\n        # we could also give color_func=image_colors directly in the constructor\n        plt.imshow(wc)\n        plt.axis(\"off\")\n        plt.savefig('../photos/' + name + '.png', dpi=800)\n        plt.close()\n\n    def PL_wordcloud(self):\n        word_dict = {'JAVA': ['java', 'jawa'], 'C++': ['c++', 'c艹'], 'C': ['c', 'c语言'],\n                     'PHP': ['php'], 'Python': ['py', 'python'], 'C#': ['c#']}\n        self.draw_wordcloud(self.word_fre(word_dict), sys._getframe().f_code.co_name)\n\n    def all_wordcloud(self, word_len=0):\n        word_dict = {}\n        stop_word = ['图片', '表情', '说']\n        for doc in self.post.find({}):\n            if len(doc['word']) > word_len and doc['word'] not in stop_word:\n                word_dict[doc['word']] = doc['item']\n        self.draw_wordcloud(word_dict, sys._getframe().f_code.co_name + str(word_len))\n\n    def company_wordcloud(self):\n        word_dict = {'Microsoft': ['微软', '巨硬', 'ms', 'microsoft'], 'Tencent': ['腾讯', 'tencent', '鹅厂'],\n                     '360': ['360', '安全卫士', '奇虎'], 'Netease': ['netease', '网易', '猪场'],\n                     'JD': ['jd', '京东', '某东', '狗东'], 'Taobao': ['淘宝', '天猫', 'taobao'],\n                     'BaiDu': ['百度', '某度', 'baidu'], 'ZhiHu': ['zhihu', '知乎', '你乎', '某乎'],\n                     'Sina': ['新浪', 'sina', '微博', 'weibo']}\n\n        self.draw_wordcloud(self.word_fre(word_dict), sys._getframe().f_code.co_name)\n\n    def word_fre(self, word_dict):\n        word_fre = {}\n        for key in word_dict.keys():\n            word_fre[key] = 0\n\n        res_dict = {}\n        for doc in self.post.find({}):\n            res_dict[doc['word']] = doc['item']\n\n        for res_key in res_dict.keys():\n            for word_key in word_dict.keys():\n                if str(res_key).lower() in word_dict[word_key]:\n                    word_fre[word_key] = word_fre[word_key] + res_dict[res_key]\n\n        return word_fre\n\n    def longest_formation_wordcloud(self):\n        word_dict = {}\n        fp = open('../visualization/list.txt', 'r')\n        for line in fp.readlines():\n            li = line.split('  ')\n            print(li)\n            word_dict[li[0]] = int(li[1])\n\n        wc = WordCloud(font_path='../visualization/msyh.ttc',\n                       width=1080, height=720,\n                       background_color=\"white\",  # 背景颜色\n                       min_font_size=6\n                       )\n        wc.fit_words(word_dict)\n        plt.imshow(wc)\n        plt.axis(\"off\")\n        plt.savefig('../photos/' + sys._getframe().f_code.co_name + '.png', dpi=800)\n        plt.show()\n        plt.close()\n\n\n\n\n\n\n\n    def work(self):\n        self.PL_wordcloud()\n        self.company_wordcloud()\n        self.all_wordcloud()\n        self.close()\n\n"
  },
  {
    "path": "license",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "photos/README.md",
    "content": ""
  }
]