国庆特辑:使用Python绘制知乎国庆话题词云

庆祝祖国母亲73岁生日!

今年国庆来临之时,中国新闻网在知乎组织了国庆话题问答,问题是“2022 年 10 月 1 日是中华人民共和国 73 岁生日,近年来我国取得的哪些成就让你感到自豪?”。

看着来自五湖四海的答案,作为一个数据分析萌新,我突发奇想:为何不将大家的回答汇总,绘制一幅词云图呢?

说干就干。

完整代码见文末

1.数据获取

最初我本想通过抓包拿到知乎API,可能是因为自己JS逆向技术水平太低,折腾了一两天仍然没弄懂。无奈之下,我选择通过selenium模拟访问知乎网页版来收集数据。

selenium的下载安装我就不再赘述了。我们的目标是尽可能多的获取所有回答的文字信息,因此网页图片请求可以暂时禁用,以减少内存占用,代码如下:

chrome_options = webdriver.ChromeOptions()
prefs = {'profile.managed_default_content_settings.images': 2}
chrome_options.add_experimental_option('prefs', prefs)
driver = webdriver.Chrome(options=chrome_options)
driver.get("https://www.zhihu.com/question/556882527")
sleep(3)

借助F12开发者工具,我们可以定位所有回答的网页元素xpath路径,使用for循环依次定位,再通过lxml库收集元素节点下的所有文字信息。

这里需要注意的是,许多回答文字中包含超链接,这对我们的词云没有太大意义,于是我们可以通过正则匹配将其替换成空格来去除,正则表达式如下:

https?://[a-zA-Z0-9\./_#&%\?_=-]+

所有已获得的数据全部以文本形式存入txt文件中,每一个回答占一行。一些回答可能仅包括视频,可能仅占一个空行。

在实际操作中会发现,知乎使用的是ajax动态加载回答内容,一些回答文字可能并未加载,因此我们需要写一个异常捕获,当检测到目标元素不存在时,就把页面下拉到最底部并等待1秒。为了更加保险,以上过程可以写一个3次循环,代码如下:

driver.find_element(By.XPATH, "/html/body/div[5]/div/div/div/div[2]/button").click()
format_ = "/html/body/div[1]/div/main/div/div/div[3]/div[1]/div/div/div/div/div/div[2]/div/"
file = open('answer_text.txt', mode='w', encoding='utf-8')
for i in range(1, 1000):  # 计划获取1000条数据,实际数会更小
    for _ in range(3):
        try:
            answer = driver.find_element(By.XPATH, format_ + f"div[{i}]/div/div/div[2]/span[1]/div/span")
            content = etree.HTML(answer.get_attribute('innerHTML')).xpath('string(.)')  # 获取节点下所有文字信息
            content = re.sub(r'https?://[a-zA-Z0-9\./_#&%\?_=-]+', ' ', content)  # 将超链接替换为空格
            file.write(content+'\n')
            if i % 10 == 0:
                print(i)  # 实时显示已获取回答数
        except NoSuchElementException as e:
            driver.execute_script("window.scrollTo(0,document.body.scrollHeight)")  # 滑动至页面最底部
            sleep(1)
        except StaleElementReferenceException as e:
            driver.execute_script("window.scrollTo(0,document.body.scrollHeight)")
            sleep(1)
        break

最终获取到五百多条回答,共64201字。

2.绘制词云

使用jieba库完成中文分词:

# 读取文本
with open("answer_text.txt", encoding="utf-8") as f:
    s = f.read()
ls = jieba.lcut(s)  # 生成分词列表
text = ' '.join(ls)  # 连接成字符串

诸如“我”“了”“也”等停用词对最终词云无价值,故我们应该剔除这些词和字符。我使用的是来源网络的中文停用词库,最后使用wordcloud库生成词云:

stopwords = [line.strip() for line in open('stop_words.txt', 'r', encoding='utf-8').readlines()]  # 去掉不需要显示的词
wc = wordcloud.WordCloud(font_path="msyh.ttc",
                         width=1000,
                         height=700,
                         margin=2,
                         mask=np.array(Image.open(r"D:\Drivers\Download\china_map.png")),  # 这里把中国地图作为词云的轮廓图
                         background_color='white',
                         max_words=100, stopwords=set(stopwords))
wc.generate(text)  # 加载词云文本
wc.to_file("知乎词云.png")  # 保存词云文件

最终词云如下:

我最初的那份感动,大概源于初中时阅读《红星照耀中国》(又名《西行漫记》)吧。凭着对红军的敬佩,我开始更加深入地了解历史。遥想高中,更是一发不可收拾。

团员、积极分子……你问我下一步在哪?我没怎么想。未来,不论何时、不论何地,为人民服务始终是最高尚的事业的标准。

回望这73年沉重的历史,我们站起来已有73年了……千言万语,欲说不出。看了所有人的回答后,不得感慨道:

人民万岁!

完整代码

知乎回答.py:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
from time import sleep
from lxml import etree
import re


chrome_options = webdriver.ChromeOptions()
prefs = {'profile.managed_default_content_settings.images': 2}
chrome_options.add_experimental_option('prefs', prefs)
driver = webdriver.Chrome(options=chrome_options)

driver.get("https://www.zhihu.com/question/556882527")
sleep(3)
driver.find_element(By.XPATH, "/html/body/div[5]/div/div/div/div[2]/button").click()
format_ = "/html/body/div[1]/div/main/div/div/div[3]/div[1]/div/div/div/div/div/div[2]/div/"
file = open('answer_text.txt', mode='w', encoding='utf-8')
for i in range(1, 1000):
    for _ in range(3):
        try:
            answer = driver.find_element(By.XPATH, format_ + f"div[{i}]/div/div/div[2]/span[1]/div/span")
            content = etree.HTML(answer.get_attribute('innerHTML')).xpath('string(.)')
            content = re.sub(r'https?://[a-zA-Z0-9\./_#&%\?_=-]+', ' ', content)
            file.write(content+'\n')
            if i % 10 == 0:
                print(i)
        except NoSuchElementException as e:
            driver.execute_script("window.scrollTo(0,document.body.scrollHeight)")
            sleep(1)
        except StaleElementReferenceException as e:
            driver.execute_script("window.scrollTo(0,document.body.scrollHeight)")
            sleep(1)
        break
driver.quit()
file.close()

词云生成.py:

import jieba
import wordcloud
from PIL import Image
import numpy as np


# 读取文本
with open("answer_text.txt", encoding="utf-8") as f:
    s = f.read()
ls = jieba.lcut(s)  # 生成分词列表
text = ' '.join(ls)  # 连接成字符串


stopwords = [line.strip() for line in open('stop_words.txt', 'r', encoding='utf-8').readlines()]  # 去掉不需要显示的词
wc = wordcloud.WordCloud(font_path="msyh.ttc",
                         width=1000,
                         height=700,
                         margin=2,
                         mask=np.array(Image.open(r"D:\Drivers\Download\china_map.png")),
                         background_color='white',
                         max_words=100, stopwords=set(stopwords))
wc.generate(text)  # 加载词云文本
wc.to_file("知乎词云.png")  # 保存词云文件