公司业务使然,很久没使用scrapy了,最近复习一下框架知识,顺带体验一下强大的scrapy调度能力
简书的it类博客很多,开始只是获取it类三十个首页推荐的大v博主,再获取大v关注的博主,没想到程序运行起来就一直不停的跑,两张表三天左右时间,一张三百多万条,另一张三十多万条数据,感觉能把简书大部分博主的信息都拿下来,手动笑哭
程序在公司的测试服务器上跑,后来服务器公司用,程序就停掉了
![](https://img.haomeiwen.com/i12326114/2a220aa96fa379c2.png)
业务从0开始,创建scrapy项目:
scrapy新建一个爬虫项目,在终端敲如下命令即可:
scrapy startproject Jianshu
cd到Jianshu项目中,生成一个爬虫:
scrapy genspider jianshublog "www.jianshu.com"
这种方式生成的是常规爬虫
因为要从推荐的三十个博主中提取出关注者,所以我们使用scapy的crawlspider模式,生成crawlspider的shell命令如下:
scrapy genspider –t crawl jianshublog "www.jianshu.com"
项目的目录就用网图来展示一下吧:
![](https://img.haomeiwen.com/i12326114/45b0041713662ff9.png)
对照一下我们之前创建的项目:
![](https://img.haomeiwen.com/i12326114/ccf25be84bd15555.png)
核心要关注的是spiders目录下的jianshublog.py,主要的抓取逻辑都在这里,讲核心逻辑之前先把配置理一下,items里面定义要抓取的结构化数据类型,博主定义了两个结构化数据,分别对应简书作者信息以及简书作者的文章信息:
![](https://img.haomeiwen.com/i12326114/3c6dd39a175b9a03.png)
从当前页面中,提取博主信息、文章信息:
博主信息:作者、关注、简介、粉丝、文章数、主页链接、字数、收获喜欢
文章信息:标题、概要、文章链接、作者
分析好之后,就可以定义items文件啦:
class JianshuItem(scrapy.Item):
# user字段对应info字段的author
user = scrapy.Field()
articles = scrapy.Field()
current_page = scrapy.Field()
url = scrapy.Field()
article_list = scrapy.Field()
class JianshuUserInfo(scrapy.Item):
author = scrapy.Field()
url = scrapy.Field()
following = scrapy.Field()
followers = scrapy.Field()
article = scrapy.Field()
chars = scrapy.Field()
collection = scrapy.Field()
author_abstract = scrapy.Field()
middleware中做一些请求干预处理,比如加代理、随机请求头之类
pipelines管道中获取爬虫的结构化返回数据,然后入库或写文件或做其他处理
现在实现核心的spider部分:
简书首页,点击it互联网,点开推荐作者:
![](https://img.haomeiwen.com/i12326114/be0a28a2bee63493.png)
稍微抓包分析一下就可以找到url了:
https://www.jianshu.com/collections/14/side_list
返回的数据是json:
{"id":2509688,"slug":"12532d36e4da",
"nickname":"阿里云云栖社区",
"avatar_source":"http://upload.jianshu.io/users/upload_avatars/2509688/e55553f1-0544-4bde-ad4d-48bcfc01e856.png",
"collection_name":"@IT·互联网",
"total_likes_count":35058,
"total_wordage":3896361,
"is_following_user":true,
"is_followed_by_user":false}
再稍做分析一下,我们就可以通过slug字段,获取到作者的主页了:
简书作者主页的格式也是有规律的,很简单:
https://www.jianshu.com/users/.../following
把slug提取出来组装到上面的url模板就可以:
def get_start_urls():
url = 'https://www.jianshu.com/collections/14/side_list'
headers = 'Mozilla/5.0(Windows;U;WindowsNT6.1;en-us)' \
'AppleWebKit/534.50(KHTML,likeGecko)Version/5.1Safari/534.50'
headers = {'User-Agent': headers}
resp = requests.get(url, headers=headers)
json_con = json.loads(resp.content.decode())
recommended_users_list = json_con.get('recommended_users')
for con in recommended_users_list:
base_url = 'https://www.jianshu.com/users/{}/following'
part = con.get('slug')
if part:
url = base_url.format(part)
yield url
# ?page=4
page = 2
url += '?page={}'
has_content = [1]
while len(has_content) > 0:
resp = requests.get(url.format(page), headers=headers)
html = resp.content.decode()
has_content = etree.HTML(html).xpath('//div[@class="info"]/a[@class="name"]')
# print('获取start_urls中...', url.format(page))
yield url.format(page)
page += 1
上面代码使用requests库,请求出种子url,值得一提的是函数中使用了yield返回,所以这是一个特殊的函数:生成器
生成器是一种特殊的迭代器,可以把生成器当成可迭代对象来使用,以上就是种子url的提取
拿到种子url就可以使用scrapy来提取其中的内容啦,首先从作者主页提取作者关注的博主:
class JianshublogSpider(CrawlSpider):
name = 'jianshublog'
allowed_domains = ['jianshu.com']
start_urls = [url for url in get_start_urls()]
rules = (
# 获取关注列表中的简述作者
Rule(LinkExtractor(restrict_xpaths='//div[@class="info"]/a[@class="name"]'), callback='parse_item',
follow=False),
)
scrapy会自动请求start_url中的种子url,然后通过rules中的规则提取出新的url,交给callback来处理
所以callback的逻辑就很重要了:
在callback中,首先提取被关注者的个人信息,然后再提取被关注者的所有文章,最后再提取被关注者的关注者,到这里,整个程序就回稍微复杂一点咯,方法内有多个逻辑处理还有递归调用
scrapy是一个基于twisted框架的异步多任务框架,这样可以大大提升效率,经常使用就会知道,在spider中都是通过yield返回数据或下一个请求,这样整体就类似于一个生成器,通过调度模块实现高效并发请求:
def parse_item(self, response):
# 进入被关注者文章列表 提取文章 被关注本人信息 提取被关注者的关注
item = response.meta.get('item')
item = item or JianshuItem()
print(response.url, 'parse_item')
first = re.search(r"order_by=shared_at", response.url)
if not first:
# 提取关注本人信息
item1 = JianshuUserInfo()
item1["url"] = response.url
item1["author"] = response.xpath('//div[@class="title"]/a/text()').extract_first()
item1["following"] = response.xpath(
'//div[@class="meta-block"]/a[contains(@href,"following")]/p/text()').extract_first()
item1["followers"] = response.xpath(
'//div[@class="meta-block"]/a[contains(@href,"followers")]/p/text()').extract_first()
item1["article"] = response.xpath('//div[@class="meta-block"]/a/p/text()').extract()
if len(item1["article"]) == 3:
item1["article"] = int(item1["article"][-1])
else:
item1["article"] = 0
item1["chars"] = response.xpath('//div[@class="meta-block"]/p/text()').extract()
if len(item1["chars"]) == 2:
chars = item1["chars"]
item1["chars"] = chars[0]
item1["collection"] = chars[1]
else:
item1["chars"] = ''
item1["author_abstract"] = response.xpath('//div[@class="js-intro"]/text()').extract_first()
if item1["author_abstract"]:
item1["author_abstract"] = item1["author_abstract"][:500]
yield item1
item["url"] = response.url
item['articles'] = 0
item['article_list'] = []
item["user"] = item1["author"]
if item1["article"]:
# print('获取到文章数量为', item1["article"], response.url)
item['articles'] = item1["article"]
# 提取被关注者关注的所有人
slug = re.search(r'https://www.jianshu.com/u/(.*)', response.url)
url = 'https://www.jianshu.com/users/{}/following'
if slug:
slug = slug.group(1)
url = url.format(slug)
print('提取到被关注者的关注列表', url)
yield scrapy.Request(url,
callback=self.parse_following_list)
# 提取被关注者所有的文章
try:
if not item['articles']:
print('作者还没开始写文章.......................................', item["user"])
return
div_list = response.xpath('//div[@class="content"]')
# 文章列表
article_list = item['article_list']
item['user'] = response.xpath('//div[@class="title"]/a[@class="name"]/text()').extract_first()
for div in div_list:
inner = dict()
inner['article_title'] = div.xpath('./a[@class="title"]/text()').extract_first()
if inner['article_title']:
inner['article_title'] = inner['article_title'][:120]
inner['article_link'] = div.xpath('./a[@class="title"]/@href').extract_first()
inner['article_abstract'] = div.xpath('./p[@class="abstract"]/text()').extract_first()
if inner['article_abstract']:
inner['article_abstract'].strip()
if inner['article_title'] or inner['article_link'] or inner['article_abstract']:
article_list.append(inner)
except Exception as e:
logger.error(str(e))
# 每个页面9篇文章
if int(item['articles']) <= 9:
# print('当前博客页仅有{}篇文章'.format(item['articles']))
yield item
return
else:
current_page = item.get('current_page')
if not current_page:
item['current_page'] = 2
else:
item['current_page'] += 1
page = int(item['articles']/9)
if item['articles'] % 9:
page += 1
if item['current_page'] <= page:
url = item['url']
# page = 2
part = '?order_by=shared_at&page={}'
next_url = url + part
url = next_url.format(item['current_page'])
print('now request {}'.format(url))
yield scrapy.Request(url,
callback=self.parse_item,
meta={"item": deepcopy(item)}
)
else:
print('当前博客文章提取完毕', response.url)
yield item
return
parse_item就是主要实现逻辑,在作者主页刷新下一页的时候,简书后台会发起ajax请求,请求的url中会带有order_by=shared_at字符串,我们以此来判断本次请求是否为当前作者第一次调用parse_item方法,如果是第一次调用,就提取作者的个人信息然后返回给管道,接着获取当前作者关注的所有人,当前作者的关注页面在新的回调方法中实现,先略过
下一步提取当前作者的所有文章,首先通过之前获取的作者信息,如果作者没有文章,跳过此步,如果作者有文章,提取文章,并计算出作者文章占用的页数,保存的当前页数和总页数,依次把所有页面的文章提取出来
关注者页面包含作者所有的关注者,这里使用另一个callback来提取:
def parse_following_list(self, response):
item3 = response.meta.get('item')
# 判断是否是关注者列表的第一页
not_first = re.search(r'\?page=', response.url)
if not not_first:
item3 = dict()
item3['page'] = 2
url = response.url
url += '?page={}'
url = url.format(item3['page'])
# url中内容第一次调用默认请求查看下一页
yield scrapy.Request(url,
callback=self.parse_following_list,
meta={"item": deepcopy(item3)})
# 判断是否能从响应中取出关注者 如果可以继续请求下一个关注列表页 反之主url的关注列表提取完成
has_content = response.xpath('//div[@class="info"]/a[@class="name"]').extract()
if len(has_content) > 0 and not_first:
item3['page'] += 1
# https://www.jianshu.com/users/(.*?)/following?page=
url = re.search(r'/users/(.*?)/following', response.url)
if url:
part = url.group(1)
url = 'https://www.jianshu.com/users/{}/following?page='.format(part)
url += str(item3['page'])
print('提取关注者列表的下一页组装url成功', url, 'a'*50)
yield scrapy.Request(url,
callback=self.parse_following_list,
meta={"item": deepcopy(item3)})
else:
logger.error('组装url产生异常:{}'.format(response.url))
else:
# 当前方法任务完成退出
# print('当前被关注者的关注列表提取完毕', 'ok'*25)
return
# 关注列表工厂 提取出所有的被关注者
urls = response.xpath('//div[@class="info"]/a[@class="name"]/@href').extract()
urls = ['https://www.jianshu.com' + u for u in urls if urls]
# print(response.url, 'parse_following_list')
for url in urls:
# print('提取工厂提取到url列表再次调用item提取方法', url)
yield scrapy.Request(url,
callback=self.parse_item)
![](https://img.haomeiwen.com/i12326114/cd59d6917073289c.png)
parse_following_list方法首先判断,当前作者是否是第一次调用方法,如果是,则默认请求关注者列表的下一页(不管关注者列表页数是否大于一页)
接下来判断当前页面是否有内容(即关注者页面是否有被关注者)
如果有内容,继续递归调用本方法请求下一页
接下来提取出当前页面中的所有被关注者,再次组装url并调用parse_item方法,到这里整个程序的复杂性就体现出来了,两个callback都是生产者又同时也是消费者,他们彼此调用同时自身递归调用
数据通过字典在scrapy请求对象meta参数中传递,众所周知python中列表、字典都是通过引用赋值,为防止字典的引用在不同的方法中操作是产生混淆,这里每次传递字典的时候使用深拷贝,deepcopy可以解决引用传递的问题
返回的数据在管道中处理:
def get_con():
host = 'localhost'
pwd = '123'
return pymysql.connect(host=host, port=3306, db='jianshu', user='abc', password=pwd, charset='utf8mb4')
class JianshuPipeline(object):
def process_item(self, item, spider):
if isinstance(item, JianshuUserInfo):
item = dict(item)
author = item.get('author')
url = item.get('url')
following = item.get('following')
followers = item.get('followers')
article = item.get('article')
chars = item.get('chars')
collection = item.get('collection')
author_abstract = item.get('author_abstract')
con = get_con()
cur = con.cursor()
params = [author, url, following, followers, article, chars, collection, author_abstract]
# print(params)
insert_sql = 'insert jianshu_user_info values(0,%s,%s,%s,%s,%s,%s,%s,%s);'
res = cur.execute(insert_sql, params)
con.commit()
cur.close()
con.close()
return item
if isinstance(item, JianshuItem):
item = dict(item)
user = item.get('user')
articles = item.get('articles')
current_page = item.get('current_page')
url = item.get('url')
article_list = item.get('article_list')
print('item print', [user, articles, current_page, url, len(article_list)])
con = get_con()
cur = con.cursor()
id_sql = 'select id from jianshu_user_info where author=%s'
cur.execute(id_sql, [user])
rst = cur.fetchone()
id = rst[0]
for article in article_list:
abstract = article.get('article_abstract')
title = article.get('article_title')
link = article.get('article_link')
params = [user, title, link, abstract, id]
insert_sql = 'insert jianshu_user_article values(0,%s,%s,%s,%s,%s);'
cur.execute(insert_sql, params)
con.commit()
cur.close()
con.close()
return item
数据库采用mysql,创建库的时候注意使用'utf8mb4'编码,兼容一些特殊编码,根据返回的不同item插入不同的表中,注意jianshu_user_article表中有一个字段usr_id, 该字段表示文章作者信息的表id
以下是数据创建脚本:
create database jianshu charset=utf8mb4;
create table jianshu_user_article(
id int(16) unsigned auto_increment primary key not null,
author varchar(20),
title varchar(120),
link varchar(64),
abstract varchar(120),
usr_id int(16));
create table jianshu_user_info(
id int(16) unsigned auto_increment primary key not null,
author varchar(20),
url varchar(64),
following varchar(16),
followers varchar(16),
article varchar(16),
chars varchar(16),
collection varchar(16),
author_abstract varchar(500));
最后贴上settings部分配置:
CONCURRENT_REQUESTS = 16
DOWNLOADER_MIDDLEWARES = {
'jianshu.middlewares.ProxyMiddleware': 100
}
ITEM_PIPELINES = {
'jianshu.pipelines.JianshuPipeline': 300,
}
并发请求数量是默认的16个,中间件以及管道也要在设置中开启,爬这么多数据,User-Agent、代理也必须要加上
整个项目就完成啦
网友评论