Python爬虫案例与实战:爬取源代码练习评测结果
本章案例将介绍用 Python编写程序实现简单网站的模拟登录,然后保持登录后的网页会话,并在会话中模拟网页表单提交,之后使用 Requests库的高级特性爬取提交之后的返回结果。在HTTP网页中,如登录、提交和上传等操作一般通过向网页发送请求实现。通过对网页抓包分析,判断请求操作的类型,进而用Python的Requests库构建一个网页请求,模拟实际的网页提交。
4.1网站分析
POJ是老牌的供ACM/ICPC(大学生程序设计竞赛)选手在线提交程序源代码进行评测的练习平台,提交代码后需要跳转至网站的评测状态页面,再寻找用户所提交代码的评测结果。由于这种操作相对麻烦,本案例将通过编写模拟登录网站,并且通过表单提交的方式,上传评测用代码,提交之后发送请求,获取评测结果输出。
为分析登录的请求方式,在登录界面,务必在输入账号密码前,打开浏览器的开发者模式,找到Net Work 选项。再登录,此时开发者模式下会显示登录后的各种请求,找到login项,如图4-1所示,容易分析得出登录时的请求方式是POST请求。
页面往下拉,登录时 POST请求的内容如图4-2所示。可以看到 POST 发送的内容,有4个参数,前两个是用户的I和密码,后两个参数固定。需注意在编写爬虫程序时,POST内容的参数值是多少。
因为只看POST请求返回结果并不能判断是否登录成功,需要分析网页验证是否登录成功。其中一种常见的验证方法是访问代码提交界面。如果未登录成功,则会弹出登录框提示登录,如图4-3所示。如果登录成功,则通过网页元素分析,查看用于POST请求的form(表格)处的操作是login(登录)还是submit(提交),如图4-4所示。对比分析得出登录状态不同时HTML 文本的不同。
与登录成功时的操作相似,想要分析代码提交页面,必须提前打开开发者界面,单击“提交”按钮后抓包,如图4-5所示。在代码提交POST 请求的内容中,参数分别代表题目ID、提交所用语言(在可选语言列表中选择相应的下标,如4代表 C++语言、5代表C语言)、源代码和固定参数。注意,源代码提交方式采用 base64编码,属于HTTP网页中常见的表单提交方式和内容加密方式。
最后,是获取评测结果,在实际中,POJ的评测量很大,公屏评测结果刷新很快,只能通过手动输入用户ID的方式查看特定用户的评测结果,评测结果显示界面的URL 的格式为“poj.org/status?problem_id=&.user_id=&.result=&.language=”,看出是采用 GET 方式请求结果,如图4-6所示。通过分析网页元素,可以发现一条评测结果位于HTML 文本节点的子节点,此时想要获取最新结果,只需要访问第一个子节点,再遍历其子节点获取具体评测结果。
4.2编写爬虫
按以上分析网页和操作的思路,可以编写爬虫,具体操作如下。
(1)main()函数部分用于创建访问会话,并将验证登录,提交代码。注意,将获取评测结果的函数的接口放在 main()函数中。
if __name__ == '__main__':
user_name = 'ajshederay00616'
logindata = {'user_id1': 'ajshederay00616',
'password1': '114514yjsnpi', 'B1': 'login', 'url': '%2F'}
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36'}
session = requests.session()
login_req = session.post('http://poj.org/login',
data=logindata, headers=header)
if login_req.status_code != 200:
print('Failed to post the login request!')
exit(1)
if loginCheck(session):
print('Welcome! %s\n--------------------' % user_name)
else:
print('Failed to log in!Please Retry!')
exit(1)
problem_id, language = input(
'Enter the Problem_id and Language:\n').split()
print('--------------------')
submitdata = createSubmit(problem_id, language)
submit_req = session.post('http://poj.org/submit', data=submitdata)
if submit_req.status_code != 200:
print('Failed to submit the source code!')
exit(1)
queryURL = 'http://poj.org/status?problem_id=%s&user_id=%s&result=&language=' % (
problem_id, user_name)
sleep(2)
query_req = requests.get(queryURL)
info = getResult(query_req)
printResult(info)
session.close()
requests.session()方法是爬虫中经常用到的方法,在会话过程中自动保持 cookies,不需要自己维护cookies内容。如果不用此方法,则需要在第一次网站请求时,一个URL 会获取cookie,在第二次网站请求时,校验第一次请求获取的 cookie。requests.session()方法可以做到在同一个session实例发出的所有请求中都保持同一个cookie,相当于浏览器在不同标签页之间打开同一个网站的内容一样,使用同一个cookie。这样就可以很方便地处理登录时的cookie问题。
[提示]会话对象(session)是Requests的高级特性,除了上述这个用法,Requests库的高级用法还包括请求与响应对象、准备的请求(Prepared Request)、SSL证书验证、客户端证书、CA证书、响应体内容工作流、保持活动状态(持久连接)、流式上传、块编码请求、POST多个分块编码的文件、事件挂钩、自定义身份验证、流式请求、代理、SOCKS、合规性、编码方式、HTTP动词、定制动词、响应头链接字段、传输适配器、阻塞和非阻塞、Header排序等。
(2)登录检查函数。
def loginCheck(session):
html = session.get('http://poj.org/submit?problem_id=1000')
sleep(1)
if html.status_code != 200:
return False
soup = BeautifulSoup(html.content, 'html.parser')
for td in soup.form.find_all('a'):
if td['href'] == 'submit':
return True
(3)创建提交代码POST请求的参数,以及对代码进行 base64编码。
def createSubmit(problem_id, language):
submitdata = {'problem_id': '1000', 'language': 0,
'source': '', 'submit': 'Submit', 'encoded': 1}
language_map = {'G++': 0, 'GCC': 1, 'Java': 2,
'Pascal': 3, 'C++': 4, 'C': 5, 'Fortran': 6}
submitdata['problem_id'], submitdata['language'] = problem_id, language_map[language]
file_path = tkinter.filedialog.askopenfilename()
print('File %s selected, ready to submit!' % file_path)
print('--------------------')
code_file = open(file_path, 'r', encoding='utf-8')
submitdata['source'] = base64encode(code_file.read())
return submitdata
base64编码是网络上最常见的用于传输8bit字节码的编码方式之一,base64编码就是一种基于64个可打印字符来表示二进制数据的方法。base64编码是从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息。采用 base64编码具有不可读性,需要解码后才能阅读。关于 base64的编码规则:base64要求把每3个8bit的字节转换为4个6bit的字节(3X8=4X6=24),然后把 6bit再添两位高位0,组成4个8bit的字节。也就是说,转换后的字符串理论上将要比原来的长1/3。
例如,要对字符串abc进行 base64编码,首先将abc对应的ASCII编码97、.98、99,写成3个二进制位的形式,即01100001、01100010、01100011,共24位,按6位为一组可分为4组,在每组的高位补上00,经过转换,转换之后为00011000、00010110、00001001、00100011,对应base64索引值为24、22、9、35,参照 base64编码表,编码后得到的字符串为YWJj,完成编码。Python自带有base64库,可以使用库函数对字符串进行base64编码和解码。
(4)在提交完成之后获取评测结果,最后输出。
def getResult(req):
soup = BeautifulSoup(req.content, 'html.parser')
for tb in soup.find_all('table'):
if tb.get('class') != None:
if tb.get('class')[0] == 'a':
score_table = tb
break
for row in score_table.find_all('tr'):
if row.get('align') != None:
result = row
break
info = result.find_all('td')
return info
def printResult(info):
printLs = ['Run ID', 'User Name', 'Problem ID', 'Result',
'Memory', 'RunTime', 'Language', 'Length', 'Submit Time']
for i in range(len(info)):
if info[i].string != None:
print("%s:%s" % (printLs[i], info[i].string))
print('--------------------')
4.3运行并查看结果
爬虫程序的运行结果如图4-7所示,输入题目ID和语言在文件选择框中选择源代码文件后,会自动提交及返回评测结果,并在本地输出。
4.4本章小结
本章案例通过模拟网站登录并提交数据获取返回结果,介绍了Requests库,使用库函数发送GET 和 POST请求,并以请求实现模拟登录、提交等操作,然后通过 Requests的高级特性使用 session()方法维持会话,在爬虫实例中保持登录时的cookies以维持登录状态,以及使用 BeautifulSoup库结合网页抓包分析,解析并获取HTML 节点中需要的内容。上述的库、网页抓包分析元素、了解 base64编码在HTTP数据传输中的运用等都是爬虫的基础。