这几天在看《从零开始学python网络爬虫》中的模拟浏览器篇,对其中的爬取好友说说比较感兴趣,不过书中只是爬取每个好友第一页说说,因此我稍微改进了下(发书名是尊重作者,不过个人认为这本书讲得比较浅,不求甚解)。
先大致说一下我遇到的坑。首先,如果想要看别人的说说,是必须要登录的(使用cookie应该也可以);然后,可能没有权限访问好友空间;最后则是获取下一页链接并点击前还要注意可能没有下一页了。
本次使用的是selenium和Chrome,书中使用的是PlantomJS,为什么不使用这个呢?看下图:
上图说的是PhantomJS已经废弃了,不建议使用,可以选用Chrome或FireFox浏览器的无界面模式。记得前几天逛csdn时有人说一开始只有PhantomJS支持selenium的,自从其他大公司支持后,老伙伴PhantomJS就没落了。。。
1.获取QQ通讯列表
QQ邮箱中可以导出好友邮箱。打开QQ邮箱中的通讯录:
之后的“工具”|“导出联系人”,选择以csv格式导出:
之后下载该csv即可。该文件字段大致如下:
我们这里主要用到的是“姓名”和“电子邮件”。
2.大致流程
因为有了QQ邮箱的存在,所以我们可以通过QQ邮箱来反推得到QQ号,不过需要注意的是,并不是所有的电子邮箱都是QQ邮箱。分析一下QQ邮箱的特征:
QQ号@qq.com
嗯~,qq号都是数字,那么可以使用正则表达式来提取出QQ号,具体实现如下:
def get_qq_numbers(filename):
'''
从csv文件中获取正确的qq号
并yield
'''
#正则匹配出qq号
pattern = re.compile('(.*?)@qq\.com')
fp = open(filename, 'r')
reader = csv.DictReader(fp)
#获取每个用户的电子邮箱
for row in reader:
name = row['姓名']
email = row['电子邮件']
qqNumber = re.search(pattern, email)
if qqNumber:
yield qqNumber.group(1), name
else:
logging.warning('该邮箱不是QQ号: %s %s' % (name, email))
fp.close()
get_qq_number()是读取刚才导出的csv文件,然后获取“电子邮件”中的值,并使用正则表达式进行提取,如果提起成功,则表示该邮箱是QQ邮箱,那么得到的一般情况下是qq号。
要导入的包如下:
import csv
import re
import logging
import json
import selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import config
logging.basicConfig(level=logging.WARNING,#控制台打印的日志级别
filename='new.log',
filemode='w',
format=
'%(asctime)s: %(message)s'
)
options = webdriver.ChromeOptions()
#无图模式
prefs = {
'profile.default_content_setting_values': {
'images': 2
}
}
options.add_experimental_option('prefs', prefs)
#浏览器隐藏
options.add_argument('--headless')
browser = webdriver.Chrome(chrome_options = options)
wait = WebDriverWait(browser, 5)
browser.implicitly_wait(3)
首先配置了logging,捕捉WARNING及以上并写入到new.log文件。
之后设置Chrome不显示图片,并且隐藏。
最后创建了一个浏览器,并同时设置了隐式等待和显示等待,个人认为两者区别不大,当没有获得到对应的节点时都会抛出异常;不过显示等待相对比较灵活,可以选择等待条件。
先看一下总调用(额,主函数?):
if __name__ == '__main__':
#限定爬取个数
start = 1
end = 5
notes = []
for qq, name in get_qq_numbers('./QQmail.csv'):
L = {}
L['qq'] = qq
L['name'] = name
L['notes'] = []
print(f'crawling {qq} {name}')
for data in get_info(qq, name, 3):
#pprint.pprint(data)
L['notes'].append(data)
if len(L['notes']) != 0:
notes.append(L)
#限制爬取数目
start += 1
if start > end:
break
#写入文件
fp = open('notes.json', 'w', encoding = 'utf-8')
fp.write(json.dumps(notes, indent = 2, ensure_ascii = False))
fp.close()
browser.close()
这里面限定了爬取的个数,可以根据要求自行修改。上述代码的逻辑大致如下:
获取csv文件中QQ号和对应的昵称;然后get_info()来获取好友的若干页说说(这里指定为3页,不指定则爬取所有说说);接着判断L中的notes列表是否为空,当该好友的空间没有权限访问时,get_info()返回None,因此需要判断是否为空,不为空则添加。最后把所有的信息写入文件即可。
3.访问QQ空间
前面的代码中并没有涉及到HTML,而接下来的get_info中大部分要和HTML打交道。首先,下面的函数来判断是否已经登录:
def is_logining(browser):
'''
判断是否需要登录
@param browser webdriver的一个实例
@return True 表示在登录中
'''
try:
browser.find_element(By.ID, 'login_frame')
ret = False
except:
ret = True
return ret
login_frame为https://i.qq.com/中的一个标签的id名,因此可以判断这个标签是否存在来判断是否需要登录,注意这里用的是隐式等待,在前面已经调用过下面这一句:
browser.implicitly_wait(3)
这个函数的说明如下:
从上面可知,在一次会话中只需要被调用一次即可(我看的那本书里面调用了这个函数多次),调用后应该会对之后的每一次非显式等待查找生效。
之后是登录函数:
def login(browser, username, passwd):
'''
QQ登录
@param browser 浏览器 webdriver的一个实例
@param username 登录账号
@param passwd 登录密码
'''
browser.switch_to.frame('login_frame')
switcher = browser.find_element(By.ID, 'switcher_plogin')
switcher.click()
#输入账号和密码
user_input = browser.find_element(By.ID, 'u')
user_input.clear()
user_input.send_keys(username)
passwd_input = browser.find_element(By.ID, 'p')
passwd_input.clear()
passwd_input.send_keys(passwd)
login_btn = browser.find_element(By.ID, 'login_button')
login_btn.click()
login负责输入账号和密码,然后点击登录。
上面两个函数都是要在get_info中使用。
def get_info(qq, name = None, maxPage = None):
'''
传入qq号,爬取其说说
'''
url = f'https://user.qzone.qq.com/{qq}/311'
browser.get(url)
#是否已经登录
logining = is_logining(browser)
#登录
if logining == False:
login(browser, config.username, config.passwd)
#是否允许访问
try:
browser.find_element(By.ID, 'QM_OwnerInfo_Icon')
permission = True
except:
permission = False
#允许访问 第一次需要切换frame
if permission == True:
browser.switch_to.frame('app_canvas_frame')
首先根据QQ号码生成url,之后访问该链接,然后判断是否需要登录;最后则通过查看标签的方式来判断是否有权限来访问QQ空间,另外用户名和密码存入了config.py这个文件中,自行修改即可。
#循环获取说说
index = 1
while permission == True and (maxPage == None or index <= maxPage):
#获取下一页按钮
try:
next_btn = wait.until(EC.presence_of_element_located( \
(By.CSS_SELECTOR, f'#pager_next_{index - 1}')))
#获取失败,没有下一页
except selenium.common.exceptions.TimeoutException as e:
#退出循环条件
maxPage = index
#获取说说内容和发表时间
contents = browser.find_elements(By.CSS_SELECTOR, '.content')
times = browser.find_elements(By.CSS_SELECTOR, '.c_tx.c_tx3.goDetail')
print('crawling page', index)
for content, tim in zip(contents, times):
data = {
'time' : tim.text,
'content': content.text,
}
yield data
#下一页
index += 1
if index <= maxPage:
next_btn.click()
#不可访问页面
if permission == False:
logging.warning('该用户不允许你访问 %s %s' % (name, qq))
print('该用户不允许你访问 %s %s' % (name, qq))
接下来的循环中就是获取该页面的所有说说和发表日期。首先,先显式等待获取到下一页按钮,如果超时,则表示没有下一页,此时获取该页面的说说后退出即可;内容和时间都是通过find_elements方法获取到的,它无法做到像xpath、beautiful soup那样灵活,如果想要获取每个说说下的评论的话,就需要修改成使用xpath等来解析页面。之后返回每个说说和对应的时间。