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
"""