上一篇一共提到了四个模块,这一篇我们来实现它们
- 请求模块
- uid 解析模块
- 数据爬取模块
- 数据保存模块
一、请求模块
分析:
- 随机选择 user-agent:可以预先设置一个保存了许多 user-agent的数组,然后用 random库从数组中随机选取一个 user-agent
- 设置代理:使用 **kwargs参数直接传递给 request模块
- 预处理:抛弃预处理,直接返回一个 xpath对象
随机选择 ua,将下面这段代码单独放到一个文件中(user-agent太多了╯︿╰):
#file random_user_agent.py
#-*- coding: utf-8 -*
import random
def randomUserAgent():
USER_AGENTS = [
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
"Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
... ...
]
return random.choice(USER_AGENTS)
请求模块主体:
接受的参数:url, **kwargs
先检查 kwargs里是否有 headers,没有的话使用默认的 headers
第二步,用 url和 kwargs和 headers(如果有的话)发起 request请求(默认 get)
代理和一些其他的设置直接通过 kwargs传递给 requests请求
第三步,用 etree.HTML()
处理 requests的响应
第四步,返回处理后的结果
代码如下(前面讲得很详细了,我就没打注释了):
#-*- coding: utf-8 -*
import requests
from lxml import etree
from random_user_agent import randomUserAgent
def getResponse(url, **kwargs):
if 'headers' not in kwargs:
kwargs['headers'] = {
'User-Agent': randomUserAgent(),
}
r = requests.get(url, **kwargs)
dom = etree.HTML(r.text)
return dom
二、uid解析模块
分析:
- 自动去重:从 uid模块移除,uid模块只负责返回 uid,去重的工作交给爬取模块
- uid生成器:使用 yield
- 无限爬取:通过递归的方式将第一次爬取的信息作为参数再传递给 uid解析模块
接受参数示例:
start_users = [{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}]
- uid:用户 uid
- follow_num:用户关注数量
- fans_num:用户粉丝数量
- article_num:用户文章数量
刚开始爬取时的种子用户,为了方便只挑了一个用户,实际爬取时应该是一个由多个用户组成的数组。
yield返回示例,与接受的参数一致:
{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}
代码如下:
def getUserUids(start_users):
#保存本次爬取的用户
next_users = []
#爬取 start_users里每个用户的所有关注对象的 uid
for user in start_users:
uid = user['uid']
follow_num = user['follow_num']
#如果 follow_num可以整除每次请求返回的 uid数量,max_page为 int(follow_num / 9),否则为 int(follow_num / 9)+1, 这里 PER_NUM为 9
max_page = int(follow_num / PER_NUM) if (follow_num % PER_NUM) == 0 else int(follow_num / PER_NUM)+1
following_urls = ['https://www.jianshu.com/users/{}/following?page={}'.format(uid, i)
for i in range(1, max_page+1)]
for following_url in following_urls:
dom = getResponse(following_url)
items = dom.xpath('//ul/li//div[@class="info"]')
for item in items:
user = {}
try:
user['uid'] = item.xpath('./a/@href')[0].split('/')[2]
user['follow_num'] = int(item.xpath('./div/span[1]/text()')[0].replace('关注','').strip())
user['fans_num'] = int(item.xpath('./div/span[2]/text()')[0].replace('粉丝', '').strip())
user['article_num'] = int(item.xpath('./div/span[3]/text()')[0].replace('文章','').strip())
next_users.append(user)
yield user
except ValueError:
pass
#递归 将本次的爬取结果作为参数再传递给 getUserUids()
next_user_uids = getUserUids(next_users)
#实现无限爬取
for user in next_user_uids:
yield user
这样当我们调用 getUserUids()
时,就得到了一个可以无限生成 uid的生成器,使用方法如下:
start_users = [{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}]
uids = getUserUids(start_users)
for uid in uids:
print(uid)
理论上来说,上面这段代码会一直在你的控制台上打印 uid,直到打印完绝大部分简书用户或者你选择停止运行
三、数据爬取模块
数据爬取模块可以直接复用之前的代码
分析:
- 去重:用一个 seen数组保存已经爬取过的 uid,每次爬取之前先判断 uid是否在 seen数组内
将之前的代码整合为一个模块:
def getArticleInfo(user):
uid = user['uid']
article_num = user['article_num']
#这里 PER_NUM为 9
max_page = int(article_num / PER_NUM) if (article_num % PER_NUM) == 0 else int(article_num / PER_NUM)+1
article_urls = ['https://www.jianshu.com/u/{}?order_by=shared_at&page={}'.format(uid, i) for i in
range(1, max_page+1)]
details = []
for article_url in article_urls:
dom = getResponse(article_url)
items = dom.xpath('//ul[@class="note-list"]/li')
for item in items:
# 对每个 li标签再提取
details_xpath = {
'link': './div/a/@href',
'title': './div/a/text()',
'read_num': './/div[@class="meta"]/a[1]/text()',
'comment_num': './/div[@class="meta"]/a[2]/text()',
'heart_num': './/div[@class="meta"]/span[1]/text()',
}
key_and_path = details_xpath.items()
detail = {}
for key, path in key_and_path:
detail[key] = ''.join(item.xpath(path)).strip()
#将数字转换为整数
for key in ['read_num', 'comment_num', 'heart_num']:
detail[key] = int(detail[key])
details.append(detail)
#返回爬取结果
return details
语句:
int(article_num / PER_NUM) if (article_num % PER_NUM) == 0 else int(article_num / PER_NUM)+1
使用了 python三目表达式 if else
使用方法:
seen = []
start_users = [{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}]
users = getUserUids(start_users)
for user in users:
if user['uid'] not in seen:
seen.append(user['uid'])
info = getArticleInfo(user)
四、数据保存模块
分析:
- 接受一个字典列表:使用 csv库的
DictWriter.writerows()
方法 - 自动判断文件是否已存在,选择合适的模块打开文件:用 os库的
os.path.isfile(filepath)
来判断 - 将数据保存模块定义为一个类,这样方便对文件的管理
代码如下:
class simplifiedCsv:
def __init__(self, filepath):
self.file, self.writer = self.openFile(filepath)
def __del__(self):
self.file.close()
def openFile(self, filepath):
if os.path.isfile(filepath):
file = open(filepath, 'a', encoding='utf-8', newline='')
writer = csv.DictWriter(file, ['link', 'title', 'read_num', 'comment_num', 'heart_num'])
return file, writer
else:
file = open(filepath, 'w', encoding='utf-8', newline='')
writer = csv.DictWriter(file, ['link', 'title', 'read_num', 'comment_num', 'heart_num'])
writer.writeheader()
return file, writer
def writerows(self, data_list):
self.writer.writerows(data_list)
使用方法:
seen = []
start_users = [{'uid': 'a3ea268aeb60', 'follow_num': 525, 'fans_num': 2521, 'article_num': 118}]
users = getUserUids(start_users)
writer = simplifiedCsv('data.csv')
for user in users:
if user['uid'] not in seen:
seen.append(user['uid'])
info = getArticleInfo(user)
writer.writerows(info)
以上就是我们上一篇讲过的所有模块的实现,至于断点续爬我们下一篇单独讲
这一次的代码版本为 v1.0
代码在 GitHub上的链接:version_1_simple_struct_all.py
下载后可以直接用 python运行(前提是安装好了所需的库)
程序停止后会在当前目录下生成一个 data.csv的文件
我试运行了十分钟左右,爬取了大概 1万 4千条数据,大家也可以下载源码自己测试一下,也算是完成了第一个小小目标,结果截图:
最后,觉得不错的话,记得关注、点赞、评论哦(❤ ω ❤)
网友评论