今天我们来介绍 Scrapy 框架给我们提供的 Request 和 Response 类,通过深入分析源码找出它的常用属性和方法以及一些使用技巧。这一小节内容主要是 Scrapy 框架中的基础知识,后面我们会经常用到这两个类。熟悉和掌握它们的源码实现,对我们在后续使用它们时会有巨大的帮助。
首先 Scrapy 中关于 Request 相关的源码位置如下:
scrapy 中 Request 相关的代码
可以看到 Request 定义相关的代码并不多,这也方便我们去学习和探索。先来看 Request 类的定义:
# 源码位置:scrapy/http/request/__init__.pyfrom w3lib.url import safe_url_string# ...class Request(object_ref): def __init__(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, dont_filter=False, errback=None, flags=None, cb_kwargs=None): self._encoding = encoding # this one has to be set first self.method = str(method).upper() self._set_url(url) self._set_body(body) if not isinstance(priority, int): raise TypeError("Request priority not an integer: %r" % priority) self.priority = priority if callback is not None and not callable(callback): raise TypeError('callback must be a callable, got %s' % type(callback).__name__) if errback is not None and not callable(errback): raise TypeError('errback must be a callable, got %s' % type(errback).__name__) self.callback = callback self.errback = errback self.cookies = cookies or {} self.headers = Headers(headers or {}, encoding=encoding) self.dont_filter = dont_filter self._meta = dict(meta) if meta else None self._cb_kwargs = dict(cb_kwargs) if cb_kwargs else None self.flags = [] if flags is None else list(flags) # ...代码块1234567891011121314151617181920212223242526272829303132333435
从上面的源码中可以看到 Scrapy 框架使用了 w3lib 模块来完成一些 Web 相关的功能,这里用到了 url 模块的相关功能。safe_url_string() 方法是将 url 转成合法的形式,也就是将一些特殊字符比如中文、空格等进行想要的编码。来看下面的例子:
>>> from w3lib.url import safe_url_strin>>> url = "http://www.baidu.com/?xxx= zyz">>> safe_url_string(url)'http://www.baidu.com/?xxx=%20zyz'代码块1234
最后得到的 URL 形式和我们在浏览器按下 Enter 键时一致。此外,对于 Request 类实例化时可以传入多种初始属性,常用的属性含义如下:
熟悉了这个 Request 类后,我们来看一些在 Request 基础上进一步扩展的请求类。其中一个是 FormRequest:
# 源码位置:scrapy/http/request/form.py# ...class FormRequest(Request): valid_form_methods = ['GET', 'POST'] def __init__(self, *args, **kwargs): formdata = kwargs.pop('formdata', None) if formdata and kwargs.get('method') is None: kwargs['method'] = 'POST' super(FormRequest, self).__init__(*args, **kwargs) if formdata: items = formdata.items() if isinstance(formdata, dict) else formdata querystr = _urlencode(items, self.encoding) if self.method == 'POST': self.headers.setdefault(b'Content-Type', b'application/x-www-form-urlencoded') self._set_body(querystr) else: self._set_url(self.url + ('&' if '?' in self.url else '?') + querystr) # ...代码块1234567891011121314151617181920212223
FormRequest 类主要用于提交表单请求,比如登录认证、比如提交订单等。它只支持 GET 和 POST 请求,且相比 Request 类,FormRequest 类多了一个表单参数属性,这个是检查提交表单请求的数据。来分析实例化时对表单参数的处理,代码如下:
if formdata: items = formdata.items() if isinstance(formdata, dict) else formdata querystr = _urlencode(items, self.encoding) if self.method == 'POST': self.headers.setdefault(b'Content-Type', b'application/x-www-form-urlencoded') self._set_body(querystr) else: self._set_url(self.url + ('&' if '?' in self.url else '?') + querystr) # ...def _urlencode(seq, enc): values = [(to_bytes(k, enc), to_bytes(v, enc)) for k, vs in seq for v in (vs if is_listlike(vs) else [vs])] return urlencode(values, doseq=1)代码块12345678910111213141516
这个代码的逻辑是非常清晰的,如果有表单数据,会分成 GET 和 POST 请求处理:
还有两个 JsonRequest 和 XmlRpcRequest 类,都是使用不同的形式来发送 HTTP 请求,我们来看两个类中非常关键的几行语句:
# 源码位置:scrapy/http/request/json_request.py# ...class JsonRequest(Request): def __init__(self, *args, **kwargs): # ... if body_passed and data_passed: # ... elif not body_passed and data_passed: kwargs['body'] = self._dumps(data) if 'method' not in kwargs: kwargs['method'] = 'POST' super(JsonRequest, self).__init__(*args, **kwargs) self.headers.setdefault('Content-Type', 'application/json') self.headers.setdefault('Accept', 'application/json, text/javascript, */*; q=0.01') # ...
这里 JsonRequest 中主要讲 data 数据转成 json 格式,然后保存到 body 属性中,然后设置了请求头的 Content-Type 属性为 “application/json”。
# 源码位置:scrapy/http/request/rpc.pyimport xmlrpc.client as xmlrpclib# ...class XmlRpcRequest(Request): def __init__(self, *args, **kwargs): # ... if 'body' not in kwargs and 'params' in kwargs: kw = dict((k, kwargs.pop(k)) for k in DUMPS_ARGS if k in kwargs) # 关键地方 kwargs['body'] = xmlrpclib.dumps(**kw) # ...代码块123456789101112131415
XmlRpcRequest 用来发送 XML-RPC 请求,关键的地方在于请求数据设置,使用了 xmlrpc 模块。
Response 类主要是封装了前面请求的响应结果,爬虫的一个很重要的部分就是解析这些 Response,得到我们想要的结果。这一部分内容我们就来深入分析 Response 类以及扩展类。
Response类相关代码
翻看源码,我们可以得到如下信息:
接下来我们的重点就是学习 Response 类和 TextResponse 类。
Response 类有如下几个常用属性值:
我们还是通过 Scrapy Shell 请求广州链家二手房的地址来看看真实的 Response 并打印上述值:
(scrapy-test) [root@server ~]# scrapy shell https://gz.lianjia.com/ershoufang/ --nolog[s] Available Scrapy objects:[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)[s] crawler [s] item {}[s] request [s] response <200 https://gz.lianjia.com/ershoufang/>[s] settings [s] spider [s] Useful shortcuts:[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)[s] fetch(req) Fetch a scrapy.Request and update local objects [s] shelp() Shell help (print this help)[s] view(response) View response in a browser>>> response.headers{b'Server': [b'Lianjia'], b'Date': [b'Sun, 12 Jul 2020 07:37:16 GMT'], b'Content-Type': [b'text/html; charset=UTF-8'], b'Vary': [b'Accept-Encoding'], b'Set-Cookie': [b'select_city=440100; expires=Mon, 13-Jul-2020 07:37:16 GMT; Max-Age=86400; path=/; domain=.lianjia.com', b'lianjia_ssid=a0980b19-93f6-4942-a898-96ea722d524d; expires=Sun, 12-Jul-20 08:07:16 GMT; Max-Age=1800; domain=.lianjia.com; path=/', b'lianjia_uuid=12165c9c-6c66-4996-9e2c-623a838efd4a; expires=Wed, 10-Jul-30 07:37:16 GMT; Max-Age=315360000; domain=.lianjia.com; path=/'], b'Via': [b'web05-online.zeus.ljnode.com']}>>> response.status200>>> response.url'https://gz.lianjia.com/ershoufang/'>>> response.ip_addressIPv4Address('211.159.232.241')>>> >>> response.request >>>
注意:关于这个 response,我们前面在分析 scrapy shell [url] 命令的执行过程中说过,如果命令后面带上要爬取的 URL 地址,那么在交互式的 shell 生成前,会将一些得到的基本的环境变量包括请求 URL 的响应结果 (response) 放到该环境变量中,这就是为什么我们能在该交互模式下直接使用 response 获取请求结果的原因。
来看看 Response 类中预留的一些方法:
# 源码位置:scrapy/http/response/__init__.py# ...class Response(object_ref): def __init__(self, url, status=200, headers=None, body=b'', flags=None, request=None, certificate=None, ip_address=None): self.headers = Headers(headers or {}) self.status = int(status) self._set_body(body) self._set_url(url) self.request = request self.flags = [] if flags is None else list(flags) self.certificate = certificate self.ip_address = ip_address # ... @property def text(self): """For subclasses of TextResponse, this will return the body as str """ raise AttributeError("Response content isn't text") def css(self, *a, **kw): """Shortcut method implemented only by responses whose content is text (subclasses of TextResponse). """ raise NotSupported("Response content isn't text") def xpath(self, *a, **kw): """Shortcut method implemented only by responses whose content is text (subclasses of TextResponse). """ raise NotSupported("Response content isn't text") # ...
上面这些预留的 text 属性、css() 方法以及 xpath() 方法都会在 TextResponse 中有相应的实现。接下来我们仔细分析 TextResponse 的这些属性和的方法:
# 源码位置: class TextResponse(Response): _DEFAULT_ENCODING = 'ascii' _cached_decoded_json = _NONE def __init__(self, *args, **kwargs): self._encoding = kwargs.pop('encoding', None) self._cached_benc = None self._cached_ubody = None self._cached_selector = None super(TextResponse, self).__init__(*args, **kwargs) # ...
从 __init__() 方法中可以看到,TextResponse 的属性和父类基本没变化,只是增加了一些用于缓存的属性。接下来我们再看几个重要的属性和方法:
# ...class TextResponse(Response): .... @property def text(self): """ Body as unicode """ # access self.encoding before _cached_ubody to make sure # _body_inferred_encoding is called benc = self.encoding if self._cached_ubody is None: charset = 'charset=%s' % benc self._cached_ubody = html_to_unicode(charset, self.body)[1] return self._cached_ubody # ...
上面这段代码的逻辑就是将 body 属性中的值转成 str,我们可以在 Scrapy Shell 模式下复现这一操作:
(scrapy-test) [root@server ~]# scrapy shell https://www.baidu.com --nolog[s] Available Scrapy objects:[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)[s] crawler [s] item {}[s] request [s] response <200 https://www.baidu.com>[s] settings [s] spider [s] Useful shortcuts:[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)[s] fetch(req) Fetch a scrapy.Request and update local objects [s] shelp() Shell help (print this help)[s] view(response) View response in a browser>>> response.bodyb'\r
ç¾åº¦ä¸ä¸ï¼ä½ å°±ç¥é
æ°é» hao123 å°å¾ è§é¢ è´´å§ æ´å¤äº§å ©2017 Baidu 使ç¨ç¾åº¦åå¿
读 æè§åé¦ äº¬ICPè¯030173å·
\r
'>>> type(response.body)>>> from w3lib.encoding import html_to_unicode>>> html_to_unicode("charset=None", response.body)('utf-8', '\r
百度一下,你就知道
新闻 hao123 地图 视频 贴吧 更多产品 \r
')
可以看到 ,Response 中的 body 属性值是 bytes 类型,通过 html_to_unicode() 方法可以将其转成 str,然后我们得到的网页文本就是 str 类型:
>>> text = html_to_unicode("charset=None", response.body)>>> type(text[1])
接下来的这三个方法我们在上一节介绍过,正是由于有了这些属性和方法,我们便可以使用 response.xpath() 或者 response.css() 这样的写法提取网页数据。
# ...class TextResponse(Response): .... @property def selector(self): from scrapy.selector import Selector if self._cached_selector is None: self._cached_selector = Selector(self) return self._cached_selector def xpath(self, query, **kwargs): return self.selector.xpath(query, **kwargs) def css(self, query): return self.selector.css(query)
TextResponse 类比较重要的属性和方法就这些,其他的则需要自行深入去研究相关的方法及其作用。我们现在来解答上一节提出的问题:
为什么 Scrapy 的 TextResponse 实例可以使用这样的表达式:response.xpath(...).extrat()[0] 或者 response.xpath(...).extrat_first()?
接下来我们带着这个问题来继续追踪下代码。我们以上一节的例子为例,打印 response.xpath() 的返回类型:
(scrapy-test) [root@server ~]# scrapy shell https://gz.lianjia.com/ershoufang/ --nolog...>>> data = response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()')>>> type(data)>>>
可以看到结果是 SelectorList 实例,我们来看对应定义的代码:
# 源码位置:scrapy/selector/unified.pyfrom parsel import Selector as _ParselSelector# ...class SelectorList(_ParselSelector.selectorlist_cls, object_ref): """ The :class:`SelectorList` class is a subclass of the builtin ``list`` class, which provides a few additional methods. """
它直接继承的是 parsel 模块中的 selectorlist_cls。继续看这个值的定义:
# 源码位置:parsel/selector.pyclass Selector(object): # ... selectorlist_cls = SelectorList # ...代码块12345678# 源码位置:parsel/selector.pyclass SelectorList(list): # ... def getall(self): """ Call the ``.get()`` method for each element is this list and return their results flattened, as a list of unicode strings. """ return [x.get() for x in self] extract = getall def get(self, default=None): """ Return the result of ``.get()`` for the first element in this list. If the list is empty, return the default value. """ for x in self: return x.get() return default extract_first = get
是不是找到了 extract() 和 extract_first() 方法?注意理解这段代码:
for x in self: return x.get()return default
self 表示的是 SelectorList 的实例,它其实也是一个列表,列表中的元素是 Selector 的实例。这个 for 循环相当于取的是一个元素,然后直接返回,返回的值是 x.get(),这里又会涉及 Selector 类的 get() 方法 :
# 源码位置:parsel/selector.pyfrom lxml import etree, html# ...class Selector(object): # ... def get(self): """ Serialize and return the matched nodes in a single unicode string. Percent encoded content is unquoted. """ try: return etree.tostring(self.root, method=self._tostring_method, encoding='unicode', with_tail=False) except (AttributeError, TypeError): if self.root is True: return u'1' elif self.root is False: return u'0' else: return six.text_type(self.root) # ...
我们可以同样在 Scrapy Shell 中来继续做个测试:
>>> data_list = response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()')>>> type(data_list)>>> data = data_list[0]>>> type(data)>>> data.get()'地铁口 总价低 精装实用小两房'
Selector 的 get() 方法最后提取出了我们匹配的文本,因此在 SelectorList 中的 extract()[0] 和 `extract_first() 方法将得到同样的结果:
>>> response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()').extract_first()'地铁口 总价低 精装实用小两房'>>> response.xpath('//ul[@class="sellListContent"]/li/div/div[@class="title"]/a/text()').extract()[0]'地铁口 总价低 精装实用小两房'
这样一步步追踪和实验,源码里面很多的语句就会清晰明了,我们在使用 Request 和 Response 类时便会显得更加得心应手。Request 实例化时需要哪些参数,Response 的实例有哪些方法可用, 这些疑惑在源码面前都会迎刃而解。
本节主要是深入 Scrapy 的源码去了解框架中定义的 Request 和 Response 类,通过源码我们就能了解其可用的属性和方法,只有这样我们才能掌握和使用好 Scrapy 框架。
留言与评论(共有 0 条评论) “” |