最近在尝试做杭电OJ上编程习题的爬取并将其进行存储,这次采用的工具是Requests库、BeautifulSoup库、re正则表达式以及MySQL数据库。通过利用Requests库获取网页内容,以BeautifulSoup库和re正则表达式从网页内容中提取自己需要的信息。第一次做,收获很多,写一篇文章予以记录自己的这一趟学习之旅。话不多说,代码安排!
一、以下是一段请求网页内容的代码。
import requests
import re
from bs4 import BeautifulSoup
# import traceback
import pymysql
def getHTMLText(url):
try:
r = requests.get(url) # 请求网页内容
r.raise_for_status() # 如果请求成功,print是None
# print(r.status_code) # 请求如果成功,应该输出200
r.encoding = r.apparent_encoding # 将根据headers编码更改为根据HTML内容编码
# print(r.text)
return r.text # 一定记得返回爬取到的网页内容
except:
print("When getHTMLText comes the error!")
二、以下是一段提取自己需要的网页内容的代码。自己需要的数据是OJ习题中的题目序号、题目、题目内容、题目输入、题目输出、题目样例输入和题目样例输出这6样数据,故只提取了这些数据。这段代码中,最重要的就是BeautifulSoup库和re正则表达式的使用。关于.text和.string的区别,也是向其他博主学习的,原文来自https://blog.csdn.net/weixin_43891121/article/details/87989080。
def parseHTMLText(infoDict, htmlText):
try:
soup = BeautifulSoup(htmlText, 'html.parser')
# soup.title的数据类型是'bs4.element.Tag',其输出结果是'<title>Problem - 1000</title>'
# soup.title.string的数据类型是'bs4.element.NavigableString',其输出结果是'Problem - 1000'
# 因为后续要根据爬取到的题号将习题存入数据库并将其作为主键,所以利用正则表达式匹配数字,需要注意的是,re.findall()的返回值其数据类型是list
list_pNumber = re.findall(r'[\d]{4}', str(soup.title.string))
pNumber = list_pNumber[0] # 因为是list,故要从list中将string型的题目序号取出
pTitle = str(soup.h1.string)
# 这里要注意re.findall()和soup.find_all()的不同
# find_all()返回值的数据类型是'bs4.element.ResultSet',是一个set
set_pOutput = soup.find_all('div', 'panel_content')
# 根据原网页要提取数据出现的先后顺序,从set_pOutput集合中提取对应的数据
# 这里采用text而非string是因为.string方法在tag包含多个子节点时,tag无法确定.string方法应该调用哪个子节点的内容,所以会输出None;但是.text方法可以获取当前tag下所有子节点的内容。
pContent = set_pOutput[0].text
pInput = set_pOutput[1].text
pOutput = set_pOutput[2].text
pSampleInput = set_pOutput[3].text
pSampleOutput = set_pOutput[4].text
infoDict = {"题目序号": pNumber, "题目名称": pTitle, "题目内容": pContent, "题目输入": pInput, "题目输出": pOutput,
"题目样本输入": pSampleInput,
"题目样本输出": pSampleOutput}
return infoDict
except:
# 因为杭电OJ中的习题会更新,比如1125-1127题更新后不存在,故对这部分习题做爬取异常处理
print("发生更新后,该道习题不存在。When parseHTMLText comes the error!")
三、以下是一段将爬取到的数据存储到MySQL数据库中的代码。
def db_dataMemory(pInfoDict, crawlFlag): # 当未爬取到任何内容,即原序号题目不存在时,其数据infoDict值为{},数据类型为 'dict'
if pInfoDict: # 如果字典不为空,即爬取到了网页内容,则进行数据提取
txtNumber = int(pInfoDict['题目序号'])
practiceNumber.append(txtNumber)
# 由于要存储入数据库的内容中可能存在单引号会导致数据库存储出错,所以利用replace函数对单引号进行转义,如此可将数据正确地存储到数据库
escapeTitile = pInfoDict['题目名称'].replace("'", "\\'").replace("\\\\'", "\\\\\\'") # 前一个replace是对"'"做处理,后一个replace是对“\'”做处理
escapeContent = pInfoDict['题目内容'].replace("'", "\\'").replace("\\\\'", "\\\\\\'")
escapeInput = pInfoDict['题目输入'].replace("'", "\\'").replace("\\\\'", "\\\\\\'")
escapeOutput = pInfoDict['题目输出'].replace("'", "\\'").replace("\\\\'", "\\\\\\'")
escapeSampleInut = pInfoDict['题目样本输入'].replace("'", "\\'").replace("\\\\'", "\\\\\\'")
escapeSampleOnut = pInfoDict['题目样本输出'].replace("'", "\\'").replace("\\\\'", "\\\\\\'")
# try:
# 再次要命,一直出问题的地方在于插入数据的SQL语句的格式!!!直接是“INSERT INTO 表名”,并不是“INSERT INTO TABLE initialData”
sql_insertData = """
INSERT INTO initialData(pNumber, ptitle, pContent, pInput, pOutput, pSampleInput, pSampleOutput)
VALUES ('%d', '%s', '%s', '%s', '%s', '%s', '%s')
""" % (
txtNumber, escapeTitile, escapeContent, escapeInput, escapeOutput, escapeSampleInut,
escapeSampleOnut)
cursor.execute(sql_insertData)
conn.commit()
print("当前已完成第%d题的存储" % txtNumber)
global realAmount # 因为在函数体中使用的是全局变量,所以最好声明一下
realAmount += 1
if txtNumber == practiceAmount:
crawlFlag = 1
return crawlFlag
# except:
# print("When db_dataMemory comes the error!")
在数据库存储这一块儿做得并不是十全十美。例如对于字符串"It turns out that they\'ve been following you, and they're amazed.",经过处理存入数据库后,其结果为:"It turns out that they've been following you, and they're amazed.",并未能将"\"保留下来,而是将其作为转义字符处理了。需要注意的是,对数据库进行操作以后,一定要向数据通过commit()语句提交自己的操作,否则数据库不会发生任何变化,最后也一定要关闭游标,断开数据库连接。
四、以下是一段主函数,主要形成爬取的网页连接,按顺序调用各部分函数。
def main():
crawlFlag = 0
start_url = "http://acm.hdu.edu.cn/showproblem.php?pid="
infoDict = dict() # 这里一定得初始化一个字典,否则无法在调用函数的时候传递参数
for offset in range(3030, practiceAmount+1):
url = start_url + str(offset)
# print(url)
htmlText = getHTMLText(url) # 获取网页内容
pInfoDict = parseHTMLText(infoDict,
htmlText) # 解析爬取到的网页内容,这里一定要设置一个变量接收存储爬取数据的字典,因为前面有初始化字典,否则输出时用的是空字典
# 定义persistentFlag是因为OJ中部分习题被删除,为结束for循环,只需要判断当前题目是否是杭电OJ中的最后一道习题。
# persistentFlag = txt_dataMemory(pInfoDict, crawlFlag)
persistentFlag = db_dataMemory(pInfoDict, crawlFlag)
if persistentFlag == 1:
break
print("--*---*---*---*---*--")
print("实际上一共有%d道习题" % realAmount)
五、连接数据库并执行主函数。
六、最后附上一段连接数据库并创建数据表initialData的代码。
# -*- coding: UTF-8 -*-
import pymysql
# 综下可知,创建游标和提交数据库的改变需要使用db,而执行SQL语句需要使用的是cursor
# 要命,一直出问题的地方在于没有规定VARCHAR的长度!!!
sql_createTable = """CREATE TABLE initialData(
pNumber INTEGER PRIMARY KEY,
pTitle VARCHAR(100),
pContent TEXT,
pInput TEXT,
pOutput TEXT,
pSampleInput TEXT,
pSampleOutput TEXT)
"""
db = pymysql.connect(host='localhost', port=3306, user='root', passwd='数据库密码', db='initialData',
charset='utf8mb4') # 密码错误时,1045, "Access denied for user 'root'@'localhost' (using password: YES)"
cursor = db.cursor()
cursor.execute("DROP TABLE IF EXISTS initialData")
cursor.execute(sql_createTable)
db.commit()
cursor.close()
db.close()
文章最后,谈谈自己的感受。大学四年没有认真学习计算机的相关课程,尤其对编程不上心,所以毕业设计也做得尤为艰难。在毕设的基础上学习新的爬虫方式,才没有那么的费劲。是所谓“庸人自扰之”,自己的高度不够,处理问题的能力和眼界也的确差了很多。谈及此,非气馁,而以此激励自己。无论他人,做好自己,相对于过去进步就好,心态最为重要,希望自己可以过得轻松一点。再针对本文章,我的能力实为有限,肯定有很多不足之处,如果您看到了,欢迎您提出来,也是给我一个学习的机会,向您表示诚挚的谢意。千万别喷,因为我吧,老心态不好的人了,需要鼓励,감사합니다~
还有,这是我第一次写文章,在实现自己代码的过程中,也查阅了很多CSDN博主的文章,在本文章中可能会有体现,绝无侵权之意,如果有不妥的地方,麻烦您联系我,我会及时更改,谢谢。