起因
2019年,大三的我在上《面向对象编程》这门课时,曾用Java爬取了厦门大学教育发展基金公开的捐赠名单。2021年厦大百年校庆,作为在厦大读了6年,马上第7年的我非常高兴能够见证这一历史时刻!本着好奇害死猫的原则,我想用Python再爬一次,看看百年校庆下捐赠名单有何不同,顺便温习Python,MySQL、Tableau的使用。
在此说明:由于网站服务器访问数量的限制,所爬取的近6万条记录中有200多条是作废的,因此接下来我所分析得到的数据仅仅是作为参考,并不准确,且本着为母校信息保护的原则,以下内容只会出现大概的数据,不会出现具体的人名、公司名等
Python爬虫
关于Python爬虫部分,由于爬虫是否合法至今没有定论,这部分只做简要说明,重在描述思路。
文中所提到的库与方法的具体使用可以看说明文档,这里不再赘述。
网页下载及解析
- 网页下载
网页下载这部分主要用到Python的urllib库,用于网页下载的部分代码如下:
import urllib.request as request
def download_page(self, page_url):
# 隐藏部分网页解析有关代码
response = request.urlopen(page_url)
content = response.read().decode('utf-8')
with open(file_name, "w", encoding="utf-8") as f:
f.write(content)
f.close()
即导入urllib中request包,使用其中的urlopen()方法与read()方法即可得到相应的网页全文,之后用Python的文件操作写入本地即可。
- 多线程
由于需要下载的网页数量众多(大概5000个页面),单线程的下载方式显然是不可取的(太慢太慢),因此在网页下载时需要使用到Python的多线程技术(threading库)。Python多线程的使用方法与Java非常相似,我采用的是:设计一个类继承threading类,并重写run()方法
在这个部分其实就是体现面向对象的好处,我将爬虫设为一个类(Ant),并重写了run()方法,代码如下:
lock = threading.Lock() # 创建一个线程锁的对象
class Ant(threading.Thread):
def __init__(self, page_dir_path, page_dir_name, urls):
threading.Thread.__init__(self)
self.page_dir_path = page_dir_path
self.page_dir_name = page_dir_name
self.urls = urls
# 隐藏部分网页解析有关代码
def run(self):
while len(self.urls) > 0:
lock.acquire()
url = self.urls.pop()
lock.release()
self.download_page(self, page_url)
def download_page(): # 上文提到的下载网页函数
多线程这部分要特别注意的是:
- 多线程涉及数据存取时,要给公共的数据“上锁”,否则不同的线程在存取同一个数据时会产生不可预期的错误。笔者这里的公共数据是urls,这是存储所有要访问的网页的列表,因此将其作为线程锁,使得在同一时间只有一个线程能够操作这个列表,防止重复下载
- 启动线程用的是start(),不是直接调用run()
- 网页解析
网页解析其实就是读取下载的html文件,并提取出所需要的信息。对于笔者感兴趣的捐赠记录而言,我需要提取的字段有:捐赠时间、捐赠人姓名、院系、校友会名称、捐赠项目、捐赠金额。既然涉及到提取,那必然需要存储,在此我选择用自定义的类存储这些信息,每个对象都存储一条记录,类的设计如下:
class DonateRecord():
def __init__(self, daytime, name, department, unit, project, money):
self.daytime = daytime
self.name = name
self.department = department
self.unit = unit
self.project = project
self.money = money
def to_string(self):
print("daytime=" + self.daytime +
" name=" + self.name +
" department=" + self.department +
" unit=" + self.unit +
" project=" + self.project +
" money=%f" % self.money)
对于如何解析的问题,考虑到这是网站开发人员的劳动成果,我作为使用者无权公开,因此这部分的解析代码展示,有兴趣的小伙伴可以自行尝试
MySQL入库
在数据库部分需要用到Python的MySQLdb库,主要的步骤分为创建数据库与数据入库,
- 创建数据库
在创建数据库时,首先要保证本地或服务端的数据库服务处于运行状态,之后的步骤为:
代码如下:
def create_db():
db = MySQLdb.connect(host='localhost', user="root", passwd="********")
cursor = db.cursor()
sql = "create database if not exists xmudonate" # 创建新数据库
cursor.execute(sql)
db.select_db("xmudonate")
sql = "drop table if exists donate" # 创建新表
cursor.execute(sql)
sql = """create table donate (
daytime date,
name varchar(50),
department varchar(50),
unit varchar(50),
project varchar(50),
money float)"""
# print(sql)
cursor.execute(sql)
cursor.close()
db.close()
- 数据入库
数据入库步骤与创建部分相同,只是把创建语句改为插入语句,需要注意的是执行SQL语句cursor.execute(sql)之后,还需要执行db.commit()提交方法,不然数据并不会插入到数据库中,代码如下:
def save_to_db(records):
db = MySQLdb.connect(host='localhost', user="root", passwd="lj123456")
cursor = db.cursor()
db.select_db("xmudonate")
cnt = 0
for record in records:
sql = """insert into donate(daytime, name, department, unit, project, money) values ('%s','%s','%s','%s','%s','%s')""" % (
record.daytime, record.name, record.department, record.unit, record.project, record.money)
# print(sql)
cursor.execute(sql)
cnt = cnt + 1
db.commit()
print("提交完毕")
cursor.close()
db.close()
Tableau分析
终于来到分析部分,这也是笔者最感兴趣的部分,我首先把Tableau连接到服务器,并数据提取到Tableau中,接下来对我提取的捐赠记录做分析。
再次强调:由于网站服务器访问数量的限制,所爬取的近6万条记录中有200多条是作废的,因此接下来我所分析得到的数据仅仅是作为参考,并不准确,且本着为母校信息保护的原则,以下内容只会出现大概的数据,不会出现具体的人名、公司名等
- 捐赠金额与年份的关系
2021年真不愧是百年校庆,捐赠的金额果然不一般。当年捐赠金额是其他年份的数倍不止,默默留下了贫穷的泪水。
- 捐赠金额与月份的关系
这部分的内容倒是可以预期,每年4月6日是我们的校庆日,捐赠的大多都集中在4月份,合情合理。不过看图12月的捐赠金额一点也不亚于4月,这点比较费解,笔者查看了源数据并没有发现什么异常,奇奇怪怪,可能跨年也是捐赠的高峰期吧(我猜的)。
- 捐赠次数与个人
这里只显示捐赠次数大于20次的信息。令人震惊,图中显示从2007年有记录开始,同一个人连续给厦大各个基金会累计捐赠60笔!总计金额高达一千多万!感谢支持! - 捐赠金额与个人
只显示累计金额大于一千万的信息。可以看到有两条信息傲立群雄,他们分别来自一个基金会和一家公司,累计金额都是6千万左右。查看源数据发现,这些大额捐赠基本上来自各个独立的基金会和公司,只有寥寥两三个是以个人名义捐赠的,可能这些公司与基金会的理事人也是厦大校友吧
- 来自各校友会的捐赠金额
只显示累计金额大于50万的校友会。可以看出饼图被分割成3各部分,最大的两个部分分别来自教职员工与澳门校友会。教职工们还是非常乐于建设自己的工作单位,而澳门同胞们更是心系祖国。
- 来自各学院的捐赠次数
只显示捐赠次数大于500次的数据。非常高兴我所在的电子科学与技术学院光荣上榜,其实作为一个才成立几年的新学院,能够拥有这么高的捐赠次数实属不易。也能看到各个学院的捐赠次数其实相差不大,后勤集团因为人数众多,所以次数上占有也合情合理。
- 各基金会得到的捐赠金额
最后是各个基金会所获得的金额,只显示大于5百万的数据。其中占比最大的,也就是正儿八经的《教育发展基金会》。在查看基金会名称时,我发现各个学院其实都有自己的基金会,可能每年校庆各教职工们所发起的捐赠都捐到了自己学院所属的基金?不止于此,我还发现了一些特别的基金名称,如《守望相助共克时艰–厦门大学支援武汉抗击新冠肺炎专项基金》、《周咏棠助贷学基金》、还有各类助学助困基金,学生奖学金等等。
总结
- 首先还是感叹厦门大学校友们的强悍实力,众多校友、公司单笔的捐赠金额都以千万为计量单位,太恐怖了。也是感谢这些校友与公司的捐赠,为厦大的发展的做出了突出贡献
- 其实第一点倒不是最令我震惊的,这些大笔的捐赠在每年的校庆都有爆料,早就不稀奇,倒是最后一张饼图中那些基金名让我印象深刻。作为学生,我们在学期间所获得的奖学金原来都来自各社会人士发起的捐赠,还有诸多以个人名义创建的助学助困基金。大一时,我是班长,我曾亲手为班级上的困难生提交过助学金申请报告,也曾因为助学金的等级评定问题犯过错,得罪过同学,但是这一点都不妨碍这些基金实打实地帮助了需要帮助的人。突然想起本科入学时辅导员说的一句话:“在厦大,永远不要担心因为没钱而读不起书”。我至今还记得本科时有一位受到资助的女生,和我一样来自农村,但是她家庭条件并不好,八千多的学费对她家而言是一笔很大的负担。助学金申请是我帮忙提交最后和辅导员商定后评定的等级是特等,每个月能收到一千多的生活费,具体金额记不清了,反正绝对够一个普通学生在学校的所有开销,而且学费全免。四年毕业后,我还曾联系她,她并没有因为大城市的繁华而留下,而是带着自己的知识与见识,回到老家作为村官建设家乡。虽然她给我的回答是“觉得家里比较好”,但是我认为这样一个大学村官回到乡下,为偏远的农村注入新鲜血液,在她的岗位上有很大的概率能够为农村做出巨大贡献,可能这就是设立助学基金的意义所在吧。