Browsed by
标签:Python

Scrapy 源码阅读(一):Twisted 基础和 Scrapy 数据流

Scrapy 源码阅读(一):Twisted 基础和 Scrapy 数据流

本文记录下自己看 Scrapy 源码的一点经验,没有涉及框架的方方面面,更多的是关注数据的流转以及代码的组织。如果你想深入框架的某个细节,那么这篇文字可以给你一个切入点。

阅读源码常规的步骤

  1. 准备好必须的基础知识
  2. 熟悉框架有哪些功能
  3. 看源码,了解代码是怎么组织的,最后按需关注特定部分的实现细节

我们一步一步来。

1 Twisted

Scrapy 基于 Twisted,所以除了要有一定的 Python 基础外,还要对 Twisted 有一些了解。

你可以将 Twisted 和 Asyncio 类比,它们都是为了支持协程而诞生的,只是前者比后者出现的更早。这 2 个技术实现的核心都是事件循环,当程序执行到某个耗时的 IO 操作时,程序的执行权限会被退回给事件循环,事件循环会检测其它准备就绪的协程,然后将执行权限交给它,当之前的协程 IO 操作完毕后,事件循环会将执行权限转给它,继续后面的操作。这样,就能在单线程内实现并发,作用和多线程类似,只是比多线程更轻量。事件循环在 Asyncio 中被叫做 event_loop,在 Twisted 中叫做 reactor。我们看一些简单的例子

阅读全文

聊聊 Scrapy 分布式的几种方案

聊聊 Scrapy 分布式的几种方案

Scrapy 是一款开源爬虫框架,但是官方对分布式支持并不多,本文聊聊几种简单可行的方案。

需要注意的是,本文的爬虫并非对几个网站的纵向爬取,而是爬取一批不同的地址。如果是其它类型的爬虫,可能不适用,但应该能让你多个思考方向。

网址分批处理

既然是一批不同的地址,那么最直接的方案就是把网址分几个部分,然后起几个进程同时处理就行了。

举个例子,我们要爬取的地址是放到 Mongo 中的,存储格式类似这样

{ "_id" : 1, "url" : "https://www.baidu.com" }
{ "_id" : 2, "url" : "https://www.google.com" }
{ "_id" : 3, "url" : "https://www.csdn.net" }
...

那我们就在编写爬虫的时候,接受 2 个参数(起始的地址主键和结束的主键),就能爬取指定的地址了。之后的工作,便是怎么分配地址了。

阅读全文

聊聊 SQLAlchemy 连接池中的连接失效问题

聊聊 SQLAlchemy 连接池中的连接失效问题

最近项目中事情比较多,也遇到了一些问题,其中有一个是关于连接池的,比较有意思,这里分享下。

一天早上,进入业务系统,点击了一个功能按钮,页面上突然弹出个 MySQL gone away 的错误,我擦,数据库挂了吗,上服务器一看正常的。又点击了一下,又报事务未正常关闭的错误,有点懵。当然,是在测试环境上 :)。仔细想了想,发现是连接池的问题,下面我重现下这个错误并说下自己的一些解决办法。

阅读全文

灵活使用 SQLAlchemy 中的 ORM 查询

灵活使用 SQLAlchemy 中的 ORM 查询

之前做查询一直觉得直接拼 SQL 比较方便,用了 SQLAlchemy 的 ORM 查询之后,发现也还可以,还提高了可读性。

这篇文章主要说说 SQLAlchemy 常用的 ORM 查询方式,偏实践。看了之后,对付开发中的查询需求,我觉得可以满足不少。

为方便说明,假设有如下数据

图书表 books

+----+--------+--------------------------+-------+
| id | cat_id | name                     | price |
+----+--------+--------------------------+-------+
|  1 |      1 | 生死疲劳                 | 40.40 |
|  2 |      1 | 皮囊                     | 31.80 |
|  3 |      2 | 半小时漫画中国史         | 33.60 |
|  4 |      2 | 耶路撒冷三千年           | 55.60 |
|  5 |      2 | 国家宝藏                 | 52.80 |
|  6 |      3 | 时间简史                 | 31.10 |
|  7 |      3 | 宇宙简史                 | 22.10 |
|  8 |      3 | 自然史                   | 26.10 |
|  9 |      3 | 人类简史                 | 40.80 |
| 10 |      3 | 万物简史                 | 33.20 |
+----+--------+--------------------------+-------+

分类表 categories

+----+--------------+
| id | name         |
+----+--------------+
|  1 | 文学         |
|  2 | 人文社科     |
|  3 | 科技         |
+----+--------------+

ORM 对象定义如下


注意:本文 Python 代码在以下环境测试通过

  • Python 3.6.0
  • PyMySQL 0.8.1
  • SQLAlchemy 1.2.8

# coding=utf-8

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Numeric
from sqlalchemy.orm import sessionmaker

Base = declarative_base()
engine = create_engine('mysql+pymysql://username:password'
                       '@127.0.0.1:3306/db_name?charset=utf8')
Session = sessionmaker(bind=engine)

session = Session()

def to_dict(self):
    return {c.name: getattr(self, c.name, None)
            for c in self.__table__.columns}
Base.to_dict = to_dict

class Book(Base):
    __tablename__ = 'books'

    id = Column(Integer, primary_key=True)
    cat_id = Column(Integer)
    name = Column('name', String(120))
    price = Column('price', Numeric)

class Category(Base):
    __tablename__ = 'categories'

    id = Column(Integer, primary_key=True)
    name = Column('name', String(30))

好了,下面进入正题。

1 根据主键获取记录

当我们获取图书的详情时,很容易用到。

book_id = 1
book = session.query(Book).get(book_id)
print(book and book.to_dict())

直接 get(primary_key) 就得到结果

{'id': 1, 'cat_id': 1, 'name': '生死疲劳',
 'price': Decimal('40.40')}

当然,这样也可以

book_id = 1
book = session.query(Book) \
    .filter(Book.id == book_id) \
    .first()
print(book and book.to_dict())

不过,还是前一种方式简洁一些。

阅读全文

说说我对 WSGI 的理解

说说我对 WSGI 的理解

先说下 WSGI 的表面意思,Web Server Gateway Interface 的缩写,即 Web 服务器网关接口。

之前不知道 WSGI 意思的伙伴,看了上面的解释后,我估计也还是不清楚,所以下面结合实际场景说明,先让大家有个大致的认识。最后我们再自己实现一个,加深对 WSGI 的理解。

我们现在使用 Python 编写 Web 应用,可以用比较流行的 Flask、Django 框架,也可以按自己的想法直接写一个。可选的服务器软件也特别多,比如常见的有 Apache、Nginx、IIS 等,除此外,也有很多小众的软件。但是,现在问题来了,我该怎么部署?在没有 WSGI 规范之前,一个服务器调度 Python 应用是用这种方式,另一款服务器使用的是那种方式,这样的话,编写出来的应用部署时只能选择局限的某个或某些服务器,达不到通用的效果。

注意:下文中的代码基于 Python 3.6 编写。

假如有这么一个服务器

wsgi/server.py

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    response = b"""
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(response)
    client_connection.close()

实现比较简单,就是监听 8080 端口,如果有请求在终端进行打印,并返回 Hello, World! 的响应。

终端中启动服务器

➜  wsgi python server.py
Serving HTTP on 0.0.0.0 port 8080 ...

再开一个终端,请求下

➜  ~ curl 127.0.0.1:8080
HTTP/1.1 200 OK

Hello, World!

说明服务器工作正常。

另外有一个 Web 应用

wsgi/app.py

# coding=utf-8

def simple_app():
    return b'Hello, World!\r\n'

现在要部署(也就是让这个整体跑起来),简单粗暴的做法就是在服务器里面直接调用 app 中相应的方法。就像这样

wsgi/server2.py

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    from app import simple_app
    response = 'HTTP/1.1 200 OK\r\n\r\n'
    response = response.encode('utf-8')
    response += simple_app()

    client_connection.sendall(response)
    client_connection.close()

运行脚本

注意:因为使用端口相同的缘故,请先关闭上次的脚本,然后再执行,不然会由于端口冲突而报错。

➜  wsgi python server2.py
Serving HTTP on 0.0.0.0 port 8080 ...

然后请求一下看看效果

➜  ~ curl 127.0.0.1:8080
Hello, World!

嗯,可以了。但是,上面的服务器和应用整体是跑起来了,那么我换一个服务器或者应用呢。由于服务器与应用之间怎么交互完全没有规范,比如服务器应该如何把请求信息传给应用,应用处理完毕后又怎么告诉服务器开始返回响应,如果都是各搞各的,服务器需要定制应用,应用也要定制服务器,这要一个应用能跑起来也太麻烦了点吧。

所以,WSGI 的出现就是为了解决上面的问题,它规定了服务器怎么把请求信息告诉给应用,应用怎么把执行情况回传给服务器,这样的话,服务器与应用都按一个标准办事,只要实现了这个标准,服务器与应用随意搭配就可以,灵活度大大提高。

阅读全文

搞清楚 Python 的迭代器、可迭代对象、生成器

搞清楚 Python 的迭代器、可迭代对象、生成器

很多伙伴对 Python 的迭代器、可迭代对象、生成器这几个概念有点搞不清楚,我来说说我的理解,希望对需要的朋友有所帮助。

1 迭代器协议

迭代器协议是核心,搞懂了这个,上面的几个概念也就很好理解了。

所谓迭代器协议,就是要求一个迭代器必须要实现如下两个方法

iterator.__iter__()
Return the iterator object itself.

iterator.__next__()
Return the next item from the container.

也就是说,一个对象只要支持上面两个方法,就是迭代器。__iter__() 需要返回迭代器本身,而 __next__() 需要返回下一个元素。

2 可迭代对象

知道了迭代器的概念,那可迭代对象又是啥呢?

这个更简单,只要对象实现了 __iter__() 方法,并且返回的是一个迭代器,那么这个对象就是可迭代对象。

比如我们常见的列表就是可迭代对象

>>> l = [1, 3, 5]
>>> iter(l)
<list_iterator object at 0x101a1d9e8>

使用 iter() 会调用对应的 __iter__() 方法,这里返回的是一个列表迭代器,所以说列表就是一个可迭代对象。

阅读全文