7個月前的一篇todo-list:一個下載新浪博客工具的to-do list
今天終于可以說是完工了际看。
主要的技術(shù)點:
- 使用urllib和urllib2獲取網(wǎng)頁內(nèi)容
- 使用BeautifulSoup和re來解析網(wǎng)頁內(nèi)容
編碼思路:
一深滚、獲取博文列表
我想要下載的目標(biāo)博客:纏中說禪的博客
分析此博客赏殃,發(fā)現(xiàn)點擊“博客目錄”后可獲取較調(diào)理的信息:
發(fā)現(xiàn)以下幾點:
- 左側(cè)有博文分類谤狡,可通過此處獲取感興趣的分類
- 下方有當(dāng)前分類的所有博文所占的頁數(shù),可通過此處獲得總工作量
class Spider:
def __init__(self, indexUrl):
self.indexUrl = indexUrl
content = indexUrl.split('/')[-1].split('_')
self.userID = content[1]
self.defaultPage = self.getPage(self.indexUrl)
def getPage(self, indexUrl):
'''獲取indexUrl頁面'''
request = urllib2.Request(indexUrl)
response = urllib2.urlopen(request)
return response.read().decode('utf-8')
def getPageNum(self,page):
'''計算有幾頁博客目錄'''
pattern = re.compile('<li class="SG_pgnext">', re.S)
result = re.search(pattern, page)
if result:
print u"目錄有多頁,正在計算……"
pattern2 = re.compile(u'<li class="SG_pgnext">.*?>共(.*?)頁', re.S)
num = re.search(pattern2, page)
pageNum = str(num.group(1))
print u"共有", pageNum, u"頁"
else:
print u"只有1頁目錄"
pageNum = 1
return int(pageNum)
博客目錄的URL(http://blog.sina.com.cn/s/articlelist_1215172700_0_1.html)羹铅,
其中1215172700是用戶ID,后面的0表示第一個分類“全部博文”愉昆,最后的1表示是次分類的第1頁。
在Spider類初始化時將此URL解析麻蹋,并傳入getPage函數(shù)中跛溉,獲取網(wǎng)頁HTML。
用正則表達式(re)來解析HTML其實并不是個好方法扮授,原因見這里:You can't parse [X]HTML with regex. Because HTML can't be parsed by regex. Regex is not a tool that can be used to correctly parse HTML.
在之后解析每篇博客內(nèi)容時芳室,re就無能為力了,我只好去使用BeautifulSoup刹勃,但是在前期我卻是參考別的文章使用了正則表達堪侯。
在getPageNum函數(shù)中使用re來獲取了當(dāng)前分類的總頁數(shù)。
然而我們剛開始的時候并不知道要選擇哪個分類荔仁,所以要將這些信息顯示出來供用戶選擇伍宦。
def getTypeNum(self):
'''計算有幾種分類'''
pattern = re.compile('<span class="SG_dot">.*?<a href="(.*?)".*?>(.*?)</a>.*?<em>(.*?)</em>', re.S)
result = re.findall(pattern, self.defaultPage)
pattern2 = re.compile(u'<strong>全部博文</strong>.*?<em>(.*?)</em>', re.S)
result2 = re.search(pattern2, self.defaultPage)
self.allType = {}
i = 0
self.allType[i] = (self.indexUrl, u"全部博文", result2.group(1)[1:-1])
for item in result:
i += 1
self.allType[i] = (item[0], item[1], item[2][1:-1])
print u"本博客共有以下", len(self.allType), "種分類:"
for i in range(len(self.allType)):
print "ID: %-2d Type: %-30s Qty: %s" % (i, self.allType[i][1], self.allType[i][2])
依然是使用re。在該函數(shù)中獲取各分類對應(yīng)的URL乏梁。
現(xiàn)在的流程梳理下就是這樣的:
- 程序獲取所有的博文分類
- 用戶選擇感興趣的分類
- 程序獲取該分類的URL和頁數(shù)
- 程序獲取并解析每篇文章(下一章)
二次洼、解析文章
首先會根據(jù)分類和頁數(shù),得到具體某一頁的博文列表的URL遇骑。具體規(guī)則上面已提到卖毁。然后需要將此頁中的所有博客的URL解析出來。
def getBlogList(self,page):
'''獲取一頁內(nèi)的博客URL列表'''
pattern = re.compile('<div class="articleCell SG_j_linedot1">.*?<a title="" target="_blank" href="(.*?)">(.*?)</a>', re.S)
result = re.findall(pattern, page)
blogList = []
for item in result:
blogList.append((item[0], item[1].replace(' ', ' ')))
return blogList
依然是使用re落萎。
def mkdir(self,path):
isExist = os.path.exists(path)
if isExist:
print u"名為", path, u"的文件夾已經(jīng)存在"
return False
else:
print u"正在創(chuàng)建名為", path, u"的文件夾"
os.makedirs(path)
def saveBlogContent(self,path,url):
'''保存url指向的博客內(nèi)容'''
page = self.getPage(url)
blogTool = sinaBlogContentTool(page)
blogTool.parse()
filename = path + '/' + blogTool.time + ' ' + blogTool.title.replace('/', u'斜杠') + '.markdown'
with open(filename, 'w+') as f:
f.write("URL: "+url)
f.write("標(biāo)簽:")
for item in blogTool.tags:
f.write(item.encode('utf-8'))
f.write(' ')
f.write('\n')
f.write("類別:")
f.write(blogTool.types.encode('utf-8'))
f.write('\n')
picNum = 0
for item in blogTool.contents:
if item[0] == 'txt':
f.write('\n')
f.write(item[1].encode('utf-8'))
elif item[0] == 'img':
f.write('\n')
f.write('!['+ str(picNum) + '](' + item[1] + ')')
picNum += 1
print u"下載成功"
接下來就是解析博客亥啦,保存內(nèi)容至本地。其中創(chuàng)建文件名時需要注意“/\”此類符號练链,我的做法是將符號變?yōu)槲淖帧靶备堋薄?/p>
優(yōu)于解析博客內(nèi)容較為復(fù)雜翔脱,我創(chuàng)建一個class專門解析。
首先觀察某篇博文兑宇,發(fā)現(xiàn)有以下幾類關(guān)鍵信息:
- 博文題目:太對不起了碍侦,被坐骨神經(jīng)折騰了一晚。
- 發(fā)表日期:2008-08-30 19:14:19
- 博文標(biāo)簽:纏中說禪 健康
- 博文分類:纏中說禪
- 博文本身:隶糕。瓷产。。枚驻。濒旦。。再登。尔邓。晾剖。。
由于博文較為復(fù)雜梯嗽,只能使用BeautifulSoup進行解析齿尽。參考eautiful Soup 4.2.0 文檔。
以上1至4均使用find函數(shù):
find( name , attrs , recursive , text , **kwargs )
由于此4類的標(biāo)簽(tag)中的屬性(attribute)較為特殊灯节,所以均以此搜索循头。
class sinaBlogContentTool:
def __init__(self,page):
self.page = page
def parse(self):
'''解析博客內(nèi)容'''
soup = BeautifulSoup(self.page)
self.title = soup.body.find(attrs = {'class':'titName SG_txta'}).string
self.time = soup.body.find(attrs = {'class':'time SG_txtc'}).string
self.time = self.time[1:-1]
print u"發(fā)表日期是:", self.time, u"博客題目是:", self.title
self.tags = []
for item in soup.body.find(attrs = {'class' : 'blog_tag'}).find_all('h3'):
self.tags.append(item.string)
self.types = u"未分類"
if soup.body.find(attrs = {'class' : 'blog_class'}).a:
self.types = soup.body.find(attrs = {'class' : 'blog_class'}).a.string
self.contents = []
self.rawContent = soup.body.find(attrs = {'id' : 'sina_keyword_ad_area2'})
for child in self.rawContent.children:
if type(child) == NavigableString:
self.contents.append(('txt', child.strip()))
else:
for item in child.stripped_strings:
self.contents.append(('txt', item))
if child.find_all('img'):
for item in child.find_all('img'):
if(item.has_attr('real_src')):
self.contents.append(('img', item['real_src']))
博文本身比較復(fù)雜,因為不僅包含文字炎疆,還有圖片卡骂。所以使用children屬性,可以遍歷Tag或BeautifulSoup對象的子項形入。
如果子項為NavigableString對象(即為字符串)全跨,則直接保存它本身。
否則亿遂,使用stripped_strings屬性浓若,將子項中的所有NavigableString對象均保存下來。同時崩掘,判斷該子項中是否有屬性為‘img’的Tag對象七嫌,若有,則取該Tag的real_src屬性保存下來苞慢。
這樣文字和圖片都獲取到了诵原。
最后,在Spider類中使用run函數(shù)將以上內(nèi)容都串起來:
def run(self):
self.getTypeNum()
i = raw_input(u"請輸入需要下載的類別ID(如需要下載類別為“全部博文”類別請輸入0):")
page0 = self.getPage(self.allType[int(i)][0])
pageNum = self.getPageNum(page0)
urlHead = self.allType[int(i)][0][:-6]
typeName = self.allType[int(i)][1]
typeBlogNum = self.allType[int(i)][2]
if typeBlogNum == '0':
print u"該目錄為空"
return
self.mkdir(typeName)
for j in range(pageNum):
print u"------------------------------------------正在下載類別為", typeName, u"的博客的第", str(j+1), u"頁------------------------------------------"
url = urlHead + str(j+1) + '.html'
page = self.getPage(url)
blogList = self.getBlogList(page)
print u"本頁共有博客", len(blogList), u"篇"
for item in blogList:
print u"正在下載博客《", item[1], u"》中……"
self.saveBlogContent(typeName, item[0])
print u"全部下載完畢"
以下是成果展示: