Scrapy+Flask+Mongodb+Swift 開發全攻略(1)
好的,這期開始之前,我們先要干兩件事。第一件事是找到spiders文件夾裡的dbmeizi_scrapy.py。打開他,上一篇教程裡,這個爬蟲文件是這麼寫的。
def parse(self, response): liResults = Selector(response).xpath('//li[@class="span3"]') for li in liResults: for img in li.xpath('.//img'): item = MeiziItem() item['title'] = img.xpath('@data-title').extract() item['dataid'] = img.xpath('@data-id').extract() item['datasrc'] = img.xpath('@data-src').extract() item['startcount'] = 0 yield item
現在我們需要改成這樣.
def parse(self, response): liResults = Selector(response).xpath('//li[@class="span3"]') for li in liResults: for img in li.xpath('.//img'): item = MeiziItem() item['title'] = img.xpath('@data-title').extract()[0] item['dataid'] = img.xpath('@data-id').extract()[0] item['datasrc'] = img.xpath('@data-src').extract()[0] item['startcount'] = 0 yield item
why?
很簡單,因為用extract這個方法得到的是一個數組,而我們的每一個字段實際上是一個string而非一個array,如果不取第一個值,那麼存入mongodb之後,title這個key對應的value是一個數組,這會導致我們將mongodb裡的數據轉換成json之後需要在客戶端再進行分解。很麻煩。
第二件事,是刪除我們上一個爬蟲爬取的數據。
如圖:
ok,重新運行我們的爬蟲,scrapy crawl dbmeiziSpider,現在,check一下數據庫裡的內容,是不是以前的每個字段對應的內容已經從數組變成了string了。
開始編寫服務器
激動人心的時刻要開始了,我們要從iOS程序員變成一個菜鳥級別的server端選手。不過能用自己編寫的iOS客戶端從自己寫的server下載數據,也挺爽的,不是麼?
在編寫服務器端的時候確保你用pip安裝了下面幾個庫。
1.pymongo
2.Flask
我們的服務端代碼如下。
from flask import Flask, request import json from bson import json_util from bson.objectid import ObjectId import pymongo app = Flask(__name__) mongoClient = pymongo.MongoClient('localhost', 27017) db = mongoClient['dbmeizi'] def toJson(data): return json.dumps(data, default=json_util.default) @app.route('/meizi/', methods=['GET']) def findmeizi(): if request.method == 'GET': lim = int(request.args.get('limit', 10)) off = int(request.args.get('offset'),0) results = db['meizi'].find().skip(off).limit(lim) json_results= [] for result in results: json_results.append(result) return toJson(json_results) if __name__ == '__main__': app.run(debug=True)
以上代碼就是我們的服務端代碼,只有短短28行,python的強大之處也在於此。
好的,我來一行一行的解釋一下。
前面5行就是import各種我們需要的庫,如果後面你使用python server.py運行的時候提示錯誤,很可能是你的機子上缺少上述的庫。
app = Flask(__name__)這句話就是利用Flask的構造方法生成一個Flask實例,name是什麼?簡單來說,你創建的任何python文件(.py),都會有一個內置屬性,叫做__name__,他有兩個用途,如果你在命令行狀態下直接運行`python .py`的時候,這個時候這個python文件裡的__name__就是__main__,如果你是在別的python文件裡import *.py,那麼這個name的東西就是這個Python文件的文件名。so,這個東西常常用來判斷,你是在import還是直接在命令行裡運行這個文件。
所以,上一行,我們生成了一個Flask實例並且把這個實例賦給了app這個變量。
mongoClient = pymongo.MongoClient('localhost', 27017) db = mongoClient['dbmeizi']
這兩句很簡單,就是用pymongo這個第三方庫,打開我們的mongodb數據庫,並且拿到我們的dbmeizi這個database。
def toJson(data): return json.dumps(data, default=json_util.default)
這句話,我們定義了一個函數,用來把mongodb裡的數據轉換為json格式。用來返回給我們的ios客戶端。
@app.route('/meizi/', methods=['GET'])
這句話的意思就是Flask的一種寫法,意思就是當我們發起了一個request,並且這個request的方法是get,url是"localhost:5000/meizi/"這種的的時候,我們就執行findmeizi()這個方法。
def findmeizi(): if request.method == 'GET': lim = int(request.args.get('limit', 10)) off = int(request.args.get('offset'),0) results = db['meizi'].find().skip(off).limit(lim) json_results= [] for result in results: json_results.append(result) return toJson(json_results)
這個方法就是我們的http server監測到用戶發起get請求,並且URL是形如'http://127.0.0.1:5000/sightings/?offset=0&limit=3'的時候,我們取出limit這個值,賦給lim這個變量,然後取出offset這個值,賦給off。
然後呢?利用我們的db(就是剛才利用pymongo獲取的mongodb實例),取出‘meizi’這個collection,skip(off)的意思就是跳過前面多少行,limit(lim)表示從數據庫取出多少個值。
整句話的意思就是,從meizi這個collection裡跳過前off個值,取後面的lim個值。
現在取到的數據都在results變量裡,我們遍歷results,放入json_results這個數組裡,然後把數組轉換成json格式返回給客戶端。
我們運行一下試試。
python server.py
perfect!
數據已經返回給浏覽器了。
這時候我們編寫一個簡單的iOS客戶端,驗證一下。
我們建立一個swift的iOS程序,用cocoapods安裝下列庫。
platform :ios, '8.0' use_frameworks! target 'HotGirls' do pod 'Alamofire' pod 'Kingfisher' end target 'HotGirlsTests' do end
注意,cocoapods務必升級到最新版,要不然安裝swift的第三方庫會出現問題。
我寫了一個超簡單的iOS客戶單,純驗證下服務端是否有效。
class ViewController: UIViewController { @IBOutlet weak var mTableView: UITableView! var imageURLStringArray:NSMutableArray = NSMutableArray() override func viewDidLoad() { super.viewDidLoad() Alamofire.request(.GET, "http://localhost:5000/meizi/?offset=0&limit=10") .responseJSON { (_, _, JSON, _) in let resultArray:NSArray = JSON as! NSArray for dict in resultArray{ self.imageURLStringArray.addObject(dict["datasrc"] as! String) } print(self.imageURLStringArray) self.mTableView.reloadData() } // Do any additional setup after loading the view, typically from a nib. } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }
extension ViewController:UITableViewDelegate, UITableViewDataSource{ func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell:ImageTableViewCell = tableView.dequeueReusableCellWithIdentifier("ImageViewCellID") as! ImageTableViewCell var imageURL:NSString = imageURLStringArray[indexPath.row] as! NSString cell.meiziImageView.kf_setImageWithURL(NSURL(string: imageURL as String)!) return cell } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return imageURLStringArray.count } func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { return 250.0 }
運行一下。
哈哈,大功告成。數據返回正確。
github地址:https://github.com/zangqilong198812/MeiziServer
但是,這樣就結束了麼?
遠遠沒有,爬蟲完全是個半成品,服務端簡直就是個玩笑,客戶端什麼炫酷特效交互都沒有,做這樣的東西簡直是打自己的臉。
現在,萬裡長征只開始了一小步。
1.爬蟲如何自己定時運行?
2.Mongodb如何避免插入重復數據?
3.Server如何提供多個接口。
4.如何Put,delete,Post,Get
5.POP,AsyncDisplaykit,collectionviewlayout,Custom Transition,Bézier curve,Private Cocoapods,Continuous integration,Unit Test.
現在,到了我展現真正實力的時候了。
請允許我小小的裝一下13
我們經過之前的學習,掌握了哪些內容呢?
1.學會了如何用python的scrapy框架來趴一些數據。
2.學會了把爬蟲爬來的數據存入mongodb。
3.學會了怎麼用Flask寫一個簡單的接口。
那麼,從現在開始,我們會把注意力集中在如何寫一個iOS app上。(主要原因是我這個server端菜??還在看服務端的資料,所以只能先做app了)。
先看一張圖。
我們的app大概長這個樣子。
關注我微博的人應該知道我前段時間做了一個card效果,主要也是為了寫這個界面,因為card那個效果應該是整個頁面最復雜的了。飯要一口一口吃吃,我們今天先講一個簡單地,就是圖中顯示PASSES的那個圓形view。
這個動畫加載的順序大概是。
1.首先是一個圓形的環的形成動畫。
2.然後是數字的滾動效果。
3.細看的話,數字滾動的速度是不同的,十位數滾動的稍慢。個位數略快一些。那麼首先腦子裡的想法就是這肯定不能用一個label來顯示,用兩個label來分別顯示個位和十位比較好做動畫。
ok,我們先來做最簡單的圓環形成的動畫。
那很簡單,用CAShapelayer來做strokeEnd動畫就行了。
我們新建一個類,繼承自UIView,然後改名叫StartView.
然後呢,給這個類添加一個屬性。
var progressLayer: CAShapeLayer = CAShapeLayer()
然後重寫我們的init方法。
加入下列代碼
progressLayer.lineWidth = 2 progressLayer.fillColor = UIColor.clearColor().CGColor progressLayer.strokeColor = UIColor.whiteColor().CGColor progressLayer.path = UIBezierPath(ovalInRect: self.bounds).CGPath self.layer.addSublayer(progressLayer)
現在我們的StarView類是這樣的。
然後呢,我們先看看效果,在Viewcontroller裡添加一個我們剛剛寫的StarView,運行模擬器。看看效果。
很好,已經有一個圓環了。
剛才我說過,要做出畫出圓環的那種動畫要使用一個叫做strokeEnd的動畫。為什麼呢?
我早期在一篇blog中講過,CABasicAnimation的keypath來源於何處。
其實keypath就是來源於CALayer的所有屬性,也就是說,layer的所有屬性都能放到keypath裡做動畫,而CAShapeLayer有一個叫做strokeEnd的屬性,專門是用來檢測CAShapeLayer的path屬性是否被賦值,如果賦值的話就畫出path的路徑。so,我們就可以用這個strokeEnd屬性做一些繪畫的動畫。
直接看代碼。
func startDrawCircleAnimation(){ var pathAnimation:CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd") pathAnimation.fromValue = 0 pathAnimation.toValue = 1 pathAnimation.duration = 0.5 progressLayer.addAnimation(pathAnimation, forKey: "pathAnimation") }
這裡我定義了一個函數,專門用來執行這個畫圓環的動畫,然後把動畫時長設置為0.5.
然後我們來運行一下。
確實是執行了畫圓環的操作,但是問題來了。繪畫的起點並不正確,起點是在三點鐘方向,而我們的原型是需要在12點鐘方向開始畫。
問題在哪呢?
問題就在progressLayer.path = UIBezierPath(ovalInRect: self.bounds).CGPath 這句話上。
因為這個函數就是默認創建一個起點在三點鐘方向的oval(圓形),所以我們通過這個函數創建的圓環是沒有辦法指定起點的。
現在我們有兩個解決方案,第一個是讓我們的layer逆時針方向做一個90度的transform,那麼我們的子layer也逆時針旋轉了九十度,剛好從3點鐘方向旋轉到了12點鐘方向。
但是這種解決方案並不合適。因為會留下很多後遺症。
比如說,你如果後期需要在cashapelayer加一些東西的話,所有的子layer全部會旋轉。
所以我們需要改變progressLayer的path的創建方式。
var radius:CGFloat = CGRectGetWidth(self.bounds)/2.0 var center = CGPointMake(radius, radius) var startAngle = -M_PI_2 var endAngle = M_PI_2*3.0 var circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true) progressLayer.path = circlePath.CGPath
我們用畫弧線的方法畫一個圓,因為這種方法可以指定startAngle和endAngle。運行一下,看看。
好的,已經能正確顯示了。
接下來是我們的滾動Label。
這種能上下滾動的label最好的方法就是創建一個scrollView,然後在scrollview中添加十個label,label的內容就是數字0-10.
ok,我們新建一個類。
然後加入如下代碼。
新建這個enum的原因就是我們的十位數和個位數彈跳的速率不同,所以我們在初始化的時候加入了一個type,用來區別創建的是十位數label還是個位數label。
然後就是創建了一個scrollview,在豎直的方向添加了十個label。
並且設置了一下每個label的字體,顏色等等屬性。
然後創建一個開始動畫的方法。
然後我們運行一下。
好的,label可以滾動了。那麼其實問題也來了。
這兩個scroll的滾動速度是一樣的,而我們的需求是十位數的速度要慢一點。
可能熟悉scrollView的同學會知道scrollView有一個屬性叫做decelerationRate 可以改變滑動速率。但是這個屬性有兩個問題,1是只有在pageEnable開啟的情況下才能生效,2是無法很細致的改動。
所以我們用另一個方法,用UIView的animation方法。
代碼如下
if scrollType == .SingleDigitType{ UIView.animateWithDuration(0.85, animations: { () -> Void in self.singleDigitsScroll.contentOffset = CGPointMake(0, CGRectGetHeight(self.bounds) * CGFloat(num)) }) }else { UIView.animateWithDuration(1, animations: { () -> Void in self.singleDigitsScroll.contentOffset = CGPointMake(0, CGRectGetHeight(self.bounds) * CGFloat(num)) }) }
相信不用我做過多解釋也能看懂。就是更改scrollView的contentoffset。
ok,核心組件已經完成。那麼我們把它組裝起來。
最後運行效果如下。
最後項目地址在這:https://github.com/zangqilong198812/HotGirls