论文中的插图在一定程度上影响着评审人对于论文质量的评价,并且人文社科类论文通常需要进行大量文字分析,词云图可以通过高亮或放大词频数较高的关键词,来突出显示所选文字材料中的重点。本文以指定搜索词的百度结果页为例,制作一张词云图。

1.导入库

import requests,lxml
from bs4 import BeautifulSoup
import csv,jieba,wordcloud,paddle
from wordcloud import WordCloud, ImageColorGenerator
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

这里用到的库主要分为三个部分:
1. 对百度结果进行爬虫操作,主要用到 requests、lxml、BeautifulSoup
2.对爬虫结果进行分词,主要用到 csv、jieba、paddle
3.对分词结果进行词云绘制,主要用到 wordcloud、numpy、matplotlib、PIL

如果不确定自己设备里是否有上述的库文件,可以在命令行中执行安装操作,有的话会提示已安装,没有的话会自动安装:

pip install 目标库

2.网址处理

本文用到的搜索信息是“基层治理”和“两邻理论”,在网址处理环节,需要先构建出每一页的链接:

base_url = " https://www.baidu.com/s?tn=news&ie=utf-8&word=两邻+\"基层\"&pn="
urls = [base_url+str(i) for i in range(0,291,10)]
print(urls)

out:

[' https://www.baidu.com/s?tn=news&ie=utf-8&word=%E4%B8%A4%E9%82%BB+%22%E5%9F%BA%E5%B1%82%22&pn=0'
...
' https://www.baidu.com/s?tn=news&ie=utf-8&word=%E4%B8%A4%E9%82%BB+%22%E5%9F%BA%E5%B1%82%22&pn=290']

首先是构建一个 base_urltn 参数设置为 news 代表“资讯”栏目,word 代表搜索词,pn 参数代表页数,但是并不能直接序列设置。通过网址观察可以发现,第一页的 pn 为 0,第二页的 pn 为 10,所以通过range函数构造一个序列,从 0 开始,步长为 10,总共爬取 30 页。

3.文章网址汇总

因为网址数量并不大,所以直接以字典加列表的方式存储:

results =[]
for url in urls:
    res = requests.get(url,headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'})
    soup = BeautifulSoup(res.text,'lxml')
    for i in soup.find_all("h3"):
        try:
            result = {
                "title":i.a["aria-label"][3:],
                "url":i.a["href"]
            }
            results.append(result)
            print(result)
        except:
            print(url)

out:

{'title': '沈阳市和平区:以“百千万”党员志愿服务践行“两邻”理念_综合...', 'url': 'https://lszhdj.lndj.gov.cn/portaluploads/html/site-5/info/42600.html'}
{'title': '浑南区五三街道召开“两邻学堂”启动仪式 暨“奋斗者..._手机网易网', 'url': 'https://3g.163.com/local/article/HDAIO22104229BRM.html'}
....
{'title': '【两邻生活】党建引领 锚定难点 沈北新区散体楼合围治理破“四难...', 'url': 'http://society.sohu.com/a/547395443_121123868'}
{'title': '“两邻”理念推动基层治理 铁西区物业管理实现新突破(一):党建...', 'url': 'http://liaomedia.net/index.php?m=Index&c=Content&a=index&cid=51&aid=2785'}

第一步主要使用 requests 库的 get 方法,获取网页内容,然后通过 BeautifulSoup 的网页解析功能,将网页以 lxml 形式格式化,以方便网页子节点的获取。

第二步主要是在网页中获取文章网址,通过网页源代码信息可以发现,文章链接在 h3 标签下,所以可以通过 find_all 方法获取所有 h3 节点,然后通过 for 循环,读取每个 h3 标签中的信息并处理。

<h3 class="news-title_1YtI1 ">
    <a href="https://www.sohu.com/a/551186753_121123868" 
       aria-label="标题:【两邻生活】道义街道:“暖心三部曲”奏响“为民服务主旋律...">
        道义街道:“暖心三部曲”奏响“为民服务主旋律...
    </a>
</h3>

观察 h3 标签下的 a 标签,新闻标题存在于 aria-label 标签下,新闻链接存在于 href 标签下,所以在 result 字典里,直接读取 a 标签下的上述两个属性即可。

第三步把存有每条新闻的 result 字典放进 results 列表中即可。

4.确定每个网站的新闻数量

首先需要确定链接中的网站:

websites=[]
for i in results:
    websites.append(i["url"].split('/')[2])

对链接进行分词,网址结构为 https://www.x.x/x ,以 “/” 为分词,第三个元素为主网址,python 列表从 0 开始,所以第三个元素的序列为 2。将每篇文章的主网址放进 websites 列表中。

from collections import Counter
counter = Counter(websites)
print(counter)

out:

Counter({'baijiahao.baidu.com': 40, 'mp.weixin.qq.com': 40, 'www.sohu.com': 22, 
'weibo.com': 15, 'view.inews.qq.com': 15, 'news.syd.com.cn': 14, 'www.lndj.gov.cn': 10, '3g.163.com': 10, 'new.qq.com': 8, 'lszhdj.lndj.gov.cn': 8, 'xw.qq.com': 7, 'finance.sina.com.cn': 5, 'www.fx361.com': 4, 'www.meipian.cn': 4, 'liaoning.news.163.com': 4, 'www.shenyang.gov.cn': 4, 'www.163.com': 4, 'k.sina.com.cn': 3, 'society.sohu.com': 3, 'www.ln.chinanews.com.cn': 3, 'liaoning.nen.com.cn': 3, 'liaomedia.net': 2, 'finance.sina.cn': 2, 'neunews.neu.edu.cn': 2, 'mzj.shenyang.gov.cn': 2, 'ln.people.com.cn': 2, 'www.doc88.com': 2, 'cpu.baidu.com': 2, 'www.mca.gov.cn': 1, 'djyj.12371.cn': 1, 'rmh.pdnews.cn': 1, 'www.rmlt.com.cn': 1, 'heping.nen.com.cn': 1, 'www.syyh.gov.cn': 1, 'www.qlwb.com.cn': 1, 'card.weibo.com': 1, 'www.renrendoc.com': 1, 'wap.eastmoney.com': 1, 'gov.sohu.com': 1, 'www.xhzaixian.cn': 1, 'app.myzaker.com': 1, 'www.sinatec.ln.cn': 1, 'www.hunnan.gov.cn': 1, 'www.12371.cn': 1, 'ex.chinadaily.com.cn': 1, 'news.lnd.com.cn': 1, 'www.xyshjj.cn': 1, 'mini.eastday.com': 1, 'k.sina.cn': 1, 'www.360doc.com': 1, 'www.lnminjin.gov.cn': 1, 'news.sohu.com': 1, 'www.zhangqiaokeyan.com': 1, 'www.spp.gov.cn': 1, 'www.chinanews.com': 1, 'cj.sina.cn': 1, 'm.chinanews.com': 1, 'news.cnhubei.com': 1, 'shenyang.creb.com.cn': 1, 'zhuanlan.zhihu.com': 1, 'xysy.shenyang.gov.cn': 1, 'finance.nen.com.cn': 1, 'dy.163.com': 1, 'ln.cri.cn': 1, 'www.chinanews.com.cn': 1})

然后对网站进行计数,这里用到的是 Counter,可以直接将列表中的元素分组计数。可以发现新闻数量最多的网站分别是“百度百家号”、“微信公众号”、“搜狐新闻”、“微博”,这里选用前三个网站,具体实践过程可以根据不同网站的新闻数量占比酌情选择。

5.为每个网站编写规则

def get_website_info(url):
    result = []
    p =[]
    Aresult = []
    website=url.split('/')[2]
    target_web = ['baijiahao.baidu.com','new.qq.com','mp.weixin.qq.com','news.sohu.com','www.sohu.com']
    try:
        if website in target_web:
            res = requests.get(url,headers={ 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.70'})
            soup = BeautifulSoup(res.text,'lxml')
            if website == "baijiahao.baidu.com": 
                p = soup.find_all("p")
            elif website == "new.qq.com":
                p = soup.find_all(class_="one-p")
            elif website == "mp.weixin.qq.com":
                p = soup.find("div", {"id": "js_content"})
            elif website == "news.sohu.com" or "www.sohu.com":
                p = soup.find_all("article",class_="article")
            for i in p:
                    if i.text != "":
                        result.append(i.text)
            Aresult = "".join(result)
    except:
        return result
    return Aresult

由于抓取的内容是文字信息,且所在的网站无明显反爬策略,所以只需要找出文章所在的位置即可,方法跟第三小节中的一样,只需要找出相关的 html 标签即可。

百家号的文章存在于p标签内;腾讯新闻的文章存在于 class 为 one-pp 标签内;微信公众号的文章存在于 idjs_contentdiv 标签内;搜狐有两个相关的网站,但是网页结构都是一样的,文章存在于 class 为 articlep 标签内。

此处只指定了数量排名靠前的网站的抓取策略,其余的网站会自动过滤并返回空信息。

6.将所有文章写入 csv 文件中

countt = 0
with open("webinfo.csv","w",newline="",encoding="utf-8-sig") as f:
    writer = csv.writer(f)
    writer.writerow(["title","url","content"])
    for i in results:
        c = get_website_info(i["url"])
        time.sleep(2)
        if c != "":
            content = c
            w = {
                "title":i["title"],
                "url":i["url"],
                "content":c
            }
            writer.writerow([w["title"],w["url"],w["content"]])
            countt += 1
        if countt % 10 == 0:
            print("已完成{}条".format(countt))
f.close()

out:

'已完成10条'
'已完成20条'
'已完成30条'
....
'已完成270条'

这一步是为了把文章内容保存在文件中,避免下一步绘制词云时操作不当丢失变量信息。

countt 变量是用来计算已经写入了多少条文章,每写入一条就 +1。写入 csv 文件时,用的 csv.writer,以及 writer.writerow,前者是用来生成一个可写入对象,后者可以将数据按行写入文件。

在写入数据时,先把标题、链接和新闻内容合并成字典,然后以字典的形式按行写入,这样就能把整体的新闻数据以符合数据表的形式存储,如果后续对此数据表有可视化操作或者其他自定义操作的话,会更加的方便。

当然也可以直接把所有新闻按顺序写入,不按照这种结构化的数据处理,即:

countt = 0
with open("webinfo.csv","w",newline="",encoding="utf-8-sig") as f:
    writer = csv.writer(f)
#   writer.writerow(["title","url","content"])
    for i in results:
        c = get_website_info(i["url"])
        time.sleep(2)
        if c != "":
#           content = c
#           w = {
#               "title":i["title"],
#               "url":i["url"],
#               "content":c
#           }
#           writer.writerow([w["title"],w["url"],w["content"]])
			writer.writerow(c)
            countt += 1
        if countt % 10 == 0:
            print("已完成{}条".format(countt))
f.close()

其中 # 代表注释,即运行时不执行此行代码。

写入数据时,open 方法里的 w 参数指的是写入,纯写入,即打开文件后,不管文件里之前有没有数据,都会从 0 开始写入数据。这里因为只写入一次,所以用的 w 模式,如果需要多次写入,可以用 a,即“追加写入”模式。

中间的 time.sleep(2) 指的是在此处暂停 2 秒,因为程序运行速度很快,get_website_info 方法的用途是爬取网页内容,如果中间不进行停止的话很容易会被网站判定为爬虫程序,进而阻止数据获取,所以需要在每爬完一个网站后暂停 2 秒,这个时间和位置可以根据实际情况自己调整。

7.分词处理

#读取 webinfo.csv
p_list = []
with open("webinfo.csv","r",encoding="utf-8-sig") as f:
    reader = csv.reader(f)
    for i in reader:
        if i[0] == "title":
            continue
        else:
            p_list.append(i[2])
f.close()

#将文章分词
p_list_2 = []
paddle.enable_static()
jieba.enable_paddle()
for i in p_list:
    seg_list = jieba.cut(i, cut_all=False)#精确模式,即尽可能保留名词
    p_list_2.append(" ".join(seg_list))

#读取停用词并删除
with open("baidu_stopwords.txt","r",encoding="utf-8-sig") as stopwords:
    stop_words = [i.strip() for i in stopwords.readlines()]
    data_1 = ''
    for i in p_list_2:
        for j in i:
            if j not in stop_words:
                data_1 += j

首先需要读取上一小节的 csv 文件,在 open 方法中,之前写入时用的是 w,这里需要用到 r,即 read。由于上一节在写入文件时候,是结构化写入,所以在读取的时候,也需要层层读取。第一步用 csv.reader 生成一个可读取的对象,第二步开始读取上述 csv 文件,先省略第一行,从第二行开始读取,因为每一行的结构都是标题、网址、文章内容,所以读的时候只需要读每一行的第 3 个元素就行了。然后把所有文章添加到一个列表中。

然后需要对所有文章进行分词,用到的是 jieba 库,在用 jieba 库之前,需要开启 paddle,即 paddle.enable_static(),然后在 jieba 中启用 paddle。分词时的主要操作,就是把一段文本分割成单个的词汇,即 jieba.cut,然后把分词后的内容汇总在一起,这里都汇总在了 p_list_2 里,查看一下 p_list_2 的内容就能发现分词结果了。

[in ] print(p_list_2)
[out] 
['社会 治理 是 在 执政党 领导 下 由 政府 组织 主导 、 吸纳 社会 组织 等 多方面 治理 主体 参与 、 对 社会 公共事务 进行 的 治理 活动 , 是 党 在 治国 理政 理念 升华 后 对 社会 建设 提出 的 基本 要求 。 '
....
'强调 社会 治理 参与 主体 、 方法 、 路径 等 在 智能化 技术 支撑 下 的 实现 效果 ,  “ 两邻 ” 理念 下  , 从 智能化 基层 社会 治理 建设 的 基本 内容 要求 入手 , 在 规划 建设 、 制度 建设 、 资源 建设 、 开放 应用 、 平台 建设 、 人才 建设']

最后一步需要进行停用词的删除,从上述的分解结果可以发现有很多无意义的虚词,如“是”、“由”、“上”、“下”、“的”、“在”等等,所以需要进行停用词删除,常用的停用词库有百度、四川大学、哈工大等,这里使用的是百度停用词表。去除停用词的主要逻辑就是从已分词的列表中剔除掉停用词表中的词。剔除后我们再输出一下 data_1 就可以发现不同之处了:

[out]
['社会 治理   执政党 领导 下  政府 组织 主导 、 吸纳 社会 组织  方面 治理 主体 参 、  社会 公共事务 进行  治理 活动 ,  党  治国 理政 理念 升华 后  社会 建设 提出  基 求 。 
....
强调 社会 治理 参 主体 、 方法 、 路径   智化 技术 支撑 下  实现 效果 , 两邻  理念 下,  智化 基层 社会 治理 建设  基 内容 求 入手 ,  规划 建设 、 制度 建设 、 资源 建设 、 开放 应 、 平台 建设 、 人才 建设']

8.词云绘制

from wordcloud import WordCloud, ImageColorGenerator
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

font = r'C:\Windows\Fonts\SIMLI.ttf';     # 自定义字体

py_mask = np.array(Image.open('LN.png'))  # 词云基准图
img_colors = ImageColorGenerator(py_mask) # 读取颜色

# 输入wordcloud
wc1 = WordCloud(
	mask = py_mask, 
    font_path=font, 
	background_color="white").generate(data_1)
wc1.recolor(color_func=img_colors)        # 上色
plt.imshow(wc1, interpolation='bilinear')
plt.axis('off')                           # 关闭坐标轴
plt.show()                                # 输出图片
wc1.to_file('wordcloud.png')                   # 生成文件

绘制词云的时候需要先导入上方的几个库,具体可以看第一节的介绍内容

在主体部分,第一步是导入自己要使用的字体,Windows 系统的字体一般存放在上述路径中,也可以自己从字体网站下载,然后运行代码的时候只需要把该字体的路径填好就可以了。

第二步是对词云基准图进行解析,这里选用的是辽宁省的地图,图片如下方所示。

2
辽宁省地图

在选取基准图的时候要注意,图片上白色区域是不会被读取的,也就是说词云只会显示在白色区域以外,所以如果有形状要求的话尽量选择白底色的图片。首先使用 Image 打开图片,然后将图片转为 numpy 数据类型,此时的 numpy 数据为三维数据,第一、第二维数据为像素位置,第三维数据为颜色。

[in ] print(Image.open('LN.png'))
[out] '<PIL.PngImagePlugin.PngImageFile image mode=RGBA size=1418x906 at 0x21DB3157730>'

[in ] print(py_mask.shape)
[out] '(906, 1418, 4)'

读取完基准图后通过 ImageColorGenerator 读取图片的颜色信息,以用于最后词云图上色。

第三步则是词云的绘制,使用的是 WordCloud,其中需要设置的是参数有:

  • mask:词云形状,即之前读取的词云基准图;
  • font_path:字体文件路径,即一开始设置的 font 变量;
  • background_color:背景颜色,通常设置为白色,可自己调节。

绘制完词云需要进行上色,通过`WordCloud.recolor(color_func=img_colors)` 上色,img_colors 就是之前从图片中读取的颜色信息。

下一步需要将词云信息转换为图片,使用的是 matplotlib.pyplot.plt.imshow,具体步骤为 plt.imshow(wc1, interpolation='bilinear'),第一个参数为词云信息,第二个参数是插值算法,这里的 bilinear 指的是双线性插值,目的是让词云内的文字图像更加平滑,也可以使用其他算法,详细内容可以参见官方文档:Interpolations for imshow/matshow

最后就是对 plt() 的细化,包括隐藏坐标轴、在信息输出栏中输出图片和保存图片。

test-2
词云

提示:在词云绘制完成后,如果发现仍有一些多余的词,可以重新将这些词添加进停用词表中重新生成新词云。

test-3
调整停用词后

以上,仅作为学习分享,欢迎进行交流探讨,如果文中有错误也欢迎联系我修正,十分感谢,联系方式已经贴在下面的 Blog 里了。

这是我的 Blog:songyp0505 ,欢迎交流,及本文原文页:基层治理爬虫及词云