說到“新能源”就只有電動車嗎?

木炭車行駛速度慢,載重能力差,主要承擔客運。當時,由市區發往百多公里以外鄰縣的班線,每天只能行使一個單程。由於車輛老舊,汽車中途拋錨現象時常發生。特別是遇上坡路段,汽車還經常熄火,需旅客幫助推車。有時木炭燒完,還要發動旅客下車揀柴禾、樹枝充做燃料。

現在說到新能源,一般會想到電動車,如果了解得多的可能還知道燃料電池或者氫動力之類,但是再早一點,國內還有另外一種不用新能源的“新能源”汽車—木炭車。

當你在想木炭車怎麼驅動車的時候是不是以為大力出奇迹?其實這裏的木炭不是直接當做發動機的燃料來產生動力,整個工作原理大致如下圖。

利用木炭在一個特殊的發生爐經過一系列程序後生成煤氣,然後讓發動機燃燒煤氣驅動車輛。聽着就很蛋疼啊,除了讓車看起來更丑,開起來更慢,可能還有更多的pM2.5(那會應該還沒有這個概念)為什麼要搞這種事情呢?

上世紀五十年代初,國家經濟處在恢復時期又遇抗美援朝戰爭,各西方國家對我國實施禁運政策,國家汽油短缺,燒汽油的車就紛紛改裝成燒木炭的車。(所以木炭車的本質還是活塞發動機的原理,只是燃料從石油變成了煤氣。)

由於木炭車的結構,每天發車前,駕駛員需要用油棉引燃木炭,先燒上10分鐘左右,然後搖動鼓風機才能啟動。上部的木炭淋有少量的水分,由於頂部的鍋爐蓋在正常工作時是密封着的,高溫密封加少量的氧,反應成一氧化碳氣體,經罐內生鐵造的爐膽管道,進入儲氣罐再送到發動機里和適量的空氣混合,形成可燃混合氣(也就是煤氣),然後導入發動機工作。(讓我們看下木炭車的動力系統是怎麼工作的)

木炭車一般從點爐到啟動需40-50分鐘。行駛中,還要添料、捅爐、掌握木炭燃燒程度,司機和助手每天工作相當辛苦。木炭車行駛速度慢,載重能力差,主要承擔客運。當時,由市區發往百多公里以外鄰縣的班線,每天只能行使一個單程。由於車輛老舊,汽車中途拋錨現象時常發生。

特別是遇上坡路段,汽車還經常熄火,需旅客幫助推車。有時木炭燒完,還要發動旅客下車揀柴禾、樹枝充做燃料。

一趟行程下來司機乘客都是灰頭土臉,但是在當時的情況下,有車就已經很滿足了,顯然這裏的“新能源”並不是所謂的綠色新能源,所以木炭車隨着我國的油田發展也就漸漸消失了。

看到上面的工程師以為木炭車中國才有嗎?其實二戰的時候有些國家也因為石油問題,甚至期間的坦克也採用“木炭車”的技術,屁股上背着一個大罐。

而在我們的鄰國朝鮮現在還有機會能看到。

雖然我們國家現在有了自己的油田,現在卻是世界最大石油進口國。不管是因為真的消耗如此巨大還是戰略上的貯備,木炭車的歷史都讓我們明白了一個道理,一定要有自己的核心技術。如果當時我們連木炭車都搗鼓不出來就真的只能幹瞪眼。現在的自主品牌中有些確實腳踏實地在潛心研發自己的技術,但是也有些品牌像寄生蟲一樣,總是想着鑽空子,鬧個大新聞,希望自主品牌越來越好的贊一個!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

主動安全很重要 8萬元以下帶ESP家用轎車選這些!

最初的遙控泊車着實讓我們驚艷的一把。速銳在7萬多的車型上就已經配備了ESp車身穩定系統。除了ESp此外,速銳身上還有許多實用配置,表現可圈可點。動力上速銳採用了1。5升的自然吸氣發動機。搭配5擋手動或者6擋雙離合變速箱,最大功率109馬力最大扭矩145牛米,這樣的動力輸出只能說夠用,但是這也為速銳帶來了非常親民的價格。

昨天小編美美走路一不小心,摔了個狗吃屎;這時候二貨同事說,你這種十八年的老車沒有ESp,肯定容易摔跤啊,主動安全性不行嘛。

的確,對於十八歲的小編美美來說,如果我媽生我的時候附帶了ESp技能,興許就不會摔跤了。

看↓↓↓

帶ESp的狗狗穩定性多好。

當然這都是玩笑,不過ESp對於汽車來說還真有這麼神奇的功效。它能夠通過對單獨車輪進行制動、切斷動力等方法來控制車身動態,減少車輛的失控機率。憑藉優異的主動安全表現,ESp榮獲了NCAp全球獎項,這也足以證明ESp在安全性上的巨大作用。

我們看看在時速40km的情況下汽車在冰面上做一個變線的情況

可以看到帶ESp的車輛很好的保持了正常行駛,而沒帶ESp的車,呵呵~

那麼多少錢的車會搭載ESp呢?一般來說10萬以上的自主車輛都配備了ESp,12萬以上的合資車都配備了ESp,如果你買了個合資車花了十幾萬還沒有ESp那你來找我,我來幫你罵它。那麼10萬內有沒有帶ESp的車呢?當然有,這幾台車不到8萬裸車的配置就帶有ESp了,而且綜合表現也不錯哦。

長安悅翔V7

指導價:5.99-8.79萬

長安悅翔V7是我們非常熟悉的一位車型。而最近它又增加了1.0T的動力系統。除了低配車型之外,中高配開始就搭載了ESp車身穩定系統,在安全性方面還是十分厚道的。

悅翔V7的外觀設計在同級別中數一數二,而在內飾上設計的也十分恰到好處。簡約的內飾清晰的功能分區以及不錯的人機設計。都值得豎個大拇指,但是對於悅翔V7來說最大的問題是我為什麼不加一點錢買逸動呢?

比亞迪速銳

指導價:6.99-9.59萬

比亞迪速銳是我們非常熟悉的一款車型,它以超高的配置和大空間作為主要的賣點。最初的遙控泊車着實讓我們驚艷的一把。速銳在7萬多的車型上就已經配備了ESp車身穩定系統。除了ESp此外,速銳身上還有許多實用配置,表現可圈可點。

動力上速銳採用了1.5升的自然吸氣發動機。搭配5擋手動或者6擋雙離合變速箱,最大功率109馬力最大扭矩145牛米,這樣的動力輸出只能說夠用,但是這也為速銳帶來了非常親民的價格。

雪佛蘭樂風RV

指導價:7.49-9.99萬

樂風RV雖然定位為小型車,但是由於兩廂的造型設計以及不錯的空間設計理念,樂風RV在內部空間表現上還是十分值得表揚的,樂風RV使用1.5升自然吸氣發動機匹配5擋手動或者4擋自動變速箱。

樂風RV在同價位的車型中十分罕見的全系標配ESp車身穩定系統,這也显示了通用對於安全性的重視以及嚴謹的造車態度,樂風RV可以說是一位不折不扣的實力选手。不過沒那麼主流的外觀可能會讓不少人拒絕它。

東風風行景逸S50

指導價:6.99-10.29萬

風行景逸S50外觀上就比較的平庸了,在內飾設計上則是東風風行家族的標準化設計,沒什麼亮點但是風行景逸S50在低配車型上也已經配備了ESp,而且加上較大的尺寸和完善的動力系統。風行景逸還是值得推薦的。

風行景逸S50採用1.5L和1.6L/2.0L 3款自然吸氣發動機自動車型匹配CVT變速箱。動力方面風行景逸S50表現平平,但是它的車身尺寸比較大,軸距長達兩米七,行李箱容積也達到了500升,對於家用車來說這樣的空間表現十分寬裕了。而我唯一需要考慮的是我要不要等即將上市的外觀更大氣內飾更加精緻的新款呢?

總結:

對於家用車來說,安全舒適是十分重要的,有ESp能夠大大提升你在下雨天、環島行駛等多種情況下的安全性,因此對於ESp小編美美是十分推薦的,當然更重要的還在於謹慎駕駛安全開車,如果你100km/h打死方向,神仙也救不了你。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

這三款國產家轎都帶全景攝像頭,最低只要6萬多

在市區遊走並不費力,雙離合變速箱在低速行車時會有拖拽感,偶爾也會卡在2擋不願升擋。內飾設計上,以酒紅色和黑色搭配,看上去較為時尚。中控的按鈕雖然多,但是都有中文標示,上手難度不大。6。99萬就有全景攝像頭,這在同級中也是絕無僅有。

當你開着車去到一些只有狹窄停車位的地方時,即便有倒車雷達也要挪得小心翼翼,生怕兩側或者車頭會發生一些刮蹭。不過,如果你的車有全景攝像頭的話,在這種情況下就可以從容淡定不少了。而有些車的全景攝像頭,甚至可以在低速前進時開啟,這無疑更好地解決了城村匯車時的煩惱。

雖說國產車的配置相當豐富,但是在13萬內能有全景攝像頭的汽車依舊是鳳毛麟角。下面編者我就給大家推薦三款帶全景攝像頭的國產車。

東風裕隆-納5

指導價:8.58-11.98萬

帶全景攝像頭的車型:11.58萬的自動智尊型和11.98萬的自動旗艦型

編者點評:納5的全景影像雖然精度不高且有一定程度的變形,但是已經能清楚看到周圍的情況,新手倒車也不再有難度。除了全景攝像頭以外,以上所說的那兩個車型還配備了前後駐車雷達,簡直是武裝到了牙齒。遇到一些較窄的拐角位,也不用像一般司機那樣拚命把頭往前探來看位置。

納5隻有一款1.8T發動機可選,與之匹配的是5擋手動和6擋手自一體變速箱,最大輸出180馬力和256牛米,整個動力相當充足。內飾設計以黑色為主,兼具運動感與高級感,烤黑鋼琴漆的面板,不失優雅。

比亞迪-速銳

指導價:6.99-9.59萬

帶全景攝像頭的車型:6.99萬的手動精英型、8.59萬的手動旗艦型和9.59萬的自動旗艦型

編者點評:速銳也只有一款1.5L發動機可選,與之匹配的是5擋手動和6擋雙離合變速箱,最大輸出109馬力和145牛米。在市區遊走並不費力,雙離合變速箱在低速行車時會有拖拽感,偶爾也會卡在2擋不願升擋。

內飾設計上,以酒紅色和黑色搭配,看上去較為時尚。中控的按鈕雖然多,但是都有中文標示,上手難度不大。6.99萬就有全景攝像頭,這在同級中也是絕無僅有。除了全景攝像頭,前後駐車雷達同樣沒有缺席,配置相當豐富。

東風乘用車-東風風神L60

指導價:8.97-12.97萬

帶全景攝像頭的車型:12.97萬的自動新睿型

編者點評:從显示的效果來說,確實是這三台車中最好的,四個角度的銜接自然。除了全景攝像以外,頂配車型還有無鑰匙進入與啟動這個亮點配置。整個內飾讓人覺得很有小清新的感覺,各處做工亦頗為精良,中央显示屏四周的烤黑鋼琴漆點綴得恰到好處。

動力總成上,L60提供1.6L與1.8L發動機,與前者匹配的是5擋手動變速箱,而後者則多了一副6擋手自一體變速箱。1.6L發動機最大輸出117馬力和150牛米,而1.8L發動機最大輸出139馬力和172牛米。總的來說,1.6L發動機動力偏弱,1.8L會顯得更為游刃有餘。

編者總結

隨着時代的發展,車越來越多,越做越大。有些狹窄路段,老司機應付起來都要小心翼翼,就更不用說新手了,所以帶有全景攝像頭的這三款車可以說是新手的福音。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

這三款國產小型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/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

別再寫一摞if-else了!再寫開除!兩種設計模式帶你消滅它!

題外話:本來不想解釋、可是看完評論,有點服氣。沒想到居然這麼多人能曲解題意。這篇文章明顯是在說,不要寫一大堆if-else,一大堆是啥意思很難懂嗎?我沒有一句話說了不要寫if-else。開頭也給出了具體需求,在這種需求的前提下不要寫if-else,沒毛病吧??

代碼潔癖狂們!看到一個類中有幾十個if-else是不是很抓狂?
設計模式學了用不上嗎?面試的時候問你,你只能回答最簡單的單例模式,問你有沒有用過反射之類的高級特性,回答也是否嗎?
這次就讓設計模式(模板方法模式+工廠模式)和反射助你消滅if-else!
真的是開發中超超超超超超有用的乾貨啊!

那個坑貨

某日,碼農胖滾豬接到上級一個需求,這個需求牛逼了,一站式智能報表查詢平台,支持mysql、pgxl、tidb、hive、presto、mongo等眾多數據源,想要啥數據都能通通給你查出來展示,對於業務人員數據分析有重大意義!

雖然各個數據源的參數校驗、查詢引擎和查詢邏輯都不一樣,但是胖滾豬對這些框架都很熟悉,這個難不倒她,她只花了一天時間就都寫完了。

領導胖滾熊也對胖滾豬的效率表示了肯定。可是好景不長,第三天,領導閑着沒事,準備做一下code review,可把胖滾熊驚呆了,一個類裏面有近30個if-else代碼,我滴個媽呀,這可讓代碼潔癖狂崩潰了。

// 檢驗入參合法性
Boolean check = false;
if(DataSourceEnum.hive.equals(dataSource)){
    check = checkHiveParams(params);
} else if(DataSourceEnum.tidb.equals(dataSource)){
    check = checkTidbParams(params);
} else if(DataSourceEnum.mysql.equals(dataSource)){
    check = checkMysqlParams(params);
} // else if ....... 省略pgxl、presto等
if(check){
    if(DataSourceEnum.hive.equals(dataSource)){
        list = queryHive(params);
    } else if(DataSourceEnum.tidb.equals(dataSource)){
        list = queryTidb(params);
    } else if(DataSourceEnum.mysql.equals(dataSource)){
        list = queryMysql(params);
    } // else if ....... 省略pgxl、presto等
}
//記錄日誌
log.info("用戶={} 查詢數據源={} 結果size={}",params.getUserName(),params.getDataSource(),list.size());

模板模式來救場

首先我們來分析下,不管是什麼數據源,算法結構(流程)都是一樣的,1、校驗參數合法性 2、查詢 3、記錄日誌。這不就是說模板一樣、只不過具體細節不一樣,沒錯吧?

讓我們來看看設計模式中模板方法模式的定義吧:

模板方法模式:定義一個操作中的算法的框架,而將一些步驟延遲到子類中. 使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。通俗的講,就是將子類相同的方法, 都放到其抽象父類中。

我們這需求不就和模板方法模式差不多嗎?因此我們可以把模板抽到父類(抽象類)中。至於特定的步驟實現不一樣,這些特殊步驟,由子類去重寫就好了。

廢話不多說了,我們先把父類模板寫好吧,完全一樣的邏輯是記錄日誌,這步在模板寫死就好。至於檢驗參數和查詢,這兩個方法各不相同,因此需要置為抽象方法,由子類去重寫。

public abstract class AbstractDataSourceProcesser <T extends QueryInputDomain> {
    public List<HashMap> query(T params){
        List<HashMap> list = new ArrayList<>();
        //檢驗參數合法性 不同的引擎sql校驗邏輯不一樣
        Boolean b = checkParam(params);
        if(b){
            //查詢
            list = queryData(params);
        }
        //記錄日誌
        log.info("用戶={} 查詢數據源={} 結果size={}",params.getUserName(),params.getDataSource(),list.size());
        return list;
    }
    //抽象方法 由子類來實現特定邏輯
    abstract Boolean checkParam(T params);
    abstract List<HashMap> queryData(T params);
}

這段代碼非常簡單。但是為了照顧新手,還是想解釋一個東西:

T這個玩意。叫泛型,因為不同數據源的入參不一樣,所以我們使用泛型。但是他們也有公共的參數,比如用戶名。因此為了不重複冗餘,更好的利用公共資源,在泛型的設計上,我們可以有一個泛型上限,<T extends QueryInputDomain>

public class QueryInputDomain<T> {
    public String userName;//查詢用戶名
    public String dataSource;//查詢數據源 比如mysql\tidb等
    public T params;//特定的參數 不同的數據源參數一般不一樣
}
public class MysqlQueryInput extends QueryInputDomain{
    private String database;//數據庫
    public String sql;//sql
}

接下來就輪到子類出場了,通過上面的分析,其實也很簡單了,不過是繼承父類,重寫checkParam()和queryData()方法,下面以mysql數據源為例,其他數據源也都一樣的套路:

@Component("dataSourceProcessor#mysql")
public class MysqlProcesser extends AbstractDataSourceProcesser<MysqlQueryInput>{
    @Override
    public Boolean checkParam(MysqlQueryInput params) {
        System.out.println("檢驗mysql參數是否準確");
        return true;
    }

    @Override
    public List<HashMap> queryData(MysqlQueryInput params) {
        List<HashMap> list = new ArrayList<>();
        System.out.println("開始查詢mysql數據");
        return list;
    }
}

這樣一來,所有的數據源,都自成一體,擁有一個只屬於自己的類,後續要擴展數據源、或者要修改某個數據源的邏輯,都非常方便和清晰了。

說實話,模板方法模式太簡單了,抽象類這東西也太基礎普遍了,一般應屆生都會知道的。但是對於初入職場的新人來說,還真不太能果斷應用在實際生產中。因此提醒各位:一定要有一個抽象思維,避免代碼冗餘重複。

另外,要再啰嗦幾句,即使工作有幾年的工程師也很容易犯一個錯誤。就是把思維局限在今天的需求,比如老闆一開始只給你一個mysql數據源查詢的需求,壓根沒有if-else,可能你就不會放在心上,直接在一個類中寫死,不會考慮到後續的擴展。直到後面越來越多的新需求,你才恍然大悟,要全部重構一番,這樣浪費自己的時間了。因此提醒各位:做需求不要局限於今天,要考慮到未來。 從一開始就做到高擴展性,後續需求變更和維護就非常爽了。

原創聲明:本文為【胖滾豬學編程】原創博文,轉載請註明出處。以漫畫形式讓編程生動有趣!原創不易,求關注!

工廠模式來救場

但是模板模式還是沒有完全解決胖滾豬的if-else,因為需要根據傳進來的dataSource參數,判斷由哪個service來實現查詢邏輯,現在是這麼寫的:

  if(DataSourceEnum.hive.equals(dataSource)){
        list = queryHive(params);
    } else if(DataSourceEnum.tidb.equals(dataSource)){
        list = queryTidb(params);
    }

那麼這種if-else應該怎麼去幹掉呢?我想先跟你講講工廠模式的那些故事。

工廠模式:工廠方法模式是一種創建對象的模式,它被廣泛應用在jdk中以及Spring和Struts框架中。它將創建對象的工作轉移到了工廠類。

為了呼應一下工廠兩字,我特意舉一個代工廠的例子讓你理解,這樣你應該會有更深刻的印象。

以手機製造業為例。我們知道有蘋果手機、小米手機等等,每種品牌的手機製造方法必然不相同,我們可以先定義好一個手機標準接口,這個接口有make()方法,然後不同型號的手機都繼承這個接口:

#Phone類:手機標準規範類(AbstractProduct)
public interface Phone {
    void make();
}
#MiPhone類:製造小米手機(Product1)
public class MiPhone implements Phone {
    public MiPhone() {
        this.make();
    }
    @Override
    public void make() {
        System.out.println("make xiaomi phone!");
    }
}
#IPhone類:製造蘋果手機(Product2)
public class IPhone implements Phone {
    public IPhone() {
        this.make();
    }
    @Override
    public void make() {
        System.out.println("make iphone!");
    }
}

現在有某手機代工廠:【天霸手機代工廠】。客戶只會告訴該工廠手機型號,就要匹配到不同型號的製作方案,那麼代工廠是怎麼實現的呢?其實也很簡單,簡單工廠模式(還有抽象工廠模式和工廠方法模式,有興趣可以了解下)是這麼實現的:

#PhoneFactory類:手機代工廠(Factory)
public class PhoneFactory {
    public Phone makePhone(String phoneType) {
        if(phoneType.equalsIgnoreCase("MiPhone")){
            return new MiPhone();
        }
        else if(phoneType.equalsIgnoreCase("iPhone")) {
            return new IPhone();
        }
    }
}

這樣客戶告訴你手機型號,你就可以調用代工廠類的方法去獲取到對應的手機製造類。你會發現其實也不過是if-else,但是把if-else抽到一個工廠類,由工廠類統一創建對象,對我們的業務代碼無入侵,不管是維護還是美觀上都會好很多。

首先,我們應該在每個特定的dataSourceProcessor(數據源執行器),比如MysqlProcesser、TidbProcesser中添加spring容器註解@Component。該註解我想應該不用多解釋了吧~重點是:我們可以把不同數據源都搞成類似的bean name,形如dataSourceProcessor#數據源名稱,如下兩段代碼:

@Component("dataSourceProcessor#mysql")
public class MysqlProcesser extends AbstractDataSourceProcesser<MysqlQueryInput>{
@Component("dataSourceProcessor#tidb")
public class TidbProcesser extends AbstractDataSourceProcesser<TidbQueryInput>{

這樣有什麼好處呢?我可以利用Spring幫我們一次性加載出所有繼承於AbstractDataSourceProcesser的Bean ,形如Map<String, AbstractDataSourceProcesser>,Key是Bean的名稱、而Value則是對應的Bean:

@Service
public class QueryDataServiceImpl implements QueryDataService {
    @Resource
    public Map<String, AbstractDataSourceProcesser> dataSourceProcesserMap;
    public static String beanPrefix = "dataSourceProcessor#";
    @Override
    public List<HashMap> queryData(QueryInputDomain domain) {
        AbstractDataSourceProcesser dataSourceProcesser = dataSourceProcesserMap.get(beanPrefix + domain.getDataSource());
        //省略query代碼
    }
}

可能你還是不太理解,我們直接看一下運行效果:

1、dataSourceProcesserMap內容如下所示,存儲了所有數據源Bean,Key是Bean的名稱、而Value則是對應的Bean:

2、我只需要通過key(即前綴+數據源名稱=beanName),就能匹配到對應的執行器了。比如當參數dataSource為tidb的時候,key為dataSourceProcessor#tidb,根據key可以直接從dataSourceProcesserMap中獲取到TidbProcesser

public static String classPrefix = "com.lyl.java.advance.service.";

AbstractDataSourceProcesser sourceGenerator = 
(AbstractDataSourceProcesser) Class.forName
(classPrefix+DataSourceEnum.getClasszByCode(domain.getDataSource()))
.newInstance();

需要注意的是,該種方法是通過className來獲取到類的實例,而前端傳參肯定是不會傳className過來的。因此可以用到枚舉類,去定義好不同數據源的類名:

public enum DataSourceEnum {
    mysql("mysql", "MysqlProcesser"),
    tidb("tidb", "TidbProcesser");
    private String code;
    private String classz;

原創聲明:本文為【胖滾豬學編程】原創博文,轉載請註明出處。以漫畫形式讓編程生動有趣!原創不易,求關注!

總結

有些童鞋總覺得設計模式用不上,因為平時寫代碼除了CRUD還是CRUD,面試的時候問你設計模式,你只能回答最簡單的單例模式,問你有沒有用過反射之類的高級特性,回答也是否。

其實不然,JAVA這23種設計模式,每一個都是經典。今天我們就用模板方法模式+工廠模式(或者反射)解決了讓人崩潰的if-else。後續對於設計模式的學習,也應該多去實踐,從真實的項目中找到用武之地,你才算真正把知識佔為己有了。

本篇文章的內容和技術點雖然很簡單,但旨在告訴大家應該要有一個很好的代碼抽象思維。杜絕在代碼中出現一大摞if-else或者其他爛代碼。

即使你有很好的代碼抽象思維,做需求開發的時候,也不要局限於當下,只考慮現在,要多想想未來的擴展性。

就像你談戀愛一樣,只考慮當下的是渣男,考慮到未來的,才算是一個負責任的人

“願世界沒有渣男”

原創聲明:本文為【胖滾豬學編程】原創博文,轉載請註明出處。以漫畫形式讓編程生動有趣!原創不易,求關注!

本文來源於公眾號:【胖滾豬學編程】。一枚集顏值與才華於一身,不算聰明卻足夠努力的女程序媛。用漫畫形式讓編程so easy and interesting!求關注!

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

【實戰】基於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地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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