欢迎大家来到“Python从零到壹”,在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界。所有文章都将结合案例、代码和作者的经验讲解,真心想把自己近十年的编程经验分享给大家,希望对您有所帮助,文章中不足之处也请海涵。Python系列整体框架包括基础语法10篇、网络爬虫30篇、可视化分析10篇、机器学习20篇、大数据分析20篇、图像识别30篇、人工智能40篇、Python安全20篇、其他技巧10篇。您的关注、点赞和转发就是对秀璋最大的支持,知识无价人有情,希望我们都能在人生路上开心快乐、共同成长。
本文属于番外篇,主要介绍Python可视化分析,利用Python采集每年发表博客的数据,再利用D3实现类似于Github的每日贡献统计,显示效果如下图所示,也是回答读者之前的疑惑。这十多年在CSDN坚持分享,也是一笔宝贵的财务啊!希望文章对您有所帮助,如果有不足之处,还请海涵。
该部分代码在作者Github的番外篇中可以下载,下载地址如下:
本文参考zjw666作者的D3代码,在此感谢,其地址如下:
前文赏析:(尽管该部分占大量篇幅,但我舍不得删除,哈哈!)
第一部分 基础语法
第二部分 网络爬虫
第三部分 数据分析和机器学习
第四部分 Python图像处理基础
第五部分 Python图像运算和图像增强
第六部分 Python图像识别和图像高阶案例
第七部分 NLP与文本挖掘
第八部分 人工智能入门知识
第九部分 网络攻防与AI安全
第十部分 知识图谱构建实战
扩展部分 人工智能高级案例
作者新开的“娜璋AI安全之家”将专注于Python和安全技术,主要分享Web渗透、系统安全、人工智能、大数据分析、图像识别、恶意代码检测、CVE复现、威胁情报分析等文章。虽然作者是一名技术小白,但会保证每一篇文章都会很用心地撰写,希望这些基础性文章对你有所帮助,在Python和安全路上与大家一起进步。
首先介绍博客数据采集方法,为保护CSDN原创,这里仅以方法为主。建议读者结合自己的方向去抓取文本知识。
核心扩展包:
核心流程:
审查元素如下图所示:
对应HTML代码如下:
<div class="article-list">
<div class="article-item-box csdn-tracking-statistics">
<h4 class="">
<a href="https://blog.csdn.net/Eastmount/article/details/124587047">
<span class="article-type type-1 float-none">原创span>
[Python人工智能] 三十六.基于Transformer的商品评论情感分析 (2)keras构建多头自注意力(Transformer)模型
a>
h4>
<p class="content">
本专栏开始,作者正式研究Python深度学习、神经网络及人工智能相关知识。
前一篇文章利用Keras构建深度学习模型并实现了情感分析。这篇文章将介绍
Transformer基础知识,并通过Keras构建多头自注意力(Transformer)模型,
实现IMDB电影影评数据情感分析。基础性文章,希望对您有所帮助!...
p>
<div class="info-box d-flex align-content-center">
<p>
<span class="date">2022-08-28 18:35:44span>
<span class="read-num"><img src="" alt="">2852span>
<span class="read-num"><img src="" alt="">4span>
p>
博客均是按照如下div进行布局。
该部分的关键代码如下:
# coding:utf-8
# By:Eastmount
import requests
from lxml import etree
#设置浏览器代理,它是一个字典
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
num = 2
url = 'https://xxxx/list/' + str(num)
#向服务器发出请求
r = requests.get(url = url, headers = headers).text
#解析DOM树结构
count = 0
html_etree = etree.HTML(r)
div = html_etree.xpath('//*[@class="article-list"]/div')
for item in div:
#标题
value = item.xpath('./h4/a/text()')
title = value[1].strip()
count += 1
print(title)
#时间
blog_time = item.xpath('./div/p/span[1]/text()')
print(blog_time)
print("博客总数:", count)
输出结果如下图所示:
如果需要进一步提取时间,则修改的代码如下所示:
#向服务器发出请求
r = requests.get(url = url, headers = headers).text
#解析DOM树结构
count = 0
title_list = []
time_list = []
year_list = []
html_etree = etree.HTML(r)
div = html_etree.xpath('//*[@class="article-list"]/div')
for item in div:
#标题
value = item.xpath('./h4/a/text()')
title = value[1].strip()
count += 1
print(title)
title_list.append(title)
#日期和时间
data = item.xpath('./div/p/span[1]/text()')
data = str(data[0])
#print(data)
#提取日期
year = data.split("-")[0]
month = data.split("-")[1]
day = data.split("-")[2].split(" ")[0]
blog_time = str(year) + "-" + month + "-" + day
print(blog_time, year)
time_list.append(blog_time)
year_list.append(year)
print("博客总数:", count)
输出结果如下图所示,每页共计40篇博客。
博客翻页主要通过循环实现,下图展示了翻页的网址。
此时增加翻页后的代码如下所示:
# coding:utf-8
# By:Eastmount
import requests
from lxml import etree
#设置浏览器代理,它是一个字典
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
num = 1
count = 0
title_list = []
time_list = []
year_list = []
while num<=18:
url = 'https://xxx/list/' + str(num)
#向服务器发出请求
r = requests.get(url = url, headers = headers).text
#解析DOM树结构
html_etree = etree.HTML(r)
div = html_etree.xpath('//*[@class="article-list"]/div')
for item in div:
#标题
value = item.xpath('./h4/a/text()')
title = value[1].strip()
count += 1
print(title)
title_list.append(title)
#日期和时间
data = item.xpath('./div/p/span[1]/text()')
data = str(data[0])
#提取日期
year = data.split("-")[0]
month = data.split("-")[1]
day = data.split("-")[2].split(" ")[0]
blog_time = str(year) + "-" + month + "-" + day
print(blog_time, year)
time_list.append(blog_time)
year_list.append(year)
num += 1
print("博客总数:", count)
print(len(title_list), len(time_list), len(year_list))
输出结果如下图所示,作者711篇博客均采集完成。
将所有博客时间存储至列表中,再按年份统计每天的发表次数,最终存储至Json文件,按照如下格式。
第一步,生成一年的所有日期,以2021年为例。
import arrow
#判断闰年并获取一年的总天数
def isLeapYear(years):
#断言:年份不为整数时抛出异常
assert isinstance(years, int), "请输入整数年,如 2018"
#判断是否是闰年
if ((years % 4 == 0 and years % 100 != 0) or (years % 400 == 0)):
days_sum = 366
return days_sum
else:
days_sum = 365
return days_sum
#获取一年的所有日期
def getAllDayPerYear(years):
start_date = '%s-1-1' % years
a = 0
all_date_list = []
days_sum = isLeapYear(int(years))
print()
while a < days_sum:
b = arrow.get(start_date).shift(days=a).format("YYYY-MM-DD")
a += 1
all_date_list.append(b)
return all_date_list
all_date_list = getAllDayPerYear("2021")
print(all_date_list)
输出结果如下所示:
['2021-01-01', '2021-01-02', '2021-01-03', ..., '2021-12-29', '2021-12-30', '2021-12-31']
第二步,遍历一年所有日期发表博客的数量。
#--------------------------------------------------------------------------
#遍历一年所有日期发表博客的数量
k = 0
res = {}
while k<len(all_date_list):
tt = all_date_list[k]
res[tt] = 0
m = 0
while m<len(time_list):
tt_lin = time_list[m]
if tt==tt_lin:
res[tt] += 1
m += 1
k += 1
print("\n------------------------统计结束-------------------------\n")
print(res)
#统计发表博客数
count_blog = 0
for i in res.keys():
if res[i] > 0:
count_blog += res[i]
print(count_blog)
输出结果可以看到2021年作者发表博客106篇,并且形成该年的每日发表博客数。
第三步,存储至Json文件。
#--------------------------------------------------------------------------
#存储至Json文件
import json
print(type(res), len(res))
with open("data-2021.json", "w", encoding='utf-8') as f:
json.dump(res, f)
print("文件写入....")
最终生成结果如下图所示:
完整代码如下所示,修改近十年的年份,将生成不同的博客数量。
# coding:utf-8
# By:Eastmount
import requests
from lxml import etree
#--------------------------------------------------------------------------
#采集CSDN博客数据
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
num = 1
count = 0
title_list = []
time_list = []
year_list = []
while num<=18:
url = 'https://xxx/list/' + str(num)
#向服务器发出请求
r = requests.get(url = url, headers = headers).text
#解析DOM树结构
html_etree = etree.HTML(r)
div = html_etree.xpath('//*[@class="article-list"]/div')
for item in div:
#标题
value = item.xpath('./h4/a/text()')
title = value[1].strip()
count += 1
#print(title)
title_list.append(title)
#日期和时间
data = item.xpath('./div/p/span[1]/text()')
data = str(data[0])
#提取日期
year = data.split("-")[0]
month = data.split("-")[1]
day = data.split("-")[2].split(" ")[0]
blog_time = str(year) + "-" + month + "-" + day
#print(blog_time, year)
time_list.append(blog_time)
year_list.append(year)
num += 1
print("博客总数:", count)
print(len(title_list), len(time_list), len(year_list))
#--------------------------------------------------------------------------
#生成一年所有日期
import arrow
#判断闰年并获取一年的总天数
def isLeapYear(years):
#断言:年份不为整数时抛出异常
assert isinstance(years, int), "请输入整数年,如 2018"
#判断是否是闰年
if ((years % 4 == 0 and years % 100 != 0) or (years % 400 == 0)):
days_sum = 366
return days_sum
else:
days_sum = 365
return days_sum
#获取一年的所有日期
def getAllDayPerYear(years):
start_date = '%s-1-1' % years
a = 0
all_date_list = []
days_sum = isLeapYear(int(years))
print()
while a < days_sum:
b = arrow.get(start_date).shift(days=a).format("YYYY-MM-DD")
a += 1
all_date_list.append(b)
return all_date_list
all_date_list = getAllDayPerYear("2021")
print(all_date_list)
#--------------------------------------------------------------------------
#遍历一年所有日期发表博客的数量
k = 0
res = {}
while k<len(all_date_list):
tt = all_date_list[k]
res[tt] = 0
m = 0
while m<len(time_list):
tt_lin = time_list[m]
if tt==tt_lin:
res[tt] += 1
m += 1
k += 1
print("\n------------------------统计结束-------------------------\n")
print(res)
#统计发表博客数
count_blog = 0
for i in res.keys():
if res[i] > 0:
count_blog += res[i]
print(count_blog)
#--------------------------------------------------------------------------
#存储至Json文件
import json
print(type(res), len(res))
with open("data-2021.json", "w", encoding='utf-8') as f:
json.dump(res, f)
print("文件写入....")
可视化主要通过D3实现,推荐读者学习D3可视化知识。本文参考了zjw666作者的代码。
读者可以在我的github下载本文的所有代码及数据。
该项目代码框架如下:
部分的关键代码如下:
index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>calendartitle>
<link rel="stylesheet" type="text/css" href="./index.css" />
head>
<body>
<script src="./lib/d3.js">script>
<script src="./index.js" type='module'>script>
body>
html>
index.css
.box{
margin: 10% auto;
width: 1500px;
height: 400px;
background-color: #fff;
}
.title{
font-size: 20px;
}
.text{
font-size: 10px;
}
.label{
font-size: 12px;
}
chart-headmap.js
export default class Chart {
constructor(){
this._width = 1300;
this._height = 400;
this._margins = {top:30, left:30, right:30, bottom:30};
this._data = [];
this._scaleX = null;
this._scaleY = null;
this._colors = d3.scaleOrdinal(d3.schemeCategory10);
this._box = null;
this._svg = null;
this._body = null;
this._padding = {top:10, left:10, right:10, bottom:10};
}
width(w){
if (arguments.length === 0) return this._width;
this._width = w;
return this;
}
height(h){
if (arguments.length === 0) return this._height;
this._height = h;
return this;
}
margins(m){
if (arguments.length === 0) return this._margins;
this._margins = m;
return this;
}
data(d){
if (arguments.length === 0) return this._data;
this._data = d;
return this;
}
scaleX(x){
if (arguments.length === 0) return this._scaleX;
this._scaleX = x;
return this;
}
scaleY(y){
if (arguments.length === 0) return this._scaleY;
this._scaleY = y;
return this;
}
svg(s){
if (arguments.length === 0) return this._svg;
this._svg = s;
return this;
}
body(b){
if (arguments.length === 0) return this._body;
this._body = b;
return this;
}
box(b){
if (arguments.length === 0) return this._box;
this._box = b;
return this;
}
getBodyWidth(){
let width = this._width - this._margins.left - this._margins.right;
return width > 0 ? width : 0;
}
getBodyHeight(){
let height = this._height - this._margins.top - this._margins.bottom;
return height > 0 ? height : 0;
}
padding(p){
if (arguments.length === 0) return this._padding;
this._padding = p;
return this;
}
defineBodyClip(){
this._svg.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', this.getBodyWidth() + this._padding.left + this._padding.right)
.attr('height', this.getBodyHeight() + this._padding.top + this._padding.bottom)
.attr('x', -this._padding.left)
.attr('y', -this._padding.top);
}
render(){
return this;
}
bodyX(){
return this._margins.left;
}
bodyY(){
return this._margins.top;
}
renderBody(){
if (!this._body){
this._body = this._svg.append('g')
.attr('class', 'body')
.attr('transform', 'translate(' + this.bodyX() + ',' + this.bodyY() + ')')
.attr('clip-path', "url(#clip)");
}
this.render();
}
renderChart(){
if (!this._box){
this._box = d3.select('body')
.append('div')
.attr('class','box');
}
if (!this._svg){
this._svg = this._box.append('svg')
.attr('width', this._width)
.attr('height', this._height);
}
this.defineBodyClip();
this.renderBody();
}
}
最重要的代码是index.js,如下所示:
index.js
import Chart from "./chart-headmap.js";
d3.json('./data/data-2021.json').then(function(data){
/* ----------------------------配置参数------------------------ */
const chart = new Chart();
const config = {
margins: {top: 80, left: 50, bottom: 50, right: 50},
textColor: 'black',
title: '2021年博客创作热力图(106篇)',
hoverColor: 'red',
startTime: '2021-01-01',
endTime: '2021-12-31',
cellWidth: 20,
cellHeight: 20,
cellPadding: 1,
cellColor1: '#F5F5F5',
cellColor2: 'green',
lineColor: 'yellow',
lineWidth: 2
}
chart.margins(config.margins);
/* ----------------------------初始化常量------------------------ */
const startTime = new Date(config.startTime);
const endTime = new Date(config.endTime);
const widthOffset = config.cellWidth + config.cellPadding;
const heightOffset = config.cellHeight + config.cellPadding;
/* ----------------------------颜色转换------------------------ */
chart.scaleColor = d3.scaleLinear()
.domain([0, d3.max(Object.values(data))])
.range([config.cellColor1, config.cellColor2]);
/* ----------------------------渲染矩形------------------------ */
chart.renderRect = function(){
let currentYear, currentMonth;
let yearGroup, monthGroup;
const initDay = startTime.getDay();
let currentDay = initDay;
const totalDays = getTotalDays(startTime, endTime) + initDay;
console.info(totalDays);
const mainBody = chart.body()
.append('g')
.attr('class', 'date')
.attr('transform', 'translate(' + 35 + ',' + 50 + ')')
while(currentDay <= totalDays){
let currentDate = getDate(startTime, currentDay).split('-');
if(!currentYear || currentDate[0] !== currentYear){
currentYear = currentDate[0];
yearGroup = mainBody
.append('g')
.attr('class', 'year ' + currentYear);
}
if (!currentMonth || currentDate[1] !== currentMonth){
currentMonth = currentDate[1];
monthGroup = yearGroup.append('g').attr('class', 'month ' + currentMonth);
}
monthGroup
.append('g')
.attr('class', 'g ' + currentDate.join('-'))
.datum(currentDate.join('-'))
.append('rect')
.attr('width', config.cellWidth)
.attr('height', config.cellHeight)
.attr('x', Math.floor(currentDay / 7) * widthOffset)
.attr('y', currentDay % 7 * heightOffset);
currentDay++;
}
d3.selectAll('.g')
.each(function(d){
d3.select(this)
.attr('fill', chart.scaleColor(data[d] || 0))
.datum({time: d, value: data[d] || 0});
});
function getTotalDays(startTime, endTime){
return Math.floor((endTime.getTime() - startTime.getTime()) / 86400000);
}
function getDate(startTime, day){
const date = new Date(startTime.getTime() + 86400000 * (day - initDay));
return d3.timeFormat("%Y-%m-%d")(date);
}
}
/* ----------------------------渲染分隔线------------------------ */
chart.renderLine = function(){
const initDay = startTime.getDay();
const days = [initDay-1];
const linePaths = getLinePath();
d3.select('.date')
.append('g')
.attr('class', 'lines')
.selectAll('path')
.data(linePaths)
.enter()
.append('path')
.attr('stroke', config.lineColor)
.attr('stroke-width', config.lineWidth)
.attr('fill', 'none')
.attr('d', (d) => d);
function getLinePath(){
const paths = [];
d3.selectAll('.month')
.each(function(d,i){
days[i+1] = days[i] + this.childNodes.length;
});
days.forEach((day,i) => {
let path = 'M';
let weekDay = day < 0 ? 6 : day % 7;
if (weekDay !== 6) {
path += Math.floor(day / 7) * widthOffset + ' ' + 7 * heightOffset;
path += ' l' + '0' + ' ' + (weekDay - 6) * heightOffset;
path += ' l' + widthOffset + ' ' + '0';
path += ' l' + '0' + ' ' + (-weekDay - 1) * heightOffset;
} else {
path += (Math.floor(day / 7) + 1) * widthOffset + ' ' + 7 * heightOffset;
path += ' l' + '0' + ' ' + (-7) * heightOffset;
}
paths.push(path);
});
return paths;
}
}
/* ----------------------------渲染文本标签------------------------ */
chart.renderText = function(){
let week = ['Sun', 'Mon', 'Tue', 'Wed', 'Tur', 'Fri', 'Sat'];
d3.select('.year')
.append('g')
.attr('class', 'week')
.selectAll('.label')
.data(week)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', -40)
.attr('y', heightOffset/2)
.attr('dy', (d,i) => i * heightOffset + 4)
.text((d)=>d);
let months = d3.timeMonth.range(new Date(startTime.getFullYear(), startTime.getMonth(), startTime.getDate()), new Date(endTime.getFullYear(), endTime.getMonth(), endTime.getDate()));
months = months.map((d) => d3.timeFormat("%b")(d));
d3.select('.year')
.append('g')
.attr('class', 'month-label')
.selectAll('text')
.data(months)
.enter()
.append('text')
.attr('x', (d,i) => i*widthOffset*4.25 + widthOffset*2)
.attr('y', -10)
.text((d) => d)
}
/* ----------------------------渲染图标题------------------------ */
chart.renderTitle = function(){
chart.svg().append('text')
.classed('title', true)
.attr('x', chart.width()/2)
.attr('y', 0)
.attr('dy', '2em')
.text(config.title)
.attr('fill', config.textColor)
.attr('text-anchor', 'middle')
.attr('stroke', config.textColor);
}
/* ----------------------------绑定鼠标交互事件------------------------ */
chart.addMouseOn = function(){
//防抖函数
function debounce(fn, time){
let timeId = null;
return function(){
const context = this;
const event = d3.event;
timeId && clearTimeout(timeId)
timeId = setTimeout(function(){
d3.event = event;
fn.apply(context, arguments);
}, time);
}
}
d3.selectAll('.g')
.on('mouseenter', function(d){
const e = d3.event;
const position = d3.mouse(chart.svg().node());
d3.select(e.target)
.attr('fill', config.hoverColor);
chart.svg()
.append('text')
.classed('tip', true)
.attr('x', position[0]+5)
.attr('y', position[1])
.attr('fill', config.textColor)
.text(d.time);
})
.on('mouseleave', function(d){
const e = d3.event;
d3.select(e.target)
.attr('fill', chart.scaleColor(d.value));
d3.select('.tip').remove();
})
.on('mousemove', debounce(function(){
const position = d3.mouse(chart.svg().node());
d3.select('.tip')
.attr('x', position[0]+5)
.attr('y', position[1]-5);
}, 6)
);
}
chart.render = function(){
chart.renderTitle();
chart.renderRect();
chart.renderLine();
chart.renderText();
chart.addMouseOn();
}
chart.renderChart();
});
由于该网页需要通过HTTP访问,因此需要搭建服务器访问。否则可能报错:
Access to script at 'file:///C:/Users/xiuzhang/Desktop/headmap_date/index.js
VS Code在扩展Extensions中搜索Live Server,选择并安装它。
打开网站文件夹,选择具体的页面,右击文件选择 Open with Live Server。
服务器就启动了,所选择浏览的页面也会在浏览器打开
最后按年爬取数据并分别显示即可,修改关键代码为年份。
最终显示结果如下图所示:
写到这里,这篇文章就介绍结束。最后,希望这篇基础性文章对您有所帮助,如果文章中存在错误或不足之处,还请海涵~作为人工智能的菜鸟,我希望自己能不断进步并深入,后续将它应用于图像识别、网络安全、对抗样本等领域,指导大家撰写简单的学术论文,一起加油!
转眼就过年了,2022年简单总结:很多遗憾,很多不足,勉强算是分秒必争,只争朝夕,但愧对家人,陪伴太少,论文、科研、分享和家庭都做得不好,这一年勉强给个65分吧。最最感恩的永远是女神,回家的感觉真好,平平淡淡,温温馨馨,虽然这辈子科研、事业和职称上没有太大的追求,和大佬们的差距如鸿沟,但能做自己喜欢的事,爱自己喜欢的人,每天前进一小步(人生勿比),一家人健康幸福,足矣。提前祝大家新春快乐,阖家幸福。小珞珞是真的逗,陪伴的感觉真好,女神是真的好,爱你们喔,晚安娜继续加油!
(By:Eastmount 2023-01-29 夜于贵阳 http://blog.csdn.net/eastmount/ )