一个简单好用的Python 异步爬虫, 你只要需要做两件事就能编写一个爬虫
- 发现新链接(提供一个匹配新链接正则表达式)
- 提供一个页面内容处理函数
aspider 是一个基于Python asyncio 开发的爬虫工具, 可以通过命令行使用, 不用写任何代码, 也可以调用api, 自定义具体爬取的规则.
aspider的设计原则是专注于爬取网页, 本身不包含如解析文本, 数据存储等内容, 这些都可以利用很多已有的工具框架自行加上. aspider 参考了
类似flask的路由装饰器的方法, 只需要定义一些方法, 每个方法用于爬取一个相应的正则路径.
author: gxtrobot
- 系统使用常用的生产者, 消费者模式, 生产者产生需要爬取的链接并加入队列, 消费者从队列获取链接进行爬取, 并解析其内容和链接, 调用相关的方法, 和用户自定义的方法, 符合要求的链接会加到队列, 这样只通过一个根路径就可以扩展到全站爬取
- 系统才用asyncio的并发协程, 每个并发协程同时担任消费者和生产者的指责, 通过一个queue(队列)进行交互
- 系统定义了一个路由(Router), 作为用户唯一需要使用的对象, 路由不需要新建, 系统只有一个Router对象, 直接获取就可以使用
- 用户所有自定义方法, 都通过路由提供的装饰器来实现, 例如route, 无需创建class或其他复杂的逻辑
To install use pip:
$ pip install aspider
Or clone the repo:
$ git clone https://github.com/gxtrobot/aspider.git
$ python setup.py install
aspider 安装完成后可以直接通过命令 aspider
使用, 或 python -m aspider
aspider --help
usage: aspider [-h] [--max_redirect N] [--max_tries N] [--max_tasks N]
[-p PROXY] [-i INCLUDE_REGEX] [-e EXCLUDE_REGEX] [-o OUTPUT]
[-c COUNT] [--nostrict] [--no_parse_links] [-v] [-q]
[roots [roots ...]]
An Asyncio Web crawler, version: 0.1.0
positional arguments:
roots 要爬取的网页路径(一般为跟路径), 可以有多个
optional arguments:
-h, --help 显示帮助信息
--max_tasks N 并发任务数, 默认为5个, 如需要可以提高
-p PROXY, --proxy PROXY
代理使用, 例如 http://localhost:1080
-i INCLUDE_REGEX, --include INCLUDE_REGEX
需要匹配爬取的网页路径正则, 匹配的网页链接将被加入爬取队列
-o OUTPUT, --output OUTPUT
输出的方法, 目前支持这几种[txt, json, stream], stream为命令行输出
-c COUNT, --count COUNT
限制爬取的数量, 如指定, 到限额后爬虫自动退出, 队列未完成的会取消
--no_parse_links 禁止解析链接, 指定后, 只爬取命令行给定的roots链接, 不会跟踪解
页面包含的链接
-v, --verbose debug 消息数据, 可以有多个v (-v , -vv)
-q, --quiet 静默输出, 只打印错误消息
- 豆瓣250电影抓取
运行以下命令, 将会爬取top250的10页html, 并存到json文件中
aspider https://movie.douban.com/top250 --include /top250.* --out json
不过这通常并没太大用处, 因为这样只是抓取完整的html, 只适合有时候需要抓取整个网页, 然后用其他工具处理, 或者不懂python 开发, 可以先用这个抓取网页, 然后用其他工具处理
保存的json文件是json line格式, 就是说每行是一个json对象, 读取时候要相应处理, 每读取一行, 将该行内容转为json对象, python 代码如下
def read_json_lines():
file = 'top250.json'
with open(file) as f:
for line in f:
d = json.loads(line)
print(d.keys()
- route(rule, verify_func=None, no_parse_links=False)
装饰器, 在用户自定义解析方法使用
Args:
rule: 处理path的正则表达式, 可以包含参数, 使用'<>'包住参数名就可以, 例如 '/page/<no>', 这样用户自定义方法可以接收no参数
verify_func: 可选, 用户路径验证方法, 如果提供可以自定义是否处理该路径, 方法签名为 verify_url(path), return True or False
no_parse_links: 可选, 是否处理该页面上的其他链接, 如果需要关闭解析该页面链接, 可以设置为True
用户被装饰方法定义如下:
@router.route('/page/<no>', verify_page_path)
def process_page(text, path, no):
Args:
text: 当前要解析页面的html代码, 可以使用其他框架如lxml, request_html, BeautifulSoup
path: 可选, 如果有该参数, 系统会自动传递该path参数供使用
其他参数, 装饰器的自定义参数
注意: 前两个参数为系统自动提供, 顺序不能错, 用户自定义参数在前两个之后
该模块提供download方法启动爬虫
- download(loop=None, extra_args=None):
Args:
loop - 用户提供asyncio loop对象, 一般不需要, 除非是在编写定时任务, 多线程使用, 这时候用户生成一个loop对象并传入
extra_args - 其他参数, aspider 命令行使用的参数可以用该dict对象传入
其中, 必须的是roots key, 对应为抓取跟路径, 是一个list
Returns:
stats - 抓取过程统计对象, 包含抓取的网页数量, 完成时间等数据
example:
options = {
'roots': ['https://movie.douban.com/top250']
}
stats = aspider.download(extra_args=options)
-
Stats 对象 该对象记录抓取过程的统计数据, 如抓取网页数量, 完成时间, 速度等
-
report() 该方法打印这次抓取的记录报告
- 先取得router对象
- 定义解析方法, 使用router.route装饰器, 提供一个参数为要解析的path正则表达式
- 定义一个main方法启动爬虫就可以
'''
例子: douban top 250 电影名单爬取
'''
from collections import namedtuple
from aspider.routeing import get_router
from aspider import aspider
from requests_html import HTML
Movie = namedtuple('Movie', ['rank', 'score', 'title'])
router = get_router()
root_url = 'https://movie.douban.com/top250'
movies_250 = []
@router.route('/top250\?start.+')
def process_page(text):
html = HTML(html=text)
item_css = '#content ol.grid_view > li'
items = html.find(item_css)
rank_css = 'em'
title_css = '.info span.title'
score_css = '.info .rating_num'
for item in items:
rank = int(item.find(rank_css, first=True).text)
title = item.find(title_css, first=True).text
score = float(item.find(score_css, first=True).text)
movies_250.append(Movie(rank, score, title))
def main():
options = {
'roots': [root_url]
}
stats = aspider.download(extra_args=options)
stats.report()
fname = 'top250.txt'
sorted_movies_250 = sorted(movies_250, key=lambda m: m.rank)
with open(fname, 'w') as f:
for movie in sorted_movies_250:
print(f'#{movie.rank:<10} {movie.score:<10.2f} - {movie.title}')
print(f'#{movie.rank:<10} {movie.score:<10.2f} - {movie.title}', file=f)
if __name__ == "__main__":
main()