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 分别是 IDDif

查询语句的基本语法就是像上图那样

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

添加计算各难度所占百分比和计算权值功能

效果是这样的

具体评级计算的各难度权值表格

DifficultyValue
入门难度1
普及-2
普及/提高-4
普及+/提高8
提高+/省选-16
省选/NOI-32
NOI/NOI+/CTSC64
尚无评定0

其中“尚无评定”难度权值为 0,因为有很多非洛谷官方题库的题目都是尚无评定难度,这些难度分布差距较大,所以不纳入计算。

Update 2

针对洛谷反爬虫机制增加过滤。
使用原来的代码将会爬出来没有 AC 过的题目并纳入计算。
新版代码会过滤掉那些混淆题目。

Update 3

  • 支持中英文 ID 输入和 UID 输入
  • 根据通过量绘制饼状图。效果如图

代码及数据库下载

旧版本 (485 KB)

原始版本 (485 KB)