最近朋友拜托我写一个实时爬虫,提醒她某人是否更新消息,作为一个爬虫小白,上网搜了一下资料,三天完成代码和部署,以下作记录贴,堤防遇到的坑,如果大家有更好的想法也欢迎提出!
整体思路
鉴于只是简单的应用,所以没有花费很多时间在图像识别上,直接调用的图鉴提供的接口进行识别,模拟人手抖动避免检测,利用虾推啥来进行微信消息提醒,其他就是一般爬虫的过程,获取网页html,使用BeautifulSoup进行html解析。
爬虫整体代码
# -*- coding: utf-8 -*-
"""
Created on Sat Aug 27 20:31:11 2022
@author: JShawn
"""
import time, random
from PIL import Image
import requests, json, base64
from selenium import webdriver
from selenium.webdriver.common.by import By
from urllib.request import urlretrieve
from selenium.webdriver.common.action_chains import ActionChains
#爬取数据使用到的库
import requests #虾推啥使用库
import bs4 #网页解析
from bs4 import BeautifulSoup
import re #正则表达式,文字匹配
import urllib #制定url
import sqlite3 #SQL操作
class Douban():
def __init__(self):
self.url = "https://www.douban.com/"
# 这里配置驱动参数,如增加代理和UA信息
opt = webdriver.ChromeOptions()
# 增加代理和UA信息
# opt.add_argument('--proxy-server=http://223.96.90.216:8085')
opt.add_argument('--no-sandbox')
opt.add_argument('--disable-dev-shm-usage')
opt.add_argument("--remote-debugging-port=9222") # this
opt.add_argument('--headless') #浏览器不提供可视化页面. linux下如果系统不支持可视化不加这条会启动失败
opt.add_argument('--disable-gpu')
opt.add_argument("window-size=1024,768")
opt.add_argument(
'--user-agent=' + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36")
# 创建webdriver实例
self.driver = webdriver.Chrome("/usr/bin/chromedriver/chromedriver",chrome_options=opt)#该处需要填chromedriver绝对路径才可以运行!
def base64_api(self, img):
# 图鉴官方提供的接口,这里稍微修改了下,因为自己使用就直接把账号密码放进去了
with open(img, 'rb') as f:
base64_data = base64.b64encode(f.read())
b64 = base64_data.decode()
data = {"username": "******", "password": '******', "typeid": 33, "image": b64}
result = json.loads(requests.post("http://api.ttshitu.com/predict", json=data).text)
if result['success']:
return result["data"]["result"]
else:
return result["message"]
return ""
def input_username_password(self):
# 实现访问主页,并输入用户名和密码
self.driver.get(self.url)
time.sleep(1)
self.driver.save_screenshot("0.jpg")
iframe = self.driver.find_element(By.TAG_NAME, 'iframe') # 主代码在iframe里面,要先切进去
self.driver.switch_to.frame(iframe) # 切到内层
time.sleep(0.5)
self.driver.find_element(By.CLASS_NAME, 'account-tab-account').click() # 模拟鼠标点击
time.sleep(0.2)
self.driver.find_element(By.ID, 'username').send_keys('******') # 模拟键盘输入
time.sleep(0.1)
self.driver.find_element(By.ID, 'password').send_keys('******') # 模拟键盘输入
time.sleep(0.2)
self.driver.find_element(By.CSS_SELECTOR, '.btn-account').click()
def get_img(self):
"""
获取验证码原图,并获得偏移量和偏移
:return:
"""
time.sleep(1)
iframe = self.driver.find_element(By.TAG_NAME, 'iframe') # 验证码仍然代码在iframe里面,要先切进去
self.driver.switch_to.frame(iframe) # 切到内层
time.sleep(0.5)
# 获取验证码图片
style_str = self.driver.find_element(By.ID, 'slideBg').get_attribute('style')
# 获取样式字符串,将该字符串转化为字典
style_dict = {i.split(':')[0]: i.split(':')[-1] for i in style_str.split('; ')}
# 组装图片地址
src = "https://t.captcha.qq.com" + style_dict['background-image'][6:-2]
# print(src)
# 将图片下载到本地
urlretrieve(src, 'douban_img1.png')
# 返回网页图片的宽和高
return (eval(style_dict['width'][1:-2]), eval(style_dict['height'][1:-3]))
def get_distance(self, a, b):
# 实现图片缩放
captcha = Image.open('douban_img1.png')
x_scale = captcha.size[0] / a
y_scale = captcha.size[1] / b
(x, y) = captcha.size
x_resize = int(x / x_scale)
y_resize = int(y / y_scale)
"""
Image.NEAREST :低质量
Image.BILINEAR:双线性
Image.BICUBIC :三次样条插值
Image.ANTIALIAS:高质量
"""
img = captcha.resize((x_resize, y_resize), Image.ANTIALIAS)
img.save('douban_img2.png')
time.sleep(0.5)
# 使用云打码获取x轴偏移量
distance = self.base64_api(img='douban_img2.png')
return distance
def get_track(self, distance):
"""
计算滑块的移动轨迹
"""
# 通过观察发现滑块并不是从0开始移动,有一个初始值18.6161
a = int((eval(distance) - 18.6161)/4)
# 构造每次的滑动参数
# 注意这里的负数作用:1.为了模拟人手,2.以上计算很多取了约数,最终结果存在误差,但误差往往在一定范围内,可以使用这些数值简单调整。
track = [a, -2.1, a, -1.8, a, -1.5, a]
return track
def shake_mouse(self):
"""
模拟人手释放鼠标抖动
"""
ActionChains(self.driver).move_by_offset(xoffset=-0.4, yoffset=0).perform()
ActionChains(self.driver).move_by_offset(xoffset=0.6, yoffset=0).perform()
ActionChains(self.driver).move_by_offset(xoffset=-1.1, yoffset=0).perform()
ActionChains(self.driver).move_by_offset(xoffset=0.9, yoffset=0).perform()
def operate_slider(self, track):
# 定位到拖动按钮
slider_bt = self.driver.find_element(By.CLASS_NAME, 'tc-slider-normal')
# 点击拖动按钮不放
ActionChains(self.driver).click_and_hold(slider_bt).perform()
# 按正向轨迹移动
# move_by_offset函数是会延续上一步的结束的地方开始移动
for i in track:
ActionChains(self.driver).move_by_offset(xoffset=i+0.3, yoffset=0).perform()
self.shake_mouse() # 模拟人手抖动
time.sleep(random.random() / 100) # 每移动一次随机停顿0-1/100秒之间骗过了极验,通过率很高
time.sleep(random.random())
# 松开滑块按钮
ActionChains(self.driver).release().perform()
time.sleep(4)
def login(self):
"""
实现主要的登陆逻辑
:param account:账号
:param password: 密码
:return:
"""
self.input_username_password()
time.sleep(0.5)
# 下载图片,并获取网页图片的宽高
a, b = self.get_img()
# 计算滑块的移动轨迹,开始拖动滑块移动
print('a:'+str(a))
print('b:'+str(b))
self.operate_slider(self.get_track(self.get_distance(a, b)))
# 输出登陆之后的cookies
# print(self.driver.get_cookies())
time.sleep(0.5)
self.driver.save_screenshot("douban.jpg")
def __del__(self):
"""
调用内建的稀构方法,在程序退出的时候自动调用
类似的还可以在文件打开的时候调用close,数据库链接的断开
"""
print("del函数调用")
self.driver.close()
#初始化数据库,传入数据库的路径,若存在连接,否则建立数据库
def init_db(self,dbpath):
sql = '''
create table if not exists 'notice_info'
(
id text,
content text,
href text,
date text
)
'''
connect = sqlite3.connect(dbpath) # 打开或创建数据库
c = connect.cursor() # 获取游标
c.execute(sql) # 执行SQL语句
connect.commit() # 提交事务
connect.close() # 关闭
# 得到一个URL的网页内容,并进行异常捕捉
def askURL(self,url):
head = { # 用户代理,告诉服务器我们可以接收什么水平的内容
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/'
'104.0.0.0 Safari/537.36'
}
request = requests.get(url, cookies=self.regroup_cookies(),headers=head) # 生成请求
#已经确定request.content可以返回html内容
return request.content
# 提取网页的最新广播,主要使用bs4
def dataExtarct(self,html):
bs = BeautifulSoup(html, "html.parser") # 解析文档,作为解析器
#日志id
id_list=bs.find_all('div',attrs={'class':'status-item'})
#日志链接
href_list=bs.find_all('div',attrs={'class':'hd'})
#日志内容
content_list=bs.find_all('div',attrs={'class':'status-saying'})
date_list=bs.find_all('span',attrs={'class':'created_at'})
#
try:
content=content_list[0].find('p').text
except:
content="无文字"
mydata = {
"href": href_list[0]['data-status-url'],
"id": id_list[0]['data-sid'],
"content": content,
"date": date_list[0]['title']
}
return mydata
def insert_data(self,mydata, connect):
sql = "insert into notice_info(id, content, href, date) values('" + mydata["id"] + "', '" + mydata["content"] + "', '" + mydata["href"] + "', '" + mydata["date"] + "');"
c = connect.cursor()
c.execute(sql)
connect.commit() # 提交事务
connect.close() # 关闭
#将信息存入数据库,如果此次搜索数据库无信息就提醒,有就忽略
def save_data(self,mydata, dbpath):
sql = "select * from notice_info where id = '" + mydata["id"] + "';"
print(sql)
connect = sqlite3.connect(dbpath) # 打开或创建数据库
c = connect.cursor() # 获取游标
cursor = c.execute(sql) # 执行SQL语句
result = 0
print("数据库"+str(cursor))
for row in cursor:
print(row)
result += 1
if result == 0:
self.insert_data(mydata, connect)
return 1
else:
return 0
# 利用吓推啥,将最新消息推送到微信上
# 主要是获取微信的token
def sendMessage(self,token, mydata):
baseurl = "http://wx.xtuis.cn/"
url = baseurl + token + ".send"
data = {
"text": '******',
"desp": '内容:' + mydata["content"] + '<br>' \
'更新时间:' +mydata["date"]
}
print(requests.post(url, data=data))
#爬取个人主页信息,并与数据库进行比对
def track_information(self):
#数据库地址
dbpath="douban.db"
#微信在虾推啥上的token
token="******"
self.init_db(dbpath)
url="https://www.douban.com/people/******/statuses"
# print(self.askURL(url))
mydata=self.dataExtarct(self.askURL(url))
flag=self.save_data(mydata,dbpath)
print(flag)
if flag==1 :
self.sendMessage(token,mydata)
#登录后获取的cookies需要重新组合
def regroup_cookies(self):
cookies = self.driver.get_cookies() # list
cookie_dict = {}
for item in cookies:
cookie_dict[item['name']] = item['value']
print(cookie_dict)
return cookie_dict
if __name__ == "__main__":
douban = Douban() # 实例化
douban.login() # 之后调用登陆方法
douban.track_information()#爬取个人主页信息
Centos部署
选择服务器
因为不需要太多内存和CPU,我在腾讯云的优惠专区,购买了一个最便宜的轻量应用服务器,选择了centos7的系统
安装finalshell
finalshell是一个SSH连接工具,可以连接到自己的云服务器,通过终端输入命令的方式执行操作,并且对于文件的增删改查十分方便。 下面给出了下载地址:
Windows版下载地址
Mac版,Linux版安装及教程
首先点击左上角的文件夹图标,打开连接管理器,再按下图点击第一个按钮。
新建的连接框中填入图中的信息,即公网IP,用户名和密码,名称可自定义。
上传爬虫和依赖库安装
服务器在之前已经安装了必要的环境,但爬虫所用到的一些第三方库还没有安装,因此需要手动安装需要的库。通过输入以下指令,即可安装爬虫所需要的第三方库。注意该版本需要python3.+进行编译,在腾讯云服务器中自带python3编译器,但在下载依赖库时,需要注意命令全部都为pip3
Linux下安装第三方库的命令,注意为pip3
pip3 install beautifulsoup4
pip3 install pysqlite3
sudo pip3 install selenium
下面还有比较复杂的库,我会贴出之前的教程,大家遇到对应的问题可以去参考一下
Centos安装Pillow(PIL)
https://blog.csdn.net/lbxoqy/article/details/121008085
Centos7安装chrome和chromedriver
一定要安装对应版本的chrome和chromedriver!而且在代码中要使用chromedriver的绝对路径,否则会找不到驱动!
https://blog.csdn.net/zulien/article/details/84990436
Tips
我在运行的时候在使用del稀构方法关闭chromedriver一直有问题,报错:
ImportError: sys.meta_path is None, Python is likely shutting down
如果遇到这样的错误,close()替换成quit()或者将quit()替换成close()
定时任务
在经过以上步骤后爬虫已经能够成功在服务器上运行,但未实现实时执行的要求。所谓实时,就是使用定时器每隔一段时间执行一次python脚本,这里用到了Linux系统的crontab指令,即定时任务。
crontab教程
在本次部署的步骤:
1.vim /ect/crontab 进入一般模式,即上图所示
2.按i进入编辑模式后,将光标移动到下方的波浪号处
3.黏贴已经写好的命令 */30 * * * * root /home/lighthouse/douban/task.sh 这里需要给出脚本文件的绝对路径和用户名,我的用户为root
4.按esc退出编辑模式,再输入 :wq! 强制保存文件并退出
5.输入 /etc/init.d/cron restart 重启程序 ,若不存在该指令,也可以输入
/sbin/service crond restart 重启服务
shell脚本:
#!/bin/sh
. /etc/profile
. ~/.bash_profile
python3 /home/lighthouse/douban/douban.py
killall -9 chrome
因为我稀构方法里无论写driver.close()还是driver.quit()都没办法关闭chromedriver,这样会影响之后爬虫的运行,所以在脚本中使用了killall -9 chrome来杀死所有chrome进程(很糙的办法,如果有更好的方法欢迎补充!)。另外,在使用crontab定时运行shell脚本的时候遇到了无法正常执行的问题,参考了下面的文章:https://magician.blog.csdn.net/article/details/84394809
在#!/bin/sh后面添加了两行代码,restart crontab之后终于运行成功了!