1. 程式人生 > >Python抓取網頁&批量下載檔案方法初探(正則表示式+BeautifulSoup)

Python抓取網頁&批量下載檔案方法初探(正則表示式+BeautifulSoup)

最近兩週都在學習Python抓取網頁方法,任務是批量下載網站上的檔案。對於一個剛剛入門python的人來說,在很多細節上都有需要注意的地方,以下就分享一下我在初學python過程中遇到的問題及解決方法。

一、用Python抓取網頁

基本方法:

import urllib2,urllib

url = 'http://www.baidu.com'
req = urllib2.Request(url)
content = urllib2.urlopen(req).read()

1)、url為網址,需要加'http://'

2)、content為網頁的html原始碼

問題:

1、網站禁止爬蟲,不能抓取或者抓取一定數量後封ip

解決:偽裝成瀏覽器進行抓取,加入headers:

import urllib2,urllib

headers = {	#偽裝為瀏覽器抓取
    	'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
	}

req = urllib2.Request(url,headers=headers)
content = urllib2.urlopen(req).read()

2、抓取網頁中的中文為亂碼問題

解決:用BeautifulSoup解析網頁(BeautifulSoup是Python的一個用於解析網頁的外掛,其安裝及使用方法下文會單獨討論)

首先需要介紹一下網頁中的中文編碼方式,一般網頁的編碼會在<meta>標籤中標出,目前有三種,分別是GB2312,GBK,GB18030,三種編碼是相容的,

從包含的中文字元個數比較:GB2312 < GBK < GB18030,因此如果網頁標稱的編碼為GB2312,但是實際上用到了GBK或者GB18030的中文字元,那麼編碼工具就會解析錯誤,導致編碼退回到最基本的windows-2152了。所以解決此類問題分兩種情況。

1)、若網頁的實際的中文編碼和其標出的相符的話,即沒有字元超出所標稱的編碼,下面即可解決

import urllib,urllib2,bs4
	
req = urllib2.Request(url)
content = urllib2.urlopen(req).read()
content = bs4.BeautifulSoup(content)
return content

2)、若網頁中的中文字元超出所標稱的編碼時,需要在BeautifulSoup中傳遞引數from_encoding,設定為最大的編碼字符集GB18030即可
import urllib,urllib2,bs4
	
req = urllib2.Request(url)
content = urllib2.urlopen(req).read()
content = bs4.BeautifulSoup(content,from_encoding='GB18030')
return content

二、用Python下載檔案

使用Python下載檔案的方法有很多,在此只介紹最簡單的一種

import urllib

urllib.urlretrieve(url, filepath)

url為下載連結,filepath即為存放的檔案路徑+檔名

三、使用正則表示式分析網頁

將網頁原始碼抓取下來後,就需要分析網頁,過濾出要用到的欄位資訊,通常的方法是用正則表示式分析網頁,一個例子如下:

import re

content = '<a href="http://www.baidu.com">'
match = re.compile(r'(?<=href=["]).*?(?=["])')
rawlv2 = re.findall(match,content)

用re.compile()編寫匹配模板,用findall查詢,查詢content中所有與模式match相匹配的結果,返回一個列表,上式的正則表示式意思為匹配以‘href="'起始,以'"'結束的欄位,使用非貪婪的規則,只取中間的部分

個人推薦第一篇,條理清晰,不重不漏

在此就不贅述正則表示式的學習,只總結一下我在實際寫正則時的認為需要注意的幾個問題:

1)、一定要使用非貪婪模式進行匹配,即*?,+?(後加?),因為Python預設使用貪婪模式進行匹配,例如'a.*b',它會匹配文件中從第一個a和最後一個b之間的文字,也就是說如果遇到一個b,它不會停止,會一直搜尋至文件末尾,直到它確認找到的b是最後一個。而一般我們只想取某個欄位的值,貪婪模式既不能返回正確的結果,還大大浪費了時間,所以非貪婪是必不可少的

2)、raw字串的使用:如果要匹配一個.,*這種元字元,就需要加'\'進行轉義,即要表示一個'\',正則表示式需要多加一個轉義,寫成'\\',但是Python字串又需要對其轉義,最終變成re.compile('\\\\'),這樣就不易理解且很亂,使用raw字串讓正則表示式變得易讀,即寫成re.compile(r'\\'),另一個方法就是將字元放到字符集中,即[\],效果相同

3)、()特殊構造的使用:一般來說,()中的匹配模式作為分組並可以通過標號訪問,但是有一些特殊構造為例外,它們適用的情況是:我想要匹配href="xxxx"這個模式,但是我只需要xxxx的內容,而不需要前後匹配的模式,這時就可以用特殊構造(?<=),和(?=)來匹配前後文,匹配後不返回()中的內容,剛才的例子便用到了這兩個構造。

4)、邏輯符的使用:如果想匹配多個模式,使用'|'來實現,比如

re.compile(r'.htm|.mid$')
匹配的就是以.htm或.mid結尾的模式,注意沒有'&'邏輯運算子

四、使用BeautifulSoup分析網頁

BeautifulSoup是Python的一個外掛,用於解析HTML和XML,是替代正則表示式的利器,下文講解BS4的安裝過程和使用方法

1、安裝BS4

下載 beautifulsoup4-4.1.3.tar.gz,解壓:linux下 tar xvf beautifulsoup4-4.1.3.tar.gz,win7下直接解壓即可

linux:

進入目錄執行:

 1, python setup.py build 

 2, python setup.py install 

或者easy_install BeautifulSoup

win7:

cmd到控制檯 -> 到安裝目錄 -> 執行上面兩個語句即可

2、使用BeautifulSoup解析網頁

1)、包含包:import bs4

2)、讀入:

req = urllib2.Request(url)
content = urllib2.urlopen(req).read()
content = bs4.BeautifulSoup(content,from_encoding='GB18030')

3)、查詢內容

a、按html標籤名查詢:

frameurl = content.findAll('frame')

framurl為儲存所有frame標籤內容的列表,例如frame[0] 為 <framename="m_rtop" target="m_rbottom"src="tops.htm">

b、按標籤屬性查詢

frameurl = content.findAll(target=True)

查詢所有含target屬性的標籤
frameurl = content.findAll(target=‘m_rbottom’)

查詢所有含target屬性且值為'm_rbottom'的標籤

c、帶有正則表示式的查詢

rawlv2 = content.findAll(href=re.compile(r'.htm$'))

查詢所有含href屬性且值為以'.htm'結尾的標籤

d、綜合查詢

frameurl = content.findAll('frame',target=‘rtop’)

查詢所有frame標籤,且target屬性值為'rtop'

4)、訪問標籤屬性值和內容

a、訪問標籤屬性值

rawlv2 = content.findAll(href=re.compile(r'.htm$'))
href = rawlv2[i]['href']
通過[屬性名]即可訪問屬性值,如上式返回的便是href屬性的值

b)、訪問標籤內容

rawlv3 = content.findAll(href=re.compile(r'.mid$'))
songname = str(rawlv3[i].text)

上式訪問了<a href=...>(內容)</a>標籤的實際內容,由於text為unicode型別,所以需要用str()做轉換

附上最終的成果,程式功能是抓取www.dugukeji.com上的所有midi檔案並下載,需要先建立./midi/dugukeji/資料夾和./midi/linklist檔案

#-*- coding:utf-8 -*-	#允許文件中有中文
import urllib2,urllib,cookielib,threading
import os
import re
import bs4
import sys
reload(sys)
sys.setdefaultencoding('utf-8')	#允許列印unicode字元


indexurl = 'http://www.dugukeji.com/'
databasepath = './midi/linklist'
path = './midi/dugukeji/'
totalresult = {}
oriresult = {}

def crawl(url):
	headers = {	#偽裝為瀏覽器抓取
    	'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
	}
	req = urllib2.Request(url,headers=headers)
	content = urllib2.urlopen(req).read()
	content = bs4.BeautifulSoup(content,from_encoding='GB18030')
	return content


def crawlframe(sourceurl,target):
	global indexurl
	content = crawl(sourceurl)
	#match = re.compile(r'(?<=target=["]'+target+'["] src=["]).*?(?=["])')	#正則表示式方法
	#frameurl = re.findall(match,content)
	frameurl = content.findAll('frame',target=target)	#beautifulsoup方法
	result = indexurl+frameurl[0]['src']
	return result

def crawllv1(frameurl,st=-1,en=-1):
	global indexurl
	content = crawl(frameurl)
	#match = re.compile(r'(?<=href=["]).*?(?=["])')
	#rawlv2 = re.findall(match,content)
	rawlv2 = content.findAll(href=re.compile(r'.htm$'))
	result = []
	if st==-1 and en==-1:
		for i in range(len(rawlv2)):
			result.append(indexurl+rawlv2[i]['href'])
	else:
		for i in range(st,en):
			result.append(indexurl+rawlv2[i]['href'])
	#dele = []
	#for i in range(len(result)):
	#	if result[i][-4:]!='.htm' and result[i][-5:]!='.html':
	#		dele.append(i)
#		else:
#			result[i]=indexurl+result[i]
#	if len(dele)>0:
#		for deli in dele:
#			del result[deli]

	#result.sort()
	return result

def crawllv2(lv2url):
	global indexurl
	content = crawl(lv2url)
	#match = re.compile(r'(?<=href=["]\.\.\/).*?[">].*?(?=[<])')
	#rawlv3 = re.findall(match,content)
	rawlv3 = content.findAll(href=re.compile(r'[..].*?[0-9].htm|.mid$'))
	#print rawlv3
	result = {}	#結果字典,key:連結,value:歌曲名
	for i in range(len(rawlv3)):
		tmp = str(rawlv3[i]['href'])
		#print tmp
		link = indexurl + tmp[tmp.rfind('..')+3:]	#有多個'..',找到最後一個
		songname = ''
		if tmp[-4:]=='.htm':	#需要訪問3級頁
			try:
				conlv3 = crawl(link)
			except:
				print 'WARNING: visit lv3 url failed!\n'
			else:
				rawlv4 = conlv3.findAll(href=re.compile(r'.mid$'))
				if not rawlv4:	#4級頁沒有.mid下載連結,略過
					continue
				else:
					tmp = str(rawlv4[0]['href'])
					link = indexurl + tmp[tmp.rfind('..')+3:]

		songname = str(rawlv3[i].text)	#將unicode型別的text轉化為string
		#songname.decode('GBK')
		#songname.encode('utf-8')
		songname = songname.replace(' ','_')	#將songname中空格和換行轉化為下劃線
		songname = songname.replace('\n','_')	#原來存在的連結,直接略過
		if oriresult.has_key(link):
			continue
		if totalresult.has_key(link) and len(songname)<len(totalresult[link]):	#如果連結已儲存且歌曲名長度比當前的長,略過
			continue
		else:
			totalresult[link] = songname
			result[link] = songname		#加入字典
	#result.sort()
	return result

def download(totalresult):
	for link in totalresult.keys():
		filepath = path + totalresult[link] + '.mid'
		print 'download: ',link,' -> ',filepath,'\n'
		urllib.urlretrieve(link, filepath)


def readdata(databasepath):
	datafile = open(databasepath,'r')	#讀資料檔案
	link = datafile.readline()
	while link:
		oriresult[link]=''
		link = datafile.readline()
	datafile.close()

def writedata(databasepath):
	datafile = open(databasepath,'a')	#追加開啟資料檔案,將新連結寫入檔案尾
	for link in totalresult.keys():
		datafile.write(link,'\n')
	datafile.close()

if __name__ == '__main__':
	try:
		readdata(databasepath)	#訪問檔案,記錄已下載的連結
	except:
		print 'WARNING:read database file failed!\n'
	else:
		print 'There is ',len(oriresult),' links in database.\n'

	try:
		frameurl1 = crawlframe(indexurl,'rtop')	#抓取主頁中一級頁url所在frame的url
	except:
		print 'WARNING: crawl lv1 frameurl failed!\n'
	try:
		urllv1 = crawllv1(frameurl1,4,20)		#抓取一級頁url
	except:
		print 'WARNING: crawl lv1 url failed!\n'

	for i in urllv1:
		print 'lv1 url:',i
		try:
			frameurl2 = crawlframe(i,'rbottom')	#抓取一級頁中二級頁url所在frame的url
		except:
			print 'WARNING: crawl lv2 frameurl failed!\n'
		else:
			print '\tlv2 frameurl:',frameurl2
			try:
				urllv2 = crawllv1(frameurl2)	#抓取二級頁url
			except:
				print 'WARNING: crawl lv2 url failed!\n'
			else:
				for j in urllv2:
					print '\t\tlv2 url:',j
					try:
						urllv3 = crawllv2(j)
					except:
						print 'WARNING: crawl lv3 url failed!\n'
					else:
						for k in urllv3.keys():
							print '\t\t\tlv3 url:',k,'\tname:',urllv3[k]
						#download(urllv3)
							
	print 'new added midi num:',len(totalresult)
	print '\nbegin to download...\n'
	download(totalresult)
	print '\nWrite database...\n'
	writedata(databasepath)
	print '\n\nDone!\n'




"""
url = 'http://www.dugukeji.com/'
req = urllib2.Request(url)
response = urllib2.urlopen(req).read()
response = unicode(response,'GBK').encode('UTF-8')
print response
"""