roshinntou 发表于 2021-3-16 17:39

威客平台新订单邮件通知系统收尾+数据库存储、参数预处理防止注入

本帖最后由 roshinntou 于 2021-3-16 17:45 编辑

第一章:针对萌新思路分享:基于urllib.request的威客平台订单发现推送爬虫(一)第二章:python基础入门IO文件读写操作与邮件发送

感谢@huangdc1987朋友的提醒,完整代码除了展示出来之外,还会提供下载,第一章的word文档也会提供附件下载

下面这个code.zip是上一期的代码,解压密码52pojie


下面这个第一章word是第一章内容的word文档,解压密码52pojie


之前我们已经可以做到执行代码如果发现最新的需求给我们的邮箱里发送邮件了。

最后关于代码,还有两部分完善:
1、把读取到的信息保存进数据库,方便未来分析、使用
2、彻底完善代码,让代码可以自己持续运行,同时对多个分类进行扫描

关于Python操作数据库,主流的数据库一般是MySQL、Oracle、SQLServer这三个,Python都可以进行操作,我自己对Oracle比较精通,不过考虑到安装使用的易用性,还是MySQL比较方便。

我这边会用php study 和 CentOS的宝塔面板自带的phpMyAdmin来介绍这方面的操作。毕竟是B/S架构,各位也不会因为使用sqlYog或者Navicat时因为版本、破解和汉化的差异导致无法完全理解。

数据库真要讲的话,单独写20章都写不完,不过类似性能优化、GI、ASM、RAC、Data Guard 我们一般学习使用的机会很少。这里就只简单介绍一下基础的读写操作。

环境的安装,php study的话大多数windows直接下载安装就行,php study 小皮官网www.xp.cn,linux的话在宝塔官网上找一下下载的yum命令就可以安装,不过服务器配置一整套下来比较麻烦。之后我专门做一期简单的环境搭建的介绍。

现在默认你已经装好phpMyadmin,这时候我们需要针对性的建立数据库,并且建立相关密码。

小皮这边:


新建数据库,自己设定数据库DBName,自己设置登陆数据库的UserName,自己设置登陆数据库的PassWord
这三个要记好。

宝塔这边一样的,新建数据库,只不过他的密码会自动生成一个,你可以修改,不过因为密码可以点击复制,所以一般都会使用生成的密码,比较安全。



另外需要注意的是数据库访问权限的问题,宝塔的截图下面我框住的部分,小皮也有



这个的意思是,告诉数据库服务允许哪些地方的访问
本地服务器:指 localhost或者本地服务器、本机访问,一般为了数据库安全都是只允许本地登陆的
所有人:指任何IP都可以远程登陆访问数据库
指定ip:指只有被指定的IP端或单个ip地址才能访问

创建好数据库之后,就可以登陆phpmyadmin了。

小皮是通过首页-数据库工具-phpMyAdmin打开的


宝塔面板是通过数据库-找到需要打开的数据-点击管理打开的



之后就是创建表、列的过程,首先是主键,主键是数据的ID,用来区分查找每一条数据,必须是int的数字类型,同时需要标记为主键,选择自增长(选择自增长之后,录入的时候不需要录入ID,只需要录入其他的东西,ID会根据现在数据库的情况自动填写)



之后一次录入其他的信息,我只创建了标题、url、发布人信息、需求类型(现在只有设计一种,未来可以继续添加)



数据类型我选择的是varchar,这个一种保存小批量数据,后期查询比较块的数据储存类型,我没有记录订单详情,如果是文字比较长的话,可能选择text格式比较好,
选择varchar后需要指定长度,如果最后保存的数据长度超出现在指定的值可能会引起数据库报错。

最后完成数据库的创建



各位注意这里有一个sql按钮。


点击之后是phpMyAdmin的sql输入框,可以直接执行sql语句,也可以快速生成一些sql案例:



右边的是咱们刚刚创建的字段,双击之后可以在光标处快出打出这个字段,不需要我们来手写了。

下面的一排按钮是快速创建SQL常用语句的按钮。

主要的几个,从左到右分别是:
1、SELECT *:点击之后生成的语句是:SELECT * FROM `task` WHERE 1   这是查询数据的SQL语句,这个里边 *代表全部,表示一次查询这个表里的所有列。
2、SELECT   : 点击后生成的语句是:SELECT `id`, `title`, `user`, `url`, `type` FROM `task` WHERE 1   ,这是查询数据的SQL语句,它是按照列明显示查询结果的意思,如果只需要查询标题、链接,那么把多余的删除,只留下SELECT `title`, `url` FROM `task` WHERE 1这样的就行。
3、INSERT: 点击之后生成的语句是:INSERT INTO `task`(`id`, `title`, `user`, `url`, `type`) VALUES (,,,,),这个是向数据库插入数据的SQL语句,前边的(`id`, `title`, `user`, `url`, `type`) 是列明,可以只填写几个(只要剩下的列允许值为空就行)然后后边的VALUES括号里根据位置顺序,与前边的列名一一对应,后边的值会保存进前边的列里边。
4、UPDATE :点击之后生成的语句是:UPDATE `task` SET `id`=,`title`=,`user`=,`url`=,`type`= WHERE 1 这个是更新,修改数据用的SQL语句,不像是INSERT,UPDATE是键值对的形式展示的,前边是列名,直接等于后边的值。
5、DELETE:点击之后生成的语句是:DELETE FROM `task` WHERE 1这个是将数据暂时移出这张表的意思,他没有列名了,只有表名和WHERE,

那么现在需要说明的是 WHERE 的这个意思。

刚刚的查询、插入、修改、删除都有这个WHERE,它是判断筛选的意思,如果我想要找到类型是设计的需求,只需要给WHERE后边加:DELETE FROM `task` WHERE `type` ='设计',这样就可以暂时移除所有类型为设计的需求了。

类似的操作也有:

WHERE ‘id’ = 1   :查找ID是1的数据
WHERE ’title‘ = '需要设计订单'   :精准查找标题是:“需要设计订单”的需求数据
WHERE ‘title’ LIKE '%设计%': 模糊查询标题里包含设计两个字的需求数据, % 百分号是通配符,代表任意个任意字符
WHERE ’title‘LIKE '设计%': 模糊查询标题里以设计两个字为开始的需求数据, 前边的%去掉后,表示开头一定是设计两个字
WHERE ‘title’LIKE '_ _设计': 下划线通配符,表示 一个任意字符,那么这里的意思就是,找到标题是 XX设计的需求数据
WHERE ‘title’LIKE '设计%': []方括号内可以是多个字符文字,括号只占一位,所以这里是以 "A" 或 "B" 或 "C" +设计开头的数据,比如A设计公司威龙,B设计领域专家,C设计研究院
WHERE `id` BETWEEN 100 AND 200 :范围查询,查找ID在 100-200之间的数据

到现在为止,数据库的创建工作就完成了,剩下我们需要完成存入数据库的语句:

python是可以用%s占位的。所以现在的SQL语句就是:
INSERT INTO `task`(`title`, `user`, `url`, `type`) VALUES (%s,%s,%s,%s)

我们使用python操作数据库,需要在命令行模式下安装相关依赖包:

Pip3 install mysqlclient

pip还是pip3根据自己的环境来定。

然后代码需要导入依赖包

import MySQLdb

最后是保存的相关代码:


#构建dbconnect
db = MySQLdb.connect(host='localhost',user='用户名',passwd='密码',db='数据库名',charset='utf8')
#构建游标
cursor = db.cursor()
#sql语句
sql = "INSERT INTO `task`(`title`, `user`, `url`, `type`) VALUES (%s,%s,%s,%s)"
#字段预处理
param = (title,user,url,1)
#执行sql
n = cursor.execute(sql,param)
#提交数据库修改
db.commit()
#关闭数据库链接
db.close()


要注意两点:
1、字段一定要预处理
2、记得commit提交,和close保存过后关闭数据库链接


有很多使用python的同学喜欢这样子来处理sql语句的拼接:
比如我现在需要验证登陆,数据库名称叫:userlogin,需要验证用户名name和密码pwd

很多人会写成:

sql = "SELECT * FROM `userlogin` WHERE `name` = '%s' AND pwd ='%s'" %(a,c)

直接在string里写上%s然后,在这一行后边写 %() 把前边的%s参数写进去。这个是python的一种字符串拼接,正如其名,这个是一个字符串的拼接方法。不经过预处理会留下简单的注入漏洞。比如这样:


# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
import MySQLdb
import time

def sqlparam():
    db = MySQLdb.connect(host='localhost',user='root',passwd='pwd',db='db',charset='utf8')
    cursor = db.cursor()
    a = 'roshinntou'
    b = "======"
    c = ''=======''
    sql = "SELECT * FROM `userlogin` WHERE `name` = %s AND pwd =%s"
    param = (a,c)
    cursor = db.cursor()
    cursor.execute(sql,param)
    results = cursor.fetchall()
    rowCount = len(results)
    print(rowCount)
    for row in results:
      name = row
      pwd = row
      print(name)

def sql(a,b,c):
    db = MySQLdb.connect(host='localhost',user='root',passwd='98g0tn13',db='zbj_info',charset='utf8')
    cursor = db.cursor()
   
    sql = "SELECT * FROM `userlogin` WHERE `name` = '%s' AND pwd ='%s'" %(a,b)

    print(sql)
    cursor = db.cursor()
    cursor.execute(sql)
    results = cursor.fetchall()
    rowCount = len(results)
    print(rowCount)
    for row in results:
      name = row
      pwd = row
      print(name)


if __name__ == '__main__':
    a = 'roshinntou'
    b = "===="
    c = '====='
    sql(a,b,c)


main方法这里,a是账号,bc都是注入参数,网上很多人说python的单双引号可以很好的避免注入问题。

我这里b变量是以开发人员以双引号保存字符串的;c变量是开发人员以单引号保存变量。

执行了两次,都成功读取到信息,相当于成功登陆,密码分别是b和c,单双引号都可以实现1=1,但是使用预处理就不会出现问题。

虽然,开发过程中理论上我们应该在前端就处理注入的问题,但是毕竟不可能每个前端都是那么放心可靠的对吧?
爬取网页的时候,遇到心黑的 打断字符串里包含一段注入脚本,1=1后边还有删表删库的命令那就GG了。

文章发布出现493拦截,不知道是不是注入的命令出发的。我删了主要的注入代码再试试。。。
完整代码在附件里


果然,删了注入代码就能正常发布了=_=可能是我这边的注入代码本身提交文章的时候出现问题,被拦截了。


这个是注入的py文件和sql表导出文件各位可以自己多尝试几次


那么我又改了一下密码,列表那里只获取需求URL,然后根据URL判断,如果是新需求,重新使用URL获取网页的信息,提取标题、发布人的信息,然后保存进数据库:
# -*- coding: utf-8 -*-
"""
Created on Tue Mar 16 15:16:12 2021

@author: roshinntou
"""


from lxml import etree
import urllib.request
import re
import os
import smtplib
from email.header import Header
from email.mime.text import MIMEText


SMTP_host = "smtp.163.com"         # SMTP服务器
mail_user = "@163.com"    # 用户名
from_passwd = ""   # 授权密码,非登录密码
from_account = '@163.com' # 发件人邮箱
toAccoutList = '@163.com'


    #获取HTML代码的方法
    #传入参数url是网址字符串
    #返回参数是结果字符串
def getHtmlCode(url):
    #创建对象
    req_one = urllib.request.Request(url)
    #header信息
    req_one.add_header('User-Agent', 'Mozilla/6.0')
    #加入 try catch模块防止request报错导致结束程序
    try:
      #注意!timeout参数如果设置了,有可能出现XX秒内没有执行完毕报错
      res_one = urllib.request.urlopen(req_one,timeout=5)
      #内容重编码后复制给变量
      htmlcode = res_one.read().decode('utf-8')
      #关闭urlopen方法
      res_one.close()
      #把结果返回
      return htmlcode
    except:
      print("报错IP问题无法获得")
      return "error"

    #通过XPath获取网页信息的方法
    #传入参数url是网址字符串
    #无返回参数
def getNewTaskUrlByXPath(code):
    #构建eTree
    html = etree.HTML(code)
    #通过XPath获取标题所在<a>标签的属性
    name = html.xpath('/html/body/div/div/div/div/div/div/h3/a')
    url = name.get('href')
    return url

def getDescByUrl(url):
    descCode = getHtmlCode(url)
    html = etree.HTML(descCode)
    title = html.xpath('/html/body/div/div/div/div/div/div/div/div/h1')
    user = html.xpath('/html/body/div/div/div/div/div/div/div/span')   
    title = title.text
    user = user.text   
    print(title)
    print(user)
    return title,user




    #通过字符串获取网页信息的方法
    #传入参数url是网址字符串
    #无返回参数
def getInfoByKeyWord(code):
    startTarget = '<div class="task_class_list_li_box">'
    #先找到列表所在的DIV层,使用index方法
    taskStartIndex = code.index(startTarget)+len(startTarget)
    taskEndIndex = code.index(startTarget,taskStartIndex,len(code))
    untreatedInfo = code
    taskUrl = re.search('https://task.epwk.com/\d*/', untreatedInfo) .group(0)
    taskTitle = re.search('title=".*?"',untreatedInfo).group(0).replace('title=','').replace('"','')
    print('通过字符串获取的最新的需求是:')
    print(taskUrl)
    print(taskTitle)
    return taskUrl,taskTitle

    #保存需求信息
    #传入参数taskInfo是需要保存的需求信息字符串
    #无返回参数
def saveTaskByOS(taskInfo):
    with open('设计最新需求.txt','w',encoding='utf-8') as file:
      file.write(taskInfo)
      print('保存成功!')
      
    #读取文件获取旧的最新需求信息
    #无参数
    #返回参数为获取的需求信息
def getOldTaskInfo():
    #读取文件需要先判断这个文件是否存在
    if os.path.exists('设计最新需求.txt'):
      #读取文件
      with open('设计最新需求.txt','r',encoding='utf-8') as fileread:
            old = fileread.read()
            print('旧需求是:')
            print(old)
            return old
    #如果文件不存在,则创建并将信息写入文件
    else:
      with open('设计最新需求.txt','w',encoding='utf-8') as file:
            file.write('null')
            return 'null'
            print('未发现文件,新建成功')
            

def send_email(SMTP_host, from_account, from_passwd, to_account, subject, content):
    message = MIMEText(content, 'HTML', 'utf-8')# 内容, 格式, 编码
    message['Subject'] = subject
    try:
      smtpObj = smtplib.SMTP_SSL(SMTP_host, 465)# 启用SSL发信, 端口一般是465
      smtpObj.login(from_account, from_passwd)# 登录验证
      smtpObj.sendmail(from_account, to_account, message.as_string())# 发送
      print("向用户"+to_account+"发送邮件成功!")
    except smtplib.SMTPException as e:
      print("向用户"+to_account+"发送邮件失败!")
      print(e)

def saveData(title,user,url):
    #构建dbconnect
    db = MySQLdb.connect(host='localhost',user='用户名',passwd='密码',db='数据库名',charset='utf8')
    #构建游标
    cursor = db.cursor()
    #sql语句
    sql = "INSERT INTO `task`(`title`, `user`, `url`, `type`) VALUES (%s,%s,%s,%s)"
    #字段预处理
    param = (title,user,url,1)
    #执行sql
    n = cursor.execute(sql,param)
    #提交数据库修改
    db.commit()
    #关闭数据库链接
    db.close()
   
if __name__ == '__main__':
    #获得html代码
    htmlCode = getHtmlCode("https://task.epwk.com/sj/?o=7")
    #从html代码里找到现在最新的需求
    url = getNewTaskUrlByXPath(htmlCode)
    #获取旧的需求
    oldInfo = getOldTaskInfo()
    #判断新需求是否为最新的需求
    if(url == oldInfo):
      print('未发现新需求')
    else:
      print('发现新需求:'+url)
      taskInfo = getDescByUrl(url)
      saveTaskByOS(url)
      print(url)
      msg='''
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta http-equiv="X-UA-Compatible" content="IE=edge">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Document</title>
            </head>
            <body>
            '''
   
      msg+='<a href="'+url+'"> <h1>'+taskInfo+'</h1> </a></a>'
      msg+="<p>需求标题:"+taskInfo+"</p>"
      msg+="<p>发 布 人:"+taskInfo+"</p>"
      msg+='''
            </body>
            </html>
            '''
      send_email(SMTP_host, from_account, from_passwd, toAccoutList,infoList,msg)
      saveData(taskInfo,taskInfo,url)

   



等各位填写了自己的邮箱信息,建立好数据库之后就可以正常使用了。

最后的一点是执行的问题。

连续执行很简单,只需要把现在的main方法里的代码提取出来形成一个新的方法,然后重新在main方法里加while循环就行

先导入time包:

import time

while True:
      try:
            startWork()
      except:
            pass
      time.sleep(round(random.uniform(1,4),2))


有两点需要注意的:
1、使用try catch块包裹主体方法,防止以外错误,这里except抛出异常时可以单独做成发一个邮件到你的邮箱提醒你。
2、time sleep这边是让程序暂停的一个方法,我这里使用了随机函数,time.sleep(round(random.uniform(1,4),2))这个里,主要是看.sleep后边的括号,round(random.uniform(1,4),2)是指,返回一个1-4之间的包含小数点后2位的一个小数。前边的(1,4)是,1-4之间,后边的2是小数点的位数

当然还可以更进一步:


while True:
      now = datetime.datetime.now()
      # print(now.hour, now.minute)
      if now.hour >= 9 and now.hour <=18:
            print(time.strftime('%Y-%m-%d %X',time.localtime()))
            startWork()
      # 每隔1小时检测一次
      time.sleep(3600)

这里是限制了如果时间在9-18点之间才执行,否则休息一小时。
需要提前导入random包:

import random
完整的代码和sql在这里:




到目前为止大体工作告一段落,只剩一些简单的优化和部署服务器了。

不过我仔细回顾了一下,好像讲解如何抓取这里还是比较简单的,设计的内容不多,之后我再找找别的网站给大家讲一下,如何通过目录列表页批量抓取未知数量的多条数据。

52changew 发表于 2021-3-16 22:22

看看; 好长; 谢谢分享!
页: [1]
查看完整版本: 威客平台新订单邮件通知系统收尾+数据库存储、参数预处理防止注入