Update on 2018/09/21 最近机房 dalao 爬取了洛谷的全部难度数据(包括主题库、CF、SP、UVa、At),爬取的速度比人手动在题目列表翻页还慢(所以应该不会导致封 IP 吧 qwq),跑了半个多小时。
前言
洛谷于 9 月 23 日加入难易度统计功能,可以在个人主页下方找到
这位 dalao 根据爬取的数据创建了一个数据库,利用这个数据库,我们可以试着统计一下洛谷的做题记录。
就以另一个 dalao 的做题记录进行统计,跑出来的结果是这样的
因为只需要爬取单个用户的主页,所以不会被封 IP(但是如果强行把它魔改成统计多个用户的另说)
准备工作
Python 库
首先需要安装一些必需的库。
爬取页面的 beautifulsoup4
库和解析 HTML 用到的 lxml
库。
这些库都可以通过 pip 来安装。(指令 pip install XXX
)
SQLite3
进行数据库操作需要 SQLite,它可以和 Python 对接(当然也有和其他语言对接的接口,比如 C++ 和 Java)
SQLite 总共只有 2MB 大,把它放到你的硬盘上并添加到环境变量吧。
这样直接在 Python 代码中愉快地 import sqlite3
就好了。
官方下载页传送门
正题
爬取通过题目
洛谷于 9 月 20 日起在用户主页加入混淆题目编号(在浏览器内不会显示但是爬虫可以获取到)
针对这样的反爬虫机制,所以更新了针对混淆题目的过滤机制的代码,这里只提供思路,不提供代码。
请大家不要滥用爬虫,爬虫本应该是一个为人服务的合理工具。
希望大家能够合理利用爬虫,为自己服务。
首先需要获取用户主页 HTML,然后在里面找出用户做过的题目就行了。可以强行在爬到的 HTML 代码里用正则表达式匹配(正则表达式已经凉凉了),当然既然能解析 HTML,那一定要解析它来获取啊。
user = request.urlopen('https://www.luogu.com.cn/space/show?uid=' + uid)
htmldoc = user.read()
soup = BeautifulSoup(htmldoc, 'lxml', from_encoding = 'UTF-8')
uinfo = soup.find('div', class_ = 'lg-article am-hide-sm')
prolist = uinfo.find_all('a')
直接找出 div
标签中 class
名为 lg-article am-hide-sm
的部分,然后里面所有的 a
标签都是题目编号。
(现在需要过滤掉 span
标签,有些带有 a
标签的是混淆题目)
所以像这样遍历一遍就能输出该用户 AC 过的题目。(现在同时会输出混淆题目)
for pid in prolist :
pid = pid.get_text()
pro.append('pid')
反反爬虫
过滤掉 span
标签里的题目。可以考虑先爬取 span
标签内的题目,并存到一个 list (filt
)里。
然后爬取 a
标签内的题目,存到一个 list (pro
)里。使用 remove
函数进行删除操作。
就像这段代码一样。
for pid in filt:
pro.remove(pid)
这样遍历过一遍之后 pro
里就只有正常题目了。
进行数据库查询
数据库查询基本操作
我们使用的数据库中只有一个表(原先本来按题库分类有好几个表的,后来嫌查询起来太麻烦合并成了一个表)
使用 .tables
命令显示表,只有一个叫 QAQ 的表。
让我们以 P1001 为例进行查询。
这个表的两个 column 分别是 ID
和 Dif
查询语句的基本语法就是像上图那样
select [column] from [tab] where [condition];
与 Python 对接
创建数据库连接
conn = sqlite3.connect('luogu_dif.db') #这里写数据库文件的名字
进行数据库查询
cursor = conn.execute('select * from QAQ where ID = "' + pid + '"')
输出查询结果
for row in cursor :
print(row[1])
进行统计
Python 可以在定义列表时这么操作(如果是 C++ 可以使用 map 映射)
diffcnt = {'入门难度':0, '普及-':0, '普及/提高-':0, '普及+/提高':0, '提高+/省选-':0, '省选/NOI-':0, 'NOI/NOI+/CTSC':0, '尚无评定':0}
for pid in prolist :
pid = pid.get_text()
cursor = conn.execute('select * from QAQ where ID = "' + pid + '"')
for row in cursor :
diffcnt[row[1]] += 1
像这样进行输出
for dif, cnt in diffcnt.items() :
print('{DIF} 通过 {CNT} 道题'.format(DIF = dif, CNT = cnt))
完整代码
旧版爬虫,有写爬虫基础的人稍微改一改就能应对反爬机制。更新日志见下方。
import os
import re
import sqlite3
from urllib import request
from bs4 import BeautifulSoup
diffcnt = {'入门难度':0, '普及-':0, '普及/提高-':0, '普及+/提高':0, '提高+/省选-':0, '省选/NOI-':0, 'NOI/NOI+/CTSC':0, '尚无评定':0}
uid = input('UID = ')
user = request.urlopen('https://www.luogu.com.cn/space/show?uid=' + uid)
htmldoc = user.read()
soup = BeautifulSoup(htmldoc, 'lxml', from_encoding = 'UTF-8')
username = soup.find('div', class_ = 'lg-toolbar').get_text()
username = re.sub(r'(U\d+ )|\n', '', username)
uinfo = soup.find('div', class_ = 'lg-article am-hide-sm')
prolist = uinfo.find_all('a')
conn = sqlite3.connect('luogu_dif.db')
for pid in prolist :
pid = pid.get_text()
cursor = conn.execute('select * from QAQ where ID = "' + pid + '"')
for row in cursor :
diffcnt[row[1]] += 1
tot = len(prolist)
print('{USER} 共计通过 {TOTAL} 道题'.format(USER = username, TOTAL = tot))
totval = 0
val = 1
for dif, cnt in diffcnt.items() :
print('{DIF} 通过 {CNT} 道题,占全部的 {PERCENT}%'.format(DIF = dif, CNT = cnt, PERCENT = round(cnt / tot * 100, 2)))
if dif == '尚无评定':
val = 0
totval += val * cnt
val *= 2
print('评级:{VAL}'.format(VAL = totval))
os.system('pause')
Update
添加计算各难度所占百分比和计算权值功能
效果是这样的
具体评级计算的各难度权值表格
Difficulty | Value |
---|---|
入门难度 | 1 |
普及- | 2 |
普及/提高- | 4 |
普及+/提高 | 8 |
提高+/省选- | 16 |
省选/NOI- | 32 |
NOI/NOI+/CTSC | 64 |
尚无评定 | 0 |
其中“尚无评定”难度权值为 0,因为有很多非洛谷官方题库的题目都是尚无评定难度,这些难度分布差距较大,所以不纳入计算。
Update 2
针对洛谷反爬虫机制增加过滤。
使用原来的代码将会爬出来没有 AC 过的题目并纳入计算。
新版代码会过滤掉那些混淆题目。
Update 3
- 支持中英文 ID 输入和 UID 输入
- 根据通过量绘制饼状图。效果如图
代码及数据库下载
旧版本 (485 KB)
原始版本 (485 KB)