post方法就反回了一個string字符串前臺怎么接_Golang Web入門(2):如何實現一個RESTful風格的路由...

摘要

在上一篇文章中,我們聊了聊在Golang中怎么實現一個Http服務器。但是在最后我們可以發現,固然DefaultServeMux可以做路由分發的功能,但是他的功能同樣是不完善的。

DefaultServeMux做路由分發,是不能實現RESTful風格的API的,我們沒有辦法定義請求所需的方法,也沒有辦法在API路徑中加入query參數。其次,我們也希望可以讓路由查找的效率更高。

所以在這篇文章中,我們將分析httprouter這個包,從源碼的層面研究他是如何實現我們上面提到的那些功能。并且,對于這個包中最重要的前綴樹,本文將以圖文結合的方式來解釋。

1 使用

我們同樣以怎么使用作為開始,自頂向下的去研究httprouter。我們先來看看官方文檔中的小例子:

package?main

import?(
????"fmt"
????"net/http"
????"log"

????"github.com/julienschmidt/httprouter"
)

func?Index(w?http.ResponseWriter,?r?*http.Request,?_?httprouter.Params)?{
????fmt.Fprint(w,?"Welcome!\n")
}

func?Hello(w?http.ResponseWriter,?r?*http.Request,?ps?httprouter.Params)?{
????fmt.Fprintf(w,?"hello,?%s!\n",?ps.ByName("name"))
}

func?main()?{
????router?:=?httprouter.New()
????router.GET("/",?Index)
????router.GET("/hello/:name",?Hello)

????log.Fatal(http.ListenAndServe(":8080",?router))
}


其實我們可以發現,這里的做法和使用Golang自帶的net/http包的做法是差不多的。都是先注冊相應的URI和函數,換一句話來說就是將路由和處理器相匹配。

在注冊的時候,使用router.XXX方法,來注冊相對應的方法,比如GETPOST等等。

注冊完之后,使用http.ListenAndServe開始監聽。

至于為什么,我們會在后面的章節詳細介紹,現在只需要先了解做法即可。

2 創建

我們先來看看第一行代碼,我們定義并聲明了一個Router。下面來看看這個Router的結構,這里把與本文無關的其他屬性省略:

type?Router?struct?{
????//這是前綴樹,記錄了相應的路由
????trees?map[string]*node

????//記錄了參數的最大數目
????maxParams??uint16

}


在創建了這個Router的結構后,我們就使用router.XXX方法來注冊路由了。繼續看看路由是怎么注冊的:

func?(r?*Router)?GET(path?string,?handle?Handle)?{
????r.Handle(http.MethodGet,?path,?handle)
}

func?(r?*Router)?POST(path?string,?handle?Handle)?{
????r.Handle(http.MethodPost,?path,?handle)
}

...

在這里還有一長串的方法,他們都是一樣的,調用了

r.Handle(http.MethodPost,?path,?handle)


這個方法。我們再來看看:

func?(r?*Router)?Handle(method,?path?string,?handle?Handle)?{
????...
????if?r.trees?==?nil?{
????????r.trees?=?make(map[string]*node)
????}

????root?:=?r.trees[method]
????if?root?==?nil?{
????????root?=?new(node)
????????r.trees[method]?=?root

????????r.globalAllowed?=?r.allowed("*",?"")
????}

????root.addRoute(path,?handle)
????...
}

在這個方法里,同樣省略了很多細節。我們只關注一下與本文有關的。我們可以看到,在這個方法中,如果tree還沒有初始化,則先初始化這顆前綴樹

然后我們注意到,這顆樹是一個map結構。也就是說,一個方法,對應了一顆樹。然后,對應這棵樹,調用addRoute方法,把URI和對應的Handle保存進去。

3 前綴樹

3.1 定義

又稱單詞查找樹,Trie樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用于統計,排序和保存大量的字符串(但不僅限于字符串),所以經常被搜索引擎系統用于文本詞頻統計。它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。

簡單的來講,就是要查找什么,只要跟著這棵樹的某一條路徑找,就可以找得到。

比如在搜索引擎中,你輸入了一個

39ef84bbe30f7fa96b56854f83bf130e.png

他會有這些聯想,也可以理解為是一個前綴樹。

再舉個例子:

4637eb193b9061d7a11b7ae38f4bc9be.png

在這顆GET方法的前綴樹中,包含了以下的路由:

  • /wow/awesome

  • /test

  • /hello/world

  • /hello/china

  • /hello/chinese ?

說到這里你應該可以理解了,在構建這棵樹的過程中,任何兩個節點,只要有了相同的前綴,相同的部分就會被合并成一個節點

3.2 圖解構建

上面說的addRoute方法,就是這顆前綴樹的插入方法。假設現在數為空,在這里我打算以圖解的方式來說明這棵樹的構建。

假設我們需要插入的三個路由分別為:

  • /hello/world

  • /hello/china

  • /hello/chinese ?

(1)插入/hello/world ?

因為此時樹為空,所以可以直接插入:

d8e0a145114b50a60c8dc73095c92d66.png

(2)插入/hello/china ?

此時,發現/hello/world/hello/china有相同的前綴/hello/

b196882fa01ce5ebda728b54a1aa1fdb.png

那么要先將原來的/hello/world結點,拆分出來,然后將要插入的結點/hello/china,截去相同部分,作為/hello/world的子節點。

9562c521b7882c2e7617386a89329268.png

(3)插入/hello/chinese ?

此時,我們需要插入/hello/chinese,但是發現,/hello/chinese和結點/hello/有公共的前綴/hello/,所以我們去查看/hello/這個結點的子節點。

注意,在結點中有一個屬性,叫indices。它記錄了這個結點的子節點的首字母,便于我們查找。比如這個/hello/結點,他的indices值為wc。而我們要插入的結點是/hello/chinese,除去公共前綴后,chinese的第一個字母也是c,所以我們進入china這個結點。

682212fd89bab23419eb0840f9a29bd4.png

這時,有沒有發現,情況回到了我們一開始插入/hello/china時候的局面。那個時候公共前綴是/hello/,現在的公共前綴是chin

所以,我們同樣把chin截出來,作為一個結點,將a作為這個結點的子節點。并且,同樣把ese也作為子節點。

cb02c5f95481c95f3b0c3865834100c6.png

3.3 總結構建算法

到這里,構建就已經結束了。我們來總結一下算法。

具體帶注釋的代碼將在本文最末尾給出,如果想要了解的更深可以自行查看。在這里先理解這個過程:

(1)如果樹為空,則直接插入 ?
(2)否則,查找當前的結點是否與要插入的URI有公共前綴
(3)如果沒有公共前綴,則直接插入
(4)如果有公共前綴,則判斷是否需要分裂當前的結點 ?
(5)如果需要分裂,則將公共部分作為父節點,其余的作為子節點 ?
(6)如果不需要分裂,則尋找有無前綴相同的子節點 ?
(7)如果有前綴相同的,則跳到(4) ?
(8)如果沒有前綴相同的,直接插入 ?
(9)在最后的結點,放入這條路由對應的Handle

但是到了這里,有同學要問了:怎么這里的路由,不帶參數的呀? ?

其實只要你理解了上面的過程,帶參數也是一樣的。邏輯是這樣的:在每次插入之前,會掃描當前要插入的結點的path是否帶有參數(即掃描有沒有/或者*)。如果帶有參數的話,將當前節點的wildChild屬性設置為true,然后將參數部分,設置為一個新的子節點

4 監聽

在講完了路由的注冊,我們來聊聊路由的監聽。

在上一篇文章的內容中,我們有提到這個:

type?serverHandler?struct?{
????srv?*Server
}

func?(sh?serverHandler)?ServeHTTP(rw?ResponseWriter,?req?*Request)?{
????handler?:=?sh.srv.Handler
????if?handler?==?nil?{
????????handler?=?DefaultServeMux
????}
????if?req.RequestURI?==?"*"?&&?req.Method?==?"OPTIONS"?{
????????handler?=?globalOptionsHandler{}
????}
????handler.ServeHTTP(rw,?req)
}

當時我們提到,如果我們不傳入任何的Handle方法,Golang將使用默認的DefaultServeMux方法來處理請求。而現在我們傳入了router,所以將會使用router來處理請求。

因此,router也是實現了ServeHTTP方法的。我們來看看(同樣省略了一些步驟):

func?(r?*Router)?ServeHTTP(w?http.ResponseWriter,?req?*http.Request)?{
????...
????path?:=?req.URL.Path

????if?root?:=?r.trees[req.Method];?root?!=?nil?{
????????if?handle,?ps,?tsr?:=?root.getValue(path,?r.getParams);?handle?!=?nil?{
????????????if?ps?!=?nil?{
????????????????handle(w,?req,?*ps)
????????????????r.putParams(ps)
????????????}?else?{
????????????????handle(w,?req,?nil)
????????????}
????????????return
????????}?
????}
????...
????//?Handle?404
????if?r.NotFound?!=?nil?{
????????r.NotFound.ServeHTTP(w,?req)
????}?else?{
????????http.NotFound(w,?req)
????}
}

在這里,我們選擇請求方法所對應的前綴樹,調用了getValue方法。

簡單解釋一下這個方法:在這個方法中會不斷的去匹配當前路徑與結點中的path,直到找到最后找到這個路由對應的Handle方法。

注意,在這期間,如果路由是RESTful風格的,在路由中含有參數,將會被保存在Param中,這里的Param結構如下:

type?Param?struct?{
????Key???string
????Value?string
}

如果未找到相對應的路由,則調用后面的404方法。

5 處理

到了這一步,其實和以前的內容幾乎一樣了。

在獲取了該路由對應的Handle之后,調用這個函數。

唯一和之前使用net/http包中的Handler不一樣的是,這里的Handle,封裝了從API中獲取的參數。

type?Handle?func(http.ResponseWriter,?*http.Request,?Params)

6 寫在最后

謝謝你能看到這里~

至此,httprouter介紹完畢,最關鍵的也就是前綴樹的構建了。在上面我用圖文結合的方式,模擬了一次前綴樹的構建過程,希望可以讓你理解前綴樹是怎么回事。當然,如果還有疑問,也可以留言或者在微信中與我交流~

當然,如果你不滿足于此,可以看看后面的附錄,有前綴樹的全代碼注釋

當然了,作者也是剛入門。所以,可能會有很多的疏漏。如果在閱讀的過程中,有哪些解釋不到位,或者理解出現了偏差,也請你留言指正。

再次感謝~

PS:如果覺得公眾號閱讀代碼困難,可以點擊閱讀原文去掘金閱讀。

7 源碼閱讀

7.1 樹的結構

type?node?struct?{

????path??????string????//當前結點的URI
????indices???string????//子結點的首字母
????wildChild?bool??????//子節點是否為參數結點
????nType?????nodeType??//節點類型
????priority??uint32????//權重
????children??[]*node???//子節點
????handle????Handle????//處理器
}

7.2 addRoute

func?(n?*node)?addRoute(path?string,?handle?Handle)?{

????fullPath?:=?path
????n.priority++

????//?如果這是個空樹,那么直接插入
????if?len(n.path)?==?0?&&?len(n.indices)?==?0?{

????????//這個方法其實是在n這個結點插入path,但是會處理參數
????????//詳細實現在后文會給出
????????n.insertChild(path,?fullPath,?handle)
????????n.nType?=?root
????????return
????}

????//設置一個flag
walk:
????for?{
????????//?找到當前結點path和要插入的path中最長的前綴
????????//?i為第一位不相同的下標
????????i?:=?longestCommonPrefix(path,?n.path)

????????//?此時相同的部分比這個結點記錄的path短
????????//?也就是說需要把當前的結點分裂開
????????if?i?len(n.path)?{
????????????child?:=?node{

????????????????//?把不相同的部分設置為一個切片,作為子節點
????????????????path:??????n.path[i:],
????????????????wildChild:?n.wildChild,
????????????????nType:?????static,
????????????????indices:???n.indices,
????????????????children:??n.children,
????????????????handle:????n.handle,
????????????????priority:??n.priority?-?1,
????????????}

????????????//?將新的結點作為這個結點的子節點
????????????n.children?=?[]*node{&child}
????????????//?把這個結點的首字母加入indices中
????????????//?目的是查找更快
????????????n.indices?=?string([]byte{n.path[i]})
????????????n.path?=?path[:i]
????????????n.handle?=?nil
????????????n.wildChild?=?false
????????}

????????//?此時相同的部分只占了新URI的一部分
????????//?所以把path后面不相同的部分要設置成一個新的結點
????????if?i?len(path)?{
????????????path?=?path[i:]

????????????//?此時如果n的子節點是帶參數的
????????????if?n.wildChild?{
????????????????n?=?n.children[0]
????????????????n.priority++

????????????????//?判斷是否會不合法
????????????????if?len(path)?>=?len(n.path)?&&?n.path?==?path[:len(n.path)]?&&
????????????????????n.nType?!=?catchAll?&&
????????????????????(len(n.path)?>=?len(path)?||?path[len(n.path)]?==?'/')?{
????????????????????continue?walk
????????????????}?else?{
????????????????????pathSeg?:=?path
????????????????????if?n.nType?!=?catchAll?{
????????????????????????pathSeg?=?strings.SplitN(pathSeg,?"/",?2)[0]
????????????????????}
????????????????????prefix?:=?fullPath[:strings.Index(fullPath,?pathSeg)]?+?n.path
????????????????????panic("'"?+?pathSeg?+
????????????????????????"'?in?new?path?'"?+?fullPath?+
????????????????????????"'?conflicts?with?existing?wildcard?'"?+?n.path?+
????????????????????????"'?in?existing?prefix?'"?+?prefix?+
????????????????????????"'")
????????????????}
????????????}

????????????//?把截取的path的第一位記錄下來
????????????idxc?:=?path[0]

????????????//?如果此時n的子節點是帶參數的
????????????if?n.nType?==?param?&&?idxc?==?'/'?&&?len(n.children)?==?1?{
????????????????n?=?n.children[0]
????????????????n.priority++
????????????????continue?walk
????????????}

????????????//?這一步是檢查拆分出的path,是否應該被合并入子節點中
????????????//?具體例子可看上文中的圖解
????????????//?如果是這樣的話,把這個子節點設置為n,然后開始一輪新的循環
????????????for?i,?c?:=?range?[]byte(n.indices)?{
????????????????if?c?==?idxc?{
????????????????????//?這一部分是為了把權重更高的首字符調整到前面
????????????????????i?=?n.incrementChildPrio(i)
????????????????????n?=?n.children[i]
????????????????????continue?walk
????????????????}
????????????}

????????????//?如果這個結點不用被合并
????????????if?idxc?!=?':'?&&?idxc?!=?'*'?{
????????????????//?把這個結點的首字母也加入n的indices中
????????????????n.indices?+=?string([]byte{idxc})
????????????????child?:=?&node{}
????????????????n.children?=?append(n.children,?child)
????????????????n.incrementChildPrio(len(n.indices)?-?1)
????????????????//?新建一個結點
????????????????n?=?child
????????????}
????????????//?對這個結點進行插入操作
????????????n.insertChild(path,?fullPath,?handle)
????????????return
????????}

????????//?直接插入到當前的結點
????????if?n.handle?!=?nil?{
????????????panic("a?handle?is?already?registered?for?path?'"?+?fullPath?+?"'")
????????}
????????n.handle?=?handle
????????return
????}
}

7.3 insertChild

func?(n?*node)?insertChild(path,?fullPath?string,?handle?Handle)?{
????for?{
????????//?這個方法是用來找這個path是否含有參數的
????????wildcard,?i,?valid?:=?findWildcard(path)
????????//?如果不含參數,直接跳出循環,看最后兩行
????????if?i?0?{
????????????break
????????}

????????//?條件校驗
????????if?!valid?{
????????????panic("only?one?wildcard?per?path?segment?is?allowed,?has:?'"?+
????????????????wildcard?+?"'?in?path?'"?+?fullPath?+?"'")
????????}

????????//?同樣判斷是否合法
????????if?len(wildcard)?2?{
????????????panic("wildcards?must?be?named?with?a?non-empty?name?in?path?'"?+?fullPath?+?"'")
????????}

????????if?len(n.children)?>?0?{
????????????panic("wildcard?segment?'"?+?wildcard?+
????????????????"'?conflicts?with?existing?children?in?path?'"?+?fullPath?+?"'")
????????}

????????//?如果參數的第一位是`:`,則說明這是一個參數類型
????????if?wildcard[0]?==?':'?{
????????????if?i?>?0?{
????????????????//?把當前的path設置為參數之前的那部分
????????????????n.path?=?path[:i]
????????????????//?準備把參數后面的部分作為一個新的結點
????????????????path?=?path[i:]
????????????}

????????????//然后把參數部分作為新的結點
????????????n.wildChild?=?true
????????????child?:=?&node{
????????????????nType:?param,
????????????????path:??wildcard,
????????????}
????????????n.children?=?[]*node{child}
????????????n?=?child
????????????n.priority++

????????????//?這里的意思是,path在參數后面還沒有結束
????????????if?len(wildcard)?len(path)?{
????????????????//?把參數后面那部分再分出一個結點,continue繼續處理
????????????????path?=?path[len(wildcard):]
????????????????child?:=?&node{
????????????????????priority:?1,
????????????????}
????????????????n.children?=?[]*node{child}
????????????????n?=?child
????????????????continue
????????????}

????????????//?把處理器設置進去
????????????n.handle?=?handle
????????????return

????????}?else?{?//?另外一種情況
????????????if?i+len(wildcard)?!=?len(path)?{
????????????????panic("catch-all?routes?are?only?allowed?at?the?end?of?the?path?in?path?'"?+?fullPath?+?"'")
????????????}

????????????if?len(n.path)?>?0?&&?n.path[len(n.path)-1]?==?'/'?{
????????????????panic("catch-all?conflicts?with?existing?handle?for?the?path?segment?root?in?path?'"?+?fullPath?+?"'")
????????????}

????????????//?判斷在這之前有沒有一個/
????????????i--
????????????if?path[i]?!=?'/'?{
????????????????panic("no?/?before?catch-all?in?path?'"?+?fullPath?+?"'")
????????????}

????????????n.path?=?path[:i]

????????????//?設置一個catchAll類型的子節點
????????????child?:=?&node{
????????????????wildChild:?true,
????????????????nType:?????catchAll,
????????????}
????????????n.children?=?[]*node{child}
????????????n.indices?=?string('/')
????????????n?=?child
????????????n.priority++

????????????//?把后面的參數部分設置為新節點
????????????child?=?&node{
????????????????path:?????path[i:],
????????????????nType:????catchAll,
????????????????handle:???handle,
????????????????priority:?1,
????????????}
????????????n.children?=?[]*node{child}

????????????return
????????}
????}

????//?對應最開頭的部分,如果這個path里面沒有參數,直接設置
????n.path?=?path
????n.handle?=?handle
}


最關鍵的幾個方法到這里就全部結束啦,先給看到這里的你鼓個掌!

這一部分理解會比較難,可能需要多看幾遍。

如果還是有難以理解的地方,歡迎留言交流~

PS:如果覺得公眾號閱讀代碼困難,可以點擊閱讀原文去掘金閱讀。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/445969.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/445969.shtml
英文地址,請注明出處:http://en.pswp.cn/news/445969.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

FFmpeg源代碼簡單分析-通用-avcodec_open2()

參考鏈接 FFmpeg源代碼簡單分析:avcodec_open2()_雷霄驊的博客-CSDN博客 avcodec_open2() 該函數用于初始化一個音視頻編解碼器的AVCodecContextavcodec_open2()的聲明位于libavcodec\avcodec.h,如下所示。 /*** Initialize the AVCodecContext to use…

統計MySQL中某數據庫硬盤占用量大小

放碼過來 select TABLE_NAME, concat(truncate(data_length/1024/1024,2), MB) as data_size, concat(truncate(index_length/1024/1024,2), MB) as index_size from information_schema.tables where TABLE_SCHEMA your_db_name order by data_length desc;運行結果 參考…

halcon 相似度_Halcon分類函數,shape模型

《zw版Halcon-delphi系列原創教程》 Halcon分類函數013,shape模型為方便閱讀,在不影響說明的前提下,筆者對函數進行了簡化::: 用符號“**”,替換:“procedure”:: 用大寫字母“X”,替換:“IHUnt…

用Python將文件夾打包成Zip并備份至U盤

需求概要 將maven工程打包并備份至U盤。為了簡單起見,只需備份工程中的src文件夾和pom.xml文件即可。 放碼過來 import os import zipfile import datetime import shutilnowTimeStr datetime.datetime.now().strftime("%Y%m%d%H%M") newZipFileName …

FFmpeg源代碼簡單分析-通用-avcodec_close()

參考鏈接 FFmpeg源代碼簡單分析:avcodec_close()_雷霄驊的博客-CSDN博客_avcodec_close avcodec_close() 該函數用于關閉編碼器avcodec_close()函數的聲明位于libavcodec\avcodec.h,如下所示。 ?該函數只有一個參數,就是需要關閉的編碼器的…

redis 緩存過期默認時間_redis緩存過期機制

筆者在線上使用redis緩存的時候發現即使某些查詢已經設置了無過期時間的緩存,但是查詢仍然非常耗時。經過排查,發現緩存確實已經不存在,導致了緩存擊穿,查詢直接訪問了mysql數據庫。因為我們用的是公司公共的redis緩存服務器,在和運維人員交流后發現可能是redis的內存淘汰…

FFmpeg源代碼簡單分析-解碼-打開媒體的函數avformat_open_input

參考鏈接 圖解FFMPEG打開媒體的函數avformat_open_input_雷霄驊的博客-CSDN博客_avformat_open_input 使用FFmpeg源代碼簡單分析:avformat_open_input()_雷霄驊的博客-CSDN博客_avformat_open_input() avformat_open_input FFmpeg打開媒體的的過程開始于avformat_…

redis session java獲取attribute_面試題:給我說說你能想到幾種分布式session實現?...

作者:yanglbme 來源:https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/distributed-session.md# 面試官心理分析面試官問了你一堆 dubbo 是怎么玩兒的,你會玩兒 dubbo 就可以把單塊系統弄成分布式系統&#xff0…

Projection投影

解釋一 Projection means choosing which columns (or expressions) the query shall return. Selection means which rows are to be returned. if the query is select a, b, c from foobar where x3;then “a, b, c” is the projection part, “where x3” the selecti…

FFmpeg源代碼簡單分析-解碼-avformat_find_stream_info()

參考鏈接 FFmpeg源代碼簡單分析:avformat_find_stream_info()_雷霄驊的博客-CSDN博客_avformat_find_stream_info avformat_find_stream_info() ?該函數可以讀取一部分視音頻數據并且獲得一些相關的信息avformat_find_stream_info()的聲明位于libavformat\avform…

Tail Recursion尾遞歸

什么是尾遞歸 Tail Recursion /te?l r??k??r?n/ In traditional recursion, the typical model is that you perform your recursive calls first, and then you take the return value of the recursive call and calculate the result. In this manner, you don’t g…

python遞歸算法案例教案_python教案

第五單元進階程序設計(總10課時)第一節選擇編程語言(1課時)一、教學目標1、了解程序設計語言和兩種翻譯方式;2、了解Python背景、功能、安裝,熟悉Python編程環境;3、編程初體驗。體驗一個小程序從建立、輸入、調試、運行、保存的全過程。掌握…

FFmpeg源代碼簡單分析-解碼-av_read_frame()

參考鏈接 ffmpeg 源代碼簡單分析 : av_read_frame()_雷霄驊的博客-CSDN博客_ffmpeg frame av_read_frame() ffmpeg中的av_read_frame()的作用是讀取碼流中的音頻若干幀或者視頻一幀。例如,解碼視頻的時候,每解碼一個視頻幀,需要…

數據庫 流量切分_私域流量之社群運營技巧,社群運營技巧解析

一、明白社群運營的目的1、社群的目的確立任何一個社群(組織)成立的時分,都是承載著一定的目的的,這個目的就像是北極星一樣,指引著我們的方向。確立運營目的的過程,也是在尋覓北極星的過程。社群運營屬于觸達用戶的一種方式&…

用Python在Tomcat成功啟動后自動打開瀏覽器訪問Web應用

前提條件 WindowsPython 2.7需設置CATALINA_HOME環境變量 放碼過來 # -*- coding: utf-8 -* import os import time import subprocesstomcatStartFilePath C:\\tomcat\\apache-tomcat-7.0.90-windows-x64\\apache-tomcat-7.0.90\\bin\\startup.bat browserPath C:\\Users…

FFmpeg源代碼簡單分析-解碼-avcodec_send_packet 和 avcodec_receive_frame 替代 avcodec_decode_video2

參考鏈接 ffmpeg 源代碼簡單分析 : avcodec_decode_video2()_雷霄驊的博客-CSDN博客_avcodec_decode_video2 avcodec_decode_video2 ffmpeg中的avcodec_decode_video2()的作用是解碼一幀視頻數據。輸入一個壓縮編碼的結構體AVPacket,輸出一個解碼后的結…

FFmpeg源代碼簡單分析-解碼-avformat_close_input()

參考鏈接 FFmpeg源代碼簡單分析:avformat_close_input()_雷霄驊的博客-CSDN博客_avformat_close_input avformat_close_input() 本文簡單分析FFmpeg的avformat_close_input()函數。該函數用于關閉一個AVFormatContext,一般情況下是和avformat_open_inp…

android 使用shell模擬觸屏_[Android]通過adb shell input上報命令模擬屏幕點擊事件【轉】...

常用的 input上報命令:input text 1234 實際向界面注入1234文字,有輸入框,能明顯看到效果input keyevent 4 鍵盤事件,4 為返回input tap 100 300 單擊觸屏事件 ,模擬點擊x100 y 300 位置input swipe 100 300 500 300 …

用Python連接MySQL并進行CRUD

Tag: MySQL, PyMySQL, Python 準備條件 Python 2.7MySQL 5.5安裝 PyMySQL pip install PyMySQL 放碼過來 創建一數據表 CREATE TABLE users (id int(11) NOT NULL AUTO_INCREMENT,email varchar(255) COLLATE utf8_bin NOT NULL,password varchar(255) COLLATE utf8_bin N…

python網絡爬蟲的方法有幾種_Python網絡爬蟲過程中5種網頁去重方法簡要介紹

一般的,我們想抓取一個網站所有的URL,首先通過起始URL,之后通過網絡爬蟲提取出該網頁中所有的URL鏈接,之后再對提取出來的每個URL進行爬取,提取出各個網頁中的新一輪URL,以此類推。整體的感覺就是自上而下進…