Chapter 08 使用Crawl插件来抓取数据
crawl
插件是一个爬虫工具,它对 requests 库进行了简单的包装,并支持同步和Gevent并发两种抓取方式。相对于Scrapy,它的优势是使用起来更加简单,你不必去继承一些莫名其妙的基类和满足好多约定才能抓取数据,但同时它的功能会相对弱一些,尤其是在抓取内容的解析方面,crawl
不提供类似XPath解析之类的辅助,这些工作需要你亲自动手,不过这些事情对于Python来说是小菜一碟。
同步抓取
我们使用crawl
来完成一个小需求,通过豆瓣读书的API找到所有东野圭吾的作品,把这些作品的基本信息输出表格并且把封面大图保存到一个指定的目录,来看一下如何使用crawl
完成这项工作:
# coding: utf-8
import shutil
from girlfriend.plugin.crawl import Req
from girlfriend.workflow.gfworkflow import Job
from girlfriend.data.table import TableWrapper, Title
logger = None
logger_level = "info"
def workflow(options):
work_units = (
# crawl
Job(
name="crawl",
plugin="crawl",
args={
"start_req": [
Req("GET", "https://api.douban.com/v2/book/search",
params={"q": u"东野圭吾"},
parser=_search_result_parser),
]
}
),
# print_table
Job(
name="print_table",
plugin="print_table",
args=["all_books"]
),
)
return work_units
def _search_result_parser(ctx, response, queue):
search_result = response.json()
all_books = []
def _save_image_parser(ctx, response, queue):
if response.status_code == 200:
with open("covers/{}".format(response.url.split("/")[-1]),
'wb') as f:
response.raw.decode_content = True
shutil.copyfileobj(response.raw, f)
for book in search_result["books"]:
all_books.append((book["title"], book["publisher"], book["pubdate"]))
large_image = book["images"]["large"]
queue.append(Req("GET", large_image, parser=_save_image_parser,
stream=True))
ctx["all_books"] = TableWrapper(
name=u"东野圭吾相关的书籍",
titles=(
Title("title", u"标题"),
Title("publisher", u"出版商"),
Title("pubdate", u"出版日期")
)
)(all_books)
crawl
Job接受了一个start_req参数,这个参数接受一个列表作为爬虫的起始点,每个起始点为一个Req对象,Req对象除了第一个参数为必须的请求方法之外,其余的参数都跟requests
库中get函数是一致的,比如这里用到了params,你还可以使用headers自定义请求头。但parser参数是crawl
插件自带的参数,这个参数用于处理返回的response对象,我们从response中获取书籍列表,并将其大图地址的请求放入到queue中,这样crawl会去依次消费queue中的Req对象。
并发抓取
crawl可以使用Gevent来提升抓取效率,使用方法很简单,只要声明一下协程池的大小即可:
# coding: utf-8
"""
Docs goes here
"""
import shutil
from girlfriend.plugin.crawl import Req
from girlfriend.workflow.gfworkflow import Job
from girlfriend.data.table import TableWrapper, Title
logger = None
logger_level = "info"
def workflow(options):
work_units = (
# crawl
Job(
name="crawl",
plugin="crawl",
args={
"start_req": [
Req("GET", "https://api.douban.com/v2/book/search",
params={"q": u"东野圭吾"},
parser=_search_result_parser),
],
"pool_size": 100 # 对!就是这个参数!
}
),
# print_table
Job(
name="print_table",
plugin="print_table",
args=["all_books"]
),
)
return work_units
def _search_result_parser(ctx, response, queue):
search_result = response.json()
all_books = []
def _save_image_parser(ctx, response, queue):
if response.status_code == 200:
with open("covers/{}".format(response.url.split("/")[-1]),
'wb') as f:
response.raw.decode_content = True
shutil.copyfileobj(response.raw, f)
for book in search_result["books"]:
all_books.append((book["title"], book["publisher"], book["pubdate"]))
large_image = book["images"]["large"]
queue.put(Req("GET", large_image, parser=_save_image_parser,
stream=True))
ctx["all_books"] = TableWrapper(
name=u"东野圭吾相关的书籍",
titles=(
Title("title", u"标题"),
Title("publisher", u"出版商"),
Title("pubdate", u"出版日期")
)
)(all_books)
然后运行:
gf_workflow -m workflow.py --gevent-patch all
看下相对于上次抓取,这次抓取是否有了效率上的提升?
除了使用pool_size让crawl自己构建协程池,还可以使用已经构建好的协程池,只需要把池对象传递给pool
参数即可,pool_size
和pool
只能指定一个。
另外要注意的是,并发抓取的时候,队列不是使用的append方法,而是使用了put方法,这是因为同步抓取的时候队列使用的是list对象,而并发抓取的时候,队列是使用的gevent.queue.Queue对象。
其它
对于编写爬虫,瓶颈往往不在自身的资源,而在于对方的antispam策略,因为crawl插件基本算作是对requests库的封装,requests本身足够强大,在crawl中你依旧可以使用requests中的方法去修改请求header或添加代理来防止爬虫被Spam掉。另外crawl还提供了一个简单的sleep参数,允许你每次抓取后暂停一次,防止因为抓取频率过快而被封杀。