穿越回十年前,我学会了它

简介: 前文介绍过 历史对比法 : 对比代码的需求变化和版本历史,从而学习需求如何被实现。今天我们一起从 requests 源码开始,使用 历史对比法,深入其实现细节,考古一下远古爬虫的实现。

前文介绍过 历史对比法 : 对比代码的需求变化和版本历史,从而学习需求如何被实现。今天我们一起从 requests 源码开始,使用 历史对比法,深入其实现细节,考古一下远古爬虫的实现。本文主要包括下面几个部分:


  • 如何使用vs-code进行历史对比
  • 如何使用pycharm-ce进行历史对比
  • v0.2.1 文件上传支持
  • v0.2.2 cookie支持
  • v0.2.3 response优化
  • v0.2.4 改进request类
  • 小结
  • 小技巧


如何使用vs-code git历史对比



工欲善其事,必先利其器,我们先花点时间了解如何使用各种IDE工具进行git历史对比操作。先是vs-code中安装git-history插件:


image.png


对比前,先切换到初始的v0.2.0版本。然后进入插件视图,搜索v0.2.1:


image.png


版本代码差异:


image.png


如何使用pycharm-ce git历史对比



pycharm的社区版本也支持git历史对比。同样进入git视图:


image.png


image.png对比代码差异:

image.png


v0.2.1 文件上传支持



我们之前已经阅读过requests的代码,没有看过朋友可以去翻翻前文 requests 源码阅读 , 今天我们就略过v0.2.0的初始化版本介绍,直接进入对比分析。


HISTORY文件中介绍了v0.2.1新增的功能:


0.2.1 (2011-02-14)
++++++++++++++++++
* Added file attribute to POST and PUT requests for multipart-encode file uploads.
* Added Request.url attribute for context and redirects


文件上传功能涉及的代码主要是:


from .packages.poster.encode import multipart_encode
from .packages.poster.streaminghttp import register_openers
...
def send(self, anyway=False)
    if self.method == 'POST':
        if self.files:
            register_openers() # 1
            datagen, headers = multipart_encode(self.files) # 2
            req = _Request(self.url, data=datagen, headers=headers, method='POST')
...


核心在register_openers和multipart_encode两行代码,一个是支持文件上传的连接,一个是支持文件数据读取。先看register_openers:


def register_openers():
    handlers = [StreamingHTTPHandler, StreamingHTTPRedirectHandler]
    if hasattr(httplib, "HTTPS"):
        handlers.append(StreamingHTTPSHandler)
    opener = urllib2.build_opener(*handlers)
    urllib2.install_opener(opener)
    return opener


主要在使用_StreamingHTTPMixin扩展了send方法:


class _StreamingHTTPMixin:
    def send(self, value):
        ...
        try:
            if hasattr(value, 'next'):
                for data in value:
                    self.sock.sendall(data)
            ...
        except socket.error, v:
            if v[0] == 32:      # Broken pipe
                self.close()
            raise


可以看到send的支持从迭代器中读取并发送到socket。multipart_encode主要是读取本地文件并生成对于的http头:


def multipart_encode(params, boundary=None, cb=None)
    boundary = gen_boundary()
    boundary = urllib.quote_plus(boundary)
    headers = get_headers(params, boundary)
    params = MultipartParam.from_params(params)
    return multipart_yielder(params, boundary, cb), headers


生成http头的方法:


def get_headers(params, boundary):
    """Returns a dictionary with Content-Type and Content-Length headers
    for the multipart/form-data encoding of ``params``."""
    # boundary = uuid.uuid4().hex
    headers = {}
    boundary = urllib.quote_plus(boundary)
    headers['Content-Type'] = "multipart/form-data; boundary=%s" % boundary
    headers['Content-Length'] = str(get_body_size(params, boundary))
    return headers
def get_body_size(params, boundary):
    size = sum(p.get_size(boundary) for p in MultipartParam.from_params(params))
    return size + len(boundary) + 6


boundary就是文件分段的分隔符,详情见: stackoverflow.com/questions/3…


本地文件的读取准备主要在下面的iter_encode函数:


def iter_encode(self, boundary, blocksize=4096):
    total = self.get_size(boundary)
    current = 0
    block = self.encode_hdr(boundary)
    current += len(block)
    yield block
    last_block = ""
    encoded_boundary = "--%s" % encode_and_quote(boundary)
    boundary_exp = re.compile("^%s$" % re.escape(encoded_boundary),
            re.M)
    while True:
        block = self.fileobj.read(blocksize)
        if not block:
            current += 2
            yield "\r\n"
            break
        last_block += block
        if boundary_exp.search(last_block):
            raise ValueError("boundary found in file data")
        last_block = last_block[-len(encoded_boundary)-2:]
        current += len(block)
        yield block


iter_encode函数将fileobj包装成一个迭代器返回,结合前面的send方法就实现了文件的http上传。


因为代码较少,我们还可以发现一些可以优化的代码, 比如下面的实现中MultipartParam.from_params执行了两次:


def multipart_encode(params, boundary=None, cb=None):
    ...
    headers = get_headers(params, boundary)
    params = MultipartParam.from_params(params)
    ...
def get_body_size(params, boundary):
    size = sum(p.get_size(boundary) for p in MultipartParam.from_params(params))
    return size + len(boundary) + 6
def get_headers(params, boundary):
    ...
    headers['Content-Length'] = str(get_body_size(params, boundary))
    return headers


v0.2.2 cookie支持



v0.2.2主要功能是支持cookie:


0.2.2 (2011-02-14)
++++++++++++++++++
* Still handles request in the event of an HTTPError. (Issue #2)
* Eventlet and Gevent Monkeypatch support.
* Cookie Support (Issue #1)


比如put-API中,request对象上接收cookiejar


def put(url, data='', headers={}, files={}, cookies=None, auth=None):
  r = Request()
  r.url = url
  r.method = 'PUT'
  r.data = data
  r.files = files
  r.headers = headers
  r.cookiejar = cookies
  r.auth = _detect_auth(url, auth)
  r.send()
  return r.response


然后使用urllib2的HTTPCookieProcessor封装cookiejar


def _get_opener(self):
  _handlers = []
  if self.auth or self.cookiejar:
    if self.cookiejar:
      cookie_handler = urllib2.HTTPCookieProcessor(cookiejar)
      _handlers.append(cookie_handler)
    opener = urllib2.build_opener(*_handlers)
    return opener.open


CookieJar的实现,就是在http头上增加Cookie字段,字段的值是一个字典序列化的字符串:


class CookieJar:
    def __init__(self, policy=None):
        if policy is None:
            policy = DefaultCookiePolicy()
        self._policy = policy
        self._cookies_lock = _threading.RLock()
        self._cookies = {}
    def add_cookie_header(self, request):
        self._cookies_lock.acquire()
        try:
            self._policy._now = self._now = int(time.time())
            cookies = self._cookies_for_request(request)
            attrs = self._cookie_attrs(cookies)
            if attrs:
                if not request.has_header("Cookie"):
                    request.add_unredirected_header(
                        "Cookie", "; ".join(attrs))
            # if necessary, advertise that we know RFC 2965
            if (self._policy.rfc2965 and not self._policy.hide_cookie2 and
                not request.has_header("Cookie2")):
                for cookie in cookies:
                    if cookie.version != 1:
                        request.add_unredirected_header("Cookie2", '$Version="1"')
                        break
        finally:
            self._cookies_lock.release()
        self.clear_expired_cookies()


配合下面cookie的示例,就很容易理解了:


Cookie: lang=zh-CN; i_like_gogs=a333326706d3bb1c; _csrf=KSQKMtYni1y4Zbi5aRpjYbW32t86MTYxNzY3NjIzNzg5MDg3NDY0MQ%3D%3D


同时代码进行了优化,增加了_build_response用来构建响应:


def _build_response(self, resp):
  """Build internal Response object from given response."""
  self.response.status_code = resp.code
  self.response.headers = resp.info().dict
  self.response.content = resp.read()
  self.response.url = resp.url


优化前后对比很明显:


# before
resp = opener(req)
self.response.status_code = resp.code
self.response.headers = resp.info().dict
if self.method == 'GET':
  self.response.content = resp.read()
self.response.url = resp.url
# after
resp = opener(req)
self._build_response(resp)        


我们可以认为这个优化符合DRY(Don't repeat yourself)规则和迪米特法则LOD(Law of Demeter)。之前在V0.2.1中response的处理,GET,POST,PUT3个分支中重复;同时send函数还需要关心response的处理细节,重构后很好的解决了这2个问题。


v0.2.3 response优化



v0.2.3主要功能是response优化:


0.2.3 (2011-02-15)
++++++++++++++++++
* New HTTPHandling Methods
    - Reponse.__nonzero__ (false if bad HTTP Status)
    - Response.ok (True if expected HTTP Status)
    - Response.error (Logged HTTPError if bad HTTP Status)
    - Reponse.raise_for_status() (Raises stored HTTPError


response增加了一个ok状态,用来描述请求成功与否:


class Response(object)
  def __init__(self):
    self.content = None
    self.status_code = None
    self.headers = dict()
    self.url = None
    self.ok = False
    self.error = None


同样response更加内聚,可以反应出请求的成功和失败:


# before
resp = opener(req)
self._build_response(resp)
success = True
# after     
resp = opener(req)
self._build_response(resp)
self.response.ok = True


同时response还支持bool判断


def __nonzero__(self):
  """Returns true if status_code is 'OK'."""
  return not self.error


__nonzero__为python的魔法函数


object.__nonzero__(self)
    Called to implement truth value testing and the built-in operation bool();
    should return False or True, or their integer equivalents 0 or 1. When this
    method is not defined, __len__() is called, if it is defined, and the object
    is considered true if its result is nonzero. If a class defines neither
    __len__() nor __nonzero__(), all its instances are considered true.


详细链接: docs.python.org/2/reference…


v0.2.4 改进request类



v0.2.4主要工作是改进Request:


0.2.4 (2011-02-15)
++++++++++++++++++
* Python 2.5 Support
* PyPy-c v1.4 Support
* Auto-Authentication tests
* Improved Request object constructor


request改进了构造函数,支持更多参数:


class Request(object):
    def __init__(self, url=None, headers=dict(), files=None, method=None,
                 params=dict(), data=dict(), auth=None, cookiejar=None):
        self.url = url
        self.headers = headers
        self.files = files
        self.method = method
        self.params = params
        self.data = data
        self.response = Response()
        self.auth = auth
        self.cookiejar = cookiejar
        self.sent = False


这样使用的时候:


# before
r = Request()
r.method = 'HEAD'
r.url = url
# return response object
r.params = params
r.headers = headers
r.cookiejar = cookies
r.auth = _detect_auth(url, auth)
r.send()
# after
r = Request(method='HEAD', url=url, params=params, headers=headers,
                cookiejar=cookies, auth=_detect_auth(url, auth))
r.send()


另外重用认证信息,对于同一个url使用缓存中的url


def add_autoauth(url, authobject):
    global AUTOAUTHS
    AUTOAUTHS.append((url, authobject))
def _detect_auth(url, auth):
    return _get_autoauth(url) if not auth else auth
def _get_autoauth(url):
    for (autoauth_url, auth) in AUTOAUTHS:
        if autoauth_url in url: 
            return auth
    return None


我们可以思考,认证按照url进行处理并不太好,按照domain可能更合适。


小结


通过v0.2.0~v0.2.4四个版本的源码历史对比法阅读,我们可以知道下面2个功能的实现逻辑:


  • 文件上传的功能就是将本地文件读取到流中再通过socket发送出去


  • cookie是在http头中的一个特殊序列化的字典


同时也了解了Reqeust和Response两个关键对象的一些优化方法,提高代码可读性的同时,也增加代码稳定性。


历史对比的同时,我们还可以发现一些有趣的题外细节。比如在2011-02-14~2011-02-15日两天,作者Kenneth Reitz完成了4个小版本的提交,果然是单身狗的快乐。


比较一下代码第一个版本的API:


>>> import requests
>>> r = requests.get('http://google.com')
>>> r.status_code
    401


下面是现在的API:


>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200


可以看到长达10年的时间内,作者最开始设计的API基本没有变化,这个还是挺厉害的。


小技巧



应该避免使用硬编码的魔法数字,比如在v0.2.1中的get_body_size:


def get_body_size(params, boundary):
    size = sum(p.get_size(boundary) for p in MultipartParam.from_params(params))
    return size + len(boundary) + 6


这里的数字6就比较难以理解。


没错,反例也是技巧 :)


参考链接




目录
相关文章
|
存储 消息中间件 RocketMQ
DLedger —基于 raft 协议的 commitlog 存储库
尊敬的阿里云用户: 您好!为方便您试用开源 RocketMQ 客户端访问阿里云MQ,我们申请了专门的优惠券,优惠券可以直接抵扣金额。请填写下您公司账号信息,点击上图,了解更多哦。 一、DLedger引入目的 在 RocketMQ 4.5 版本之前,RocketMQ 只有 Master/Slave 一种部署方式,一组 broker 中有一个 Master ,有零到多个 Slave,Slave 通过同步复制或异步复制的方式去同步 Master 数据。
13322 99
|
8月前
|
算法 安全 数据安全/隐私保护
基于AES的遥感图像加密算法matlab仿真
本程序基于MATLAB 2022a实现,采用AES算法对遥感图像进行加密与解密。主要步骤包括:将彩色图像灰度化并重置大小为256×256像素,通过AES的字节替换、行移位、列混合及轮密钥加等操作完成加密,随后进行解密并验证图像质量(如PSNR值)。实验结果展示了原图、加密图和解密图,分析了图像直方图、相关性及熵的变化,确保加密安全性与解密后图像质量。该方法适用于保护遥感图像中的敏感信息,在军事、环境监测等领域具有重要应用价值。
357 35
|
搜索推荐 算法 小程序
基于Java协同过滤算法的电影推荐系统设计和实现(源码+LW+调试文档+讲解等)
基于Java协同过滤算法的电影推荐系统设计和实现(源码+LW+调试文档+讲解等)
|
安全 前端开发 Windows
Windows Electron 应用更新的原理是什么?揭秘 NsisUpdater
本文介绍了 Electron 应用在 Windows 中的更新原理,重点分析了 `NsisUpdater` 类的实现。该类利用 NSIS 脚本,通过初始化、检查更新、下载更新、验证签名和安装更新等步骤,确保应用的更新过程安全可靠。核心功能包括差异下载、签名验证和管理员权限处理,确保更新高效且安全。
371 4
Windows Electron 应用更新的原理是什么?揭秘 NsisUpdater
|
监控 Java 开发者
监控堆外JVisualVM工具
监控堆外JVisualVM工具
280 2
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
523 4
|
存储 Unix Linux
揭秘Linux硬件组成:从内核魔法到设备树桥梁,打造你的超级系统,让你的Linux之旅畅通无阻,震撼体验来袭!
【8月更文挑战第5天】Linux作为顶级开源操作系统,凭借其强大的功能和灵活的架构,在众多领域大放异彩。本文首先概述了Linux的四大核心组件:内核、Shell、文件系统及应用程序,并深入探讨了内核的功能模块,如存储、CPU及进程管理等。接着介绍了设备树(Device Tree),它是连接硬件与内核的桥梁,通过DTS/DTB文件描述硬件信息,实现了跨平台兼容。此外,还简要介绍了Linux如何通过本地总线高效管理硬件资源,并阐述了文件系统与磁盘管理机制。通过这些内容,读者可以全面了解Linux的硬件组成及其核心技术。
233 3
|
Web App开发 JavaScript 前端开发
Node.js | 从前端到全栈的必经之路
深入浅出Node.js,最适合前端开发人员进入全栈时学习的服务端语言,以JavaScript为基础,使前端人员能够平滑过渡到全栈,赶快来认识一下Node.js吧!
Node.js | 从前端到全栈的必经之路
|
消息中间件 监控 关系型数据库
实时计算 Flink版产品使用问题之运行后,怎么进行监控和报警
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
使用Pattern.compile进行正则表达式匹配
使用Pattern.compile进行正则表达式匹配