python tkinter界面 多进程启动scrapy爬取百度贴吧的回复,显示爬取进度,并可以搜索回帖人,指定时间生成词云图,用pyinstaller打包成exe(九)

对生成的帖子json文件,进行搜索,可以根据回帖内容关键字、或者回帖人 查找回帖内容,还可以依据回复内容,生成词云图

python tkinter界面 多进程启动scrapy爬取百度贴吧的回复,显示爬取进度,并可以搜索回帖人,指定时间生成词云图,用pyinstaller打包成exe(九)_第1张图片

python tkinter界面 多进程启动scrapy爬取百度贴吧的回复,显示爬取进度,并可以搜索回帖人,指定时间生成词云图,用pyinstaller打包成exe(九)_第2张图片

 

 

tk界面,search.py

'''

'''构造:
        toplevel:——notebook,2个页面:
                             ----搜索回复内容(search_frame):---选择文件夹部分(dir_fr):搜索、保存文件夹,搜索进度
                                                           ---搜索回复(search_fr):关键字、回帖人
                                                           ---显示回复(value_text): 显示点击的帖子内容
                                                           ---tree
                             ----获取回复内容(content_frame):---选择文件夹部分(dir_fr):搜索、保存文件夹,搜索进度
                                                            ---设定部分(content_fr):输入时间后搜索、
                                                                                    设定帖子回复文件、模板图片、屏蔽词
                                                            ---显示词云图(wc_label)
                            '''

class Search_tk():
    def __init__(self,my_tk):
        self.root=my_tk
        self.root.title('搜索贴吧')
        self.root.geometry('920x650+400+0')
        
        self.search_dir = StringVar() #要搜索的文件夹路径
        self.save_dir = StringVar()   #搜索后保存的文件路径
        self.board_var=StringVar()    #显示搜索进度的label
        self.set_defaultpath()       #设定的好默认的路径
        
        self.creat_notebook()       #创建notebook及其2个页面
        self.creat_search_frame()
        self.creat_content_frame()



    def creat_notebook(self):
        '''创建notebook,生成2个页面:1.存放搜索帖子内容  2.获取回复生成云图'''
        self.notebook=Notebook(self.root)
        self.notebook.pack()
        #搜索回复内容页面frame
        self.search_frame = Frame(self.root)
        self.search_frame.pack()
        #获取所有回复页面frame
        self.content_frame = Frame(self.root)
        self.content_frame.pack()

        self.notebook.add(self.search_frame, text="搜索页面",sticky='E')
        self.notebook.add(self.content_frame, text="获取回复")

    def creat_dirframe(self,frame):
        '''创建2个页面的共同部分:1.所要搜索的文件夹、2.保存文件的文件夹、3.显示遍历文件进度
            '''
        dir_fr=Frame(frame)
        dir_fr.pack()
        path_label1 = Label(dir_fr, text='目标贴吧:', anchor='e')
        change_button1 = Button(dir_fr, text='更改', command=lambda:self.set_dirpath(self.search_dir))
        search_dir_entry=Entry(dir_fr,width=45, textvariable=self.search_dir)
        path_label1.grid(row=1, column=0)
        search_dir_entry.grid(row=1, column=1)
        change_button1.grid(row=1, column=2)

        path_label2 = Label(dir_fr, text='保存路径:', anchor='e')
        change_button2 = Button(dir_fr, text='更改', command=lambda: self.set_dirpath(self.save_dir))
        save_dir_entry = Entry(dir_fr, width=45, textvariable=self.save_dir)
        path_label2.grid(row=2, column=0)
        save_dir_entry.grid(row=2, column=1)
        change_button2.grid(row=2, column=2)


        self.board_text=tkinter.Label(dir_fr,width=60,font=('system', 14, 'bold'),foreground='blue',background='Wheat',textvariable=self.board_var)
        self.board_var.set('请设定要搜索的文件夹以及搜索条件!!')
        self.board_text.grid(row=4, column=0,columnspan=3)
        tkinter.Label(dir_fr, height=1).grid(row=5, column=0)

    def creat_content_frame(self):
        '''获取所有回复页面,共6部分
           1.选择文件夹部分(共用)
           2.输入搜索时间,获取该时间点后的所有回复
           3.包含所有回复的文件,搜索完成后会自动填入,也可手选
           4.选择词云图所用的模板图片,不设定/设定错误,直接生成为方形
           5.添加屏蔽词,设定词云图不显示的词语
           6.显示词云图label
           '''
        self.creat_dirframe(self.content_frame)# 创建 搜索保存文件夹部分

        content_fr = Frame(self.content_frame)
        content_fr.pack()

        self.the_time = StringVar()
        now_time = datetime.datetime.now()
        yes_time = now_time + datetime.timedelta(days=-1)
        yes_time = yes_time.strftime('%Y-%m-%d %H:%M')
        self.the_time.set(yes_time)     #默认时间设定为当前时间的前一天
        time_label = Label(content_fr, text='输入时间:', anchor='e')
        time_button = Button(content_fr, text='获取回复', command=self.get_content)
        time_combobox = Combobox(content_fr, width=20, textvariable=self.the_time,values=['获取所有内容'])
        format_label=Label(content_fr, text='格式:2018-10-25或2018-10-25 15:00', foreground='gray',anchor='e')
        time_label.grid(row=0, column=0)
        time_combobox.grid(row=0, column=1)
        time_button.grid(row=0, column=3)
        format_label.grid(row=0, column=2)

        self.content_file = StringVar()
        cfile_label = Label(content_fr, text='回复文件:', anchor='e')
        cfile_button = Button(content_fr, text='更改文件',command= lambda:self.set_filepath(self.content_file))
        cfile_entry = Entry(content_fr, width=55, textvariable=self.content_file)
        cfile_label.grid(row=1, column=0)
        cfile_entry.grid(row=1, column=1,columnspan=2)
        cfile_button.grid(row=1, column=3)

        self.pic_file = StringVar()
        pic_label = Label(content_fr, text='模板图片:', anchor='e')
        pic_button = Button(content_fr, text='选择文件', command= lambda:self.set_filepath(self.pic_file))
        pic_entry = Entry(content_fr, width=55, textvariable=self.pic_file)
        pic_label.grid(row=2, column=0)
        pic_entry.grid(row=2, column=1, columnspan=2)
        pic_button.grid(row=2, column=3)

        self.add_stopword = StringVar()
        self.add_stopword.set('就是, 不是, 现在, 没有, 可以, 还是, 这个, 怎么, 什么, 自己, 一个')
        stopword_label = Label(content_fr, text='添加屏蔽词:', anchor='e')
        stopword_entry = Entry(content_fr, width=55, textvariable=self.add_stopword)
        wc_button=Button(content_fr, text='生成图片', command=self.generate_pic)
        stopword_label.grid(row=3, column=0)
        stopword_entry.grid(row=3, column=1, columnspan=2)
        wc_button.grid(row=3, column=3)

        #插入图片的label
        self.wc_label=Label(self.content_frame)
        self.wc_label.pack()

    def creat_search_frame(self):
        '''搜索帖子回复部分,共5部分:
           1.选择文件夹部分(共用)
           2.根据 关键字 搜索回复内容
           3.根据 回帖人 搜索回复内容
           4.text,显示tree中你所点的某个回复
           5.tree,搜索结束后,插入搜索内容
'''

        self.creat_dirframe(self.search_frame)# 创建 搜索保存文件夹部分

        # 搜索关键字/回帖人部分
        search_fr = Frame(self.search_frame)
        search_fr.pack()

        self.target_word = StringVar()
        sword_label = Label(search_fr, text='搜索回复内容----关键字:', anchor='e')
        sword_button = Button(search_fr, text='搜索', command=self.search_keyword)
        sword_entry = Entry(search_fr, width=20, textvariable=self.target_word)
        sword_label.grid(row=0, column=0)
        sword_entry.grid(row=0, column=1)
        sword_button.grid(row=0, column=2)

        self.author = StringVar()
        sauthor_label = Label(search_fr, text='搜索回复内容----回帖人:', anchor='e')
        sauthor_button = Button(search_fr, text='搜索', command=self.search_author)
        sauthor_entry = Entry(search_fr, width=20, textvariable=self.author)
        sauthor_label.grid(row=1, column=0)
        sauthor_entry.grid(row=1, column=1)
        sauthor_button.grid(row=1, column=2)

        # 显示tree所选的item帖子内容
        self.value_text = Text(self.search_frame, width=120, height=3, )
        self.value_text.pack()
        self.value_text.tag_config('default', font=('system', 10))

        #创建tree
        self.creat_tree()

###==========================获取回复内容,生成词云图所用函数========================#
    def generate_pic(self):
        '''回调函数,生成词云图
           运行过程:1.获取文件路径、屏蔽词等
                    2.判断包含回复的文件路径:---正确,判断模板图片路径:---正确,根据模板图片生成词云图
                                                                  ---不对,直接生成方形图
                                          ---不对,报错'''
        pic_list=['.jpg', '.png', '.jpeg', '.bmp']
        pic_file=self.pic_file.get().strip()
        content_file = self.content_file.get().strip()

        #屏蔽词,过滤逗号等符号,转换为list
        stopword=self.add_stopword.get().strip()
        stopword=re.sub(r'[(\s)(,)(,)(\\)(/\)]', ' ', stopword)
        stopword=stopword.split()

        #判断包含回复的文件路径正确与否
        if os.path.exists(content_file) and os.path.splitext(content_file)[1]=='.json':
            try:
                #判断模板图片路径正确与否
                if os.path.exists(pic_file) and os.path.splitext(pic_file)[1] in pic_list :
                    self.board_var.set('根据模板图片生成词云图.......')
                    self.root.update()
                    wc_file=self.generate_wc(stopword,content_file,pic_file)    #生成词云图
                else:
                    self.board_var.set('没有正确选择模板图片,生成方形云图.......')
                    self.root.update()
                    wc_file=self.generate_wc(stopword,content_file)
                self.board_var.set('已生成词云图!!!!')

                #将词云图显示在tk上
                img = Image.open(wc_file)  # 打开图片
                self.wc_label.img = ImageTk.PhotoImage(img)  # 用PIL模块的PhotoImage打开
                self.wc_label['image'] = self.wc_label.img

            except Exception as e:
                logging.error("ERROR:%s\n"
                              "%s" % (e, traceback.format_exc()))
                self.board_var.set('生成词云图出现问题!!')

        else:
            self.board_var.set('回复内容文件选择不对!!')

    def generate_wc(self,stopword,list_file, pic=None):
        '''生成词云图,               PS:图片路径为空,直接生成方形词云图
        :param stopword:屏蔽词
        :param list_file:里面是list的json文件
        :param pic:模板图片路径
        :return:返回生成的词云图路径
        '''
        if pic:
            pic = np.array(Image.open(pic))  # 解析图片

        with open(list_file, 'r', encoding='utf-8') as f:
            words = f.read()
            words = json.loads(words)
        text = "".join(words)  # 将list转换为str

        # 使用jieba将词语分割       false直接将句子分割,他 来到 上海交通大学;True会出现相同的字 ,他 来到 上海 上海交通大学 交通 大学
        jieba_text = jieba.cut(text, cut_all=False)  #
        wc_text = " ".join(jieba_text)

        # 屏蔽词
        stopwords = set()
        if stopword!=[]:stopwords.update(stopword)

        wc = WordCloud(background_color='white',  # 背景颜色
                       max_words=500,  # 要显示的词的最大个数
                       width=900,  # 图片的宽
                       height=400,  # 图片的长
                       mask=pic,  # 以该参数值作图绘制词云,这个参数不为空时,width和height会被忽略
                       max_font_size=100,  # 显示字体的最大值,对应就是min最小值
                       stopwords=stopwords,  # 使用内置的屏蔽词,
                       font_path=r"wordcloud\simsun.ttc",  # 解决显示口字型乱码问题,可进入C:/Windows/Fonts/目录更换字体
                       random_state=42,  # 为每个词返回一个PIL颜色
                       scale=1,  # 按照比例进行放大画布,如设置为1.5,则长和宽都是原来画布的1.5倍

                       )

        wc.generate(wc_text)

        # 这2行代码,就是将云图里的字体颜色设定跟所选图片一致
        if pic is not None:
            image_colors = ImageColorGenerator(pic)
            plt.imshow(wc.recolor(color_func=image_colors), interpolation="bilinear")  # interpolation不太懂

        plt.imshow(wc)
        plt.axis("off")  # 不显示坐标轴
        #plt.show()  # 弹出图片,显示

        wc_file=self.content_file.get().replace('json','jpg').strip()
        wc.to_file(wc_file)  # 保存文件
        return wc_file
###==========================获取回复内容,生成词云图所用函数========================#




###==========================搜索/获取所有帖子文件,实例化Search_tiezi后进行遍历==========#
    def search_keyword(self):
        '''回帖函数,搜索关键字
           先清空tree,再实例化Search_tiezi 进行搜索
           搜索结果直接该实例中插入到tree
        '''
        search_dir = self.search_dir.get().strip()
        target_word=self.target_word.get().strip()

        if target_word!='':
            items = self.tree.get_children()
            [self.tree.delete(item) for item in items]#清空tree

            aa=Search_tiezi(self)
            try:
                aa.search_keyword(target_word)
            except Exception as e:
                self.board_var.set('请正确选择要搜索的文件夹')
                logging.error("ERROR:%s\n"
                              "%s" % (e, traceback.format_exc()))
        
    def search_author(self):
        '''回帖函数,搜索回帖人
            先清空tree,再实例化Search_tiezi 进行搜索
            搜索结果直接该实例中插入到tree
        '''
        search_dir = self.search_dir.get().strip()
        author = self.author.get().strip()

        if author!='':
            items = self.tree.get_children()
            [self.tree.delete(item) for item in items]#清空tree
            aa=Search_tiezi(self)
            try:
                aa.search_author(author)
            except Exception as e:
                self.board_var.set('请正确选择要搜索的文件夹')
                logging.error("ERROR:%s\n"
                              "%s" % (e, traceback.format_exc()))

    def get_content(self):
        '''回帖函数,获取所有回复
            调用get_timestamp,判断所输入的日期格式
            正确后,再实例化Search_tiezi,
            若有符合条件的内容,保存下来后会其路径显示在content_file中
        '''
        search_dir = self.search_dir.get().strip()
        the_time = self.the_time.get().strip()

        if the_time=='获取所有内容':the_time='无'
        right_time=Search_tiezi.get_timestamp(the_time)
        if right_time is not None:
            aa = Search_tiezi(self)
            try:
                aa.get_content(the_time)
            except Exception as e:
                self.board_var.set('请正确选择要搜索的文件夹')
                logging.error("ERROR:%s\n"
                              "%s" % (e, traceback.format_exc()))
            else:
                if os.path.exists(aa.save_file):
                    self.content_file.set(aa.save_file)
                else:
                    self.board_var.set('没有符合的回复,请正确输入时间!!')
        else:
            self.board_var.set('请正确输入时间!!')
###==========================搜索/获取所有帖子文件,实例化Search_tiezi后进行遍历==========#


###=========================创建tree,以及其所用到的函数=======================#
    def creat_tree(self):
        '''创建tree
           1.创建tree与滚动条
           2.绑定列头、双击、选择item时的回调函数'''

        columns=['title','floor','type','page','author','content','time','url']
        self.tree = Treeview(self.search_frame, columns=columns,show='headings' ,height=50)
        heads=("标题", '楼层','回帖',"页数",  "回帖人", "回复内容", "回复时间",'url')
        widths = ( 230, 50,50,40, 100, 270, 110,35)

        for i in range(len(columns)):
            self.tree.heading(columns[i], text=heads[i])
            self.tree.column(columns[i], width=widths[i], minwidth=widths[i], anchor='center')
            #绑定点击tree列头时,所有内容排序
            self.tree.heading(columns[i], command=lambda _col=columns[i]: self.treeview_sort_column(self.tree, _col, True))

        ysb = Scrollbar(self.search_frame,orient=VERTICAL, command=self.tree.yview)#滚动条
        self.tree.configure(yscrollcommand=ysb.set)
        self.tree.pack(side=LEFT)
        ysb.pack(side=RIGHT,fill=Y)

        self.tree.bind('<>',self.show_value)#选中的帖子,其内容显示在value_text中
        self.tree.bind("", self.open_url)         #双击打开在浏览器打开选中的帖子

    def treeview_sort_column(self,tv, col, reverse):  # Treeview、列名、排列方式
        '''列头的回调函数,
           让每个帖子的item与col对应的值 构成映射关系,以值作为排列依据(发帖时间、楼层转换为数字作为比较),
           排列后,根据其顺序,用tree的move重新排列所有帖子'''

        #构造映射关系,l:[(该列对应的值1,item1),(该列对应的值2,item2).....]
        l=[]
        for item in tv.get_children(''):
            value=tv.set(item,col)
            if col=='time':
                value = time.mktime(time.strptime(value, "%Y-%m-%d %H:%M"))
            elif col=='floor':
                value=re.findall(r'(\d*)楼',value)[0]
                value=int(value)
            l.append((value,item))

        # 根据第一个元素(时间戳)排序!
        l.sort(reverse=reverse)

        # 根据排序后索引移动
        for index, (value, item) in enumerate(l):
           tv.move(item, '', index)
        # 重写标题,使之成为再点倒序的标题
        tv.heading(col, command=lambda: self.treeview_sort_column(tv, col, not reverse))
        
    def filter(self,the_value):
        '''过滤插入tree中的表情、特殊符号'''
        the_value = [re.sub(r':.*?:', '', emoji.demojize(str(one))) for one in the_value]  # 有时候title里有表情,得去掉
        co = re.compile(u'[\U00010000-\U0010ffff]')  # 有时候什么表情都不是,也会报错,所以还得过滤掉
        the_value = [co.sub('', one) for one in the_value]
        return the_value
    
    def tree_insert(self,one_tiezi):
        '''tree中插入帖子的楼层、楼内楼回复
           插入内容:"标题", '楼层','回帖',"页数",  "回帖人", "回复内容", "回复时间",'url
           因为有些楼层只存在楼内楼,所以只有len(3)>3时,才能插入楼层信息,接着循环其楼内楼'''

        title=one_tiezi['title']
        for post in one_tiezi['post_list']:
            floor=post['floor']
            url=post['url']
            if len(post)>3:
                post_value=(title,floor,'楼层',post['page'],post['author'],post['content'],post['time'],url)
                post_value = self.filter(post_value)
                self.tree.insert('', 'end', value=post_value)
            for comment in post['comment_list']:
                comment_value=(title,floor,'楼内楼',comment['page'], comment['author'], comment['content'], comment['time'],url)
                comment_value = self.filter(comment_value)
                self.tree.insert('', 'end', value=comment_value)

###=========================创建tree,以及其所用到的函数=======================#


###=======================tree鼠标操作bind绑定的函数=========================#

    def show_value(self, event):
        '''bind绑定函数,
           选中的帖子,其内容显示在value_text中 '''
        values = self.get_values()
        self.value_text['state'] = NORMAL
        title = '标题:%s' % values[0]
        author = '回帖人:%s' % values[4]
        content = '回复内容:%s' % values[5]
        self.value_text.delete('1.0', 'end')
        self.value_text.insert("end", '%s\n%s\n%s' % (title, author, content), )
        self.value_text['state'] = DISABLED  # 让text栏不可修改

    def open_url(self, event):
        '''bind绑定函数,双击打开在浏览器打开选中的帖子'''
        values = self.get_values()
        url = values[-1]
        webbrowser.open_new(url)

    def get_values(self):
        '''获取选中item的value'''
        item = self.tree.selection()[0]
        values = self.tree.item(item, "values")
        return values
###=======================tree鼠标操作bind绑定的函数=========================#
    

##================设定当前路径等的 函数================##
    #文本框默认显示的路径:含贴吧那个------保存、选择贴吧,都一样
    #更改路径,输入文本框变量,分别设定
    def set_defaultpath(self):
        '''获得当前路径,创建名为[贴吧]的文件夹,设定为默认路径,显示在路径文本框内'''
        self.the_path=os.path.split(os.path.realpath(__file__))[0]#获取当前所在的文件夹绝对路径
        self.defaultpath = self.the_path + os.sep + '贴吧'
        if os.path.exists(self.defaultpath) is False:       #创建存放帖子json文件的文件夹
            os.mkdir(self.defaultpath)
        self.search_dir.set(self.defaultpath)                  #设定搜索文本框内的路径
        self.save_dir.set(self.defaultpath)                     #设定保存文本框内的路径

    def set_dirpath(self,dir):
        '''弹出选择路径窗口,设定文件保存的路径
            选择好了文件夹,那该路径显示在文本框内,否则 设定为默认的文件夹'''
        a=tkinter.filedialog.askdirectory(initialdir=self.defaultpath)
        if a != '':
            dir.set(a)
        elif a == '' and dir.get().strip() == '':
            dir.set(self.defaultpath)

    def set_filepath(self, file):
        '''弹出选择路径窗口,设定文件保存的路径(这是选择文件,而不是文件夹)
            选择好了文件夹,那该路径显示在文本框内,否则 设定为默认的文件夹'''
        a = tkinter.filedialog.askopenfilename(initialdir=self.defaultpath)
        if a == '' and file.get().strip()=='':
            file.set(self.defaultpath)
        elif a != '':
            file.set(a)
###================设定当前路径等的 函数================##

 

 

搜索时,调用到的处理帖子dict的类:

'''
3个搜索函数:
search_keyword:搜索回帖内容里的某个关键字
search_author: 搜索回帖人回复过的内容
get_content:   某时间后所有的发帖内容(或全部内容) 格式:'2018-10-25'

运行过程:
》传入Search_tk,调用的部分为:搜索、保存路径/显示搜索进度、结果/插入到tree
》获取所有文件的绝对路径list(self.files)
》运行搜索函数,
》调用装饰器:1.遍历每个文件,循环每一行
             2.正式运行搜索函数的代码(上面3个)
               ——设定储存文件名(set_savefile)
               ——利用find_path,获取匹配后的所有路径
               ——筛选,补全楼层信息后,添加到post_list中
             3.将每个文件搜索的内容 保存到存储文件
             4.合并存储文件内的每一行,并且显示在tree上/生成云图

'''

class Search_tiezi(object):

    def __init__(self,Search_tk):
        self.Search_tk=Search_tk   #直接将整个搜索tk传进来
        self.search_dir=self.Search_tk.search_dir.get().strip() #搜索tk中的搜索路径
        self.save_dir=self.Search_tk.save_dir.get().strip() #搜索tk中的保存路径
        self.files=self.get_files()                         #所有帖子的路径list
        self.save_file = ''                                 #保存文件的绝对路径,在搜索函数中设定

    def get_files(self):
        '''根据存放json文件的文件夹,得到json文件的绝对地址组成的list'''
        file_list = []
        for name in os.listdir(self.search_dir):
            file_path = os.path.join(self.search_dir, name)
            if os.path.isfile(file_path):
                file_list.append(file_path)
        return file_list

    def set_savefile(self,search_type,target_word):
        '''设定保存文件路径,如果已经存在就删除'''
        tieba=os.path.split(self.search_dir)[1]
        self.save_file = self.save_dir+os.sep+'%s~%s:%s.json'%(tieba,search_type,target_word)
        self.save_file.replace(':',':')
        if os.path.exists(self.save_file):  # 返回文件名,如果存在,删掉文件
            os.remove(self.save_file)


    def save(self,data):
        '''将每条搜索结果(dict/list)写入保存文件内'''
        with codecs.open(self.save_file, 'a', encoding='utf-8') as f:
            line = json.dumps(data, ensure_ascii=False) + "\n"
            f.write(line)


    def save_insert(self):
        '''合并保存文件的每一行,并且在Search_tk显示
           PS:之前是符合条件的一行一行写入文件,现在提取文件的所有dict/list都合并到一个list,然后保存 显示
           步骤:搜索后的文件:----存在,》读取每一行,判断类型:----搜索内容:1.将每一行添加到all_data
                                                                       2.在tk的tree插入该行信息(每一行为一个帖子)
                                                          ----获取回复: 只将每一行的post_list添加到all_data
                                      》再次保存
                                      》在tk的board_text上显示搜索结果
                             ----不存在,在tk上提示没有搜索内容
            '''
        #存在搜索文件才进行合并,否则说明所有帖子中没有要搜索的内容
        if os.path.exists(self.save_file):
            all_data=[]
            with open(self.save_file, 'r', encoding='utf-8') as f:
                for one_dict in f.readlines():
                    one_tiezi=json.loads(one_dict)
                    if '搜索条件' in self.save_file:
                        all_data.append(one_tiezi)
                        self.Search_tk.tree_insert(one_tiezi)
                    elif '回帖内容' in self.save_file: #只需 回帖内容content
                        all_data+=one_tiezi['post_list']
            #先删了再保存
            os.remove(self.save_file)
            self.save(all_data)

            #搜索结束后,在label_text上显示搜索结果
            if '搜索条件' in self.save_file:
                content_num=len(self.Search_tk.tree.get_children(''))
            else:
                content_num=len(all_data)
            the_searching=os.path.split(self.save_file)[1].split('.')[0]
            self.Search_tk.board_var.set('%s——完成,共%s条回复'%(the_searching,content_num))
        else:
            self.Search_tk.board_var.set('没有符合条件的搜索结果')

###====================================装饰器,循环打开每个文件、读取每一行,再进行搜索====================###
    def read_search(search_func):
        '''装饰器,装饰3种查询方法
            步骤:----遍历所有文件:----在tk上显示搜索进度
                                  ----读取文件:----创建post_list,用以存放符合条件的楼层
                                               ----循环每一行:     》搜索每一行
                                               ----post_list不为空:》每个文件(帖子)作为一行保存到文件中
                 ----合并保存文件的每一行,并且在tk上显示出搜索结果
'''
        def wrapper(self,target_word,):
            # 循环该贴吧的所有文件
            files_num=len(self.files)
            search_num=0
            for one_file in self.files:
                search_num += 1
                label_text='搜索文件进度————%s/%s'%(search_num,files_num)
                self.Search_tk.board_var.set(label_text)
                self.Search_tk.root.update()
                with open(one_file, 'r', encoding='utf-8') as f:
                    post_list = []
                    serach_tiezi = {}
                    # 循环该文件的所有行
                    for one_dict in f.readlines():
                        the_tiezi = json.loads(one_dict)
                        serach_tiezi = {'title': the_tiezi['title'], 'author': the_tiezi['author'],
                                       'tid': the_tiezi['tid']}
                        search_func(self,target_word,the_tiezi,post_list)

                    if post_list != []:  # 该帖子内有符合条件的楼层,才会被保存
                        serach_tiezi['post_list'] = post_list
                        self.save(serach_tiezi)  # 把符合条件的search_tiezi写入保存文件

            self.save_insert()#合并保存文件的每一行,并且在tk上显示出搜索结果
        return wrapper

###====================================装饰器,循环打开每个文件、读取每一行,再进行搜索====================###


###===================================搜索回帖内容/ 搜索回帖人==========================###
    @read_search
    def search_keyword(self, target_word,the_tiezi=None,post_list=None,):
        '''输入关键字,运行find_path,获取在the_tiezi的路径后,补全楼层信息后添加到post_list中
            :param target_word: 回帖内容里的关键字
            :param the_tiezi: 帖子文件内的一行帖子内容dict
            :param post_list: 存放所有楼层的list
            :return:
                '''
        if self.save_file=='':
            self.set_savefile('搜索条件—关键字', target_word) #设定保存文件名

        a = Find_path(the_tiezi)
        all_path = a.in_value_path(target_word)  # 包含匹配,搜索内容包含关键字的路径

        self.sort_path(all_path)  # 将路径排序,保证楼内楼所在的楼层在前
        # 循环所有路径
        for one_path in all_path:
            # 提出 字典的key与列表的元素位置
            indexex = re.findall(r'\[(.*?)\]', one_path) # one_path格式:楼层--"['post_list'][2]['content']",,楼内楼--"['post_list'][2]['comment_list'][38]['content']"
            indexex = [eval(index) for index in indexex]
            # 只保留 是回帖内容的搜索结果 路径
            if indexex[-1] == 'content'and len(indexex)>1:
                self.add_post(indexex, the_tiezi, post_list)#将路径对应的楼层/楼内楼 的发帖人、时间等信息补充完整为dict,添加到post_list

    @read_search
    def search_author(self, author, the_tiezi=None, post_list=None, ):
        '''输入回帖人昵称,运行find_path,获取在the_tiezi的路径后,补全楼层信息后添加到post_list中
        :param author: 回帖人
        :param the_tiezi: 帖子文件内的一行帖子内容dict
        :param post_list: 存放所有楼层的list
        :return:
        '''
        if self.save_file == '':
            self.set_savefile('搜索条件—发帖人', author)  # 设定保存文件名

        a = Find_path(the_tiezi)
        all_path = a.the_value_path(author) #完全匹配,搜索结果就是author的路径

        self.sort_path(all_path)  # 将路径排序,保证楼内楼所在的楼层在前
        # 循环所有路径
        for one_path in all_path:
            # 提出 字典的key与列表的元素位置
            indexex = re.findall(r'\[(.*?)\]', one_path)
            indexex = [eval(index) for index in indexex]
            # 只保留 是回帖人的搜索结果 路径
            if indexex[-1] == 'author' and len(indexex)>1:
                self.add_post(indexex, the_tiezi, post_list)  # 将路径对应的楼层/楼内楼 的发帖人、时间等信息补充完整为dict,添加到post_list


    def sort_path(self, all_path):
        '''将所有路径排序,保证楼内楼所位于的楼层排列在前
            缘由:因为在读取搜索时,返回的路径有时候顺序是打乱的,楼内楼在其所在的楼层之前,后面add_post添加楼层时就混乱,所以排序
            '''

        def sort_key(one_path, index):
            # 如果index=3,那么楼层排序key就是0,自然就在最前面
            # one_path格式:楼层--"['post_list'][2]['content']",
            #              楼内楼--"['post_list'][2]['comment_list'][38]['content']"
            indexex = re.findall(r'\[(.*?)\]', one_path)
            indexex = [eval(index) for index in indexex]
            if len(indexex) > index:
                return indexex[index]
            else:
                return 0

        all_path.sort(key=lambda one_path: sort_key(one_path, 3))  # 先按楼内楼顺序排列,因为楼层只有3个元素,所以都在最前面
        all_path.sort(key=lambda one_path: sort_key(one_path, 1))  # 最后按楼层顺序排列


    def add_post(self, indexex, the_tiezi, post_list):
        '''根据楼层/楼内楼 的路径,获取其 回帖时间、内容、回帖人等,组成dict,添加到post_list中
           过程:1.楼层是直接添加,
                2.当是楼内楼时,判断该楼内楼所在的楼层就是post_list最后的楼层,不是的话,添加一个新的楼层,再把该楼内楼添加进去
        :param indexex: 路径,如['post_list',2,'comment_list',38,content']
        :param the_tiezi: 帖子文件内的一行帖子内容dict
        :param post_list:
        :return: 存放所有楼层的list
        保存结构:
                {贴吧名、发帖人、tid、
                'post_list':[{楼层1post,楼层2post,····}]}

                楼层post:
                          当前路径是楼层:包含的键值有
                                       {floor、page、url、author、content、time、comment_list:[]}
                                    或:{floor、page、url、                    、comment_list:[]}
                          楼内楼comment:
                                      当前路径是楼内楼,包含的键值有
                                                  {author、content、time、comment_page}
     '''

        # 锁定 当前楼层/楼内楼所在的楼层
        target_post = the_tiezi['post_list'][indexex[1]]
        url = 'http://tieba.baidu.com/p/%s?pid=%s#%s' % (
        the_tiezi['tid'], target_post['pid'], target_post['pid'])  # 该楼层指向的url

        # 此路径是楼层,直接添加到post_list
        if len(indexex) == 3:
            search_post = copy.deepcopy(target_post)
            search_post.pop('comment_num')  # 删除楼内楼数量
            search_post.pop('pid')  # 删除pid
            search_post['comment_list'] = []  # 楼内楼list先为空
            search_post['url'] = url
            post_list.append(search_post)

        # 此路径是楼内楼
        elif len(indexex) == 5:
            # 当前楼内楼所在的楼层不符合条件被收录,只存在楼内楼,那么创建该楼层
            if post_list == [] or post_list[-1]['floor'] != target_post['floor']:
                search_post = {}
                search_post['floor'] = target_post['floor']
                #search_post['page'] = target_post['page']
                search_post['comment_list'] = []  # 楼内楼list先为空
                search_post['url'] = url
                post_list.append(search_post)
            # 锁定最后一个楼层,添加楼内楼
            comment_dict = target_post['comment_list'][indexex[3]]  # 目的楼内楼
            comment_dict.pop('spid')
            post_list[-1]['comment_list'].append(comment_dict)  # 将楼内楼添加到post_list最后的楼层

###===================================搜索回帖内容/ 搜索回帖人==========================###



###===================================获取所有回复(回帖内容)==========================###
    @read_search
    def get_content(self, time='无', the_tiezi=None, content_list=None, ):
        '''输入特定时间,获取该时间点后所有的回复内容(默认为无,获取所有回复)
        :param time:     指定时间
        :param the_tiezi: 帖子文件内的一行帖子内容dict
        :param content_list: 存放所有回复内容的list
        :return:
        '''

        #设定保存文件名
        if self.save_file == '':
            the_time=re.sub(r':',':',time)
            self.set_savefile('回帖内容—某时间后', the_time)  # 设定保存文件名

        timestamp = self.get_timestamp(time)#将输入的日期时间转换为时间戳
        if timestamp is not None:#输入格式不对的时间或其他内容,则忽略
            a = Find_path(the_tiezi)
            time_paths = a.the_key_path('time')#键匹配,获取所有time路径

            for the_time in time_paths :
                send_time = eval('the_tiezi' + the_time)
                send_timestamp = self.get_timestamp(send_time)#将发送时间转化为时间戳
                #指定时间之后的回复则添加到content_list中
                if send_timestamp > timestamp:
                    content_path = the_time.replace('time', 'content')
                    content=eval('the_tiezi' + content_path)
                    self.filter_content(content_list, content) #过滤部分内容,添加到list中

    @staticmethod
    def get_timestamp(date_time='无'):
        '''将输入的日期时间转换为时间戳,方便比对大小,若没有输入时间,那就返回0'''
        if re.match(r'\d*-\d*-\d* \d*:\d*', date_time):
            return time.mktime(time.strptime(date_time, "%Y-%m-%d %H:%M"))
        elif re.match(r'\d*-\d*-\d*', date_time):
            return time.mktime(time.strptime(date_time , "%Y-%m-%d"))
        elif date_time == '无':
            return 0

    def filter_content(self,content_list,content):
        '''单纯提取回复内容时,过滤掉url、楼内楼回复中的前缀“回复 xxx :”,因为有些回复是表情,所以为空,也要过滤掉
           然后把回复添加到list中'''
        re_url = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')
        re_reply = re.compile(r'回复.*:')
        content=re.sub(re_reply, '', content)
        content = re.sub(re_url, '', content)
        if content!='':
            content_list.append(content)
###===================================获取所有回复(回帖内容)==========================###

 

 

自写的find_path类,可以获取所查询元素的路径 find_path.py

class Find_path():
    def __init__(self,target):
        self.target=target

    def find_the_value(self,target,value,path='',path_list=None):
        '''完全匹配,每经过一层(list、dict)都会记录path,到了最后一层且当前target就是要找的目标,才把对应的path记录下来
        :param target: 被搜索的目标
        :param value: 要搜索的关键字
        :param path: 当前所在的路径
        :param path_list: 存放所有path的列表
        判断当前target类型:···是字典,循环内容,每个键值都记录下路径path,然后以当前值v为判断target,调用自身传入添加了的path判断
                             ···是列表,循环内容,每个元素都记录下路径path,然后以当前元素为判断target,调用自身传入添加了的path判断
                             ···是str或者int,那么就判断当前target是否就是要搜索的value,如果是,那就把路径path放进list里面'''
        if isinstance(target, dict):
            for k, v in target.items():
                path1 = copy.deepcopy(path)
                path1=path1+str([k])
                self.find_the_value(v, value, path1, path_list)

        elif isinstance(target, (list, tuple)):  # 判断了它是列表
            for i in target:
                path1 = copy.deepcopy(path)
                posi = target.index(i)
                path1 = path1+'[%s]' % posi
                self.find_the_value(i, value, path1, path_list)

        elif isinstance(target, (str, int)) :
            if  str(value) ==str(target):   #必须完全相同
                path_list.append(path)


    def find_in_value(self,target,value,path='',path_list=None):
        '''包含匹配,内容跟上面一样,只是最后判断时不同'''
        if isinstance(target, dict):
            for k, v in target.items():
                path1 = copy.deepcopy(path)
                path1=path1+str([k])
                self.find_in_value(v, value, path1, path_list)

        elif isinstance(target, (list, tuple)):  # 判断了它是列表
            for i in target:
                path1 = copy.deepcopy(path)
                posi = target.index(i)
                path1 = path1+'[%s]' % posi
                self.find_in_value(i, value, path1, path_list)

        elif isinstance(target, (str, int)) :
            if  str(value) in str(target):   #
                path_list.append(path)

    def find_the_key(self,target,key,path='',path_list=None):
        '''查找key,每经过一层(list、dict)都会记录path,在字典时,若当前的k就是要找的key,那就把对应的path记录下来
                :param target: 被搜索的目标
                :param key: 要搜的键
                :param path: 当前所在的路径
                :param path_list: 存放所有path的列表
                判断当前target类型:···是字典,循环内容,每个键值都记录下路径path,判断当前k是否要查找的:~~~是,那就把路径path放进list里面
                                                                                                 ~~~不是,以当前值v为判断target,调用自身传入添加了的path判断
                                  ···是列表,循环内容,每个元素都记录下路径path,然后以当前元素为判断target,调用自身传入添加了的path判断
                                     '''
        if isinstance(target, dict):
            for k, v in target.items():
                path1 = copy.deepcopy(path)
                path1=path1+str([k])
                if str(key) == str(k):
                    path_list.append(path1)
                else:
                    self.find_the_key(v, key, path1, path_list)

        elif isinstance(target, (list, tuple)):  # 判断了它是列表
            for i in target:
                path1 = copy.deepcopy(path)
                posi = target.index(i)
                path1 = path1+'[%s]' % posi
                self.find_the_key(i, key, path1, path_list)

#====================================================================================

    def in_value_path(self,value):
        '''包含匹配value'''
        path_list=[]
        self.find_in_value(self.target, value,path_list=path_list)
        return path_list

    def the_value_path(self,value):
        '''完全匹配value'''
        path_list=[]
        self.find_the_value(self.target, value,path_list=path_list)
        return path_list

    def the_key_path(self,value):
        '''只查找key'''
        path_list = []
        self.find_the_key( self.target, value,path_list=path_list)
        return path_list

 

 

 

查询后所生成的文件:

python tkinter界面 多进程启动scrapy爬取百度贴吧的回复,显示爬取进度,并可以搜索回帖人,指定时间生成词云图,用pyinstaller打包成exe(九)_第3张图片

你可能感兴趣的:(python,爬取百度贴吧的帖子)