第七色在线视频,2021少妇久久久久久久久久,亚洲欧洲精品成人久久av18,亚洲国产精品特色大片观看完整版,孙宇晨将参加特朗普的晚宴

為了賬號(hào)安全,請(qǐng)及時(shí)綁定郵箱和手機(jī)立即綁定
2.1 TemplateResponse 和 SimpleTemplateResponse

很早之前,我們介紹過(guò) HttpResponse,它用于生成 HTTP 請(qǐng)求的相應(yīng),返回的內(nèi)容由 content 屬性確定,主要是用于提供靜態(tài)內(nèi)容顯示。TemplateResponse 對(duì)象則不同,它允許裝飾器或中間件在通過(guò)視圖構(gòu)造響應(yīng)之后修改響應(yīng)內(nèi)容。TemplateResponse 對(duì)象保留視圖提供的、用于計(jì)算響應(yīng)內(nèi)容的模板和上下文數(shù)據(jù),直到最后需要時(shí)才計(jì)算相應(yīng)內(nèi)容并返回響應(yīng)。SimpleTemplateResponse 對(duì)象是 TemplateResponse 的父類,兩者功能和使用基本類似,幾乎是一致的。# 源碼位置: django/template/response.pyclass TemplateResponse(SimpleTemplateResponse): rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_request'] def __init__(self, request, template, context=None, content_type=None, status=None, charset=None, using=None): super().__init__(template, context, content_type, status, charset, using) self._request = requestSimpleTemplateResponse 是繼承自 HttpResponse 對(duì)象,并做了諸多擴(kuò)展。它的重要屬性和方法如下:重要屬性:template_name:模板文件名;context_data : 上下文字典數(shù)據(jù);rendered_content:指使用當(dāng)前的模板和上下文字段數(shù)據(jù)已渲染的響應(yīng)內(nèi)容。一個(gè)作為屬性的方法,調(diào)用該屬性時(shí)啟動(dòng)渲染過(guò)程;is_rendered: 布爾類型,判斷響應(yīng)內(nèi)容是否已經(jīng)被渲染。重要方法:__init__(template, context=None, content_type=None, status=None, charset=None, using=None):類初始化函數(shù),各參數(shù)的含義與 HttpResponse 相同;resolve_context(context):預(yù)處理會(huì)被用于模板渲染的上下文數(shù)據(jù) ;resolve_template(template):接收(如由 get_template() 返回的) backend-dependent 的模板對(duì)象、模板名字、或者多個(gè)模板名字組成的列表。返回 backend-dependent 的模板對(duì)象實(shí)例,后面用于渲染;add_post_render_callback():添加渲染完成后的回調(diào)函數(shù),如果該方法運(yùn)行時(shí)渲染已完成,回調(diào)函數(shù)會(huì)被立即調(diào)用;render():設(shè)置 response.content 的結(jié)果為 SimpleTemplateResponse.rendered_content 的值,執(zhí)行所有渲染后的回調(diào)函數(shù),返回所有響應(yīng)對(duì)象。render() 只會(huì)在第一次調(diào)用時(shí)起作用。在隨后的調(diào)用中,它將返回從第一個(gè)調(diào)用獲得的結(jié)果。實(shí)驗(yàn)部分:我們來(lái)使用 TemplateResponse 來(lái)完成一個(gè)簡(jiǎn)單的案例。同樣是在 first_django_app 工程中,準(zhǔn)備的代碼內(nèi)容參考如下,分別是模板文件、視圖文件以及 URLConf 配置文件。# 模板文件: template/test.html<p>{{ content }}</p><div>{{ spyinx.age }}</div># URLConf配置文件: hello_app/urls.pyurlpatterns = [ path('test-cbv/', views.TestView.as_view(), name="test-cbv")]# 視圖文件: hello_app/views.pydef my_render_callback(response): # Do content-sensitive processing print('執(zhí)行渲染完成后的回調(diào)函數(shù),渲染內(nèi)容:\n{}\n是否完成渲染:{}'.format(response.rendered_content, response.is_rendered))class TestView(View): def get(self, request, *args, **kwargs): response = TemplateResponse(request, 'test.html', context={'content': '正文1', 'spyinx':{'age': 29}}) response.add_post_render_callback(my_render_callback) return response我們?cè)谠浦鳈C(jī)上使用 curl 命令發(fā)送 HTTP 請(qǐng)求,觀察結(jié)果:# 使用runserver命令啟動(dòng)first_django_app工程...# 打開(kāi)另一個(gè)xshell窗口,使用curl命令發(fā)送請(qǐng)求結(jié)果[root@server ~]# curl http://127.0.0.1:8888/hello/test-cbv/<p>正文1</p><div>29</div># 回到上一個(gè)窗口,查看打印結(jié)果(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888Watching for file changes with StatReloaderPerforming system checks...System check identified no issues (0 silenced).April 16, 2020 - 07:10:18Django version 2.2.11, using settings 'first_django_app.settings'Starting development server at http://0.0.0.0:8888/Quit the server with CONTROL-C.執(zhí)行渲染完成后的回調(diào)函數(shù),渲染內(nèi)容:<p>正文1</p><div>29</div>是否完成渲染:True[16/Apr/2020 07:10:38] "GET /hello/test-cbv/ HTTP/1.1" 200 29

3. Pandas 排名操作

排名操作是根據(jù)數(shù)據(jù)的大小,判斷出該數(shù)據(jù)在數(shù)據(jù)集中的名次,默認(rèn)是從 1 開(kāi)始一直到數(shù)據(jù)中有效數(shù)據(jù)的長(zhǎng)度,如果存在重復(fù)數(shù)據(jù),則會(huì)求出這幾個(gè)數(shù)據(jù)的平均排名。Pandas 庫(kù)中針對(duì)排名操作提供了方便的操作函數(shù) rank () .df.rank(axis=0, method='average', numeric_only=None, na_option='keep', ascending=True, pct=False)接下來(lái)我們列舉該函數(shù)常用的一些參數(shù):參數(shù)名說(shuō)明 axis 指定是在行上,還是列上進(jìn)行排名。默認(rèn)是 axis=0 從列上進(jìn)行排名 method 平級(jí)排名的取值方法,有四種方式。method 用于平級(jí)數(shù)據(jù),也就是要排名的數(shù)據(jù)中,他們的大小是一樣的,這種平級(jí)的數(shù)據(jù)有四種排名的方式:average:在一組相等的排名數(shù)據(jù)中,為每個(gè)數(shù)據(jù)取他們的平均排名;min:在一組相等的排名數(shù)據(jù)中,使用最小的排名給每個(gè)數(shù)據(jù);max:在一組相等的排名數(shù)據(jù)中,使用最大的排名給每個(gè)數(shù)據(jù);first:在一組相等的排名數(shù)據(jù)中,按各個(gè)值在原始數(shù)據(jù)中的出現(xiàn)順序進(jìn)行排名。下面我們通過(guò)程序代碼詳細(xì)講解排名函數(shù)的用法:# 導(dǎo)入pandas包import pandas as pddata_path="C:/Users/13965/Documents/myFuture/IMOOC/pandasCourse-progress/data_source/第15小節(jié)/execl數(shù)據(jù)demo.xlsx"# 解析數(shù)據(jù)data = pd.read_excel(data_path)print(data)# --- 輸出結(jié)果 --- BB AA CC EE DD0 11 3 3230.0 45.6 20.01 4 2 2124.0 67.0 NaN2 7 23 345.0 33.9 23.03 5 11 2361.0 59.5 4.04 10 45 326.0 69.9 55.05 33 33 NaN 75.0 67.0# rank() 函數(shù)# 1. 默認(rèn)的 axis=0,在列上進(jìn)行排序data_res=data.rank()print(data_res)# --- 輸出結(jié)果 --- BB AA CC EE DD0 5.0 2.0 5.0 2.0 2.01 1.0 1.0 3.0 4.0 NaN2 3.0 4.0 2.0 1.0 3.03 2.0 3.0 4.0 3.0 1.04 4.0 6.0 1.0 5.0 4.05 6.0 5.0 NaN 6.0 5.0# 結(jié)果解析: 通過(guò) rank() 函數(shù)默認(rèn)的在列方向進(jìn)行排名的操作,比如"BB"列中,每一個(gè)數(shù)據(jù)在該列中的名次都得到了體現(xiàn),但對(duì)于出現(xiàn)的缺失值 NaN 數(shù)據(jù),是不進(jìn)行排名計(jì)算的。# 2. 設(shè)置的 axis=1,在行放方向上進(jìn)行排序data_res=data.rank(axis=1)print(data_res)# --- 輸出結(jié)果 --- BB AA CC EE DD0 2.0 1.0 5.0 4.0 3.01 2.0 1.0 4.0 3.0 NaN2 1.0 2.5 5.0 4.0 2.53 2.0 3.0 5.0 4.0 1.04 1.0 2.0 5.0 4.0 3.05 1.5 1.5 NaN 4.0 3.0# 結(jié)果解析:這里我們?cè)O(shè)置了在行上進(jìn)行排名的計(jì)算,這里看到最后一行,因?yàn)槲覀冊(cè)瓟?shù)據(jù)該行的前兩列數(shù)據(jù)時(shí)候相同的,都是 33,這里因?yàn)槟J(rèn)的 method=average,所有33取了他們平均排名 1+2 的平均值 1.5。 接下來(lái)我們?cè)O(shè)置 method=first ,看一下處理效果:# 3. 設(shè)置 method=‘first’,修改默認(rèn)的相同排名的處理方式data_res=data.rank(axis=1,method='first')print(data_res)# --- 輸出結(jié)果 --- BB AA CC EE DD0 2.0 1.0 5.0 4.0 3.01 2.0 1.0 4.0 3.0 NaN2 1.0 2.0 5.0 4.0 3.03 2.0 3.0 5.0 4.0 1.04 1.0 2.0 5.0 4.0 3.05 1.0 2.0 NaN 4.0 3.0# 結(jié)果解析:同樣的我們還是看最后一行,因?yàn)槲覀冊(cè)O(shè)置了 method='first' ,所以對(duì)于相同排名的數(shù)據(jù),會(huì)使用該數(shù)據(jù)在原數(shù)據(jù)中的出現(xiàn)順序進(jìn)行處理,所以第五行的前兩列的數(shù)據(jù)排名分別為 1,2。

2.2 鏈接

鏈接語(yǔ)法aapt2 link path-to-input-files [options] -ooutputdirectory/outputfilename.apk --manifest AndroidManifest.xml在以下示例中,AAPT2 將兩個(gè)中間文件(drawable_Image.flat 和 values_values.arsc.flat)與 AndroidManifest.xml 文件進(jìn)行了合并。AAPT2 會(huì)根據(jù) android.jar 文件鏈接結(jié)果,該文件中包含了 android 軟件包中定義的資源:aapt2 link -o output.apk -I android_sdk/platforms/android_version/android.jar compiled/res/values_values.arsc.flat compiled/res/drawable_Image.flat --manifest /path/to/AndroidManifest.xml -v鏈接選項(xiàng)命令選項(xiàng)說(shuō)明-o指定鏈接的資源 APK 的輸出路徑。–manifest指定要構(gòu)建的 Android 清單文件的路徑。-I提供平臺(tái)的 android.jar 或其他 APK(如 framework-res.apk)的路徑。-A指定要包含在 APK 中的資產(chǎn)目錄。-R傳遞要鏈接的單個(gè) .flat 文件,使用 overlay 語(yǔ)義。–package-id指定要用于應(yīng)用的軟件包 ID。–allow-reserved-package-id允許使用保留的軟件包 ID。–java指定要在其中生成 R.java 的目錄。–proguard為 ProGuard 規(guī)則生成輸出文件。–proguard-conditional-keep-rules為主 dex 的 ProGuard 規(guī)則生成輸出文件。–no-auto-version停用自動(dòng)樣式和布局 SDK 版本控制。–no-version-vectors停用矢量可繪制對(duì)象的自動(dòng)版本控制。–no-version-transitions停用轉(zhuǎn)換資源的自動(dòng)版本控制。–no-resource-deduping禁止在兼容配置中自動(dòng)刪除具有相同值的重復(fù)資源。–enable-sparse-encoding允許使用二進(jìn)制搜索樹(shù)對(duì)稀疏條目進(jìn)行編碼。-z要求對(duì)標(biāo)記為“建議”的字符串進(jìn)行本地化。-c提供以英文逗號(hào)分隔的配置列表。–preferred-density允許 AAPT2 選擇最相符的密度并刪除其他所有密度。–output-to-dir將 APK 內(nèi)容輸出到 -o 指定的目錄中。–min-sdk-version設(shè)置要用于 AndroidManifest.xml 的默認(rèn)最低 SDK 版本。–target-sdk-version設(shè)置要用于 AndroidManifest.xml 的默認(rèn)目標(biāo) SDK 版本。–version-code指定沒(méi)有版本代碼時(shí)要注入 AndroidManifest.xml 中的版本代碼。–compile-sdk-version-name指定沒(méi)有版本名稱時(shí)要注入 AndroidManifest.xml 中的版本名稱。–proto-format以 Protobuf 格式生成已編譯的資源。–non-final-ids使用非最終資源 ID 生成 R.java。–emit-ids在給定的路徑上生成一個(gè)文件,該文件包含資源類型的名稱及其 ID 映射的列表。–stable-ids使用通過(guò) --emit-ids 生成的文件,該文件包含資源類型的名稱以及為其分配的 ID 的列表。–custom-package指定要在其下生成 R.java 的自定義 Java 軟件包。–extra-packages生成相同的 R.java 文件,但軟件包名稱不同。–add-javadoc-annotation向已生成的所有 Java 類添加 JavaDoc 注釋。–output-text-symbols生成包含指定文件中 R 類的資源符號(hào)的文本文件。–auto-add-overlay允許在疊加層中添加新資源。–rename-manifest-package重命名 AndroidManifest.xml 中的軟件包。–rename-instrumentation-target-package更改插樁的目標(biāo)軟件包的名稱。-0指定不想壓縮的文件的擴(kuò)展名。–split根據(jù)一組配置拆分資源,以生成另一個(gè)版本的 APK。-v可提高輸出的詳細(xì)程度。

1.2 CrawlerRunner

該類有比較詳細(xì)的注釋,源碼里對(duì)它的介紹如下:這是一個(gè)簡(jiǎn)便的輔助類,用于跟蹤、管理和運(yùn)行已設(shè)置中的爬蟲(chóng)程序。我們先來(lái)看該類的實(shí)例化方法:# 源碼位置:scrapy/crawler.py# ...class CrawlerRunner: # ... @staticmethod def _get_spider_loader(settings): """ Get SpiderLoader instance from settings """ cls_path = settings.get('SPIDER_LOADER_CLASS') # 導(dǎo)入spider loader類 loader_cls = load_object(cls_path) excs = (DoesNotImplement, MultipleInvalid) if MultipleInvalid else DoesNotImplement try: verifyClass(ISpiderLoader, loader_cls) except excs: # 打印異常信息 # ... return loader_cls.from_settings(settings.frozencopy()) def __init__(self, settings=None): # scrapy全局配置 if isinstance(settings, dict) or settings is None: settings = Settings(settings) self.settings = settings # 根據(jù)爬蟲(chóng)名,加載相應(yīng)的Spdier類 self.spider_loader = self._get_spider_loader(settings) # 用于保存所有的Crawler對(duì)象 self._crawlers = set() # self._active用于保存調(diào)用Crawler.crawl()方法,返回的defer對(duì)象 self._active = set() self.bootstrap_failed = False self._handle_twisted_reactor()注意比較重要的屬性值有:self.settings:保存了全局的配置信息;self.spider_loader:用于根據(jù)相應(yīng)的爬蟲(chóng)名加載對(duì)應(yīng)的 Spider 類;self._crawlers:用于保存所有的 Crawler 對(duì)象,使用的是 python 中的集合類型;接下來(lái)我們看看該類中幾個(gè)重要方法:# 源碼位置:scrapy/crawler.py# ...class CrawlerRunner: # ... @property def spiders(self): # 打印告警信息 # ... return self.spider_loader def crawl(self, crawler_or_spidercls, *args, **kwargs): if isinstance(crawler_or_spidercls, Spider): # 拋出異常,不能是Spider對(duì)象,只能是Spider類或者Crawler對(duì)象 # ... crawler = self.create_crawler(crawler_or_spidercls) return self._crawl(crawler, *args, **kwargs) def _crawl(self, crawler, *args, **kwargs): self.crawlers.add(crawler) # 調(diào)用crawl()方法生成一個(gè)Deferred對(duì)象 d = crawler.crawl(*args, **kwargs) # 加入活躍爬蟲(chóng)的集合 self._active.add(d) def _done(result): # 執(zhí)行完后的收尾工作 self.crawlers.discard(crawler) self._active.discard(d) self.bootstrap_failed |= not getattr(crawler, 'spider', None) return result # 將_done()方法加入成功和失敗的回調(diào)鏈中 return d.addBoth(_done) def create_crawler(self, crawler_or_spidercls): if isinstance(crawler_or_spidercls, Spider): # 拋出異常,不能是Spider對(duì)象,只能是Spider類或者Crawler對(duì)象 # ... if isinstance(crawler_or_spidercls, Crawler): # 如果是Crawler實(shí)例 return crawler_or_spidercls # 否則創(chuàng)建Crawler實(shí)例 return self._create_crawler(crawler_or_spidercls) def _create_crawler(self, spidercls): if isinstance(spidercls, str): spidercls = self.spider_loader.load(spidercls) # 返回Crawler實(shí)例 return Crawler(spidercls, self.settings) 上面的代碼量都比較少,比較容易理解。不用糾結(jié)代碼的細(xì)節(jié),把握代碼的作用即可,對(duì)于 Scrapy 框架有一個(gè)全貌即可。來(lái)簡(jiǎn)單描述下上面的方法:crawl():爬蟲(chóng)開(kāi)始執(zhí)行爬取動(dòng)作,crawl 命令調(diào)用的入口方法;create_crawler():創(chuàng)建一個(gè) Crawler 實(shí)例;在看看這個(gè)類剩余的幾個(gè)方法:# 源碼位置:scrapy/crawler.py# ...class CrawlerRunner: # ... def stop(self): return defer.DeferredList([c.stop() for c in list(self.crawlers)]) @defer.inlineCallbacks def join(self): while self._active: yield defer.DeferredList(self._active) def _handle_twisted_reactor(self): if self.settings.get("TWISTED_REACTOR"): verify_installed_reactor(self.settings["TWISTED_REACTOR"])來(lái)簡(jiǎn)單說(shuō)明下上面幾個(gè)方法:stop() 和 join() 方法都是停止或者同步管理的爬蟲(chóng),返回一個(gè) Deferred 對(duì)象;_handle_twisted_reactor:默認(rèn)配置中 TWISTED_REACTOR 的值為 None。其校驗(yàn)安裝的 reactor 代碼也比較簡(jiǎn)單,就是導(dǎo)入 TWISTED_REACTOR 路徑對(duì)應(yīng)的類,然后判斷 twisted 中的 reactor 是否為該類的一個(gè)實(shí)例,不是就拋出異常:# 源碼位置:scrapy/utils/reactor.py# ...def verify_installed_reactor(reactor_path): from twisted.internet import reactor # 導(dǎo)入reactor_path位置的類 reactor_class = load_object(reactor_path) # 判斷twisted中的reactor是否為其實(shí)例 if not isinstance(reactor, reactor_class): # 拋出異常 # ...到此,這個(gè)類的相關(guān)屬性及方法就介紹完畢了,接下來(lái)我們學(xué)習(xí)它的一個(gè)子類:CrawlerProcess。

5.1 字符串函數(shù)

字符串函數(shù)主要提供了字符串類型的相關(guān)操作,就像在 javascript 中一樣,Sass 提供的字符串函數(shù)可以獲取字符串的長(zhǎng)度,字符串的下標(biāo)以及字符串中的大小寫(xiě)字母轉(zhuǎn)換等等。5.1.1 quote ($ string) 和 unquote($ string)這兩個(gè)函數(shù)我們放在一起講解,它們都接收 1 個(gè)參數(shù),參數(shù)是字符串類型,quote($string) 函數(shù)的返回結(jié)果是 以帶引號(hào)的形式返回你傳入的字符串,反之 unquote($string) 函數(shù)的返回結(jié)果是以不帶引號(hào)的形式返回你傳入的字符串,我們舉例看下:string.quote(aaa) //=> "aaa"unquote("bbb") //=> bbb5.1.2 str-index($string, $substring)str-index($string, $substring) 函數(shù)接收 2 個(gè)參數(shù),返回 $substring 在 $string 中的第一次出現(xiàn)的索引,如果在 $string 中不包含 $substring 則返回 null ,我們舉例看下:str-index("abcde", "a") //=> 1str-index("abcde", "c") //=> 35.1.3 str-insert($string, $insert, $index)看見(jiàn) insert 這個(gè)詞我們就能猜到,這個(gè)函數(shù)是用于字符串的插入,str-insert($string, $insert, $index) 函數(shù)接收 3 個(gè)參數(shù),第 1 個(gè)參數(shù)是一個(gè)字符串,第 2 個(gè)參數(shù)是要插入的字符串,第 3 個(gè)參數(shù)是插入的位置,返回結(jié)果是插入后的字符串:str-insert("abcde", "j", 1) //=> "jabcde"str-insert("abcde", "j", 4) //=> "abcjde"str-insert("abcde", "j", 100) //=> "abcdej"str-insert("abcde", "j", -20) //=> "jabcde"從上面的例子我們可以看到,當(dāng)?shù)?3 個(gè)參數(shù)大于 $string 的長(zhǎng)度,將會(huì)插入到,末尾;反之,如果小于 $string 長(zhǎng)度的負(fù)值,則會(huì)插入到開(kāi)始位置。5.1.4 str-length($string)這個(gè)函數(shù)用于獲取傳入的字符串的長(zhǎng)度,只接收一個(gè)字符串參數(shù),返回值是它的長(zhǎng)度,返回值是 number 類型,我們舉例看下:str-length("abcde") //=> 55.1.5 str-slice($string, $start-at, $end-at)這個(gè)函數(shù)用于字符串的截取,str-slice($string, $start-at, $end-at) 函數(shù)接收 3 個(gè)參數(shù),第 1 個(gè)參數(shù)是一個(gè)字符串,第 2 個(gè)參數(shù)是要截取的開(kāi)始位置,第 3 個(gè)參數(shù)是要截取的結(jié)束位置,返回結(jié)果是截取到的字符串;要記住 Sass 的字符串截取函數(shù)返回的字符串是包含截取的開(kāi)始和結(jié)束位置字符的,我們舉例看下:str-slice("abcde", 1, 2) //=> "ab"str-slice("abcde", 2, 4) //=> "bcd"5.1.6 to-upper-case($string) 和 to-lower-case($string)這兩個(gè)函數(shù)我們放在一起來(lái)講解,它們都接收 1 個(gè)字符串參數(shù);to-upper-case($string) 函數(shù) 將傳入的字符串轉(zhuǎn)換為大寫(xiě)并返回,to-lower-case($string) 函數(shù)將傳入的字符串轉(zhuǎn)換為小寫(xiě)并返回:to-upper-case("abcde") //=> "ABCDE" 轉(zhuǎn)為大寫(xiě)to-upper-case("Abc") //=> "ABC" 轉(zhuǎn)為大寫(xiě)to-lower-case("ABC") //=> "abc" 轉(zhuǎn)為小寫(xiě)to-lower-case("Abc") //=> "abc" 轉(zhuǎn)為小寫(xiě)5.1.7 unique-id()unique-id() 函數(shù)會(huì)返回一個(gè)隨機(jī)的字符串,并且這個(gè)字符串在 Sass 編譯中是唯一的,這個(gè)我們用得不多,不過(guò)當(dāng)你需要生成一個(gè)唯一的字符串標(biāo)識(shí)的時(shí)候你可以使用它:unique-id() //=> urgdjis上面我們講解了字符串函數(shù),字符串函數(shù)可以讓你方便地操作字符串,還為你提供了對(duì)字符串的增刪改查功能,下面我們來(lái)講解數(shù)字函數(shù)。

1. TemplateView 類介紹和使用

TemplateView 視圖類是用來(lái)渲染給定的模板文件,其上下文字典包含從 URL 中捕獲的參數(shù)。首先來(lái)看看最簡(jiǎn)單的模板渲染示例:準(zhǔn)備模板文件,放到 template 目錄,在 settings.py 中需要配置好模板 (TEMPLATES) 相關(guān)的參數(shù):[root@server first_django_app]# cat templates/test.html <p>{{ content }}</p><div>{{ spyinx.age }}</div>準(zhǔn)備好類視圖,處理相關(guān) HTTP 請(qǐng)求,這里使用今天要學(xué)習(xí)的 TemplateView 類:class TestTemplateView(TemplateView): template_name = 'test.html' @csrf_exempt def dispatch(self, request, *args, **kwargs): return super(TestTemplateView, self).dispatch(request, *args, **kwargs)配置相應(yīng)的 URLConf:context_data = {'content':'正文1', 'spyinx':{'age': 29}}urlpatterns = [ path('test_template_view/', views.TestTemplateView.as_view(extra_context=context_data), name='test_template_View')]啟動(dòng) first_django_app 工程,然后使用 curl 命令簡(jiǎn)單測(cè)試,發(fā)送 GET 請(qǐng)求:# 啟動(dòng)first_django_app工程,監(jiān)聽(tīng)8888端口(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888Watching for file changes with StatReloaderPerforming system checks...System check identified no issues (0 silenced).April 16, 2020 - 12:27:49Django version 2.2.11, using settings 'first_django_app.settings'Starting development server at http://0.0.0.0:8888/Quit the server with CONTROL-C.# 打開(kāi)xshell另一個(gè)窗口,使用curl發(fā)送get請(qǐng)求[root@server ~]# curl http://127.0.0.1:8888/hello/test_template_view/<p>正文1</p><div>29</div># 這個(gè)報(bào)405錯(cuò)誤,不支持POST請(qǐng)求[root@server ~]# curl -XPOST http://127.0.0.1:8888/hello/test_template_view/[root@server ~]#可以看到,我們只是指定了 template_name 屬性,連對(duì)應(yīng)的請(qǐng)求函數(shù)都沒(méi)寫(xiě) (寫(xiě)的 dispatch() 函數(shù)只是為了能執(zhí)行POST請(qǐng)求,避免 csrf 報(bào)錯(cuò)),只是繼承一下這個(gè)視圖類 (TemplateView) 能處理 GET 請(qǐng)求,并能返回渲染的模板,對(duì)一些情況來(lái)說(shuō)是很方便的,節(jié)省了一些代碼。注意的是,其他請(qǐng)求方式不行,因?yàn)?TemplateView 內(nèi)部只實(shí)現(xiàn)了 get() 方法,所以只能處理 GET 請(qǐng)求。上面的 context_data 是自定義的,現(xiàn)在我們來(lái)從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù),動(dòng)態(tài)填充模板內(nèi)容。同樣是涉及到模板視圖,所以可以通過(guò) TemplateView 類來(lái)實(shí)現(xiàn)。首先準(zhǔn)備新的模板文件 test.html。這個(gè)模板頁(yè)面使用表格分頁(yè)顯示會(huì)員數(shù)據(jù),查詢的數(shù)據(jù)表是我們前面多次實(shí)驗(yàn)的 member 表。<html><head><style type="text/css"> .page{ margin-top: 10px; font-size: 14px; } .member-table { width: 50%; text-align: center; }</style></head><body><p>會(huì)員信息-第{{ current_page }}頁(yè), 每頁(yè){{ page_size }}條, 總共{{ sum }}條</p><div><table border="1" class="member-table"> <thead> <tr> <th>姓名</th> <th>年齡</th> <th>性別</th> <th>職業(yè)</th> <th>所在城市</th> </tr> </thead> <tbody> {% for member in members %} <tr> <td>member.name</td> <td>member.age</td> {% if member.sex == 0 %} <td>男</td> {% else %} <td>女</td> {% endif %} <td>member.occupation</td> <td>member.city</td> </tr> {% endfor %} </tbody></table><div ><div class="page"></div></div></div></body></html>修改上面的視圖函數(shù),分頁(yè)查詢 Member 表中的數(shù)據(jù):class TestTemplateView(TemplateView): template_name = 'test.html' def get(self, request, *args, **kwargs): params = request.GET page = int(params.get('page', 1)) size = int(params.get('size', 5)) data = {} data['sum'] = Member.objects.all().count() members = Member.objects.all()[(page - 1) * size:page * size] data['current_page'] = page data['page_size'] = size data['members'] = members return self.render_to_response(context=data)這里我們使用了前面學(xué)習(xí)的 Django ORM 模型,獲取 member 表中的數(shù)據(jù),然后使用 TemplateView 中的模板渲染方法 render_to_response() 返回渲染后的 HTML 文本給到客戶端。測(cè)試結(jié)果如下,以下兩張圖片是設(shè)置了不同的查詢參數(shù)(當(dāng)前頁(yè)碼和頁(yè)大小),可以看到查詢參數(shù)是起了效果的:

1.2 Django 中使用 Mixin

首先需要了解一下 Mixin 的概念,這里有一篇介紹 Python 中 Mixin 的文章:<<多重繼承>> ,可以認(rèn)真看下,加深對(duì) Mixin 的理解。在我的理解中,Mixin 其實(shí)就是單獨(dú)的一塊功能類。假設(shè) Django 中提供了 A、B、C 三個(gè)視圖類,又有 X、Y、Z三個(gè) Mixin 類。如果我們想要視圖 A,同時(shí)需要額外的 X、Y功能,那么使用 Python 中的多重繼承即可達(dá)到目的:class NewView(A, X, Y): """ 定義新的視圖 """ pass我們來(lái)看看 Django 的官方文檔是如何引出 Mixin 的:Django’s built-in class-based views provide a lot of functionality, but some of it you may want to use separately. For instance, you may want to write a view that renders a template to make the HTTP response, but you can’t use TemplateView;perhaps you need to render a template only on POST, with GET doing something else entirely. While you could use TemplateResponse directly, this will likely result in duplicate code.For this reason, Django also provides a number of mixins that provide more discrete functionality. Template rendering, for instance, is encapsulated in the TemplateResponseMixin.翻譯過(guò)來(lái)就是: Django 內(nèi)置的類視圖提供了許多功能,但是我們可能只需要其中的一部分功能。例如我想寫(xiě)一個(gè)視圖,該視圖使用由模板文件渲染后的 HTML 來(lái)響應(yīng)客戶端的 HTTP 請(qǐng)求,但是我們又不能使用 TemplateView 來(lái)實(shí)現(xiàn),因?yàn)槲抑幌朐?POST 請(qǐng)求上使用這個(gè)模板渲染的功能,而在 GET 請(qǐng)求時(shí)做其他事情。當(dāng)然,可以直接使用 TemplateResponse 來(lái)完成,這樣就會(huì)導(dǎo)致代碼重復(fù)?;谶@個(gè)原因, Django 內(nèi)部提供了許多離散功能的 mixins??梢钥吹?,這里的 mixins 就是一些單獨(dú)功能的類,配合視圖類一起使用,用于組合出各種功能的視圖。接下來(lái),我們結(jié)合前面的 Member 表來(lái)使用下 mixin 功能。具體的步驟如下:改造原來(lái)的視圖類-TestView。我們給原來(lái)的視圖類多繼承一個(gè) mixin,用于實(shí)現(xiàn)單個(gè)對(duì)象查找查找功能;from django.shortcuts import renderfrom django.http import HttpResponsefrom django.views.decorators.csrf import csrf_exemptfrom django.views.generic import Viewfrom django.views.generic.detail import SingleObjectMixinfrom .models import Member# Create your views here.class TestView(SingleObjectMixin, View): model = Member def get(self, request, *args, **kwargs): return HttpResponse('hello, get\n') def post(self, request, *args, **kwargs): self.object = self.get_object() return HttpResponse('hello, {}\n'.format(self.object.name)) def put(self, request, *args, **kwargs): return HttpResponse('hello, put\n') def delete(self, request, *args, **kwargs): return HttpResponse('hello, delete\n') @csrf_exempt def dispatch(self, request, *args, **kwargs): return super(TestView, self).dispatch(request, *args, **kwargs)修改 URLConf 配置,傳遞一個(gè)動(dòng)態(tài)參數(shù),用于查找表中記錄:urlpatterns = [ path('test-cbv/<int:pk>/', views.TestView.as_view(), name="test-cbv")]啟動(dòng)服務(wù)器,然后進(jìn)行測(cè)試:[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/2/hello, 會(huì)員2[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/4/hello, spyinx-0[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/9/hello, spyinx-5[root@server first_django_app]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/9/hello, get[root@server first_django_app]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/9/hello, put[root@server first_django_app]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/9/hello, delete可以看到在 POST 請(qǐng)求中,我們通過(guò)傳遞主鍵值,就能返回 Member 表中對(duì)應(yīng)記錄中的 name 字段值,這一功能正是由SingleObjectMixin 中的 get_object() 方法提供的。通過(guò)繼承這個(gè)查詢功能,我們就不用再使用 ORM 模型進(jìn)行查找了,這簡(jiǎn)化了我們的代碼。當(dāng)然,這只能滿足一小部分的場(chǎng)景,對(duì)于更多復(fù)雜的場(chǎng)景,我們還是需要實(shí)現(xiàn)自己的邏輯,我們也可以把復(fù)雜的功能拆成各種 mixin,然后相關(guān)組合繼承,這樣可以很好的復(fù)用代碼,這是一種良好的編碼方式。

3.2 成員方法

StringBuilder 類下面也提供了很多與 String 類相似的成員方法,以方便我們對(duì)字符串進(jìn)行操作。下面我們將舉例介紹一些常用的成員方法。3.2.1 字符串連接可以使用 StringBuilder 的 StringBuilder append(String str) 方法來(lái)實(shí)現(xiàn)字符串的連接操作。我們知道,String 的連接操作是通過(guò) + 操作符完成連接的:String str1 = "Hello";String str2 = "World";String str3 = str1 + " " + str2;如下是通過(guò) StringBuilder 實(shí)現(xiàn)的字符串連接示例:708運(yùn)行結(jié)果:Hello World由于 append() 方法返回的是一個(gè) StringBuilder 類型,我們可以實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用。例如,上述連續(xù)兩個(gè) append() 方法的調(diào)用語(yǔ)句,可以簡(jiǎn)化為一行語(yǔ)句:str.append(" ").append("World");如果你使用 IDE 編寫(xiě)如上連接字符串的代碼,可能會(huì)有下面這樣的提示(IntelliJ idea 的代碼截圖):提示內(nèi)容說(shuō)可以將 StringBuilder 類型可以替換為 String 類型,也就是說(shuō)可以將上邊地代碼改為:String str = "Hello" + " " + "World";這樣寫(xiě)并不會(huì)導(dǎo)致執(zhí)行效率的下降,這是因?yàn)?Java 編譯器在編譯和運(yùn)行期間會(huì)自動(dòng)將字符串連接操作轉(zhuǎn)換為 StringBuilder 操作或者數(shù)組復(fù)制,間接地優(yōu)化了由于 String 的不可變性引發(fā)的性能問(wèn)題。值得注意的是,append() 的重載方法有很多,可以實(shí)現(xiàn)各種類型的連接操作。例如我們可以連接 char 類型以及 float 類型,實(shí)例如下:709運(yùn)行結(jié)果:小明的身高為:172.5上面代碼里連續(xù)的兩個(gè) append() 方法分別調(diào)用的是重載方法 StringBuilder append(char c) 和 StringBuilder append(float f)。3.2.2 獲取容量可以使用 int capacity() 方法來(lái)獲取當(dāng)前容量,容量指定是可以存儲(chǔ)的字符數(shù)(包含已寫(xiě)入字符),超過(guò)此數(shù)將進(jìn)行自動(dòng)分配。注意,容量與長(zhǎng)度(length)不同,長(zhǎng)度指的是已經(jīng)寫(xiě)入字符的長(zhǎng)度。例如,構(gòu)造方法 StringBuilder() 構(gòu)造一個(gè)空字符串生成器,初始容量為 16 個(gè)字符。我們可以獲取并打印它的容量,實(shí)例如下:710運(yùn)行結(jié)果:str的初始容量為:16連接操作后,str的容量為343.2.3 字符串替換可以使用 StringBuilder replace(int start, int end, String str) 方法,來(lái)用指定字符串替換從索引位置 start 開(kāi)始到 end 索引位置結(jié)束(不包含 end)的子串。實(shí)例如下:711運(yùn)行結(jié)果:Hello Java!也可使用 StringBuilder delete(int start, int end) 方法,先來(lái)刪除索引位置 start 開(kāi)始到 end 索引位置(不包含 end)的子串,再使用 StringBuilder insert(int offset, String str) 方法,將字符串插入到序列的 offset 索引位置。同樣可以實(shí)現(xiàn)字符串的替換,例如:StringBuilder str = new StringBuilder("Hello World!");str.delete(6, 11);str.insert(6, "Java");3.2.4 字符串截取可以使用 StringBuilder substring(int start) 方法來(lái)進(jìn)行字符串截取,例如,我們想截取字符串的后三個(gè)字符,實(shí)例如下:712運(yùn)行結(jié)果:str截取后子串為:慕課網(wǎng)如果我們想截取示例中的” 歡迎 “二字,可以使用重載方法 StringBuilder substring(int start, int end) 進(jìn)行截?。篠tring substring = str.substring(3, 5);3.2.5 字符串反轉(zhuǎn)可以使用 StringBuildr reverse() 方法,對(duì)字符串進(jìn)行反轉(zhuǎn)操作,例如:713運(yùn)行結(jié)果:str經(jīng)過(guò)反轉(zhuǎn)操作后為:avaJ olleH

1. Django ORM 模型的增刪改查操作

話不多說(shuō),直接進(jìn)入 django 的交互命令模式:[root@server ~]# pyenv activate django-manual pyenv-virtualenv: prompt changing will be removed from future release. configure `export PYENV_VIRTUALENV_DISABLE_PROMPT=1' to simulate the behavior.(django-manual) [root@server ~]# cd django-manual/first_django_app/(django-manual) [root@server first_django_app]# python manage.py shellPython 3.8.1 (default, Dec 24 2019, 17:04:00) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>>前面在 hello_app 應(yīng)用目錄下的 models.py 中定義了 Member 模型類,導(dǎo)入進(jìn)來(lái)。然后我們實(shí)例化 Member 類,并給實(shí)例的屬性賦值,最后調(diào)用模型類的 save() 方法,將該實(shí)例保存到表中:>>> from hello_app.models import Member>>> from hello_app.models import Member>>> m1 = Member()>>> m1.name = 'spyinx'>>> m1.age = 29>>> m1.sex = 0>>> m1.occupation = "程序員">>> m1.phone_num = '18054293763'>>> m1.city = 'guangzhou'>>> m1.save()通過(guò) mysql 客戶端可以查看該保存的記錄,如下:[root@server first_django_app]# mysql -u store -pstore.123@ -h 180.76.152.113 -P 9002Welcome to the MariaDB monitor. Commands end with ; or \g.Your MySQL connection id is 73555Server version: 5.7.26 MySQL Community Server (GPL)Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.MySQL [(none)]> use django_manualReading table information for completion of table and column namesYou can turn off this feature to get a quicker startup with -ADatabase changedMySQL [django_manual]> select * from member where 1=1\G;*************************** 1. row *************************** id: 1 name: spyinx age: 29 sex: 0 occupation: 程序員 phone_num: 18054293763 email: city: guangzhouregister_date: 2020-04-05 07:30:45.0433771 row in set (0.00 sec)ERROR: No query specifiedMySQL [django_manual]>接下來(lái)是查詢的操作介紹,為了能更好的演示查詢操作,我們通過(guò)如下代碼在 member 中添加100條記錄:from datetime import datetimeimport randomimport MySQLdboccupations = ['web', 'server', 'ops', 'security', 'teacher', 'ui', 'product', 'leader']cities = ['beijing', 'guangzhou', 'shenzhen', 'shanghai', 'wuhan']def gen_phone_num(): phone_num = "18" for i in range(9): phone_num += str(random.randint(0, 9)) return phone_numconn = MySQLdb.connect(host='180.76.152.113', port=9002, user='store', passwd='store.123@', db='django_manual')conn.autocommit(True)data = (('spyinx-%d' % i, \ random.randint(20, 40), \ random.randint(0, 1), \ occupations[random.randint(0, len(occupations) - 1)], \ gen_phone_num(), \ '22%d@qq.com' % i, \ cities[random.randint(0, len(cities) - 1)], \ datetime.now().strftime("%Y-%m-%d %H:%M:%S")) for i in range(100))try: cursor = conn.cursor() cursor.executemany('insert into member(`name`, `age`, `sex`, `occupation`, `phone_num`, `email`, `city`, `register_date`) values (%s, %s, %s, %s, %s, %s, %s, %s);', data) print('批量插入完成')except Exception as e: print('插入異常,執(zhí)行回滾動(dòng)作: {}'.format(str(e))) conn.rollback()finally: if conn: conn.close()執(zhí)行 python 代碼后,我們通過(guò) mysql 客戶端確認(rèn)100條數(shù)據(jù)已經(jīng)成功插入到數(shù)據(jù)庫(kù)中:MySQL [django_manual]> select count(*) from member where 1=1\G;*************************** 1. row ***************************count(*): 1011 row in set (0.00 sec)我們執(zhí)行如下操作:>>> type(Member.objects)<class 'django.db.models.manager.Manager'>>>> Member.objects.get(name='spyinx')<Member: <spyinx, 18054293763>>>>> Member.objects.all().count()101>>> type(Member.objects.all())<class 'django.db.models.query.QuerySet'>上面的語(yǔ)句中 Member.objects.get(name='spyinx') 中,objects 是一個(gè)特殊的屬性,通過(guò)它來(lái)查詢數(shù)據(jù)庫(kù),它是模型的一個(gè) Manager。首先來(lái)看看這個(gè) Manager 類提供的常用方法:all():查詢所有結(jié)果,返回的類型為 QuerySet 實(shí)例;filter(**kwargs):根據(jù)條件過(guò)濾查詢結(jié)果,返回的類型為 QuerySet 實(shí)例;get(**kwargs):返回與所給篩選條件相匹配的記錄,只返回一個(gè)結(jié)果。如果符合篩選條件的記錄超過(guò)一個(gè)或者沒(méi)有都會(huì)拋出錯(cuò)誤,返回的類型為模型對(duì)象實(shí)例;exclude(**kwargs):和 filter() 方法正好相反,篩選出不匹配的結(jié)果,返回的類型為 QuerySet 實(shí)例;values(*args):返回一個(gè)ValueQuerySet,一個(gè)特殊的QuerySet,運(yùn)行后得到的并不是一系列 model 的實(shí)例化對(duì)象,而是一個(gè)可迭代的字典序列;values_list(*args):它與 values() 類似,只不過(guò) values_list() 返回的是一個(gè)元組序列,而 values() 返回的是一個(gè)字典序列;order_by(*args):對(duì)結(jié)果按照傳入的字段進(jìn)行排序,返回的類型為 QuerySet 實(shí)例;reverse():對(duì)查詢結(jié)果反向排序,返回的類型為 QuerySet 實(shí)例;distinct():去掉查詢結(jié)果中重復(fù)的部分,返回的類型為 QuerySet 實(shí)例;count():返回?cái)?shù)據(jù)庫(kù)中匹配查詢的記錄數(shù),返回類型為 int;first():返回第一條記錄,結(jié)果為模型對(duì)象實(shí)例;;last():返回最后一條記錄,結(jié)果為模型對(duì)象實(shí)例;exists():如果 QuerySet 包含數(shù)據(jù),就返回 True,否則返回 False。如果上述這些方法的返回結(jié)果是一個(gè) QuerySet 實(shí)例,那么它也同樣具有上面這些方法,因此可以繼續(xù)調(diào)用,形成鏈?zhǔn)秸{(diào)用,示例如下:>>> Member.objects.all().count()101>>> Member.objects.all().reverse().first()<Member: <spyinx-99, 18022422977>>此外,在 filter() 方法中還有一些比較神奇的雙下劃線輔助我們進(jìn)一步過(guò)濾結(jié)果:MySQL [django_manual]> select id, name, phone_num from member where name like 'spyinx-2%';+----+-----------+-------------+| id | name | phone_num |+----+-----------+-------------+| 24 | spyinx-2 | 18627420378 || 42 | spyinx-20 | 18687483216 || 43 | spyinx-21 | 18338528387 || 44 | spyinx-22 | 18702966393 || 45 | spyinx-23 | 18386787195 || 46 | spyinx-24 | 18003292724 || 47 | spyinx-25 | 18160946579 || 48 | spyinx-26 | 18517339819 || 49 | spyinx-27 | 18575613014 || 50 | spyinx-28 | 18869175798 || 51 | spyinx-29 | 18603950130 |+----+-----------+-------------+11 rows in set (0.00 sec)>>> Member.objects.all().filter(name__contains='spyinx-2')<QuerySet [<Member: <spyinx-2, 18627420378>>, <Member: <spyinx-20, 18687483216>>, <Member: <spyinx-21, 18338528387>>, <Member: <spyinx-22, 18702966393>>, <Member: <spyinx-23, 18386787195>>, <Member: <spyinx-24, 18003292724>>, <Member: <spyinx-25, 18160946579>>, <Member: <spyinx-26, 18517339819>>, <Member: <spyinx-27, 18575613014>>, <Member: <spyinx-28, 18869175798>>, <Member: <spyinx-29, 18603950130>>]>>>> Member.objects.all().filter(name__contains='spyinx-2').filter(id__lt=47, id__gt=42)<QuerySet [<Member: <spyinx-21, 18338528387>>, <Member: <spyinx-22, 18702966393>>, <Member: <spyinx-23, 18386787195>>, <Member: <spyinx-24, 18003292724>>]>這種雙下劃線的過(guò)濾字段有:contains/icontains:過(guò)濾字段的值包含某個(gè)字符串的結(jié)果;in:和 SQL 語(yǔ)句中的 in 類似,過(guò)濾字段的值在某個(gè)列表內(nèi)的結(jié)果,比如:>>> Member.objects.all().filter(name__contains='spyinx-2').filter(id__in=[42, 43])<QuerySet [<Member: <spyinx-20, 18687483216>>, <Member: <spyinx-21, 18338528387>>]>lt/gt:過(guò)濾字段值小于或者大于某個(gè)值的結(jié)果;range:過(guò)濾字段值在某個(gè)范圍內(nèi)的結(jié)果;>>> Member.objects.all().filter(name__contains='spyinx-2').filter(id__range=[48, 50])<QuerySet [<Member: <spyinx-26, 18517339819>>, <Member: <spyinx-27, 18575613014>>, <Member: <spyinx-28, 18869175798>>]>startswith/istartswith:匹配字段的值以某個(gè)字符串開(kāi)始,前面的 i 標(biāo)識(shí)是否區(qū)分大小寫(xiě);endswith/iendswiths:匹配字段的值以某個(gè)字符串結(jié)束;>>> Member.objects.all().filter(id__gt=90).filter(name__endswith='2')<QuerySet [<Member: <spyinx-72, 18749521006>>, <Member: <spyinx-82, 18970386795>>, <Member: <spyinx-92, 18324708274>>]>F查詢和Q查詢前面我們構(gòu)造的過(guò)濾器都只是將字段值與某個(gè)常量做比較。如果我們要對(duì)兩個(gè)字段的值做比較,就需要使用 Django 提供 F() 來(lái)做這樣的比較。F() 的實(shí)例可以在查詢中引用字段,來(lái)比較同一個(gè) model 實(shí)例中兩個(gè)不同字段的值:>>> from django.db.models import Q# 找出id值大于age*4的記錄>>> Member.objects.all().filter(id__gt=F('age')*4)<QuerySet [<Member: <spyinx-70, 18918359267>>, <Member: <spyinx-77, 18393464230>>, <Member: <spyinx-90, 18272147421>>, <Member: <spyinx-91, 18756265752>>, <Member: <spyinx-92, 18324708274>>, <Member: <spyinx-97, 18154031313>>]># 將所有記錄中age字段的值加1>>> Member.objects.all().update(age=F('age')+1)101此外,前面的多個(gè) filter() 方法實(shí)現(xiàn)的是過(guò)濾條件的 “AND” 操作,如果想實(shí)現(xiàn)過(guò)濾條件 “OR” 操作呢,就需要使用到 Django 為我們提供的 Q() 方法:>>> from django.db.models import Q# 過(guò)濾條件的 OR 操作>>> Member.objects.all().filter(Q(name='spyinx-22') | Q(name='spyinx-11'))<QuerySet [<Member: <spyinx-11, 18919885274>>, <Member: <spyinx-22, 18702966393>>]># 過(guò)濾條件的 AND 操作>>> Member.objects.all().filter(Q(name__contains='spyinx-2') & Q(name__endswith='2'))<QuerySet [<Member: <spyinx-2, 18627420378>>, <Member: <spyinx-22, 18702966393>>]>對(duì)于記錄的更新和刪除操作,我們同樣有對(duì)應(yīng)的 update() 方法以及 delete() 方法:# 刪除name=spyinx的記錄>>> Member.objects.all().filter(Q(name='spyinx')).delete()(1, {'hello_app.Member': 1})>>> Member.objects.all().count()100# 所有記錄的年齡字段加1>>> Member.objects.all().update(age=F('age')+1)101

1. 實(shí)戰(zhàn):直接構(gòu)建鏡像

首先 我們需要新建一個(gè)目錄 dockerfiledir,用于存放 Dockerfile 文件。mkdir dockerfiledir# 在這個(gè)目錄下新建個(gè)空文件 Dockerfile,之后填充內(nèi)容 touch dockerfiledir/Dockerfile新建一個(gè)目錄code,用來(lái)存放flask和c的源代碼。mkdir code將之前 app.py 和 helloworld.c 兩個(gè)源碼文件放入到 code 目錄下,當(dāng)前的目錄結(jié)構(gòu)應(yīng)該是這樣的:進(jìn)入 dockerfiledir 目錄,編輯 Dockerfile 文件:# 從 ubuntu系統(tǒng)鏡像開(kāi)始構(gòu)建FROM ubuntu # 標(biāo)記鏡像維護(hù)者信息MAINTAINER user <user@imooc.com># 切換到鏡像的/app目錄,不存在則新建此目錄WORKDIR /app# 將 宿主機(jī)的文件拷貝到容器中COPY ../code/app.py .COPY ../code/helloworld.c .# 安裝依賴 編譯helloworldRUN apt update >/dev/null 2>&1 && \ apt install -y gcc python3-flask python3-redis >/dev/null 2>&1 && \ cc /app/helloworld.c -o /usr/bin/helloworld# 設(shè)定執(zhí)行用戶為userRUN useradd userUSER user# 設(shè)定flask所需的環(huán)境變量ENV FLASK_APP app# 默認(rèn)啟動(dòng)執(zhí)行的命令CMD ["flask", "run", "-h", "0.0.0.0"]# 將flask的默認(rèn)端口暴露出來(lái)EXPOSE 5000然后執(zhí)行:docker build .出現(xiàn)如下報(bào)錯(cuò):COPY failed: Forbidden path outside the build context: ../code/app.py ()解決這個(gè)問(wèn)題,需要引入一個(gè)重要的概念——構(gòu)建上下文。docker build .命令在執(zhí)行時(shí),當(dāng)前目錄.被指定成了構(gòu)建上下文,此目錄中的所有文件或目錄都將被發(fā)送到 Docker 引擎中去,Dockerfile中的切換目錄和復(fù)制文件等操作只會(huì)對(duì)上下文中的內(nèi)容生效。Tips:在默認(rèn)情況下,如果不額外指定 Dockerfile 的話,會(huì)將構(gòu)建上下文對(duì)應(yīng)的目錄下 Dockerfile 的文件作為 Dockerfile。但這只是默認(rèn)行為,實(shí)際上 Dockerfile 的文件名并不要求必須為 Dockerfile,而且并不要求必須位于上下文目錄中,比如可以用 -f ../demo.txt參數(shù)指定父級(jí)目錄的demo.txt文件作為 Dockerfile。一般來(lái)說(shuō),我們習(xí)慣使用默認(rèn)的文件名 Dockerfile,將其置于鏡像構(gòu)建上下文目錄.中。我們需要將 code 目錄納入到上下文中,一個(gè)直接的方法是,調(diào)整dockerfile中的COPY指令的路徑。# 將 .. 改為 .COPY ./code/app.py .COPY ./code/helloworld.c .然后將 code 所在的目錄指定為構(gòu)建上下文。由于我們當(dāng)前的目錄是 dockerfiledir,所以我們執(zhí)行:docker build -f ./Dockerfile ..如果你留意查看構(gòu)建過(guò)程,會(huì)發(fā)現(xiàn)類似這樣的提示:Sending build context to Docker daemon 421.309 MB如果..目錄除了code和dockerfiledir,還包含其他的文件或目錄,docker build也會(huì)將這個(gè)數(shù)據(jù)傳輸給Docker,這會(huì)增加構(gòu)建時(shí)間。避免這種情況,有兩種解決方法:使用.dockerignore文件:在構(gòu)建上下文的目錄下新建一個(gè).dockerignore文件來(lái)指定在傳遞給 docker 時(shí)需要忽略掉的文件或文件夾。.dockerignore 文件的排除模式語(yǔ)法和 Git 的 .gitignore 文件相似。使用一個(gè)干凈的目錄作為構(gòu)建上下文(推薦):使用 Dockerfile 構(gòu)建鏡像時(shí)最好是將 Dockerfile 放置在一個(gè)新建的空目錄下。然后將構(gòu)建鏡像所需要的文件添加到該目錄中。在我們當(dāng)前的示例中,將code目錄移入dockerfiledir。mv ../code .現(xiàn)在的目錄層級(jí)如下:執(zhí)行 docker build -t myhello . 執(zhí)行構(gòu)建即可獲得我們的自定義鏡像 myhello。使用鏡像 myhello 創(chuàng)建 myhello 容器:# 這里使用--net=host,方便使用之前章節(jié)中部署的redis容器服務(wù),與之進(jìn)行數(shù)據(jù)交換docker run -dit --net=host --name myhello myhello 確保部署之前的 redis 容器正常啟動(dòng),然后在 Docker 宿主機(jī)的瀏覽器中訪問(wèn)http://127.0.0.1:5000:說(shuō)明 myhello 中的 flask 應(yīng)用已經(jīng)正常運(yùn)行了。接下來(lái),我們?cè)龠\(yùn)行測(cè)試一下編譯的 helloworld。docker exec myhello /usr/bin/helloworld得到輸出:Hello, World!Tips: myhello容器已經(jīng)完成任務(wù),記得執(zhí)行docker rm -f myhello刪除它.

1. 初識(shí) Admin Web

首先 Django 工程中默認(rèn)自帶 Admin 管理工具并作為內(nèi)置應(yīng)用在 settings.py 中的 INSTALLED_APPS 上進(jìn)行了注冊(cè):INSTALLED_APPS = [ # 注冊(cè) admin 應(yīng)用 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 注冊(cè)應(yīng)用 'hello_app']接下來(lái)為了能訪問(wèn)到這個(gè)后臺(tái)管理的 Web 頁(yè)面,Django 在初始化項(xiàng)目時(shí),自動(dòng)為我們添加了這個(gè) Web 頁(yè)面的相關(guān)的路由信息:# first_django_app/urls.pyurlpatterns = [ # admin后臺(tái)管理頁(yè)面的地址 url('admin/', admin.site.urls),]注意:如果不想要這個(gè)自帶的后臺(tái)管理系統(tǒng),也可以直接刪除這個(gè) URLconf 配置即可。我們啟動(dòng)測(cè)試的 Django 工程,然后手動(dòng)訪問(wèn)這個(gè) admin 后臺(tái),具體操作如下:(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888Watching for file changes with StatReloaderPerforming system checks...System check identified no issues (0 silenced).You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.Run 'python manage.py migrate' to apply them.April 12, 2020 - 15:11:39Django version 2.2.11, using settings 'first_django_app.settings'Starting development server at http://0.0.0.0:8888/Quit the server with CONTROL-C.注意:對(duì)于上面出現(xiàn)的告警信息是因?yàn)槲覀儧](méi)有做數(shù)據(jù)庫(kù)的遷移。在 Django 中為我們?cè)O(shè)計(jì)了一些內(nèi)部的表,比如用來(lái)保存 admin 管理工具賬號(hào)的表,比如保存 session 的表等。只需要使用 Django 提供的 makemigrations 和 migrate 命令即可:(django-manual) [root@server first_django_app]# python manage.py makemigrationsMigrations for 'hello_app': hello_app/migrations/0002_auto_20200412_1512.py - Alter field vip_level on member(django-manual) [root@server first_django_app]# python manage.py migrateOperations to perform: Apply all migrations: admin, auth, contenttypes, hello_app, sessionsRunning migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying hello_app.0002_auto_20200412_1512... OK Applying sessions.0001_initial... OK可以通過(guò) Navicate 工具查看到 Django 給我們生成了 10 張表內(nèi)部表,這 10 張表之間存在許多關(guān)聯(lián)的地方。如下圖所示:再次使用 runserver 命令啟動(dòng) Django 工程時(shí),就不會(huì)再有遷移相關(guān)的告警提示了。這個(gè)時(shí)候我們?cè)L問(wèn) admin/ 地址,就可以看到如下的登錄頁(yè)面。這里的登錄賬號(hào)和密碼我們可以通過(guò) Django 提供的 createsuperuser 命令完成:(django-manual) [root@server first_django_app]# python manage.py createsuperuserUsername (leave blank to use 'root'): adminEmail address: 22@11.comPassword: Password (again): Superuser created successfully.上面命令執(zhí)行成功后,我們可以看到數(shù)據(jù)庫(kù)的 auth_user 表中多出了一條用戶信息的記錄,正是我們上面設(shè)置的用戶名和密碼信息。使用這個(gè)剛創(chuàng)建的用戶名和密碼登錄進(jìn)入管理系統(tǒng)頁(yè)面如下:在首頁(yè)中,我們可以對(duì)用戶和用戶組進(jìn)行管理,包括對(duì)表中數(shù)據(jù)的更新、修改和刪除。如下是新增用戶以及新增完成后的操作示例。完成新增完成后,還可以選擇該條記錄進(jìn)行調(diào)整,直接影響的就是數(shù)據(jù)庫(kù)中的 auth_user 表。除了管理內(nèi)置的表外,Django 的 admin 功能還可以管理我們定義的模型表,實(shí)現(xiàn)基本的增刪改查操作。我們繼續(xù)用前面的會(huì)員表和會(huì)員等級(jí)表進(jìn)行實(shí)操。在 hello_app/admin.py 文件中添加如下代碼,將我們定義的模型注冊(cè)到 admin 模塊中:from django.contrib import adminfrom .models import Member, VIPLevel# Register your models here.admin.site.register([Member, VIPLevel])重啟 Django 工程,然后繼續(xù)訪問(wèn) admin 管理頁(yè)面,讓我們可以看到我們定義的模型出現(xiàn)在了管理頁(yè)面上。而且我們還可以操作我們的模型,進(jìn)行增刪改查數(shù)據(jù)。注意:這里表中數(shù)據(jù)的展示方式是按照模型類中定義的 __str__ 魔法函數(shù)決定的。這里可以看到 Member 表的關(guān)聯(lián)表中數(shù)據(jù),使用起來(lái)非常方便。

3.1 Django 中 Cookie 操作相關(guān)源碼

從前面的操作 Cookie 講解中,我們只用到了和增和查兩部分的方法,分別對(duì)應(yīng) HttpResponse 和 HttpRequest 兩個(gè)類。接下來(lái),我們?nèi)?duì)應(yīng)的源碼中查找所涉及的和 Cookie 相關(guān)的代碼。request.COOKIES['xxx']request.COOKIES.get('xxx', None)# 源碼位置:django/core/handlers/wsgi.pyclass WSGIRequest(HttpRequest): # ... @cached_property def COOKIES(self): raw_cookie = get_str_from_wsgi(self.environ, 'HTTP_COOKIE', '') return parse_cookie(raw_cookie) # ...# 源碼位置:django/http/cookie.pyfrom http import cookies# For backwards compatibility in Django 2.1.SimpleCookie = cookies.SimpleCookie# Add support for the SameSite attribute (obsolete when PY37 is unsupported).cookies.Morsel._reserved.setdefault('samesite', 'SameSite')def parse_cookie(cookie): """ Return a dictionary parsed from a `Cookie:` header string. """ cookiedict = {} for chunk in cookie.split(';'): if '=' in chunk: key, val = chunk.split('=', 1) else: # Assume an empty name per # https://bugzilla.mozilla.org/show_bug.cgi?id=169091 key, val = '', chunk key, val = key.strip(), val.strip() if key or val: # unquote using Python's algorithm. cookiedict[key] = cookies._unquote(val) return cookiedict上面的代碼并不復(fù)雜,在 WSGIRequest 類中的 COOKIES 屬性是先從客戶端請(qǐng)求中取出 Cookie 信息,調(diào)用 get_str_from_wsgi() 方法是從 WSGI 中拿到對(duì)應(yīng)的 Cookie 字符串。接下來(lái)用 parse_cookie() 方法將原始 Cookie 字符串中的 key=value 解析出來(lái)做成字典形式并返回。這就是為什么我們能像操作字典一樣操作 request.COOKIES 的原因。下面的方法是實(shí)驗(yàn)1中調(diào)用的 get_signed_cookie() 的源碼,也不復(fù)雜,同樣是從self.COOKIES 中取出對(duì)應(yīng) key 的 value 值,然后使用對(duì)應(yīng)的 salt 解密即可。# 源碼位置:django/http/request.py class HttpRequest: # ... def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None): """ Attempt to return a signed cookie. If the signature fails or the cookie has expired, raise an exception, unless the `default` argument is provided, in which case return that value. """ try: cookie_value = self.COOKIES[key] except KeyError: if default is not RAISE_ERROR: return default else: raise try: value = signing.get_cookie_signer(salt=key + salt).unsign( cookie_value, max_age=max_age) except signing.BadSignature: if default is not RAISE_ERROR: return default else: raise return value # ...接下來(lái)是涉及到創(chuàng)建 Cookie 的方法,我們需要查找 HttpResponse 類或者相關(guān)的父類:# 源碼位置:django/http/response.pyclass HttpResponseBase: # ... def set_cookie(self, key, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, samesite=None): """ Set a cookie. ``expires`` can be: - a string in the correct format, - a naive ``datetime.datetime`` object in UTC, - an aware ``datetime.datetime`` object in any time zone. If it is a ``datetime.datetime`` object then calculate ``max_age``. """ self.cookies[key] = value if expires is not None: if isinstance(expires, datetime.datetime): if timezone.is_aware(expires): expires = timezone.make_naive(expires, timezone.utc) delta = expires - expires.utcnow() # Add one second so the date matches exactly (a fraction of # time gets lost between converting to a timedelta and # then the date string). delta = delta + datetime.timedelta(seconds=1) # Just set max_age - the max_age logic will set expires. expires = None max_age = max(0, delta.days * 86400 + delta.seconds) else: self.cookies[key]['expires'] = expires else: self.cookies[key]['expires'] = '' if max_age is not None: self.cookies[key]['max-age'] = max_age # IE requires expires, so set it if hasn't been already. if not expires: self.cookies[key]['expires'] = http_date(time.time() + max_age) if path is not None: self.cookies[key]['path'] = path if domain is not None: self.cookies[key]['domain'] = domain if secure: self.cookies[key]['secure'] = True if httponly: self.cookies[key]['httponly'] = True if samesite: if samesite.lower() not in ('lax', 'strict'): raise ValueError('samesite must be "lax" or "strict".') self.cookies[key]['samesite'] = samesite def set_signed_cookie(self, key, value, salt='', **kwargs): value = signing.get_cookie_signer(salt=key + salt).sign(value) return self.set_cookie(key, value, **kwargs) def delete_cookie(self, key, path='/', domain=None): # Most browsers ignore the Set-Cookie header if the cookie name starts # with __Host- or __Secure- and the cookie doesn't use the secure flag. secure = key.startswith(('__Secure-', '__Host-')) self.set_cookie( key, max_age=0, path=path, domain=domain, secure=secure, expires='Thu, 01 Jan 1970 00:00:00 GMT', ) # ...從上面的代碼可以看到,最核心的方法是 set_cookie(),而刪除 cookie 和 設(shè)置加鹽的 cookie 方法最后都是調(diào)用 set_cookie() 這個(gè)方法。而這個(gè)方法也比較簡(jiǎn)單,就是將對(duì)應(yīng)的傳遞過(guò)來(lái)的參數(shù)值加到 self.cookies 這個(gè)字典中。最后我們思考下,難道就這樣就完了嗎?是不是還需要有一步是需要將 self.cookies 中的所有 key-value 值組成字符串,放到頭部中,然后才返回給前端?事實(shí)上,肯定是有這一步的,代碼如下。在用 “#” 號(hào)包圍起來(lái)的那一段代碼正是將 self.cookies 中的所有 key-value 值組成字符串形式,然后放到頭部的 “Set-Cookie” 中,正是有了這一步的動(dòng)作,我們前面設(shè)置的 self.cookie 內(nèi)部的 key-value 值才能真正生效。# 源碼位置:django/core/handlers/wsgi.pyclass WSGIHandler(base.BaseHandler): request_class = WSGIRequest def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.load_middleware() def __call__(self, environ, start_response): set_script_prefix(get_script_name(environ)) signals.request_started.send(sender=self.__class__, environ=environ) request = self.request_class(environ) response = self.get_response(request) response._handler_class = self.__class__ status = '%d %s' % (response.status_code, response.reason_phrase) ############################################################################## response_headers = [ *response.items(), *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()), ] ############################################################################# start_response(status, response_headers) if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'): response = environ['wsgi.file_wrapper'](response.file_to_stream) return response

7. ReentrantLock 與 Condition 實(shí)現(xiàn)生產(chǎn)者與消費(fèi)者

非常熟悉的場(chǎng)景設(shè)計(jì),這是我們?cè)谥v解生產(chǎn)者與消費(fèi)者模型時(shí)使用的案例設(shè)計(jì),那么此處有細(xì)微的修改如下,請(qǐng)學(xué)習(xí)者進(jìn)行比照學(xué)習(xí),印象更加深刻。場(chǎng)景修改:創(chuàng)建一個(gè)工廠類 ProductFactory,該類包含兩個(gè)方法,produce 生產(chǎn)方法和 consume 消費(fèi)方法(未改變);對(duì)于 produce 方法,當(dāng)沒(méi)有庫(kù)存或者庫(kù)存達(dá)到 10 時(shí),停止生產(chǎn)。為了更便于觀察結(jié)果,每生產(chǎn)一個(gè)產(chǎn)品,sleep 3000 毫秒(5000 變 3000,調(diào)用地址也改變了,具體看代碼);對(duì)于 consume 方法,只要有庫(kù)存就進(jìn)行消費(fèi)。為了更便于觀察結(jié)果,每消費(fèi)一個(gè)產(chǎn)品,sleep 5000 毫秒(sleep 調(diào)用地址改變了,具體看代碼);庫(kù)存使用 LinkedList 進(jìn)行實(shí)現(xiàn),此時(shí) LinkedList 即共享數(shù)據(jù)內(nèi)存(未改變);創(chuàng)建一個(gè) Producer 生產(chǎn)者類,用于調(diào)用 ProductFactory 的 produce 方法。生產(chǎn)過(guò)程中,要對(duì)每個(gè)產(chǎn)品從 0 開(kāi)始進(jìn)行編號(hào) (新增 sleep 3000ms);創(chuàng)建一個(gè) Consumer 消費(fèi)者類,用于調(diào)用 ProductFactory 的 consume 方法 (新增 sleep 5000ms);創(chuàng)建一個(gè)測(cè)試類,main 函數(shù)中創(chuàng)建 2 個(gè)生產(chǎn)者和 3 個(gè)消費(fèi)者,運(yùn)行程序進(jìn)行結(jié)果觀察(未改變)。實(shí)例:public class DemoTest { public static void main(String[] args) { ProductFactory productFactory = new ProductFactory(); new Thread(new Producer(productFactory),"1號(hào)生產(chǎn)者"). start(); new Thread(new Producer(productFactory),"2號(hào)生產(chǎn)者"). start(); new Thread(new Consumer(productFactory),"1號(hào)消費(fèi)者"). start(); new Thread(new Consumer(productFactory),"2號(hào)消費(fèi)者"). start(); new Thread(new Consumer(productFactory),"3號(hào)消費(fèi)者"). start(); }}class ProductFactory { private LinkedList<String> products; //根據(jù)需求定義庫(kù)存,用 LinkedList 實(shí)現(xiàn) private int capacity = 10; // 根據(jù)需求:定義最大庫(kù)存 10 private Lock lock = new ReentrantLock(false); private Condition p = lock.newCondition(); private Condition c = lock.newCondition(); public ProductFactory() { products = new LinkedList<String>(); } // 根據(jù)需求:produce 方法創(chuàng)建 public void produce(String product) { try { lock.lock(); while (capacity == products.size()) { //根據(jù)需求:如果達(dá)到 10 庫(kù)存,停止生產(chǎn) try { System.out.println("警告:線程("+Thread.currentThread().getName() + ")準(zhǔn)備生產(chǎn)產(chǎn)品,但產(chǎn)品池已滿"); p.await(); // 庫(kù)存達(dá)到 10 ,生產(chǎn)線程進(jìn)入 wait 狀態(tài) } catch (InterruptedException e) { e.printStackTrace(); } } products.add(product); //如果沒(méi)有到 10 庫(kù)存,進(jìn)行產(chǎn)品添加 System.out.println("線程("+Thread.currentThread().getName() + ")生產(chǎn)了一件產(chǎn)品:" + product+";當(dāng)前剩余商品"+products.size()+"個(gè)"); c.signalAll(); //生產(chǎn)了產(chǎn)品,通知消費(fèi)者線程從 wait 狀態(tài)喚醒,進(jìn)行消費(fèi) } finally { lock.unlock(); } } // 根據(jù)需求:consume 方法創(chuàng)建 public String consume() { try { lock.lock(); while (products.size()==0) { //根據(jù)需求:沒(méi)有庫(kù)存消費(fèi)者進(jìn)入wait狀態(tài) try { System.out.println("警告:線程("+Thread.currentThread().getName() + ")準(zhǔn)備消費(fèi)產(chǎn)品,但當(dāng)前沒(méi)有產(chǎn)品"); c.await(); //庫(kù)存為 0 ,無(wú)法消費(fèi),進(jìn)入 wait ,等待生產(chǎn)者線程喚醒 } catch (InterruptedException e) { e.printStackTrace(); } } String product = products.remove(0) ; //如果有庫(kù)存則消費(fèi),并移除消費(fèi)掉的產(chǎn)品 System.out.println("線程("+Thread.currentThread().getName() + ")消費(fèi)了一件產(chǎn)品:" + product+";當(dāng)前剩余商品"+products.size()+"個(gè)"); p.signalAll();// 通知生產(chǎn)者繼續(xù)生產(chǎn) return product; } finally { lock.unlock(); } }}class Producer implements Runnable { private ProductFactory productFactory; //關(guān)聯(lián)工廠類,調(diào)用 produce 方法 public Producer(ProductFactory productFactory) { this.productFactory = productFactory; } public void run() { int i = 0 ; // 根據(jù)需求,對(duì)產(chǎn)品進(jìn)行編號(hào) while (true) { productFactory.produce(String.valueOf(i)); //根據(jù)需求 ,調(diào)用 productFactory 的 produce 方法 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } i++; } }}class Consumer implements Runnable { private ProductFactory productFactory; public Consumer(ProductFactory productFactory) { this.productFactory = productFactory; } public void run() { while (true) { productFactory.consume(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }}結(jié)果驗(yàn)證:線程(1號(hào)生產(chǎn)者)生產(chǎn)了一件產(chǎn)品:0;當(dāng)前剩余商品1個(gè)線程(2號(hào)生產(chǎn)者)生產(chǎn)了一件產(chǎn)品:0;當(dāng)前剩余商品2個(gè)線程(1號(hào)消費(fèi)者)消費(fèi)了一件產(chǎn)品:0;當(dāng)前剩余商品1個(gè)線程(2號(hào)消費(fèi)者)消費(fèi)了一件產(chǎn)品:0;當(dāng)前剩余商品0個(gè)警告:線程(3號(hào)消費(fèi)者)準(zhǔn)備消費(fèi)產(chǎn)品,但當(dāng)前沒(méi)有產(chǎn)品線程(2號(hào)生產(chǎn)者)生產(chǎn)了一件產(chǎn)品:1;當(dāng)前剩余商品1個(gè)線程(1號(hào)生產(chǎn)者)生產(chǎn)了一件產(chǎn)品:1;當(dāng)前剩余商品2個(gè)線程(3號(hào)消費(fèi)者)消費(fèi)了一件產(chǎn)品:1;當(dāng)前剩余商品1個(gè)線程(2號(hào)消費(fèi)者)消費(fèi)了一件產(chǎn)品:1;當(dāng)前剩余商品0個(gè)警告:線程(1號(hào)消費(fèi)者)準(zhǔn)備消費(fèi)產(chǎn)品,但當(dāng)前沒(méi)有產(chǎn)品

1. Django ORM 中外鍵的使用

為了能演示 ORM 中外鍵的使用,我們?cè)谇懊娴臅?huì)員 Member 的基礎(chǔ)上新增一個(gè)關(guān)聯(lián)表:會(huì)員等級(jí)表(vip_level)。這個(gè)會(huì)員等級(jí)有 VIP、VVIP 以及超級(jí) VIP 的 VVVIP 三個(gè)等級(jí),我們?cè)?models.py 中添加如下模型類,并在會(huì)員表中添加對(duì)應(yīng)的外鍵字段,連接到會(huì)員等級(jí)表中:# hello_app/models.py# ...class VIPLevel(models.Model): name = models.CharField('會(huì)員等級(jí)名稱', max_length=20) price = models.IntegerField('會(huì)員價(jià)格,元/月', default=10) remark = models.TextField('說(shuō)明', default="暫無(wú)信息") def __str__(self): return "<%s>" % (self.name) class Meta: db_table = 'vip_level' class Member(models.Model): # ... # 添加外鍵字段 vip_level = models.ForeignKey('VIPLevel', on_delete=models.CASCADE, verbose_name='vip level') # ...# ...首先,我們需要把前面生成的 Member 表刪除,同時(shí)刪除遷移記錄文件,操作如下:(django-manual) [root@server first_django_app]# pwd/root/django-manual/first_django_app# 刪除遷移記錄表(django-manual) [root@server first_django_app]# rm -f hello_app/migrations/0001_initial.py 此外,還需要將數(shù)據(jù)庫(kù)中的原 member 表、django_migrations 表刪除,即還原到最初狀態(tài)。接下來(lái),我們使用數(shù)據(jù)庫(kù)遷移命令:(django-manual) [root@server first_django_app]# python manage.py makemigrationsMigrations for 'hello_app': hello_app/migrations/0001_initial.py - Create model VIPLevel - Create model Member(django-manual) [root@server first_django_app]# python manage.py migrate hello_appOperations to perform: Apply all migrations: hello_appRunning migrations: Applying hello_app.0001_initial... OK注意: 如果 migrate 后面不帶應(yīng)用會(huì)生成許多 Django 內(nèi)置應(yīng)用的表,比如權(quán)限表、用戶表、Session表等。生成的 member 表 上面我們可以看到,我們生成的會(huì)員表中相比之前對(duì)了一個(gè) vip_level_id 字段,這個(gè)字段關(guān)聯(lián)的是 vip_level 表的 id 字段?,F(xiàn)在我們首先在 vip_level 中新建三條記錄,分別表示 VIP、VVIP 以及 VVVIP:(django-manual) [root@server first_django_app]# python manage.py shellPython 3.8.1 (default, Dec 24 2019, 17:04:00) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>> from hello_app.models import VIPLevel>>> vip = VIPLevel(name='vip', remark='普通vip', price=10)>>> vip.save()>>> vvip = VIPLevel(name='vvip', remark='高級(jí)vip', price=20)>>> vvip.save()>>> vvvip = VIPLevel(name='vvvip', remark='超級(jí)vip', price=30)>>> vvvip.save()>>> VIPLevel.objects.all()<QuerySet [<VIPLevel: <vip>>, <VIPLevel: <vvip>>, <VIPLevel: <vvvip>>]>接下來(lái),我們操作 member 表,生成幾條記錄并關(guān)聯(lián)到 vip_level 表:>>> from hello_app.models import Member>>> m1 = Member(name='會(huì)員1', age=29, sex=0, occupation='python', phone_num='18054299999', city='guangzhou')>>> m1.vip_level = vip>>> m1.save()>>> m2 = Member(name='會(huì)員2', age=30, sex=1, occupation='java', phone_num='18054299991', city='shanghai')>>> m2.vip_level = vvip>>> m2.save()>>> m3 = Member(name='會(huì)員3', age=35, sex=0, occupation='c/c++', phone_num='18054299992', city='beijing')>>> m3.vip_level = vvvip>>> m3.save()查看會(huì)員表中生成的數(shù)據(jù)如下:會(huì)員表 可以看到,這里我們并沒(méi)有直接寫(xiě) vip_level_id 值,而是將 Member 的 vip_level 屬性值直接賦值,然后保存。最后 Django 的 ORM 模型在這里會(huì)自動(dòng)幫我們處理這個(gè)關(guān)聯(lián)字段的值,找到關(guān)聯(lián)記錄的 id 值,并賦值給該字段。接下來(lái),我們看下外鍵關(guān)聯(lián)的查詢操作:>>> Member.objects.get(age=29).vip_level<VIPLevel: <vip>>>>> type(Member.objects.get(age=29).vip_level)<class 'hello_app.models.VIPLevel'>>>> vip = VIPLevel.objects.get(name='vip')>>> vip.member_set.all()<QuerySet [<Member: <會(huì)員1, 18054299999>>]>>>> type(vip.member_set)<class 'django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager'>上面的操作示例中我們給出了關(guān)聯(lián)表 vip_level (往往成為主表) 和 member (往往成為子表) 之間的正向和反向查詢。在 Django 默認(rèn)每個(gè)主表都有一個(gè)外鍵屬性,這個(gè)屬性值為:從表_set,通過(guò)這個(gè)屬性值我們可以查到對(duì)應(yīng)的從表記錄,比如上面的 vip.member_set.all() 語(yǔ)句就是查詢所有 vip 會(huì)員。當(dāng)然這個(gè)外鍵屬性是可以修改的,我們需要在 member 表中的外鍵字段那里加上一個(gè)屬性值:class Member(models.Model): ... vip_level = models.ForeignKey('VIPLevel', related_name="new_name", on_delete=models.CASCADE, verbose_name='vip level') ...這樣我們想再次通過(guò)主表查詢子表時(shí),就要變成如下方式了:>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> vip = VIPLevel.objects.get(name='vip')>>> vip.member_set.all()Traceback (most recent call last): File "<console>", line 1, in <module>AttributeError: 'VIPLevel' object has no attribute 'member_set'>>> vip.new_name.all()<QuerySet [<Member: <會(huì)員1, 18054299999>>]>>>>前面在定義外鍵時(shí),我們添加了一個(gè) on_delete 屬性,這個(gè)屬性控制著在刪除子表外鍵連接的記錄時(shí),對(duì)應(yīng)字表的記錄會(huì)如何處理,它有如下屬性值:CASCADE:級(jí)聯(lián)操作。如果外鍵對(duì)應(yīng)的那條記錄被刪除了,那么子表中所有外鍵為那個(gè)記錄的數(shù)據(jù)都會(huì)被刪除。對(duì)于例中,就是如果我們將會(huì)員等級(jí) vip 的記錄刪除,那么所有 vip 會(huì)員會(huì)被一并刪除;# 前面使用的正是CASCADE>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objects.get(name='vip')<VIPLevel: <vip>>>>> VIPLevel.objects.get(name='vip').delete()(2, {'hello_app.Member': 1, 'hello_app.VIPLevel': 1})>>> Member.objects.all()<QuerySet [<Member: <會(huì)員2, 18054299991>>, <Member: <會(huì)員3, 18054299992>>]>PROTECT:受保護(hù)。即只要子表中有記錄引用了外鍵的那條記錄,那么就不能刪除外鍵的那條記錄。如果我們強(qiáng)行刪除,Django 就會(huì)報(bào) ProtectedError 異常;# 修改外鍵連接的 on_delete 屬性值為 PROTECT>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objects.get(name='vvip').delete()Traceback (most recent call last): File "<console>", line 1, in <module> File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/base.py", line 918, in delete collector.collect([self], keep_parents=keep_parents) File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/deletion.py", line 224, in collect field.remote_field.on_delete(self, field, sub_objs, self.using) File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/db/models/deletion.py", line 22, in PROTECT raise ProtectedError(django.db.models.deletion.ProtectedError: ("Cannot delete some instances of model 'VIPLevel' because they are referenced through a protected foreign key: 'Member.vip_level'", <QuerySet [<Member: <會(huì)員2, 18054299991>>]>)SET_NULL:設(shè)置為空。如果外鍵的那條數(shù)據(jù)被刪除了,那么子表中所有外鍵為該條記錄的對(duì)應(yīng)字段值會(huì)被設(shè)置為 NULL,前提是要指定這個(gè)字段可以為空,否則也會(huì)報(bào)錯(cuò);# hello_app/models.pyvip_level = models.ForeignKey('VIPLevel', related_name="new_name", on_delete=models.SET_NULL, verbose_name='vip level', null=True)>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objects.get(name='vvip').delete()>>> Member.objects.get(name='會(huì)員2').vip_level_id is NoneTrue注意:注意加上null=True是不夠的,因?yàn)閿?shù)據(jù)庫(kù)在使用遷移命令時(shí)候已經(jīng)默認(rèn)是不可為空,這里測(cè)試時(shí)還需要手動(dòng)調(diào)整下表 vip_level 字段屬性,允許為 null。允許 vip_level_id 為 nullSET_DEFAULT:設(shè)置默認(rèn)值。和上面類似,前提是字表的這個(gè)字段有默認(rèn)值;SET():如果外鍵的那條數(shù)據(jù)被刪除了。那么將會(huì)獲取SET函數(shù)中的值來(lái)作為這個(gè)外鍵的值。SET函數(shù)可以接收一個(gè)可以調(diào)用的對(duì)象(比如函數(shù)或者方法),如果是可以調(diào)用的對(duì)象,那么會(huì)將這個(gè)對(duì)象調(diào)用后的結(jié)果作為值返回回去;# hello_app/models.py# 新增一個(gè)設(shè)置默認(rèn)值函數(shù)def default_value(): # 刪除記錄時(shí)會(huì)調(diào)用,在這里可以做一些動(dòng)作 # ... # 返回臨時(shí)指向一條記錄的id,返回不存在的id時(shí)會(huì)報(bào)錯(cuò);返回?cái)?shù)字也會(huì)報(bào)錯(cuò),要注意 return '4'# ...class Member(models.Model): # ... vip_level = models.ForeignKey('VIPLevel', related_name="new_name", on_delete=models.SET(default_value), verbose_name='vip level', null=True) # ...>>> from hello_app.models import VIPLevel>>> from hello_app.models import Member>>> VIPLevel.objetcs.get(name='會(huì)員3').vip_level_id3# 新建一個(gè)臨時(shí)過(guò)渡vip記錄>>> tmp_vip=VIPLevel(name='等待升級(jí)vip', price=30, remark='臨時(shí)升級(jí)過(guò)渡')>>> tmp_vip.save()>>> tmp_vip.id4# 刪除vvvip記錄>>> VIPLevel.objects.all().get(name='vvvip').delete()(1, {'hello_app.VIPLevel': 1} # 可以看到,會(huì)員表中曾經(jīng)指向?yàn)関vvip的記錄被重新指向了臨時(shí)過(guò)渡vip>>> Member.objects.get(name='會(huì)員3').vip_level_id4DO_NOTHING:什么也不做,你刪除你的,我保留我的,一切全看數(shù)據(jù)庫(kù)級(jí)別的約束。在 MySQL 中,這種情況下無(wú)法執(zhí)行刪除動(dòng)作。

1. 需求分析與初步實(shí)現(xiàn)

今天我們的目的是使用 Scrapy 和 Selenium 結(jié)合來(lái)爬取京東商城中搜索 “網(wǎng)絡(luò)爬蟲(chóng)” 得到的所有圖書(shū)數(shù)據(jù),類似于下面這樣的數(shù)據(jù):京東商城搜索 網(wǎng)絡(luò)爬蟲(chóng)搜索出的結(jié)果有9800+條數(shù)據(jù),共計(jì)100頁(yè)。我們現(xiàn)在要抓取所有的和網(wǎng)絡(luò)爬蟲(chóng)相關(guān)的書(shū)籍?dāng)?shù)據(jù)。有一個(gè)問(wèn)題需要注意,搜索的100頁(yè)數(shù)據(jù)中必定存在重復(fù)的結(jié)果,我們可以依據(jù)圖書(shū)的詳細(xì)地址來(lái)進(jìn)行去重。此外,我們提取的圖書(shū)數(shù)據(jù)字段有:圖書(shū)名;價(jià)格;評(píng)價(jià)數(shù);店鋪名稱;圖書(shū)詳細(xì)地址;需求已經(jīng)非常明確,現(xiàn)在開(kāi)始使用 Selenium 和 Scrapy 框架結(jié)合來(lái)完成這一需求。來(lái)看看如果我們是單純使用 Selenium 工具,該如何完成數(shù)據(jù)爬取呢?這里會(huì)有一個(gè)問(wèn)題需要注意:按下搜索按鈕后,顯示的數(shù)據(jù)只有30條,只有使用鼠標(biāo)向下滾動(dòng)后,才會(huì)加載更多數(shù)據(jù),最終顯示60條結(jié)果,然后才會(huì)到達(dá)翻頁(yè)的地方。在 selenium 中我們可以使用如下兩行代碼實(shí)現(xiàn)滾動(dòng)條滑到最底端:height = driver.execute_script("return document.body.scrollHeight;")driver.execute_script(f"window.scrollBy(0, {height})")time.sleep(2)可以看到,上面兩行代碼主要是執(zhí)行 js 語(yǔ)句。第一行代碼是得到頁(yè)面的底部位置,第二行代碼是使用 scrollBy() 方法控制頁(yè)面滾動(dòng)條移動(dòng)到底部。接下來(lái),我們來(lái)看看頁(yè)面數(shù)據(jù)的提取,直接右鍵 F12,可以通過(guò) xpath 表達(dá)式得到所有需要抓取的數(shù)據(jù)。為此,我編寫(xiě)了一個(gè)根據(jù)頁(yè)面代碼提取圖書(shū)數(shù)據(jù)的方法,具體如下:def parse_book_data(html): etree_html = etree.HTML(html) # 獲取列表 gl_items = etree_html.xpath('//div[@id="J_goodsList"]/ul/li') print('總共獲取數(shù)據(jù):{}'.format(len(gl_items))) res = [] for item in gl_items: book_name_em = item.xpath('.//div[@class="p-name"]/a/em/text()')[0] book_name_font = item.xpath('.//div[@class="p-name"]/a/em/font/text()') book_name_font = "".join(book_name_font) if book_name_font else "" # 獲取圖書(shū)名 book_name = f"{book_name_em}{book_name_font}" # 獲取圖書(shū)的詳細(xì)介紹地址 book_detail_url = item.xpath('.//div[@class="p-name"]/a/@href')[0] # 獲取圖書(shū)價(jià)格 price = item.xpath('.//div[@class="p-price"]/strong/i/text()')[0] # 獲取評(píng)論數(shù) comments = item.xpath('.//div[@class="p-commit"]/strong/a/text()')[0] # 獲取店鋪名稱 shop_name = item.xpath('.//div[@class="p-shopnum"]/a/text()') shop_name = shop_name[0] if shop_name else "" data = {} data['book_name'] = book_name data['book_detail_url'] = book_detail_url data['price'] = price data['comments'] = comments data['shop_name'] = shop_name res.append(data) # 返回頁(yè)面解析的結(jié)果 print('本頁(yè)獲取的結(jié)果:{}'.format(res)) return res現(xiàn)在來(lái)思考下如何能使用 selenium 一頁(yè)一頁(yè)訪問(wèn)?我給出了如下代碼:def get_page_data(driver, page): """ :driver 驅(qū)動(dòng) :page 第幾頁(yè) """ # 請(qǐng)求當(dāng)前頁(yè) if page > 1: WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, 'J_bottomPage')) ) driver.find_element_by_xpath(f'//div[@id="J_bottomPage"]/span/a[text()="{page}"]').click() time.sleep(2) # 滾動(dòng)到最下面,出現(xiàn)京東圖書(shū)剩余書(shū)籍?dāng)?shù)據(jù) height = driver.execute_script("return document.body.scrollHeight;") driver.execute_script(f"window.scrollBy(0, {height})") time.sleep(2) return parse_book_data(driver.page_source)對(duì)于第一頁(yè)的訪問(wèn)是在輸入關(guān)鍵字<網(wǎng)絡(luò)爬蟲(chóng)>后點(diǎn)擊按鈕得到的,我們不需要放到這個(gè)函數(shù)來(lái)得到,只需要滾動(dòng)到底部得到所有的圖書(shū)數(shù)據(jù)即可;而對(duì)于第2頁(yè)之后的頁(yè)面,我們需要使用 selenium 的模擬鼠標(biāo)點(diǎn)擊功能,點(diǎn)擊下對(duì)應(yīng)頁(yè)后便能跳轉(zhuǎn)得到該頁(yè),然后再滾動(dòng)到底部,就可以得到整頁(yè)的搜索結(jié)果。我們來(lái)看看完整的實(shí)現(xiàn):import timeimport randomimport refrom selenium import webdriverfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.common.by import Byfrom selenium.webdriver import ActionChainsfrom lxml import etreedef get_page_data(driver, page): """ :driver 驅(qū)動(dòng) :page 第幾頁(yè) """ # 具體代碼參考上面 # ... def parse_book_data(html): """ 解析頁(yè)面圖書(shū)數(shù)據(jù) """ # 具體代碼參考上面 # ... options = webdriver.ChromeOptions()options.add_experimental_option("excludeSwitches", ['enable-automation'])driver = webdriver.Chrome(options=options, executable_path="C:/Users/Administrator/AppData/Local/Google/Chrome/Application/chromedriver.exe")driver.maximize_window()driver.get("https://www.jd.com/")# 輸入網(wǎng)絡(luò)爬蟲(chóng),然后點(diǎn)擊搜索driver.find_element_by_id('key').send_keys('網(wǎng)絡(luò)爬蟲(chóng)')driver.find_elements_by_xpath('//div[@role="serachbox"]/button')[0].click()time.sleep(2)max_page = 100for i in range(1, max_page + 1): get_page_data(driver, i)下面來(lái)看看代碼執(zhí)行的效果,這里為了能盡快執(zhí)行完,我將 max_page 參數(shù)調(diào)整為10,只獲取10頁(yè)搜索結(jié)果,一共是600條數(shù)據(jù):106從上面的演示中,可以看到最后每頁(yè)抓取的數(shù)據(jù)都是60條。

4.4 響應(yīng)前端的請(qǐng)求

有求也要有應(yīng)。服務(wù)端也需要在前端發(fā)出請(qǐng)求的時(shí)候做出相應(yīng)的響應(yīng)。4.4.1 使用 nodeconst express = require("express");const mysql = require('mysql');const bodyParser = require("body-parser");const router = express.Router(); // express 路由const app = express();// 使用 bodyParser 中間件app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: true }));registerRouter(); // 路由注冊(cè)執(zhí)行app.use(router); // 使用 router/** * 構(gòu)建返回結(jié)果 * @param {*} code * @param {*} data * @param {*} error */const buildResponse = (code, data = {}, error = null) => { return { code, data, error }}// 創(chuàng)建 mysql 鏈接, 本地?cái)?shù)據(jù)庫(kù)為 mkconst connection = mysql.createConnection({ host : 'localhost', user : 'root', password : 'ok36369ok', database : 'mk'});connection.connect();// 端口號(hào)const port = process.env.PORT || 8080;// 監(jiān)聽(tīng)module.exports = app.listen(port, () => { console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`);});/***********路由注冊(cè)模塊*************//***路由注冊(cè)函數(shù)*/function registerRouter() { // 查詢課程 router.get("/course/get", function(req, res) { connection.query('SELECT id, name, teacher, start_time as startTime, end_time as endTime from course', function (error, results, fields) { if (error) throw error; const responseData = buildResponse(0, {items: results}) // mysql 查詢結(jié)果包裝后進(jìn)行返回 res.send(responseData); // send 結(jié)果 }); }); // other router ..}如上所示,我們引入了 Express 框架、bodyParser 以及 mysql 相關(guān)的庫(kù)。其中, bodyParser 可以解析請(qǐng)求中 body 的內(nèi)容。 不過(guò)我們的重點(diǎn)應(yīng)該是最下面的函數(shù) registerRouter,這個(gè)函數(shù)是用來(lái)注冊(cè)我們所有的路由的。我們之后的路由也都會(huì)寫(xiě)在這個(gè)函數(shù)里面。好了,回歸正題。為了使服務(wù)端響應(yīng)前端的請(qǐng)求,我們?cè)谏厦娴拇a中注冊(cè)了一個(gè)路由: router.get("/course/get", callback)如果前端發(fā)送請(qǐng)求到 “/course/get” ,那服務(wù)端會(huì)觸發(fā)回調(diào)函數(shù) callback,對(duì)應(yīng)到上面代碼中,我們可以看到:內(nèi)部會(huì)執(zhí)行一個(gè)查詢所有課程的 sql 語(yǔ)句;將查詢后的數(shù)據(jù)進(jìn)行包裝,變?yōu)?{code: 0, data: xxx, error: xxxx} 這樣的格式;返回?cái)?shù)據(jù)。相同的,我們也可以使用 java 來(lái)實(shí)現(xiàn)后端的邏輯。4.4.2 使用 Javapackage com.demo;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;import java.sql.*;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;@WebServlet("/course/get")public class HelloWorld extends HttpServlet { // JDBC 驅(qū)動(dòng)名 static final String JDBC_DRIVER = "com.mysql.jdbc.Driver"; // 數(shù)據(jù)庫(kù) URL static final String DB_URL = "jdbc:mysql://localhost:3306/mk?useUnicode=true&useJDBCCompliantTimezoneShift\n" + "=true&useLegacyDatetimeCode=false&serverTimezone=UTC"; // 數(shù)據(jù)庫(kù)的用戶名 static final String USER = "root"; // 數(shù)據(jù)庫(kù)的密碼 static final String PW = "ok36369ok"; /** * 包裝返回結(jié)果 */ private Map buildResponse(int code, Object data, String error) { Map<String, Object> res = new HashMap<String, Object>(); res.put("code", code); res.put("data", data); res.put("error", error); return res; } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } // 獲取課程使用 GET, 會(huì)進(jìn)入 doGet protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Connection conn = null; Statement stmt = null; // 設(shè)置編碼格式 request.setCharacterEncoding("utf-8"); response.setContentType("text/json; charset=utf-8"); PrintWriter out = response.getWriter(); Map<String,Object> resMap = new HashMap<String,Object>(); // 返回結(jié)果, Map 類型 try{ // 注冊(cè) JDBC 驅(qū)動(dòng) Class.forName(JDBC_DRIVER); // 打開(kāi)鏈接 System.out.println("連接數(shù)據(jù)庫(kù)..."); conn = DriverManager.getConnection(DB_URL,USER,PW); // 執(zhí)行查詢 System.out.println(" 實(shí)例化Statement對(duì)象..."); stmt = conn.createStatement(); String sql; sql = "SELECT * FROM course"; ResultSet rs = stmt.executeQuery(sql); List<Map> Courses = new ArrayList<Map>(); // 展開(kāi)結(jié)果集數(shù)據(jù)庫(kù) while(rs.next()){ // 通過(guò)字段檢索 Map<String,Object> map = new HashMap<String, Object>(); int id = rs.getInt("id"); String name = rs.getString("name"); String teacher = rs.getString("teacher"); Date startTime = rs.getDate("start_time"); Date endTime = rs.getDate("end_time"); // 分別將 mysql 查詢結(jié)果 put 到 map中 map.put("id", id); map.put("name", name); map.put("teacher", teacher); map.put("startTime", startTime); map.put("endTime", endTime); Courses.add(map); } Map<String, List> data = new HashMap<String, List>(); // 定義返回?cái)?shù)據(jù)的 data部分, Map 類型 data.put("items", Courses); // data 添加 items, items 就是我們要的課程列表數(shù)據(jù) // 構(gòu)建輸出數(shù)據(jù) resMap = buildResponse(0, data, null); // 完成后關(guān)閉 rs.close(); stmt.close(); conn.close(); }catch(SQLException se){ // 處理 JDBC 錯(cuò)誤 se.printStackTrace(); }catch(Exception e){ // 處理 Class.forName 錯(cuò)誤 e.printStackTrace(); }finally{ // 關(guān)閉資源 try{ if(stmt!=null) stmt.close(); }catch(SQLException se2){ } try{ if(conn!=null) conn.close(); }catch(SQLException se){ se.printStackTrace(); } } String responseData = JSON.toJSONString(resMap);// 將 Map 類型的結(jié)果序列化為 String out.println(responseData); // 返回結(jié)果 }}這里主要使用的是 servlet 的方式來(lái)為前端提供服務(wù),對(duì)于請(qǐng)求課程列表來(lái)說(shuō),使用到 GET 方法,因此本例子中會(huì)進(jìn)入到 doGet 方法中。另外,所用到的技術(shù)在章節(jié)須知中也有講到,這里就不再累贅。實(shí)現(xiàn)的代碼雖然和 node 端有所差別,但是思想都是一樣的。無(wú)非也是使用 MySQL 的查詢結(jié)果, 拼裝成前端所需要的數(shù)據(jù)。并進(jìn)行返回。

4.3 awk命令詳解

4.3.1 awk 輸出awk print輸出,例如:print item1,item2...1.各字段之間逗號(hào)隔開(kāi),輸出時(shí)以空白字符分隔;2.輸出的字段可以為字符串或數(shù)值,當(dāng)前記錄的字段(如$1)、變量或 awk 的表達(dá)式;數(shù)值先會(huì)轉(zhuǎn)換成字符串然后輸出;3.print 命令后面的 item 可以省略,此時(shí)其功能相當(dāng)于print $0,如果想輸出空白,可以使用print "";例如:[root@master ~]# awk -F: '{print $1,$NF}' /etc/passwd|column -troot /bin/bashbin /sbin/nologindaemon /sbin/nologinadm /sbin/nologinlp /sbin/nologinsync /bin/syncawk printf 輸出printf 命令的使用格式:printf <format> item1,item2...要點(diǎn):1.其與 print 命令最大區(qū)別,printf 需要指定 format,format 必須給出;2.format 用于指定后面的每個(gè) item 輸出格式;3.printf 語(yǔ)句不會(huì)自動(dòng)打印換行字符\n。format 格式的指示符都以 % 開(kāi)頭,后跟一個(gè)字符:%c:顯示ascall碼%d:%i:十進(jìn)制整數(shù)%e,%E:科學(xué)計(jì)數(shù)法%f:浮點(diǎn)數(shù)%s:字符串%u:無(wú)符號(hào)整數(shù)%%:顯示%自身修飾符:#[.#]:第一個(gè)#控制顯示的寬度:第二個(gè)#表示小數(shù)點(diǎn)后的精度:%3.1f-:左對(duì)齊+:顯示數(shù)組符號(hào)例如:[root@master ~]# awk -F: '{printf "Username:%-15s ,Uid:%d\n",$1,$3}' /etc/passwdUsername:root ,Uid:0Username:bin ,Uid:1Username:daemon ,Uid:2Username:adm ,Uid:3Username:lp ,Uid:4Username:sync ,Uid:5Username:shutdown ,Uid:64.3.2 awk變量記錄變量:IFS(input field separator),輸入字段分隔符(默認(rèn)空白)OFS(output field separator),輸出字段分隔符RS(Record separator):輸入文本換行符(默認(rèn)回車(chē))ORS:輸出文本換行符數(shù)據(jù)變量NR:the number of input records,awk 命令所處理的文件的行數(shù),如果有多個(gè)文件,這個(gè)數(shù)目會(huì)將處理的多個(gè)文件計(jì)數(shù)NF:number of field,當(dāng)前記錄的 field 個(gè)數(shù){print NF},{print $NF}ARGV:數(shù)組,保存命令行本身這個(gè)字符串ARGC:awk 命令的參數(shù)個(gè)數(shù)FILENAME:awk 命令處理的文件名稱ENVIRON:當(dāng)前 shell 環(huán)境變量及其值的關(guān)聯(lián)數(shù)組awk 'BEGIN{print ENVIRON["PATH"]}'自定義變量-v var=value變量名區(qū)分大小寫(xiě),例如:[root@master ~]# awk -v test="abc" 'BEGIN{print test}'abc[root@master ~]# awk 'BEGIN{var="name";print var}'name4.3.3 操作符算術(shù)運(yùn)算+,-,*,/,^,%。例如:[root@master ~]# awk 'BEGIN{a=5;b=3;print "a + b =",a+b}'a + b = 8字符串操作無(wú)符號(hào)操作符,表示字符串連接,例如:[root@master ~]# awk 'BEGIN { str1="Hello,"; str2="World"; str3 = str1 str2; print str3 }'Hello,World賦值操作符:=,+=,-=,*=,/=,%=,^=,例如:[root@master ~]# awk 'BEGIN{a=5;b=6;if(a == b) print "a == b";else print "a!=b"}' a!=b[root@master ~]# awk -F: '{sum+=$3}END{print sum}' /etc/passwd72349比較操作符:>,>=,<,<=,!=,==模式匹配符:~:是否匹配!~:是否不匹配例如:[root@master ~]# awk -F: '$1~"root"{print $0}' /etc/passwdroot:x:0:0:root:/root:/bin/bash邏輯操作符:&& 、 || 、 !,例如:[root@master ~]# awk 'BEGIN{a=6;if(a > 0 && a <= 6) print "true";else print "false"}'true函數(shù)調(diào)用:function_name(argu1,augu2)條件表達(dá)式(三元運(yùn)算):selection?if-true-expresssion:if-false-expression[root@master ~]# awk -F: '{$3>=100?usertype="common user":usertype="sysadmin";printf "%15s:%s\n",$1,usertype}' /etc/passwd root:sysadmin bin:sysadmin daemon:sysadmin adm:sysadmin lp:sysadmin sync:sysadmin shutdown:sysadmin halt:sysadmin4.3.4 Patternempty:空模式,匹配每一行/regular expression/:僅處理能被此處模式匹配到的行,例如;[root@master ~]# awk -F: '$NF=="/bin/bash"{printf "%15s,%s\n",$NF,$1}' /etc/passwd /bin/bash,rootrelational expression:關(guān)系表達(dá)式,結(jié)果為“真”有“假”,結(jié)果為“真”才會(huì)被處理。Tips:使用模式需要使用雙斜線括起來(lái),真:結(jié)果為非0值,非空字符串。[root@master ~]# awk -F: '$3>100{print $1,$3}' /etc/passwdsystemd-network 192polkitd 999ceph 167kube 998etcd 997gluster 996nfsnobody 65534chrony 995redis 994awk -F: '$NF=="/bin/bash"{printf "%15s,%s\n",$NF,$1}' /etc/passwdawk -F: '$NF~/bash$/{printf "%15s,%s\n",$NF,$1}' /etc/passwddf -Th|awk '/^\/dev/{print}'line ranges:行范圍,制定startline,endline。[root@master ~]# awk -F: '/10/,/20/{print $1}' /etc/passwdgamesftpnobodysystemd-networkdbuspolkitdpostfixsshdcephkubeetcdglusterrpcBEGIN/END模式BEGIN{}:僅在開(kāi)始處理文本之前執(zhí)行一次END{}:僅在文本處理完成之后執(zhí)行一次 [root@master ~]# awk -F: 'BEGIN{print "username uid\n--------------------"}{printf "%-15s:%d\n",$1,$3}END{print "-----------------\nend"}' /etc/passwdusername uid -------------------- root :0 bin :1 daemon :2 adm :3 lp :4 rpc :32 rpcuser :29 nfsnobody :65534 chrony :995 redis :994 ----------------- end4.3.5 控制語(yǔ)句if(condition) {statements},例如:[root@master ~]# awk -F: '{if($3>100) print $1,$3}' /etc/passwdsystemd-network 192polkitd 999ceph 167kube 998etcd 997gluster 996nfsnobody 65534chrony 995redis 994if(condition) {statments} [else {statments}],例如:[root@master ~]# awk -F: '{if($3>100) {printf "Common user:%-15s\n",$1} else {printf "sysadmin user:%-15s\n",$1}}' /etc/passwdsysadmin user:root sysadmin user:bin sysadmin user:daemon sysadmin user:adm sysadmin user:lp sysadmin user:sync sysadmin user:shutdown sysadmin user:halt sysadmin user:mail sysadmin user:operator sysadmin user:games

2. 數(shù)據(jù)重塑

Pandas 對(duì)應(yīng)數(shù)據(jù)的重塑有三種操作方式,分別為重塑操作 stack () , unstack () 和軸向旋轉(zhuǎn)操作 pivot ():stack ():該操作是將數(shù)據(jù)的列 “旋轉(zhuǎn)” 為行;unstack ():該操作是將數(shù)據(jù)的行 “旋轉(zhuǎn)” 為列。下面我們先構(gòu)造一些模擬數(shù)據(jù),然后詳細(xì)講解這兩種數(shù)據(jù)重塑的方式:# 導(dǎo)入 Pandas 庫(kù)import pandas as pd# 構(gòu)造數(shù)據(jù)集df_data=pd.DataFrame([[96,92,83,94],[85,86,77,88],[69,90,91,82]], index=['語(yǔ)文','數(shù)學(xué)','英語(yǔ)'], columns=['月考1','月考2','月考3','月考4'])print(df_data)# --- 輸出結(jié)果 --- 月考1 月考2 月考3 月考4語(yǔ)文 96 92 83 94數(shù)學(xué) 85 86 77 88英語(yǔ) 69 90 91 82# 結(jié)果解析:這就是我們前面幾節(jié)課學(xué)習(xí)的,創(chuàng)建一個(gè) DataFrame 數(shù)據(jù)集的方式。# 接下來(lái)我們?yōu)?df_data 數(shù)據(jù)集指定行索引和列索引的索引名,分別為 “科目”,“??肌保@里要注意索引名和索引值的區(qū)別,我們上面的 index 和 columns 參數(shù)指定的是行索引和列索引值。df_data.index.name='科目'df_data.columns.name='???print(df_data)# --- 輸出結(jié)果 ---模考 月考1 月考2 月考3 月考4科目 語(yǔ)文 96 92 83 94數(shù)學(xué) 85 86 77 88英語(yǔ) 69 90 91 82# 結(jié)果解析:這里可以看到 df_data 數(shù)據(jù)集中有了行索引名“科目”和列索引名“??肌?。1. stack() 函數(shù)通過(guò)該函數(shù),我們將 df_data 數(shù)據(jù)集的數(shù)據(jù)列轉(zhuǎn)為數(shù)據(jù)行:# df_data 為上述構(gòu)建的數(shù)據(jù)集print(df_data.stack())# --- 輸出結(jié)果 ---科目 模考 語(yǔ)文 月考1 96 月考2 92 月考3 83 月考4 94數(shù)學(xué) 月考1 85 月考2 86 月考3 77 月考4 88英語(yǔ) 月考1 69 月考2 90 月考3 91 月考4 82結(jié)果解析:通過(guò)列旋轉(zhuǎn)為行的重塑方式,我們看月考列數(shù)據(jù)變?yōu)榱诵袛?shù)據(jù),得到數(shù)據(jù)集有了層次化的列索引,一維的 “科目”,二維的 “??肌保@樣的數(shù)據(jù)結(jié)構(gòu)對(duì)于我們分析每科在各次月考中成績(jī)的變化有很好的幫助。2. unstack() 函數(shù)通過(guò)該函數(shù),我們將 df_data 數(shù)據(jù)集的數(shù)據(jù)行轉(zhuǎn)為數(shù)據(jù)列:# df_data 為上述構(gòu)建的數(shù)據(jù)集print(df_data.unstack())# --- 輸出結(jié)果 ---模考 科目月考1 語(yǔ)文 96 數(shù)學(xué) 85 英語(yǔ) 69月考2 語(yǔ)文 92 數(shù)學(xué) 86 英語(yǔ) 90月考3 語(yǔ)文 83 數(shù)學(xué) 77 英語(yǔ) 91月考4 語(yǔ)文 94 數(shù)學(xué) 88 英語(yǔ) 82結(jié)果解析:這里可以看到,原數(shù)據(jù)中的科目行數(shù)據(jù),變?yōu)榱肆袛?shù)據(jù),索引也是層次化的索引,對(duì)于我們分析每次月考各科目成績(jī)的波動(dòng)有很好的幫助。如果我們對(duì) stack () 操作后返回的數(shù)據(jù)集,再進(jìn)行 unstack () 操作,會(huì)發(fā)現(xiàn)回到了原數(shù)據(jù)結(jié)構(gòu),說(shuō)明 unstack () 是 stack () 的逆操作:print(df_data.stack().unstack())# --- 輸出結(jié)果 ---??? 月考1 月考2 月考3 月考4科目 語(yǔ)文 96 92 83 94數(shù)學(xué) 85 86 77 88英語(yǔ) 69 90 91 823. pivot() 函數(shù)該函數(shù)用于指定行索引,列索引,以及數(shù)據(jù)值,生成一個(gè) “pivot” 數(shù)據(jù)表格。該函數(shù)有三個(gè)參數(shù):參數(shù)名說(shuō)明 index 新數(shù)據(jù)集的列索引 columns 新數(shù)據(jù)集的行索引 values 對(duì)應(yīng)行和列所要填充的數(shù)據(jù)值,如果沒(méi)有,填充 NaN下面我們先模擬一個(gè) DataFrame 數(shù)據(jù)集,便于該函數(shù)的操作效果展示:# 這里模擬了3年某位學(xué)生各學(xué)期語(yǔ)文、數(shù)學(xué)得分的數(shù)據(jù)表。df_data_pivot=pd.DataFrame([["2018","上學(xué)期",83,94],["2018","下學(xué)期",77,88], ["2019","上學(xué)期",83,94],["2019","下學(xué)期",83,94], ["2020","上學(xué)期",83,94],["2020","下學(xué)期",91,82]], index=['a','b','c','d','e','f'], columns=['年度','學(xué)期','語(yǔ)文','數(shù)學(xué)'])print(df_data_pivot)# --- 輸出結(jié)果 --- 年度 學(xué)期 語(yǔ)文 數(shù)學(xué)a 2018 上學(xué)期 83 94b 2018 下學(xué)期 77 88c 2019 上學(xué)期 83 94d 2019 下學(xué)期 83 94e 2020 上學(xué)期 83 94f 2020 下學(xué)期 91 82接下來(lái)我們將通過(guò)函數(shù) pivot () 指定行索引、列索引以及數(shù)據(jù)值,生成新的數(shù)據(jù)集:# pivot(index="年度", columns="學(xué)期", values="語(yǔ)文")new_df=df_data_pivot.pivot(index="年度", columns="學(xué)期", values="語(yǔ)文")print(new_df)# --- 輸出結(jié)果 ---學(xué)期 上學(xué)期 下學(xué)期年度 2018 83 772019 83 832020 83 91輸出解析:我們這里指定行索引為年度,列索引為學(xué)期,填充對(duì)應(yīng)行和列索引的語(yǔ)文數(shù)據(jù)值,通過(guò)輸出結(jié)果可以看到新的數(shù)據(jù)集,這樣處理之后的數(shù)據(jù)結(jié)構(gòu)是很利于我們分析各年度各學(xué)期語(yǔ)文成績(jī)的一個(gè)變化情況。

1.2 Field 核心屬性

前面的實(shí)驗(yàn)中我們用到的 django 的中的 CharField,并在初始化該 Field 示例時(shí)傳遞了一些參數(shù),如 label、min_length 等。接下來(lái),我們首先看看 Field 對(duì)象的一些核心屬性:Field.required:默認(rèn)情況下,每個(gè) Field 類會(huì)假定該 Field 的值時(shí)必須提供的,如果我們傳遞的時(shí)空值,無(wú)論是 None 還是空字符串(""),在調(diào)用 Field 的 clean() 方法時(shí)就會(huì)拋出異常ValidationError ;>>> from django import forms>>> f = forms.CharField()>>> f.clean('foo')'foo'>>> f.clean('')Traceback (most recent call last): File "<console>", line 1, in <module> File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/fields.py", line 149, in clean self.validate(value) File "/root/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/forms/fields.py", line 127, in validate raise ValidationError(self.error_messages['required'], code='required')django.core.exceptions.ValidationError: ['This field is required.']>>> f = forms.CharField(required=False)>>> f.clean('')''Field.label:是給這個(gè) field 一個(gè)標(biāo)簽名;>>> from django import forms>>> class CommentForm(forms.Form):... name = forms.CharField(label='名稱')... url = forms.URLField(label='網(wǎng)站地址', required=False)... comment = forms.CharField()... >>> f = CommentForm()>>> print(f)<tr><th><label for="id_name">名稱:</label></th><td><input type="text" name="name" required id="id_name"></td></tr><tr><th><label for="id_url">網(wǎng)站地址:</label></th><td><input type="url" name="url" id="id_url"></td></tr><tr><th><label for="id_comment">Comment:</label></th><td><input type="text" name="comment" required id="id_comment"></td></tr>可以看到,這個(gè) label 參數(shù)最后在會(huì)變成 HTML 中的 <label> 元素。Field.label_suffix:這個(gè)屬性值是在 label 屬性值后面統(tǒng)一加一個(gè)后綴。(django-manual) [root@server first_django_app]# python manage.py shellPython 3.8.1 (default, Dec 24 2019, 17:04:00) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>> from django import forms>>> class CommentForm(forms.Form):... name = forms.CharField(label='Your name')... url = forms.URLField(label='網(wǎng)站地址', label_suffix='?', required=False)... comment = forms.CharField()... >>> c>>> print(f.as_p())<p><label for="id_name">Your name#</label> <input type="text" name="name" required id="id_name"></p><p><label for="id_url">網(wǎng)站地址?</label> <input type="url" name="url" id="id_url"></p><p><label for="id_comment">Comment#</label> <input type="text" name="comment" required id="id_comment"></p>>>>注意:Form 也有 label_suffix 屬性,會(huì)讓所有字段都加上這個(gè)屬性值。但是如果字段自身定義了這個(gè)屬性值,則會(huì)覆蓋全局的 label_suffix,正如上述測(cè)試的結(jié)果。Field.initial:指定字段的初始值;Field.widget:這個(gè)就是指定該 Field 轉(zhuǎn)成 HTML 的標(biāo)簽,我們class LoginForm(forms.Form): name = forms.CharField( label="賬號(hào)", min_length=4, required=True, error_messages={'required': '賬號(hào)不能為空', "min_length": "賬號(hào)名最短4位"}, widget=forms.TextInput(attrs={'class': "input-text", 'placeholder': '請(qǐng)輸入登錄賬號(hào)'}) ) # ...Field.help_text:給 Field 添加一個(gè)描述;Field.error_messages:該 error_messages 參數(shù)可以覆蓋由 Form 中對(duì)應(yīng)字段引發(fā)錯(cuò)誤的默認(rèn)提示;Field.validators:可以通過(guò)該參數(shù)自定義字段數(shù)據(jù)校驗(yàn);下面看我們上一講的實(shí)驗(yàn)2中自定義了一個(gè)簡(jiǎn)單的密碼校驗(yàn),如下:def password_validate(value): """ 密碼校驗(yàn)器 """ pattern = re.compile(r'^(?=.*[0-9].*)(?=.*[A-Z].*)(?=.*[a-z].*).{6,20}$') if not pattern.match(value): raise ValidationError('密碼需要包含大寫(xiě)、小寫(xiě)和數(shù)字') class LoginForm(forms.Form): # ... password = forms.CharField( label="密碼", validators=[password_validate, ], min_length=6, max_length=20, required=True, error_messages={'required': '密碼不能為空', "invalid": "密碼需要包含大寫(xiě)、小寫(xiě)和數(shù)字", "min_length": "密碼最短8位", "max_length": "密碼最長(zhǎng)20位"}, widget=forms.TextInput(attrs={'class': "input-text",'placeholder': '請(qǐng)輸入密碼', 'type': 'password'}), help_text='密碼必須包含大寫(xiě)、小寫(xiě)以及數(shù)字', ) # ...Field.disabled:如果為 True,那么該字段將禁止輸入,會(huì)在對(duì)應(yīng)生成的 input 標(biāo)簽中加上 disabled 屬性(django-manual) [root@server first_django_app]# python manage.py shellPython 3.8.1 (default, Dec 24 2019, 17:04:00) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>> from django import forms>>> class CommentForm(forms.Form):... name = forms.CharField(label='Your name', disabled=True)... >>> f = CommentForm()>>> print(f)<tr><th><label for="id_name">Your name:</label></th><td><input type="text" name="name" required disabled id="id_name"></td></tr>Field.widget:這個(gè) widget 的中文翻譯是 “小器物,小裝置”,每種 Field 都有一個(gè)默認(rèn)的 widget 屬性值,Django 會(huì)根據(jù)它來(lái)將 Field 渲染成對(duì)應(yīng)的 HTML 代碼。(django-manual) [root@server first_django_app]# python manage.py shellPython 3.8.1 (default, Dec 24 2019, 17:04:00) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>> from django.forms.fields import BooleanField,CharField,ChoiceField>>> BooleanField.widget<class 'django.forms.widgets.CheckboxInput'>>>> CharField.widget<class 'django.forms.widgets.TextInput'>>>> ChoiceField.widget<class 'django.forms.widgets.Select'>

2. Scrapy 與 Selenium 結(jié)合爬取京東圖書(shū)數(shù)據(jù)

接下來(lái)我們對(duì)上面的代碼進(jìn)行調(diào)整和 Scrapy 框架結(jié)合,而第一步需要做的就是建立好相應(yīng)的工程:# 創(chuàng)建爬蟲(chóng)項(xiàng)目PS D:\shencong\scrapy-lessons\code\chap17> scrapy startproject jdbooks# ...# 進(jìn)入到spider目錄,使用genspider命令創(chuàng)建爬蟲(chóng)文件PS D:\shencong\scrapy-lessons\code\chap17\jd_books\jd_books\spiders> scrapy genspider jd www.jd.com創(chuàng)建好工程后就是編寫(xiě) items.py 中的 JdBooksItem 類,這非常簡(jiǎn)單,直接根據(jù)我們前面定義好的字段編寫(xiě)相應(yīng)的代碼即可:class JdBooksItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() book_name = scrapy.Field() price = scrapy.Field() comments = scrapy.Field() shop_name = scrapy.Field() book_detail_url = scrapy.Field()整個(gè)項(xiàng)目的難點(diǎn)是如何實(shí)現(xiàn)下一頁(yè)數(shù)據(jù)的爬???前面可以使用 selenium 去自動(dòng)點(diǎn)擊頁(yè)號(hào)而進(jìn)入下一個(gè),然而在 Scrapy 中卻不太好這樣處理。我們通過(guò)分析京東搜索的 URL 后發(fā)現(xiàn),其搜索的 URL 可以簡(jiǎn)化為如下形式:https://search.jd.com/Search?keyword=搜索關(guān)鍵字&page=(頁(yè)號(hào)* 2 - 1),我們只需要提供搜索的關(guān)鍵字以及相應(yīng)的請(qǐng)求頁(yè)號(hào)即可。例如下圖所示:京東搜索 URL 參數(shù)因此我們?cè)?settings.py 中準(zhǔn)備兩個(gè)參數(shù):一個(gè)是搜索的關(guān)鍵字,另一個(gè)是爬取的最大頁(yè)數(shù)。具體的形式如下:# settings.py# ...KEYWORD = "網(wǎng)絡(luò)爬蟲(chóng)"MAX_PAGE = 10緊接著我們可以構(gòu)造出請(qǐng)求不同頁(yè)的 URL 并交給 Scrapy 的引擎和調(diào)度器去處理,對(duì)應(yīng)的 Spider 代碼如下:# 代碼位置:jd_books/jd_books/spiders/jd.pyfrom urllib.parse import quotefrom scrapy import Spider, Requestfrom jd_books.items import JdBooksItemclass JdSpider(Spider): name = 'jd' allowed_domains = ['www.jd.com'] start_urls = ['http://www.jd.com/'] base_url = "https://search.jd.com/Search?keyword={}&page={}" def start_requests(self): keyword = self.settings.get('KEYWORD', "Python") for page in range(1, self.settings.get('MAX_PAGE') + 1): url = self.base_url.format(quote(keyword), page * 2 - 1) yield Request(url=url, callback=self.parse_books, dont_filter=True) def parse_books(self, response): goods_list = response.xpath('//div[@id="J_goodsList"]/ul/li') print('本頁(yè)獲取圖書(shū)數(shù)目:{}'.format(len(goods_list))) for good in goods_list: book_name_em = good.xpath('.//div[@class="p-name"]/a/em/text()').extract()[0] book_name_font = good.xpath('.//div[@class="p-name"]/a/em/font/text()').extract() book_name_font = "".join(book_name_font) if book_name_font else "" book_name = f"{book_name_em}{book_name_font}" book_detail_url = good.xpath('.//div[@class="p-name"]/a/@href').extract()[0] price = good.xpath('.//div[@class="p-price"]/strong/i/text()').extract()[0] comments = good.xpath('.//div[@class="p-commit"]/strong/a/text()').extract()[0] shop_name = good.xpath('.//div[@class="p-shopnum"]/a/text()').extract()[0] item = JdBooksItem() item['book_name'] = book_name item['book_detail_url'] = book_detail_url item['price'] = price item['comments'] = comments item['shop_name'] = shop_name yield item上面的代碼就是單純的生成多頁(yè)的 Request 請(qǐng)求 (start_requests() 方法) 和解析網(wǎng)頁(yè)數(shù)據(jù) (parse_books() 方法)。這個(gè)解析數(shù)據(jù)完全依賴于我們獲取完整的頁(yè)面源碼,那么如何在 Scrapy 中使用 selenium 去請(qǐng)求 URL 然后獲取頁(yè)面源碼呢?答案就是下載中間件。我們?cè)诰帉?xiě)一個(gè)下載中間件,攔截發(fā)送的 request 請(qǐng)求,對(duì)于請(qǐng)求京東圖書(shū)數(shù)據(jù)的請(qǐng)求我們會(huì)切換成 selenium 的方式去獲取網(wǎng)頁(yè)源碼,然后將得到的頁(yè)面源碼封裝成 Response 響應(yīng)并返回。在生成 Scrapy 項(xiàng)目中已經(jīng)為我們準(zhǔn)備好了一個(gè) middleware.py 文件,我們按照上面的思路來(lái)完成相應(yīng)代碼,具體內(nèi)容如下:import timefrom scrapy import signalsfrom scrapy.http.response.html import HtmlResponsefrom selenium import webdriverfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.common.by import By# useful for handling different item types with a single interfacefrom itemadapter import is_item, ItemAdapteroptions = webdriver.ChromeOptions()# 注意,使用這個(gè)參數(shù)我們就不會(huì)看到啟動(dòng)的google瀏覽器,無(wú)界面運(yùn)行options.add_argument('-headless')options.add_experimental_option("excludeSwitches", ['enable-automation'])class JdBooksSpiderMiddleware: # 保持不變 # ... class JdBooksDownloaderMiddleware: # Not all methods need to be defined. If a method is not defined, # scrapy acts as if the downloader middleware does not modify the # passed objects. def __init__(self): self.driver = webdriver.Chrome(options=options, executable_path="C:/Users/Administrator/AppData/Local/Google/Chrome/Application/chromedriver.exe") # ... def process_request(self, request, spider): # Called for each request that goes through the downloader # middleware. # Must either: # - return None: continue processing this request # - or return a Response object # - or return a Request object # - or raise IgnoreRequest: process_exception() methods of # installed downloader middleware will be called print('使用 selenium 請(qǐng)求頁(yè)面:{}'.format(request.url)) if request.url.startswith("https://search.jd.com/Search"): # 如果是獲取京東圖書(shū)數(shù)據(jù)的請(qǐng)求,使用selenium方式獲取頁(yè)面 self.driver.get(request.url) time.sleep(2) # 將滾動(dòng)條拖到最底端,獲取一頁(yè)完整的60條數(shù)據(jù) height = self.driver.execute_script("return document.body.scrollHeight;") self.driver.execute_script(f"window.scrollBy(0, {height})") time.sleep(2) # 將最后渲染得到的頁(yè)面源碼作為響應(yīng)返回 return HtmlResponse(url=request.url, body=self.driver.page_source, request=request, encoding='utf-8', status=200) # ...緊接著,我們需要將這個(gè)下載中間件在 settings.py 中啟用:DOWNLOADER_MIDDLEWARES = { 'jd_books.middlewares.JdBooksDownloaderMiddleware': 543,}最后我們來(lái)完成下數(shù)據(jù)的存儲(chǔ),繼續(xù)使用 mongodb 來(lái)保存抓取到的數(shù)據(jù)。從實(shí)際測(cè)試中發(fā)現(xiàn)京東的搜索結(jié)果在100頁(yè)中肯定會(huì)有不少重復(fù)的數(shù)據(jù)。因此我們的 item pipelines 需要完成2個(gè)處理,分別是去重和保存。來(lái)直接看代碼:import pymongofrom scrapy.exceptions import DropItemfrom itemadapter import ItemAdapterclass JdBooksPipeline: def open_spider(self, spider): self.client = pymongo.MongoClient(host='47.115.61.209', port=27017) self.client.admin.authenticate("admin", "shencong1992") db = self.client.scrapy_manual self.collection = db.jd_books def process_item(self, item, spider): try: book_info = { 'book_name': item['book_name'], 'comments': item['comments'], 'book_detail_url': item['book_detail_url'], 'shop_name': item['shop_name'], 'price': item['price'], } self.collection.insert_one(book_info) except Exception as e: print("插入數(shù)據(jù)異常:{}".format(str(e))) return item def close_spider(self, spider): self.client.close()class DuplicatePipeline: """ 去除重復(fù)的數(shù)據(jù),重復(fù)數(shù)據(jù)直接拋出異常,不會(huì)進(jìn)入下一個(gè)流水線處理 """ def __init__(self): self.book_url_set = set() def process_item(self, item, spider): if item['book_detail_url'] in self.book_url_set: print('重復(fù)搜索結(jié)果:book={}, url={}'.format(item['book_name'], item['book_detail_url'])) raise DropItem('duplicate book info, drop it') self.book_url_set.add(item['book_detail_url']) return item我們直接使用 Item 的 book_detail_url 字段來(lái)判斷數(shù)據(jù)是否重復(fù)。此外,同樣需要將這兩個(gè) Item Pipelines 在 settings.py 中啟用,且保證 DuplicatePipeline 需要先于 JdBooksPipeline 處理:ITEM_PIPELINES = { 'jd_books.pipelines.DuplicatePipeline': 200, 'jd_books.pipelines.JdBooksPipeline': 300,}最后剩下一步就是禁止遵守 Robot 協(xié)議:ROBOTSTXT_OBEY = True至此,我們的 Scrapy 和 Selenium 結(jié)合爬取京東圖書(shū)數(shù)據(jù)的項(xiàng)目就算完成了。為了快速演示效果,我們將最大請(qǐng)求頁(yè)設(shè)置為10,然后運(yùn)行代碼看看實(shí)際的爬取效果:107

2.3 shell 命令的執(zhí)行過(guò)程

我們?cè)谏弦还?jié)中介紹了 scrapy shell [url] 這樣的指令,它幫助我們進(jìn)入交互模式去執(zhí)行調(diào)試獲取網(wǎng)頁(yè)的 xpath 表達(dá)式。我們有沒(méi)有想過(guò)這個(gè)命令背后的原理?今天我們專門(mén)學(xué)習(xí)了 Scrapy Command,那么就正好借此機(jī)會(huì)看看 scrapy shell [url] 這條命令背后的原理是什么。根據(jù)前面跟蹤代碼的經(jīng)驗(yàn),我們可以直接定位到 scrapy/commands/shell.py 下 Command 類中的 run() 方法即可:# 源碼位置: scrapy/commands/shell.py# ...class Command(ScrapyCommand): # ... def run(self, args, opts): url = args[0] if args else None if url: # first argument may be a local file url = guess_scheme(url) spider_loader = self.crawler_process.spider_loader spidercls = DefaultSpider if opts.spider: spidercls = spider_loader.load(opts.spider) elif url: # 如果傳入了url參數(shù),后面需要做請(qǐng)求 spidercls = spidercls_for_request(spider_loader, Request(url), spidercls, log_multiple=True) crawler = self.crawler_process._create_crawler(spidercls) crawler.engine = crawler._create_engine() crawler.engine.start() # 啟動(dòng)爬蟲(chóng)線程爬取url self._start_crawler_thread() shell = Shell(crawler, update_vars=self.update_vars, code=opts.code) shell.start(url=url, redirect=not opts.no_redirect) def _start_crawler_thread(self): t = Thread(target=self.crawler_process.start, kwargs={'stop_after_crawl': False}) t.daemon = True t.start()其實(shí)上面代碼的執(zhí)行邏輯是比較簡(jiǎn)單的,總的來(lái)說(shuō)就做了2件事情:創(chuàng)建 scrapy 引擎并單獨(dú)啟動(dòng)一個(gè)線程,后臺(tái)運(yùn)行;啟動(dòng) shell 線程;我們來(lái)關(guān)注下 Shell 這個(gè)類:# 源碼位置:scrapy/shell.py# ...class Shell: # ... def start(self, url=None, request=None, response=None, spider=None, redirect=True): # disable accidental Ctrl-C key press from shutting down the engine signal.signal(signal.SIGINT, signal.SIG_IGN) if url: self.fetch(url, spider, redirect=redirect) elif request: self.fetch(request, spider) elif response: request = response.request self.populate_vars(response, request, spider) else: self.populate_vars() if self.code: print(eval(self.code, globals(), self.vars)) else: """ Detect interactive shell setting in scrapy.cfg e.g.: ~/.config/scrapy.cfg or ~/.scrapy.cfg [settings] # shell can be one of ipython, bpython or python; # to be used as the interactive python console, if available. # (default is ipython, fallbacks in the order listed above) shell = python """ cfg = get_config() section, option = 'settings', 'shell' env = os.environ.get('SCRAPY_PYTHON_SHELL') shells = [] if env: shells += env.strip().lower().split(',') elif cfg.has_option(section, option): shells += [cfg.get(section, option).strip().lower()] else: # try all by default shells += DEFAULT_PYTHON_SHELLS.keys() # always add standard shell as fallback shells += ['python'] start_python_console(self.vars, shells=shells, banner=self.vars.pop('banner', ''))從上面的代碼我們可以看到一點(diǎn),當(dāng)傳入的參數(shù)有 url 或者 request 時(shí),會(huì)調(diào)用 fetch() 方法去下載網(wǎng)頁(yè)數(shù)據(jù),它會(huì)調(diào)用 twisted 框架中的 threads 來(lái)執(zhí)行網(wǎng)頁(yè)的下載動(dòng)作,并設(shè)置變量 response 。這就是為什么我們能在 scrapy shell 中直接使用 response 獲取下載網(wǎng)頁(yè)內(nèi)容的原因。# 源碼位置:scrapy/shell.py# ...class Shell: # ... def fetch(self, request_or_url, spider=None, redirect=True, **kwargs): from twisted.internet import reactor if isinstance(request_or_url, Request): request = request_or_url else: url = any_to_uri(request_or_url) request = Request(url, dont_filter=True, **kwargs) if redirect: request.meta['handle_httpstatus_list'] = SequenceExclude(range(300, 400)) else: request.meta['handle_httpstatus_all'] = True response = None try: response, spider = threads.blockingCallFromThread( reactor, self._schedule, request, spider) except IgnoreRequest: pass # 設(shè)置response結(jié)果 self.populate_vars(response, request, spider) def populate_vars(self, response=None, request=None, spider=None): import scrapy self.vars['scrapy'] = scrapy self.vars['crawler'] = self.crawler self.vars['item'] = self.item_class() self.vars['settings'] = self.crawler.settings self.vars['spider'] = spider self.vars['request'] = request self.vars['response'] = response if self.inthread: self.vars['fetch'] = self.fetch self.vars['view'] = open_in_browser self.vars['shelp'] = self.print_help self.update_vars(self.vars) if not self.code: self.vars['banner'] = self.get_help() # ...繼續(xù)跟蹤前面的 start() 方法,很明顯我們的核心函數(shù)就是一句:start_python_console(self.vars, shells=shells, banner=self.vars.pop('banner', ''))self.vars 就是需要帶到 shell 環(huán)境中的變量,shells 是我們選擇交互的環(huán)境,后面可以看到總共支持4種交互環(huán)境,分別是 ptpython、ipython、ipython、和 python。banner 參數(shù)則表示進(jìn)入交互模式是給出的提示語(yǔ)句。我們來(lái)看 start_python_console() 方法的源碼:# 源碼位置:scrapy/utils/console.py# ...DEFAULT_PYTHON_SHELLS = OrderedDict([ ('ptpython', _embed_ptpython_shell), ('ipython', _embed_ipython_shell), ('bpython', _embed_bpython_shell), ('python', _embed_standard_shell),])def get_shell_embed_func(shells=None, known_shells=None): """Return the first acceptable shell-embed function from a given list of shell names. """ if shells is None: # list, preference order of shells shells = DEFAULT_PYTHON_SHELLS.keys() if known_shells is None: # available embeddable shells known_shells = DEFAULT_PYTHON_SHELLS.copy() for shell in shells: if shell in known_shells: try: # function test: run all setup code (imports), # but dont fall into the shell return known_shells[shell]() except ImportError: continuedef start_python_console(namespace=None, banner='', shells=None): """Start Python console bound to the given namespace. Readline support and tab completion will be used on Unix, if available. """ if namespace is None: namespace = {} try: shell = get_shell_embed_func(shells) if shell is not None: shell(namespace=namespace, banner=banner) except SystemExit: # raised when using exit() in python code.interact pass通過(guò)分析代碼可知:get_shell_embed_func() 方法最終會(huì)返回 DEFAULT_PYTHON_SHELLS 中對(duì)應(yīng)值得那個(gè),比如我們傳入的 shells 值為 ['python'],則最后返回 _embed_standard_shell() 這個(gè)函數(shù)。最后就是調(diào)用這個(gè)函數(shù),即可得到 scrapy shell 的交互模式。來(lái)最后看一看 _embed_standard_shell() 這個(gè)神奇的方法:# 源碼位置:scrapy/utils/console.py# ...def _embed_standard_shell(namespace={}, banner=''): """Start a standard python shell""" import code try: # readline module is only available on unix systems import readline except ImportError: pass else: import rlcompleter # noqa: F401 readline.parse_and_bind("tab:complete") @wraps(_embed_standard_shell) def wrapper(namespace=namespace, banner=''): code.interact(banner=banner, local=namespace) return wrapper這段代碼雖然簡(jiǎn)短,但它卻是實(shí)現(xiàn) scrapy shell 交互模式的核心方法。接下來(lái),我們將基于上面這些方法來(lái)模擬構(gòu)造一個(gè)簡(jiǎn)化的交互式模式來(lái)幫助我們更好的理解這些方法的作用。來(lái)看我抽取這些方法,簡(jiǎn)單編寫(xiě)的一個(gè) test.py 腳本:[root@server2 shen]# cat test.py from functools import wrapsdef _embed_standard_shell(namespace={}, banner=''): """Start a standard python shell""" import code try: # readline module is only available on unix systems import readline except ImportError: pass else: import rlcompleter # noqa: F401 readline.parse_and_bind("tab:complete") @wraps(_embed_standard_shell) def wrapper(namespace=namespace, banner=''): code.interact(banner=banner, local=namespace) return wrapperdef start_python_console(namespace=None, banner='', shells=None): """Start Python console bound to the given namespace. Readline support and tab completion will be used on Unix, if available. """ if namespace is None: namespace = {} try: shell = _embed_standard_shell() shell(namespace=namespace, banner=banner) except SystemExit: # raised when using exit() in python code.interact passstart_python_console({'hello': 'world'}, banner='nothing to say')我們來(lái)運(yùn)行下這個(gè)測(cè)試腳本看看效果:[root@server2 shen]# python3 test.py nothing to say>>> hello'world'>>> xxxTraceback (most recent call last): File "<console>", line 1, in <module>NameError: name 'xxx' is not defined>>> exit()是不是和 scrapy shell 交互式一模一樣?到此為止,我們對(duì) scrapy shell 這個(gè)命令已經(jīng)分析的非常清楚了,大家是不是已經(jīng)都理解了呢?

2. 基于 Selenium 完成發(fā)票認(rèn)證

接下來(lái),我們基于 Scrapy 和 Selenium 來(lái)完成筆者工作中的一個(gè)需求。我們每個(gè)月有一筆通信發(fā)票的報(bào)銷,需要使用對(duì)自己在營(yíng)業(yè)廳中買(mǎi)的發(fā)票進(jìn)行校驗(yàn),然后要截圖留存。報(bào)銷的時(shí)間有時(shí)候是3個(gè)月一次,有時(shí)候是半年,所以累積下來(lái)有不少發(fā)票,這些發(fā)票都需要校驗(yàn)和截圖才能報(bào)銷。我們應(yīng)屆生剛工作的時(shí)候,我們大部分新員工都是手工截圖,非常笨拙,且耗時(shí)。由于是搞技術(shù)的公司,于是有前端人員寫(xiě)了相關(guān)的前端插件來(lái)自動(dòng)化截圖和生成發(fā)票校驗(yàn)文檔,然后在公司內(nèi)廣泛應(yīng)用。我也寫(xiě)了這樣一段基于 Selenium 的自動(dòng)化截圖代碼,不過(guò)代碼依賴 chrome 和 webdriver,所以組內(nèi)的部分人會(huì)把發(fā)票的起始編號(hào)和張數(shù)發(fā)給我,我運(yùn)行程序截好圖后將圖片打包發(fā)給他們自己放到 word 文檔中去。來(lái)看看發(fā)票校驗(yàn)的網(wǎng)站的截圖如下:廣東通信發(fā)票校驗(yàn)網(wǎng)站驗(yàn)證的方式非常機(jī)械,正是因?yàn)闄C(jī)械操作,才給了我們自動(dòng)化的可能、看上圖中的四個(gè)輸入框,表示的含義分別為:發(fā)票代碼:固定值;發(fā)票號(hào)碼:通常而言,從移動(dòng)營(yíng)業(yè)廳拿的通信發(fā)票是連續(xù)的,這樣就可以用 for 循環(huán)實(shí)現(xiàn);納稅人識(shí)別號(hào):這個(gè)是個(gè)固定值;發(fā)票面額:固定50元。假設(shè)我買(mǎi)300元的卡,就會(huì)對(duì)應(yīng)6張發(fā)票;我們填好每張發(fā)票的相應(yīng)信息,只有一個(gè)變量。在查詢之前,需要先拖動(dòng)滑塊進(jìn)行驗(yàn)證,驗(yàn)證通過(guò),再點(diǎn)擊查詢就可以進(jìn)行認(rèn)證。得到了認(rèn)證結(jié)果后,截張圖,如此進(jìn)行下去,直到所有的發(fā)票都校驗(yàn)截圖完畢。令人欣慰的是這里的滑塊驗(yàn)證只需要做一次,得到了相應(yīng)的認(rèn)證截圖后,每次調(diào)整下一張發(fā)票的發(fā)票號(hào)碼,在截一張圖即可。看下面的做法:104因此,我們得到了這樣的機(jī)械化動(dòng)作:打開(kāi)發(fā)票校驗(yàn)頁(yè)面;輸入第一張發(fā)票的四個(gè)參數(shù),然后拖動(dòng)滑塊到最右端完成校驗(yàn);截圖留存;改動(dòng)第二個(gè)輸入框的發(fā)票號(hào)碼,為下一個(gè)發(fā)票編號(hào),然后直接截圖;重復(fù),直到最后一張發(fā)票截圖成功;對(duì)應(yīng)這樣的動(dòng)作我們翻譯成相應(yīng)的 Selenium 自動(dòng)化代碼如下:"""測(cè)試 selenium 工具"""import timeimport randomfrom selenium import webdriverfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.common.by import Byfrom selenium.webdriver import ActionChains# 固定值ticket_code = "144011690802"identification_number = "91440101618652334F"face_value = 50# 起始編號(hào)ticket_start_num = 15415104# 發(fā)票總數(shù)total_count = 10def click_space(driver): """ 點(diǎn)擊下空白處,使得輸入框失去焦點(diǎn) """ driver.find_elements_by_xpath('//div[@class="check-main"]/table/tbody/tr[1]/td[1]')[0].click()def fill_input(driver, idx): """ 填充輸入框 """ input_value = [ticket_code, idx, identification_number, face_value] table = driver.find_elements_by_xpath('//div[@class="check-main"]/table/tbody')[0] for i in range(1, len(input_value) + 1): input = table.find_elements_by_xpath(f'./tr[{i}]/td[2]/input')[0] input.clear() input.send_keys(input_value[i - 1]) click_space(driver)def get_track(distance): """ 參考文獻(xiàn)2代碼,模擬人移動(dòng)鼠標(biāo) :distance為傳入的總距離 """ # 移動(dòng)軌跡 track = [] # 當(dāng)前位移 current = 0 # 減速閾值 mid = distance * 3 / 5 # 計(jì)算間隔 t = 0.4 # 初速度 v = 1 while current < distance: if current < mid: # 加速度為一個(gè)隨機(jī)值 a = random.randint(2, 6) else: # 加速度為一個(gè)隨機(jī)負(fù)值 a = -1 * random.randint(1, 2) v0 = v # 當(dāng)前速度 v = v0 + a * t # 移動(dòng)距離 move = v0 * t + 0.5 * a * t * t # 當(dāng)前位移 current += move # 加入軌跡 track.append(round(move)) return trackdef move_to_gap(slider, tracks): """ 參考文獻(xiàn)2代碼 :slider是要移動(dòng)的滑塊,tracks是要傳入的移動(dòng)軌跡 """ action = ActionChains(driver) action.click_and_hold(slider).perform() for x in tracks: ActionChains(driver).move_by_offset(xoffset=x,yoffset=0).perform() time.sleep(0.1) ActionChains(driver).release().perform()# 發(fā)票校驗(yàn)地址verify_address = "https://gs.etax-gd.gov.cn/gsyw/service/fpyw/fpcy/index"# 想屏蔽selenium標(biāo)識(shí),避免被服務(wù)端檢測(cè)到,似乎沒(méi)起作用options = webdriver.ChromeOptions()options.add_experimental_option("excludeSwitches", ['enable-automation'])driver = webdriver.Chrome(options=options, executable_path="C:/Users/spyinx/AppData/Local/Google/Chrome/Application/chromedriver.exe")driver.maximize_window()# 第一步先去發(fā)票查詢頁(yè)面driver.get(verify_address)# 等待滑塊出現(xiàn)WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, 'nc_1_n1z')))# 填充輸入框fill_input(driver, ticket_start_num)# 等待幾秒后driver.implicitly_wait(5)# 找到滑動(dòng)按鈕,模擬鼠標(biāo)按下不松開(kāi)slider = driver.find_element_by_id('nc_1_n1z')move_to_gap(slider, get_track(300))driver.implicitly_wait(2)driver.find_element_by_id("CxBtn").click()# 查詢第一個(gè)需要滑動(dòng)滑塊driver.get_screenshot_as_file(f"{ticket_start_num}.png")for i in range(1, total_count): ticket_num = ticket_start_num + i fill_input(driver, ticket_num) driver.get_screenshot_as_file(f"{ticket_num}.png") driver.implicitly_wait(1)上面的代碼已經(jīng)做好了詳細(xì)的注釋,請(qǐng)查找相關(guān)資料了解 Selenium 提供的相關(guān)方法,比如定位頁(yè)面元素的方法、對(duì)頁(yè)面元素點(diǎn)擊(click)、輸入框情況 (clear) 以及輸入元素(send_keys) 以及鼠標(biāo)的相關(guān)操作方法。下面我們直接來(lái)看代碼的演示效果,最后的截圖也全部得到。105注意:由于這個(gè)滑動(dòng)驗(yàn)證碼是接入的阿里滑動(dòng)驗(yàn)證碼插件,具備超強(qiáng)的反爬蟲(chóng)能力,能識(shí)別出是瀏覽器否被 selenium 控制。本次測(cè)試重復(fù)了無(wú)數(shù)次,才終于有一次沒(méi)有出現(xiàn)滑塊校驗(yàn)錯(cuò)誤,才成功錄下此視頻。具體的可以搜索大神如何破解阿里的滑動(dòng)驗(yàn)證碼,不過(guò)大部分代碼已經(jīng)過(guò)時(shí),無(wú)法突破驗(yàn)證??偠灾厦娴牟僮魇遣皇且欢ǔ潭壬夏軒椭覀儨p少手工操作,節(jié)約了時(shí)間成本?如果大家感興趣的話,可以嘗試使用 Selenium 完成京東商城的自動(dòng)登錄操作,這里會(huì)有滑動(dòng)圖片缺口補(bǔ)全的校驗(yàn),會(huì)稍微有點(diǎn)復(fù)雜,對(duì)你們來(lái)說(shuō)也是一個(gè)不錯(cuò)的挑戰(zhàn)。

1.2 標(biāo)簽

DTL 中標(biāo)簽的寫(xiě)法為: {% 標(biāo)簽 %},常用的標(biāo)簽有 for 循環(huán)標(biāo)簽,條件判斷標(biāo)簽 if/elif/else。部分標(biāo)簽在使用時(shí),需要匹配對(duì)應(yīng)的結(jié)束標(biāo)簽。Django 中常用的內(nèi)置標(biāo)簽如下表格所示:標(biāo)簽描述{% for %}for 循環(huán),遍歷變量中的內(nèi)容{% if %}if 判斷{% csrf_token %}生成 csrf_token 的標(biāo)簽{% static %}讀取靜態(tài)資源內(nèi)容{% with %}多用于給一個(gè)復(fù)雜的變量起別名{% url %}反向生成相應(yīng)的 URL 地址{% include 模板名稱 %}加載指定的模板并以標(biāo)簽內(nèi)的參數(shù)渲染{% extends 模板名稱 %}模板繼承,用于標(biāo)記當(dāng)前模板繼承自哪個(gè)父模板{% block %}用于定義一個(gè)模板塊1.2.1 for 標(biāo)簽的用法:{# 遍歷列表 #}<ul>{% for person in persons %}<li>{{ person }}</li>{% endfor %}</ul>{# 遍歷字典 #}<ul>{% for key, value in data.items %}<li>{{ key }}:{{ value }}</li>{% endfor %}</ul>在 for 循環(huán)標(biāo)簽中,還提供了一些變量,供我們使用:變量描述forloop.counter當(dāng)前循環(huán)位置,從1開(kāi)始forloop.counter0當(dāng)前循環(huán)位置,從0開(kāi)始forloop.revcounter反向循環(huán)位置,從n開(kāi)始,到1結(jié)束forloop.revcounter0反向循環(huán)位置,從n-1開(kāi)始,到0結(jié)束forloop.first如果是當(dāng)前循環(huán)的第一位,返回Trueforloop.last如果是當(dāng)前循環(huán)的最后一位,返回Trueforloop.parentloop在嵌套for循環(huán)中,獲取上層for循環(huán)的forloop實(shí)驗(yàn):(django-manual) [root@server first_django_app]# cat templates/test_for.html 遍歷列表:<ul>{% spaceless %}{% for person in persons %}{% if forloop.first %}<li>第一次:{{ forloop.counter }}:{{ forloop.counter0 }}:{{ person }}:{{ forloop.revcounter }}:{{ forloop.revcounter }}</li>{% elif forloop.last %}<li>最后一次:{{ forloop.counter }}:{{ forloop.counter0 }}:{{ person }}:{{ forloop.revcounter }}:{{ forloop.revcounter }}</li>{% else %}</li>{{ forloop.counter }}:{{ forloop.counter0 }}:{{ person }}:{{ forloop.revcounter }}:{{ forloop.revcounter }}</li>{% endif %}{% endfor %}{% endspaceless %}</ul>{% for name in name_list %} {{ name }}{% empty %} <p>name_list變量為空</p>{% endfor %} 倒序遍歷列:{% spaceless %}{% for person in persons reversed %}<p>{{ person }}:{{ forloop.revcounter }}</p>{% endfor %}{% endspaceless %}遍歷字典:{% spaceless %}{% for key, value in data.items %}<p>{{ key }}:{{ value }}</p>{% endfor %}{% endspaceless %}(django-manual) [root@server first_django_app]# python manage.py shellPython 3.8.1 (default, Dec 24 2019, 17:04:00) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>> from django.template.loader import get_template>>> tp = get_template('test_for.html')>>> content = tp.render(context={'persons':['張三', '李四', '王二麻子'], 'name_list': [], 'data': {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}})>>> print(content)遍歷列表:<ul><li>第一次:1:0:張三:3:3</li></li>2:1:李四:2:2</li><li>最后一次:3:2:王二麻子:1:1</li></ul> <p>name_list變量為空</p> 倒序遍歷列:<p>王二麻子:3</p><p>李四:2</p><p>張三:1</p>遍歷字典:<p>key1:value1</p><p>key2:value2</p><p>key3:value3</p>1.2.2 if 標(biāo)簽:支持嵌套,判斷的條件符號(hào)與變量之間必須使用空格隔開(kāi),示例如下。(django-manual) [root@server first_django_app]# cat templates/test_if.html{% if spyinx.sex == 'male' %}<label>他是個(gè)男孩子</label>{% else %}<label>她是個(gè)女孩子</label>{% endif %}(django-manual) [root@server first_django_app]# python manage.py shellPython 3.8.1 (default, Dec 24 2019, 17:04:00) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>> from django.template.loader import get_template>>> tp = get_template('test_if.html')>>> tp.render(context={'spyinx': {'age':29, 'sex': 'male'}})'\n<label>他是個(gè)男孩子</label>\n\n'>>> tp.render(context={'spyinx': {'age':29, 'sex': 'male'}})1.2.3 csrf_token 標(biāo)簽:這個(gè)標(biāo)簽會(huì)生成一個(gè)隱藏的 input 標(biāo)簽,其值為一串隨機(jī)的字符串。這個(gè)標(biāo)簽通常用在頁(yè)面上的 form 標(biāo)簽中。在渲染模塊時(shí),使用 RequestContext,由它處理 csrf_token 這個(gè)標(biāo)簽。下面來(lái)做個(gè)簡(jiǎn)單的測(cè)試:# 模板文件[root@server first_django_app]# cat templates/test_csrf.html <html><head></head><body><form enctype="multipart/form-data" method="post">{% csrf_token %}<div><span>賬號(hào):</span><input type="text" style="margin-bottom: 10px" placeholder="請(qǐng)輸入登錄手機(jī)號(hào)/郵箱" /></div><div><span>密碼:</span><input type="password" style="margin-bottom: 10px" placeholder="請(qǐng)輸入密碼" /></div><div><label style="font-size: 10px; color: grey"><input type="checkbox" checked="checked"/>7天自動(dòng)登錄</label></div><div style="margin-top: 10px"><input type="submit"/></div></form></body></html># 定義視圖:hello_app/views.py[root@server first_django_app]# cat hello_app/views.py from django.shortcuts import render# Create your views here.def test_csrf_view(request, *args, **kwargs): return render(request, 'test_csrf.html', context={})# 配置URLconf:hello_app/urls.py[root@server first_django_app]# cat hello_app/urls.pyfrom django.urls import pathurlpatterns = [ path('test-csrf/', views.test_csrf_view),]# 最后激活虛擬環(huán)境并啟動(dòng)django工程[root@server first_django_app] pyenv activate django-manual(django-manual) [root@server first_django_app]# python manage.py runserver 0:8881Watching for file changes with StatReloaderPerforming system checks...System check identified no issues (0 silenced).You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.Run 'python manage.py migrate' to apply them.March 27, 2020 - 04:10:05Django version 2.2.11, using settings 'first_django_app.settings'Starting development server at http://0:8881/Quit the server with CONTROL-C.現(xiàn)在通過(guò)外部請(qǐng)求這個(gè)URL,效果圖如下。通過(guò)右鍵的檢查功能,可以看到 {% csrf_token %} 被替換成了隱藏的 input 標(biāo)簽,value 屬性是一個(gè)隨機(jī)的長(zhǎng)字符串:csrf_token標(biāo)簽1.2.4 with 標(biāo)簽:對(duì)某個(gè)變量重新命名并使用:(django-manual) [root@server first_django_app]# cat templates/test_with.html {% spaceless %}{% with age1=spyinx.age %}<p>{{ age1 }}</p>{% endwith %}{% endspaceless %}{% spaceless %}{% with spyinx.age as age2 %}<div>{{ age2 }} </div>{% endwith %}{% endspaceless %}(django-manual) [root@server first_django_app]# python manage.py shellPython 3.8.1 (default, Dec 24 2019, 17:04:00) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.(InteractiveConsole)>>> from django.template.loader import get_template>>> tp = get_template('test_with.html')>>> content = tp.render(context={'spyinx': {'age': 29}})>>> print(content)<p>29</p><div>29 </div>1.2.5 spaceless 標(biāo)簽:移除 HTML 標(biāo)簽中的空白字符,包括空格、tab鍵、換行等。具體示例參見(jiàn)上面的示例;1.2.6 cycle 標(biāo)簽:循環(huán)提取 cycle 中的值,用法示例如下# 假設(shè)模板如下:{% for l in list %}<tr class="{% cycle 'r1' 'r2' 'r3'%}">{{l}}</tr>{% endfor %}# 對(duì)于傳入的 list 參數(shù)為:['l1', 'l2', 'l3'],最后生成的結(jié)果如下:<tr class="r1">l1</tr><tr class="r2">l2</tr><tr class="r3">l3</tr>1.2.7 include 標(biāo)簽:加載其他模板進(jìn)來(lái)。{% include "base/base.html" %}除了加載模板進(jìn)來(lái)外,include 標(biāo)簽還可以像加載進(jìn)來(lái)的模板傳遞變量。假設(shè)我們有個(gè) base/base.html 模板文件,其內(nèi)容為:{# base/base.html #}Hello {{ name|default:"Unknown" }}此時(shí),我們引入 base.html 模板文件時(shí),可以給 name 傳遞變量值:{% include "base/base.html" with name="test" %}

2. 如何深入插件源碼學(xué)習(xí)?

我們以 DRF 框架為例,聊一聊如何深入 DRF 框架的源碼學(xué)習(xí)。首先肯定是下載穩(wěn)定版本為 DRF 源碼到本地,這是為了方便自己閱讀代碼。截止到2020年5月10日,DRF 的 Github 官方地址發(fā)布的最新版本為3.11.0,我們會(huì)用該版本的代碼來(lái)進(jìn)行相關(guān)的演示和說(shuō)明。以下是 DRF-3.11.0 源代碼截圖,里面的代碼量還是比較大的,不過(guò)相對(duì)于 Django 的代碼而言就會(huì)少很多,我們前面能學(xué)習(xí)并跟蹤 Django 框架的源碼,拿下 DRF 源碼自然也不在話下。一般而言,推薦學(xué)習(xí)一個(gè) Django 第三方插件源碼的過(guò)程如下:第一步:熟練使用 Django 框架以及熟悉 Django 框架源碼。所有的 Django 第三方插件代碼里會(huì)大量調(diào)用 Django 源碼的類或者方法,并在其基礎(chǔ)上進(jìn)行擴(kuò)展或者進(jìn)一步創(chuàng)新。以我們必須先掌握 Django 的源碼,才能繼續(xù)學(xué)習(xí) DRF 的源碼;第二步:仔細(xì)閱讀官方文檔手冊(cè)進(jìn)行學(xué)習(xí),掌握框架的基本用法;第三步:通過(guò)官方文檔,實(shí)戰(zhàn) DRF 框架;每次在用熟練 DRF 提供的類或者方法后,就可以對(duì)應(yīng)地查看源碼,并分析 DRF 背后所做的工作。每掌握一個(gè)模塊的基本用法,就可以深入學(xué)習(xí)對(duì)應(yīng)模塊的源碼,同時(shí)在源碼中我們還可以發(fā)現(xiàn)該模塊中的更多用法,然后再次實(shí)踐,以加深對(duì)源碼的理解。我們按照上面的過(guò)程來(lái)簡(jiǎn)單走一遍。首先我們前面對(duì) Django 的幾大模塊的源碼都有涉獵,算是滿足了第一步要求。接下來(lái)我們用官方給的快速入門(mén)教程完成我們的第一次 Django REST framework 框架的初體驗(yàn)。模型序列化器:給會(huì)員表 member 添加一個(gè)序列化器類,放到新建的 serializers.py 文件中。from rest_framework import serializersfrom hello_app.models import Memberclass MemberSerializer(serializers.ModelSerializer): class Meta: model = Member fields = ("id", "name", "age", "sex", "occupation", "phone_num", "email", "city", "vip_level_id")準(zhǔn)備 View 視圖:添加一個(gè)對(duì)會(huì)員表操作的視圖類,我們用最簡(jiǎn)單的形式即可。# 代碼位置:hello_app/views.py# ...from rest_framework import viewsetsfrom rest_framework import permissions# ...class MemberViewSet(viewsets.ModelViewSet): # 設(shè)置queryset queryset = Member.objects.all().order_by('-register_date') # 設(shè)置序列化器 serializer_class = MemberSerializer # 設(shè)置認(rèn)證器 permission_classes = [permissions.IsAuthenticated]編寫(xiě) URLConf 配置:Django REST framework 框架改良了 URLConf 配置的寫(xiě)法,后面會(huì)研究這種寫(xiě)法,先直接使用官方的示例即可。# 代碼位置:hello_app/urls.py# ...from rest_framework import routersrouter = routers.DefaultRouter()router.register(r'members', views.MemberViewSet)urlpatterns = [ # ... path('', include(router.urls))]另外,由于我們對(duì) MemberViewSet 視圖加上了認(rèn)證,所以必須要在入口的 urls.py 中上如下的 URLConf 的配置。# 代碼位置: first_django_app/urls.py# ...urlpatterns = [ # ... path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))]注意:不添加和添加這行 URLConf 配置的效果圖如下所示。接下來(lái),最后一步是設(shè)置視圖的相關(guān)配置以及注冊(cè) rest_framework 應(yīng)用。# 代碼位置:first_django_app/settings.py# ...INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 注冊(cè)第三方應(yīng)用 'rest_framework', # 注冊(cè)應(yīng)用 'hello_app']REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 5}# ...最后我們啟動(dòng)服務(wù),來(lái)一起看看效果。我們之前創(chuàng)建過(guò)一個(gè)超級(jí)用戶admin/admin.1234!,接下來(lái)會(huì)用這個(gè)通過(guò) DRF 的認(rèn)證。25從上面的演示中,我們看到了 Django REST framework 框架給我們做的接口測(cè)試頁(yè)面,我們只需要簡(jiǎn)單繼承下MemberViewSet 即可,然后添加相關(guān)屬性即可立即擁有這樣一個(gè)完整的接口測(cè)試頁(yè)面。后臺(tái)服務(wù)主要提供接口數(shù)據(jù),我們也可以使用 curl 命令來(lái)獲取和操作相應(yīng)的模型表。[root@server ~]# curl -H 'Accept: application/json; indent=4' -u admin:admin.1234! http://127.0.0.1:8888/hello/members/?page=3{ "count": 103, "next": "http://127.0.0.1:8888/hello/members/?page=4", "previous": "http://127.0.0.1:8888/hello/members/?page=2", "results": [ { "id": 9, "name": "spyinx-5", "age": "39", "sex": 0, "occupation": "product", "phone_num": "18015702646", "email": "225@qq.com", "city": "shanghai", "vip_level_id": null }, { "id": 10, "name": "spyinx-6", "age": "26", "sex": 0, "occupation": "ops", "phone_num": "18790082215", "email": "226@qq.com", "city": "beijing", "vip_level_id": null }, { "id": 11, "name": "spyinx-7", "age": "23", "sex": 0, "occupation": "security", "phone_num": "18354491889", "email": "227@qq.com", "city": "guangzhou", "vip_level_id": null }, { "id": 12, "name": "spyinx-8", "age": "26", "sex": 1, "occupation": "ui", "phone_num": "18406891676", "email": "228@qq.com", "city": "wuhan", "vip_level_id": null }, { "id": 13, "name": "spyinx-9", "age": "26", "sex": 0, "occupation": "ops", "phone_num": "18036496230", "email": "229@qq.com", "city": "wuhan", "vip_level_id": null } ]}在上面這個(gè)過(guò)程走通之后,我們可以看到其實(shí)這個(gè)例子中已經(jīng)涉及到了 DRF 中的許多類,比如用于序列化的類ModelSerializer、視圖類 ModelViewSet、分頁(yè)類 PageNumberPagination 等等。從這個(gè)案例中,我們可以找到許多學(xué)習(xí) DRF 源碼的切入點(diǎn)。首先看用到的視圖類 ModelViewSet:# 源碼位置:rest_framework/viewsets.pyclass ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet): """ A viewset that provides default `create()`, `retrieve()`, `update()`, `partial_update()`, `destroy()` and `list()` actions. """ pass通過(guò)學(xué)習(xí) Django 的視圖,我們了解了 Mixin 這個(gè)概念,所以容易理解這里的代碼,視圖繼承 GenericViewSet,同時(shí)也繼承了數(shù)個(gè) Mixin。這些 Mixin 從命名上就很容易知道其功能用法。進(jìn)一步翻看其實(shí)現(xiàn)類,也能發(fā)現(xiàn)其具體含義 。以 mixins.CreateModelMixin 類為例:# rest_framework/mixins.pyclass CreateModelMixin: """ Create a model instance. """ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() def get_success_headers(self, data): try: return {'Location': str(data[api_settings.URL_FIELD_NAME])} except (TypeError, KeyError): return {}CreateModelMixin 的主要功能就是提供了 create() 方法,讓視圖擁有新增記錄的功能。其他的 Mixin 會(huì)提供類似的函數(shù),讓視圖具有某一特定的功能。接下來(lái)我們的重點(diǎn)放到 GenericViewSet 類的學(xué)習(xí)上。# 源碼位置:rest_framework/viewsets.pyclass GenericViewSet(ViewSetMixin, generics.GenericAPIView): """ The GenericViewSet class does not provide any actions by default, but does include the base set of generic view behavior, such as the `get_object` and `get_queryset` methods. """ pass這里又是多繼承,一個(gè) ViewSetMixin 類,另一個(gè) generics.GenericAPIView 類。先追后面的 View 類,實(shí)現(xiàn)代碼如下:從這里我們看到了一些熟悉的屬性,如 queryset,serializer_class 以及用于分頁(yè)的 pagination_class。這個(gè)繼承的 APIView 類同樣也是 Django REST framework 框架自己定義的類,我們繼續(xù)追進(jìn) APIView 類的實(shí)現(xiàn)代碼:最后 APIView 這個(gè)類繼承的 View 正是 Django 中我們學(xué)過(guò)的 View 視圖類。from django.views.generic import View那這樣子,我們也算清楚了一些事情。Django REST framework 框架中定義的視圖是在 Django 的 View 視圖類上封裝和改進(jìn)來(lái)的?,F(xiàn)在一個(gè)疑問(wèn)就來(lái)了,看我們前面使用 Django 的視圖中,URLConf 配置如下: urlpatterns = [ path('test-cbv/', views.TestView.as_view(), name="test-cbv"), ]我們也分析過(guò)對(duì)應(yīng)的 View 類以及 as_view() 方法,它將 GET 請(qǐng)求映射到視圖類的 get() 方法,POST 請(qǐng)求則映射到 post() 方法;然而我們這里一路走下來(lái)并有沒(méi)有看到對(duì)應(yīng)的 get() 或者 post() 方法。但是視圖類繼承的多個(gè) Mixin 中提供了 create()、list() 等這樣的方法,那么他們是如何和 URLConf 配置對(duì)應(yīng)上的呢?我們現(xiàn)在要通過(guò)代碼去找出前面配置 URLConf 代碼的內(nèi)部原理:from django.conf.urls import includefrom rest_framework import routersrouter = routers.DefaultRouter()router.register(r'members', views.MemberViewSet)urlpatterns = [ # ... path('', include(router.urls))]來(lái)看看上面的 URLConf 配置。這個(gè)時(shí)候,我們需要去看 Django REST Framework 中的 DefaultRouter 類,包括注冊(cè)方法 register() 以及 urls 屬性值的獲取。最后還要看 Django 中的 include() 方法的代碼,才能理清楚 URL 和視圖的映射關(guān)系。先追蹤 Django REST Framework 中的 DefaultRouter 類實(shí)現(xiàn),該類繼承自 SimpleRouter,SimpleRouter 又繼承自 BaseRouter。為了加快速度,我們直接定位到基類 BaseRouter,可以看到 register() 方法和 urls 屬性的定義,如下:# 源碼位置:rest_framework/routers.pyclass BaseRouter: def __init__(self): self.registry = [] def register(self, prefix, viewset, basename=None): if basename is None: basename = self.get_default_basename(viewset) self.registry.append((prefix, viewset, basename)) # invalidate the urls cache if hasattr(self, '_urls'): del self._urls def get_default_basename(self, viewset): """ If `basename` is not specified, attempt to automatically determine it from the viewset. """ raise NotImplementedError('get_default_basename must be overridden') def get_urls(self): """ Return a list of URL patterns, given the registered viewsets. """ raise NotImplementedError('get_urls must be overridden') @property def urls(self): if not hasattr(self, '_urls'): self._urls = self.get_urls() return self._urls可以看到,在執(zhí)行 router.register(r'members', views.MemberViewSet) 后其實(shí)等同于給 registry 數(shù)組添加一個(gè)元組元素,用于存儲(chǔ)映射關(guān)系。而 urls 屬性值則是調(diào)用 get_urls() 方法得到的。class DefaultRouter(SimpleRouter): """ The default router extends the SimpleRouter, but also adds in a default API root view, and adds format suffix patterns to the URLs. """ # ... def get_urls(self): """ Generate the list of URL patterns, including a default root view for the API, and appending `.json` style format suffixes. """ urls = super().get_urls() if self.include_root_view: view = self.get_api_root_view(api_urls=urls) root_url = url(r'^$', view, name=self.root_view_name) urls.append(root_url) if self.include_format_suffixes: urls = format_suffix_patterns(urls) return urls可以看到它先是調(diào)用了父類的 get_urls() 方法,另外又添加了一些映射規(guī)則。我們添加如下一行 print() 語(yǔ)句:class DefaultRouter(SimpleRouter): def get_urls(self): """ Generate the list of URL patterns, including a default root view for the API, and appending `.json` style format suffixes. """ urls = super().get_urls() print('父類調(diào)用得到的urls={}'.format(urls)) # ...然后啟動(dòng)服務(wù),可以看到如下的結(jié)果:(django-manual) [root@server first_django_app]# python manage.py runserver 0:8888Watching for file changes with StatReloaderPerforming system checks...父類調(diào)用得到的urls=[<URLPattern '^members/$' [name='member-list']>, <URLPattern '^members/(?P<pk>[^/.]+)/$' [name='member-detail']>]System check identified no issues (0 silenced).May 15, 2020 - 13:30:04Django version 2.2.12, using settings 'first_django_app.settings'Starting development server at http://0:8888/Quit the server with CONTROL-C可以看到,這個(gè) ^members/$ 的URL 配置是由父類的 get_urls() 方法得到的。在父類 SimpleRouter 中的get_urls()方法中,我已經(jīng)做好了相關(guān)的注釋,最關(guān)鍵的代碼就在最后的 append() 部分,那里添加的便是最后 URL 和 視圖函數(shù)的關(guān)系。class SimpleRouter(BaseRouter): # ... def get_urls(self): # ... # 前面介紹過(guò)這個(gè) registry 屬性,就是通過(guò) register() 方法得到的 for prefix, viewset, basename in self.registry: # ... for route in routes: # Only actions which actually exist on the viewset will be bound mapping = self.get_method_map(viewset, route.mapping) if not mapping: continue # Build the url pattern regex = route.url.format( prefix=prefix, lookup=lookup, # 尾部加上"/" trailing_slash=self.trailing_slash ) # 處理一些簡(jiǎn)單情況 if not prefix and regex[:2] == '^/': regex = '^' + regex[2:] initkwargs = route.initkwargs.copy() initkwargs.update({ 'basename': basename, 'detail': route.detail, }) # 最最核心的部分代碼,這里得到視圖函數(shù) view = viewset.as_view(mapping, **initkwargs) # 視圖名稱 name = route.name.format(basename=basename) # 添加映射規(guī)則 ret.append(url(regex, view, name=name)) return ret 我們可以看到最后添加的映射規(guī)則就是這一句:ret.append(url(regex, view, name=name)) ,我們繼續(xù)看看這個(gè) url() 方法,它調(diào)用的正是 Django 中的 url() 方法,內(nèi)容如下:# 源碼路徑:django/conf/urls.py# ...def url(regex, view, kwargs=None, name=None): return re_path(regex, view, kwargs, name)這個(gè) url() 方法和我們之前在 Django 中用 repath() 以及 path() 差不多一致的。第一個(gè)參數(shù)是 url 規(guī)則,第二個(gè)便是視圖函數(shù)。比較重要的就是這里得到 view 的函數(shù)了,它便是真正的視圖函數(shù)。它和前面 Django 中的一樣,通過(guò) as_view() 得到的。那么這個(gè) as_view() 方法在哪呢,通過(guò)父類追蹤,可知 Django 的父類中本身就有 as_view() 方法,但是在前一個(gè)繼承的Mixin 中重寫(xiě)了該方法,因此調(diào)用的便是該 Mixin 中的 as_view() 方法:class ViewSetMixin: """ This is the magic. Overrides `.as_view()` so that it takes an `actions` keyword that performs the binding of HTTP methods to actions on the Resource. For example, to create a concrete view binding the 'GET' and 'POST' methods to the 'list' and 'create' actions... view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) """ @classonlymethod def as_view(cls, actions=None, **initkwargs): """ Because of the way class based views create a closure around the instantiated view, we need to totally reimplement `.as_view`, and slightly modify the view function that is created and returned. """ # ... def view(request, *args, **kwargs): self = cls(**initkwargs) # We also store the mapping of request methods to actions, # so that we can later set the action attribute. # eg. `self.action = 'list'` on an incoming GET request. self.action_map = actions # Bind methods to actions # This is the bit that's different to a standard view for method, action in actions.items(): handler = getattr(self, action) setattr(self, method, handler) if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs # And continue as usual return self.dispatch(request, *args, **kwargs) # take name and docstring from class update_wrapper(view, cls, updated=()) # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) # We need to set these on the view function, so that breadcrumb # generation can pick out these bits of information from a # resolved URL. view.cls = cls view.initkwargs = initkwargs view.actions = actions return csrf_exempt(view)和 Django 中的一樣,這里最后的 as_view() 方法最后返回的便是視圖函數(shù)。那么對(duì)應(yīng)的 /hello/members/ 請(qǐng)求進(jìn)來(lái)后,有 view() 方法進(jìn)行處理,最后調(diào)用的和 Django 中的一樣:return self.dispatch(request, *args, **kwargs)我們?nèi)?Django 中看這個(gè) dispatch() 方法的源碼:# 源碼位置:django/views/generic/base.pyclass View: """ Intentionally simple parent class for all views. Only implements dispatch-by-method and simple sanity checking. """ # ... def dispatch(self, request, *args, **kwargs): # Try to dispatch to the right method; if a method doesn't exist, # defer to the error handler. Also defer to the error handler if the # request method isn't on the approved list. if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs)那么執(zhí)行 /hello/members/ 請(qǐng)求到這里是,handler 是哪個(gè)?我們繼續(xù)翻看前面的 Mixin 類,有這樣一段代碼:class ViewSetMixin: # ... @classonlymethod def as_view(cls, actions=None, **initkwargs): def view(request, *args, **kwargs): # ... for method, action in actions.items(): handler = getattr(self, action) setattr(self, method, handler) # ... # ... # ...這里就非常明顯了,我們大概也能猜到一些。就是設(shè)置 (get|post|put|delete) 請(qǐng)求對(duì)應(yīng)的方法,比較好的方式時(shí)我們?cè)谶@里打印下請(qǐng)求,并在前端進(jìn)行下請(qǐng)求測(cè)試,看看這里到底設(shè)置了啥?class ViewSetMixin: # ... @classonlymethod def as_view(cls, actions=None, **initkwargs): def view(request, *args, **kwargs): # ... for method, action in actions.items(): print('請(qǐng)求處理view視圖函數(shù):method={}, action={}'.format(method, action)) handler = getattr(self, action) setattr(self, method, handler) # ... # ... # ...我們啟動(dòng)服務(wù)請(qǐng)求以下路徑 /hello/members/,可以得到如下輸出結(jié)果:(django-manual) [root@server first_django_app]# python manage.py runserver 0:8888Watching for file changes with StatReloaderPerforming system checks...System check identified no issues (0 silenced).May 15, 2020 - 17:07:38Django version 2.2.12, using settings 'first_django_app.settings'Starting development server at http://0:8888/Quit the server with CONTROL-C.請(qǐng)求處理view視圖函數(shù):method=get, action=list請(qǐng)求處理view視圖函數(shù):method=post, action=create[15/May/2020 17:07:43] "GET /hello/members/ HTTP/1.1" 200 14426結(jié)合 Django 中的 dispatch() 方法,我們終于知道了 get 請(qǐng)求最后會(huì)調(diào)用視圖類中的 list() 方法去處理,而這個(gè) list() 方法正是 ListModelMixin 中的。另外 post 請(qǐng)求則對(duì)應(yīng)著視圖類中的 create() 方法,而這個(gè)屬性則來(lái)自 CreateModelMixin。這樣我們總算理解了前面的 URLConf 的映射流程以及對(duì)應(yīng)的真正視圖處理函數(shù)。帶著問(wèn)題去追源碼是我比較推薦的一個(gè)學(xué)習(xí)方式。完成一個(gè)模塊的學(xué)習(xí)就要去思考,去追蹤這個(gè)案例背后的執(zhí)行過(guò)程,這樣才能更好的掌握這個(gè)模塊。今天的分享就到此結(jié)束了,DRF 中還有很多代碼等著你們?nèi)ヌ剿鳎?shí)踐,祝大家學(xué)習(xí)愉快!

1. Windows 系統(tǒng)上的安裝

友情提示:以下內(nèi)容為安裝步驟演示與補(bǔ)充說(shuō)明,幫助加深理解。如果只想學(xué)習(xí)快速安裝的話,記住一句話:一路 Next 就可以。忽略以下內(nèi)容直接從 1.13 開(kāi)始看吧。1.1 首先從 Git 官網(wǎng)直接下載安裝程序。打開(kāi)官網(wǎng)可以看到 Windows 版本的安裝包下載位置,如紅色箭頭所示,點(diǎn)擊即可開(kāi)始下載最新版本安裝包。安裝包下載完成后,即可進(jìn)行本地安裝。接下來(lái)我將以 Git-2.15.1.2-64 版本來(lái)進(jìn)行講解。1.2 雙擊下載好的 .exe 文件,彈出如下安裝界面,直接點(diǎn)擊 “Next”。1.3 選擇安裝路徑,點(diǎn)擊右側(cè) “Browse” 按鈕更改路徑。建議大家單獨(dú)創(chuàng)建一個(gè)目錄,專門(mén)進(jìn)行安裝。我一般習(xí)慣固定使用一個(gè)非 C 盤(pán)來(lái)專門(mén)安裝辦公軟件,每個(gè)軟件單獨(dú)使用一個(gè)文件夾,這樣方便管理,盡量養(yǎng)成一個(gè)良好的習(xí)慣。1.4 選擇好安裝路徑后,直接點(diǎn)擊 “Next”,出現(xiàn)如下界面。這一步默認(rèn)勾選了紅色框內(nèi)容,其他選項(xiàng)大家可以依據(jù)需要進(jìn)行選擇。我還多選擇了 “Additional icons” 項(xiàng)目,表示會(huì)在桌面生成圖標(biāo)。倒數(shù)第二項(xiàng)表示:在所有控制臺(tái)窗口中使用 TrueType 字體。最后一項(xiàng)表示:是否每天檢查 Git 是否有 Windows 更新。1.5 選擇完畢后,繼續(xù) “Next”,出現(xiàn)如下界面。這一步?jīng)]有什么特別需要注意的,默認(rèn)即可。然后同樣點(diǎn)擊 “Next”。1.6 接下來(lái)出現(xiàn)這個(gè)頁(yè)面是選擇 Git 使用的文本編輯器,默認(rèn)即可。然后點(diǎn)擊 “Next”。1.7 這一步是用來(lái)調(diào)整 Path 環(huán)境。第一種配置是 “僅從 Git Bash 使用 Git”。這是最安全的選擇,因?yàn)槟?PATH 根本不會(huì)被修改,只能使用 Git Bash 的 Git 命令行工具。但是這將不能通過(guò)第三方軟件使用。第二種配置是 “從命令行以及第三方軟件進(jìn)行 Git”。該選項(xiàng)也是安全的,因?yàn)樗鼉H向 PATH 添加了一些最小的 Git 包裝器,以避免使用可選的 Unix 工具造成環(huán)境混亂。能夠從 Git Bash,命令提示符和 Windows PowerShell 以及在 PATH 中尋找 Git 的任何第三方軟件中使用 Git。這也是推薦的選項(xiàng)。第三種配置是 “從命令提示符使用 Git 和可選的 Unix 工具”。警告:這將覆蓋 Windows 工具,如 “ find 和 sort ”。只有在了解其含義后才使用此選項(xiàng)。使用推薦配置即可,點(diǎn)擊 “Next” 按鈕繼續(xù)到下圖的界面:  1.8 在這個(gè)界面選擇 HTTP 傳輸。第一個(gè)選項(xiàng)是 “使用 OpenSSL 庫(kù)”。服務(wù)器證書(shū)將使用 ca-bundle.crt 文件進(jìn)行驗(yàn)證。第二個(gè)選項(xiàng)是 “使用本地 Windows 安全通道庫(kù)”。服務(wù)器證書(shū)將使用 Windows 證書(shū)存儲(chǔ)驗(yàn)證。此選項(xiàng)還允許您使用公司的內(nèi)部根 CA 證書(shū),例如通過(guò) Active Directory Domain Services 。我使用默認(rèn)選項(xiàng),點(diǎn)擊 “Next” 按鈕繼續(xù)到下圖的界面:  1.9 繼續(xù)來(lái)到這個(gè)界面,配置行尾符號(hào)轉(zhuǎn)換。第一個(gè)選項(xiàng)是 “簽出 Windows 風(fēng)格,提交 Unix 風(fēng)格的行尾”。簽出文本文件時(shí),Git 會(huì)將 LF 轉(zhuǎn)換為 CRLF。提交文本文件時(shí),CRLF 將轉(zhuǎn)換為 LF。對(duì)于跨平臺(tái)項(xiàng)目,這是 Windows 上的推薦設(shè)置(“ core.autocrlf” 設(shè)置為 “ true”)第二個(gè)選項(xiàng)是 “按原樣簽出,提交 Unix 樣式的行尾”。簽出文本文件時(shí),Git 不會(huì)執(zhí)行任何轉(zhuǎn)換。 提交文本文件時(shí),CRLF 將轉(zhuǎn)換為 LF。對(duì)于跨平臺(tái)項(xiàng)目,這是 Unix 上的建議設(shè)置(“ core.autocrlf” 設(shè)置為 “ input”)第三種選項(xiàng)是 “按原樣簽出,按原樣提交”。當(dāng)簽出或提交文本文件時(shí),Git 不會(huì)執(zhí)行任何轉(zhuǎn)換。不建議跨平臺(tái)項(xiàng)目選擇此選項(xiàng)(“ core.autocrlf” 設(shè)置為 “ false”)那么 CRLF 和 LF 有什么區(qū)別?CRLF 是 carriage return line feed 的縮寫(xiě),中文意思是 回車(chē)換行。句尾使用回車(chē)換行兩個(gè)字符 (即我們常在 Windows 編程時(shí)使用”\r\n” 換行)。LF 是 line feed 的縮寫(xiě),中文意思是換行。我選擇默認(rèn)第一項(xiàng),點(diǎn)擊 “Next” 按鈕繼續(xù)到下一步:  1.10 配置終端模擬器和 Git Bash 一起使用第一個(gè)選項(xiàng)是 “使用 MinTTY(MSYS2 的默認(rèn)終端)”。Git Bash 將使用 MinTTY 作為終端模擬器,該模擬器具有可調(diào)整大小的窗口,非矩形選擇和 Unicode 字體。Windows 控制臺(tái)程序(例如交互式 Python)必須通過(guò) “ winpty” 啟動(dòng)才能在 MinTTY 中運(yùn)行。第二個(gè)選項(xiàng)是 “使用 Windows 的默認(rèn)控制臺(tái)窗口”。Git 將使用 Windows 的默認(rèn)控制臺(tái)窗口(“cmd.exe”),該窗口可以與 Win32 控制臺(tái)程序(如交互式 Python 或 node.js)一起使用,但默認(rèn)的回滾非常有限,需要配置為使用 unicode 字體以正確顯示非 ASCII 字符,并且在 Windows 10 之前,其窗口不能自由調(diào)整大小,并且只允許矩形文本選擇。此處默認(rèn)選了第一種選項(xiàng),然后繼續(xù)點(diǎn)擊 “Next” 按鈕進(jìn)入下一步:1.11 配置額外選項(xiàng)第一個(gè)選項(xiàng)是 “啟用文件系統(tǒng)緩存”。文件系統(tǒng)數(shù)據(jù)將被批量讀取并緩存在內(nèi)存中用于某些操作(“core.fscache” 設(shè)置為 “true”),性能顯著提升。第二個(gè)選項(xiàng)是 “啟用 Git 憑證管理器”。Windows 的 Git 憑證管理器為 Windows 提供安全的 Git 憑證存儲(chǔ),最顯著的是對(duì) Visual Studio Team Services 和 GitHub 的多因素身份驗(yàn)證支持。 (需要 .NET Framework v4.5.1 或更高版本)。第三個(gè)選項(xiàng)是 “啟用符號(hào)鏈接”。啟用符號(hào)鏈接(需要 SeCreateSymbolicLink 權(quán)限)。請(qǐng)注意,現(xiàn)有存儲(chǔ)庫(kù)不受此設(shè)置的影響。默認(rèn)選了第一、第二選項(xiàng),繼續(xù)點(diǎn)擊 “Next” 按鈕進(jìn)入下一步界面:  1.12 到這一步點(diǎn)擊 Finish 按鈕就完成安裝了。等安裝進(jìn)度條滿后,就可以在開(kāi)始菜單里找到 “Git”->“Git Bash”,點(diǎn)擊后出現(xiàn)一個(gè)類似命令行窗口的東西,就說(shuō)明 Git 安裝成功!此后,就可以在 Git Bash 窗口進(jìn)行 windows 環(huán)境下的 Git 操作了!1.13 同樣,我們不要忘記進(jìn)行身份信息配置# git config --global user.name "Your Name"# git config --global user.email "Your Email"好了,windows 環(huán)境的安裝步驟到這里已經(jīng)結(jié)束了。大家可以看到其中并沒(méi)有太多難點(diǎn),基本上每一步按默認(rèn)選項(xiàng)選擇,一直點(diǎn)擊 “Next” 往下走就可以完成基本的安裝配置,絕對(duì)不會(huì)錯(cuò)。是不是很簡(jiǎn)單!接下來(lái)向大家介紹一些 Git 的基本命令,以便于在后續(xù)的學(xué)習(xí)中可以隨時(shí)查看,同時(shí)也為我們之后的正式學(xué)習(xí)開(kāi)一個(gè)頭。請(qǐng)繼續(xù)往下看:

2. xpath 解析實(shí)戰(zhàn)

lxml 是 Python 中的一個(gè)解析庫(kù),支持 HTML 和 XML 的解析,支持 XPath 解析方式,而且解析效率非常高。本節(jié)將安裝該模塊解析 html 文本并提取相應(yīng)的數(shù)據(jù)。[store@server2 ~]$ sudo pip3 install lxmlWARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.Collecting lxml Downloading http://mirrors.cloud.aliyuncs.com/pypi/packages/55/6f/c87dffdd88a54dd26a3a9fef1d14b6384a9933c455c54ce3ca7d64a84c88/lxml-4.5.1-cp36-cp36m-manylinux1_x86_64.whl (5.5MB) 100% |████████████████████████████████| 5.5MB 82.9MB/s Installing collected packages: lxmlSuccessfully installed lxml-4.5.1我們先準(zhǔn)備好素材,也就是要解析的 HTML 文檔。為了更有代入感,我直接使用慕課網(wǎng) wiki 頁(yè)面的數(shù)據(jù)進(jìn)行操作,獲取數(shù)據(jù)的方式如下圖所示:獲取慕課網(wǎng) wiki 頁(yè)面的 HTML 數(shù)據(jù)最后保存到一個(gè) test.html 文本,然后我們要準(zhǔn)備一段 Python 代碼:from lxml import etreetree = etree.parse('test.html', etree.HTMLParser(encoding='utf8'))def print_result(exp, results): print('xpath表達(dá)式為:{},其匹配結(jié)果為:'.format(exp)) for res in results: print(res.strip()) print('')def test_xpath_expression(exp): results = tree.xpath(exp) print_result(exp, results)將這個(gè) Python 文件命名為 test_xpath.py 和 test.html 放在同一級(jí)目錄下:[store@server2 ~]$ lsshen test.html test_xpath.py接下來(lái)我們就可以進(jìn)行激動(dòng)人心的測(cè)試了,來(lái)完成一個(gè)簡(jiǎn)單的實(shí)驗(yàn):慕課網(wǎng) wiki 頁(yè)面數(shù)據(jù)獲取第一個(gè)實(shí)驗(yàn)的目標(biāo)就是拿到 javascript 分類下的教程的三個(gè)數(shù)據(jù):標(biāo)題、總節(jié)數(shù)以及訪問(wèn)次數(shù)。通過(guò) F12 查看相關(guān)的 HTML 結(jié)構(gòu),我們可以通過(guò)如下的 Xpath表達(dá)式獲取相應(yīng)的數(shù)據(jù):Python 3.6.8 (default, Apr 2 2020, 13:34:55) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.>>> from test_xpath import test_xpath_expression>>> exp1 = '//h2[@class="language-title"]/text()'>>> test_xpath_expression(exp1)xpath表達(dá)式為://h2[@class="language-title"]/text(),其匹配結(jié)果為:JavaScriptHTML & CSS服務(wù)器開(kāi)發(fā)工具其他后端語(yǔ)言基礎(chǔ)應(yīng)用框架應(yīng)用基礎(chǔ)應(yīng)用Python Web 開(kāi)發(fā)MySQL接下來(lái)看一看元素的結(jié)構(gòu):javascript 專欄的節(jié)點(diǎn)結(jié)構(gòu)可以看到 javascript 專欄標(biāo)題是 h2 節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)同級(jí)下有一個(gè) div,它下面的四個(gè) div 節(jié)點(diǎn)正是那四個(gè)專欄。我們首先匹配下這四個(gè)專欄元素:>>> exp1 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]'>>> test_xpath_expression(exp1)xpath表達(dá)式為://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"],其匹配結(jié)果為:<Element div at 0x7f7015bf8808><Element div at 0x7f700c656788><Element div at 0x7f700c6567c8><Element div at 0x7f700c656808>那么我們來(lái)進(jìn)一步分析每個(gè) div 內(nèi)部如何得到教程標(biāo)題、總節(jié)數(shù)以及訪問(wèn)次數(shù)這些數(shù)據(jù):獲取教程數(shù)據(jù)可以看到,在前面找到 div 節(jié)點(diǎn)的基礎(chǔ)上在往下兩層,找到 class 屬性值為 text 的 div 節(jié)點(diǎn),所有的數(shù)據(jù)都在這個(gè)節(jié)點(diǎn)中:標(biāo)題:上面找到的 div 節(jié)點(diǎn)下的第一個(gè) a 節(jié)點(diǎn)的文本值;教程總節(jié)數(shù):上面找到的 div 節(jié)點(diǎn)下的第一個(gè) p 節(jié)點(diǎn)下第一個(gè) span 元素的文本值;總訪問(wèn)次數(shù):上面找到的 div 節(jié)點(diǎn)下的第一個(gè) p 節(jié)點(diǎn)下第二個(gè) span 元素的文本值;這樣我們就能進(jìn)行寫(xiě)出提取相應(yīng)數(shù)據(jù)的 Xpath 路徑表達(dá)式了,測(cè)試如下:>>> exp1 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/a[1]/text()'>>> test_xpath_expression(exp1)xpath表達(dá)式為://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/a[1]/text(),其匹配結(jié)果為:Javascript 入門(mén)教程TypeScript 入門(mén)教程Vue 入門(mén)教程Ajax 入門(mén)教程>>> exp2 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[1]/text()'>>> test_xpath_expression(exp2)xpath表達(dá)式為://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[1]/text(),其匹配結(jié)果為:56小節(jié)38小節(jié)39小節(jié)9小節(jié)>>> exp3 = '//h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[2]/text()'>>> test_xpath_expression(exp3)xpath表達(dá)式為://h2[contains(text(), "JavaScript")]/following-sibling::div/div[@class="course-card"]/child::div/div[@class="text"]/p/span[2]/text(),其匹配結(jié)果為:9832354736281800接下來(lái)我們整理下 Python 代碼,將整個(gè) wiki 頁(yè)面上的教程都解析出來(lái),并將數(shù)據(jù)整理成 json 格式。預(yù)期最后的結(jié)果應(yīng)該是這樣的:{ '前端開(kāi)發(fā)': { 'JavaScript': [ {'title': 'JavaScript入門(mén)教程', 'total_chapters': 56, 'total_visited': 9001}, {...}, {...}, {...} ], 'HTML & CSS': [ ... ] } '服務(wù)端相關(guān)': { }, ...}這樣的難度再次增加,其核心的獲取數(shù)據(jù)的過(guò)程和上面一致。后面獲取其他數(shù)據(jù)的結(jié)果過(guò)程不作分析,大家有興趣仔細(xì)研究下代碼,然后動(dòng)手實(shí)操。話不多說(shuō),上代碼:# 代碼文件:test_xpath2.pyfrom lxml import etreedef get_direction_data(direction_tree): """ 獲取一個(gè)方向下的課程數(shù)據(jù) :return: """ direction_data = {} cards = direction_tree.xpath('.//div[@class="language-card"]') for card in cards: title = card.xpath('.//h2[@class="language-title"]/text()')[0] course_list = card.xpath('.//div[@class="course-card"]') courses = [] for course in course_list: course_title = course.xpath('.//div[@class="text"]/a[1]/text()')[0] course_total_chaps = course.xpath('.//div[@class="text"]/p/span[1]/text()')[0] course_total_visit_count = course.xpath('.//div[@class="text"]/p/span[2]/text()')[0] courses.append({ 'course_title': course_title.strip(), 'course_total_chaps': course_total_chaps.strip(), 'course_total_visit_count': int(course_total_visit_count.strip()) }) direction_data[title] = courses return direction_datadef get_all_data(): """ 解析慕課網(wǎng)wiki數(shù)據(jù) :return: """ result = {} html = etree.parse('test.html', etree.HTMLParser(encoding='utf8')) directions = html.xpath('//div[@class="direction-con"]') for direction in directions: # 提取方向key,注意一定要有點(diǎn)號(hào),表示從當(dāng)前元素開(kāi)始提取 direction_name = direction.xpath('./div[@class="title-con"][1]/text()') if direction_name: result[direction_name[0]] = get_direction_data(direction) return result運(yùn)行的結(jié)果如下:[store@server2 ~]$ python3Python 3.6.8 (default, Apr 2 2020, 13:34:55) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.>>> from test_xpath2 import get_all_dat>>> get_all_data(){'前端開(kāi)發(fā)': {'JavaScript': [{'course_title': 'Javascript 入門(mén)教程', 'course_total_chaps': '56小節(jié)', 'course_total_visit_count': 9832}, {'course_title': 'TypeScript 入門(mén)教程', 'course_total_chaps': '38小節(jié)', 'course_total_visit_count': 3547}, {'course_title': 'Vue 入門(mén)教程', 'course_total_chaps': '39小節(jié)', 'course_total_visit_count': 3628}, {'course_title': 'Ajax 入門(mén)教程', 'course_total_chaps': '9小節(jié)', 'course_total_visit_count': 1800}], 'HTML & CSS': [{'course_title': 'CSS3 入門(mén)教程', 'course_total_chaps': '32小節(jié)', 'course_total_visit_count': 1512}, {'course_title': 'Less 入門(mén)教程', 'course_total_chaps': '22小節(jié)', 'course_total_visit_count': 364}, {'course_title': '雪碧圖入門(mén)教程', 'course_total_chaps': '24小節(jié)', 'course_total_visit_count': 915}]}, '服務(wù)端相關(guān)': {'服務(wù)器': [{'course_title': 'Nginx 入門(mén)教程', 'course_total_chaps': '24小節(jié)', 'course_total_visit_count': 4500}, {'course_title': 'HTTP 入門(mén)教程', 'course_total_chaps': '16小節(jié)', 'course_total_visit_count': 456}, {'course_title': 'Docker 入門(mén)教程', 'course_total_chaps': '25小節(jié)', 'course_total_visit_count': 1067}, {'course_title': 'Shell 入門(mén)教程', 'course_total_chaps': '17小節(jié)', 'course_total_visit_count': 2060}, {'course_title': 'Linux 入門(mén)教程', 'course_total_chaps': '25小節(jié)', 'course_total_visit_count': 1430}], '開(kāi)發(fā)工具': [{'course_title': 'Gradle 入門(mén)教程', 'course_total_chaps': '12小節(jié)', 'course_total_visit_count': 1121}, {'course_title': 'Vim 入門(mén)教程', 'course_total_chaps': '14小節(jié)', 'course_total_visit_count': 1491}, {'course_title': 'RESTful 規(guī)范教程', 'course_total_chaps': '13小節(jié)', 'course_total_visit_count': 1316}, {'course_title': 'Markdown 入門(mén)教程', 'course_total_chaps': '31小節(jié)', 'course_total_visit_count': 733}, {'course_title': 'Maven 入門(mén)教程', 'course_total_chaps': '17小節(jié)', 'course_total_visit_count': 155}, {'course_title': 'GitHub 入門(mén)教程', 'course_total_chaps': '9小節(jié)', 'course_total_visit_count': 261}], '其他后端語(yǔ)言': [{'course_title': 'C 語(yǔ)言入門(mén)教程', 'course_total_chaps': '45小節(jié)', 'course_total_visit_count': 1933}, {'course_title': 'Go 入門(mén)教程', 'course_total_chaps': '36小節(jié)', 'course_total_visit_count': 691}, {'course_title': 'Ruby 入門(mén)教程', 'course_total_chaps': '26小節(jié)', 'course_total_visit_count': 410}]}, 'Java': {'基礎(chǔ)應(yīng)用': [{'course_title': 'Java 入門(mén)教程', 'course_total_chaps': '39小節(jié)', 'course_total_visit_count': 5229}, {'course_title': 'Android 入門(mén)教程', 'course_total_chaps': '29小節(jié)', 'course_total_visit_count': 553}, {'course_title': '算法入門(mén)教程', 'course_total_chaps': '11小節(jié)', 'course_total_visit_count': 628}], '框架應(yīng)用': [{'course_title': 'Spring Boot 入門(mén)教程', 'course_total_chaps': '25小節(jié)', 'course_total_visit_count': 4861}, {'course_title': 'Spring 入門(mén)教程', 'course_total_chaps': '21小節(jié)', 'course_total_visit_count': 850}, {'course_title': 'Hibernate 入門(mén)教程', 'course_total_chaps': '23小節(jié)', 'course_total_visit_count': 619}, {'course_title': 'MyBatis 入門(mén)教程', 'course_total_chaps': '23小節(jié)', 'course_total_visit_count': 895}]}, 'Python': {'基礎(chǔ)應(yīng)用': [{'course_title': 'Python 入門(mén)語(yǔ)法教程', 'course_total_chaps': '24小節(jié)', 'course_total_visit_count': 3617}, {'course_title': 'Python 原生爬蟲(chóng)教程', 'course_total_chaps': '19小節(jié)', 'course_total_visit_count': 2001}, {'course_title': 'Python 進(jìn)階應(yīng)用教程', 'course_total_chaps': '29小節(jié)', 'course_total_visit_count': 726}], 'Python Web 開(kāi)發(fā)': [{'course_title': 'Django 入門(mén)教程', 'course_total_chaps': '33小節(jié)', 'course_total_visit_count': 668}, {'course_title': 'NumPy 入門(mén)教程', 'course_total_chaps': '21小節(jié)', 'course_total_visit_count': 152}]}, '數(shù)據(jù)庫(kù)': {'MySQL': [{'course_title': 'MySQL 入門(mén)教程', 'course_total_chaps': '32小節(jié)', 'course_total_visit_count': 3638}, {'course_title': 'SQL 入門(mén)教程', 'course_total_chaps': '47小節(jié)', 'course_total_visit_count': 2406}]}}是不是實(shí)現(xiàn)了預(yù)期效果?爬取網(wǎng)頁(yè),解析數(shù)據(jù)的過(guò)程和這個(gè)類似。掌握好今天的內(nèi)容,你就已經(jīng)掌握了爬蟲(chóng)的一個(gè)核心步驟。

2. Scrapy Shell 實(shí)戰(zhàn)

上面介紹了一些 Scrapy Shell 和 Response 的基礎(chǔ)知識(shí),我們現(xiàn)在就來(lái)在 Scrapy Shell 中實(shí)戰(zhàn) Selector 選擇器。本次測(cè)試的網(wǎng)站為廣州鏈家,測(cè)試頁(yè)面為二手房頁(yè)面:鏈家二手房網(wǎng)站我已經(jīng)在上面標(biāo)出了想要爬取的網(wǎng)頁(yè)信息,后面也主要測(cè)試這些數(shù)據(jù)的 xpath 表達(dá)式 或者 css 表達(dá)式。首先使用 scrapy shell 目標(biāo)網(wǎng)址 命令進(jìn)行想要的命令行,此時(shí) Scrapy 框架已經(jīng)為我們將目標(biāo)網(wǎng)站的網(wǎng)頁(yè)數(shù)據(jù)爬取了下來(lái):(scrapy-test) [root@server ~]# scrapy shell https://gz.lianjia.com/ershoufang/...>>> response<200 https://gz.lianjia.com/ershoufang/>我們看到響應(yīng)的網(wǎng)頁(yè)數(shù)據(jù)已經(jīng)有了,接下來(lái)我們就可以開(kāi)始進(jìn)行網(wǎng)頁(yè)分析來(lái)抓取圖片中標(biāo)記的數(shù)據(jù)了。首先是標(biāo)題信息:提取二手房數(shù)據(jù)的標(biāo)題信息根據(jù)上面的網(wǎng)頁(yè)結(jié)構(gòu),可以很快得到標(biāo)題的 xpath 路徑表達(dá)式:標(biāo)題://ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()在 Scrapy Shell 中我們實(shí)戰(zhàn)一把:>>> response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()')[<Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='地鐵口 總價(jià)低 精裝實(shí)用小兩房'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='剛需精裝小三房/三房?jī)蓮d一廚一衛(wèi)/廣州東綠湖國(guó)際城'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='周門(mén)社區(qū) 綠雅苑六樓 精裝三房'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='金碧領(lǐng)秀國(guó)際 精裝修一房 中樓層采光好'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='戶型方正 采光好 通風(fēng)透氣 小區(qū)安靜'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='毛紡小區(qū) 南向兩房 方正實(shí)用 采光好'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='南奧疊層復(fù)式 前后樓距開(kāi)闊 南北對(duì)流通風(fēng)好'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='丹桂園 實(shí)用三房精裝修 南向戶型 擰包入住'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='周門(mén)小區(qū)大院管理 近地鐵總價(jià)低全明正規(guī)一房一廳'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='云鶴北街 精裝低樓層 南向兩房'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='中海譽(yù)城東南向兩房,住家安靜,無(wú)抵押交易快'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='精裝兩房,步梯中層,總價(jià)低,交通方便,配套齊全'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='中層南向四房 格局方正 樓層適中'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='精裝修 戶型好 中空一房 采光保養(yǎng)很好'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='業(yè)主急售,價(jià)格優(yōu)質(zhì)看房方便有密碼'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='匯僑新城北區(qū) 精裝三房 看花園 戶型靚'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='小區(qū)中間,安靜,前無(wú)遮擋,視野寬闊,望別墅花園'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='小區(qū)側(cè)邊位 通風(fēng)采光好 小區(qū)管理 裝修保養(yǎng)好'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='萬(wàn)科三房 南北對(duì)流 中高層采光好'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='南北對(duì)流,采光充足,配套設(shè)施完善,交通便利'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='美力倚睛居3房南有精裝修拎包入住'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='美心翠擁華庭二期 3室2廳 228萬(wàn)'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='天河公園門(mén)口 交通便利 配套成熟'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='保利香檳花園 高層視野好 保養(yǎng)很好 居家感好'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='恒安大廈兩房,有公交,交通方便,價(jià)格方面可談'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='此房是商品房,低層,南北對(duì)流,全明屋'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='水蔭直街 原裝電梯戶型 方正三房格局'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='嘉誠(chéng)國(guó)際公寓 可明火 正規(guī)一房一廳'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='近地鐵兩房 均價(jià)低 業(yè)主自住 裝修保養(yǎng)好'>, <Selector xpath='//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()' data='宏城匯 三房 南向 采光好 戶型方正 交通便利'>]上面結(jié)果返回的是 SelectorList 類型:>>> data_list = response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()')>>> type(data_list)<class 'scrapy.selector.unified.SelectorList'>最后我們通過(guò)提取 Selector 的 root 屬性,只得到相應(yīng)的文本信息:>>> data = [d.root for d in data_list]>>> data['地鐵口 總價(jià)低 精裝實(shí)用小兩房', '剛需精裝小三房/三房?jī)蓮d一廚一衛(wèi)/廣州東綠湖國(guó)際城', '周門(mén)社區(qū) 綠雅苑六樓 精裝三房', '金碧領(lǐng)秀國(guó)際 精裝修一房 中樓層采光好', '戶型方正 采光好 通風(fēng)透氣 小區(qū)安靜', '毛紡小區(qū) 南向兩房 方正實(shí)用 采光好', '南奧疊層復(fù)式 前后樓距開(kāi)闊 南北對(duì)流通風(fēng)好', '丹桂園 實(shí)用三房精裝修 南向戶型 擰包入住', '周門(mén)小區(qū)大院管理 近地鐵總價(jià)低全明正規(guī)一房一廳', '云鶴北街 精裝低樓層 南向兩房', '中海譽(yù)城東南向兩房,住家安靜,無(wú)抵押交易快', '精裝兩房,步梯中層,總價(jià)低,交通方便,配套齊全', '中層南向四房 格局方正 樓層適中', '精裝修 戶型好 中空一房 采光保養(yǎng)很好', '業(yè)主急售,價(jià)格優(yōu)質(zhì)看房方便有密碼', '匯僑新城北區(qū) 精裝三房 看花園 戶型靚', '小區(qū)中間,安靜,前無(wú)遮擋,視野寬闊,望別墅花園', '小區(qū)側(cè)邊位 通風(fēng)采光好 小區(qū)管理 裝修保養(yǎng)好', '萬(wàn)科三房 南北對(duì)流 中高層采光好', '南北對(duì)流,采光充足,配套設(shè)施完善,交通便利', '美力倚睛居3房南有精裝修拎包入住', '美心翠擁華庭二期 3室2廳 228萬(wàn)', '天河公園門(mén)口 交通便利 配套成熟', '保利香檳花園 高層視野好 保養(yǎng)很好 居家感好', '恒安大廈兩房,有公交,交通方便,價(jià)格方面可談', '此房是商品房,低層,南北對(duì)流,全明屋', '水蔭直街 原裝電梯戶型 方正三房格局', '嘉誠(chéng)國(guó)際公寓 可明火 正規(guī)一房一廳', '近地鐵兩房 均價(jià)低 業(yè)主自住 裝修保養(yǎng)好', '宏城匯 三房 南向 采光好 戶型方正 交通便利']是不是非常簡(jiǎn)單就爬到了數(shù)據(jù)?另外,我們還可以使用 extract()[0] 或者 extract_first() 這樣的方式來(lái)提取結(jié)果列表中的第一個(gè)文本數(shù)據(jù):>>> data = response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()').extract()[0]>>> data2 = response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()').extract_first()>>> data == data2True>>> data'地鐵口 總價(jià)低 精裝實(shí)用小兩房'接下來(lái),我們依次找出獲取二手房位置、房屋價(jià)格、房屋信息的 xpath 路徑表達(dá)式:房屋位置://ul[@class="sellListContent"]/li/div/div[@class="food"]/div[@class="positionInfo"]/a[]/text()房屋信息://ul[@class="sellListContent"]/li/div/div[@class="address"]/div[@class="houseInfo"]/text()房屋價(jià)格://ul[@class="sellListContent"]/li/div/div[@class="priceInfo"]/div[@class="totalPrice"]/span/text()有了這些之后,我們就可以依次提取出二手房的【標(biāo)題介紹】、【房屋位置】、【房屋信息】以及【房屋價(jià)格】這些信息。此外對(duì)于提取的【房屋信息】字段還要進(jìn)一步處理,分割成【房屋結(jié)構(gòu)】、【房屋大小】以及【朝向】等信息。這些信息將在 Spider 模塊中進(jìn)行提取,也就是我們前面互動(dòng)出版網(wǎng)爬蟲(chóng)的 ChinaPubCrawler.py 文件中的 ChinaPubCrawler 類來(lái)解析。最后我們?cè)诮榻B下 scrapy shell 命令的參數(shù):(scrapy-test) [root@server ~]# scrapy shell --helpUsage===== scrapy shell [url|file]Interactive console for scraping the given url or file. Use ./file.html syntaxor full path for local file.Options=======--help, -h show this help message and exit-c CODE evaluate the code in the shell, print the result and exit--spider=SPIDER use this spider--no-redirect do not handle HTTP 3xx status codes and print response as-isGlobal Options----------------logfile=FILE log file. if omitted stderr will be used--loglevel=LEVEL, -L LEVEL log level (default: DEBUG)--nolog disable logging completely--profile=FILE write python cProfile stats to FILE--pidfile=FILE write process ID to FILE--set=NAME=VALUE, -s NAME=VALUE set/override setting (may be repeated)--pdb enable pdb on failure比較常用的有 --no-redirect 和 -s 選項(xiàng):--no-redirect : 指的是不處理重定向,直接按照原始響應(yīng)返回即可;-s:替換 settings.py 中的配置。常用的有設(shè)置 USER_AGENT 等。

2. 深入理解 Django 類視圖

這里在介紹完類視圖的基本使用后,我們來(lái)深入學(xué)習(xí)下 Django 的源代碼,看看 Django 是如何將對(duì)應(yīng)的 HTTP 請(qǐng)求映射到對(duì)應(yīng)的函數(shù)上。這里我們使用的是 Django 2.2.10 的源代碼進(jìn)行說(shuō)明。我們使用 VSCode 打開(kāi) Django 源碼,定位到 django/views/generic 目錄下,這里是和視圖相關(guān)的源代碼。首先看 __init__.py 文件,內(nèi)容非常少,主要是將該目錄下的常用視圖類導(dǎo)入到這里,簡(jiǎn)化開(kāi)發(fā)者導(dǎo)入這些常用的類。其中最重要的當(dāng)屬 base.py 文件中定義的 view 類,它是其他所有視圖類的基類。# base.py中常用的三個(gè)view類from django.views.generic.base import RedirectView, TemplateView, View# dates.py中定義了許多和時(shí)間相關(guān)的視圖類from django.views.generic.dates import ( ArchiveIndexView, DateDetailView, DayArchiveView, MonthArchiveView, TodayArchiveView, WeekArchiveView, YearArchiveView,)# 導(dǎo)入DetailView類from django.views.generic.detail import DetailView# 導(dǎo)入增刪改相關(guān)的視圖類from django.views.generic.edit import ( CreateView, DeleteView, FormView, UpdateView,)# 導(dǎo)入list.py中定義的顯示列表的視圖類from django.views.generic.list import ListView__all__ = [ 'View', 'TemplateView', 'RedirectView', 'ArchiveIndexView', 'YearArchiveView', 'MonthArchiveView', 'WeekArchiveView', 'DayArchiveView', 'TodayArchiveView', 'DateDetailView', 'DetailView', 'FormView', 'CreateView', 'UpdateView', 'DeleteView', 'ListView', 'GenericViewError',]# 定義一個(gè)通用的視圖異常類class GenericViewError(Exception): """A problem in a generic view.""" pass接下來(lái),我們查看 base.py 文件,重點(diǎn)分析模塊中定義的 View 類:# 源碼路徑 django/views/generic/base.py# 忽略導(dǎo)入# ...class View: http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] def __init__(self, **kwargs): # 忽略 # ... @classonlymethod def as_view(cls, **initkwargs): """Main entry point for a request-response process.""" for key in initkwargs: if key in cls.http_method_names: raise TypeError("You tried to pass in the %s method name as a " "keyword argument to %s(). Don't do that." % (key, cls.__name__)) if not hasattr(cls, key): raise TypeError("%s() received an invalid keyword %r. as_view " "only accepts arguments that are already " "attributes of the class." % (cls.__name__, key)) def view(request, *args, **kwargs): self = cls(**initkwargs) if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.setup(request, *args, **kwargs) if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) return self.dispatch(request, *args, **kwargs) view.view_class = cls view.view_initkwargs = initkwargs # take name and docstring from class update_wrapper(view, cls, updated=()) # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) return view # ... def dispatch(self, request, *args, **kwargs): # Try to dispatch to the right method; if a method doesn't exist, # defer to the error handler. Also defer to the error handler if the # request method isn't on the approved list. if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs) def http_method_not_allowed(self, request, *args, **kwargs): logger.warning( 'Method Not Allowed (%s): %s', request.method, request.path, extra={'status_code': 405, 'request': request} ) return HttpResponseNotAllowed(self._allowed_methods()) # 忽略其他函數(shù) # ...# ...我們來(lái)仔細(xì)分析 view 類中的這部分代碼。view 類首先定義了一個(gè)屬性 http_method_names,表示其支持的 HTTP 請(qǐng)求方法。接下來(lái)最重要的是 as_view() 方法和 dispatch() 方法。在上面使用視圖類的示例中,我們定義的 URLConf 如下:# first_django_app/hello_app/urls.pyfrom . import viewsurlpatterns = [ # 類視圖 url(r'test-cbv/', views.TestView.as_view(), name='test-cbv'),]這里結(jié)合源碼可以看到,views.TestView.as_view() 返回的結(jié)果同樣是一個(gè)函數(shù):view(),它的定義和前面的視圖函數(shù)一樣。as_view() 函數(shù)可以接收一些參數(shù),函數(shù)調(diào)用會(huì)先對(duì)接收的參數(shù)進(jìn)行檢查:for key in initkwargs: if key in cls.http_method_names: raise TypeError("You tried to pass in the %s method name as a " "keyword argument to %s(). Don't do that." % (key, cls.__name__)) if not hasattr(cls, key): raise TypeError("%s() received an invalid keyword %r. as_view " "only accepts arguments that are already " "attributes of the class." % (cls.__name__, key))上面的代碼會(huì)對(duì) as_view() 函數(shù)傳遞的參數(shù)做兩方面檢查:首先確保傳入的參數(shù)不能有 get、post 這樣的 key 值,否則會(huì)覆蓋 view 類中的對(duì)應(yīng)方法,這樣對(duì)應(yīng)的請(qǐng)求就無(wú)法正確找到函數(shù)進(jìn)行處理。覆蓋的代碼邏輯如下:class View: # ... def __init__(self, **kwargs): # 這里會(huì)將所有的傳入的參數(shù)通過(guò)setattr()方法給屬性類賦值 for key, value in kwargs.items(): setattr(self, key, value) # ... @classonlymethod def as_view(cls, **initkwargs): # ... def view(request, *args, **kwargs): # 調(diào)用視圖函數(shù)時(shí),會(huì)將這些參數(shù)傳給View類來(lái)實(shí)例化 self = cls(**initkwargs) # ... # ... # ...此外,不可以傳遞類中不存在的屬性值。假設(shè)我們將上面的 URLConf 進(jìn)行略微修改,如下:from . import viewsurlpatterns = [ # 類視圖 url(r'test-cbv/', views.TestView.as_view(no_key='hello'), name='test-cbv'),]啟動(dòng)后,可以發(fā)現(xiàn) Django 報(bào)錯(cuò)如下,這正是由本處代碼拋出的異常。接下來(lái)看下 update_wrapper() 方法,這個(gè)只是 python 內(nèi)置模塊中的一個(gè)方法,只是比較少用,所以會(huì)讓很多人感到陌生。先看它的作用:update_wrapper() 這個(gè)函數(shù)的主要功能是負(fù)責(zé)復(fù)制原函數(shù)的一些屬性,如 moudle、name、doc 等。如果不加 update_wrapper(), 那么被裝飾器修飾的函數(shù)就會(huì)丟失其上面的一些屬性信息。具體看一個(gè)測(cè)試代碼示例:from functools import update_wrapperdef test_wrapper(f): def wrapper_function(*args, **kwargs): """裝飾函數(shù),不保留原信息""" return f(*args, **kwargs) return wrapper_functiondef test_update_wrapper(f): def wrapper_function(*args, **kwargs): """裝飾函數(shù),使用update_wrapper()方法保留原信息""" return f(*args, **kwargs) update_wrapper(wrapper_function, f) return wrapper_function@test_wrapperdef test_wrapped(): """被裝飾的函數(shù)""" pass@test_update_wrapperdef test_update_wrapped(): """被裝飾的函數(shù),使用了update_wrapper()方法""" passprint('不使用update_wrapper()方法:')print(test_wrapped.__doc__) print(test_wrapped.__name__) print()print('使用update_wrapper()方法:')print(test_update_wrapped.__doc__) print(test_update_wrapped.__name__) 執(zhí)行結(jié)果如下:不使用update_wrapper()方法:裝飾函數(shù),不保留原信息wrapper_function使用update_wrapper()方法:被裝飾的函數(shù),使用了update_wrapper()方法test_update_wrapped可以看到,不使用 update_wrapper() 方法的話,函數(shù)在使用裝飾器后,它的一些基本屬性比如 __name__ 等都是正真執(zhí)行函數(shù)(比如上面的 wrapper_function() 函數(shù))的屬性。不過(guò)這個(gè)函數(shù)在分析視圖函數(shù)的處理流程上并不重要。接下來(lái)看 as_view 中定義的 view() 方法,它是真正執(zhí)行 HTTP 請(qǐng)求的視圖函數(shù):def view(request, *args, **kwargs): self = cls(**initkwargs) # 如果有g(shù)et方法而沒(méi)有head方法,對(duì)于head請(qǐng)求則直接使用get()方法進(jìn)行處理 if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get # 將Django對(duì)應(yīng)傳過(guò)來(lái)的請(qǐng)求實(shí)例以及相應(yīng)參數(shù)賦給實(shí)例屬性 self.setup(request, *args, **kwargs) # 如果沒(méi)有request屬性,表明可能重寫(xiě)了setup()方法,而且setup()里面忘記了調(diào)用super() if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) # 調(diào)用dispatch()方法 return self.dispatch(request, *args, **kwargs)view() 方法里面會(huì)調(diào)用 setup() 方法將 Django 給視圖函數(shù)傳遞的參數(shù)賦給實(shí)例變量,然后會(huì)調(diào)用 dispatch()方法去處理請(qǐng)求。兩個(gè)函數(shù)的代碼如下:def setup(self, request, *args, **kwargs): """Initialize attributes shared by all view methods.""" self.request = request self.args = args self.kwargs = kwargsdef dispatch(self, request, *args, **kwargs): # Try to dispatch to the right method; if a method doesn't exist, # defer to the error handler. Also defer to the error handler if the # request method isn't on the approved list. if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs)這里最核心的就是這個(gè) dispatch() 方法了。首先該方法通過(guò) request.method.lower() 這個(gè)可以拿到 http 的請(qǐng)求方式,比如 get、post、put 等,然后判斷是不是在預(yù)先定義好的請(qǐng)求方式的列表中。如果滿足,那么最核心的代碼來(lái)了:handler = getattr(self, request.method.lower(), self.http_method_not_allowed)假設(shè)客戶端發(fā)的是 get 請(qǐng)求,那么 request.method.lower() 就是 “get” ,接下來(lái)執(zhí)行上面的代碼,就會(huì)得到我們定義的視圖類中定義的 get 函數(shù),最后返回的是這個(gè)函數(shù)的處理結(jié)果。這就是為啥 get 請(qǐng)求能對(duì)應(yīng)到視圖函數(shù)中g(shù)et() 方法的原因。其他的請(qǐng)求也是類似的,如果是不支持的請(qǐng)求,則會(huì)執(zhí)行 http_method_not_allowed() 方法。return handler(request, *args, **kwargs)如果對(duì)這部分代碼的執(zhí)行流程還有疑問(wèn)的,我們可以在 Django 的源碼中添加幾個(gè) print() 函數(shù),然后通過(guò)實(shí)際請(qǐng)求來(lái)看看執(zhí)行過(guò)程:[root@server first_django_app]# cat ~/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/views/generic/base.py class View: ... @classonlymethod def as_view(cls, **initkwargs): ... def view(request, *args, **kwargs): print('調(diào)用view函數(shù)處理請(qǐng)求') ... ... def dispatch(self, request, *args, **kwargs): # Try to dispatch to the right method; if a method doesn't exist, # defer to the error handler. Also defer to the error handler if the # request method isn't on the approved list. print('調(diào)用dispatch()方法處理http請(qǐng)求,請(qǐng)求方式:{}'.format(request.method.lower())) if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) print('得到的handler:{}'.format(handler)) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs)接下來(lái)我們還是使用前面定義的視圖類 TestView 來(lái)進(jìn)行操作,操作過(guò)程以及實(shí)驗(yàn)結(jié)果如下:# 一個(gè)窗口啟動(dòng) django 工程(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888Watching for file changes with StatReloaderPerforming system checks...System check identified no issues (0 silenced).April 15, 2020 - 04:30:04Django version 2.2.11, using settings 'first_django_app.settings'Starting development server at http://0.0.0.0:8888/Quit the server with CONTROL-C.# 另一個(gè)窗口發(fā)送http請(qǐng)求[root@server django-manual]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/hello, get[root@server django-manual]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/hello, post[root@server django-manual]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/hello, put[root@server django-manual]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/hello, delete

3. 示例程序

#include <stdio.h>#include <string.h>#define StudentNumbers 50#define NameLength 50typedef struct{ int id; char name[NameLength]; int age; int score; int flag;} Student;int add(Student student, Student Students[]);int del(int id, Student students[]);int display(Student students[]);int update(int id, Student students[]);int search(char name[], Student students[]);int main(){ int id = -1; char name[NameLength]; int choice = 0; int stop = 0; Student students[StudentNumbers]; Student student; for (int i = 0; i < StudentNumbers; i++) { students[i].id = i; students[i].flag = 0; } while (stop == 0) { printf("-------------------------\n"); printf("* 學(xué)生管理系統(tǒng) *\n"); printf("-------------------------\n"); printf("1 添加\n"); printf("2 修改成績(jī)\n"); printf("3 查詢\n"); printf("4 刪除\n"); printf("5 顯示學(xué)生列表\n"); printf("0 退出程序\n"); printf("請(qǐng)直接輸入數(shù)字選項(xiàng):"); scanf("%d", &choice); switch (choice) { case 1: printf("請(qǐng)輸入學(xué)生姓名:"); scanf("%s", student.name); printf("請(qǐng)輸入學(xué)生的年齡:"); scanf("%d", &student.age); printf("請(qǐng)輸入學(xué)生成績(jī):"); scanf("%d", &student.score); add(student, students); break; case 2: printf("請(qǐng)輸入要修改成績(jī)的學(xué)生編號(hào):"); scanf("%d", &id); update(id, students); break; case 3: printf("請(qǐng)輸入要查找的學(xué)生姓名:"); scanf("%s", name); search(name, students); break; case 4: printf("請(qǐng)輸入要?jiǎng)h除的學(xué)生編號(hào):"); scanf("%d", &id); del(id, students); break; case 5: display(students); break; case 0: stop = 1; break; default: printf("輸入選項(xiàng)有誤\n"); break; } } return 0;}int add(Student student, Student students[]){ for (int i = 0; i < StudentNumbers; i++) { if (students[i].flag == 0) { strcpy(students[i].name, student.name); students[i].age = student.age; students[i].score = student.score; students[i].flag = 1; return 0; } } return 1;}int del(int id, Student students[]){ for (int i = 0; i < StudentNumbers; i++) { if (students[i].id == id) { students[i].flag = 0; return 0; } } return 1;}int display(Student students[]){ printf("******************\n"); printf("學(xué)生列表\n"); printf("******************\n"); for (int i = 0; i < StudentNumbers; i++) { if (students[i].flag == 1) { printf("學(xué)生編號(hào):%d,學(xué)生姓名:%s,年齡:%d,成績(jī):%d\n", students[i].id, students[i].name, students[i].age, students[i].score); } } printf("******************\n"); return 0;}int update(int id, Student students[]){ int score = -1; printf("請(qǐng)輸入新的成績(jī):"); scanf("%d", &score); for (int i = 0; i < StudentNumbers; i++) { if (students[i].id == id) { students[i].score = score; return 0; } } return 1;}int search(char name[], Student students[]){ for (int i = 0; i < StudentNumbers; i++) { if (strcmp(name, students[i].name) == 0) { printf("學(xué)生編號(hào): %d,學(xué)生姓名: %s,年齡: %d,成績(jī): %d\n", students[i].id, students[i].name, students[i].age, students[i].score); return 0; } } printf("沒(méi)有查找到相關(guān)學(xué)生信息。\n"); return 1;}很多人可能會(huì)第一次接觸這么長(zhǎng)的程序,會(huì)產(chǎn)生畏懼的心理。其實(shí)不用擔(dān)心。要相信自己可以看懂的。我們分開(kāi)來(lái)講解一下。在程序的最開(kāi)始我們需要引入程序中可能需要使用的函數(shù)的頭文件。這里我們因?yàn)橐褂?printf 、 scanf 等,所以需要 stdio 函數(shù)庫(kù)。因?yàn)橐褂?strcpy 、 strcmp 函數(shù),所以需要 string 函數(shù)庫(kù)。#include <stdio.h>#include <string.h>為了便于程序中的維護(hù),不用在很多出修改共用的數(shù)值。所以這里定義了一個(gè)常量#define StudentNumbers 50#define NameLength 50為了存儲(chǔ)學(xué)生的信息。我們用了 struct 來(lái)定義學(xué)生的信息。里面包含學(xué)生的編號(hào) id ,姓名 name 這是一個(gè)字符串,年齡 age ,成績(jī) score ,標(biāo)志位 flag 這個(gè)變量是用來(lái)表示是否有學(xué)生信息存儲(chǔ)在該位置的。不過(guò)這里我們使用了之前沒(méi)有介紹的一個(gè) typedef 。這個(gè)關(guān)鍵字使用的好處是使得后面使用這個(gè) struct 的時(shí)候不用每次都用關(guān)鍵字 struct 來(lái)定義,只要用這個(gè)結(jié)構(gòu)的名稱直接定義就可以了,如同我們定義整數(shù)等內(nèi)置類型一樣方便。typedef struct{ int id; char name[NameLength]; int age; int score; int flag;} Student;為了便于維護(hù),我們沒(méi)有按照函數(shù)出現(xiàn)的順序來(lái)寫(xiě)。不過(guò) C 語(yǔ)言一直秉承著先定義再使用的原則。所以。如果你使用的函數(shù)沒(méi)有在使用前出現(xiàn),而是在后面的話,那么你就需要先讓編譯器知道這個(gè)函數(shù)的基本情況。這個(gè)時(shí)候我們會(huì)先把函數(shù)的定義寫(xiě)在前面。我們可以看到下面我們定義了這個(gè)系統(tǒng)的功能。每個(gè)功能我們都會(huì)寫(xiě)一個(gè)函數(shù)。其實(shí)不寫(xiě)這些函數(shù),把所有的功能寫(xiě)在 main 函數(shù)內(nèi)部也是可以的。但是這樣會(huì)在維護(hù)上存在問(wèn)題。進(jìn)行測(cè)試也會(huì)變得困難。int add(Student student, Student Students[]);int del(int id, Student students[]);int display(Student students[]);int update(int id, Student students[]);int search(char name[], Student students[]);這里定義了一些需要使用的變量。stop 變量是用來(lái)控制程序循環(huán)的,也就是控制程序在什么時(shí)候可以結(jié)束循環(huán)的。我們定義了一個(gè) Student 的數(shù)組,用來(lái)存儲(chǔ)學(xué)生的信息。用一個(gè)單獨(dú)的變量來(lái)存儲(chǔ)單條的學(xué)生信息。int id = -1;char name[NameLength];int choice = 0;int stop = 0;Student students[StudentNumbers];Student student;這里我們通過(guò)循環(huán)來(lái)初始化我們的數(shù)組。for (int i = 0; i < StudentNumbers; i++){ students[i].id = i; students[i].flag = 0;}循環(huán)語(yǔ)句如果在不改變條件的情況下會(huì)一直循環(huán)。確保我們的系統(tǒng)可以一直運(yùn)行。while (stop == 0)在接收到輸入后。我們就會(huì)通過(guò) switch 來(lái)進(jìn)行相應(yīng)的匹配。完成對(duì)應(yīng)的操作。這比使用大量的 if 語(yǔ)句簡(jiǎn)約了很多。switch (choice)在子程序中,也就是實(shí)現(xiàn)增、刪、改、查這些功能程序中。我們用了循環(huán)語(yǔ)句來(lái)訪問(wèn)數(shù)組中的元素。同時(shí),利用了判斷語(yǔ)句與特定的變量,來(lái)判斷該位置是否存有學(xué)生信息。運(yùn)行結(jié)果:utopia@DESKTOP:~$ ./test-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):1請(qǐng)輸入學(xué)生姓名:張三請(qǐng)輸入學(xué)生的年齡:22請(qǐng)輸入學(xué)生成績(jī):100-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):1請(qǐng)輸入學(xué)生姓名:李四請(qǐng)輸入學(xué)生的年齡:21請(qǐng)輸入學(xué)生成績(jī):90-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):1請(qǐng)輸入學(xué)生姓名:王二請(qǐng)輸入學(xué)生的年齡:23請(qǐng)輸入學(xué)生成績(jī):99-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):5******************學(xué)生列表******************學(xué)生編號(hào):0,學(xué)生姓名:張三,年齡:22,成績(jī):100學(xué)生編號(hào):1,學(xué)生姓名:李四,年齡:21,成績(jī):90學(xué)生編號(hào):2,學(xué)生姓名:王二,年齡:23,成績(jī):99******************-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):2請(qǐng)輸入要修改成績(jī)的學(xué)生編號(hào):1請(qǐng)輸入新的成績(jī):80-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):5******************學(xué)生列表******************學(xué)生編號(hào):0,學(xué)生姓名:張三,年齡:22,成績(jī):100學(xué)生編號(hào):1,學(xué)生姓名:李四,年齡:21,成績(jī):80學(xué)生編號(hào):2,學(xué)生姓名:王二,年齡:23,成績(jī):99******************-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):4請(qǐng)輸入要?jiǎng)h除的學(xué)生編號(hào):1-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):5******************學(xué)生列表******************學(xué)生編號(hào):0,學(xué)生姓名:張三,年齡:22,成績(jī):100學(xué)生編號(hào):2,學(xué)生姓名:王二,年齡:23,成績(jī):99******************-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):1請(qǐng)輸入學(xué)生姓名:張五請(qǐng)輸入學(xué)生的年齡:20請(qǐng)輸入學(xué)生成績(jī):70-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):5******************學(xué)生列表******************學(xué)生編號(hào):0,學(xué)生姓名:張三,年齡:22,成績(jī):100學(xué)生編號(hào):1,學(xué)生姓名:張五,年齡:20,成績(jī):70學(xué)生編號(hào):2,學(xué)生姓名:王二,年齡:23,成績(jī):99******************-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):3請(qǐng)輸入要查找的學(xué)生姓名:張五學(xué)生編號(hào): 1,學(xué)生姓名: 張五,年齡: 20,成績(jī): 70-------------------------* 學(xué)生管理系統(tǒng) *-------------------------1 添加2 修改成績(jī)3 查詢4 刪除5 顯示學(xué)生列表0 退出程序請(qǐng)直接輸入數(shù)字選項(xiàng):在程序中,我們首先添加了 3 條學(xué)生的記錄。然后我們進(jìn)行了列表顯示。接著,我們嘗試修改了其中一個(gè)學(xué)生成績(jī),并再次查看列表,發(fā)現(xiàn)成績(jī)修改生效了。然后,我們刪除了一個(gè)學(xué)生,列表顯示結(jié)果其已經(jīng)被刪除了。然后我們又嘗試添加了一個(gè)學(xué)生。列表顯示結(jié)果添加成功。最后我們按照姓名查找了一個(gè)學(xué)生。

首頁(yè)上一頁(yè)2223242526下一頁(yè)尾頁(yè)
直播
查看課程詳情
微信客服

購(gòu)課補(bǔ)貼
聯(lián)系客服咨詢優(yōu)惠詳情

幫助反饋 APP下載

慕課網(wǎng)APP
您的移動(dòng)學(xué)習(xí)伙伴

公眾號(hào)

掃描二維碼
關(guān)注慕課網(wǎng)微信公眾號(hào)