Python爬虫快速获取西工大NOJ习题文档攻略
写在前面:
非计算机科班,仅仅是个人爱好。第一次尝试爬虫,前端也没有学过,一定会有不太对的地方,欢迎大佬批评指正!
这是我为了偷懒做出来的小玩意,也是我的python期末大作业,正文内容直接是我当时的报告。你可以使用或者进一步修正这个程序,以此方便你的学习和考试。请不要用它去交作业,选题太特殊了,老师和助教给了满分,所以一定会认得我的东西,会直接被判为抄袭。如果你是外校,也进不去这个网站。
p.s.学号就不马赛克了,实名上网了属于是。麻烦认识的人不要盒我,因为在网上风格和线下不太一样TT
以下是正文:
开发背景:
众所周知,noj最讨人厌的就是不能复制输入输出(强烈建议放入系统升级日程),导致我上个学期学习C语言写字符串的时候频繁破防。
因此我写了这个程序,输入你的noj账号密码,你就可以获取所有noj题目,并且可以自定义是否需要输入输出,是否需要英语描述。这样就可以不用进入开发者选项就能复制粘贴题目,达到一劳永逸的效果。同时在期末打印实验课资料的时候也不用一张一张截图,并且可以通过排版尽可能地节约纸张。达到一个保护环境的效果(?)。
开发工具和第三方库:
PyCharm 2023.3.4
tkinter:用于创建GUI界面。
requests:用于发送HTTP请求。
BeautifulSoup:用于解析HTML内容。
docx:用于生成Word文档。
re:用于正则表达式匹配。
io:用于处理二进制流。
主要功能:
这是一个从noj的在线题库中自动抓取题目信息的GUI应用程序。以下是其主要功能概述:
- 用户可以在GUI界面中输入noj账户名和密码进行登录验证,界面有弹窗会指引用户操作。
- 成功登录后,用户可以自行选择是否需要题目英文描述,是否需要题目输入输出范例。
- 选择结束后,程序会抓取noj题库页面文字和图片,并将抓取的内容输出到文档内。
- 程序能够在pycharm中编译、运行。
设计思路:
爬虫部分实现思路分析:
我们可以发现,打开NOJ网站之后首先需要登录,我们建立一个字典,把用户输入的用户名和密码放在里面:
login_data = {
'username': username,
'password': password
}
然后将其作为data,发送一个post请求给登录URL来实现登录,紧接着检验是否登陆成功,即是否成功跳转到了http://10.12.13.248/cpbox/webfile.aspx界面。万幸的是,登录系统用户名及密码没有进行过加密。
然而,跳转成功之后我们并没有在写题的界面,题目内容也无法搜索到,而正常情况下,我们需要依次点击作业空间—NOJ作业然后到达写题界面,打开开发者工具我们可以发现:
左边框和右边框各是一个独立的iframe,iframe是前端内嵌页面,访问域名与主网页不同,requests请求无法获取,技术可以的话,可以从解析js抓取iframe域名,然而我对HTML的了解就仅仅是皮毛,显然是不符合的……
一般遇到这种问题解决的常用思路:通过selenium模拟点击两下之后再继续requests。
但是,结合之前网易云的失败经验,后面还有100道题,如果每个都这样做不得累死,而且selenium也太慢了。蛋柿!天无绝人之路,经过我的到处挖呀挖呀挖,我发现题目界面的域名就正大光明地写在了里面:
点开之后:
一样的问题,里面的东西都是js实时加载的,我们沿用之前的思路。
这里的URL看起来很可疑,但是明显前面还差个什么前缀(当时太蠢了没想到直接加上级域名)。
发现它就是前面神秘URL的完整版,复制一下打开,我们成功找到了题目界面,此时题目以及可以成功作为文本被找到了,以此类推,输入输出、题目名字也可以。此时已经完成了百分之九十。
剩下的工作就是找规律,使得找到一个可以类推到一百个。
此时之前发现的可疑链接就有了用处,思路就是用beautifulsoup找到id为cpIPPSFReader的所有内容,然后按照发现的规律给每个字符串补上url里缺失的前缀:
all_link = soup.find_all("a", id="cpIPPSFReader")
urls = re.findall(r"url:'(.*?)'", onclick_value)
if urls:
full_url = 'http://10.12.13.248/cpbox/' + urls[0]
以此类推,我们就获得了100个题目页面的子域名。这个时候继续用beautifulsoup按题名、题目、输入、输出的不同id来查找文本即可!
到这里主体思路分析就结束了,下面只需要写代码实现主体功能,增添一些辅助功能即可,后面的部分都相对简单,就不再赘述。
使用了许多第三方库来实现程序的一些功能,在此一一列举:
tkinter:用于创建GUI界面,使界面更加美观,同时来给予用户提示。
requests:用于发送HTTP请求,实现网站访问与登录。
BeautifulSoup:用于解析HTML内容,查找页面内题目文本的具体位置。
docx:用于生成word文档,并且把查询到的文本插入Word文档、做好文档排版。
re:用于正则表达式匹配,方便爬虫从网页里获取的url。具体来说,就是从每个link元素中获取onclick属性的值。
io:用于处理二进制流,具体的来说是在题目里有图片的时候下载图片。但是现在存在的bug是,虽然大部分题都是只有一张图,但是有些题有两张图,而且图片大小应该怎么去调整到合适的程度?目前我还没有想到相对好的处理方法。
如何实现区分中英文:我观察发现,大部分题目都是中文在前,英文在后,而且标点符号相对规范,因此只需要从右往左查找到最后一个中文标点符号就可以成功分割。然而,在观察输出结果时,我发现仍然有一两道是不符合规范的(比如青蛙过河),但是总体来说影响不大。
调用 root.mainloop() 使得窗口保持持续的显示状态直到人为关闭。
代码部分:
import tkinter as tk
from tkinter import messagebox
import requests
from bs4 import BeautifulSoup
from docx import Document
import re
from io import BytesIO
headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"
}
root = tk.Tk()
root.title("CrawlerForNoj")
space = tk.Label(root)
space.grid(row=0, column=0, sticky="e")
tip_label = tk.Label(root, text="""众所周知,noj最讨人厌的就是不能复制输入输出,尤其会在写字符串的时候导致频繁破防\n因此我写了这个程序,输入你的noj账号密码,你就可以获取所有noj题目,并且自定义是否需要输入输出\n它不会收集你的个人数据,或者你可以用我的noj账号进行测试:
账号不告诉你
密码也不告诉你 """, padx=10, wraplength=300, justify=tk.LEFT)
tip_label.grid(row=1, columnspan=2)
username_label = tk.Label(root, text="账户名:")
username_label.grid(row=2, column=0, sticky="e")
username_entry = tk.Entry(root)
username_entry.grid(row=2, column=1)
password_label = tk.Label(root, text="密码:")
password_label.grid(row=3, column=0, sticky="e")
password_entry = tk.Entry(root, show="*")
password_entry.grid(row=3, column=1)
space = tk.Label(root)
space.grid(row=4, column=0, sticky="e")
def SepChAndEn(text):
result = text.rsplit('。', 1)[0]
flag = True
if result == text:
result = text.rsplit('?', 1)[0]
flag = False
if flag:
result += '。'
else:
result += '?'
return result
def login():
username = username_entry.get()
password = password_entry.get()
session = requests.Session()
login_data = {
'username': username,
'password': password
}
login_response = session.post('http://10.12.13.248/cpbox/', data=login_data, headers=headers)
after_login_response = session.get('http://10.12.13.248/cpbox/webfile.aspx', headers=headers)
if login_response.ok and after_login_response.ok:
messagebox.showinfo("CrawlerForNoj", "登录成功!")
# 开始爬了
scrape(session)
else:
messagebox.showerror("CrawlerForNoj", f"登录失败,状态码: {login_response.status_code}")
def scrape(session):
if messagebox.askyesno("CrawlerForNoj", "是否需要样例输入输出?"):
sample_needed = True
else:
sample_needed = False
if messagebox.askyesno("CrawlerForNoj", "是否需要英文描述?"):
english_needed = True
else:
english_needed = False
response = session.get('http://10.12.13.248/cpbox/cpNPUOJ.aspx#')
if response.ok:
doc = Document()
print("成功跳转到 http://10.12.13.248/cpbox/cpNPUOJ.aspx# 页面")
html = response.text
soup = BeautifulSoup(html, 'html.parser')
name = soup.find("span", id="lblSTRealName")
if name:
messagebox.showinfo("Welcome", f"{name.text.strip()} 同学,欢迎使用!")
soup = BeautifulSoup(html, 'html.parser')
all_link = soup.find_all("a", id="cpIPPSFReader")
count = 1
for link in all_link:
onclick_value = link.get('onclick')
urls = re.findall(r"url:'(.*?)'", onclick_value)
if urls:
full_url = 'http://10.12.13.248/cpbox/' + urls[0]
final_response = session.get(full_url, headers=headers)
if final_response.ok:
soup = BeautifulSoup(final_response.text, 'html.parser')
title = soup.find("span", id="lblIPPDFtitle")
if title:
print("题目", count, ":", title.text.strip())
doc.add_heading("题目" + str(count) + ": " + title.text.strip(), 2)
count += 1
description = soup.find("span", id="lblIPPDFdescription")
if description:
if english_needed == False:
# 如果不需要英语描述,就需要进行字符串切分
first = description.text.strip()
ch = SepChAndEn(first)
print("描述:", ch)
doc.add_paragraph("描述: " + ch)
else:
print("描述:", description.text.strip())
# 获取图片(如果有的话)
image = soup.find("img")
if image:
src = image.get("src")
if src:
full_match = 'http://10.12.13.248/cpbox/' + src
print(full_match)
try:
response = requests.get(full_match)
image_bytes = BytesIO(response.content)
doc.add_picture(image_bytes)
except Exception as e:
print("插入错误:", e)
input_section = soup.find("span", id="lblIPPDFiutput")
if input_section:
if english_needed == False:
first = input_section.text.strip()
ch = SepChAndEn(first)
print("输入:", ch)
doc.add_paragraph("输入: " + ch)
else:
print("输入:", input_section.text.strip())
output_section = soup.find("span", id="lblIPPDFoutput")
if output_section:
if english_needed == False:
first = output_section.text.strip()
ch = SepChAndEn(first)
print("输出:", ch)
doc.add_paragraph("输出: " + ch)
else:
print("输出:", output_section.text.strip())
if sample_needed:
sample_input_section = soup.find("span", id="lblIPPDFsampleinput")
if sample_input_section:
print("样例输入:", sample_input_section.text.strip())
doc.add_paragraph("样例输入: " + sample_input_section.text.strip())
sample_output_section = soup.find("span", id="lblIPPDFsampleoutput")
if sample_output_section:
print("样例输出:", sample_output_section.text.strip())
doc.add_paragraph("样例输出: " + sample_output_section.text.strip())
print("\n")
doc.add_paragraph("\n")
doc.add_paragraph("\ncode by 外国语学院 2023303822 丁中惠")
doc.save("当前noj题目.docx")
messagebox.showinfo("CrawlerForNoj", "生成成功,关闭全部对话框即可看到docx文件")
else:
print("跳转失败")
login_button = tk.Button(root, text="登录", command=login)
login_button.grid(row=5, columnspan=2)
space = tk.Label(root)
space.grid(row=6, column=0, sticky="e")
root.mainloop()
写在最后的碎碎念:
虽然现在看来它的功能真的很简单,但是对当时是个小白的我来说确实是一个不小的挑战。翻了很多很多教程:安装各种第三方库(到现在甚至还没有解决无法升级pip库的问题),学习怎么使用它们,学习看HTML、分析它的结构……从3月15号早上写到凌晨三点终于实现了基本的功能。后续几天又陆陆续续增添了一些功能,加了一个框框水了水行数。这是我第一次写一个程序来帮助自己解决现实中的问题,中间有很崩溃的时候,但是最后真的实现的时候感觉乳腺都通了()。
收获大概是,发现了一个把老师都忽悠了的秘密:
Noj全部点开之后两个礼拜已经不会过期了,可以正常提交。(问就是发现功能实现了之后副作用是把题目全部点开)
真诚地感谢CSDN上大佬的帖子TT,感谢我的小猫的陪伴,感谢男朋友的夸夸。
作者:端木小饼干