manage.py 是一個命令行工具,用于與 Django 進行不同方式的交互腳本,通過 python manage.py help 命令,我們可以看到 manage.py 文件支持的所有操作:(django-manual) [root@server first_django_app]# python manage.py helpType 'manage.py help <subcommand>' for help on a specific subcommand.Available subcommands:[auth] changepassword createsuperuser[contenttypes] remove_stale_contenttypes[django] check compilemessages createcachetable dbshell diffsettings dumpdata flush inspectdb loaddata makemessages makemigrations migrate sendtestemail shell showmigrations sqlflush sqlmigrate sqlsequencereset squashmigrations startapp startproject test testserver[sessions] clearsessions[staticfiles] collectstatic findstatic runserver比較常用的命令有:createsuperuser:創(chuàng)建超級用戶,用來登錄 Django 自帶的管理系統(tǒng);shell:進入shell 模式可以直接對 Django 中的模型進行增刪改查等測試,這個后續(xù)介紹 Django 的 ORM 模型中使用;makemigrations/migrate:用來進行數(shù)據(jù)庫遷移工作的,比如我們修改了數(shù)據(jù)庫的表的字段,然后需要保存修改,直接使用makemigrations 生成遷移文件,然后到了新環(huán)境使用 migrate 命令根據(jù)遷移文件生成相應的數(shù)據(jù)表;showmigrations:查看遷移表中的信息;(django-manual) [root@server first_django_app]# python manage.py showmigrationsadmin [ ] 0001_initial [ ] 0002_logentry_remove_auto_add [ ] 0003_logentry_add_action_flag_choicesauth [ ] 0001_initial [ ] 0002_alter_permission_name_max_length [ ] 0003_alter_user_email_max_length [ ] 0004_alter_user_username_opts [ ] 0005_alter_user_last_login_null [ ] 0006_require_contenttypes_0002 [ ] 0007_alter_validators_add_error_messages [ ] 0008_alter_user_username_max_length [ ] 0009_alter_user_last_name_max_length [ ] 0010_alter_group_name_max_length [ ] 0011_update_proxy_permissionscontenttypes [ ] 0001_initial [ ] 0002_remove_content_type_namesessions [ ] 0001_initialstartproject/startapp:創(chuàng)建項目和應用。這里我們發(fā)現(xiàn)它的作用和前面使用 django-admin startproject/startapp 作用是一樣的;runserver:在調試時會常常使用。例如下面的操作,啟動 Django 內置的 Web 服務器,監(jiān)聽8000端口,等待 HTTP 請求:(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8000Watching 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 14, 2020 - 13:47:04Django version 2.2.11, using settings 'first_django_app.settings'Starting development server at http://0.0.0.0:8000/Quit the server with CONTROL-C.
關于 Nginx 模塊的分類有很多種方式,目前網(wǎng)上博客中寫的較多的是按照功能進行分類,有如下幾大類:event 模塊: 搭建 獨立于操作系統(tǒng)的事件處理機制的框架,以及 提供各種具體事件的處理。代表性的模塊有:ngx_events_module, ngx_event_core_module, ngx_epoll_module;handler 模塊: 主要負責處理客戶端請求并產(chǎn)生待響應的內容,比如 ngx_http_static_module 模塊,負責客戶端的靜態(tài)頁面請求處理并將對應的磁盤 文件準備為響應內容輸出;filter 模塊: 主要 負責處理輸出的內容,包括修改輸出內容。代表性的模塊有: ngx_http_sub_module;upstream 模塊: 該類模塊都是用于實現(xiàn)反向代理功能,將真正的請求轉發(fā)到后端服務器上,并從后端服務器上讀取響應,發(fā)回給客戶端。比如前面介紹到轉發(fā) http、websocket、grpc、rtmp等協(xié)議的模塊都可以劃分為這一類;負載均衡模塊: 負載均衡的模塊,實現(xiàn)相應算法。這類模塊都是用于實現(xiàn) Nginx 的負載均衡功能。extend 模塊: 又稱第三方模塊,非 Nginx 官方提供,由各大企業(yè)的開發(fā)人員結合自身業(yè)務開發(fā)而成。Nginx 提供了非常好的模塊編寫機制,遵循相關的標準可以很快定制出符合我們業(yè)務場景的模塊,而且內部調用 Nginx 內部提供的方法進行處理,使得第三方模塊往往都具備很好的性能對于官方提供的模塊,我們可以直接在官網(wǎng)文檔上學習,學習的方式和學習其他互聯(lián)網(wǎng)組件的方式一致,首先學習如何使用,在用至熟練后可以深入分析其源碼了解功能實現(xiàn)背后的原理。我們以前面介紹到的 Nginx 的限速模塊(limit_req模塊)進行說明。首先是掌握該模塊的用法,在該模塊的官方地址中,有關于該模塊的詳細介紹,包括該模塊提供的所有指令以及所有變量說明。此外,還有比較豐富的指令用例。在多次使用該指令并自認為掌握了該模塊的用法之后,想了解限速背后的原理以及相關算法時,就可以深入到源碼學習了。進入 Nginx 的源碼目錄,使用ls查看源碼文件,限速模塊是在 http 目錄中的。[root@server nginx-1.17.6]# cd src/[root@server src]# lscore event http mail misc os stream[root@server src]# ls http/modules/ngx_http_limit_*.chttp/modules/ngx_http_limit_conn_module.chttp/modules/ngx_http_limit_req_module.c找到 Nginx 模塊對應的代碼文件后,我們就可以閱讀里面的代碼進行學習。往往源碼的閱讀是枯燥無味的,我們可以借助海量的網(wǎng)絡資源輔助我們學習。這里就有一篇文章,作者深入分析了 Nginx 的限流模塊的源碼以及相應限流算法,最后進行了相關的實驗測試。通過這樣一個個模塊深入學習,最后在使用每一個 Nginx 指令時,也會非常熟練,最后成為 Nginx 高手。
這部分內容會有點復雜和枯燥,我會盡量簡化代碼,并使用前面的上傳實驗幫助我們在源碼中打印一些 print語句,輔助我們更好的理解整個上傳過程。思考問題:為什么上傳文件時,我們能通過 request.FILES['file'] 拿到文件?Django 幫我們把文件信息存到這里面,那么它是如何處理上傳的文件的呢?我們現(xiàn)在的目的就是要搞清楚上面的問題,可能里面的代碼會比較復雜,目前我們不深入研究代碼細節(jié),只是搞清楚整個過程以及 Django 幫我們做了哪些工作。首先,我們打印下視圖函數(shù)的 request 參數(shù),發(fā)現(xiàn)它是 django.core.handlers.wsgi.WSGIRequest 的一個實例,這在很早之前也是介紹過的。我們重點看看 WSGIRequest 類中的 FILES 屬性:# 源碼位置:django/core/handlers/wsgi.py# ...class WSGIRequest(HttpRequest): # ... @property def FILES(self): if not hasattr(self, '_files'): self._load_post_and_files() return self._files # ...看到這里,我們就大概知道 FILES 屬性值的來源了,就是通過 self._load_post_and_files() 這個方法設置self._files 值,而這個就是 FILES 的值。接下來就是繼續(xù)深入 self._load_post_and_files() 這個方法,但是我們不追究代碼細節(jié)。# 源碼位置:django/http/request.pyclass HttpRequest: """A basic HTTP request.""" # ... def _load_post_and_files(self): """Populate self._post and self._files if the content-type is a form type""" if self.method != 'POST': self._post, self._files = QueryDict(encoding=self._encoding), MultiValueDict() return if self._read_started and not hasattr(self, '_body'): self._mark_post_parse_error() return if self.content_type == 'multipart/form-data': if hasattr(self, '_body'): # Use already read data data = BytesIO(self._body) else: data = self try: self._post, self._files = self.parse_file_upload(self.META, data) except MultiPartParserError: # An error occurred while parsing POST data. Since when # formatting the error the request handler might access # self.POST, set self._post and self._file to prevent # attempts to parse POST data again. self._mark_post_parse_error() raise elif self.content_type == 'application/x-www-form-urlencoded': self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict() else: self._post, self._files = QueryDict(encoding=self._encoding), MultiValueDict() # ...一般而言,我們使用的是 form 表單提交的上傳,對應的 content-type 大部分時候是 multipart/form-data。所以,獲取 _files 屬性的最重要的代碼就是:self._post, self._files = self.parse_file_upload(self.META, data)咋繼續(xù)追蹤 self.parse_file_upload() 這個方法。class HttpRequest: """A basic HTTP request.""" # ... def _initialize_handlers(self): self._upload_handlers = [uploadhandler.load_handler(handler, self) for handler in settings.FILE_UPLOAD_HANDLERS] @property def upload_handlers(self): if not self._upload_handlers: # If there are no upload handlers defined, initialize them from settings. self._initialize_handlers() return self._upload_handlers @upload_handlers.setter def upload_handlers(self, upload_handlers): if hasattr(self, '_files'): raise AttributeError("You cannot set the upload handlers after the upload has been processed.") self._upload_handlers = upload_handlers def parse_file_upload(self, META, post_data): """Return a tuple of (POST QueryDict, FILES MultiValueDict).""" self.upload_handlers = ImmutableList( self.upload_handlers, warning="You cannot alter upload handlers after the upload has been processed." ) parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding) return parser.parse() # ...這三個涉及的函數(shù)都比較簡單,主要是獲取處理上傳文件的 handlers。settings.FILE_UPLOAD_HANDLERS 這個值是取得 global_settings.py 中設置的,而非項目的 settings.py 文件(該文件默認沒有設置該參數(shù)值)。但是我們可以在 settings.py 文件中設置 FILE_UPLOAD_HANDLERS 的值以覆蓋默認的 handlers。# 源碼位置:django\conf\global_settings.py# ...# List of upload handler classes to be applied in order.FILE_UPLOAD_HANDLERS = [ 'django.core.files.uploadhandler.MemoryFileUploadHandler', 'django.core.files.uploadhandler.TemporaryFileUploadHandler',]# ...最后可以看到 parse_file_upload() 方法的核心語句也只有一句:parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)最后調用 parser.parse() 方法獲得結果。最后要說明的是 parser.parse() 比較復雜,我們簡單看下函數(shù)的大致內容即可,課后在繼續(xù)深究函數(shù)的細節(jié):# 源碼位置:django/http/multipartparser.pyclass MultiPartParser: # ... def parse(self): """ Parse the POST data and break it into a FILES MultiValueDict and a POST MultiValueDict. Return a tuple containing the POST and FILES dictionary, respectively. """ from django.http import QueryDict encoding = self._encoding handlers = self._upload_handlers # HTTP spec says that Content-Length >= 0 is valid # handling content-length == 0 before continuing if self._content_length == 0: return QueryDict(encoding=self._encoding), MultiValueDict() # See if any of the handlers take care of the parsing. # This allows overriding everything if need be. for handler in handlers: result = handler.handle_raw_input( self._input_data, self._meta, self._content_length, self._boundary, encoding, ) # Check to see if it was handled if result is not None: return result[0], result[1] # Create the data structures to be used later. self._post = QueryDict(mutable=True) self._files = MultiValueDict() # Instantiate the parser and stream: stream = LazyStream(ChunkIter(self._input_data, self._chunk_size)) # Whether or not to signal a file-completion at the beginning of the loop. old_field_name = None counters = [0] * len(handlers) # Number of bytes that have been read. num_bytes_read = 0 # To count the number of keys in the request. num_post_keys = 0 # To limit the amount of data read from the request. read_size = None # ... # Signal that the upload has completed. # any() shortcircuits if a handler's upload_complete() returns a value. any(handler.upload_complete() for handler in handlers) self._post._mutable = False return self._post, self._files可以看到,這個函數(shù)最后得到 self._post, self._files, 然后返回該結果。有興趣的話可以自行在這幾個重要的地方加上 print() 方法看看對應的 self._post, self._files 的輸出結果,有助于加深印象。
本次實驗按照如下步驟進行:首先我們安裝 WSGI server,直接使用 pip 安裝即可:pip install uwsgi -i https://pypi.tuna.tsinghua.edu.cn/simple編寫test.py文件:def application(env, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return [b'hello world\n',]啟動 WSGI server并監(jiān)聽7000端口。# 指定socket連接,監(jiān)聽端口,應用代碼文件以及進程數(shù)$ uwsgi --socket :7000 --wsgi-file test.py --master --processes 4在nginx.conf中添加如下 server 指令塊:server { listen 7001; default_type text/plain; access_log logs/uwsgi.log; location / { include uwsgi_params; uwsgi_pass 127.0.0.1:7000; }}最后在本地可以請求 Nginx 服務地址的7001端口,可以看到返回 “hello world” 字符串,說明 Nginx 轉發(fā) uwsgi 協(xié)議成功。[shen@shen ~]$ curl http://180.76.152.113:7001hello world
本節(jié)通過實例說明 Python 多線程的使用場景?,F(xiàn)在需要編寫程序獲取 baidu.com、taobao.com、qq.com 首頁,程序包括 3 個任務:獲取 baidu.com 的首頁獲取 taobao.com 的首頁獲取 qq.com 的首頁本節(jié)需要使用到 python 的 requests 模塊,requests 模塊的用于 http 請求,requests 模塊提供了 get 方法用于獲取網(wǎng)頁。在 3.1 小節(jié)演示串行執(zhí)行這 3 個任務,并記錄串行完成 3 個任務總共所需要的時間;在 3.2 小節(jié)演示并行執(zhí)行這 3 個任務,并記錄并行完成 3 個任務總共所需要的時間。
根據(jù) RESTful 規(guī)范,應該盡量使用專用的域名用于部署 API,于是我們和校方溝通,使用下方域名作為 API 訪問地址:https://api.demo.com但是經(jīng)過溝通,發(fā)現(xiàn)上述域名已被占用,校方否決了我們的提議,考慮到 API 相對簡單,于是我們使用下面地址部署 API:https://www.demo.com/api上述地址中,https 代表協(xié)議名稱,常見的還有 http,二者區(qū)別在于前者在傳輸過程中是將信息加密后傳輸?shù)?,而后者是明文傳輸;www.demo.com 為域名,可以理解成某個機房里一臺電腦的地址,通過這個地址,就能訪問這臺電腦提供的資源;api 代表一個資源路徑,可以想象成這臺電腦中一個文件夾的路徑。
頁面引用 iframe 元素,相當于引用一個完整的 HTML 網(wǎng)頁。這樣做的好處是:代碼可復用性,相同的頁面無需重復實現(xiàn),只需要引用即可;iframe 是一個封閉的運行環(huán)境,環(huán)境變量完全獨立、隔離,不會污染宿主環(huán)境;iframe 可以用于創(chuàng)建新的宿主環(huán)境,用于隔離或者訪問原始接口及對象,提升網(wǎng)站的安全性缺點是:被引用的 iframe 如果過多的話,可能會產(chǎn)生過量的 HTTP 請求;跨域問題;樣式不容易適配基于 iframe 的優(yōu)缺點來看,在實際項目開發(fā)中,一般用來加載廣告、播放器、富文本編輯器等非核心的或者需要格里運行的網(wǎng)頁代碼。
code () 和 message () 屬性定義:code 屬性就是描述接口返回的響應數(shù)據(jù)的狀態(tài)碼。message 屬性就是對接口返回的響應數(shù)據(jù)的狀態(tài)碼進行描述。使用方法:在 ApiResponse 注解中直接聲明 code 屬性和 message 屬性的值即可,例如,對于用戶登錄接口,我想添加一個值為 666 的狀態(tài)碼,其描述為 success ,代表當接口返回的狀態(tài)碼為 666 時表示請求是成功(success)的。鑒于上述業(yè)務場景,我們可以這樣寫:code = 666, message = “success”(現(xiàn)在你不需要理解業(yè)務代碼代表什么意思,重點看實體類上使用的注解及屬性即可,下同 。@ApiResponse(code = 666, message = "success")public ServerResponse<User> login(User user){ // do something...} 代碼解釋:第 1 行,我們在用戶登錄接口方法的上方定義了 ApiResponse 注解的 code 屬性的值為 666,message 屬性的值為 success 來對用戶登錄接口添加特定的返回信息。顯示結果:可以看到,在用紅框框起來的地方就是我們使用 code 屬性和 message 屬性所展示的效果了。Tips :一般而言,在實際項目開發(fā)中,http 協(xié)議自帶的返回狀態(tài)碼已經(jīng)夠用了,不需要開發(fā)者再特殊指定,如果業(yè)務要求必須遵照一定的規(guī)則,那就只能額外規(guī)定了。responseHeaders () 屬性定義:該屬性就是對接口的返回頭做一個描述,即猶如請求接口時所規(guī)定的請求頭為 ‘a(chǎn)pplication/json’ 類型那樣。使用方法:在 ApiResponse 注解中,直接聲明 responseHeaders 屬性的值即可,例如,我想把用戶登錄接口的返回頭類型定義為‘multipart/file’ (這樣定義顯然是不合理的,這里只做演示) ,則可以這樣寫:description = “用戶實體中包含用戶相關的所有業(yè)務字段,如有需要請另行添加” 。@ApiResponse(code = 666, message = "success", responseHeaders = { @ResponseHeader(name = "userLoginHeader", description = "multipart/file") }),public ServerResponse<User> login(User user){ // do something...} 代碼解釋:第 1 行,我們在用戶實體類的上方定義了 responseHeaders 屬性的值來對用戶登錄接口的返回頭類型添加額外的描述信息。由于篇幅原因這里就不給大家截圖了。Tips :responseHeaders 屬性值的定義應該按照 http 協(xié)議規(guī)定好的類別進行描述,如果我們所描述的不是 http 協(xié)議所規(guī)定的類型,那么在 Swagger 界面上是不會顯示出來的,這點需要注意。responseHeaders 屬性雖然是 ApiResponse 注解中的,但是使用該屬性需要以數(shù)組的形式使用,即如上述代碼示例,因為該注解源碼中就是這樣定義的。
環(huán)境配置是最為復雜的一項配置,MyBatis 提供了多環(huán)境配置機制,例如:開發(fā)環(huán)境和生產(chǎn)環(huán)境上的數(shù)據(jù)庫配置就大概率不一樣。每個 environment 都有一個唯一的 id 字段,且 environments 需要提供一個默認環(huán)境,如下:<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/imooc?useSSL=false"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments></configuration>在每個 environment 下又有兩個子配置項,它們分別負責管理事務和數(shù)據(jù)源。
下面我們將redis:latest鏡像改個名字,傳到私有倉庫。# 將redis:latest鏡像名稱改為127.0.0.1:6000/myredis:v1# 127.0.0.1:6000/xxx是固定寫法,與之前的地址對應docker tag redis:latest 127.0.0.1:6000/myredis:v1# 上傳到私有倉庫docker push 127.0.0.1:6000/myredis:v1# 查看私有倉庫中的鏡像curl http://127.0.0.1:6000/v2/_catalog得到結果{"repositories":["myredis"]}說明上傳成功。接下來,我們將本地的127.0.0.1:6000/myredis:v1刪除,嘗試從私有倉庫中拉去# 刪除鏡像docker rmi 127.0.0.1:6000/myredis:v1# 把redis鏡像也刪除掉,這樣可以清理掉相關的緩存層,使后面鏡像的下載過程和結果更清楚docker rmi redis# 拉取鏡像docker pull 127.0.0.1:6000/myredis:v1運行docker images可以看到此鏡像已經(jīng)被緩存到本地了。
Tornado 是一個開源的網(wǎng)絡服務器框架,它是基于社交聚合網(wǎng)站 FriendFeed 的實時信息服務開發(fā)而來的。Tornado 跟其他主流的 Python Web 服務器框架不同是采用 epoll 非阻塞 IO,響應快速,可處理數(shù)千并發(fā)連接,特別適用用于實時的 Web 服務。優(yōu)點:完備的 Web 框架;提供了異步 I/O 支持、超時事件處理;提供高效的內部 HTTP 服務器;完備的 WebSocket 支持。缺點:沒有 ORM ,提供的支持和模板少,缺少后臺支持,對小型項目來說開發(fā)速度沒有 django 快。安裝:通過 pip 直接安裝:pip install Tornado通過源碼下載并安裝$ git clone https://github.com/tornadoweb/tornado$ cd tornado$ python setup.py install
上節(jié)課我們學習了如何使用 Postman 發(fā)起一個 API 請求,既然發(fā)送了請求,那么就必然要接收響應。為了更好的接收響應,Postman 內置了響應查看器。Postman 響應查看器有助于確保 API 響應的準確性。API 響應是由響應主體、響應頭和狀態(tài)碼組成的。類似于上面的請求配置部分,Postman 將響應的不同部分顯示在不同的選項卡中。在選項卡旁邊可以看到 API 調用的狀態(tài)碼和完成時間。響應還包含符合 HTTP 規(guī)范的默認描述。API 作者也可以添加自定義的返回消息。下面看一些基本功能和操作:
前面從默認的 lua 腳本中我們已經(jīng)看到了 Splash 的一些常用方法,如 go()、wait()、html()、png() 等,我們來一一進行介紹:splash:go():這個方法比較熟悉了,就是跳轉去對應的 url 地址,目前它只支持 GET 和 POST 請求。該方法支持指定 HTTP 請求頭,表單等數(shù)據(jù)。對應的方法原型如下:ok, reason = splash:go{url, baseurl=nil, headers=nil, http_method="GET", body=nil, formdata=nil}函數(shù)參數(shù)以及返回結果詳情可參考:splash:go,官方已經(jīng)給出了非常詳細的說明,這里就不再進行翻譯了。splash:wait(): 控制頁面等待時間,函數(shù)原型如下:ok, reason = splash:wait{time, cancel_on_redirect=false, cancel_on_error=true}cancel_on_redirect 參數(shù)默認為 false,表示如果等待中發(fā)生重定向則停止等待并返回重定向結果;cancel_on_error 默認為 true,表示在等待渲染中出現(xiàn)了錯誤則停止等待并返回 nil, "<error string>",其中 error string 指的是加載錯誤的原因;三個和執(zhí)行 js 相關的方法:splash:jsfunc(),該方法用于將 JavaScript 方法轉換成 Lua 中可調用的方法。注意所調用的 JavaScript 函數(shù)必須在一對雙中括號內,類似如下寫法:function main(splash, args) -- get_div_count 就是表示jsfunc中定義的js方法 local get_div_count = splash:jsfunc([[ function () { var body = document.body; var divs = body.getElementsByTagName('div'); return divs.length; } ]]) splash:go(args.url) return ("There are %s DIVs in %s"):format( get_div_count(), args.url)endsplash:evaljs(),直接在渲染的頁面中執(zhí)行 js 腳本。來看看如下示例:local title = splash:evaljs("document.title")splash:runjs(),它和 evaljs() 方法功能類似,也是執(zhí)行 JavaScript 代碼。前者它更偏向于執(zhí)行某些動作或者定義某些方法:-- 這樣子的寫法,foo便會加入到全局上下文中,下面注釋的這樣寫法就是錯誤的-- assert(splash:runjs("function foo(){return 'bar'}"))-- 下面這個為正確寫法assert(splash:runjs("foo = function (){return 'bar'}"))local res = splash:evaljs("foo()") -- this returns 'bar'splash:autoload(),該方法用于設置每個頁面訪問時自動加載的 JavaScript 代碼,該方法只負責加載代碼并不執(zhí)行。我們通常會用該方法去加載一些必須的 js 庫函數(shù),如 jQuery 等,也會使用該方法加載我們自定義的 js 函數(shù)。assert(splash:autoload("https://code.jquery.com/jquery-2.1.3.min.js"))splash:call_later(),該方法通過設置任務的延長時間來實現(xiàn)任務的延遲執(zhí)行。splash:http_get(),該方法發(fā)送 http 的 get 請求并返回響應,方法的原型如下:response = splash:http_get{url, headers=nil, follow_redirects=true}splash:http_post(),該方法發(fā)送 http 的 post 請求并返回響應,方法的原型如下:response = splash:http_post{url, headers=nil, follow_redirects=true, body=nil}splash:set_content(),該方法用于設置當前頁面的內容并等待頁面加載;我們來看看官方給的一個簡單示例:function main(splash) assert(splash:set_content("<html><body><h1>hello</h1></body></html>")) return splash:png()end渲染效果如下:splash:html():獲取渲染后的網(wǎng)頁源碼;splash:png():獲取 png 格式的頁面截圖;splash:jpg():獲取 jpg 格式的頁面截圖;splash:url():獲取當前訪問頁面的 url;cookie 相關的方法:splash:get_cookies():獲取 CookieJar 的內容-腳本中所有 cookies 的列表;splash:add_cookie():添加一個 cookie;splash:init_cookies():將當前所有 cookies 替換成傳入的 cookiessplash:clean_cookies():清除所有的 cookies;splash:delete_cookies():刪除指定的 cookies;splash:set_viewport_full():設置瀏覽器全屏顯示;splash:on_request():在每個 http 請求之前注冊要調用的函數(shù)。這個方法非常有用,官方給出了6中用途示例,如記錄所有的請求、丟棄某個特殊的請求 (比如以 .css 結尾的請求) 等,這也從某方面說明了該方法的重要性;接下來我們看看 Splash 中一些更高級的用法,包括頁面元素定位、填充輸入框以及模擬鼠標操作等方法。
前面這種簡單檢查 referer 頭部值的防盜鏈方法過于脆弱,盜用者很容易通過偽造 referer 的值輕而易舉跳過防盜措施。在 Nginx 中有一種更為高級的防盜方式,即基于 secure_link 模塊,該模塊能夠檢查請求鏈接的權限以及是否過期,多用于下載服務器防盜鏈。這個模塊默認未編譯進 Nginx,需要在源碼編譯時候使用 --with-secure_link_module 添加。該模塊的通過驗證 URL 中的哈希值的方式防盜鏈。它的防盜過程如下:由服務器或者 Nginx 生成安全的加密后的 URL, 返回給客戶端;客戶端使用安全的 URL 訪問 Nginx,獲取圖片等資源,由 Nginx 的 secure_link 變量判斷是否驗證通過;secure_link 模塊中總共有3個指令,其格式和說明分別如下:Syntax: secure_link expression;Default: —Context: http, server, locationSyntax: secure_link_md5 expression;Default: —Context: http, server, locationSyntax: secure_link_secret word;Default: —Context: location通過配置 secure_link, secure_link_md5 指令,可實現(xiàn)對鏈接進行權限以及過期檢查判斷的功能。和 referer 模塊中的 $invalid_referer 變量一樣,secure_link 模塊也是通過內置變量 KaTeX parse error: Expected 'EOF', got '判' at position 14: secure\_link 判?斷驗證是否通過。secure_link 的值有如下三種情況:空字符串: 驗證不通過0: URL 過期1: 驗證通過通常使用這個模塊進行 URL 校驗,我們需要考慮的是如何生成合法的 URL ?另外,需要在 Nginx 中做怎樣的配置才可以校驗這個 URL?對于第一個問題,生成合法的 URL 和 指令 secure_link_md5 有關。例如:secure_link_md5 "$secure_link_expires$uri$remote_addr secret";如果 Nginx 中secure_link_md5 是上述配置,那么生成合法 url 的命令如下:# 2020-02-05 21:00:00 轉換成時間戳為1580907600echo -n '1580907600/test.png127.0.0.1 secret' | \ openssl md5 -binary | openssl base64 | tr +/ -_ | tr -d =通過上述命令,我們得到了一個 md5 值:cPnjBG9bAZvY_jbPOj13mA,這個非常重要。接下來,構造合的 URL 和指令 secure_link 相關。如果 secure_link 指令的配置如下:secure_link $arg_md5,$arg_expires;那么我們的請求的 url 中必須帶上 md5 和 expires 參數(shù),例如:http://180.76.152.113:9008/test.png?md5=cPnjBG9bAZvY_jbPOj13mA&expires=1580907600對于 Nginx 中的校驗配置示例如下:location ~* .(gif|jpg|png|swf|flv|mp4)$ { secure_link $arg_md5,$arg_expires; secure_link_md5 "$secure_link_expires$uri$remote_addr secret"; # 空字符串,校驗不通過 if ($secure_link = "") { return 403; } # 時間過期 if ($secure_link = "0") { return 410 "URL過期,請重新生成"; } root /root/test;}在 Nginx 的配置中,除了前面提到的 secure_link 和 secure_link_md5 指令外,我們對通過校驗和校驗失敗的情況進行了處理。接下來請看實驗部分。
我們來完成一個簡單的自定義 http 模塊,來實現(xiàn)前面Echo模塊的最簡單形式,即使用指令輸出 “hello, world” 字符串。首先新建一個目錄echo-nginx-module,然后在目錄下新建兩個文件config和ngx_http_echo_module.c[root@server echo-nginx-module]# pwd/root/shencong/echo-nginx-module[root@server echo-nginx-module]# lsconfig ngx_http_echo_module.c兩個文件內容分別如下:[root@server echo-nginx-module]# cat config ngx_addon_name=ngx_http_echo_module# 指定模塊名稱HTTP_MODULES="$HTTP_MODULES ngx_http_echo_module"# 指定模塊源碼路徑NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_echo_module.c"[root@server echo-nginx-module]# cat ngx_http_echo_module.c#include <ngx_config.h>#include <ngx_core.h>#include <ngx_http.h>/* Module config */typedef struct { ngx_str_t ed;} ngx_http_echo_loc_conf_t;static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf);static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);/* 定義指令 */static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), /* 指令名稱,利用ngx_string宏定義 */ NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, /* 用在 location 指令塊內,且有1個參數(shù) */ ngx_http_echo, /* 處理回調函數(shù) */ NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), /* 指定參數(shù)讀取位置 */ NULL }, ngx_null_command};/* Http context of the module */static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf, /* create location configration */ ngx_http_echo_merge_loc_conf /* merge location configration */};/* Module */ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, /* module context */ ngx_http_echo_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING};/* Handler function */static ngx_int_tngx_http_echo_handler(ngx_http_request_t *r){ ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; /* 獲取指令的參數(shù) */ elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { /* 如果不是 HEAD/GET/PUT 請求,則返回405 Not Allowed錯誤 */ return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } /* 向用戶發(fā)送相應包 */ return ngx_http_output_filter(r, &out);}static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf){ ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); /* 指定處理的handler */ clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK;}static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf){ ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf;}static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child){ ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK;}這樣一個第三方模塊包就完成了,接下來我們要向之前使用第三方模塊一樣,將它編譯進 Nginx,具體操作如下。[root@server shencong]# cd nginx-1.17.6/[root@server nginx-1.17.6]# ./configure --prefix=/root/shencong/nginx-echo --add-module=/root/shencong/echo-nginx-module...[root@server nginx-1.17.6] # make && make install...[root@server nginx-1.17.6]# cd ../nginx-echo/sbin/[root@server sbin]# ./nginx -Vnginx version: nginx/1.17.6built by gcc 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) configure arguments: --prefix=/root/shencong/nginx-echo --add-module=/root/shencong/echo-nginx-module接下來,我們只要在 nginx.conf 中加入我們的指令,并給一個參數(shù),就能看到我們自定義的輸出了。...http { ... server { listen 80; server_name localhost; location / { root html; index index.html index.htm; } location /test { echo hello,world; } ... }}...最后我們請求主機的80端口,URI=/test,瀏覽器輸出"hello, world",說明我們的自定義模塊成功了!
最早我們學習了 Nginx 命令行操作,這些命令行操作都是給 Master 進程發(fā)信號,然后再由 Master 進程發(fā)送信號給 Worker 進程,從而達到控制 Worker 進程的目標。我們以 Nginx 的熱部署命令./nginx -s reload 來描述 Nginx 命令行的執(zhí)行流程。具體過程如下:首先 Master 進程會檢查 nginx.conf 文件是否存在語法錯誤,并從中找到 nginx.pid 配置路徑(沒有配置會使用默認值)reload 參數(shù)表示向 Master 進程發(fā)送 HUP 信號。Nginx 會根據(jù)會保存在 nginx.pid 文件中的值找到 Master 進程的 pid。如果 Nginx 進程沒有啟動,則沒有該 nginx.pid 文件,命令行會報錯;# 在 Nginx 的配置文件中配置 nginx.pid 的保存路徑[root@server sbin]# ./nginx -s reloadnginx: [error] open() "/root/nginx/logs/nginx.pid" failed (2: No such file or directory)Master 進程打開新的監(jiān)聽端口;Master 進程用新配置啟動新的 Worker 進程。新的 Worker 進程起來后,開始接收 Http 請求并處理,此時老的 Worker進程會停止接受 Http 請求;Master 進程會向老的 Worker進程發(fā)送 QUIT 信號;老的 Worker 進程關閉監(jiān)聽句柄,處理完正在進行的請求后結束進程。Nginx 命令行中 -s 參數(shù)的每個值都對應這一個信號。因此,我們也可以直接對 Master 進程發(fā)生相應信號達到同樣的目的。# pid表示Nginx的主進程id號# kill -s 信號 pid
TCP 是面向字節(jié)流的傳輸協(xié)議。所謂字節(jié)流是指 TCP 并不理解它所傳輸?shù)臄?shù)據(jù)的含義,在它眼里一切都是字節(jié),1 字節(jié)是 8 比特。比如,TCP 客戶端向服務器發(fā)送“Hello Server,I’m client。How are you?”,TCP 客戶端發(fā)送的是具有一定含義的數(shù)據(jù),但是對于 TCP 協(xié)議棧來說,傳輸?shù)氖且淮止?jié)流,具體如何解釋這段數(shù)據(jù)需要 TCP 服務器的應用程序來完成,這就涉及到“應用層協(xié)議設計”的問題。在 TCP/IP 協(xié)議棧的四層協(xié)議模型中,操作系統(tǒng)內核協(xié)議棧實現(xiàn)了鏈路層、網(wǎng)絡層、傳輸層,將應用層留給了應用程序來實現(xiàn)。在編程實踐中,通常有文本協(xié)議和二進制協(xié)議兩種類型,前者通常通過一個分隔符區(qū)分消息語義,而后者通常是需要通過一個 length 字段指定消息體的大小。比如著名的 HTTP 協(xié)議就是文本協(xié)議,通過 “\r\n” 來區(qū)分 HTTP Header 的每一行。而 RTMP 協(xié)議是一個二進制協(xié)議,通過 length 字段來指定消息體的大小。解析 TCP 字節(jié)流的語義通常叫做消息解析,如果按照傳統(tǒng) C 語言函數(shù)的方式來實現(xiàn),還是比較麻煩的,有很多細節(jié)需要處理。好在 Java 為我們提供了很多工具類,給我們的工作帶來了極大地便利。
這一節(jié)課中會帶著大家快速的過一遍 HTML/CSS/JS 的基礎知識,當然這節(jié)課所學到的基礎知識只是針對本門課程,可以讓你在學習這門課程的時候更加順暢,深入的 HTML/CSS/JS 知識還是要去上面推薦的幾門課程中學習。
功能描述: 請求包中有很多消息頭信息,都是以 key:value 的格式存在。在開發(fā)過程中,當需要獲取這些消息頭信息時,可以使用 @RequestHeader 注解自動綁定。實例:@RequestMapping(value="/header")public String getHeader(@RequestHeader("Accept-Language") String accpetLanguage){ ...}當請求格式類似于 http://localhost:8888/header 時,**getHeader()**方法的 accpetLanguage 參數(shù)會被注入請求包中 key 名為 Accept-Language 的消息頭值。當然,你可以更改成你所需要的消息頭的 key 名,然后獲取其對應的值。@RequestHeader 注解中的方法如下:@AliasFor("name")String value() default "";String name() default "";boolean required() default true;String defaultValue() default ValueConstants.DEFAULT_NONE;其功能和 @RequestMapping 注解中的一樣。兩者只是關心的請求包中的數(shù)據(jù)不同而已。
前面第9節(jié)中我們簡單介紹了 Django FBV 和 CBV,分別表示以函數(shù)形式定義的視圖和以類形式定義的視圖。函數(shù)視圖便于理解,但是如果一個視圖函數(shù)對應的 URL 路徑支持多種不同的 HTTP 請求方式時,如 GET, POST, PUT 等,需要在一個函數(shù)中寫不同的業(yè)務邏輯,這樣導致寫出的函數(shù)視圖可讀性不好。此外,函數(shù)視圖的復用性不高,大量使用函數(shù)視圖,導致的一個結果就是大量重復邏輯和代碼,嚴重影響項目質量。而 Django 提供的 CBV 正是要解決這個問題而出現(xiàn)的,這也是官方強烈推薦使用的方式。
實際項目中使用 websocket 需要注意一些問題 :websocket 創(chuàng)建之前需要使用 HTTP 協(xié)議進行一次握手請求,服務端正確回復相應的請求之后才能創(chuàng)建 websocket 連接;創(chuàng)建 websocket 時需要進行一些類似 token 之類的登錄認證,不然任何客戶端都可以向服務器進行 websocket 連接;websocket 是明文傳輸,敏感的數(shù)據(jù)需要進行加密處理;由于 websocket 是長連接,當出現(xiàn)異常時連接會斷開,服務端的進程也會丟失,所以服務端最好有守護進程進行監(jiān)控重啟;服務器監(jiān)聽的端口最好使用非系統(tǒng)性且不常使用的端口,不然可能會導致端口沖突
這里我們演示在 Nginx 中使用第三方模塊。 Openresty 社區(qū)提供了一款 Nginx 中的 Echo 模塊,即echo-nginx-module。在 Nginx 中添加了該模塊后,我們在配置文件中可以使用該模塊提供的 echo 指令返回用戶響應,簡單方便。該模塊的源碼在 github 上,并且有良好的文檔和使用示例,非常方便開發(fā)者使用?,F(xiàn)在我們在 Nginx 的源碼編譯階段加入該第三方模塊,具體操作如下:[root@server shencong]# pwd/root/shencong[root@server shencong]# mkdir nginx-echo# 下載 nginx 源碼包和第三方模塊的源碼包 [root@server shencong]# wget http://nginx.org/download/nginx-1.17.6.tar.gz[root@server shencong]# wget https://github.com/openresty/echo-nginx-module/archive/v0.62rc1.tar.gz# 解壓[root@server shencong]# tar -xzf nginx-1.17.6.tar.gz[root@server shencong]# tar -xzf v0.62rc1.tar.gz[root@server shencong]# lsecho-nginx-module-0.62rc1 nginx-1.17.6 nginx-1.17.6.tar.gz nginx-echo v0.62rc1.tar.gz[root@server shencong]# cd nginx-1.17.6# 使用--add-module添加第三方模塊,參數(shù)為第三方模塊源碼[root@server shencong]# ./configure --prefix=/root/shencong/nginx-echo --add-module=/root/shencong/echo-nginx-module-0.62rc1編譯完成后,我們就可以去nginx-echo目錄中的 nginx.conf文件中添加echo 指令 。準備如下的配置(可以參參考社區(qū)提供的示例):...http { server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root html; index index.html index.htm; } # 新增測試 echo 指令配置 location /timed_hello { default_type text/plain; echo_reset_timer; echo hello world; echo "'hello world' takes about $echo_timer_elapsed sec."; echo hiya igor; echo "'hiya igor' takes about $echo_timer_elapsed sec."; } location /echo_with_sleep { default_type text/plain; echo hello world; echo_flush; # ensure the client can see previous output immediately echo_sleep 2.5; # in sec echo "'hello' takes about $echo_timer_elapsed sec."; } }}...啟動 Nginx 后,我們就可以在瀏覽器上請求者兩個 URI 地址,看到相應 echo 返回的信息了。第二個配置是使用了 echo_sleep 指令,會使得請求在休眠 2.5s 后才返回。
關于 Nginx,百度百科的介紹如下: Nginx (engine x)是一個開源、高性能的 HTTP 和反向代理 Web 服務器,同時也提供了 IMAP/POP3/SMTP 服務”。首先,對 Web 服務器做一個簡要說明:Web 服務器一般指網(wǎng)站服務器,是指駐留于因特網(wǎng)上某種類型計算機的程序,可以向瀏覽器等 Web 客戶端提供文檔,也可以放置網(wǎng)站文件,讓全世界瀏覽。可以放置數(shù)據(jù)文件,讓全世界下載。常見的 Web 服務器有: Apache、Nginx、微軟的 IIS 和 Tomcat。比如當我啟動 Nginx 服務后,服務監(jiān)聽服務器上的端口,當從外面訪問這個 ip+ 端口 的地址時,我們能對應訪問服務器上的某些靜態(tài)文件,或者動態(tài)服務響應,對相應的 http 請求進行處理并返回某個結果。這樣就是通過瀏覽器和 Web 服務器(也就是 Nginx )進行交互。Nginx 是由俄羅斯的工程師 Igor Sysoev 在 Rambler 集團任職系統(tǒng)管理員時利用業(yè)余時間所開發(fā)高性能 web 服務,官方測試 Nginx 能夠支撐 5 萬并發(fā)鏈接,并且 cpu、內存等資源消耗卻非常低,運行非常穩(wěn)定,所以現(xiàn)在很多知名的公司都在使用 Nginx 或者在此基礎上進行了二次開發(fā),包括淘寶、新浪、百度等。對于中小型企業(yè)而言,開源免費而又性能強大的 Nginx 必然也是首選,后續(xù)我們將看到一組統(tǒng)計數(shù)據(jù)來說明 Nginx 的應用之廣泛。
將 nginx 訪問日志文件存儲為 nginx.log,可以利用 cat 命令來創(chuàng)建文件cat > nginx.log <<EOF112.65.61.117 - - [05/Nov/2019:17:10:54 +0800] "GET /js/chunk-2eca3a5a.2f1d5ea3.js HTTP/1.1" 200 5276 "https://smartsds.tools.anchnet.com/index" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36 QBCore/4.0.1278.400 QQBrowser/9.0.2524.400 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2875.116 Safari/537.36 NetType/WIFI MicroMessenger/7.0.5 WindowsWechat" "-"...EOF編寫代碼,其中為們利用 cut 對 nginx 日志文件進行分割,狀態(tài)碼的列尾第九列#!/bin/bash# Description: nginx status# Auth: kaliarch# Email: kaliarch@163.com# function: nginx status check# Date: 2020-03-21 14:00# Version: 1.0# 對輸入的nginx日志文件進行判斷NGINX_LOG=$1# 定義管理數(shù)組declare -A HTTP_STATUS# 對數(shù)組進行內容賦值# 利用netstat命令來過濾出關系的一列數(shù)據(jù)for status in $(cat ${NGINX_LOG} |cut -d" " -f9)do # 對狀態(tài)相同狀態(tài)的HTTP進行數(shù)值累加 let HTTP_STATUS[${status}]++done# 將統(tǒng)計完成的TCP鏈接狀態(tài)及數(shù)據(jù)記錄到日志中for i in ${!HTTP_STATUS[@]}do if [ ${i} -eq 404 ];then echo -e "\033[34;40m文件${NGINX_LOG}中狀態(tài)碼為${i}的數(shù)量為${HTTP_STATUS[${i}]} \033[0m" elif [ ${i} -eq 500 ];then echo -e "\033[31;40m文件${NGINX_LOG}中狀態(tài)碼為${i}的數(shù)量為${HTTP_STATUS[${i}]} \033[0m" elif [ ${i} -eq 200 ];then echo -e "\033[32;40m文件${NGINX_LOG}中狀態(tài)碼為${i}的數(shù)量為${HTTP_STATUS[${i}]} \033[0m" else echo -e "\033[36;40m文件${NGINX_LOG}中狀態(tài)碼為${i}的數(shù)量為${HTTP_STATUS[${i}]} \033[0m" fidone執(zhí)行結果[root@master shell_echo]# bash nginx_status.sh nginx.log 文件nginx.log中狀態(tài)碼為200的數(shù)量為17 文件nginx.log中狀態(tài)碼為301的數(shù)量為1 文件nginx.log中狀態(tài)碼為404的數(shù)量為2 文件nginx.log中狀態(tài)碼為500的數(shù)量為2
更改 UserAgent我們可以在請求頭中替換我們的請求媒介,讓網(wǎng)站誤認為是我們是通過移動端的訪問,運行下面的代碼后,當我們打開 hupu.html,我們會發(fā)現(xiàn)返回的是移動端的虎撲的頁面而不是網(wǎng)頁端的。import requestsfrom bs4 import BeautifulSoupheader_data = {'User-Agent': 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; SCH-I535 Build/KOT49H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'}re = requests.get('https://www.hupu.com/', headers=header_data)bs = BeautifulSoup(re.content)with open('hupu.html', 'wb') as f: f.write(bs.prettify(encoding='utf8'))減少爬取頻率,設置間隔時間比如,我們可以設置一個隨機的間隔時間,來模擬用戶的行為,減少訪問的次數(shù)和頻率。我們可以在我們爬蟲的程序中,加入如下的代碼,讓爬蟲休息3秒左右,再進行爬取,可以有效地避開網(wǎng)站的對爬蟲的檢測和識別。import timeimport randomsleep_time = random.randint(0.,2) + random.random()time.sleep(sleep_time)運用代理機制代理就是通過訪問第三方的機器,然后通過第三方機器的 IP 進行訪問,來隱藏自己的真實IP地址。import requestslink = "http://www.baidu.com/"proxies = {'http':'XXXXXXXXXXX'} //代理地址,可以自己上網(wǎng)查找,這里就不做推薦了response = requests.get(link, proxies=proxies)由于第三方代理良莠不齊,而且不穩(wěn)定,經(jīng)常出現(xiàn)斷線的情況,爬取速度也會慢許多,如果對爬蟲質量有嚴格要求的話,不建議使用此種方法進行爬取。變換IP進行爬取可以通過動態(tài)的 IP 撥號服務器來變換 IP,也可以通過 Tor 代理服務器來變換 IP。
接下來我們要追蹤一下 requests.get() 請求的完整過程。首先是找到相應的 get() 方法:# 源碼位置: requests/api.pyfrom . import sessionsdef request(method, url, **kwargs): with sessions.Session() as session: return session.request(method=method, url=url, **kwargs) def get(url, params=None, **kwargs): kwargs.setdefault('allow_redirects', True) return request('get', url, params=params, **kwargs)def options(url, **kwargs): kwargs.setdefault('allow_redirects', True) return request('options', url, **kwargs)def head(url, **kwargs): kwargs.setdefault('allow_redirects', False) return request('head', url, **kwargs)def post(url, data=None, json=None, **kwargs): return request('post', url, data=data, json=json, **kwargs)def put(url, data=None, **kwargs): return request('put', url, data=data, **kwargs)def patch(url, data=None, **kwargs): return request('patch', url, data=data, **kwargs)def delete(url, **kwargs): return request('delete', url, **kwargs)可以看到,所有的請求最后都是調用同一個 session.request() 方法,我們繼續(xù)追進去:# 源碼位置:requests/sessions.py# ...class Session(SessionRedirectMixin): # ... # 有了這兩個方法就可以使用 with 語句了: # with Session() as session: # pass def __enter__(self): return self def __exit__(self, *args): self.close() # ... def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None): # Create the Request. req = Request( method=method.upper(), url=url, headers=headers, files=files, data=data or {}, json=json, params=params or {}, auth=auth, cookies=cookies, hooks=hooks, ) prep = self.prepare_request(req) proxies = proxies or {} settings = self.merge_environment_settings( prep.url, proxies, stream, verify, cert ) # Send the request. send_kwargs = { 'timeout': timeout, 'allow_redirects': allow_redirects, } send_kwargs.update(settings) # 核心地方,發(fā)送 http 請求 resp = self.send(prep, **send_kwargs) return resp # ... 我們不過多陷入細節(jié),這些細節(jié)函數(shù)由讀者自行去跟蹤和調試。我們從上面的代碼中可以看到核心發(fā)送 http 請求的代碼如下:resp = self.send(prep, **send_kwargs)prep 是一個 PreparedRequest 類實例,它和 Request 類非常像。我們繼續(xù)追蹤這個 send() 方法的源碼:# 源碼位置:requests/sessions.py:# ...class Session(SessionRedirectMixin): # ... def send(self, request, **kwargs): """Send a given PreparedRequest. :rtype: requests.Response """ # Set defaults that the hooks can utilize to ensure they always have # the correct parameters to reproduce the previous request. kwargs.setdefault('stream', self.stream) kwargs.setdefault('verify', self.verify) kwargs.setdefault('cert', self.cert) kwargs.setdefault('proxies', self.proxies) # It's possible that users might accidentally send a Request object. # Guard against that specific failure case. if isinstance(request, Request): raise ValueError('You can only send PreparedRequests.') # Set up variables needed for resolve_redirects and dispatching of hooks allow_redirects = kwargs.pop('allow_redirects', True) stream = kwargs.get('stream') hooks = request.hooks # Get the appropriate adapter to use adapter = self.get_adapter(url=request.url) # Start time (approximately) of the request start = preferred_clock() # Send the request r = adapter.send(request, **kwargs) # Total elapsed time of the request (approximately) elapsed = preferred_clock() - start r.elapsed = timedelta(seconds=elapsed) # Response manipulation hooks r = dispatch_hook('response', hooks, r, **kwargs) # Persist cookies if r.history: # If the hooks create history then we want those cookies too for resp in r.history: extract_cookies_to_jar(self.cookies, resp.request, resp.raw) extract_cookies_to_jar(self.cookies, request, r.raw) # Resolve redirects if allowed. if allow_redirects: # Redirect resolving generator. gen = self.resolve_redirects(r, request, **kwargs) history = [resp for resp in gen] else: history = [] # Shuffle things around if there's history. if history: # Insert the first (original) request at the start history.insert(0, r) # Get the last request made r = history.pop() r.history = history # If redirects aren't being followed, store the response on the Request for Response.next(). if not allow_redirects: try: r._next = next(self.resolve_redirects(r, request, yield_requests=True, **kwargs)) except StopIteration: pass if not stream: r.content return r代碼會有點長,大家需要自行看看這個方法的邏輯,不要陷入細節(jié)。從上面的代碼我們可以發(fā)現(xiàn)兩個關鍵語句:adapter = self.get_adapter(url=request.url):獲取合適的請求適配器;r = adapter.send(request, **kwargs):發(fā)送請求,獲取響應結果;第一個 adapter 怎么來的呢?繼續(xù)看那個 self.get_adapter() 方法:# 源碼位置:requests/sessions.py:# ...class Session(SessionRedirectMixin): # ... def __init__(self): # ... # Default connection adapters. self.adapters = OrderedDict() self.mount('https://', HTTPAdapter()) self.mount('http://', HTTPAdapter()) # ... def get_adapter(self, url): """ Returns the appropriate connection adapter for the given URL. :rtype: requests.adapters.BaseAdapter """ for (prefix, adapter) in self.adapters.items(): if url.lower().startswith(prefix.lower()): return adapter # Nothing matches :-/ raise InvalidSchema("No connection adapters were found for {!r}".format(url)) # ...其實仔細在分析下,就可以知道我們在初始化 (__init__.py) 中添加了請求前綴 prefix (https:// 和 http://) 對應的連接適配器 (HTTPAdapter()),因此這里 adapter 對應的就是 HTTPAdapter 類實例。此時要找發(fā)送 http 請求的 send() 方法就需要去 ``HTTPAdapter` 中查找:# 源碼位置:requests/adapters.py# ...class BaseAdapter(object): """The Base Transport Adapter""" def __init__(self): super(BaseAdapter, self).__init__() def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): raise NotImplementedError def close(self): """Cleans up adapter specific items.""" raise NotImplementedError class HTTPAdapter(BaseAdapter): # ... def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): try: conn = self.get_connection(request.url, proxies) # 自行加上一個打印語句,查看conn類型 # print('conn:', type(conn)) except LocationValueError as e: raise InvalidURL(e, request=request) self.cert_verify(conn, request.url, verify, cert) url = self.request_url(request, proxies) self.add_headers(request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies) chunked = not (request.body is None or 'Content-Length' in request.headers) # ... try: if not chunked: resp = conn.urlopen( method=request.method, url=url, body=request.body, headers=request.headers, redirect=False, assert_same_host=False, preload_content=False, decode_content=False, retries=self.max_retries, timeout=timeout ) # Send the request. else: # ... except (ProtocolError, socket.error) as err: raise ConnectionError(err, request=request) except MaxRetryError as e: # ... except ClosedPoolError as e: raise ConnectionError(e, request=request) except _ProxyError as e: raise ProxyError(e) except (_SSLError, _HTTPError) as e: # ... return self.build_response(request, resp)就我們前面的請求而言,request.body 往往為 None,所以 chunked 一般為 False。那么最終的請求走的就是conn.urlopen() 方法。注意:這里最關鍵的步驟是得到連接遠端服務的信息 conn,后面發(fā)送數(shù)據(jù)都是通過 conn 走的。# 源碼位置:requests/adapters.py# ...class BaseAdapter(object): """The Base Transport Adapter""" def get_connection(self, url, proxies=None): """Returns a urllib3 connection for the given URL. This should not be called from user code, and is only exposed for use when subclassing the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`. :param url: The URL to connect to. :param proxies: (optional) A Requests-style dictionary of proxies used on this request. :rtype: urllib3.ConnectionPool """ proxy = select_proxy(url, proxies) if proxy: # 使用代理 # ... else: # Only scheme should be lower case parsed = urlparse(url) url = parsed.geturl() conn = self.poolmanager.connection_from_url(url) return conn我們可以運行并打印這個 conn 變量。這里需要改源代碼,在源碼位置加上一行 print() 方法:>>> import requests>>> payload = {'key1': 'value1', 'key2': ['value2', 'value3']}>>> r = requests.get('https://httpbin.org/get', params=payload)conn: <class 'urllib3.connectionpool.HTTPSConnectionPool'>>>>我們終于看到,最后 requests 庫其實就是封裝 Python 內置的 urllib3 模塊來完成 http 請求的。上面獲取 conn 值的代碼比較多且繞,有興趣的讀者可以自行跟蹤下,限于篇幅,這里就不過多描述了。
常用的配置項我們簡單帶過,比如:data:代表著發(fā)送到服務器的數(shù)據(jù)。 在不同的情況下會轉化字符串的格式,在 GET 方法的時候,會變?yōu)?“&” 拼接的參數(shù)附帶在 url 后面。dataType:預期服務器返回的數(shù)據(jù)類型。 如果沒有指定的話,Ajax 會根據(jù) HTTP 的 MIME 信息來進行判斷。cache:緩存控制相關。 默認是 true,如果設置為 false,那么瀏覽器不緩存此頁面。headers:請求頭。 我們通常會給一個 { key : value} 這樣的鍵值對對象來設置我們的請求頭內容。type:請求方法。默認是 GET 。
修改客戶端的配置文件 application.properties ,以便指定客戶端指向的服務端的地址。由于剛剛服務端已經(jīng)占用了 8080 端口,所以將客戶端的端口設置為 8091 。還有一個必要設置是客戶端的名稱,當我們監(jiān)控的項目實例比較多時,需要通過客戶端名稱來區(qū)分。實例:# 配置端口server.port=8091# 配置監(jiān)控管理端地址spring.boot.admin.client.url=http://127.0.0.1:8080# 客戶端的名稱,用于區(qū)分不同的客戶端spring.boot.admin.client.instance.name=CLIENT1TIps:此處指定監(jiān)控管理端地址使用的是 spring.boot.admin.client.url ,我個人認為應使用 spring.boot.admin.server.url 更加合理。當然大家不用糾結于此,此處只是特別提示。
TLS 是指傳輸層安全(Transport Layer Security)。TLS 的作用包括:機密性。保護消息的內容不被攻擊者獲取。完整性。保護消息不被攻擊者篡改,是真實可靠的。防重放。保護消息不被攻擊者截獲并再次發(fā)送。身份識別。允許客戶端驗證服務器的可靠性(注意客戶端的真實性只有在客戶端認證開啟時才進行)。TLS 同樣也被很多其他協(xié)議用于保護機密性和完整性,并且有多種用法。我們在這里主要關注的是基于 HTTP 協(xié)議的 B/S 應用,將它升級為 HTTPS。
其實可以發(fā)現(xiàn),開關控件和上一節(jié)講到的選擇框很類似,每一個選擇框也是有兩種狀態(tài)——“選中、未選中”。開關控件的“開、關”狀態(tài)和它非常類似,我們可以借鑒 RadioButton 及 Checkbox 的例子。首先按照第 2 小節(jié)介紹的屬性編寫布局文件:<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" tools:context=".MainActivity"> <ToggleButton android:id="@+id/toggle_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textOff="關" android:textOn="開" /> <Switch android:id="@+id/switch_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:switchMinWidth="56dp" android:showText="true" android:text="WLAN" android:switchPadding="10dp" android:textOff="OFF" android:textOn="ON" /> <Button android:id="@+id/getBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:text="獲取狀態(tài)" /></LinearLayout>我們在布局中放置了 3 個控件,前兩個分別是本節(jié)的主角——ToggleButton 和 Switch,用來控制兩個開關值,而第三個是一個Button控件,用來隨時獲取開關的狀態(tài)。接著編寫 Java 代碼,通過兩種方式來接收開關狀態(tài),一種是開關變化的時候主動回調,另一種是點擊 Button 去查詢開關狀態(tài):package com.emercy.myapplication;import android.app.Activity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.CompoundButton;import android.widget.Switch;import android.widget.Toast;import android.widget.ToggleButton;public class MainActivity extends Activity implements CompoundButton.OnCheckedChangeListener, View.OnClickListener { ToggleButton mToggleButton; Switch mSwitchButton; Button mButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mToggleButton = findViewById(R.id.toggle_button); mSwitchButton = findViewById(R.id.switch_button); mButton = findViewById(R.id.getBtn); mToggleButton.setOnCheckedChangeListener(this); mSwitchButton.setOnCheckedChangeListener(this); mButton.setOnClickListener(this); } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { switch (buttonView.getId()) { case R.id.toggle_button: Toast.makeText(this, "toggle state changed : " + isChecked, Toast.LENGTH_SHORT).show(); break; case R.id.switch_button: Toast.makeText(this, "wlan state changed : " + isChecked, Toast.LENGTH_SHORT).show(); break; default: Toast.makeText(this, "no state changed", Toast.LENGTH_SHORT).show(); } } @Override public void onClick(View v) { String toggle = (mToggleButton.isChecked() ? mToggleButton.getTextOn() : mToggleButton.getTextOff()).toString(); String wlan = (mSwitchButton.isChecked() ? mSwitchButton.getTextOn() : mSwitchButton.getTextOff()).toString(); Toast.makeText(this, "toggle is : " + toggle + "\n" + "wlan is :" + wlan, Toast.LENGTH_SHORT).show(); }}我們的 Activity 實現(xiàn)了OnCheckedChangeListener和OnClickListener,前者用來接收 ToggleButton / Switch 的切換回調,后者用來監(jiān)聽 Button 的點擊從而獲取開關狀態(tài)。例子比較簡單,效果如下: