Python爬虫爬取个人主页信息(拖拽验证码验证)+Linux部署

最近朋友拜托我写一个实时爬虫,提醒她某人是否更新消息,作为一个爬虫小白,上网搜了一下资料,三天完成代码和部署,以下作记录贴,堤防遇到的坑,如果大家有更好的想法也欢迎提出!

整体思路

鉴于只是简单的应用,所以没有花费很多时间在图像识别上,直接调用的图鉴提供的接口进行识别,模拟人手抖动避免检测,利用虾推啥来进行微信消息提醒,其他就是一般爬虫的过程,获取网页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之后终于运行成功了!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值