轉(zhuǎn)載請(qǐng)注明:陳熹 chenx6542@foxmail.com (簡(jiǎn)書(shū)號(hào):半為花間酒)
若公眾號(hào)內(nèi)轉(zhuǎn)載請(qǐng)聯(lián)系公眾號(hào):早起Python
寫(xiě)在開(kāi)頭:本文運(yùn)行爬蟲(chóng)的示例網(wǎng)站為 生信坑 https://www.bioinfo.info/
是一個(gè)開(kāi)放式的生信學(xué)習(xí)交流論壇趁窃,歡迎大家加入
這里特別感謝孟叔馅巷、劉博和一眾學(xué)習(xí)伙伴對(duì)我R語(yǔ)言和生信學(xué)習(xí)的幫助 : )
本文以爬取 生信坑 所有發(fā)表帖子的各類(lèi)相關(guān)信息作為小案例
要爬取任何網(wǎng)頁(yè)尘执,首先需要查看robots協(xié)議,這是爬蟲(chóng)的入門(mén)禮儀
可通過(guò) 域名+/robots.txt 查看
wecenter的robots規(guī)則中User-agent是*溪椎,表示對(duì)象是所有爬蟲(chóng)普舆。
可以看到下面寫(xiě)了一堆disallow : )
需要仔細(xì)查看禁止范圍,本例中不去涉及disallow的目錄校读,并且已經(jīng)征得網(wǎng)站負(fù)責(zé)人孟叔同意
應(yīng)明白沼侣,robots協(xié)議是一個(gè)禮貌性協(xié)議,不會(huì)對(duì)爬蟲(chóng)強(qiáng)制限制歉秫,但依然需要遵守
接下來(lái)是正文
可能用到的庫(kù):requests, lxml, datetime, time, pymysql, openpyxl
首先進(jìn)入坑里發(fā)帖的頁(yè)面:https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-1
判斷是靜態(tài)頁(yè)面還是動(dòng)態(tài)頁(yè)面
可以判斷當(dāng)前頁(yè)面為非ajax動(dòng)態(tài)加載頁(yè)面蛾洛,可直接獲取源碼爬取
import requests
url = 'https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-1'
r = requests.get(url).text
print(r)
可以返回源碼,所以不需要單獨(dú)的請(qǐng)求頭設(shè)置(其實(shí)有個(gè)好習(xí)慣加上headers也更好)
可能我是第一個(gè)爬取該網(wǎng)站的人雁芙,孟叔沒(méi)有給網(wǎng)站設(shè)置過(guò)多的反爬舉措轧膘,不然這會(huì)是一個(gè)更有意思的案例
但出現(xiàn)了亂碼,不過(guò)沒(méi)事兔甘,你看見(jiàn)charset了沒(méi)谎碍?
charset提示網(wǎng)頁(yè)用utf-8編碼
如果不想在網(wǎng)頁(yè)源碼中尋找編碼方式,也可以用代碼獲取
from bs4 import BeautifulSoup
import urllib.request
content = urllib.request.urlopen(url)
soup = BeautifulSoup(content)
print(soup.original_encoding)
# > utf-8
故重新調(diào)整代碼
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)
解決了編碼問(wèn)題洞焙,接下來(lái)就是常規(guī)的源碼解析了蟆淀。
解析工具有很多,比較大眾的是Xpath BeautifulSoup pyquery等等澡匪,當(dāng)然正則大法好扳碍。
正則匹配是文本匹配效率極高的工具,上手難度較大仙蛉,不過(guò)配合pyquery修改html樹(shù)可謂利器。
本文以Xpath做演示碱蒙,其他工具基本思想類(lèi)似
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("http://div[@class='aw-question-content']")
for i in questions:
# 標(biāo)題
title = i.xpath("h4/a/text()")[0]
# 鏈接
link = i.xpath("h4/a/@href")[0]
# 話題分類(lèi)
issue = i.xpath("p/a[1]/text()")[0]
# 在代碼實(shí)際運(yùn)行中發(fā)現(xiàn)荠瘪,有些時(shí)候名字可能顯示不出來(lái),這里簡(jiǎn)單用try去捕獲
# 參與者
try:
participant = i.xpath("p/a[2]/text()")[0]
except:
participant = 'anonymous'
# 其他信息
others = i.xpath("p/span/text()")[0]
# 拆解具體信息
others_parse = others.split(' ? ')
# 參與類(lèi)型(發(fā)起/回復(fù))
type = others_parse[0][:2].strip()
# 關(guān)注數(shù)
# 如果有些話題沒(méi)有人關(guān)注赛惩,則該信息不會(huì)顯示哀墓,因此需要判斷
if '關(guān)注' in others_parse[1]:
follow = others_parse[1][:-3].strip()
else:
follow = '0'
# 回復(fù)數(shù)和瀏覽量不會(huì)因?yàn)?而消失,但由于關(guān)注數(shù)的不確定性導(dǎo)致反向切片比較穩(wěn)妥
# 回復(fù)數(shù)
reply = others_parse[-3][:-3].strip()
# 瀏覽量
browse = others_parse[-2][:-3].strip()
# 時(shí)間
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)
返回的結(jié)果如下:
基本已經(jīng)獲取到了我們需要的信息喷兼。
簡(jiǎn)單分析一下篮绰,其實(shí)互動(dòng)類(lèi)型和回復(fù)數(shù)兩個(gè)信息是相互關(guān)聯(lián)的,如果回復(fù)數(shù)為0季惯,那么最后一個(gè)參與者的互動(dòng)類(lèi)型一定是發(fā)起問(wèn)題吠各,回復(fù)數(shù)不為0則互動(dòng)類(lèi)型一定是回復(fù)臀突。這種關(guān)聯(lián)性分析對(duì)于數(shù)據(jù)預(yù)處理和分析尤為重要,也是降維的依據(jù)
因此給我們一個(gè)警示:數(shù)據(jù)的獲取需要有針對(duì)性
另外我們發(fā)現(xiàn)贾漏,時(shí)間上不統(tǒng)一(逼死強(qiáng)迫癥)候学,一周之內(nèi)的時(shí)間會(huì)顯示為 x天前 的形式。
對(duì)于詳細(xì)顯示的時(shí)間纵散,其實(shí)我們更關(guān)注的是date梳码,即年月日,而不是time
仔細(xì)分析可以發(fā)現(xiàn)這個(gè)顯示的時(shí)間是最后一次互動(dòng)的時(shí)間:發(fā)起問(wèn)題或回復(fù)問(wèn)題伍掀。
我們點(diǎn)進(jìn)其中的一個(gè)問(wèn)題看看:
這意味著即使你提高爬取深度只為了獲取HHHH-MM-DD形式的時(shí)間也是徒勞掰茶。
(一般也不會(huì)這么做,提高IO阻塞的風(fēng)險(xiǎn)就為了拿個(gè)時(shí)間蜜笤?)
不過(guò)根據(jù)已有的信息已經(jīng)可以處理時(shí)間信息了
補(bǔ)充一個(gè)時(shí)間加減的知識(shí)
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模塊獲取今天的時(shí)間濒蒋,并且用timedelta完成相加減
strftime方法幫助對(duì)時(shí)間格式進(jìn)行自定義格式調(diào)整,如果不知道這個(gè)方法可以直接用文本處理切割獲取date瘩例,不過(guò)應(yīng)注意時(shí)間格式需要先轉(zhuǎn)化為str格式啊胶,這里如果對(duì)面向?qū)ο缶幊淌煜さ男』锇閼?yīng)該很清楚,str()先實(shí)例化對(duì)象才能調(diào)用文本方法
print(str(today + delta).split()[0])
# > 2020-04-03
對(duì)之前的時(shí)間代碼進(jìn)行條件判斷
# 時(shí)間
time = others_parse[-1]
if '前' in time:
# 一定是7天之內(nèi)垛贤,故肯定只有1位數(shù)字焰坪,直接用文本切片獲取再轉(zhuǎn)成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]
結(jié)果一片祥和
接下來(lái)加上多頁(yè)爬取的代碼
第一頁(yè):
https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0
第四頁(yè):
https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-4
最后一頁(yè):
https://www.bioinfo.info/?/sort_type-new__day-0__is_recommend-0__page-79
可以看到,翻頁(yè)的url邏輯很簡(jiǎn)單聘惦,就是對(duì)應(yīng)修改最后的數(shù)字即可某饰,第一頁(yè)默認(rèn)也可以加上 __page-1(但不是所有的網(wǎng)站都允許默認(rèn),不允許的情況則需要額外添加)
故翻頁(yè)可以用for循環(huán)迭代善绎,設(shè)置最后一頁(yè)是79黔漂,但發(fā)帖的數(shù)量是動(dòng)態(tài)的,不能每次都去人為觀察最后一頁(yè)是第幾頁(yè)然后添加進(jìn)range禀酱。
我們觀察一下超出最后一頁(yè)會(huì)如何:
頁(yè)面沒(méi)有404炬守!
但是沒(méi)有任何帖子,審查網(wǎng)頁(yè)元素也可以看到之前解析的html元素都不存在剂跟,因此可以考慮建立while True死循環(huán)减途,只需要用try捕獲異常,如果報(bào)解析異常直接終止循環(huán)跳出去就可以了曹洽。
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
# 為了防止給孟叔的網(wǎng)站太大壓力 每次爬取睡3s
time.sleep(3)
except Exception as error:
print(error)
break
要注意 xpath是不會(huì)主動(dòng)觸發(fā)異常鳍置,所以需要在解析中自行判斷和觸發(fā)
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("http://div[@class='aw-question-content']")
if not questions:
# 你想觸發(fā)什么異常都可以
raise AttributeError
至此,簡(jiǎn)單的爬取帖子信息的代碼就完成了送淆,最后就是存儲(chǔ)了
持久化存儲(chǔ)可以用存成csv或者excel税产,也可以存到mysql
我個(gè)人比較喜歡用mysql,畢竟學(xué)了很長(zhǎng)時(shí)間不用也是浪費(fèi)。也可以用mongodb儲(chǔ)存辟拷,但我目前非關(guān)系型用redis比較多撞羽,mongodb不是特別熟悉,不做介紹梧兼。
以下對(duì)兩種方式都做展示
利用openpyxl存儲(chǔ)excel
導(dǎo)入需要的庫(kù)
from openpyxl import Workbook
實(shí)例化新表以及表頭設(shè)置
wb = Workbook()
sheet = wb.active
sheet.title = 'bioinfo帖子信息'
header = ['標(biāo)題', '鏈接', '話題', '最后互動(dòng)者', '互動(dòng)形式', '關(guān)注數(shù)', '回復(fù)數(shù)', '瀏覽數(shù)', '最后互動(dòng)日期']
sheet.append(header)
基本的準(zhǔn)備工作做完后面就很簡(jiǎn)單放吩,只需要在網(wǎng)頁(yè)解析的最后加上兩行代碼,最后記得保存就可以
# 要和表頭對(duì)應(yīng)羽杰,存儲(chǔ)的時(shí)候別搞烏龍
row = [title, link, issue, participant, type, follow, reply, browse, date]
sheet.append(row)
主線程最后加上:
wb.save('bioinfo帖子信息.xlsx') # 也可以指定絕對(duì)路徑
excel存儲(chǔ)就完成了
利用pymysql存儲(chǔ)至mysql數(shù)據(jù)庫(kù)
這里可以把pymysql封裝成一個(gè)類(lèi)至本地方便使用
import pymysql
class Mysqlhelper(object):
def __init__(self):
self.connect = pymysql.connect(host='localhost',
port=3306,
user='xxxxxx',
password='xxxxxx',
db='xxxxxx', # 賬號(hào)密碼用自己的渡紫,數(shù)據(jù)庫(kù)可以指定
charset='utf8')
self.cursor = self.connect.cursor()
def execute_sql(self, sql, data):
self.cursor.execute(sql, data)
self.connect.commit()
# 析構(gòu)函數(shù)在python的垃圾回收器觸發(fā)銷(xiāo)毀的時(shí)候調(diào)用,一般很少用考赛,但此處可以用來(lái)關(guān)閉游標(biāo)和連接
def __del__(self):
self.cursor.close()
self.connect.close()
表格需要自己在mysql里建好惕澎,參考語(yǔ)法如下
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后續(xù)操作
# 實(shí)例化對(duì)象
mysqlhelper = Mysqlhelper()
解析網(wǎng)頁(yè)后構(gòu)建sql語(yǔ)句
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)
最后爬取的結(jié)果 全部782條帖子:
以上就是簡(jiǎn)單爬取生信坑bioinfo帖子信息的小案例
附上完整代碼:
import requests
from lxml import html
import datetime
import time
import pymysql
# 個(gè)人建議mysql的類(lèi)放在其他文件中
# 以from mysqlhelper import Mysqlhelper形式導(dǎo)入 功能上會(huì)更加清晰
class Mysqlhelper(object):
def __init__(self):
self.connect = pymysql.connect(host='localhost',
port=3306,
user='xxxxxx',
password='xxxxxx',
db='xxxxxx', # 賬號(hào)密碼用自己的颜骤,數(shù)據(jù)庫(kù)可以指定
charset='utf8')
self.cursor = self.connect.cursor()
def execute_sql(self, sql, data):
self.cursor.execute(sql, data)
self.connect.commit()
# 析構(gòu)函數(shù)在python的垃圾回收器觸發(fā)銷(xiāo)毀的時(shí)候調(diào)用唧喉,一般很少用,但此處可以用來(lái)關(guān)閉游標(biāo)和連接
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
# 為了防止給孟叔的網(wǎng)站太大壓力每次爬取睡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("http://div[@class='aw-question-content']")
# 要注意 xpath是不會(huì)主動(dòng)觸發(fā)異常忍抽,所以需要自行判斷
if not questions:
# 你想什么異常都可以
raise AttributeError
for i in questions:
# 標(biāo)題
title = i.xpath("h4/a/text()")[0]
# 鏈接
link = i.xpath("h4/a/@href")[0]
# 話題分類(lèi)
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(' ? ')
# 參與類(lèi)型(發(fā)起/回復(fù))
type = others_parse[0][:2].strip()
# 關(guān)注數(shù)
# 如果有些話題沒(méi)有人關(guān)注八孝,則該信息不會(huì)顯示,因此需要判斷
if '關(guān)注' in others_parse[1]:
follow = others_parse[1][:-3].strip()
else:
follow = '0'
# 回復(fù)數(shù)和瀏覽量不會(huì)因?yàn)?而消失鸠项,但由于關(guān)注數(shù)的不確定性導(dǎo)致反向切片比較穩(wěn)妥
# 回復(fù)數(shù)
reply = others_parse[-3][:-3].strip()
# 瀏覽量
browse = others_parse[-2][:-3].strip()
# 時(shí)間
time = others_parse[-1].strip()
if '前' in time:
# 一定是7天之內(nèi)干跛,故肯定只有1位數(shù)字,直接用文本切片獲取再轉(zhuǎn)成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行
總結(jié):
這個(gè)最終的代碼中間發(fā)現(xiàn)了兩個(gè)問(wèn)題楼入,不斷去修復(fù)和調(diào)整代碼,
第一個(gè) 就是發(fā)現(xiàn)最后一個(gè)互動(dòng)的人可能沒(méi)有名字:
這個(gè)是真的迷惑牧抽,點(diǎn)進(jìn)去也可以發(fā)現(xiàn)名字嘉熊,可能是前端bug
第二個(gè) 是問(wèn)題如果無(wú)人關(guān)注則不會(huì)顯示該信息:
這些問(wèn)題在一開(kāi)始分析網(wǎng)頁(yè)和寫(xiě)代碼的時(shí)候很難意識(shí)到,因此需要不斷調(diào)整讓代碼更健壯扬舒。
這也是爬蟲(chóng)的魅力之一
從這個(gè)項(xiàng)目中也延伸出很多新的有趣的思路:
- 利用詞塊切割和語(yǔ)義分析評(píng)估目前近800個(gè)帖子的關(guān)注重心在哪
- 深入一層獲取每個(gè)帖子的評(píng)論和各類(lèi)信息阐肤,并進(jìn)行可視化
(想夸一下R,可視化能力總體比matplotlib要強(qiáng)讲坎,但如果是動(dòng)態(tài)交互性可視化可以用pyecharts或者Q版手繪風(fēng)的cutecharts) - 論壇中只能顯示100位用戶的相關(guān)信息泽腮,利用全論壇爬取可以挖掘出所有在論壇留下過(guò)足跡的用戶的全部信息,從而進(jìn)行人物畫(huà)像和行為分析(笑)
- 加速爬取衣赶,本例也可以使用成熟的爬取框架scrapy。如果不使用框架的情況下厚满,可以利用多進(jìn)程 多線程 協(xié)程的方式爬取府瞄,有時(shí)候?yàn)榱俗晕蚁拗坪捅匾淖远x需要利用隊(duì)列和回調(diào)
……
思路是無(wú)止境的,這些新的案例今后我會(huì)繼續(xù)分享
Life is short, you need Python