转载请注明:陈熹 [email protected] (号:半为花间酒)
若公众号内转载请联系公众号:早起Python
写在开头:本文运行爬虫的示例网站为 生信坑 https://www.bioinfo.info/
是一个开放式的生信学习交流论坛,欢迎大家加入
这里特别感谢孟叔、刘博和一众学习伙伴对我R语言和生信学习的帮助 : )
本文以爬取 生信坑 所有发表帖子的各类相关信息作为小案例
要爬取任何网页,首先需要查看robots协议,这是爬虫的入门礼仪
可通过 域名+/robots.txt 查看
wecenter的robots规则中User-agent是*,表示对象是所有爬虫。
可以看到下面写了一堆disallow : )
需要仔细查看禁止范围,本例中不去涉及disallow的目录,并且已经征得网站负责人孟叔同意
应明白,robots协议是一个礼貌性协议,不会对爬虫强制限制,但依然需要遵守
接下来是正文
可能用到的库:requests, lxml, datetime, time, pymysql, openpyxl
首先进入坑里发帖的页面:https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-1
判断是静态页面还是动态页面
可以判断当前页面为非ajax动态加载页面,可直接获取源码爬取
import requests
url = 'https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-1'
r = requests.get(url).text
print(r)
可以返回源码,所以不需要单独的请求头设置(其实有个好习惯加上headers也更好)
可能我是第一个爬取该网站的人,孟叔没有给网站设置过多的反爬举措,不然这会是一个更有意思的案例
但出现了乱码,不过没事,你看见charset了没?
charset提示网页用utf-8编码
如果不想在网页源码中寻找编码方式,也可以用代码获取
from bs4 import BeautifulSoup
import urllib.request
content = urllib.request.urlopen(url)
soup = BeautifulSoup(content)
print(soup.original_encoding)
# > utf-8
故重新调整代码
url = 'https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-1'
r = requests.get(url)
r.encoding = 'utf-8'
print(r.text)
解决了编码问题,接下来就是常规的源码解析了。
解析工具有很多,比较大众的是Xpath BeautifulSoup pyquery等等,当然正则大法好。
正则匹配是文本匹配效率极高的工具,上手难度较大,不过配合pyquery修改html树可谓利器。
本文以Xpath做演示,其他工具基本思想类似
import requests
from lxml import html
def parse_content(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)\
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'}
response = requests.get(url, headers=headers)
response.encoding = 'utf-8'
selector = html.fromstring(response.text)
questions = selector.xpath("//div[@class='aw-question-content']")
for i in questions:
# 标题
title = i.xpath("h4/a/text()")[0]
# 链接
link = i.xpath("h4/a/@href")[0]
# 话题分类
issue = i.xpath("p/a[1]/text()")[0]
# 在代码实际运行中发现,有些时候名字可能显示不出来,这里简单用try去捕获
# 参与者
try:
participant = i.xpath("p/a[2]/text()")[0]
except:
participant = 'anonymous'
# 其他信息
others = i.xpath("p/span/text()")[0]
# 拆解具体信息
others_parse = others.split(' • ')
# 参与类型(发起/回复)
type = others_parse[0][:2].strip()
# 关注数
# 如果有些话题没有人关注,则该信息不会显示,因此需要判断
if '关注' in others_parse[1]:
follow = others_parse[1][:-3].strip()
else:
follow = '0'
# 回复数和浏览量不会因为0而消失,但由于关注数的不确定性导致反向切片比较稳妥
# 回复数
reply = others_parse[-3][:-3].strip()
# 浏览量
browse = others_parse[-2][:-3].strip()
# 时间
time = others_parse[-1].strip()
print(title)
print(link)
print(issue, participant, type, follow, reply, browse, time)
print('-' * 10)
if __name__ == '__main__':
url = 'https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-1'
parse_content(url)
返回的结果如下:
基本已经获取到了我们需要的信息。
简单分析一下,其实互动类型和回复数两个信息是相互关联的,如果回复数为0,那么最后一个参与者的互动类型一定是发起问题,回复数不为0则互动类型一定是回复。这种关联性分析对于数据预处理和分析尤为重要,也是降维的依据
因此给我们一个警示:数据的获取需要有针对性
另外我们发现,时间上不统一(逼死强迫症),一周之内的时间会显示为 x天前 的形式。
对于详细显示的时间,其实我们更关注的是date,即年月日,而不是time
仔细分析可以发现这个显示的时间是最后一次互动的时间:发起问题或回复问题。
我们点进其中的一个问题看看:
这意味着即使你提高爬取深度只为了获取HHHH-MM-DD形式的时间也是徒劳。
(一般也不会这么做,提高IO阻塞的风险就为了拿个时间?)
不过根据已有的信息已经可以处理时间信息了
补充一个时间加减的知识
import datetime
today = datetime.datetime.today()
delta = datetime.timedelta(days=3)
print(today + delta)
print((today + delta).strftime('%Y-%m-%d'))
# > 2020-04-03 14:04:23.356261
# > 2020-04-03
利用datetime模块获取今天的时间,并且用timedelta完成相加减
strftime方法帮助对时间格式进行自定义格式调整,如果不知道这个方法可以直接用文本处理切割获取date,不过应注意时间格式需要先转化为str格式,这里如果对面向对象编程熟悉的小伙伴应该很清楚,str()先实例化对象才能调用文本方法
print(str(today + delta).split()[0])
# > 2020-04-03
对之前的时间代码进行条件判断
# 时间
time = others_parse[-1]
if '前' in time:
# 一定是7天之内,故肯定只有1位数字,直接用文本切片获取再转成int
# 获取的
days = int(time.strip()[0])
today = datetime.datetime.today()
delta = datetime.timedelta(days)
date = str((today - delta).strftime('%Y-%m-%d'))
else:
date = time.split()[0]
结果一片祥和
接下来加上多页爬取的代码
第一页:
https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0
第四页:
https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-4
最后一页:
https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-79
可以看到,翻页的url逻辑很简单,就是对应修改最后的数字即可,第一页默认也可以加上 __page-1(但不是所有的网站都允许默认,不允许的情况则需要额外添加)
故翻页可以用for循环迭代,设置最后一页是79,但发帖的数量是动态的,不能每次都去人为观察最后一页是第几页然后添加进range。
我们观察一下超出最后一页会如何:
页面没有404!
但是没有任何帖子,审查网页元素也可以看到之前解析的html元素都不存在,因此可以考虑建立while True死循环,只需要用try捕获异常,如果报解析异常直接终止循环跳出去就可以了。
import time
def parse_page():
url_init = 'https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-{}'
num = 1
while True:
try:
parse_content(url_init.format(num))
num += 1
# 为了防止给孟叔的网站太大压力 每次爬取睡3s
time.sleep(3)
except Exception as error:
print(error)
break
要注意 xpath是不会主动触发异常,所以需要在解析中自行判断和触发
def parse_content(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)\
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'}
response = requests.get(url, headers=headers)
response.encoding = 'utf-8'
selector = html.fromstring(response.text)
questions = selector.xpath("//div[@class='aw-question-content']")
if not questions:
# 你想触发什么异常都可以
raise AttributeError
至此,简单的爬取帖子信息的代码就完成了,最后就是存储了
持久化存储可以用存成csv或者excel,也可以存到mysql
我个人比较喜欢用mysql,毕竟学了很长时间不用也是浪费。也可以用mongodb储存,但我目前非关系型用redis比较多,mongodb不是特别熟悉,不做介绍。
以下对两种方式都做展示
利用openpyxl存储excel
导入需要的库
from openpyxl import Workbook
实例化新表以及表头设置
wb = Workbook()
sheet = wb.active
sheet.title = 'bioinfo帖子信息'
header = ['标题', '链接', '话题', '最后互动者', '互动形式', '关注数', '回复数', '浏览数', '最后互动日期']
sheet.append(header)
基本的准备工作做完后面就很简单,只需要在网页解析的最后加上两行代码,最后记得保存就可以
# 要和表头对应,存储的时候别搞乌龙
row = [title, link, issue, participant, type, follow, reply, browse, date]
sheet.append(row)
主线程最后加上:
wb.save('bioinfo帖子信息.xlsx') # 也可以指定绝对路径
excel存储就完成了
利用pymysql存储至mysql数据库
这里可以把pymysql封装成一个类至本地方便使用
import pymysql
class Mysqlhelper(object):
def __init__(self):
self.connect = pymysql.connect(host='localhost',
port=3306,
user='xxxxxx',
password='xxxxxx',
db='xxxxxx', # 账号密码用自己的,数据库可以指定
charset='utf8')
self.cursor = self.connect.cursor()
def execute_sql(self, sql, data):
self.cursor.execute(sql, data)
self.connect.commit()
# 析构函数在python的垃圾回收器触发销毁的时候调用,一般很少用,但此处可以用来关闭游标和连接
def __del__(self):
self.cursor.close()
self.connect.close()
表格需要自己在mysql里建好,参考语法如下
CREATE TABLE bioinfo(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
title TEXT, link TEXT, issue VARCHAR(30),
participant VARCHAR(30), type VARCHAR(10),
follow VARCHAR(10), reply VARCHAR(10), browse VARCHAR(10), date VARCHAR(20)
)ENGINE=INNODB DEFAULT CHARSET=UTF8MB4;
建表配置完后,就可以在python后续操作
# 实例化对象
mysqlhelper = Mysqlhelper()
解析网页后构建sql语句
insert_sql = 'insert into bioinfo(title, link, issue, participant, type, follow, reply, browse, date)' \
'values(%s, %s, %s, %s, %s, %s, %s, %s, %s)'
data = (title, link, issue, participant, type, follow, reply, browse, date)
mysqlhelper.execute_sql(insert_sql, data)
最后爬取的结果 全部782条帖子:
以上就是简单爬取生信坑bioinfo帖子信息的小案例
附上完整代码:
import requests
from lxml import html
import datetime
import time
import pymysql
# 个人建议mysql的类放在其他文件中
# 以from mysqlhelper import Mysqlhelper形式导入 功能上会更加清晰
class Mysqlhelper(object):
def __init__(self):
self.connect = pymysql.connect(host='localhost',
port=3306,
user='xxxxxx',
password='xxxxxx',
db='xxxxxx', # 账号密码用自己的,数据库可以指定
charset='utf8')
self.cursor = self.connect.cursor()
def execute_sql(self, sql, data):
self.cursor.execute(sql, data)
self.connect.commit()
# 析构函数在python的垃圾回收器触发销毁的时候调用,一般很少用,但此处可以用来关闭游标和连接
def __del__(self):
self.cursor.close()
self.connect.close()
def parse_page():
url_init = 'https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-{}'
num = 1
while True:
try:
parse_content(url_init.format(num))
num += 1
# 为了防止给孟叔的网站太大压力每次爬取睡3s
time.sleep(3)
except Exception as error:
print(error)
break
def parse_content(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)\
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'}
response = requests.get(url, headers=headers)
response.encoding = 'utf-8'
selector = html.fromstring(response.text)
questions = selector.xpath("//div[@class='aw-question-content']")
# 要注意 xpath是不会主动触发异常,所以需要自行判断
if not questions:
# 你想什么异常都可以
raise AttributeError
for i in questions:
# 标题
title = i.xpath("h4/a/text()")[0]
# 链接
link = i.xpath("h4/a/@href")[0]
# 话题分类
issue = i.xpath("p/a[1]/text()")[0]
# 参与者
try:
participant = i.xpath("p/a[2]/text()")[0]
except:
participant = 'anonymous'
# 其他信息
others = i.xpath("p/span/text()")[0]
# 拆解具体信息
others_parse = others.split(' • ')
# 参与类型(发起/回复)
type = others_parse[0][:2].strip()
# 关注数
# 如果有些话题没有人关注,则该信息不会显示,因此需要判断
if '关注' in others_parse[1]:
follow = others_parse[1][:-3].strip()
else:
follow = '0'
# 回复数和浏览量不会因为0而消失,但由于关注数的不确定性导致反向切片比较稳妥
# 回复数
reply = others_parse[-3][:-3].strip()
# 浏览量
browse = others_parse[-2][:-3].strip()
# 时间
time = others_parse[-1].strip()
if '前' in time:
# 一定是7天之内,故肯定只有1位数字,直接用文本切片获取再转成int
days = int(time[0])
today = datetime.datetime.today()
delta = datetime.timedelta(days)
date = str((today - delta).strftime('%Y-%m-%d'))
else:
date = time.split()[0]
print(title)
print(link)
print(issue, participant, type, follow, reply, browse, date)
print('-' * 10)
insert_sql = 'insert into bioinfo(title, link, issue, participant, type, follow, reply, browse, date)' \
'values(%s, %s, %s, %s, %s, %s, %s, %s, %s)'
data = (title, link, issue, participant, type, follow, reply, browse, date)
mysqlhelper.execute_sql(insert_sql, data)
if __name__ == '__main__':
mysqlhelper = Mysqlhelper()
parse_page()
可以看到代码量不大,蛮大蛮算接近100行
总结:
这个最终的代码中间发现了两个问题,不断去修复和调整代码,
第一个 就是发现最后一个互动的人可能没有名字:
这个是真的迷惑,点进去也可以发现名字,可能是前端bug
第二个 是问题如果无人关注则不会显示该信息:
这些问题在一开始分析网页和写代码的时候很难意识到,因此需要不断调整让代码更健壮。
这也是爬虫的魅力之一
从这个项目中也延伸出很多新的有趣的思路:
- 利用词块切割和语义分析评估目前近800个帖子的关注重心在哪
- 深入一层获取每个帖子的评论和各类信息,并进行可视化
(想夸一下R,可视化能力总体比matplotlib要强,但如果是动态交互性可视化可以用pyecharts或者Q版手绘风的cutecharts) - 论坛中只能显示100位用户的相关信息,利用全论坛爬取可以挖掘出所有在论坛留下过足迹的用户的全部信息,从而进行人物画像和行为分析(笑)
- 加速爬取,本例也可以使用成熟的爬取框架scrapy。如果不使用框架的情况下,可以利用多进程 多线程 协程的方式爬取,有时候为了自我限制和必要的自定义需要利用队列和回调
……
思路是无止境的,这些新的案例今后我会继续分享
Life is short, you need Python