通过nginx日志分析网站流量
网站流量的分析一般通过外部网站流量统计工具(如Google Analytics和百度统计)进行,但是对于不以营利为目的、不需深入跟踪和分析用户行为的站点而言,服务器访问日志本身就可以提供足够的信息。本文以本站点(
blog.jaspirit[.]cc
)的Nginx日志为例,展示对站点访问日志的分析过程。
1. 日志解析
根据Nginx日志配置,服务器访问日志包含以下内容(表1):
1 | log_format combined '$remote_addr - $remote_user [$time_local] ' |
指令 | 意义 | 示例 | 备注 |
---|---|---|---|
$remote_addr | 远程客户端地址 | 12.34.56.78 | 本站点仅接入ipv4网络,因此只会出现ipv4地址;另外,因透明反向代理和NAT的存在,此地址不一定是用户真实IP地址,但能一定程度反映用户所处地域 |
$remote_user | 远程客户端用户名(基本身份验证Basic authentication提供的用户名) | — | 本站点不进行身份验证,因此,此项在日志中全为- (空) |
$time_local | 请求时间 | 01/Jan/2001:01:23:45 +0800 | 时间格式为Apache HTTP 服务器日志标准格式 |
$request | 请求信息,包括请求方式、请求URI和HTTP协议版本 | GET /robots.txt HTTP/1.1 | — |
$status | 状态码 | 200 | — |
$body_bytes_sent | 响应主体大小 | 12345 | 单位为字节 |
$http_referer | HTTP来源地址 | https://cn.bing.com/ |
表示用户从哪个页面发起了请求,即用户从哪个页面跳转到当前页面。比如,来源地址为https://cn.bing.com/ 意味着用户是从Bing搜索引擎的搜索结果页面跳转到本站点的。当浏览器有较严格的隐私设置时,请求中将不会包含此项或只包含部分信息,以尽可能阻止网站对用户行为的追踪 |
$http_user_agent | HTTP用户代理(User-Agent)信息 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0 | 包含用户所使用的浏览器、操作系统、硬件信息等(如可判别用户使用的是桌面端还是手机端,使用的浏览器是Chorme还是Edge),也可用于识别网络爬虫(网络爬虫的User-Agent中通常包含bot 、spider 等词语,比如以下日志示例的第一条即为OpenAI的内容抓取机器人在读取robots.txt 中的内容以确定可抓取的内容) |
1 | 52.230.152.165 - - [03/Aug/2024:09:52:26 +0800] "GET /robots.txt HTTP/1.1" 200 240 "-" "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.2; +https://openai.com/gptbot)" |
我们需要解析以上格式的日志文件,为此编写了parse_log
函数,负责使用pandas
的read_csv
函数肚读取文本文件为DataFrame
对象,并将其中的请求时间(time_local
列)使用datetime.strptime
转换为pandas
的时间戳格式(pandas._libs.tslibs.timestamps.Timestamp
)
1 | import pandas as pd |
使用blog=parse_log('blog.log')
读取日志文件后生成的DataFrame示例如表2所示(共1186635次请求,表2为使用blog.sample(5).to_markdown()
随机选取了5次请求,输出markdown格式的字符串,并由本博客的hexo
引擎渲染的结果。下文出现的所有数据表格均由类似方法生成,不再赘述)
remote_addr | time_local | method | request_uri | http_version | status | body_bytes_sent | http_referer | http_user_agent | |
---|---|---|---|---|---|---|---|---|---|
116815 | 141.52.248.4 | 2022-07-19 23:06:41 | GET | /posts/ca2188fc/2.png | HTTP/1.1 | 200 | 30656 | https://blog.jaspirit.cc/posts/ca2188fc/ | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 |
658624 | 117.151.144.178 | 2024-02-23 10:15:51 | GET | /archives/2022/07/ | HTTP/1.1 | 200 | 20378 | - | Go-http-client/1.1 |
490602 | 113.140.6.198 | 2023-10-08 15:02:40 | GET | /posts/ca2188fc/10.png | HTTP/1.1 | 200 | 213589 | https://blog.jaspirit.cc/posts/ca2188fc/ | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60 |
518008 | 96.126.112.220 | 2023-11-01 21:23:28 | GET | / | HTTP/1.1 | 200 | 7973 | - | Mozilla/5.0 |
955383 | 202.200.237.112 | 2024-06-12 08:47:29 | GET | /posts/ca2188fc/12.png | HTTP/2.0 | 200 | 84319 | https://blog.jaspirit.cc/posts/ca2188fc/ | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0 |
2. 数据清洗
网络流量中存在大量机器人和攻击流量,需要识别并剔除它们才能得到真实用户访问情况。我们首先筛选有效IP和合法请求,然后识别网络爬虫请求,最后根据请求频率区分正常/异常请求,同时统计会话数。
2.1 有效IP的筛选
编写is_valid_ipv4
函数识别remote_addr
是否是有效的ipv4地址(需要导入ipaddress
库),并用DataFrame的drop
方法删除无效列(剔除了1个请求,剩余1186634个请求)。
1 | import ipaddress |
2.2 合法请求的筛选
本站为静态站点,用户只需且只可发送GET请求以获取内容,所以GET请求以外的请求是非法的,并且响应状态码应为200
(OK)表示请求成功。我们通过blog.value_counts(['method','status'])
获取不同请求方式-状态码组合的请求数量(表3),可见在GET请求中有高达21.2%的请求返回404
状态码。
count | |
---|---|
(‘GET’, 200) | 880617 |
(‘GET’, 404) | 245476 |
(‘GET’, 304) | 14367 |
(‘GET’, 400) | 11924 |
(‘GET’, 301) | 6736 |
(‘POST’, 404) | 4695 |
(‘POST’, 405) | 2668 |
(‘CONNECT’, 400) | 1902 |
(‘GET’, 403) | 1453 |
(‘HEAD’, 404) | 1230 |
(‘HEAD’, 200) | 1108 |
共有880617个请求(74.4%)是GET请求方式且请求成功的,将它们标记为合法请求。
1 | blog['is_ok']=((blog.status==200)&(blog.method=='GET')).astype(bool) |
2.3 非法请求的分析
在返回404
状态码的请求中,请求/ads.txt
,/favicon.ico
和/.env
的请求数最高,三者合计占9.9%。其中,
/ads.txt
是用于确定和验证站点的广告资源是否仅通过授权的卖家销售的文件,由于本站点没有接入广告商,所以此项可忽略;/favicon.ico
是站点图标,缺失站点图标可能导致搜索引擎展示站点条目时不能同步展示站点图标(尤其是站点未显式指明其图标的位置时)。目前已经通过使用Nginx的alias
指令将/favicon.ico
的请求映射到/assets/images/hexo/favicon.ico
解决;/.env
是这三个请求中最值得警惕的,因为对它的请求属于网络攻击请求。.env
常被用来存储(交互式网站的)应用程序的环境变量,如数据库地址、API密钥等,一旦泄露有被恶意利用的风险。此文件不应被公开访问。
另外,在非成功请求中,有高达8.7%的请求含有wp-*
(如wp-admin
、wp-login
、wp-includes
、wp-add
)或wordpress
字样,它们是对流行的Wordpress系统的攻击,可见网络攻击活动之普遍和猖獗。
2.4 机器人和网络爬虫识别
网络抓取机器人和网络爬虫是自动浏览网页的程序,一般用于编制网络索引供搜索引擎使用,近年来也开始用于抓取文本以建立大语言模型语料库。合规的网络爬虫会在User-Agent中用bot
、spider
等关键词亮明自己的机器人身份并附带相关说明链接(见日志示例第一条),且遵循网站的robots.txt
文件中的抓取策略进行内容抓取。当然,也有带有非法利用和网络攻击性质的机器人和爬虫,它们可能会伪装成正常用户以规避网站对机器人流量的限制,或者根本不做伪装,赤裸裸地展示自己自动程序的特征。我们通过pandas
字符串处理功能中的contains
函数筛选匹配特定正则表达式的User-Agent字符串,将323201个请求(27.2%)标记为机器人流量。
1 | regexp_bots = r"bot|spider|crawl|slurp|feed|track|index|Go-http-client|python-requests|http|\+\w+@\w+\.\w+|^-$|Mediapartners-Google|Wget|facebookexternalhit|curl|msray-plus|axios|GoogleOther|aria2" |
列出匹配每种正则表达式的请求数量(表4)。因为请求的User-Agent可能同时与多个正则表达式匹配,所以下表中请求数量的加和高于总请求数:
1 | bot_requests = pd.DataFrame( |
Bot Identfier | Requests |
---|---|
http | 260793 |
Go-http-client | 151772 |
bot | 99953 |
^-$ | 26671 |
spider | 17345 |
crawl | 10394 |
python-requests | 6168 |
curl | 3175 |
facebookexternalhit | 1455 |
feed | 443 |
+\w+@\w+.\w+ | 434 |
GoogleOther | 122 |
index | 96 |
Wget | 90 |
axios | 87 |
Mediapartners-Google | 74 |
slurp | 48 |
msray-plus | 37 |
track | 1 |
aria2 | 1 |
在被标记为机器人流量的请求中,有47%请求使用Go语言常用网络库的默认User-Agent,另有8.3%的请求User-Agent为空值,还有13次请求根本没有User-Agent字段。剩余的31.3%为合规机器人。
我们使用blog.drop(blog[(~blog.is_ok)|blog.is_bot].index,inplace=True)
从数据集中剔除非法请求和被标记为机器人流量的请求,剩余653620个请求,占总请求数的55.1%。至于伪装成正常用户的User-Agent的请求,我们会在下一步根据请求频率进行判别。
2.5 请求频率筛选和会话数计算
真人浏览网站时,不会在极短时间内访问多个页面,而自动程序的行为则与之相反。因此,可通过客户端发送请求的频率分辨真人和机器人。这一步可以识别出一部分伪装User-Agent的爬虫请求。
会话的区分
请求频率是以会话(Session)为单位计算的。会话是指用户从进入网站浏览页面开始到离开网站为止的一段交互过程。用户在一次会话中可访问网站的多个页面。会话数和用户数的区分,在流量分析工具中是通过Cookie实现的。用户首次访问站点时,流量分析工具向用户发放Cookie作为具唯一性的身份凭证,当用户带着Cookie再次访问站点时,流量分析工具就可以通过Cookie识别新老用户身份,进而计算独立用户数量和会话次数。但是,在2024年9月之前,本站点未启用基于Cookie的分析,所以只能通过IP地址、请求时间和User-Agent三类信息区分不同会话,且不能做到区分用户。
我们认为:来源于同一IP地址、同一User-Agent,且相邻请求间隔小于特定时长(该时长设定为变量max_interval
)的请求属于同一会话。为了计算请求间隔时间,首先需要按照同一IP地址、同一User-Agent的汇总条件进行分类汇总操作。我们将DataFrame按照第一关键字remote_addr
、第二关键字http_user_agent
和第三关键字time_local
排序(此排序操作是为了与接下来的分类汇总操作结果的条目索引顺序相匹配),然后调用groupby
方法,以第一关键字remote_addr
、第二关键字http_user_agent
进行分类汇总,汇总量为访问时间time_local
,聚合函数(对分类汇总后的每一组中的数据进行统计分析的函数)为差分函数diff
(计算每组中相邻请求的间隔时间)。
1 | blog.sort_values(['remote_addr','http_user_agent','time_local'],inplace=True) |
其结果如表5所示(为展示简洁仅选择了remote_addr
、time_local
、http_user_agent
、time_diff
四列),其中time_diff
中的NaT
表示一次会话的起始。在示例中的请求,第1条和第2条间隔仅1秒,而第2条和第3条间相差354天,因此第1、2条应被归为一次会话,同理第3、4条被归为另一次会话。
remote_addr | time_local | http_user_agent | time_diff |
---|---|---|---|
1.117.192.188 | 2022-05-04 00:55:50 | Mozilla/5.0 (X11; Linux x… | NaT |
1.117.192.188 | 2022-05-04 00:55:51 | Mozilla/5.0 (X11; Linux x… | 0 days 00:00:01 |
1.117.192.188 | 2023-04-23 17:35:55 | Mozilla/5.0 (X11; Linux x… | 354 days 16:40:04 |
1.117.192.188 | 2023-04-23 17:35:56 | Mozilla/5.0 (X11; Linux x… | 0 days 00:00:01 |
1.117.192.188 | 2022-03-06 10:59:03 | Mozilla/5.0 (compatible; … | NaT |
1.117.192.188 | 2022-03-06 10:59:03 | Mozilla/5.0 (compatible; … | 0 days 00:00:00 |
1.117.192.188 | 2022-05-04 00:56:56 | Mozilla/5.0 (compatible; … | 58 days 13:57:53 |
1.117.192.188 | 2022-05-04 00:56:56 | Mozilla/5.0 (compatible; … | 0 days 00:00:00 |
1.117.63.160 | 2024-03-12 17:23:49 | Mozilla/5.0 (iPhone; CPU … | NaT |
1.117.63.160 | 2024-03-14 22:21:02 | Mozilla/5.0 (iPhone; CPU … | 2 days 04:57:13 |
1.117.66.65 | 2024-03-12 17:23:49 | Mozilla/5.0 (iPhone; CPU … | NaT |
1.117.66.65 | 2024-03-12 17:23:49 | Mozilla/5.0 (iPhone; CPU … | 0 days 00:00:00 |
1.117.66.65 | 2024-03-12 17:23:49 | Mozilla/5.0 (iPhone; CPU … | 0 days 00:00:00 |
1.117.66.65 | 2024-03-12 17:23:49 | Mozilla/5.0 (iPhone; CPU … | 0 days 00:00:00 |
接下来编写identify_session
函数确定请求所属会话,我们使用uuid.uuid4
给每次会话赋唯一标识符:
1 | def identify_session(time_diff, max_interval=3600): |
计算不同绘制max_interval
下的会话数(表6)并绘制关系图(图1):
1 | max_intervals = [ |
max_interval | sessions |
---|---|
30 | 98817 |
60 | 97175 |
120 | 95865 |
300 | 94415 |
600 | 93609 |
1800 | 92511 |
3600 | 91842 |
7200 | 91021 |
14400 | 90248 |
28800 | 89285 |
43200 | 88686 |
86400 | 86855 |
172800 | 84233 |
可见随着max_interval
的增大,曲线由陡峭下降转为平缓下降。我们在陡峭与平缓的过渡处取3600秒(1小时)作为参数,区分出91842次会话,将请求所属会话的UUID存储于session_uuid
列(分析证明max_interval
即使取得很大如86400
,后续分析结果差异也不大),接下来便可以计算每次会话的请求频率了。
1 | blog["session_uuid"] = identify_session(blog.time_diff, max_interval=3600) |
请求频率计算与筛选:
用户访问网页时,为了构建和渲染完整的页面,浏览器除了请求HTML文档外,还需要请求文档中指明需加载的CSS、JS文件和媒体资源(如图片、视频等)。假设页面中含有10张图片,那么访问一个页面时的请求频率可能超过10次/秒。由于不同页面包含的多媒体资源数量不一,导致页面的“正常”请求频率也不同,这给按请求频率区分机器人和正常用户造成了困难。为应对此问题,我们只计算对HTML文档发起请求的频率。
使用以下正则表达式筛选对HTML文档(包括主页、所有博文、归档页、类别页)的请求(is_pageview
)和仅对博文页面的请求(is_postview
),符合前者的请求共有72578次(11.1%),符合后者的请求共有38573次(5.9%)。包含对HTML文档请求的会话共有62679次(用blog[blog.is_pageview].session_uuid.unique().size
得出),占总会话数的68.2%,占总剩余请求数的89.5%。
1 | from urllib.parse import unquote |
以会话为单位,计算对页面HTML文档的请求的频率(以Hz为单位,即每秒访问多少个页面):
1 | session_requests_freq = ( |
在0-2Hz范围内作直方图(图2,直方图不包括0Hz即只访问一个页面的会话,共58741次)及累加曲线 ,可见在1Hz和2Hz频率存在显著峰值,这很可能是自动程序的频率设置。我们认为请求频率超过0.1Hz(10秒之内访问多于1个页面)的为异常(图中红色虚线右侧),将1399次会话(2.3%)和87973个请求标记为机器人流量并剔除,剩余61977次会话和566490个请求,占上步筛选剩余请求数(653620)的86.5%,占总请求数(1186635)的47.7%。
1 | valid_sessions = session_requests_freq[session_requests_freq < 0.1].index.tolist() |
继续筛选对页面HTML文档的请求,生成名为blog_real_pageview
的DataFrame,并去除其中2024年9月的部分(此月份数据不完整),blog_real_pageview
中共含有来自60432次会话的65349个请求。
1 | blog_real_pageview = ( |
至此我们完成了数据清洗,将以blog_real_pageview
为数据源进行后续分析。
3. 访问量分析
3.1 页面总访问量
为了获取每个请求对应页面的标题,需要读取博文的markdown源文档,标题和链接(abbrlink
)在源文档的front matter
中:
1 | allposts = glob.glob("hexo/source/_posts/*.md") |
使用blog_real_pageview.title.value_counts()
计算各页面访问量(表7、图3)。若包括主页,访问量总计为65349;若排除主页只统计博文,则访问量总计为34588(以下分析均排除主页访问,使用blog_real_postview
为数据源)。其中《X射线吸收精细结构——原理及应用 》访问量最高,达13006次(37.6%);访问量前5名占总量的72.9%,前10名占总量的87.9%。
title | abbrlink | count |
---|---|---|
主页 | / | 30761 |
X射线吸收精细结构——原理及应用 | ca2188fc | 13006 |
HER 和 OER 反应机理与其 Tafel 斜率推导 | cd97ad66 | 5174 |
三个电化学基本方程的推导 | 243b81e8 | 2860 |
校园网环境下软路由及NAS的搭建 | 69f0ff47 | 2179 |
《神经科学——探索脑》 第一篇(1-7章)思维导图及课后答案 | 241ae90f | 2008 |
神经电极的发展历程 | abcc4c8b | 1651 |
深入理解主成分分析 PCA | d195a098 | 1054 |
同位素峰强度分布计算原理及python实现 | ddeab4f8 | 1026 |
2048游戏算法的进一步研究及C++语言实现 | 5d16216f | 814 |
膜蒸馏技术——原理与应用 | 4ecad084 | 639 |
基于Python的Bilibili和Bangumi动画评分综合分析 | a9da3a59 | 613 |
生物化学和化学生物学的区别 | bf4db457 | 539 |
基于 nginx 的高性能 jsdelivr 反向代理 | ffee5413 | 515 |
化学分析和仪器分析的区别 | 5bbc70af | 425 |
神经科学的早期发展 | 2ebf08cd | 359 |
涉及X射线研究的诺贝尔奖 | 158db4df | 311 |
两岸统一态度调研 | 505831fa | 277 |
电解水过程中的催化普适性原理 | 6d41b4bf | 246 |
互联网时代的艺术:LBRY简介 | 3c74ee4e | 242 |
线性回归的显著性检验 | 3a8848f3 | 229 |
安全套的前世今生——高分子材料如何改变着这个世界 | 815b5cac | 157 |
追迹互联网(2010-2016) | c9caace5 | 106 |
LBRY 汉化相关问题 | 3916b4b8 | 90 |
ロビンソン和STYX HELIX中文翻译 | 9f288f6d | 68 |
本博客自上线之日起就使用了不蒜子计数器, 该计数器对博文访问量(PV)的统计结果与本分析得出的结果十分接近(误差小于10%),相互印证了两种计数方式的准确性。
3.2 月度访问量
使用blog_real_postview.time_local.dt.to_period("Y")
或to_period("M")
提取请求所在年份或月份,通过分类汇总(如blog_real_postview.groupby("Year").size()
)得到年度或月度访问量变化:
1 | blog_real_postview["Year"] = blog_real_postview.time_local.dt.to_period("Y") |
2022年(2月-12月)、2023年(全年)、2024年(1月-8月)的访问量分别为5222,9608和19758。月度访问量变化如图4所示。可见自2024年3月起,博文月访问量从1000左右陡增至3000左右,我们将在后续分析中探查访问量陡增的原因。
3.3 页面访问量变化
上文中我们计算了每篇博文全部时间的浏览量和按月统计的所有博文浏览量,那么可否将二者结合起来,即同时按月份和按每篇博文进行访问量统计?pandas
的数据透视表(pivot table) 功能完美符合我们的需求。
首先,为了使图表整洁一些,我们只取总访问量前10名的博文进行透视分析。
1 | top10_posts=blog_real_postview.abbrlink.value_counts().head(10).index.values |
然后,使用pd.pivot_table
创建数据透视表,将 Month
列作为数据透视表的行索引,将 abbrlink
列作为数据透视表的列索引,使用 size
作为聚合函数,计算每个组合的大小(即访问量计数),并将缺失值(即某文章某月访问量为0的情况)填充为0。
1 | views_by_post_by_month = pd.pivot_table( |
创建的数据透视表(表8)如下:
Month | 241ae90f | 243b81e8 | 4ecad084 | 5d16216f | 69f0ff47 | abcc4c8b | ca2188fc | cd97ad66 | d195a098 | ddeab4f8 |
---|---|---|---|---|---|---|---|---|---|---|
2022-02 | 33 | 24 | 0 | 1 | 62 | 0 | 126 | 0 | 34 | 19 |
2022-03 | 148 | 36 | 0 | 3 | 119 | 0 | 112 | 0 | 29 | 19 |
2022-04 | 59 | 29 | 0 | 5 | 73 | 0 | 211 | 0 | 51 | 27 |
2022-05 | 41 | 37 | 0 | 8 | 57 | 0 | 300 | 4 | 73 | 35 |
2022-06 | 45 | 65 | 0 | 24 | 75 | 165 | 318 | 50 | 33 | 32 |
2022-07 | 35 | 49 | 0 | 1 | 43 | 8 | 90 | 32 | 10 | 15 |
2022-08 | 32 | 25 | 9 | 1 | 62 | 6 | 53 | 14 | 12 | 6 |
2022-09 | 18 | 25 | 3 | 3 | 56 | 11 | 71 | 6 | 13 | 7 |
2022-10 | 33 | 56 | 4 | 0 | 74 | 8 | 76 | 24 | 15 | 13 |
2022-11 | 85 | 49 | 17 | 1 | 78 | 17 | 127 | 28 | 22 | 16 |
2022-12 | 72 | 50 | 31 | 6 | 156 | 10 | 153 | 34 | 20 | 13 |
2023-01 | 41 | 28 | 6 | 2 | 49 | 8 | 114 | 54 | 14 | 4 |
2023-02 | 77 | 43 | 16 | 3 | 110 | 17 | 191 | 67 | 23 | 19 |
2023-03 | 56 | 47 | 33 | 1 | 101 | 26 | 264 | 146 | 31 | 14 |
2023-04 | 38 | 33 | 13 | 1 | 55 | 34 | 184 | 147 | 10 | 24 |
2023-05 | 34 | 52 | 36 | 9 | 85 | 74 | 236 | 192 | 8 | 22 |
2023-06 | 33 | 28 | 24 | 1 | 45 | 47 | 268 | 128 | 15 | 33 |
2023-07 | 46 | 28 | 14 | 4 | 37 | 38 | 217 | 106 | 11 | 23 |
2023-08 | 63 | 27 | 27 | 5 | 60 | 31 | 203 | 88 | 5 | 20 |
2023-09 | 66 | 43 | 22 | 3 | 43 | 44 | 276 | 134 | 11 | 26 |
2023-10 | 100 | 62 | 30 | 8 | 31 | 109 | 373 | 152 | 21 | 39 |
2023-11 | 118 | 63 | 29 | 17 | 61 | 66 | 355 | 171 | 15 | 41 |
2023-12 | 117 | 65 | 27 | 48 | 47 | 49 | 492 | 206 | 43 | 40 |
2024-01 | 77 | 71 | 16 | 7 | 91 | 47 | 476 | 173 | 32 | 32 |
2024-02 | 33 | 46 | 21 | 2 | 73 | 32 | 346 | 116 | 9 | 14 |
2024-03 | 87 | 148 | 24 | 18 | 70 | 79 | 1089 | 360 | 21 | 51 |
2024-04 | 98 | 164 | 49 | 38 | 54 | 168 | 1066 | 561 | 41 | 71 |
2024-05 | 104 | 225 | 50 | 86 | 93 | 122 | 1122 | 704 | 81 | 81 |
2024-06 | 89 | 405 | 40 | 172 | 100 | 111 | 1446 | 557 | 49 | 115 |
2024-07 | 78 | 458 | 59 | 189 | 66 | 145 | 1331 | 477 | 137 | 99 |
2024-08 | 52 | 379 | 39 | 147 | 53 | 179 | 1320 | 443 | 165 | 56 |
我们使用matplotlib
的stackplot
(堆叠面积图)可视化数据透视表的内容:
1 | colors = plt.cm.tab20.colors # Ensure enough colors |
可见,2024年3月及之后,除了241ae90f
这篇博文外,其他文章的访问量均有增加。我们可以进一步计算每月每篇文章的访问量占比并绘图(图6),这样既可以识别出哪些文章在特定时间段内更受欢迎,又可以避免访问量陡增造成的视觉上的干扰:
1 | views_by_post_by_month_ratio = views_by_post_by_month.div( |
从上图中我们发现,在2024年3月前后,各页面浏览量占比并没有出现特别剧烈的变化。综合以上信息,我们认为:访问量陡增并不是因为部分博文浏览量突然增加,而是整个站点在搜索引擎上的曝光度增加导致的。在下一节中我们证实了这一猜测。
4. 来源分析
4.1 用户地区分布
使用geoip2
库解析IP属地:
1 | import geoip2.database |
分类汇总结果(图7)显示,68.5%的用户来自中国内地,来自美国、新加坡、中国香港、日本的用户位列其后。不过可以肯定的是,由于本网站服务器位于香港,部分中国内地用户访问本网站时,会因分流规则致其使用非真实IP发起请求。因此,实际来自中国内地的用户占比应更大,可能超过80%。
4.2 客户端分布
User-Agent当中可提取出用户使用的客户端(桌面端或移动端)、浏览器和操作系统类型。编写parse_user_agent
函数,通过正则表达式匹配和提取以上信息。
需要注意的是,因为兼容性等原因,User-Agent中可能包含其他浏览器的标识。比如,Chrome
和Safari
标识就出现在Edge和Opera浏览器的User-Agent当中(这些浏览器虽有特有标识,但出现于通用标识之后。比如Edge浏览器的标识是Edg
,但以Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0
形式出现)。所以,在匹配浏览器信息时,需要注意匹配的先后顺序。在匹配逻辑中如果通用标识Chrome
在先,将会忽略后面可能出现的特有标识,进而造成误判。因此,对通用标识的匹配应排在最后,确保特有标识先被匹配到,详见以下代码中的browser_patterns
。
1 | def parse_user_agent(user_agent): |
对http_user_agent
列运用apply
方法,将解析结果保存在名为user_agent_parsed
的字典中,绘制各属性分布扇形图(其中对于操作系统和浏览器,第4名之后不再单独列出)
1 | user_agent_parsed = pd.DataFrame( |
由上图可得:
- 客户端和操作系统方面:桌面端-移动端比例为9:2。其中,桌面端Windows-MacOS比例约为7:1,移动端安卓-苹果比例约为7:2;
- 浏览器方面:Chrome和Edge分庭抗礼,两者共占据80%以上的份额。
在不单独列出的Others
部分,存在以下有趣的事实:
- 3.6%的用户来源于Linux系统;
- 2.4%的用户使用微信内置浏览器(标识为
MicroMessenger
)访问本网站,其中80%的请求被微信安全中心拦截,但这些用户仍选择继续访问。这是结合对HTTP来源地址的分析得到的结果。如果用户是从微信安全中心拦截页面(域名为weixin110.qq[.]com
)跳转过来的,那么请求头的HTTP来源地址中会包含weixin110
字样。对HTTP来源地址的分析详见下节。
4.3 HTTP来源分析
如果用户是从其他页面(比如搜索引擎结果页面)跳转到本站的,并且浏览器安全设置处于较宽松模式,那么请求头中的HTTP来源地址将包含来源页面的域名,还可能获得来源页面的完整URL,比如带搜索词的完整搜索链接(如google[.]com/search?q=123456
),这使得我们能进行(搜索引擎)来源分析和关键字研究。
首先我们计算来源于各搜索引擎、CSDN、本站(自引用)、微信安全中心拦截页面的点击数及其比例(表9):
1 | blog_real_postview['source']=blog_real_postview.http_referer.str.extract(r'(Bing|Baidu|Google|Yahoo|Sogou|360|DuckDuckGo|Yandex|CSDN|jaspirit|weixin110)',flags=re.IGNORECASE) |
Source | Requests | Ratio |
---|---|---|
Bing | 13292 | 60.43% |
4756 | 21.62% | |
jaspirit | 1901 | 8.64% |
Baidu | 1059 | 4.81% |
weixin110 | 705 | 3.21% |
CSDN | 200 | 0.91% |
Yandex | 28 | 0.13% |
DuckDuckGo | 16 | 0.07% |
Yahoo | 13 | 0.06% |
Sogou | 13 | 0.06% |
360 | 11 | 0.05% |
一共有22013次(63.6%)博文访问的HTTP来源地址不为空,上列来源能包含这部分几乎全部(99.91%)的请求。由上表可得:
- 搜索引擎方面,Bing、Google和Baidu占绝大多数,三者合计占86.9%。计算每个搜索引擎点击量前5名的博文的点击量分布(图9)。其中,百度对本站收录意愿不大,只收录了几篇有外链链接的博文,本站在百度上的可见度和曝光量也远不如其他两个搜索引擎大;
- 自引用占比8.6%;
- 由于博主在CSDN上本人转载了本博客的几篇博文,有不到1%的来源于CSDN查看原文的访客;
- 来源于微信(微信安全中心拦截页面)的比例要比上节中计算的2.4%高,是因为来源于此页面的请求基本不会隐藏HTTP来源地址。相比于其他浏览器,隐藏HTTP来源地址的比例低,导致最终占比偏高。
为了研究三家搜索引擎点击量随时间的变化,我们仿照前文方法构建数据透视表views_by_source_by_month
,这次是以source
作为汇总的列索引:
1 | views_by_source_by_month = pd.pivot_table( |
绘制点击量变化(图10)和相对占比变化(图11),可见从2024年3月起,本站在Bing上的点击量陡增,相比之下来自于Baidu和Google的点击量变化不大,这导致Bing的相对占比陡增,从60%左右上升到80%左右。因此,本站于2024年3月起的访问量陡增可归因于在Bing上的可见性和曝光度的增加。
4.4 关键字研究
可以进一步提取搜索词进行关键字研究。我们首先提取HTTP来源地址中非自引用的外链,去重后使用urllib.parse.unquote
解码URL:
1 | from urllib.parse import unquote |
得到632个不重复的外链extlinks_set
。接下来编写函数从其中提取搜索字符串,并删除只包含英文和数字字符的字符串(它们一般不包含搜索词)
1 | def extract_search_string(url): |
得到以下搜索关键词/关键字。关键字研究可以帮助站长了解用户需求,通过使用更匹配用户需求的关键字提升网站点击率。理论上,可进一步进行分词和词云分析,但本文因篇幅原因不再详述。
1 | 神经电极的发展历程 |
5. 结语
通过对本站点的Nginx日志进行深入分析,我们获得了丰富的网站流量信息,这些信息对于理解网站运营情况和用户行为具有重要意义。主要发现包括:
- 数据清洗过程中,我们识别并剔除了大量机器人和攻击流量,最终保留了47.7%的请求,这凸显了互联网环境中自动化程序的普遍存在。
- 访问量分析显示,少数几篇文章贡献了大部分的流量,其中《X射线吸收精细结构——原理及应用》一文的访问量最高,占比37.6%。
- 来源分析显示,桌面端用户占主导地位(约82%),其中Windows系统用户最多。在浏览器方面,Chrome和Edge是最常用的两种。
- 结合访问量分析和来源分析,发现搜索引擎是主要流量来源,其中Bing贡献了最多的流量(60.43%)。特别是从2024年3月开始,来自Bing的流量显著增加,这解释了总体访问量的突然上升。
值得注意的是,尽管服务器日志分析提供了丰富的信息,但它也有局限性。例如,无法准确区分独立用户,也难以追踪用户在站内的完整行为路径。因此,对于需要更深入用户行为分析的网站,可以考虑结合使用如Google Analytics等专业的流量分析工具。
总的来说,这种基于服务器日志的分析方法为小型、非商业性质的网站提供了一个简单但强大的流量分析途径,既能最大限度地保护用户隐私,又能获得有价值的网站流量信息。