1. 程式人生 > >python漸進---html和json解析

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);
遇到&gt;型別的轉義字元會呼叫handle_entityref(name);
遇到&#62型別的轉義會呼叫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漸進的系列就先告一段落了。以後的更新內容和時間均未定。休息一下。