這三款國產小型SUV都帶全景攝像頭,最低只要7萬多

動力總成上,S3有1。5L與1。6L兩款發動機可選,前者最大輸出113馬力和146牛米,後者最大輸出120馬力和150牛米,與它們匹配的有6擋手動和CVT變速箱。內飾上以簡潔為主,輔以一些銀色和碳纖維裝飾條來襯托,更多的橫向設計,看起來很舒展。

新手上路,最怕的是什麼?當然是怕去到一些人多路窄的地方來個倒車入庫或者側方停車,既要瞻前,又要顧后,還要控制油門剎車,簡直就是影分身都忙不過來。

不過,隨着汽車工業的發展,有一項高級實用配置開始漸漸下放到一些10萬元以下的車,那就是全景攝像頭。只要有了這個東西,倒車就從容許多了。因此,編者我就給大家推薦三款10萬內帶全景攝像頭的國產小型SUV。

凱翼汽車-凱翼X3

指導價:6.66-9.69萬

帶全景攝像頭的車型:7.99萬的手動智聯IV版、8.69萬的手動智聯V版、8.99萬的CVT智聯IV版和CVT智聯V版

編者點評:整個圖像的分辨率還可以,四個角落的拼接自然,這樣的價位確實很難再挑剔什麼。除此以外,這四個攝像頭還能充當行車記錄儀,讓各種碰瓷黨無處可逃。最方便的是,可以在屏幕上選擇錄像重放,讓碰瓷黨當即現形。這個功能,即便在不少豪車上也是沒有的,能出現在一輛國產小型SUV上,確實逆天了!

動力方面,凱翼X3用的是1.6L發動機,與之匹配的是5擋手動和CVT變速箱,最大輸出126馬力和160牛米。內飾以黑色為主,各種設計都是一板一眼,雖沒新奇的地方,但勝在簡約耐看。

江淮汽車-瑞風S3

指導價:6.58-9.58萬

帶全景攝像頭的車型:8.58萬的手動智能互聯型和9.58萬的CVT智能互聯型

編者點評:S3的圖像显示比凱翼X3要稍差,而且銜接處的畸變有點大,不夠自然。但這兩款車型都帶前後駐車雷達,所以結合著來使用,各種停車還是游刃有餘的。

動力總成上,S3有1.5L與1.6L兩款發動機可選,前者最大輸出113馬力和146牛米,後者最大輸出120馬力和150牛米,與它們匹配的有6擋手動和CVT變速箱。內飾上以簡潔為主,輔以一些銀色和碳纖維裝飾條來襯托,更多的橫向設計,看起來很舒展。

比亞迪-元

指導價:5.99-24.98萬

帶全景攝像頭的車型:8.59萬的手動尊榮型和9.39萬的自動豪雅型

編者點評:元的圖像四角拼接自然,但其他方面有較明顯的畸變。還好的是這兩款車型同樣具有前後駐車雷達。

動力方面,元有1.5L和1.5T發動機可選,1.5L發動機最大輸出109馬力和145牛米,而1.5T則是154馬力和240牛米。其中,與1.5L發動機匹配的有5擋手動和6擋雙離合變速箱,而1.5T只有6擋雙離合變速箱。

一眼望去元的內飾,會有種逼死處女座的感覺,特別是座椅上那條橙色的橫條,更像是為了設計而設計的。中控兩側凸起的銀色裝飾條顯得並不友好,甚至有點突兀。兩側旋風式的出風口倒是挺時尚。

編者總結

全景攝像頭曾經只在豪車上會有配置,不過隨着國產車的雄起,已經越來越多車型將其納入自身的“軍備庫”中,提升競爭力。誠然在一些車型上的精度依舊不高,但是有總好過沒有,而且隨着時間的推移,全景攝像肯定會被設置得更加清晰,同時價格還有望進一步下探。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

15萬買車要最個性最實用 恐怕只能選擇這三輛!

東風日產-啟辰T90指導價:10。98-15。48萬說到個性,如今關注度極高的啟辰T90是不能落下的,它定位為中型SUV,採用了與寶馬、奔馳、謳歌等豪華品牌類似的轎跑式SUV設計風格。2765mm的軸距、4793mm的車身長度讓它的側面造型修長而且非常動感。

現在的車型除了配置高、動力強以外也越來越講究功能性。關於功能性好與不好,其實可以簡單理解為這些車型能不能“裝”、能不能適應各種道路狀況!

“裝”當然是指車輛的裝載能力,實際體現如三廂轎車很難裝載一個冰箱或洗衣機!而大兩廂車型或SUV就能輕鬆裝載。

(還記得之前爆火的“後置發動雞、雙渦輪增鴨”嗎?)

能不能適應各種道路狀況就體現在車輛的離地間隙是否夠高,它能防止托底的情況出現,還要有濾振到位的懸架,走爛路時車內的乘客保持舒適的體驗!

當然,現在買車還要求外觀漂亮,最好要足夠個性!還有更重要的是“逼格”!

今天我們就來看看15萬的預算該買哪些兼顧個性、實用性的車型。

一汽-大眾-蔚領

指導價:12.59-16.29萬

用實用性兼顧個性來形容大眾蔚領一點不為過,它旅行車的車身結構讓車尾的造型異常獨特,同時這樣的車身設計也帶來了極其實用的後備廂空間。

蔚領的後備廂容積為589L,放倒後排座椅后儲物空間能擴展為1507L!雖然蔚領的軸距為2614mm,與傑德的2760mm軸距有較大差異,但是儲物空間依然表現不錯。

另外,蔚領還標配了全景天窗、車頂行李架、定速巡航、後排座椅比例放倒、胎壓監測、ESp等與長途自駕旅行相關的配置,可以說是比較厚道的!

編者點評:

蔚領的造型時尚也不失穩重,雖然車身尺寸比傑德要小,但是它配置更厚道、價格稍低,在性價比方面也有不錯的表現!

東風日產-啟辰T90

指導價:10.98-15.48萬

說到個性,如今關注度極高的啟辰T90是不能落下的,它定位為中型SUV,採用了與寶馬、奔馳、謳歌等豪華品牌類似的轎跑式SUV設計風格。2765mm的軸距、4793mm的車身長度讓它的側面造型修長而且非常動感!

而在動力方面,它搭載一款代號為MR20的2.0L自然吸氣發動機,搭配6擋手動/CVT無級變速箱,在動力方面固然無法跟售價數倍於它的豪華品牌SUV相提並論,但是啟辰T90駕駛起來動力充足,平順性很不錯,所以也適合家用。

編者點評:

啟辰T90的定位獨特,而且售價比較接地氣,這導致在同價位中基本找不到直接的競爭對手!它除了外觀個性外,在做工、用料方面都表現不錯,值得大家多多考慮。

眾泰汽車-眾泰SR9

指導價:10.88-16.18萬

在這個看臉的時代,一個帥氣的外觀是必不可少的。眾泰SR9的外觀設計不用我多說,非常運動時尚,也給你很保時捷的感覺,而至於這樣的造型大家是否能接受呢?

眾泰SR9的車身長寬高為4744*1929*1647mm,軸距達2850mm,定位是中型SUV,這樣的車身尺寸跟本田冠道有得一拼。

它搭載一款來自三菱的2.0T渦輪增壓發動機,最大功率190馬力、峰值扭矩250牛米/2400-4800轉,動力參數在同級別車型裏面屬於中等水平,匹配6速濕式雙離合變速箱或5擋手動變速箱。

編者點評:

現在關於眾泰SR9的爭議不少,不過正是由於這些爭議,讓SR9的關注度一直居高不下,而之前推出的SR7也有着不錯的銷量。綜合來看,眾泰SR9的空間、配置、性價比表現不錯,在SUV車型銷量火爆的前提下,它有可能成為市面上悶聲發大財的車型之一!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

這款美系SUV底盤質感和操控都不錯 而且只賣9萬出頭!

底盤有高級感創酷的后懸架用的是複合扭力梁式非獨立懸架,調校出來很有厚實感。在濾震的時,總能很巧妙地化解,傳進車廂內的不過是一股很柔和的力。不過,礙於車身偏高,在過急彎時的側傾有點大。乘坐表現中規中矩創酷的後排座椅靠背較直,且包裹性一般。

說起美系SUV,不得不提昂科拉與創酷。雖然它們都沒有美國車的特性,但卻是極具中國特色的美系車。下面編者我就講講定位稍低一點的創酷。

上汽通用雪佛蘭-創酷

指導價:9.99-14.99萬

外觀時尚精緻

創酷的車身尺寸為4255*1776*1675mm,軸距為2555mm。前臉設計就像瘦身成功的胖子,展現出一些美系的肌肉感。鍍鉻裝飾條的使用不多,但圍着中網和霧燈區的設計,顯得恰到好處。

側面很簡潔,只是在門板下方到后翼子板處隱約露出了一條“肌肉線”。

后擋風玻璃的面積偏小,三段式的尾燈設計很有層次感,隱藏式的排氣更顯高級。

內飾設計簡約為主

內飾以棕色和黑色為主色調,中控檯面板上下方的波浪形設計很時尚。烤黑鋼琴漆的显示屏面板凸顯高級,銀色飾條將其與中央出風口圍起來,很有整體性。空調旋鈕區簡潔明了,同樣的設計還有方向盤。方向盤僅右側有少量按鈕,看上去讓人很舒服。

動力輸出輕快

創酷只有1.4T發動機可選,與之匹配的是6擋手動和6擋手自一體變速箱。最大輸出143馬力和205牛米,最大扭矩在1800轉開始爆發,能維持到4800轉,持續時間足夠長。

在市區中遊走,車子感覺很輕快,不會有乏力的情況。加速時,也能很好地執行動作。這副6AT在急加速時,能感覺到一絲絲闖動。這種感覺很輕微,乘客是感覺不到的,只有細心的駕駛者才會發現。

底盤有高級感

創酷的后懸架用的是複合扭力梁式非獨立懸架,調校出來很有厚實感。在濾震的時,總能很巧妙地化解,傳進車廂內的不過是一股很柔和的力。不過,礙於車身偏高,在過急彎時的側傾有點大。

乘坐表現中規中矩

創酷的後排座椅靠背較直,且包裹性一般。不過座墊的長度對於中等身材的人來說很充足,且腿部和頭部都能有很好的空間。中央地板有一定凸起,對中間乘客的舒適型有不少影響。

油耗表現與保養費用

多位車主反映手動擋創酷的百公里綜合油耗為7.1L,而自動擋為8.6L。

保養費用方面,無論是手動擋還是自動擋,六萬公里的總保養費用均為9355元。這是因為自動擋的變速箱油要到8萬公里才更換。這個費用相比同級其他對手來說,是偏高了一點。正常來說,應該在7000元左右。

配置分析

對於創酷這款車,自動擋車型來說,我會推薦買指導價為13.39萬的自動兩驅豪華型。儘管其比自動擋的最低配車型貴了14000元,但是多出了不少實用且增添高級感的配置,具體可看下錶。

至於手動擋車型,我建議直接買指導價9.99萬的最低配車型。因為手動擋只有兩個車型可選,更高一級的車型要多花1萬元且是2016款,配置上也沒多出很多。

編者總結

創酷這台車其實內在與昂科拉是基本一致的,但是卻比昂科拉賣得要更便宜。在机械質感與底盤舒適性上都表現得很不錯,僅僅是保養費用和空間上會稍遜同級對手一點。如果你就想要一台美系SUV,創酷會是一個極具性價比的選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

油耗高達6.1L 這款混合動力車不便宜點怎麼行

(使用2。5L發動機的凱美瑞雙擎工信部油耗也高達5。3升/100公里)。其它方面表現如何。介紹完動力之後,咱們再看看全新起亞K7的其它方面,外觀上比老款K7更加帥氣動感,精緻感要更加好,看起來更加像一款平民豪車。K7的尺寸為4970mm*1869mm*1470mm。

各位朋友們,臨近年關,不知道現在你們還有沒有心情工作呢?

廢話不說那麼多,今天小編美美又來給各位介紹新車啦,這一款新車就是起亞K7混合動力版。

有的朋友可能對起亞K7非常的陌生,起亞在我們的印象中比較熟悉的車型就是智跑、K5、K3這一類的,其實起亞K7是起亞K系列的一款高端中級轎車。他的中文名叫做起亞凱尊,起亞凱尊是起亞的一款定位B+級的中級轎車產品,採用全進口的銷售方式,售價為24.3萬~32.46萬,雖然是進口身份,但是它24.3萬元的起價已經比定位中大型車的福特金牛座還要更加貴,所以在國內賣的不好也是意料之中。

現款起亞凱尊↑↑↑

但是這並不妨礙我們看一下起亞的這個混動軍團的又一款產品起亞K7混動的實力。

新一代起亞K7於2016年1月在韓國亮相,至今正好一年的時間,而一年後的現在起亞發布了K7混動的消息,K7混動採用2.4升的自然吸氣發動機加38千瓦的電動機的動力總成。最大功率為157馬力,最大扭矩為206牛米。鋰電池的電池容量為1.76kwh。官方公布的百公里綜合油耗為6.1L/100km。

現款凱尊的2.4L發動機↑↑↑

目前關於這款2.4L的發動機並沒有太多的消息,是否是全新的發動機也是未知,但是參考其157馬力/206牛米的動力輸出來看,這樣的輸出水平比2013款凱尊2.4L發動機都要低了不少(2013款凱尊國五:173馬力/224牛米),因此小編猜測這是一台新發動機,並且採用了混動車型多半喜歡的阿特金森循環(這種發動機動力弱但是油耗表現優秀)。

而38千瓦(52馬力)電機和現款起亞K5混動的電機功率相同,因此電機可能是和現款K5採用相同的38千瓦(52馬力)/205牛米輸出的電機。因此發動機和電動機的總輸出功率可能為225馬力/429牛米。

為什麼綜合油耗偏高?

相比較同級別的雅閣混動、君越混動來說,K7的6.1升的綜合油耗是偏高的表現了,君越混動採用1.8L發動機+114千瓦電機的動力系統,工信部油耗為4.7升/100公里,雅閣採用2.0L發動機+135千瓦電機的動力系統,工信部油耗為4.2升/100公里。

那為什麼會有偏高的油耗呢?由於K7電機的功率偏弱,因此電機主要的工作區間為低速區間,因此需要使用大排量發動機來彌補中段加速的動力,因此K7使用了2.4L的發動機,較大的發動機排量也會導致油耗偏高。(使用2.5L發動機的凱美瑞雙擎工信部油耗也高達5.3升/100公里)。

其它方面表現如何?

介紹完動力之後,咱們再看看全新起亞K7的其它方面,外觀上比老款K7更加帥氣動感,精緻感要更加好,看起來更加像一款平民豪車。K7的尺寸為4970mm*1869mm*1470mm。軸距為2855mm,這樣的尺寸其實已經是逼近一台中大型轎車了。

內飾上K7也是以穩重設計為主,畢竟是起亞的高端價位的車型,現款K7的內飾做工質感在同價位就是比較出色的了,因此對於全新的K7的內飾質感小編也絲毫不擔心。而且得益於較大的車身尺寸,全新K7的內部空間貌似還比較寬敞。

動力上,除了混動的K7之外,還會有2.0T和2.4L發動機的車型,在美國市場上K7還有6缸3.3L發動機,最大功率290馬力,並且匹配8AT變速箱,不過不用想了,即使3.3L車型來到國內,受限於稅率相信也沒多少人會買的。

售價多少?

回到混動K7上,看完了混動K7的數據之後相信大家會好奇,這款起亞K7混動會賣多少錢呢?綜合目前國產的起亞K5混動售價為19.98萬-24.88萬,比K5更加高級並且採用進口身份的K7售價也會更加高,但是在前不久上市的起亞極睿卻給我們帶來了極具震撼的性價比;因此作為起亞的混動新生代車型,K7混動價格預計也不會貴到離譜,因此小編猜測K7混動的價格會在23.98萬左右,你們覺得這個價格如何呢?

競爭對手:

本田雅閣混動

23.98-27.98萬

相比起亞來說本田的品牌在中國的認可度更加高,而更加低的綜合油耗以及技術上的優勢都是雅閣混動的優勢所在,也是K7混動要面對的。

起亞K5混動

19.98萬-24.88萬

為什麼說K5混動是K7混動的對手呢?在許多人看來,K5和K7同為中級車,但是K7要貴得多,因此選擇K5也就更加划算了,而對於選擇K7混動的人來說,為什麼不選擇一個同樣大空間低油耗的中級車K5呢?

別克君越混動

27.58-30.58萬

國人對於別克的喜愛是十分明顯的,君越高級的底盤質感以及配置上的優勢都是同價位比較明顯的,而君越混動的油耗也比K7混動更加低,品質感也絲毫不遜色於K7混動,但是君越混動的價格偏高也是一個問題,如果K7混動也定這個價的話,那麼它倆就可以正面廝殺了。

總結:

其實無論是對於哪個品牌來說,受限於日益嚴格的排放法規,混動都是接下來的主戰場,而從起亞極睿的上市來看,技術偏弱的起亞選擇了爭取時間優勢,率先以低價入市的方式佔領市場打開口碑,之後有了良好的市場基礎,也就不用懼怕其它車型的競爭,因此低價入市的戰略應該是起亞的主要思路,這個角度來看,起亞K7的價格還是十分值得期待的。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

【實戰】基於OpenCV的水表字符識別(OCR)

目錄

  • 1. USB攝像頭取圖
  • 2. 圖像預處理:獲取屏幕ROI
    • 2.1. 分離提取屏幕區域
    • 2.2. 計算屏幕區域的旋轉角度
    • 2.3. 裁剪屏幕區域
    • 2.4. 旋轉圖像至正向視角
    • 2.5. 提取文字圖像
    • 2.6. 封裝上述過程
  • 3. 字符分割,獲取單個字符的圖像
  • 4. 模板匹配:確定字符內容
    • 4.1. make_template
    • 4.2. 模板修復
    • 4.3. 重新加載模板數據
    • 4.4. 模板匹配

1. USB攝像頭取圖

由於分辨率越高,處理的像素就越多,導致分析圖像的時間變長,這裏,我們設定攝像頭的取圖像素為(240,320):

cap = cv2.VideoCapture(0)  # 根據電腦連接的情況填入攝像頭序號
assert cap.isOpened()

# 以下設置显示屏的寬高
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('M', 'J', 'P', 'G'))

這裏提幾個常用的標準分辨率:

  • VGA (Video Graphics Array): 640×480
  • QVGA (QuarterVGA): 240×320
  • QQVGA: 120×160

接下來可以捕獲一幀數據看一下狀態:

# %% 捕獲一幀清晰的圖像
def try_frame():
    while True:
        ret, im_frame = cap.read()
        cv2.imshow("frame", im_frame)  # 显示圖像

        # im_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 可選擇轉換為灰度圖
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cv2.destroyAllWindows()
    return im_frame

im_frame = try_frame()
env.imshow(im_frame)

ps: 鏡頭角度會存在一定的歪斜,沒有關係,我們後面會進行處理。

2. 圖像預處理:獲取屏幕ROI

利用屏幕的亮度,通過簡單的閾值操作和輪廓操作,獲取屏幕輪廓,然後將圖像角度校正,最後獲得正向的文字內容。

2.1. 分離提取屏幕區域

通過OTSU的閾值化操作,將圖像處理為二值狀態。這個很重要,因為如果直接使用彩圖或灰度圖,會由於外部光線的變化,導致後期字符匹配時整體灰度值與模板的差別而降低置信度,導致較大的誤差。而二值圖可以避免這個問題。

然後利用開運算(白底黑字,如果黑底白字則為閉運算),消除噪點。

im_latest = try_frame()
im_gray = mvlib.color.rgb2gray(image)
im_bin = mvlib.filters.threshold(im_gray, invert=False)
# im_erosion = mvlib.morphology.erosion(im_bin, (11, 11))
# im_dilation = mvlib.morphology.dilation(im_erosion, (5, 5))
im_opening = mvlib.morphology.opening(im_bin, (11, 11))
env.imshow(im_opening)

2.2. 計算屏幕區域的旋轉角度

提取圖像的最大輪廓,然後獲取其包絡矩形。

list_cnts = mvlib.contours.find_cnts(im_opening)
if len(list_cnts) != 1:
    print(f"非唯一輪廓,請通過面積篩選過濾")
    # assert 0
    cnts_sorted = mvlib.contours.cnts_sort(list_cnts, mvlib.contours.cnt_area)
    list_cnts = [cnts_sorted[0]]

box, results = mvlib.contours.approx_rect(list_cnts[0], True)
angle = results[2]  # 此處的角度是向逆時針傾斜,記作:-4
if abs(angle) > 45:
    angle = (angle + 45) % 90 - 45
print(angle, box)

上述過程輸出:

1.432098388671875
[[282 173]
 [ 29 167]
 [ 32  41]
 [285  47]]

2.3. 裁剪屏幕區域

至此可以丟棄im_opening以及im_bin的圖像了。我們重新回到im_gray上進行操作(需要重新進行閾值化以獲取文字的二值圖)。

list_width = box[:,0]
list_height= box[:,1]
w_min, w_max = min(list_width), max(list_width)
h_min, h_max = min(list_height), max(list_height)

im_screen = im_gray[h_min:h_max, w_min:w_max]
env.imshow(im_screen)

2.4. 旋轉圖像至正向視角

im_screen_orthogonal = mvlib.transform.rotate(im_screen, angle, False)
# env.imshow(im_screen_orthogonal)
im_screen_core = im_screen_orthogonal[20:-20, 20:-20]
env.imshow(im_screen_core)

2.5. 提取文字圖像

第二次執行閾值化操作,但這一次是在屏幕內部,排除了屏幕外複雜的背景后,可以很容易的獲取到文字的內容。由於我們只關心数字,所以通過閉運算將細體字過濾掉。

im_core_bin = mvlib.filters.threshold(im_screen_core, invert=False)
im_closing = mvlib.morphology.closing(im_core_bin, (3,3))
env.imshow(im_closing)

2.6. 封裝上述過程

瑣碎的預處理過程就告一段落了,我們可以將上述的內容封裝成一個簡單的函數:

def preprocess():
    # 獲取屏幕區域
    im_latest = try_frame()
    ...
    im_closing = mvlib.morphology.closing(im_core_bin, (3,3))
    return im_closing

3. 字符分割,獲取單個字符的圖像

字符分割,一方面是製作模板的需要(當然,你也可以直接用畫圖工具裁剪出一張模板圖像);另一方面是為了加速模板匹配的效率。當然,你完全可以在整張圖像上利用 match_template() 查找模板,但如果進行多模板匹配,重複的掃描整張圖像,效率就大打折扣了。

先提供完整的代碼

char_width_min = 7
gap_height_max = 5

def segment_chars(im_core):
    list_char_img = []
    # 字符區域
    raw_bkg = np.all(im_core, axis=0)
    col_bkg = np.all(im_core, axis=1)

    # 計算字高
    ndarr_char_height = np.where(False == col_bkg)[0]
    char_height_start = ndarr_char_height[0]
    item_last = ndarr_char_height[0]
    for item in ndarr_char_height:
        if item - item_last > gap_height_max:
            char_height_start = item
        item_last = item
    char_height_end = ndarr_char_height[-1] +1
    print(f"字高【{char_height_end - char_height_start}】")

    ndarr_chars_pos = np.where(False == raw_bkg)[0]
    ndarr_chars_pos = np.append(ndarr_chars_pos,
                                im_core.shape[1] + char_width_min)

    last_idx = ndarr_chars_pos[0]
    curr_char_width = 1
    for curr_idx in ndarr_chars_pos:
        idx_diff = curr_idx - last_idx
        # 這裏應該限制最小寬度>=2,否則認為是一個粘連字
        if idx_diff <= 2:
            curr_char_width += idx_diff
        else:  # 新的字符
            char_width_end = last_idx +1
            char_width_start = char_width_end - curr_char_width
            im_char_last = im_core[char_height_start:char_height_end,
                                char_width_start:char_width_end]
            list_char_img.append(im_char_last)
            curr_char_width = 0
        last_idx = curr_idx
    return list_char_img

按照行列,獲取圖像中的文字像素點集:

raw_bkg = np.all(im_core, axis=0)
col_bkg = np.all(im_core, axis=1)

由此,可以知道255(黑色)的區域從大約 39 到 75,那麼 75 - 29 = 36 就是字高。

另外,圖像中有可能存在噪點,去掉就是了(我這裏只是簡單粗暴的處理下,請見諒)。

行的處理同樣。如果發現間隔,那麼就可以分離字符。最後,輸出每個字符的圖像。

檢驗下效果:

list_char_imgs = segment_chars(im_core)
env.imshow(list_char_imgs[1])

4. 模板匹配:確定字符內容

利用模板匹配,實現字符識別的過程。這裏不再細說OpenCV的 cv2.matchTemplate() 函數,只描述應用過程。

4.1. make_template

首先,有必要把字符先作為模板存儲下來。

def make_tpls(list_tpl_imgs, dir_save, dict_tpl=None):
    if not dict_tpl:
        dict_tpl = {}

    str_items = input("請輸入模板上的文本內容,用於校對(例如215801): ")

    assert len(str_items) == len(list_tpl_imgs)
    for i, v in enumerate(str_items):
        filename = v
        if v in dict_tpl:
            filename = v + "_" + str(random.random())
        else:
            dict_tpl[v] = list_tpl_imgs[i]
        path_save = os.path.join(dir_save, filename + ".jpg")
        mvlib.io.imsave(path_save, list_tpl_imgs[i])

    return dict_tpl

這裏,同一字符有必要多存儲幾張,最後擇優(或者一個字符通過多個模板匹配的結果來確定)。

4.2. 模板修復

這個過程,雖然沒啥子技術含量,但卻對結果影響很大。在前一步驟中,我們每一個字符都收集了多張模板圖像。現在,從中擇優錄取。還有,可以手動編輯模板的圖片,去除模板多餘的白邊(邊並不是文字內容的一部分,而且會降低字符的匹配度)。

4.3. 重新加載模板數據

def load_saved_tpls(dir_tpl):
    saved_tpls = os.listdir(dir_tpl)

    dict_tpl = {}  # {"1": imread("mvdev/tmp/tpl/1.jpg"), ...}
    for i in saved_tpls:
        filename = os.path.splitext(i)[0]
        path_tpl = os.path.join(dir_tpl, i)

        im_rgb = cv2.imread(path_tpl)
        im_gray = mvlib.color.rgb2gray(im_rgb)
        dict_tpl[filename] = im_gray
    return dict_tpl

dir_tpl = "tpl/"
dict_tpls = load_saved_tpls(dir_tpl)

4.4. 模板匹配

def number_ocr_matching(im_char):
    most_likely = [1, ""]
    for key, im_tpl in dict_tpls.items():
        try:
            pos, similarity = mvlib.feature.match_template(im_char, im_tpl, way="most")
            if similarity < most_likely[0]:
                most_likely = [similarity, key]
        except:
            im_char_old = im_char.copy()
            h = max(im_char.shape[0], im_tpl.shape[0])
            w = max(im_char.shape[1], im_tpl.shape[1])
            im_char = np.ones((h,w), dtype="uint8") * 255
            # im_char2 = mvlib.pixel.bitwise_and(z, im_char)
            im_char[:im_char_old.shape[0], :im_char_old.shape[1]] = im_char_old

            pos, similarity = mvlib.feature.match_template(im_char, im_tpl, way="most")
            if similarity < most_likely[0]:
                most_likely = [similarity, key]

    print(f"字符識別為【{most_likely[1]}】相似度【{most_likely[0]}】")
    return most_likely[1]

def application(list_char_imgs):
    str_ocr = ""
    for im_char in list_char_imgs:
        width_img = im_char.shape[1]
        # 判斷字符
        match_char = number_ocr_matching(im_char)
        str_ocr += match_char
    return str_ocr

str_ocr2 = application(list_char_imgs)
print(str_ocr2)

過程中,opencv出現了報錯,是由於模板的shape大於當前分割字符的shape。這個很正常,採集圖像時由於距離的微調(注意,距離變化不能太大,OpenCV的默認算子不支持模板縮放)可能導致字符尺寸更小。解決方案也很簡單,直接把字符圖像拓展到大於模板的狀態就OK了。

額,忘了刪除debug信息了……再來一次~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

Nginx 的變量究竟是怎麼一回事?

之前說了很多關於 Nginx 模塊的內容,還有一部分非常重要的內容,那就是 Nginx 的變量。變量在 Nginx 中可以說無處不在,認識了解這些變量的作用和原理同樣是必要的,下面幾乎囊括了關於 Nginx 的所有變量,單獨看起來可能比較枯燥,放心,後面依然有實戰內容。

Nginx 變量的運行原理

圍繞 Nginx 中的變量模塊可以分為兩類,一類是提供變量的模塊,另外一類是使用變量的模塊。

  • 提供變量的模塊
    • 在 Preconfiguration 源代碼中定義變量名以及可以解析出變量的方法
  • 使用變量的模塊
    • 解析 nginx.conf 時定義變量的使用方式

也就是在 Nginx 啟動時,已經定義了變量,而只有當真正處理請求的時候,才會根據 nginx.conf 解析出來的變量使用方式調用 Preconfiguration 中定義的方法來實際獲取值。

這也是變量的兩個特性:

  • 惰性求值:只有使用的時候才會去調方法解析
  • 變量值可以時刻變化,其值為使用的那一時刻的值。例如發送響應包體字節數,實際在發送的過程中是一直在變化的。

除了 Nginx 的模塊之外,Nginx 框架也包含許多的變量,這些變量不需要通過編譯模塊來引入,而且,Nginx 框架所提供的變量往往反映了處理請求的細節,因此,了解 Nginx 框架所提供的變量是十分有必要的。

HTTP 請求相關的變量

先來看一下關於 HTTP 請求的相關變量。

  • arg_參數名:URL 中某個具體參數的值

  • query_string:與 args 變量完全相同

  • args:全部 URL 參數

  • is_args:如果請求 URL 中有參數則返回 ?,否則返回空

  • content_length:HTTP 請求中標識包體長度的 Content-Length 頭部的值。如果請求中沒有攜帶這個參數,那麼就取不到對應的值。

  • content_type:標識請求包體類型的 Content-Type 頭部的值。同樣需要用戶請求中攜帶對應的參數。

  • uri:請求的 URI(不同於 URL,不包括 ? 后的參數)

  • document_uri:與 uri 完全相同。由於歷史原因而存在的。

  • request_uri:請求的 URL(包括 URI 以及完整的參數)

  • scheme:協議名,例如 HTTP 或者 HTTPS

  • request_method:請求方法,例如 GET 或者 POST

  • request_length:所有請求內容的大小,包括請求行、頭部、包體等

  • remote_user:由 HTTP Basic Authentication 協議傳入的用戶名

  • request_body_file:很多時候會將用戶請求的包體存放到文件中,這個變量就是臨時存放請求包體的文件

    • 如果包體非常小則不會存文件
    • client_body_in_file_only 指令強制所有包體存入文件,且可決定是否刪除
  • request_body:請求中的包體,這個變量當且僅當使用反向代理,且設定用內存暫存包體時才有效

  • request:原始的 URL 請求,含有方法與協議版本,例如 GET /?a=1&b=22 HTTP/1.1

  • host

    • 先從請求行中獲取
    • 如果含有 Host 頭部,則用其值替換掉請求行中的主機名
    • 如果前兩者都取不到,則使用匹配上的 server_name
  • http_頭部名字:返回一個具體請求頭部的值

    特殊變量,這些變量會做一些處理。

    • http_host
    • http_user_agent
    • http_referer
    • http_via
    • http_x_forwarded_for
    • http_cookie

    通用變量,除了以上的變量,都可以取到對應的值。

TCP 連接相關的變量

下面是關於 TCP 連接的變量。

  • binary_remote_addr:客戶端地址的整形格式,對於 IPv4 是 4 字節,對於 IPv6 是 16 字節,所以在 limit_req 和 limit_conn 中通常可以用作 key (詳見:Nginx 處理 HTTP 請求的 11 個階段 中的 preaccess 階段)
  • connection:遞增的連接序號
  • connection_requests:當前連接上執行過的請求數,對 keepalive 連接有意義
  • remote_addr:客戶端地址
  • remote_port:客戶端端口
  • proxy_protocol_addr:若使用了 proxy_protocol 協議,則返回協議中的地址,否則返回空
  • proxy_protocol_port:若使用了 proxy_protocol 協議則返回協議中的端口,否則返回空
  • server_addr:服務端地址
  • server_port:服務器端端口
  • TCP_INFO:TCP 內核層參數,包括 $tcpinfo_rtt, ​$tcpinfo_rttvar,​$tcpinfo_snd_cwnd, $tcpinfo_rcv_space
  • server_protocol:服務器端協議,例如 HTTP/1.1

Nginx 處理請求過程中產生的變量

Nginx 處理 HTTP 請求的過程中也會產生很多變量。

  • request_time:請求處理到現在的耗時,單位為秒,精確到毫秒
  • server_name:匹配上請求的 server_name 值
  • https:如果開啟了 TLS/SSL 則返回 on,否則返回空
  • request_completion:若請求處理完則返回 OK,否則返回空
  • request_id:以 16 進制輸出的請求表示 id,該 id 共含有 16 個字節,是隨機生成的
  • request_filename:待訪問文件的完整路徑
  • document_root:由 URI 和 root、alias 規則生成的文件夾路徑
  • realpath_root:將 document_root 中的軟鏈接等換成真實路徑
  • limit_rate:返回客戶端響應時的速度上限,單位為每秒字節數。可以通過 set 指令修改對請求產生的效果

發送 HTTP 響應時相關的變量

  • body_bytes_sent:響應中 body 包體的長度

  • bytes_sent:全部 http 響應的長度

  • status:http 響應中的返回碼

  • sent_trailer_名字:把響應結尾內容里的值返回

  • sent_http_頭部名字:響應中某個具體頭部的值

    特殊處理,下面這些變量需要經過特殊處理:

    • sent_http_content_type
    • sent_http_content_length
    • sent_http_location
    • sent_http_last_modified
    • sent_http_connection
    • sent_http_keep_alive
    • sent_http_transfer_encoding
    • sent_http_cache_control
    • sent_http_link

    通用:除了上面這些頭部,其他的頭部都是通用型的,也就是可以直接拿來用。

Nginx 系統變量

  • time_local:以本地時間標準輸出的當前時間,例如 14/Nov/2018:15:55:37 +0800
  • time_iso8601:使用 ISO8601 標準輸出的當前時間,例如 2018-11-14T15:55:37+08:00
  • nginx_version:Nginx 版本號
  • pid:所屬 worker 進程的進程 id
  • pipe:使用了管道則返回 p,否則返回 .
  • hostname:所在服務器的主機名,與 hostname 命令輸出一致
  • msec:1970 年 1 月 1 日到現在的時間,單位為秒,小數點后精確到毫秒

實戰

配置文件:

log_format  vartest  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status bytes_sent=$bytes_sent body_bytes_sent=$body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$sent_http_abc"';

server {
	server_name var.ziyang.com localhost;
	#error_log logs/myerror.log debug;
	access_log logs/vartest.log vartest;
	listen 9090;
	
	location / {
		set $limit_rate 10k;
        # return 200; tcpinfo: $tcpinfo_rtt,$tcpinfo_rttvar, $tcpinfo_snd_cwnd, $tcpinfo_rcv_space 
		return 200 '
arg_a: $arg_a,arg_b: $arg_b,args: $args
connection: $connection,connection_requests: $connection_requests
cookie_a: $cookie_a
uri: $uri,document_uri: $document_uri, request_uri: $request_uri
request: $request
request_id: $request_id
server: $server_addr,$server_name,$server_port,$server_protocol
            
host: $host,server_name: $server_name,http_host: $http_host
limit_rate: $limit_rate
hostname: $hostname
content_length: $content_length
status: $status
body_bytes_sent: $body_bytes_sent,bytes_sent: $bytes_sent
time: $request_time,$msec,$time_iso8601,$time_local
';
	}	
}

從上面這個配置文件中,我們可以看出來,返回的響應裡面包含了一系列的變量,實際驗證一下:

  test_nginx curl -H 'Content-Length: 0' -H 'Cookie: a=c1' 'localhost:9090?a=1&b=22'

arg_a: 1,arg_b: 22,args: a=1&b=22
connection: 2,connection_requests: 1
cookie_a: c1
uri: /,document_uri: /, request_uri: /?a=1&b=22
request: GET /?a=1&b=22 HTTP/1.1
request_id: 5d40b1ff29d2b87d5db5c4f95ebf5e4d
server: 127.0.0.1,var.ziyang.com,9090,HTTP/1.1
host: localhost,server_name: var.ziyang.com,http_host: localhost:9090
limit_rate: 10240
hostname: yuanzizhen.local
content_length: 0
status: 200
body_bytes_sent: 0,bytes_sent: 0
time: 0.000,1590842354.866,2020-05-30T20:39:14+08:00,30/May/2020:20:39:14 +0800

大家可以對比一下響應和配置文件中的值是不是一一對應的,更加深刻的理解一下變量的含義。

好了,這一節咱們學習了。關於 Nginx 的變量就講完了,下一節講一下實際應用變量的兩個模塊,大家會有更深刻的理解。

本文首發於我的個人博客:iziyang.github.io,所有配置文件我已經放在了 Nginx 配置文件,大家可以自取。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

.Net Core微服務入門全紀錄(二)——Consul-服務註冊與發現(上)

前言

上一篇【.Net Core微服務入門全紀錄(一)——項目搭建】講到要做到服務的靈活伸縮,那麼需要有一種機制來實現它,這個機制就是服務註冊與發現。當然這也並不是必要的,如果你的服務實例很少,並且很穩定,那麼就沒有必要使用服務註冊與發現。

服務註冊與發現

  • 服務註冊:簡單理解,就是有一個註冊中心,我們的每個服務實例啟動時,都去註冊中心註冊一下,告訴註冊中心我的地址,端口等信息。同樣的服務實例要刪除時,去註冊中心刪除一下,註冊中心負責維護這些服務實例的信息。
  • 服務發現:既然註冊中心維護了各個服務實例的信息,那麼客戶端通過註冊中心就很容易發現服務的變化了。

有了服務註冊與發現,客戶端就不用再去配置各個服務實例的地址,改為從註冊中心統一獲取。
那註冊中心又是怎麼保證每個地址的可用狀態呢,假如某個實例掛了怎麼辦呢?原則上掛掉的實例不應該被客戶端獲取到,所以就要提到:健康檢查 。

  • 健康檢查:每個服務都需要提供一個用於健康檢查的接口,該接口不具備業務功能。服務註冊時把這個接口的地址也告訴註冊中心,註冊中心會定時調用這個接口來檢測服務是否正常,如果不正常,則將它移除,這樣就保證了服務的可用性。

常見註冊中心有 Consul、ZooKeeper、etcd、Eureka。

Consul

Consul官網:https://www.consul.io/
Consul的主要功能有服務註冊與發現、健康檢查、K-V存儲、多數據中心等。

  • Consul安裝:很簡單,直接在官網下載解壓即可。
  • Consul運行:在consul.exe目錄下打開命令行執行 consul.exe agent -dev
  • 瀏覽器訪問:http://localhost:8500/

    Consul已成功運行。

服務註冊

  • 首先Nuget安裝一下Consul:

    這個類庫里封裝了Consul的api操作,方便我們直接使用。當然自己去寫http調用Consul的接口也不是不行。。。接口說明:https://www.consul.io/api-docs

  • 改造一下訂單服務的代碼:

ConsulHelper.cs:

    public static class ConsulHelper
    {
        /// <summary>
        /// 服務註冊到consul
        /// </summary>
        /// <param name="app"></param>
        /// <param name="lifetime"></param>
        public static IApplicationBuilder RegisterConsul(this IApplicationBuilder app, IConfiguration configuration, IHostApplicationLifetime lifetime) 
        {
            var consulClient = new ConsulClient(c =>
            {
                //consul地址
                c.Address = new Uri(configuration["ConsulSetting:ConsulAddress"]);
            });

            var registration = new AgentServiceRegistration()
            {
                ID = Guid.NewGuid().ToString(),//服務實例唯一標識
                Name = configuration["ConsulSetting:ServiceName"],//服務名
                Address = configuration["ConsulSetting:ServiceIP"], //服務IP
                Port = int.Parse(configuration["ConsulSetting:ServicePort"]),//服務端口 因為要運行多個實例,端口不能在appsettings.json里配置,在docker容器運行時傳入
                Check = new AgentServiceCheck()
                {
                    DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服務啟動多久后註冊
                    Interval = TimeSpan.FromSeconds(10),//健康檢查時間間隔
                    HTTP = $"http://{configuration["ConsulSetting:ServiceIP"]}:{configuration["ConsulSetting:ServicePort"]}{configuration["ConsulSetting:ServiceHealthCheck"]}",//健康檢查地址
                    Timeout = TimeSpan.FromSeconds(5)//超時時間
                }
            };

            //服務註冊
            consulClient.Agent.ServiceRegister(registration).Wait();

            //應用程序終止時,取消註冊
            lifetime.ApplicationStopping.Register(() =>
            {
                consulClient.Agent.ServiceDeregister(registration.ID).Wait();
            });

            return app;
        }
    }

appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConsulSetting": {
    "ServiceName": "OrderService",
    "ServiceIP": "localhost",
    "ServiceHealthCheck": "/healthcheck",
    "ConsulAddress": "http://host.docker.internal:8500"//注意,docker容器內部無法使用localhost訪問宿主機器,如果是控制台啟動的話就用localhost
  }
}

Startup.cs:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            //服務註冊
            app.RegisterConsul(Configuration, lifetime);
        }
    }

OrdersController.cs:

    [Route("[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly ILogger<OrdersController> _logger;
        private readonly IConfiguration _configuration;

        public OrdersController(ILogger<OrdersController> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;
        }

        [HttpGet]
        public IActionResult Get()
        {
            string result = $"【訂單服務】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
                $"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
            return Ok(result);
        }
    }

HealthCheckController.cs:

    [Route("[controller]")]
    [ApiController]
    public class HealthCheckController : ControllerBase
    {
        /// <summary>
        /// 健康檢查接口
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public IActionResult Get()
        {
            return Ok();
        }
    }

至此就完成了服務註冊,取消註冊,健康檢查等功能的代碼編寫。

  • 同樣的改造一下產品服務,代碼差不多一樣,就不貼了。

運行服務

繼續在docker中運行服務實例,不習慣docker的話用控制台啟動也行。–ConsulSetting:ServicePort參數就是傳入容器的端口信息。

docker build -t orderapi:1.0 -f ./Order.API/Dockerfile .
docker run -d -p 9060:80 --name orderservice orderapi:1.0 --ConsulSetting:ServicePort="9060"
docker run -d -p 9061:80 --name orderservice1 orderapi:1.0 --ConsulSetting:ServicePort="9061"
docker run -d -p 9062:80 --name orderservice2 orderapi:1.0 --ConsulSetting:ServicePort="9062"

docker build -t productapi:1.0 -f ./Product.API/Dockerfile .
docker run -d -p 9050:80 --name productservice productapi:1.0 --ConsulSetting:ServicePort="9050"
docker run -d -p 9051:80 --name productservice1 productapi:1.0 --ConsulSetting:ServicePort="9051"
docker run -d -p 9052:80 --name productservice2 productapi:1.0 --ConsulSetting:ServicePort="9052"

至此,6個服務器實例都已運行,並且成功註冊到Consul。

隨便停止2個服務:

可以看到停止的服務已經在Consul中被移除。注意,這個是我們停止程序時主動調用Consul移除的。

//應用程序終止時,取消註冊
lifetime.ApplicationStopping.Register(() =>
{
    consulClient.Agent.ServiceDeregister(registration.ID).Wait();
});

當然程序發生異常,健康檢查不能正確響應的話,Consul也會移除,有一點區別。

那麼註冊,發現,健康檢查功能都完成了,下一步就該考慮客戶端如何拿到這些服務實例的地址了。

代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

未完待續…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

Vue —— 精講 VueRouter(1)

最近被Boos調去給新人做培訓去了,目前把自己整理的一些東西分享出來,希望對大家有所幫助

demo源代碼地址:https://github.com/BM-laoli/BMlaoli-learn-VueRouter

本章節為VueRouter前端 路由的章節部分

大綱

一、基本概念

路由就是通過網絡把訊息從源地址傳輸到目的地的活動
需要一些映射表

  1. 做路由
  2. 做信息的轉發(核心就是:轉發)

後端路由還有前端路由,後端渲染和前端渲染

前端渲染(前後端分離API生態),後端渲染(view嵌套一起)

前端路由的核心概念
地址變化的時候改變url的時候,不進行整體頁面刷新

改變url但是不刷新頁面,的解決方式

我們有這樣的一個需求,改變url跳轉地址,我們獲取新的頁面,但是不希望頁面發生刷新

解決方案1:locaion.hash = ‘/’

這個是vueRouter的底層實現

監聽hash的變化,從而改變網頁數據的獲取機制,渲染對應的組件,

解決方案2:H5的histroray模式

  1. pushState
    history.pushState({},”,’home’),第三個參數就是url

這裏的push實際上就是一個棧結構(先進后出),

假設我們這裏需要回去,使用back()彈棧

history.pushState({},'','home'),
history.pushState({},'','about'),
history.pushState({},'','user'),

//執行這個之後就能進行back()出棧了
history.back(),
// 此時的url是 /about

  1. repalceState

這裡有一個方法和push方法很像,但是不會back()不能點擊後腿按鈕

history.repalceState({},'','user'),
  1. go

這裏的go是對棧的一個操作,
go(-1)彈出一個
go(-2)彈出二個

go(1)壓入一個
go(2)壓入二個

go(-1)

以上就是我們的基本的前端路由原理

二、v-router基本使用

前端三大框架都有自己的router,可以用來構建SPA應用

使用小提示,還是非常非常的簡單的:

  1. 如果你沒有安裝就需要 npm install vue-router去安裝
    • 導入路由對象,並且調用Vue.use(VueRouter)安裝這個路由插件
    • 創建路由實例,傳入映射配置wxain
    • 在vue實例中掛載創建好了的路由

1.導入路由對象,並且配置optionn給路由

/router/index.js


/**
 * 配置路由相關的信息
 */
// 1. 導入
 import Router from 'vue-router'
 
 // 2.1 導入vue實例
import Vue from 'vue'

// 導入組件
import Home from '../components/Home.vue'
import About from '../components/About.vue'


// 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
Vue.use(Router)

// 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西

// 4. 抽離配置項出來
const routes = []

const router = new Router({routes})

//4. 導出
export default router 
 

2.配置路由映射

/router/index.js

const routes = [
 
 {path:'/home',component:Home},
 {path:'/about',component:About},

] 

3.在實例中使用路由

/main.js

import Vue from 'vue'
import App from './App'
import router from './router'//注意啊模塊查找規則index.js

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,// 主要是在這裏掛載進去就好了
  render: h => h(App)
}) 

4.小心,我們的路由入口還有連接link

/App.vue


<template>
  <div id="app">
    <!-- //這兩個是一個全局祖冊過着個組件,這個就是一個a標籤 -->
    <router-link to='/home'>首頁</router-link>
    <router-link to='/about'>關於</router-link>
    <!-- 路由出口,既:渲染的出口,這個就是一個佔位符號 -->
    <router-view></router-view>
  </div>
</template>

以下是我們的兩個組件

/Home.vue

<template>
    <div>
        <h2>我是首頁</h2>
        <p>我是首頁內容哈哈哈</p>
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style scoped>

</style>

/About.vue

<template>
    <div>
        <h2>我是關於頁面</h2>
        <p>我是首關於內容哈哈哈</p>
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style>

</style>

以上就是我們非常簡單的使用

三、其它的知識點補充

路由的默認值,並且修改成mode=>hisary模式

我們希望默認显示的就是一個首頁
解決方式,映射一個’/’,然後進行重定向
/index.js

  {
    path:'/',
    redirect:'/home'
  },

我們為什麼要去做這調整成一個history,因為我們希望去掉#這個標識

只需要在new 的時候指定一下就好了
/index,js

const router = new Router({
  routes,
  mode:"history"//就是這裏的這個更改路由方式
})

router-link的屬性

  1. tage
    to是一個屬性 ,默認是渲染成一個a鏈接,假設我現在需要默認渲染成一個buttmm怎麼辦呢?
    加一個tag就好了
    <router-link to='/home' tag='button'  >首頁</router-link>
  1. 更改模式replceStats 不允許瀏覽器回退
    replace加上去就好了
<router-link to='/about' tag='button' replace >關於</router-link>
  1. 我們可以利用一些默認的東西去非常方便的做到想要的效果
<style>
.router-link-active{
  color: blue;
}
</style>

替換值:我們希望不要怎麼長,我們希望.active就能改樣式怎麼搞?
加一個active-calss就好了,這個直接就是acitve做為類就好了

 <router-link to='/home' tag='button'  active-class  >首頁</router-link>
 <style>
    .active{
        bgc:red
    }
 </style>

代碼路由跳轉,意思就是重定向

注意啊!route != router
在我們學習路由的時候,this.$router是一個非常重要的對象

這個東西在開中經常的使用

// this.$router.push('重定向的值就好了')。
// this.$router.push('/home')
// 如果你不想有出現回退按鈕,這樣來做就好了
this.$router.replace('/home')

四、動態路由參數

這裏只是簡單的介紹了理由傳參的地址欄拼接模式,但是還有更多更奇奇怪怪的傳值方式,詳見官方Router文檔,

this.$router.parmas
// 這個parmas裏面就是我們的路由參數存放點

這裏我們有這樣的一個需求,我們希望點擊user頁面的時候可以,得到任意的路由參數

比如我們現在/user/zhnsang,的時候可以獲取zhangshang,/user/lishi的時候可以獲取lishi>

  1. 首先我們需要在路由裏面加:
    /router/index.js
   {
        path: "/user/:usermsg",
        component: User
    }
]
  1. 頁面傳遞數據
    /App.vue
router-link :to="'/user/'+username">用戶相關</router-link>
<!-- 路由出口,既:渲染的出口,這個就是一個佔位符號 -->
<router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      username: 'lisi'
    }
  },

  1. 頁面獲取數據

一定要注意了,一定是rouer裏面定義的才能從另一路由拿出來

/User.vue


<template>
    <div>
        <h2>我是用戶相關專業</h2>
        <p>我是用戶訊息相關頁面,嘿嘿嘿嘿嘿</p>
        <h1>{{ $route.params.usermsg }}44</h1>
        <hr>
        <h2>{{username}}</h2>
    </div>
</template>

<script>
    export default {    
        computed: {
            username() {
                return this.$route.params.usermsg
            }
        },
    }
</script>

<style scpoe>

</style>

五、細節詳解

注意啊!再說一遍route != router

注意啊,這裏的$route實際上是我們在main裏面new的一個Router得到的,
並且 這個route對象是隨着請求的地方不一樣,而改變的。也就是說,這個的route是當前頁面中的route對象,而且在vue只能只有一個route實例存在

六、 Vue的webpack打包詳解 + 路由懶加載

一個vue項目的簡單打包目錄結構分析

我們來看看,在一個vue項目中,簡單的三個文件是怎麼打包的

假設目前有這樣的三個文件 ,我們需要對他們進行打包,mian是入口,有一個add業務,有一個math依賴模塊。那麼我們webpack打包成的三個文件到底是如何運行的呢?

在vue中 使用webpack打包的時候,會把一些東西給分模塊的打包出來,它打包的東西的目錄結構如下
裏面我們實際打包的時候會把css,js都給分開,各自有各自的作用

| dist
| ---static
| ---css
| ---js
| -----app.XXXX.js         (這個是項目的業務邏輯所在)
| -----manifest.xxxx.js    (這個是底層打包的依賴文件所在)
| -----vendor.xxxx.js      (這個是依賴所在)
| idnex.html

路由懶加載

  1. 概念的理解

目前呢,我們打包的情況是這樣的:我們所有的代碼都是集中放在了以一個app.xxx.js文件中,這樣其實不利於後期的維護和開發,因為如果我們有很多很多的大量的代碼的時候,我們的這個文件就會變得非常非常的大,於是呢,我們就需要路由懶加載,所謂懶加載就是:‘在需要的時候,才去加載某個資源文件’,路由懶加載,就是把每一個路由對應的業務邏輯代碼,在打包的時候分割到不同的js文件中,如何在需要用的時候再去請求它

經過這樣的打包的懶加載之後,我們的目錄會變成這個樣子

| dist
| ---static
| ---css
| ---js
| -----0.xxx.js            (假設是路由home的業務邏輯代碼)
| -----1.xxx.js             (假設是路由about的業務邏輯代碼)
| -----app.XXXX.js         (這個是項目的業務邏輯所在)
| -----manifest.xxxx.js    (這個是底層打包的依賴文件所在)
| -----vendor.xxxx.js      (這個是依賴所在)
| idnex.html
  1. 如何使用

使用非常的簡單,主要有如下的三種方式去使用,但是我最喜歡的還是最後一種方式
/rouetr/index.js

- 使用vue的異步組價和webpack的寫法,早期的時候
const Home = resolve =>{ require.ensure(['../compenet/Home.vue'],()=>{
   resolve (require('../compenet/Home.vue'))
})}

- AMD規範的寫法
const About = resolve =>{ require(['../compenent/About.vue'],resolve) }


- ES6的結合異步組件的方式(最常用)
const Home = () => import('../compenet/Home.vue')

實際的使用
/router/index.js

/**
 * 配置路由相關的信息
 */
// 1. 導入
import Router from 'vue-router'

// 2.1 導入vue實例
import Vue from 'vue'

// 導入組件
// import Home from '../components/Home.vue'
// import About from '../components/About.vue'
// import User from '../components/User'
const Home = () =>
    import ('../components/Home.vue')
const About = () =>
    import ('../components/About.vue')
const User = () =>
    import ('../components/User')


// 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
Vue.use(Router)

// 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西

// 4. 抽離配置項出來
const routes = [{
        path: '/',
        redirect: '/home'
    },
    {
        path: '/home',
        component: Home
    },
    {
        path: '/about',
        component: About
    },
    {
        path: "/user/:usermsg",
        component: User
    }
]

const router = new Router({
    routes,
    mode: "history"
})

//4. 導出
export default router

//6. 去main裏面掛載

七、 路由嵌套

我們目前有這樣的一個需求:我們希望我們在hone下,可以/home/new去到home下的一個子組件,/home/message去到另一個子組件

  1. 首先 我們需要有組件
    /components/HomeMessage.vue
<template>
    <div>
      <ul>
          <li1>我是消息1</li1>
          <li2>我是消息2</li2>
          <li3>我是消息3</li3>
          <li4>我是消息4</li4>
      </ul>
    </div>
</template>

<script>
    export default {
        name:"HomeMessage"
    }   
</script>

<style>

</style>

/components/HomeNews

<template>
    <div>
    <ul>
        <li1>新1</li1>
        <li2>新2</li2>
        <li3>新3</li3>
        <li4>新4</li4>
        <li5>新5</li5>
    </ul>
    </div>
</template>

<script>
    export default {
        name:"HomeNews"
    }
</script>

<style>

</style>
  1. 在路由裏面去配置
const HomeNews = () =>
    import ('../components/HomeNews')
const HomeMessage = () =>
    import ('../components/HomeNews')


// 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
Vue.use(Router)

// 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西

// 4. 抽離配置項出來
const routes = [{
        path: '/',
        redirect: '/home'
    },
    {
        path: '/home',
        component: Home,
        children: [{
                path: '',
                redirect: 'news'
            },
            {
                path: 'news',// 這裏寫路由實際上應該是/home/news,這裏只是一個相對路由地址,
                component: HomeNews
            },
            {
                path: 'message',
                component: HomeMessage
            },

        ]
    },
    {
  1. 打入口router-view(瞎起的名字實際上就是路由的佔位符)
    /Home.vue
<template>
    <div>
        <h2>我是首頁</h2>
        <p>我是首頁內容哈哈哈</p>
     <router-link to="/home/news">news</router-link>
     <router-link to="/home/message">message</router-link>
    <router-view></router-view>
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style scoped>

</style>

這裏如果是有關狀態的保持,我們需要使用key-alive,後面我們再做詳細的講解

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

小師妹學JavaIO之:用Selector來發好人卡

目錄

  • 簡介
  • Selector介紹
  • 創建Selector
  • 註冊Selector到Channel中
  • SelectionKey
  • selector 和 SelectionKey
  • 總的例子
  • 總結

簡介

NIO有三寶:Buffer,Channel,Selector少不了。本文將會介紹NIO三件套中的最後一套Selector,並在理解Selector的基礎上,協助小師妹發一張好人卡。我們開始吧。

Selector介紹

小師妹:F師兄,最近我的桃花有點旺,好幾個師兄莫名其妙的跟我打招呼,可是我一心向著工作,不想談論這些事情。畢竟先有事業才有家嘛。我又不好直接拒絕,有沒有什麼比較隱晦的方法來讓他們放棄這個想法?

更多內容請訪問www.flydean.com

這個問題,我沉思了大約0.001秒,於是給出了答案:給他們發張好人卡吧,應該就不會再來糾纏你了。

小師妹:F師兄,如果給他們發完好人卡還沒有用呢?

那就只能切斷跟他們的聯繫了,來個一刀兩斷。哈哈。

這樣吧,小師妹你最近不是在學NIO嗎?剛好我們可以用Selector來模擬一下發好人卡的過程。

假如你的志偉師兄和子丹師兄想跟你建立聯繫,每個人都想跟你建立一個溝通通道,那麼你就需要創建兩個channel。

兩個channel其實還好,如果有多個人都想同時跟你建立聯繫通道,那麼要維持這些通道就需要保持連接,從而浪費了資源。

但是建立的這些連接並不是時時刻刻都有消息在傳輸,所以其實大多數時間這些建立聯繫的通道其實是浪費的。

如果使用Selector就可以只啟用一個線程來監聽通道的消息變動,這就是Selector。

從上面的圖可以看出,Selector監聽三個不同的channel,然後交給一個processor來處理,從而節約了資源。

創建Selector

先看下selector的定義:

public abstract class Selector implements Closeable

Selector是一個abstract類,並且實現了Closeable,表示Selector是可以被關閉的。

雖然Selector是一個abstract類,但是可以通過open來簡單的創建:

Selector selector = Selector.open();

如果細看open的實現可以發現一個很有趣的現象:

public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

open方法調用的是SelectorProvider中的openSelector方法。

再看下provider的實現:

 public SelectorProvider run() {
   if (loadProviderFromProperty())
        return provider;
    if (loadProviderAsService())
        return provider;
      provider = sun.nio.ch.DefaultSelectorProvider.create();
      return provider;
    }
 });

有三種情況可以加載一個SelectorProvider,如果系統屬性指定了java.nio.channels.spi.SelectorProvider,那麼從指定的屬性加載。

如果沒有直接指定屬性,則從ServiceLoader來加載。

最後如果都找不到的情況下,使用默認的DefaultSelectorProvider。

關於ServiceLoader的用法,我們後面會有專門的文章來講述。這裏先不做多的解釋。

註冊Selector到Channel中

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

如果是在服務器端,我們需要先創建一個ServerSocketChannel,綁定Server的地址和端口,然後將Blocking設置為false。因為我們使用了Selector,它實際上是一個非阻塞的IO。

注意FileChannels是不能使用Selector的,因為它是一個阻塞型IO。

小師妹:F師兄,為啥FileChannel是阻塞型的呀?做成非阻塞型的不是更快?

小師妹,我們使用FileChannel的目的是什麼?就是為了讀文件呀,讀取文件肯定是一直讀一直讀,沒有可能讀一會這個channel再讀另外一個channel吧,因為對於每個channel自己來講,在文件沒讀取完之前,都是繁忙狀態,沒有必要在channel中切換。

最後我們將創建好的Selector註冊到channel中去。

SelectionKey

SelectionKey表示的是我們希望監聽到的事件。

總的來說,有4種Event:

  • SelectionKey.OP_READ 表示服務器準備好,可以從channel中讀取數據。
  • SelectionKey.OP_WRITE 表示服務器準備好,可以向channel中寫入數據。
  • SelectionKey.OP_CONNECT 表示客戶端嘗試去連接服務端
  • SelectionKey.OP_ACCEPT 表示服務器accept一個客戶端的請求
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

我們可以看到上面的4個Event是用位運算來定義的,如果將這個四個event使用或運算合併起來,就得到了SelectionKey中的interestOps。

和interestOps類似,SelectionKey還有一個readyOps。

一個表示感興趣的操作,一個表示ready的操作。

最後,SelectionKey在註冊的時候,還可以attach一個Object,比如我們可以在這個對象中保存這個channel的id:

SelectionKey key = channel.register(
  selector, SelectionKey.OP_ACCEPT, object);
key.attach(Object);
Object object = key.attachment();

object可以在register的時候傳入,也可以調用attach方法。

最後,我們可以通過key的attachment方法,獲得該對象。

selector 和 SelectionKey

我們通過selector.select()這個一個blocking操作,來獲取一個ready的channel。

然後我們通過調用selector.selectedKeys()來獲取到SelectionKey對象。

在SelectionKey對象中,我們通過判斷ready的event來處理相應的消息。

總的例子

接下來,我們把之前將的串聯起來,先建立一個小師妹的ChatServer:

public class ChatServer {

    private static String BYE_BYE="再見";

    public static void main(String[] args) throws IOException, InterruptedException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        while (true) {
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();
            while (iter.hasNext()) {
                SelectionKey selectionKey = iter.next();
                if (selectionKey.isAcceptable()) {
                    register(selector, serverSocketChannel);
                }
                if (selectionKey.isReadable()) {
                    serverResonse(byteBuffer, selectionKey);
                }
                iter.remove();
            }
            Thread.sleep(1000);
        }
    }

    private static void serverResonse(ByteBuffer byteBuffer, SelectionKey selectionKey)
            throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        socketChannel.read(byteBuffer);
        byteBuffer.flip();
        byte[] bytes= new byte[byteBuffer.limit()];
        byteBuffer.get(bytes);
        log.info(new String(bytes).trim());
        if(new String(bytes).trim().equals(BYE_BYE)){
            log.info("說再見不如不見!");
            socketChannel.write(ByteBuffer.wrap("再見".getBytes()));
            socketChannel.close();
        }else {
            socketChannel.write(ByteBuffer.wrap("你是個好人".getBytes()));
        }
        byteBuffer.clear();
    }

    private static void register(Selector selector, ServerSocketChannel serverSocketChannel)
            throws IOException {
        SocketChannel socketChannel = serverSocketChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
    }
}

上面例子有兩點需要注意,我們在循環遍歷中,當selectionKey.isAcceptable時,表示服務器收到了一個新的客戶端連接,這個時候我們需要調用register方法,再註冊一個OP_READ事件到這個新的SocketChannel中,然後繼續遍歷。

第二,我們定義了一個stop word,當收到這個stop word的時候,會直接關閉這個client channel。

再看看客戶端的代碼:

public class ChatClient {

    private static SocketChannel socketChannel;
    private static ByteBuffer byteBuffer;

    public static void main(String[] args) throws IOException {

        ChatClient chatClient = new ChatClient();
        String response = chatClient.sendMessage("hello 小師妹!");
        log.info("response is {}", response);
        response = chatClient.sendMessage("能不能?");
        log.info("response is {}", response);
        chatClient.stop();

    }

    public void stop() throws IOException {
        socketChannel.close();
        byteBuffer = null;
    }

    public ChatClient() throws IOException {
        socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527));
        byteBuffer = ByteBuffer.allocate(512);
    }

    public String sendMessage(String msg) throws IOException {
        byteBuffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        socketChannel.write(byteBuffer);
        byteBuffer.clear();
        socketChannel.read(byteBuffer);
        byteBuffer.flip();
        byte[] bytes= new byte[byteBuffer.limit()];
        byteBuffer.get(bytes);
        response =new String(bytes).trim();
        byteBuffer.clear();
        return response;

    }
}

客戶端代碼沒什麼特別的,需要注意的是Buffer的讀取。

最後輸出結果:

server收到: INFO com.flydean.ChatServer - hello 小師妹!
client收到: INFO com.flydean.ChatClient - response is 你是個好人
server收到: INFO com.flydean.ChatServer - 能不能?
client收到: INFO com.flydean.ChatClient - response is 再見

解釋一下整個流程:志偉跟小師妹建立了一個連接,志偉向小師妹打了一個招呼,小師妹給志偉發了一張好人卡。志偉不死心,想繼續糾纏,小師妹回復再見,然後自己關閉了通道。

總結

本文介紹了Selector和channel在發好人卡的過程中的作用。

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/java-io-nio-selector/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Shell語法規範

  • ver:1.0
  • 博客:https://www.cnblogs.com/Rohn
  • 本文介紹了Shell編程的一些語法規範,主要參考依據為谷歌的Shell語法風格。

目錄

  • 背景
    • 使用哪一種Shell
    • 什麼時候使用Shell
  • 註釋
    • 頂層註釋
    • 功能註釋
    • TODO註釋
  • 格式
    • 縮進
    • 行的長度和長字符串
    • 管道
    • 循環
      • if-else語句
      • for-do和while-do語句
    • case語句
    • 變量擴展
  • 特性
    • 命令替換
    • 文件名的通配符擴展
  • 命名約定
    • 函數名
    • 變量名
    • 常量和環境變量名
    • 源文件名
    • 只讀變量
    • 使用本地變量
  • 調用命令
    • 檢查返回值

背景

博客:https://www.cnblogs.com/Rohn

使用哪一種Shell

可執行文件必須以 #!/bin/bash 和最小數量的標誌開始。請使用 set 來設置shell的選項,使得用 <script_name>調用你的腳本時不會破壞其功能。

推薦使用:

#!/usr/bin/env bash

env一般固定在/usr/bin目錄下,而其餘解釋器的安裝位置就相對不那麼固定。

限制所有的可執行Shell腳本為bash使得我們安裝在所有計算機中的shell語言保持一致性。

無論你是為什麼而編碼,對此唯一例外的是當你被迫時可以不這麼做的。其中一個例子是Solaris SVR4包,編寫任何腳本都需要用純Bourne shell

[root@test ~]# echo $SHELL
/bin/bash

什麼時候使用Shell

使用Shell需要遵守的一些準則:

  • 如果你主要是在調用其他的工具並且做一些相對很小數據量的操作,那麼使用Shell來完成任務是一種可接受的選擇。
  • 如果你在乎性能,那麼請選擇其他工具,而不是使用Shell。
  • 如果你發現你需要使用數據而不是變量賦值(如 ${PHPESTATUS} ),那麼你應該使用Python腳本。
  • 如果你將要編寫的腳本會超過100行,那麼你可能應該使用Python來編寫,而不是Shell。

請記住,當腳本行數增加,儘早使用另外一種語言重寫你的腳本,以避免之後花更多的時間來重寫。

註釋

博客:https://www.cnblogs.com/Rohn

Bash只支持單行註釋,使用#開頭的都被當作註釋語句。

頂層註釋

每個文件必須包含一個頂層註釋,對其內容進行簡要概述。版權聲明和作者信息是可選的。
例如:

#!/usr/bin/env bash
# Author: Rohn
# Version: 1.0
# Created Time: 2020/06/06
# Perform hot backups of MySQL databases.
  • 第1行,指明解釋器,使用bash

#!叫做”Shebang”或者”Sha-bang”(Unix術語中,#號通常稱為sharp,hash或mesh;而!則常常稱為bang),指明了執行這個腳本文件的解釋程序。當然,如果使用bash test.sh這樣的命令來執行腳本,那麼#!這一行將會被忽略掉。

  • 第2-5行,分別為作者、版本號、創建時間、功能說明。

功能註釋

任何不是既明顯又短的函數都必須被註釋。任何庫函數無論其長短和複雜性都必須被註釋。

其他人通過閱讀註釋(和幫助信息,如果有的話)就能夠學會如何使用你的程序或庫函數,而不需要閱讀代碼。

所有的函數註釋應該包含:

  • 函數的描述
  • 全局變量的使用和修改
  • 使用的參數說明
  • 返回值,而不是上一條命令運行后默認的退出狀態

例如:

#!/usr/bin/env bash
# Author: Rohn
# Version: 1.0
# Created Time: 2020/06/06
# Perform hot backups of Oracle databases.

export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin'

#######################################
# Cleanup files from the backup dir
# Globals:
#   BACKUP_DIR
#   ORACLE_SID
# Arguments:
#   None
# Returns:
#   None
#######################################
cleanup() {
  ...
}

TODO註釋

TODOs應該包含全部大寫的字符串TODO,接着是括號中你的用戶名。冒號是可選的。最好在TODO條目之後加上bug或者ticket的序號。

例如:

# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)

格式

博客:https://www.cnblogs.com/Rohn

縮進

縮進兩個空格,沒有製表符。例如:

if [ a > 1 ];then
  echo '${a} > 1'
fi

行的長度和長字符串

行的最大長度為80個字符。例如:

# DO use 'here document's
cat <<END;
I am an exceptionally long
string.
END

# Embedded newlines are ok too
long_string="I am an exceptionally
  long string."

管道

如果一行容不下整個管道操作,那麼請將整個管道操作分割成每行一個管段。

應該將整個管道操作分割成每行一個管段,管道操作的下一部分應該將管道符放在新行並且縮進2個空格。這適用於使用管道符|的合併命令鏈以及使用||&&的邏輯運算鏈。

例如:

# All fits on one line
command1 | command2

# Long commands
command1 \
  | command2 \
  | command3 \
  | command4

循環

if-else語句

if; then放在同一行,;后空一格,else單獨一行,fi單獨一行,並與if垂直對齊。即:

if condition; then
  statement(s)
else
  statement(s)
fi

for-do和while-do語句

while/for; do放在同一行,donewhile/for垂直對齊,即:

# while structure
while condition; do
  statement(s)
done

# for structure
for condition; do
  statement(s)
done

例如:

for dir in ${dirs_to_cleanup}; do
  if [[ -d "${dir}/${ORACLE_SID}" ]]; then
    log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
    rm "${dir}/${ORACLE_SID}/"*
    if [[ "$?" -ne 0 ]]; then
      error_message
    fi
  else
    mkdir -p "${dir}/${ORACLE_SID}"
    if [[ "$?" -ne 0 ]]; then
      error_message
    fi
  fi
done

case語句

  • 通過2個空格縮進可選項。
  • 在同一行可選項的模式右圓括號之後和結束符 ;;之前各需要一個空格。
  • 長可選項或者多命令可選項應該被拆分成多行,模式、操作和結束符;;在不同的行。

匹配表達式比caseesac 縮進一級。多行操作要再縮進一級。一般情況下,不需要引用匹配表達式。模式表達式前面不應該出現左括號。避免使用;&;;&符號。即:

# case structure
case in expression in
  pattern1)
    statement1
    ;;
  pattern2)
    statement2
    ;;
  ...
  *)
    statementn
    ;;
esac

例如:

case "${expression}" in
  a)
    variable="..."
    some_command "${variable}" "${other_expr}" ...
    ;;
  absolute)
    actions="relative"
    another_command "${actions}" "${other_expr}" ...
    ;;
  *)
    error "Unexpected expression '${expression}'"
    ;;
esac

只要整個表達式可讀,簡單的命令可以跟模式和;; 寫在同一行。這通常適用於單字母選項的處理。當單行容不下操作時,請將模式單獨放一行,然後是操作,最後結束符;; 也單獨一行。當操作在同一行時,模式的右括號之後和結束符;;之前請使用一個空格分隔。

verbose='false'
aflag=''
bflag=''
files=''
while getopts 'abf:v' flag; do
  case "${flag}" in
    a) aflag='true' ;;
    b) bflag='true' ;;
    f) files="${OPTARG}" ;;
    v) verbose='true' ;;
    *) error "Unexpected option ${flag}" ;;
  esac
done

變量擴展

按優先級順序:保持跟你所發現的一致;引用你的變量;推薦用${var}而不是$var

例如

# Section of recommended cases.

# Preferred style for 'special' variables:
echo "Positional: $1" "$5" "$3"
echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."

# Braces necessary:
echo "many parameters: ${10}"

# Braces avoiding confusion:
# Output is "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"

# Preferred style for other variables:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read f; do
  echo "file=${f}"
done < <(ls -l /tmp)

# Section of discouraged cases

# Unquoted vars, unbraced vars, brace-quoted single letter
# shell specials.
echo a=$avar "b=$bvar" "PID=${$}" "${1}"

# Confusing use: this is expanded as "${1}0${2}0${3}0",
# not "${10}${20}${30}
set -- a b c
echo "$10$20$30"

特性

博客:https://www.cnblogs.com/Rohn

命令替換

使用 $(command)而不是反引號。

嵌套的反引號要求用反斜杠轉義內部的反引號。而$(command) 形式嵌套時不需要改變,而且更易於閱讀。

例如:

# This is preferred:
var="$(command "$(command1)")"

# This is not:
var="`command \`command1\``"

文件名的通配符擴展

當進行文件名的通配符擴展時,請使用明確的路徑。

因為文件名可能以-開頭,所以使用擴展通配符./**來得安全得多。

# Here's the contents of the directory:
# -f  -r  somedir  somefile

# This deletes almost everything in the directory by force
psa@bilby$ rm -v *
removed directory: `somedir'
removed `somefile'

# As opposed to:
psa@bilby$ rm -v ./*
removed `./-f'
removed `./-r'
rm: cannot remove `./somedir': Is a directory
removed `./somefile'

命名約定

博客:https://www.cnblogs.com/Rohn

函數名

使用小寫字母,並用下劃線分隔單詞。使用雙冒號 :: 分隔庫。函數名之後必須有圓括號。關鍵詞 function 是可選的,但必須在一個項目中保持一致。

如果你正在寫單個函數,請用小寫字母來命名,並用下劃線分隔單詞。如果你正在寫一個包,使用雙冒號 :: 來分隔包名。大括號必須和函數名位於同一行(就像在Google的其他語言一樣),並且函數名和圓括號之間沒有空格。

# Single function
my_func() {
  ...
}

# Part of a package
mypackage::my_func() {
  ...
}

當函數名后存在 () 時,關鍵詞 function 是多餘的。但是其促進了函數的快速辨識。

變量名

使用小寫字母,循環的變量名應該和循環的任何變量同樣命名。例如:

for zone in ${zones}; do
  something_with "${zone}"
done

常量和環境變量名

全部使用大寫字母,用下劃線分隔,聲明在文件的頂部。例如:

# Constant
readonly PATH_TO_FILES='/some/path'

# Both constant and environment
declare -xr ORACLE_SID='PROD'

源文件名

使用小寫字母,如果需要的話使用下劃線分隔單詞。例如: maketemplate 或者 make_template ,而不是 make-template

只讀變量

使用小寫字母,使用 readonly 或者 declare -r 來確保變量只讀。

因為全局變量在Shell中廣泛使用,所以在使用它們的過程中捕獲錯誤是很重要的。當你聲明了一個變量,希望其只讀,那麼請明確指出。

zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; then
  error_message
else
  readonly zip_version
fi

使用本地變量

使用小寫字母,使用 local 聲明特定功能的變量。聲明和賦值應該在不同行。

使用 local 來聲明局部變量以確保其只在函數內部和子函數中可見。這避免了污染全局命名空間和不經意間設置可能具有函數之外重要性的變量。

當賦值的值由命令替換提供時,聲明和賦值必須分開。因為內建的 local 不會從命令替換中傳遞退出碼。

my_func2() {
  local name="$1"

  # Separate lines for declaration and assignment:
  local my_var
  my_var="$(my_func)" || return

  # DO NOT do this: $? contains the exit code of 'local', not my_func
  local my_var="$(my_func)"
  [[ $? -eq 0 ]] || return

  ...
}

調用命令

博客:https://www.cnblogs.com/Rohn

檢查返回值

對於非管道命令,使用$?或直接通過一個if語句來檢查以保持其簡潔。例如:

if ! mv "${file_list}" "${dest_dir}/" ; then
  echo "Unable to move ${file_list} to ${dest_dir}" >&2
  exit "${E_BAD_MOVE}"
fi

# Or
mv "${file_list}" "${dest_dir}/"
if [[ "$?" -ne 0 ]]; then
  echo "Unable to move ${file_list} to ${dest_dir}" >&2
  exit "${E_BAD_MOVE}"
fi

Bash也有 PIPESTATUS 變量,允許檢查從管道所有部分返回的代碼。如果僅僅需要檢查整個管道是成功還是失敗,以下的方法是可以接受的:

tar -cf - ./* | ( cd "${dir}" && tar -xf - )
if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then
  echo "Unable to tar files to ${dir}" >&2
fi

可是,只要你運行任何其他命令, PIPESTATUS 將會被覆蓋。如果你需要基於管道中發生的錯誤執行不同的操作,那麼你需要在運行命令后立即將 PIPESTATUS 賦值給另一個變量(別忘了 [ 是一個會將 PIPESTATUS 擦除的命令)。

tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
return_codes=(${PIPESTATUS[*]})
if [[ "${return_codes[0]}" -ne 0 ]]; then
  do_something
fi
if [[ "${return_codes[1]}" -ne 0 ]]; then
  do_something_else
fi

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!