python漸進---html和json解析
原載於
https://mp.weixin.qq.com/s/uVlcqRFo_QngoQQ7rRhVfA
從網路中取得一個檔案後,就進入到了處理檔案的階段了。
從網路取回的位元組流,可能會是亂碼。這個問題可能由兩個原因產生。
一個是在請求的時候,在http頭中加入了accept-encoding域,比如說加入了“accept-encoding:gzip,deflate,sdch,br”。這樣伺服器就認為你可以接受壓縮檔案,於是給你返回了一個壓縮的位元組流。此時就需要根據對應的解壓演算法來對位元組流進行解壓。否則這一串位元組流就沒法使用。
另一個原因是網頁的編碼和程式預設的不對應。比如網頁是gb2312,而程式預設為utf-8格式的編碼,這樣也會導致處理失敗。
對於第一個問題,不帶accept-encoding域訪問就可以避免壓縮。
而第二個問題,其實就是一個字元編碼的問題,在之前講字元的時候已經涉及過了。這裡的問題主要是怎麼獲取網頁字元編碼的問題。之前講過使用urllib2拿到返回的網頁,可以通過f.info()取得http協議返回頭。而這個http頭的content-type域,就包含了字元編碼資訊,這個域的值可能是長這樣的“text/html; charset=utf-8”。所以可以通過處理這個字串的方式,來獲取到charset=後面的那個字元編碼資訊,整個處理的程式碼可能是這樣:
contenttype=f.info()['content-type'] contentypelist = contenttype.split(';') for ct in contentypelist: if ct.find('charset=')>=0: ctl=ct.split('=') if ctl[1] != '': codingtype = ctl[1] print(codingtype) try: if codingtype!='utf-8': data=data.decode(codingtype,'ignore') data=data.encode(utf-8) print('utf-8 encode') except Exception,e: print('decode page failed!!')
上面的程式碼獲取了codingtype之後,如果發現不是utf-8編碼,就把它解碼後再編碼成utf-8格式。這樣就保證了data最後是utf-8格式,和程式預設編碼一致。
把字元壓縮和字元編碼問題解決了之後,就是一個文字解析的問題了。而從網路中取得的檔案,絕大部份是html檔案或者是json物件。
python的基本庫中,使用htmlparser來進行html檔案的解析。使用json類來進行json檔案的解析。
18.1 htmlparser類
htmlparser類提供了很多的方法供重寫,只要htmlparser類碰到了相應的標籤,就會呼叫相應的方法。
htmlparser在遇到<tag>型別的標籤會呼叫handle_starttag(tag,attrs)方法,比如說碰到<a href='test'>會呼叫handle_starttag,並且引數tag=a,而attrs是它的屬性(key,value)對;
遇到</tag>型別的標籤會呼叫handle_endtag(tag);
遇到<img/>型別的同時是開始標籤也是結束標籤的,會呼叫handle_startendtag(tag,attrs);
遇到 <>data</>型別的資料會呼叫handle_data(data);
遇到>型別的轉義字元會呼叫handle_entityref(name);
遇到>型別的轉義會呼叫handle_charref(name);
遇到<--! -->型別的註釋資料會呼叫handle_comment(data)。
一般而言,重寫這些方法,就可以獲取到每個標籤和資料的內容了。
htmlparser只關注當下的標籤和資料,不會記住當前標籤的狀態,更不會知道標籤的巢狀。如果想要通過路徑的形式來定位某個標籤,需要在進入標籤的時候加上本標籤,同時在退出標籤的時候減去本標籤。比如說進入<html>標籤就把本標籤加上變成'/html',再進入<head>標籤,又把標籤加上變成'/html/head',就這樣一層一層下去。而退出</head>標籤的時候把'/head'從路徑中拿掉,又變成了'/html',接著進入<body>標籤又變成了'/html/body'。這樣就可以知道當前的標籤路徑是什麼了。
一個完整的自定義解析類可能是這樣:
import urllib2
import traceback
from HTMLParser import HTMLParser
class wxhtml(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.sd='share data'
self.tagpath=""
def handle_starttag(self,tag,attrs):
self.tagpath=self.tagpath+'/'+tag
print('enter '+self.tagpath)
for attr in attrs:
print(attr)
def handle_endtag(self,tag):
i=len('/'+tag)
self.tagpath=self.tagpath[:-i]
print('exit '+self.tagpath)
def handle_startendtag(self,tag,attrs):
self.tagpath=self.tagpath+'/'+tag
print('startend '+self.tagpath)
for attr in attrs:
print(attr)
i=len('/'+tag)
self.tagpath=self.tagpath[:-i]
def handle_data(self,data):
print('data '+data)
def handle_entityref(self,name):
print('entityref '+name)
def handle_charref(self,name):
print('charref '+name)
def handle_comment(self,comment):
print('comment '+comment)
要注意的是,如果要重寫__init__()方法的話,就需要呼叫父類的__init__()。不然會沒有辦法工作。在上面的程式碼中,__init__()方法初始化了兩個例項變數,一個是記錄標籤路徑的self.tagpath,一個是和外部程式碼共享的變數self.sd。有了self.sd,就可以在網頁解析完畢之後,從self.sd中獲取自己想要的內容。
定義好了這個類之後,使用htmlparser.feed()可以啟動一個html的解析。解析完畢之後使用close()來關閉一個html控制代碼。示例程式碼如下:
wxparser=wxhtml()
wxparser.feed(data)
sd=wxparser.sd
使用微信的主頁作為演示物件,整體的程式碼如下:
import urllib2
import traceback
from HTMLParser import HTMLParser
class wxhtml(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.sd='share data'
self.tagpath=""
def handle_starttag(self,tag,attrs):
self.tagpath=self.tagpath+'/'+tag
print('enter '+self.tagpath)
for attr in attrs:
print(attr)
def handle_endtag(self,tag):
i=len('/'+tag)
self.tagpath=self.tagpath[:-i]
print('exit '+self.tagpath)
def handle_startendtag(self,tag,attrs):
self.tagpath=self.tagpath+'/'+tag
print('startend '+self.tagpath)
for attr in attrs:
print(attr)
i=len('/'+tag)
self.tagpath=self.tagpath[:-i]
def handle_data(self,data):
print('data '+data)
def handle_entityref(self,name):
print('entityref '+name)
def handle_charref(self,name):
print('charref '+name)
def handle_comment(self,comment):
print('comment '+comment)
httpheaders=dict()
httpheaders["Connection"]="keep-alive"
httpheaders["User-Agent"]="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
httpheaders["Accept"]="*/*"
httpheaders["Accept-Language"]="zh-CN,zh;q=0.8"
try:
ustr='http://weixin.qq.com'
rq=urllib2.Request(ustr,headers=httpheaders)
f=urllib2.urlopen(rq,timeout=15)
data=f.read()
contenttype=f.info()['content-type']
contentypelist = contenttype.split(';')
for ct in contentypelist:
if ct.find('charset=')>=0:
ctl=ct.split('=')
if ctl[1] != '':
codingtype = ctl[1]
print(codingtype)
try:
if codingtype!='utf-8':
data=data.decode(codingtype,'ignore')
data=data.encode(utf-8)
print('utf-8 encode')
except Exception,e:
print('decode page failed!!')
wxparser=wxhtml()
wxparser.feed(data)
sd=wxparser.sd
print(sd)
wxparser.close()
except:
print(traceback.format_exc())
finally:
f.close()
因為返回太長了,所以就不貼了。這段程式碼是所有的標籤和資料都打印出來了,如果只是想要其中某個路徑下面的資料,可以在獲取資料的時候判斷一下當前的self.tagpath是否自己想要的,再打印出來。
18.2 json資料的解析
json格式的資料解析比html格式的要簡單許多。不過要注意的是,有時候伺服器返回的直接就是一個json格式的序列化的資料流,有時候會把一個json格式的資料流包含在類似於'jsoncallback('和')'的字串之間,有時候會包含在類似於'var=([{'和‘}])’的字串之間。這都是為了前端程式碼好直接執行設定的。如果要做解析的話,就必須把前後的無關緊要的字元全部去掉,還原一個真正的json格式,這樣才可以解析成功。
因為json預設是utf8編碼。所以在解析之前要把位元組流的編碼調整到utf-8。
一般情況下,使用json.loads()來載入資料。這個方法會把json格式的資料流轉成python裡面對應的list、dict、字串、數字等格式。接著就可以按照python的資料結構來訪問資料內容了
使用json.dumps()可以把一個python的資料物件變成json格式序列化資料流。
以下的程式碼從鳳凰財經的cgi當中讀取5分鐘線,並且把返回的json格式的資料打印出來。
import urllib2
import traceback
import json
httpheaders=dict()
httpheaders["Connection"]="keep-alive"
httpheaders["User-Agent"]="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
httpheaders["Accept"]="*/*"
httpheaders["Accept-Language"]="zh-CN,zh;q=0.8"
try:
ustr='http://api.finance.ifeng.com/akmin/?scode=sh600000&type=5'
rq=urllib2.Request(ustr,headers=httpheaders)
f=urllib2.urlopen(rq,timeout=15)
data=f.read()
codingtype='utf-8'
contenttype=f.info()['content-type']
contentypelist = contenttype.split(';')
for ct in contentypelist:
if ct.find('charset=')>=0:
ctl=ct.split('=')
if ctl[1] != '':
codingtype = ctl[1]
print(codingtype)
try:
if codingtype!='utf-8':
data=data.decode(codingtype,'ignore')
data=data.encode(utf-8)
print('utf-8 encode')
except Exception,e:
print('decode page failed!!')
d=json.loads(data)
stockdatalist=d['record']
for stockdata in stocklist:
print(stock)
#print(d)
except:
print(traceback.format_exc())
finally:
f.close()
鳳凰財經的5分鐘線cgi返回的資料是一個字典,並且只有一個鍵值 record。這個鍵值對應的就是一個裝滿了五分鐘線的資料列表,列表中的每一項就是一個五分鐘k線的資料。使用json.loads()方法很快就把這個資料流轉化為python的字典了,接著就可以訪問k線資料了。
更新到這裡,基本上python的基本知識點都覆蓋了,明天把時間相關的內容整理一下。python漸進的系列就先告一段落了。以後的更新內容和時間均未定。休息一下。