Spider爬虫笔记[更新中...]

本文详细介绍了Python网络爬虫的学习过程,包括使用urllib、requests库进行网络请求,处理cookie和代理,以及利用lxml和BeautifulSoup4进行数据解析。此外,还探讨了正则表达式的应用和数据存储到json、csv、mysql、MongoDB的方法。文章还涵盖了多线程爬虫、Selenium模拟浏览器、Scrapy框架的使用,并分享了动态网页和反反爬虫策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[前言]
本来是跟着老师一起学习,结果第一节课就学崩了,只能自己放面慢速度,开始了自学之路。希望自己可以坚持学习。以后不要这么懒惰!
望与诸位共勉,有不合理的地方,请指教。

一个不像目录的目录:

网络请求

  • urllib库
  • request库

数据解析

  • xpath语法和lxml模块
  • BeautifulSoup4(bs4)
  • re正则表达式

数据存储

  • json
  • csv
  • mysql
  • MongoDB

爬虫进阶

  • threading
  • queue
  • ajax
  • selenium
  • tesseract

Scrapy

网络请求

urllib

urllib库是python中最基本的一个网页请求库,可以模拟浏览器行为,向指定的服务器发送一个请求,并可以保存服务器返回的数据。

request下的urlopen()和urlretrieve()

urlopen()
from urllib import request
# urlopen() 打开一个网站
resp = request.urlopen('http://www.baidu.com')
# read()读取内容
print(resp.read())
# read()读取内容
# print(resp.read())   全文读取
# print(resp.readline())  读取单行
# print(resp.readlines())   读取多行
# print(resp.getcode())  读取状态码
def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
            *, cafile=None, capath=None, cadefault=False, context=None):
    

1.url: 请求的url

2.data:请求的data,如果设置了这个值,那么将变成post请求

3.返回值: 返回值是一个http:HTTPResponse对象,这个对象是一个类文件句柄对象。

类文件句柄()

有read(size),readline,以及getcode(获取状态码)等方法。

urlretrieve()

该函数可以方便的将一个文件保存到本地.

from urllib import request

# request.urlretrieve('http://baidu.com','baidu.html')
request.urlretrieve(
    'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1598854'
    '222513&di=30f4723181e431766e2fb7bf12e6fd74&imgtype=0&src=http%3A%2F%2Fimage.9game.cn%2F'
    '2020%2F8%2F30%2F172674335.jpg',
    '杨戬.jpg')
print('Already Done')

parse下的urlencode()和urldecode()

urlencode()

编码

from urllib import request, parse

# params = {
#     'name': '张三',
#     'age': 18,
#     'sex': '男'
# }
# # parse解析  成浏览器可读的方式
# result = parse.urlencode(params)
# print(result)

url = 'https://www.baidu.com/s'
params = {
    'wd':'刘德华'
}
qs = parse.urlencode(params)
print(qs)
# 拼接
url = url + '?' + qs
print(url)

resp = request.urlopen(url)
print(resp.read())
print('ok')
parse_qs

可以将编码后的url参数解码,举例如上

urlparse和urlsplit

对url分割。

from urllib import request, parse

url = 'http://www.baidu.com/s?wd=python&username=abc#1'

# 用变量接出解析结果
# result = parse.urlparse(url)
result = parse.urlsplit(url)
print(result)
# ParseResult(scheme='http', netloc='www.baidu.com', path='/s', params='', query='wd=python&username=abc', fragment='1')

# urlparse会将url解析成6个部分。 通过属性访问详细信息
# urlparse和urlsplit的区别:urlsplist没有params
print('scheme', result.scheme)   # 网络协议
print('netloc', result.netloc)   # 服务器位置
print('path', result.path)  # 网页文件在服务器中的位置
# print('params', result.params)   # 可选参数
print('query', result.query)   # &连接键值对
print('fragment', result.fragment)

"""
输出结果:
SplitResult(scheme='http', netloc='www.baidu.com', path='/s', query='wd=python&username=abc', fragment='1')
scheme http
netloc www.baidu.com
path /s
query wd=python&username=abc
fragment 1

"""
request:Request类

在请求时设置请求头(在爬取网页时,没有请求头会被浏览器识别出来)。

from urllib import request, parse

url = 'https://www.lagou.com/jobs/list_python?labelWords=&fromSearch=true&suginput='

# resp = request.urlopen(url)
# # 反爬机制 在不设置请求头的情况下,输出乱码
# print(resp.read())

headers = {
    'referer': 'https://www.lagou.com/jobs/list_python?labelWords=&fromSearch=true&suginput=',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',

}
# data 需要urlcode才可以传入
data = {
from urllib import request, parse

url = 'https://www.lagou.com/jobs/list_python?labelWords=&fromSearch=true&suginput='

# resp = request.urlopen(url)
# # 反爬机制 在不设置请求头的情况下,输出乱码
# print(resp.read())

headers = {
    'referer': 'https://www.lagou.com/jobs/list_python?labelWords=&fromSearch=true&suginput=',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',

}
# data 需要urlcode才可以传入
data = {
    'first': 'true',
    'pn': 1,
    'kd': 'python'
}

"""
在python3中,默认编码方式时unicode 需要转化成字节方式
Request类里面的参数
    def __init__(self, url, data=None, headers={},
                 origin_req_host=None, unverifiable=False,
                 method=None):
"""
req = request.Request(url, headers=headers, data=parse.urlencode(data).encode('utf-8'), method='POST')
resp = request.urlopen(req)
# decode 解码可查看结果
# request.urlretrieve(url, 'lagou.html')
print(resp.read().decode('utf-8'))

}

"""
在python3中,默认编码方式时unicode 需要转化成字节方式
Request类里面的参数
    def __init__(self, url, data=None, headers={},
                 origin_req_host=None, unverifiable=False,
                 method=None):
"""
req = request.Request(url, headers=headers, data=parse.urlencode(data).encode('utf-8'), method='POST')
resp = request.urlopen(req)
# decode 解码可查看结果
# request.urlretrieve(url, 'lagou.html')
print(resp.read().decode('utf-8'))
ProxyHandler处理器(代理设置)

在某天的某段时间内,同一IP访问次数(被访问网站通过流量统计和系统日志等记录)过多时,该IP可能会被禁止。设置代理服务器,及每隔一点时间换一个代理,就是IP地址被禁用,依旧可以换一个IP 继续爬取数据。

urllib中通过ProxyHandler来设置代理服务器

ProxyHandler处理器(代理):

1.代理的原理:在请求某个目的服务器前,先请求代理服务器,然后由代理服务器请求目的网站,代理服务器获取数据后转发到原请求的服务器

2.http://www.httpbin.org/get

http://www.httpbin.org/ip

查看一些参数

3.在代码中使用代理

from urllib import request

# 未使用代理
url = 'http://httpbin.org/ip'
# resp = request.urlopen(url)
# print(resp.read())


# 使用代理
"""
1.使用ProxyHandler,传入代理构建一个Handler
2.使用上面创建的Handler构建一个opener
3.使用opener发送一个请求
"""

handler = request.ProxyHandler({
    # 字典形式  k协议(当前代理支持的传输协议):v代理服务器的IP地址 代理服务器的端口
    'http': '1.196.177.199:9999'
})

opener = request.build_opener(handler)
resp = opener.open(url)
print(resp.read())

常用的代理:

代理云:http://www.dailiyun.com

快代理:http://www.kuaidaili.com

cookie

http请求时无状态的,即用户多次请求数据,服务器不能识别是同一用户发起的请求,

这是某位大佬在优快云发布的一篇关于cookie的介绍,可供参考

https://blog.youkuaiyun.com/zhangquan_zone/article/details/77627899

客户端将用户信息封装在cookie中,第一次在客户端向服务器请求数据时,将cookie封装在请求中发送给服务器,服务器接收请求返回时,将返回信息和cookie一起发向客户端,浏览器保存到本地,当用户第二次请求时,就会自动把上次请求存储的cookie数据自动的携带给服务器,服务器通过浏览器携带的信息,即可识别发起请求的用户,但cookie的存储很有限,不同的浏览器存储大小不超过4KB,因此cookie只能存储少量信息。

参数定义:

Set-cookie: NAME=VALUE Expires/Max-age=DATE Path=PATH Domain=DOMAIN_NAME SECURE
参数意义:
NAME: cookie的名字
VALUE: cookie的值。
Expires: cookie的过期时间。
Path: cookie作用的路径。
Domain: cookie作用的域名。
SECURE:是否只在htps协议下起作用

cookie作用:识别登录状态

使用cookie和HTTPCookiePrcoessor在代码中模拟登录
from urllib import request

"""
人人网登录页面 http://www.renren.com/
个人主页  http://renren.com/880151247/profile

"""
# 不使用cookie请求个人主页
dapeng_url = 'http://renren.com/880151247/profile'
# 定制请求头
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',
    'Cookie': 'anonymid=keia16m3-dsupce; depovince=JS; _r01_=1; taihe_bi_sdk_uid=c58c14fd617ac95e13470406d3be8343; wp_fold=0; JSESSIONID=abcmQC_PSWWIS4WPIVdrx; taihe_bi_sdk_session=51e967a4ad36088530d4d9617672af3b; ick_login=3b8dbfe8-0ec8-4b55-a1c1-a61b4caf3b9e; t=f9bf6882a2526d42d48400e3b9b868a08; societyguester=f9bf6882a2526d42d48400e3b9b868a08; id=975034228; xnsid=bc418c2b; jebecookies=2553464d-037d-4014-b1f9-13d3790af6b3|||||; ver=7.0; loginfrom=null'
}

req = request.Request(url=dapeng_url, headers=headers)
resp = request.urlopen(req)
print(resp.read().decode('utf-8'))
with open('dapeng.html', 'w', encoding='utf-8') as f:
    """
    write函数需要写入一个str的数据类型
    resp.read()读出来的是bytes类型
    bytes --> decode --> str
    str --> encode --> bytes
    """
    f.write(resp.read().decode('utf-8'))
# request.urlretrieve(dapeng_url, 'test.html')

http.cookiejar

该模块的主要类CookieJar、FileCookieJar、MozillaCookieJar、LWPCookieJar.

CookieJar:管理HTTP cookie值、存储HTTP请求生成的cookie,向传出的HTTP请求添加的cookie的对象。整个cookie都存储在内存中,对Cookie实例进行垃圾回收就的cookie也将丢失

FileCookieJar:用来创建 FileCookieJar实例,检索cookie信息并将cookie存储到文件中,filename是存储cookie的文件名,delayload为True时延迟访问文件,即只有在需要才读取文件或在文件中存储数据。

MozillaCookieJar:创建Mozilla浏览器cookie兼容的FileCookieJar实例

LWPCookieJar:从FileCookieJar派生

利用http.cookiejar和request.HTTPCookieProcessor登录人人网。

from urllib import request, parse
from http.cookiejar import CookieJar

"""
1.登录
1.1创建一个cookiejar对象
1.2使用cookie创建一个cookoeprocess对象
1.3使用上一步创建的handler创建一个opener
1.4 试用版opener发送登录的请求(人人网邮箱和密码)
"""

cookiejar = CookieJar()  # 动态获取cookie信息

handler = request.HTTPCookieProcessor(cookiejar)

opener = request.build_opener(handler)

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'
}

data = {
    'email': 'youemail@qq.com',
    'password': 'password'
}

login_url = 'http://www.renren.com/'
req = request.Request(url=login_url, data=parse.urlencode(data).encode('utf-8'), headers=headers)
opener.open(req)
# data获取的参数需要使用urlencode编码成浏览器课识别的格式,编码后变成了unicode(str),
# 此处的data需要的参数类型是bytes,需要encode()进行编码转成字节类型

dapeng_url = 'http://renren.com/880151247/profile'
req = request.Request(dapeng_url,headers=headers)
# 获取个人主页的时候,不必新建opener,使用之前的opener(已经包含了登录所需要的信息),
resp = opener.open(dapeng_url)

with open('renren.html', 'w', encoding='utf-8') as f:
    f.write(resp.read().decode('utf-8'))
    print('Aleady Done')

# 加强版
from urllib import request, parse
from http.cookiejar import CookieJar

"""
1.登录
1.1创建一个cookiejar对象
1.2使用cookie创建一个cookoeprocess对象
1.3使用上一步创建的handler创建一个opener
1.4 试用版opener发送登录的请求(人人网邮箱和密码)
"""
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'
}


def get_opener():
    cookiejar = CookieJar()  # 动态获取cookie信息

    handler = request.HTTPCookieProcessor(cookiejar)
    opener = request.build_opener(handler)
    return opener


def login_renren(opener):
    data = {
        'email': 'youemail@qq.com',
        'password': 'password'
    }

    login_url = 'http://www.renren.com/PLogin.do'
    req = request.Request(url=login_url, data=parse.urlencode(data).encode('utf-8'), headers=headers)
    opener.open(req)


# data获取的参数需要使用urlencode编码成浏览器课识别的格式,编码后变成了unicode(str),
# 此处的data需要的参数类型是bytes,需要encode()进行编码转成字节类型

def visit_profile(opener):
    dapeng_url = 'http://renren.com/880151247/profile'
    req = request.Request(dapeng_url, headers=headers)
    # 获取个人主页的时候,不必新建opener,使用之前的opener(已经包含了登录所需要的信息),
    resp = opener.open(dapeng_url)

    with open('renren.html', 'w', encoding='utf-8') as f:
        f.write(resp.read().decode('utf-8'))
        print('Aready Done')


if __name__ == '__main__':
    opener = get_opener()
    login_renren(opener)
    visit_profile(opener)

保存cookie到本地

保存cookie到本地,可以使用cookiejar的save方法,并需要指定一个文件名。

from urllib import request
from http.cookiejar import MozillaCookieJar

def test():
    # 创建 对象且显示保存文件的名称
    cookiejar = MozillaCookieJar('cookie.txt')
    # 加载已过期的cookie信息
    cookiejar.load(ignore_discard=True)
	# handler处理器
    handler = request.HTTPCookieProcessor(cookiejar)
    opener = request.build_opener(handler)

    # 向相关网页发起请求
    resp = opener.open('http://httpbin.org/cookies/set?course=abc')
    # 保存到本地
    for cookie in cookiejar:
        print(cookie)
    # cookiejar.save(ignore_discard=True)
    # ignore_discard=True 可保存即将过期的cookie信息
    # 本地存储的cookie信息重用
requests库

安装步骤

pip install requests

中文文档:https://requests.readthedocs.io/zh_CN/latest/user/quickstart.html

发送GET请求

1.最简单的发送get请求就是靠requests.get来调用

response = requests.get('http://www.baidu.com/')

2.添加headers和查询参数

如果想添加headers,可以传入headers参数来增加请求头中的headers的信息,可以利用params参数。

import requests
"""

response.content是直接在网络上面抓取的的数据,没有经过任何解码,所以
是一个bytes类型。在硬盘和网络上传输的数据都是字节类型
可使用response.content.decode('utf-8')进行手动解码


response.text 是str数据类型,将response.content进行解码的字符传,解码需要指定一个编码方式,
所以有时候会猜测错误,导致解码生成乱码。
"""
# response = requests.get('http://www.baidu.com')
# # 可能出现乱码 .text查看响应内容, 返回值为unicode
# print(response.text)
# print(type(response.text))
# print('*' * 80)
# 返回字节数据
# print(response.content) 
# print(type(response.content))
# print(response.content.decode('utf-8'))
# print(type(response.content.decode('utf-8')))
#
# # 查看响应的url地址
# print('请求的URL:', response.url)
#
# print('编码方式:', response.encoding)
#
# print('响应状态码:', response.status_code)


params = {
    'wd': '中国'
}

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'
}
response = 'http://www.baidu.com/s'
req = requests.get(url=response, params=params, headers=headers)
with open('baidu.html', 'w', encoding='utf-8') as f:
    # write写入的内容必须是str,req的数据是二进制数据,需要将内容进行utf-8解码
    f.write(req.content.decode('utf-8'))

print(req.url)
print('Already Done')

发送post请求

1.最基本的POST请求可以使用post方法:

response = requests.post('http://www.baidu.com',data=data)

2.传入data数据:

import requests

data = {
    'first': 'T=true',
    'pn': '1',
    'kd': 'python'
}
"""
有些网站的反爬机制,会把一下原本该是get请求的设置为post请求,此时应该查看请求头,确定请求方法正确

针对网站的反爬机制,应该尽可能完整的设置请求头,高精度的模仿浏览器的请求方式
"""
headers = {
    'Referer': 'https://www.lagou.com/jobs/list_python?labelWords=&fromSearch=true&suginput=',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'
}
response = requests.post('https://www.lagou.com/jobs/positionAjax.json?needAddtionalResult=false', data=data,
                         headers=headers)
print(response.json())
print(type(response.json()))
print(response.content.decode('utf-8'))
print(response.text)
"""
post请求直接调用requests.post,如果返回的是json数据,那么可以调用response.json
将json字符串转换成字典或者列表
"""

request使用代理
import requests

# 查询本地IP
# url = 'http://httpbin.org/ip'
# response = requests.get(url)
# print(response.text)

# 使用代理
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',
    'Connection': 'close'
}

proxy = {
    'http': 'http://175.44.109.253:9999'
}

resp = requests.get('http://httpbin.org/ip', proxies=proxy, headers=headers, verify=False)
print(resp.text)

question:

requests.exceptions.ProxyError: HTTPConnectionPool(host=‘175.44.109.253’, port=9999): Max retries exceeded with url: http://httpbin.org/ip (Caused by ProxyError(‘Cannot connect to proxy.’, NewConnectionError(’<urllib3.connection.HTTPConnection object at 0x000001A2AA595788>: Failed to establish a new connection: [WinError 10060] 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。’)))

requests处理cookie
session

urllib库使用opener发送多个请求,多个请求之间可以共享cookie。

requests要实现共享cookie,可使用session对象,与web开发之中的不同,此处使用session代表一个对话的意思。

tips:

使用代理:

在请求方法中加入proxies参数

proxy = {
    'http': 'http://175.44.109.253:9999'
}

resp = requests.get('http://httpbin.org/ip', proxies=proxy, headers=headers, verify=False)

处理cookie,使用session创建对象

import requests

url = 'http://www.baidu.com'

response = requests.get(url)
# 以字典的方式返回cookie信息
print(response.cookies)
print(response.cookies.get_dict())

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'
}
data = {
    'email': '',
    'password': ''
}

test_url = 'http://renren.com/PLogin.do'
# session和Session区别不大
session = requests.Session()
session.post(url=test_url, data=data, headers=headers)

resp = session.get('http://renren.com/880151247/profile')
with open('sessionTest.html', 'w', encoding='utf-8') as f:
    f.write(resp.text)
    print('Already Done')

处理不信任的SSL证书

对于被信任的网站,requests可直接正常获取相应

设置verify=false可以不验证证书。

resp = requests.get('http://www.baidu.com',verify=false)
print(resp.content.decode('utf-8'))

数据解析

XPath语法和lxml模块
XPath( XML Path Lanage):

XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历.

XPath 是 W3C XSLT 标准的主要元素,并且 XQuery 和 XPointer 都构建于 XPath 表达之上。

XPath开发工具

1.Chrome插件XPath Helper

2.Firefox插件 Try XPath

[tips]

三种提取数据的方式:XPath、 正则表达式 、BeautifulSoup

XPath语法:
选取节点

XPath使用路径表达式来选取XML文档中的节点或者节点集。

表达式描述示例结果
nodename(节点名)选取此节点的所有子节点bookstore选取bookstore下所有的子节点
/如果是在最前面,代表从根节点选取,否则选择某节点下的某个节点/bookstore选取根元素下所有的bookstore节点
//从全局节点中选择节点,随便使用在那个位置//book从全局节点找到所有的book节点
@选取某个节点的属性//book[@price]选取所有拥有price属性得book节点
谓语

谓语用来查找某个特定的系欸但或包含某个指定值得节点,被嵌在方括号内。

路径表达式描述
/bookstore/book[1]选取bookstore下的第一个子元素
/bookstore/book[last()]选取bookstore下面倒数第二个book元素
bookstore/book[postion()❤️]选取booksrore下前面的两个子元素
//book[@price]选取拥有price属性的book元素
//book[@price=10]选取所有属性price属性等于10的book元素

1.[]中括号过滤元素,XPath里面下标从1开始(谓词中的下表从1开始)

2.contains:有时候某个属性包含了多个值,可以使用该函数

//div[contains(@class,‘job_detail)]
通配符
通配符描述示例结果
*匹配任意节点/bookstore/*选取bookstore下所有的子元素
@*匹配节点中的任何属性//book[@*]选取所有带有属性book元素
  • 路径写法
    • / 依次查找
    • // 间接查找
    • ./ 从当前元素下查找
    • .// 从当前元素的间接子节点查找
  • 位置条件
    • //li[1] 整个文档中的第一个<li>标签
    • //li[last()] 最后一个
    • //li[position() < 3] 前2个
    • //li[position() - 2] 倒数第2个
  • 属性条件
    • //li[@id="xxxx"]
    • //li[@class=""] @class 属性名
    • //li[@class="" and @name=""] 多个属性的且的关系
  • 同时提取两个元素
    • //title/text() | //img/@src
  • 模糊条件
    • //div[contains(@class, "page")] 查找class属性包含page的所有div标签
    • //div[starts-with(@class, "box")] 第一个class的属性值为box的div标签
    • //div[ends-with(@class, "clearfix")]最一个class的属性值为clearfix的div标签
lxml库

lxml是一个HTML/XML的解析器,主要功能是如何解析和提取HTML/XML数据

lxml和正则一样,是用c实现的,是一款高性能的Python HTML/XML解析器,我们可以用之前学到XPath语法,来快速的定位元素以及节点信息。

pip install lxml
基本使用

利用lxml解析HTML代码,并且在解析HTML代码的时候,如果HTML不规范,则会进行自动补全。

使用lxml解析 HTML代码的两种方式

1     htmlElement = etree.parse('parse_file.html')
2  	  htmlElement = etree.HTML(text)

方式1:

from lxml import etree

text = """
    <p>content</p>
"""
# 字符串解析 
htmlElement = etree.HTML(text)
print(type(htmlElement))    # <class 'lxml.etree._Element'>

# encoding='utf-8' 编码方式   .decode('utf-8') 解码,打印出来更好看   此过程防止乱码
print(etree.tostring(htmlElement, encoding='utf-8').decode('utf-8'))   # 转化成html代码  b'

方式2:

from lxml import etree

text = """
    <p>content</p>
"""
def parse_text():
    htmlElement = etree.HTML(text)
    print(type(htmlElement))  # <class 'lxml.etree._Element'>
    print(etree.tostring(htmlElement, encoding='utf-8').decode('utf-8'))
def parse_file():

    htmlElement = etree.parse('parse_file.html')
    print(etree.tostring(htmlElement, encoding='utf-8').decode('utf-8'))


def parse_lagou():
    # 定义解析器  parser默认的xml解析器。针对不规范的代码需要使用定义HTML解析器
    parser = etree.HTMLParser(encoding='utf-8')
    htmlElement = etree.parse('text.html',parser=parser)
    print(etree.tostring(htmlElement, encoding='utf-8').decode('utf-8'))


if __name__ == '__main__':
    parse_lagou()
lxml和xpath结合使用详解
from lxml import etree

parser = etree.HTMLParser(encoding='utf-8')
html = etree.parse('text.html', parser=parser)

# 1.获取所有的tr标签
# //tr  xpath返回的是一个列表
lis = html.xpath('//li')
print(lis)
for li in lis:
    print(etree.tostring(li, encoding='utf-8').decode('utf-8'))

# 2.获取第二个li标签
print('*' * 82)
li2 = html.xpath('//li[2]')[0]
print(li2)
print(etree.tostring(li2, encoding='utf-8').decode('utf-8'))

# 3.获取所有class为even的标签
print('&' * 80)
# lic = html.xpath('//li[contains(@class,"con_list_item")]')
lic = html.xpath('//li[@class="con_list_item"]')
for li in lic:
    print(etree.tostring(li, encoding='utf-8').decode('utf-8'))

# 4.获取所有a标签的href属性
# //a/@href 获取符合条件的标签
# //a/@href 获取符合条件的值
am = html.xpath('//a/@href')
for a in am:
    # print(etree.tostring(a,encoding='utf-8').decode('utf-8'))
    # 获取的是值  值本身是str类型,所以不需要上面的步骤
    print(a)

# 5.获取所有的职位信息(纯文本)
# 获取tr标签
lis = html.xpath('//li[position()>1]')
for li in lis:
    # print(li)
    # 获取li下的a标签下href的值
    href = li.xpath('.//a/@href')
    print(href)
    # 获取a标签下面的h3的文本
    title = li.xpath('.//a/h3/text()')[0]
    print(title)
    break
豆瓣电影爬取练习
# 豆瓣电影爬虫
from lxml import etree

import requests

# 定义headers
# 1.将目标网站上的页面抓取下来
headers = {
    # User-Agent必须参数
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',
    'Referer': 'https://movie.douban.com/'
}

url = 'https://movie.douban.com/cinema/nowplaying/xian/'

response = requests.get(url, headers=headers)
text = response.text
# print(text)
# print(response.content.decode('utf-8'))
"""
正规的网站可以使用text,   编码非正规获取数据后需要解码
response.text 返回的是一个经过解码后的字符串,是str(unicode)类型 
response.content 返回的是一个原生字符串,即直接从网页上面抓取下来,没有经过任何处理的
"""

# 2.将抓取下来得数据根据一定得规则进行提取
# etree.HTML()可以用来解析字符串格式的HTML文档对象,将传进去的字符串转变成_Element对象
html = etree.HTML(text)

# 获取整个列表信息
ul = html.xpath("//ul[contains(@class,'lists')]")[0]
# ul = html.xpath("//ul[@class='lists']")
print(ul)
# etree.tostring()方法用来将_Element对象转换成字符串。
# print(etree.tostring(ul, encoding='utf-8').decode('utf-8'))

# 获取正在上映的每一部电影
movies = []
lis = ul.xpath('./li')
for i in lis:
    # print(etree.tostring(ul, encoding='utf-8').decode('utf-8'))
    # @attr   获取某一属性
    title = i.xpath('@data-title')[0]
    score = i.xpath('@data-score')[0]
    duration = i.xpath('@data-director')[0]
    # /@attr 获取属性的值
    post = i.xpath('.//img/@src')[0]
    # print(post)
    # 变成字典
    movie = {
        'title': title,
        'score': score,
        'duration': duration,
        'post': post
    }
    movies.append(movie)

print(movies)

思路总结

Step1:获取目标网站

​ 利用requests.get获取目标网页,在request对象中设置参数(headers,伪装浏览器访问)

Step2:提取所需数据

​ etree.HTML()可以用来解析字符串格式的HTML文档对象,将传进去的字符串转变成_Element对象。

​ 用xpath过滤数据,获取数据后转化成字典类型,在将字典类型加入列表。

电影天堂爬取[人生滑铁卢:失败的案例…运行结果不理想…稍后修改]
from lxml import etree
import requests

# 全局变量可设置为常量
BASE_DOMAIN = 'https://www.dytt8.net/index.htm'
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',
    'Referer': 'https://www.dytt8.net/'
}


# 获取详情页的href
def get_detail_urls(url):
    # 请求的url
    url = 'https://www.dytt8.net/html/gndy/dyzz/list_23_1.html'
    # 获取请求对象
    response = requests.get(BASE_DOMAIN, headers=HEADERS)
    # 打印报错  requests.exceptions.ConnectionError: ('Connection aborted.', OSError(0, 'Error'))
    # <META http-equiv=Content-Type content="text/html; charset=gb2312">
    # text = response.content.decode('gbk')    # GBK解码时出错,因为出现了不能解析的字符
    print(response.encoding)  # 打印默认的解码方式
    # text = response.text.encode('utf-8').decode('utf-8')
    text = response.text
    html = etree.HTML(text)

    detail_urls = html.xpath('//ul/table//a/@href')  # 获取详情页url
    """
    和匿名函数作用相同,匿名函数更简洁
    def like_map():
        return BASE_DOMAIN + url

    index = 0
    for detail_url in detail_urls:
        # print(BASE_DOMAIN + detail_url)
        detail_url = like_map(detail_url)
        detail_urls[index] = detail_url
        index += 1
    """

    detail_urls = map(lambda url: BASE_DOMAIN + url, detail_urls)
    return detail_urls


# 详情页
def parse_detail_page(url):
    movies = []
    response = requests.get(url, headers=HEADERS)
    text = response.content.decode('gbk')
    html = etree.HTML(text)
    title = html.xpath('//h1//font')[0]
    # print(title)   # 打印title查看获取内容是否匹配
    # for x in title:   解码查看获取内容是否符合要求
    #     print(etree.tostring(x, encoding='utf-8').decode('utf-8'))
    zoomE = html.xpath('//div[@id="Zoom"]')[0]  # 返回的时列表
    imgs = zoomE.xpath('.//img/@src')
    post = imgs[0]
    screenshot = imgs[1]

    def parse_info(info, rule):
        return info.replace(rule, "").strip()

    infos = zoomE.xpath('.//text()')  # 获取子孙文本信息 以列表形式返回
    for index, info in infos:
        if info.startswith('◎年  代'):
            # print(info)
            info = parse_info(info, '◎年  代')
            movies['year'] = info
        elif info.startswith('◎产  地'):
            info = parse_info(info, '◎产  地')
            movies['country'] = info
            # print(country)
        elif info.startswith('◎主  演'):
            info = parse_info(info, '◎主  演')
            actors = [info]
            for x in range(index + 1, len(infos)):
                actor = infos[x].strip()
                if actor.startswith('◎'):
                    break
                actors.append(actor)
                # print(actor)
            movies['actors'] = info
        elif info.startswith('◎简  介'):
            info = parse_info(info, '◎简  介')
            for x in range(index + 1, len(info)):
                profile = info[x].strip()
                movies['profile'] = profile
    download_url = html.xpath("//td[@bgcolor='@fdfddf]/a/@href")  # //*[@id="Zoom"]/span/p[1]/a
    movies['download_url'] = download_url
    return movies


def spider():
    movies = []
    base_url = 'https://www.dytt8.net/html/gndy/dyzz/list_23_().html'
    for x in range(1, 8):
        # 控制页数
        url = base_url.format(x)
        detail_urls = get_detail_urls(url)
        # 遍历页面中每个电影详情url
        for detail_url in detail_urls:
            # print(detail_url)
            movie = parse_detail_page(detail_url)
            movies.append(movie)
            print(movies)
    # print(movies)
    #         break  # 获取当前页的详情url
    #     break  # 遍历到第七页结束后退出循环
    """
    针对第三页报错的测试代码
    url = 'https://www.dytt8.net/html/gndy/dyzz/list_23_3.html'
    detail_urls = get_detail_urls(url)
    for detail_url in detail_urls:
        print(detail_url)
    """


if __name__ == '__main__':
    spider()
BeautifulSoup4(bs4)
pip install bs4

bs4和lxml都是HTML/XML的解析库,最主要功能就是解析和提取HTML/XML数据。

lxml只会局部遍历,bs4是基于HTML DOM的,会载入整个文档,解析整个DOM树,因此时间和内存开销都会很大,性能低于lxml

bs用来解析html比较简单,API非常人性化,支持css选择器,Python标准库中的HTML解析器,也支持lxml的XML解析器。

几大解析工具对比
解析工具解析速度使用难
bs4最慢最简单
lxml简单
正则最快最难
BeautifulSoup4 相关函数

官方文档:https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/#

find_all()

find_all():查找所有满足条件要求的元素节点,如果只查找一个元素,使用find()[使用方法与find_all()类似,但是它只返回第一个满足要求的节点,而不是一个列表。]

    def find_all(self, name=None, attrs={}, recursive=True, text=None,
                 limit=None, **kwargs):

[tips]:

self: 表明它是一个类成员函数

name=None: name是要查找的tag元素的名称,默认是None,如果不提供,即查找所有u蒜素

attrs={}: 元素的属性,字典,默认为空,如果提供就是查找有这个指定元素的属性

recursive=True:指定查找是否在元素节点的子树下面全范围进行默认是True

text=None:

limit=None:

**kwargs:

解析器比较

[tips]

推荐使用lxml作为解析器,因为效率更高。在Python2.7.3之前的版本和Python3中3.2.2之前的版本中,必须安装lxml或html5lib,因为那些Python版本的标准库中内置的HTML解析方法不够稳定。

提示:如果一段HTML或XML文档格式不正确的话,那么在不同的解析器中返回的结果可能是不一样的

另一个可供选择的解析器是纯Python实现的html5lib,html5lib的解析方式与浏览器相同,

解析器使用方法优势劣势
Python标准库BeautifulSoup(markup, “html.parser”)Python的内置标准库执行速度适中文档容错能力强Python 2.7.3或3.2.2)前的版本中文档容错能力差
lxml HTML解析器BeautifulSoup(markup, “lxml”)速度快 文档容错能力强需要安装C语言库
lxml XML解析器BeautifulSoup(markup, [“lxml-xml”])``BeautifulSoup(markup, “xml”)速度快 唯一支持XML的解析器需要安装C语言库
html5libBeautifulSoup(markup, “html5lib”)最好的容错性 以浏览器的方式解析文档 生成HTML5格式的文档速度慢 不依赖外部扩展
bs4获取数据基础练习
from bs4 import BeautifulSoup

html = """
<html>
<head>
        <title>The Dormouse's story</title></head>
<body>
    <p class="title" id="text" name="dromouse"><b>The Dormouse's story</b></p>
    <p class="story">Once upon a time there were three little sisters; and their names were
    <a  class="title" id="text" href="http://example.com/elsie" class="sister"><!-- Elsie --></a>,
    <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
    <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
    and they lived at the bottom of a well.</p>
    <p class="story">...</p>
"""
# 1.获取所有的a标签
bs = BeautifulSoup(html, 'lxml')
resp = bs.find_all('a')
for i in resp:
    print(i, type(i))
    # from bs4.element import Tag
    # 将元素将字符串的形式打印出来,但实际形式是   <class 'bs4.element.Tag'>
print("-----------第一题结束---------------")

# 2.获取第二个a标签
second = bs.find_all('a', limit=2)[1]  # find_all 返回列表
print(second)

print("-----------第二题结束---------------")
# 3.获取所有class=title的标签
resp3 = bs.find_all(attrs={"class": "title"})
print("3", resp3)
print("-----------第三题结束---------------")

# 4.获取所有id=text且class="test"的标签提取出来
resp4 = bs.find_all('a',attrs={"class": "title", "id": 'text'})
print(resp4)
print("-----------第四题结束---------------")

# 5.获取所有的a标签的href属性
resp5 = bs.find_all('a')
# 获取属性的方式1
for a in resp5:
    # 通过类型下标的方式提取属性  常用写法,简洁
    # href = a['href']
    # print(href)
    # 方式2 传统方式
    href = a.attrs['href']
    print(href)

print("-----------第五题结束---------------")

# 不是上述html的代码。。  仅供参考做法。
# 6.获取所有的文本信息
trs = bs.find_all('tr')[1:]
infos = []
for tr in trs:
    info = {}
    # 方式1:
    tds = tr.find_all('td')
    title = tds[0].string
    # print(title)   # 获取到标签
    # print(title.string)  # 获取文本信息

    category = tds[1].string
    num = tds[2].string
    pubtime = tds[3].string
    info['title'] = title
    info['category'] = category
    info['num'] = num
    info['pubtime'] = pubtime
    infos.append(info)
    
    # 方式2:
    # 缺陷:会打印转义字符
    # infos = list(tr.strings)
    # infos = list(tr.stripped_strings)    加强版
    # eg info['title'] = infos[0]
    # print(infos)   # 返回的是生成器
    # for info in infos:    # 遍历获取值
    #     print(info)
print(infos)
a = bs.find_all('a')[1]
text = a.strings
print(text)

[tips]

strings和stripped_strings、string属性以及get_text()

1.strings 获取某个标签下的子孙非标签字符串,返回的是一个生成器

2.stripped_strings 获取某个标签下面的子孙非标签字符串,会去掉空白字符,返回来的是一个生成器

3.string 获取某个标签下的非标签字符串,返回来的是个字符串

4.get_text() 获取某个标签下的子孙非标签字符串,不是以列表的形式返回。

select方法

使用select方法可以方便的找出元素,但是在使用css选择器的方法更加方便,使用css选择器语法,应该使用select方法。

常见的css选择器方法
  • 通过标签名查找

  • 通过类名查找

  • 通过id查找

  • 通过属性查找

  • 查找子孙元素

  • 查找直接子元素 >

select()基本用法练习
from bs4 import BeautifulSoup

text = """
<table class="tablelist" cellpadding="0" cellspacing="0">
    <tbody>
        <tr class="h">
            <td class="l" width="374">职位名称</td>
            <td>职位类别</td>
            <td>人数</td>
            <td>地点</td>
            <td>发布时间</td>
        </tr>
        <tr class="even">
            <td class="l square"><a target="blank"
href="https://www.baidu.com/">研发工程师(上海1)</a></td>
            <td>技术类</td>
            <td>1</td>
            <td>上海</td>
            <td>2020-1-1</td>
        </tr>
        <tr class="odd">
            <td class="l square"><a target="blank"
href="https://www.baidu.com/">工程师(北京2)</a></td>
            <td>技术类</td>
            <td>2</td>
            <td>北京</td>
            <td>2020-2-2</td>
        </tr>
        <tr class="even">
            <td class="l square"><a target="blank"
href="https://www.baidu.com/">工程师(上饶3)</a></td>
            <td>管理类</td>
            <td>3</td>
            <td>上饶</td>
            <td>2020-3-3</td>
        </tr>
    </tbody>
</table>
"""
soup = BeautifulSoup(text, 'lxml')
# 1.获取所有的tr标签
resp1 = soup.select('tr')
for tr in resp1:
    print(tr, type(tr))
    print('-----------------------')
2.获取第二个tr标签
resp2 = soup.select('tr')[1]
print(resp2)

# 3.获取所有class=even的tr标签
# resp3 = soup.select('tr.even')
resp3 = soup.select('tr[class="even"]')
# resp3 = soup.select('.even')
for tr in resp3:
    print(tr)

# 4.获取所有a便签的href属性
resp4 =  soup.find_all('a')
for a in resp4:
    href = a['href']
    print(href)

# 5.获取所有的职位信息(纯文本)
trs = soup.select('tr')
for tr in trs:
    # info = tr.stripped_strings
    info = list(tr.stripped_strings)
    print(info)



BeautifulSoup的四个常用对象

BeautifulSoup将复杂的HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4中:

1.Tag: BeautifulSoup中所有的标签都是Tag类型,并且BeautifulSoup的对象其实本质上也是一个Tag类型,所以find()、find_all()并不是BeautifulSoup的方法,而是Tag的。

2.NavigatableString: 继承自python中的str,和python的str用法相同

3.BeautifulSoup: 继承自Tag,用来生成BeautifulSoup树

4.Comment:继承自NavigatableString

代码示例:见下一个代码块

遍历文档树
1.contents[返回列表]和children[返回迭代器(可遍历)

返回某个便签下的直接子元素。

from bs4 import BeautifulSoup

text = """
<table class="tablelist" cellpadding="0" cellspacing="0">
    <tbody>
        <tr class="h">
            <td class="l" width="374">职位名称</td>
            <td>职位类别</td>
            <td>人数</td>
            <td>地点</td>
            <td>发布时间</td>
        </tr>
        <tr class="even">
            <td class="l square"><a target="blank"
href="https://www.baidu.com/">研发工程师(上海1)</a></td>
            <td>技术类</td>
            <td>1</td>
            <td>上海</td>
            <td>2020-1-1</td>
        </tr>
        <tr class="odd">
            <td class="l square"><a target="blank"
href="https://www.baidu.com/">工程师(北京2)</a></td>
            <td>技术类</td>
            <td>2</td>
            <td>北京</td>
            <td>2020-2-2</td>
        </tr>
        <tr class="even">
            <td class="l square"><a target="blank"
href="https://www.baidu.com/">工程师(上饶3)</a></td>
            <td>管理类</td>
            <td>3</td>
            <td>上饶</td>
            <td>2020-3-3</td>
        </tr>
    </tbody>
</table>
<div>
我是div文本
</div>
<p><!--我是注释字符串--></p>
"""
from bs4.element import Tag, NavigableString, Comment

"""
BeautifulSoup继承自Tag,其本质就是一个Tag
find_all()   find()其实是Tag的方法
"""

soup = BeautifulSoup(text, 'lxml')
print(type(soup))  # <class 'bs4.BeautifulSoup'>
table = soup.find('table')
table.find_all('tr')  # <class 'bs4.element.Tag'>
print(type(table))

div = soup.find('div')
print(type(div.string))  # <class 'bs4.element.NavigableString'>

p = soup.find('p')
print(p.string)
print(type(p.string))  # <class 'bs4.element.Comment'>

# 标签下多行字符使用 .contents
print(p.contents)
print(type(p.contents))  # <class 'list'>

# for element in table.children:
for element in table.contents:

    print(element,type(element))
    print('---------------------------')

中国天气数据爬取练习
from bs4 import BeautifulSoup
import requests


def parse_page(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44'
    }
    response = requests.get(url, headers=headers)
    # print(response.content.decode('utf-8'))
    # 提取原生字符串,自己解码
    text = response.content.decode('utf-8')
    soup = BeautifulSoup(text, 'html5lib')
    # soup = BeautifulSoup(text, 'lxml')
    conMidtab = soup.find('div', attrs={'class': 'conMidtab'})
    # print(conMidtab)
    tables = conMidtab.find_all('table')
    for table in tables:
        # print(table)
        trs = table.find_all('tr')[2:]
        for index, tr in enumerate(trs):
            # print(tr)
            # print('----------------')
            tds = tr.find_all('td')
            city_td = tds[0]
            if index == 0:
                city_td = tds[1]

            city = list(city_td.stripped_strings)[0]
            # print(city)
            temp_td = tds[-2]
            min_temp = list(temp_td.stripped_strings)[0]
            # print(temp)

            print({
                'city': city,
                'temp': int(min_temp)
            })


def main():
    urls = [
        'http://www.weather.com.cn/textFC/hb.shtml',
        'http://www.weather.com.cn/textFC/db.shtml',
        'http://www.weather.com.cn/textFC/hn.shtml',
        'http://www.weather.com.cn/textFC/xb.shtml',
        'http://www.weather.com.cn/textFC/gat.shtml'
    ]
    for url in urls:
        parse_page(url)


if __name__ == '__main__':
    main()


[思路总结]

定义 parse_page(),获取解析网页获取数据

  • requests.get()获取页面
  • response.content.decode()将获取的页面解码
  • 创建bs4对象解析 soup = BeautifulSoup(text, ‘html5lib’)

​ 港澳台页面的编写不够规范,lxml解析时容错率低,未能正确解析,此时选用html5lib

pip install html5lib

  • 获取相应数据

    ​ conMidtab.find_all(‘table’)获取元素标签

    ​ index, tr in enumerate(trs) 利用枚举型获取index, 因为第一个td做了列合并,需要判断数据是是否正确

    ​ if index == 0 时 city_td = tds[1]

    ​ 获取文本信息 city = list(city_td.stripped_strings)[0] 由于stripped_strings返回的是一个生成器,所以需要转化成list

    定义主函数

    • 将多个页面的url加入list ,遍历list可获取每页的数据
正则表达式和re模块

通俗理解:按照一定的规则,从某个字符串当中匹配出所需数据。

标准参考百度百科

正则表达式常用匹配规则
匹配某个字符串
text = 'hello'
ret = re.match('he', text)  # match()会从头匹配,若从头开始为匹配则或报错
print(ret)  # re.Match object; span=(0, 2), match='he'>
print(ret.group())  # group会打印出匹配到的数据

点(.)匹配任意字符
test = 'hello'
ret = re.match('.', test)  # .仅匹配一个(任意)字符,但是不能匹配换行符
print(ret.group())
\d匹配任意的数字 digit
text = '135'
ret = re.match('\d\d',text)
print(ret.group())  # 13
\D匹配任意的非数字
text = '?13?5'
ret = re.match('\D', text)
print(ret.group())
\s匹配空白字符(包括:\n \t \r 空格)
text = ' '
ret = re.match('\s', text)
print(ord(ret.group()))   # 为了显示打印结果,利用ord()转换成ascii码 查看打印结果是否符合要求
\w匹配字母数字下划线
text = 'adsasf1511--2___wea'
ret = re.match('\w+', text)
print(ret.group())
\W与\w相反
text = '@@@###adsasf1511--2___wea'
ret = re.match('\W+', text)
print(ret.group())
[]组合的方式,只要满足括号中的某一项都算匹配成功
text = '2@@@###adsasf1511--2___wea'
ret = re.match('[\W\d]+', text)
print(ret.group())

text = '2@@@###adsasf1511--2___wea'
ret = re.match('^[0-9a-zA-Z]+', text)
print(ret.group())
匹配多个字符

1.*可以匹配0或者任意多个字符

text = '135516dasfdaasda1556156'
ret = re.match('\d*', text)
print(ret.group())

2.+ 可以匹配一个或多个字符

text = '135516dasfdaasda1556156'
ret = re.match('\d+', text)
print(ret.group())

3.?匹配的字符可以出现一次或0次(没有或者仅有一个)

text = '135516dasfdaasda1556156'
ret = re.match('\d?', text)
print(ret.group())

4.{m}匹配m个字符

text = '135516dasfdaasda1556156'
ret = re.match('\d{3}', text)
print(ret.group())

5.{m,n}匹配m~n个(按最多匹配)

text = '135516dasfdaasda1556156'
ret = re.match('\d{1,3}', text)
print(ret.group())
正则练习案例
1.验证手机号码,1开头
text = '18577562352'
ret = re.match('1[34578]\d{9}', text)
print(ret.group())import re


def tel():
    tel = input('请输入验证号码:')
    ret = re.match('1[34578]\d{9}', tel)
    if ret:
        print('您输入的号码:', ret.group(), '验证通过')
    else:
        print('验证失败')


if __name__ == '__main__':
    tel()
2.验证QQ邮箱
import re


def tel():
    email = input('请输入验证邮箱:')
    ret = re.match('[1-9]\d{8}@qq.com', email)
    if ret:
        print('您输入的邮箱:', ret.group(), '验证通过')
    else:
        print('验证失败')


if __name__ == '__main__':
    tel()

3.验证URL

text = 'https://blog.youkuaiyun.com/sinat_38682860/article/details/103122308'
ret = re.match('(http|https|ftp)://[^\s]+',text)
print(ret.group())
^(脱字号):表示以…开始

使用match函数,可以不使用^,使用search()用^寻找匹配的首字符,^在中括号中表示取反

$表示以…结束
|匹配多个字符串或者表达式
贪婪模式

尽可能多的匹配字符

非贪婪模式

表达式后+?表示非贪婪模式

ret = re.match('\d+?',tel)
转义字符和原生字符串

转义字符:在字符前加入反斜杠\可以取消特殊字符的含义,仅表示匹配当前字符。

原生字符:在判断前加r也不会把字符串内的内容做任何特殊处理。

text = r'\n'   # r-->raw adj. 生的;未加工的
print(text)    # \n
text = r'\n'
print(text)
ret = re.match(r'\\n', text)
print(ret.group())
re模块的常用函数
match()

从开始位置匹配,如果开始位置未匹配,则匹配失败

search

在字符串中找满足条件的字符

分组

在正则表达式中,可以对过滤的字符串分组,分组使用圆括号的方式。

text = 'apple`s price $99,orange`s price is $10'
# print(text)
ret = re.search('.*(\$\d+).*(\$\d+)', text)
resp = ret.group(1, 2)
resp2 = ret.groups()
print(resp)
print(resp2)
print(type(resp))   # <class 'tuple'>
findall()

找出所有满足条件的值,并且返回一个列表。正则 re.findall 的简单用法(返回string中所有与pattern相匹配的全部字串,返回形式为数组)

text = 'apple`s price $99,orange`s price is $10'
ret = re.findall('\$\d+', text)
print(ret)
sub

匹配到的内容 替换成其他内容

text = 'apple`s price $99,orange`s price is $10'
ret = re.sub('\$\d+','$0', text)
print(ret)    # apple`s price $0,orange`s price is $0
split()

分割字符

text = 'apple`s price $99,orange`s price is $10'
ret = re.split(' ', text)
print(ret)  # ['apple`s', 'price', '$99,orange`s', 'price', 'is', '$10']

compile()

可以使用compile进行编译

re获取古诗文网站爬虫练习
import requests
import re


# 定义解析函数
def parse_page(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44',
        'Referer': 'https://www.gushiwen.cn'
    }
    # 获取需要解析的网页
    response = requests.get(url, headers)
    # print(response.text)
    # print(response.encoding)
    text = response.text
    # 默认采用贪婪模式
    titles = re.findall(r'<div\sclass="cont">.*?<b>(.*?)</b>', text, re.DOTALL)
    # print(titles)
    dynasties = re.findall(r'<p\sclass="source">.*?<a.*?>(.*?)</a>', text)
    # print(dynasties)
    authors = re.findall(r'<p\sclass="source">.*?<a.*?>.*?<a.*?>(.*?)</a>', text, re.DOTALL)
    # print(authors)
    content_tags = re.findall(r'<div\sclass="contson" .*?>(.*?)</div>', text, re.DOTALL)
    contents = []
    for content in content_tags:
        # print(content)
        ret = re.sub(r'<.*?>', ' ', content)
        contents.append(ret.split())
    # print(contents)
    peomes = []
    for value in zip(titles, dynasties, authors, contents):
        title, dynasty, author, content = value
        peom = {
            'title': title,
            'dynasty': dynasty,
            'author': author,
            'contents': content
        }
        peomes.append(peom)
    for single in peomes:
        print(single)


def main():
    # urls = [
    #     'https://www.gushiwen.cn/default_1.aspx',
    #     'https://www.gushiwen.cn/default_2.aspx',
    #     'https://www.gushiwen.cn/default_3.aspx',
    #     'https://www.gushiwen.cn/default_4.aspx',
    #     'https://www.gushiwen.cn/default_5.aspx',
    #     'https://www.gushiwen.cn/default_6.aspx',
    #     'https://www.gushiwen.cn/default_7.aspx',
    #     'https://www.gushiwen.cn/default_8.aspx',
    #     'https://www.gushiwen.cn/default_9.aspx',
    # ]
    # for url in urls:
    #     parse_page(url)
    # 获取10页的第二种方式。
    url = 'https://www.gushiwen.cn/default_9.aspx'
    for x in range(1, 10):
        url = 'https://www.gushiwen.cn/default_%s.aspx' % x
        parse_page(url)


if __name__ == '__main__':
    main()

[思路总结]

对任何数据的爬取都需要先获取页面,解析页面,在解析页面的基础上面通过一种方式提取所需要的数据。

糗事百科数据爬取练习
import re
import requests


def parse_page(url):
    # 请求页面数据
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44'
    }

    response = requests.get(url, headers=headers)
    # print(response.encoding)  查看网页的编码方式
    # print(response.text)
    # 解析网页
    text = response.text

    # 设置提取数据的规则(正则表达式)
    name_tags = re.findall(r'<h2>(.*?)</h2>', text, re.DOTALL)
    # print(names)
    content_tags = re.findall(r'<div\sclass="content">.*?<span>(.*?)</span>', text, re.DOTALL)
    names = []
    for n in name_tags:
        # print(n)
        a = re.sub(r'\n', ' ', n)
        names.append(a)
    # print(name)
    contents = []
    for content in content_tags:
        # print(content)
        x = re.sub(r'<.*?>', ' ', content)
        contents.append(x.split())
    print(contents)
    double = []
    for value in zip(names, contents):
        name, content = value
        single = {
            'name': name,
            'content': content
        }
        double.append(single)
    for single in double:
        print(single)


def main():
    for i in range(1, 11):
        url = 'https://www.qiushibaike.com/text/page/%s/' % i
        parse_page(url)


if __name__ == '__main__':
    main()

数据存储

json文件处理

什么是json?

JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。它基于 ECMAScript (欧洲计算机协会制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。

[参考百度百科]https://baike.baidu.com/item/JSON/2462549?fr=aladdin

json支持的数据格式

1.对象(字典):使用花括号

2.数组(列表):使用方括号

3.整型,浮点型

4.字符串类型(字符串必须要使用双引号)

多个数据之间用,分割。json本质上是一个字符串

  • loads():将json数据转化成dict数据 和文件无关
  • dumps():将dict数据转化成json数据
  • load():读取json文件数据,转成dict数据 和文件有关
  • dump():将dict数据转化成json数据后写入json文件
字典列表转JSON
import json

# 将python对象转换成json字符串

person = [
    {
        'username': "sami",
        'country': 'China',
        'age': 20
    },
    {
        'username': "哈哈",
        'country': 'Africa',
        'age': 3
    }
]

print(type(person))  # <class 'list'>
json_str = json.dumps(person)  # dumps()可以将数据转换成json字符串
print(json_str, type(json_str))  # <class 'str'>
with open('person.json', 'w', encoding='utf-8') as f:
    # 存储的两种方式
    # f.write(json_str)
    # 不设置ensure_ascii默认为True,打印的数据是ascii编码 若要存储为中文格式,需要将ensure_ascii设置为False,然后在上面的加入encoding='utf-8'
    json.dump(person, f, ensure_ascii=False)
    print('Already Done')
将一个json字符串load成Python对象

json.dumps()用于将字典形式的数据转化为字符串,json.loads()用于将字符串形式的数据转化为字典

import json

json_str = '[{"username": "sami", "country": "China", "age": 20}, {"username": "哈哈", "country": "Africa", "age": 3}]'
print(type(json_str))  # <class 'list'>
person = json.loads(json_str)
print(type(person))  # <class 'list'>
for people in person:
    print(people)

直接从文件中读取json
with open('person.json', 'r', encoding='utf-8') as f:
    persons = json.load(f)
    print(type(persons))
    for people in persons:
        print(people)
CSV文件处理

逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样被解读的数据。CSV文件由任意数目的记录组成,记录间以某种换行符分隔;每条记录由字段组成,字段间的分隔符是其它字符或字符串,最常见的是逗号或制表符。通常,所有记录都有完全相同的字段序列。通常都是纯文本文件。建议使用WORDPAD或是记事本来开启,再则先另存新档后用EXCEL开启,也是方法之一。

[参考]百度百科 https://baike.baidu.com/item/CSV/10739?fr=aladdin

csv并不是一种单一的、定义明确的格式。因此在实践中,属于 csv泛指具有一下特征的任何文件:

1.纯文本,使用某个符号集,比如ASCII、unicode等。

2.由记录组成(典型的是每一行的记录)

3.每条记录被分隔符分割成字段(典型分隔符有逗号、分号或制表符,有时候也可选空格)

4.每条记录都有同样的字段序列

读取csv文件(两种方式)
import csv


def reader_csv1():
    with open('text.csv', 'r', encoding='utf-8') as f:
        reader = csv.reader(f)  # reader是迭代器 ,可以通过for循环获取数据
        # print(list(reader))
        """
        for循环遍历时,会以列表的形式将数据取出,其实包括标题行
        若不需要标题,可调用next()函数,遍历是会从排除第一行(即标题行)
        """
        next(reader)
        # 通过下标获取数据
        for data in reader:
            name = data[1]
            age = data[-2]
            hobby = data[-1]
            print({
                'name': name,
                'age': age,
                'hobby': hobby
            })
            # print(data)
        print('Already Done')

if __name__ == '__main__':
    reader_csv1()

[tips]

以上方式是以下标的形式获取列表的值,这种获取数据的凡是可能存在问题,比如某个数据不存在时,最好的方式是通过列名获取(类似于操作字典)。

def reader_csv2():
    with open('text.csv', 'r', encoding='utf-8') as f:
        # DictReader创建的reader对象,不会包含标题行的数据
        reader = csv.DictReader(f)  # reader是一个迭代器,返回的数据类型为字典
        for x in reader:
            value = {
                "username": x['username'], 
                'password': x['password']
            }
            # print(x,type(x))  # <class 'collections.OrderedDict'>
            print(value, type(value))  # <class 'dict'>


if __name__ == '__main__':
    reader_csv2()
如何将数据写入csv文件
# 元组方式写入
headers = ['username', 'age', 'sex']
values = [
    ('张三', 18, '男'),
    ('张四', 15, '男'),
    ('张五', 13, '女'),
]

with open('classroom.csv', 'w',  newline='') as f:
    writer = csv.writer(f)
    # 写入表头信息
    writer.writerow(headers)
    """
    writerows将value数据多行写入,但是产生了文件此时存在两个问题,
    问题         解决方案
    乱码         在open()里面添加属性encoding='utf-8'
   多余空行       在open()里面添加属性newline=''   不设置时,默认为newline='\n'
    """
    writer.writerows(values)
    print('Already Done')
# 字典方式写入
import csv


def write_csv_in_dict():
    # DictReader需要传入两个值(文件指针,头部信息) 表头信息需要手动写入
    headers = ['username', 'age', 'sex']
    values = [
        {'username': 'sami', 'age': 13, 'sex': '男'},
        {'username': 'jack', 'age': 18, 'sex': '男'},
        {'username': 'lucy', 'age': 23, 'sex': '女'},
    ]
    with open('classroom.csv', 'w', newline='') as f:
        writer = csv.DictWriter(f, headers)   # DictWriter 手动调用下面的两个函数才可以写入数据
        writer.writeheader()  # 需要调用.writeheader()才可以加入表头
        writer.writerows(values)  # .writerows()写入数据


if __name__ == '__main__':
    write_csv_in_dict()

python代码测试数据库连接(安装过程自己发挥)
import pymysql

conn = pymysql.connect(host='localhost', user='root', password='root',
                       database='pymysql_demo', port=3306)

cursor = conn.cursor()
result = cursor.fetchone()
print(result)

conn.close()
python代码插入数据
import pymysql

conn = pymysql.connect(host='localhost', user='root', password='root',
                       database='pymysql_demo', port=3306)

cursor = conn.cursor()
# result = cursor.fetchone()
# print(result)

# 给user直接插入数据
#
# sql = """
# insert into user(id,username,,age,passward) values(2,'sami',18,'aaaaa')
# """
# # 执行sql语句
# cursor.execute(sql)
# # 提交数据
# conn.commit()


"""
变量指定为null,会根据默认自增长
username,age,password 均设置为%s
"""
# 插入变量打方式,推荐
sql = """
insert into user(id,username,age,passward) values(null,%s,%s,%s)
"""
username = 'sami'
age = 15
password = '123456'

# 执行sql语句
cursor.execute(sql, (username, password, age))
# 提交数据
conn.commit()

conn.close()

查找数据

使用pymysql查询数据,可以使用fetch方法

  1. fetchone():这个方法每次只获取一条数据
  2. fetchall():接收全部的返回数据
  3. fetchmany(): 查找多个
import pymysql

conn = pymysql.connect(host='localhost', user='root', password='root',
                       database='pymysql_demo', port=3306)

cursor = conn.cursor()

# select * from user
# select username,age,password from user where id=1
sql = """
select username,age,password from user where id=1
"""
# 执行sql语句
cursor.execute(sql)
# result = cursor.fetchone()

# print(result)

# 获取符合 条件的所有数据 方式一
while True:
    result = cursor.fetchone()
    if result:
        print(result)
    else:
        break
# 获取符合 条件的所有数据 方式二
# results = cursor.fetchall()
# for  result in results:
#     print(result)

# fetchmany(2)  2为返回书数据条个数
# result = cursor.fetchmany(2)


conn.close()
使用代码删除和更新数据
import pymysql

conn = pymysql.connect(host='localhost', user='root', password='root',
                       database='pymysql_demo', port=3306)

cursor = conn.cursor()
# 删除整条信息
sql = """
delete from user where id=1
"""

cursor.execute(sql)
# 数据发生改变时,均需要.commit()同步数据库
conn.commit()

conn.close()
更新数据
import pymysql

conn = pymysql.connect(host='localhost', user='root', password='root',
                       database='pymysql_demo', port=3306)

cursor = conn.cursor()
# 删除整条信息
sql = """
update user set username='aaa' where id=2
"""

cursor.execute(sql)
# 数据发生改变时,均需要.commit()同步数据库
conn.commit()

conn.close()
MongoDB数据库操作(以后再整理。。)

MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

MySQL是一种开放源代码’关系型数据库管理系统(RDBMS),使用最常用的数据库管理语言–结构化查询语言(SQL)进行数据库管理。

爬虫进阶

多线程爬虫

有些时候,例如下载图片,因为下载是一个耗时的操作,如果采用之前那种同步的方式下周再。效率较低,因此可以考虑多线程的方式下载资源。

多线程

多线程是为了同步完成多项任务,通过提高资源使用率来提高系统效率,线程是在同一时间要完成多项任务的时候实现的。

threading模块介绍

[参考某位大佬的优快云]https://blog.youkuaiyun.com/wqx521/article/details/82754617

# coding=gbk
# 传统方式
import time


def coding():
    for x in range(3):
        print('正在写代码%s' % x)
        time.sleep(1)


def drawing():
    for x in range(3):

            print('正在画图%s' % x)
            time.sleep(1)


def main():
    coding()
    drawing()


if __name__ == '__main__':
    main()
# 使用线程
# coding=gbk
import threading
import time


def coding():
    for x in range(3):
        print('正在写代码%s' % x)
        time.sleep(1)


def drawing():
    for x in range(3):
        print('正在画图%s' % x)
        time.sleep(1)


def main():
    # 创建线程
    t1 = threading.Thread(target=coding)
    # target传入的是函数名
    t2 = threading.Thread(target=drawing)

    # 启动线程
    t1.start()
    t2.start()


if __name__ == '__main__':
    main()

查看进程中的线程数

在上面的案例中,main函数下面,线程有3个,一是主进程中的主线程,其他两个是建立的两个线程t1,t2。

threading.enumerate()可查看查看进程中的线程数

# coding=gbk
import threading
import time


def coding():
    for x in range(3):
        print('正在写代码%s' % x)
        time.sleep(1)


def drawing():
    for x in range(3):
        print('正在画图%s' % x)
        time.sleep(1)


def main():
    # 创建线程
    t1 = threading.Thread(target=coding)
    # target传入的是函数名
    t2 = threading.Thread(target=drawing)

    # 启动线程
    t1.start()
    t2.start()

    print(
        threading.enumerate())  # [<_MainThread(MainThread, started 15304)>, <Thread(Thread-1, started 14964)>, <Thread(Thread-2, started 11624)>]


if __name__ == '__main__':
    main()

查看当前线程
# coding=gbk
import threading
import time


def coding():
    for x in range(3):
        print('正在写代码%s' % threading.current_thread())
        time.sleep(1)


def drawing():
    for x in range(3):
        print('正在画图%s' % threading.current_thread())
        time.sleep(1)


def main():
    # 创建线程
    t1 = threading.Thread(target=coding)
    # target传入的是函数名
    t2 = threading.Thread(target=drawing)

    # 启动线程
    t1.start()
    t2.start()

    print(
        threading.enumerate())  # [<_MainThread(MainThread, started 15304)>, <Thread(Thread-1, started 14964)>, <Thread(Thread-2, started 11624)>]

    # 正在写代码<Thread(Thread-1, started 13248)>
    # 正在画图<Thread(Thread-2, started 5276)>
    # [<_MainThread(MainThread, started 3412)>, <Thread(Thread-1, started 13248)>, <Thread(Thread-2, started 5276)>]
    # 正在画图<Thread(Thread-2, started 5276)>
    # 正在写代码<Thread(Thread-1, started 13248)>
    # 正在画图<Thread(Thread-2, started 5276)>正在写代码<Thread(Thread-1, started 13248)>


if __name__ == '__main__':
    main()
继承自threaing.Thread类

为了让代码更好的封装,可以使用threading模块下的Thread类,继承自这个类,然后实现run方法,线程就会自动运行run方法中的代码

自定义类

# coding=gbk
import threading
import time


# 创建2个线程类
class CodingThread(threading.Thread):
    def run(self):
        for x in range(3):
            print('正在写代码%s' % threading.current_thread())
            time.sleep(1)


class DrawingThread(threading.Thread):
    def run(self):
        for x in range(3):
            print('正在画图%s' % threading.current_thread())
            time.sleep(1)


def main():
    t1 = CodingThread()
    t2 = DrawingThread()

    t1.start()
    t2.start()


if __name__ == '__main__':
    main()

[总结]

创建自定义线程时,首先要定义线程类,在线程类中重写threading下的run()方法。

然后在创建线程对象,启动线程。

多线程共享全局变量的问题

多线程都是在一个进程里面运行的,因此在进程中的变量所有线程是可共享的,这就造成了一个问题,因为线程的执行是无序(执行顺去取决于代码块的复杂程度)的,有可能或造成错误。

# coding=gbk
import threading
import time

VALUES = 0


def add_value():
    global VALUES
    for i in range(10000000):
        VALUES += 1  # 此处的VALUES是全局变量因此需要使用global声明。
        # 由于VALUES是全局变量,在函数内部引用时,不声明函数自身不会识别是全局变量还是新变量
    print('VALUES_END:%d' % VALUES)
    time.sleep(2)


def main():
    for i in range(2):
        t1 = threading.Thread(target=add_value)
        t1.start()


if __name__ == '__main__':
    main()

打印的结果:

VALUES_END:11335889

VALUES_END:12372956

可以看出在,这个打印结果不是本身应该获得的数据,因为两个线程同时执行,同时把VALUES增加,故获得的数据比预期结果大,在range(1000)时,出现这种情况的概率会比较小。

解决方法:

锁机制

首先在全局变量中声明 glock = threading.Lock()

在程序执行时,请求锁 glock.acquire()

内部函数结束时,释放锁,可以避免脏数据。

# coding=gbk
import threading
import time

VALUES = 0

glock = threading.Lock()


def add_value():
    global VALUES
    glock.acquire()
    for i in range(10000000):
        VALUES += 1  # 此处的VALUES是全局变量因此需要使用global声明。
        # 由于VALUES是全局变量,在函数内部引用时,不声明函数自身不会识别是全局变量还是新变量
    glock.release()
    print('VALUES_END:%d' % VALUES)
    time.sleep(2)


def main():
    for i in range(2):
        t1 = threading.Thread(target=add_value)
        t1.start()


if __name__ == '__main__':
    main()
Lock版本生产者和消费者模式

生产者的线程专门用来生产一些数据,存放到一个中间变量中。消费者再从这个中间的变量中取出数据进行消费。但是因为要使用中间变量,中间变量通常是一些全局变量,因此需要使用锁来保证数据完整性

为了解决共享全局变量的问题。threading提供了一个Lock类,这个类可以在某个线程访问某个 变量的时候加锁,其他线程此时就不能进来,直到当前线程处理完后,把锁释放了,其他线程才 能进来处理。

使用锁的原则: 1. 把尽量少的和不耗时的代码放到锁中执行。 2. 代码执行完成后要记得释放锁。

生产者和消费者模式是多线程开发中经常见到的一种模式。生产者的线程专门用来生产一些数 据,然后存放到一个中间的变量中。消费者再从这个中间的变量中取出数据进行消费。通过生产 者和消费者模式,可以让代码达到高内聚低耦合的目标,程序分工更加明确,线程更加方便管 理。

生产者的线程专门用来生产一些数据,存放到一个中间变量中。消费者再从这个中间的变量中取出数据进行消费。但是因为要使用中间变量,中间变量通常是一些全局变量,因此需要使用锁来保证数据完整性

# coding=gbk
import threading
import random
import time

gMoney = 1000
gLock = threading.Lock()  # 创建全局锁
gTotalTime = 10  # 总共生产次数
gTimes = 0


class Producer(threading.Thread):
    def run(self):
        global gMoney, gTimes
        while True:
            money = random.randint(10, 100)
            gLock.acquire()
            if gTimes >= 10:
                gLock.release()
                break    # 在此处break,下面的代码不执行,则锁不会被释放,进程卡死其他线程还在等待
            gMoney += money
            print('%s生产了%d元钱,剩余%d元' % (threading.current_thread(), money, gMoney))
            gTimes += 1
            gLock.release()
            time.sleep(0.5)


class Consume(threading.Thread):
    def run(self):
        global gMoney
        while True:
            # 执行钱需要判断money是否足够
            money = random.randint(100, 500)
            gLock.acquire()
            if gMoney >= money:
                gMoney -= money
                print('%s消费了了%d元钱,剩余%d元' % (threading.current_thread(), money, gMoney))
            else:
                if gTimes >= gTotalTime:
                    gLock.release()
                    print('挣得不够你花的!')
                    break
            gLock.release()
            time.sleep(0.5)


def main():
    # 生产者同时5个线程
    for x in range(5):
        t = Producer(name='生产者线程%d' % x)
        t.start()
    # 消费者3个一直消费
    for x in range(3):
        t = Consume(name='消费者线程%d' % x)
        t.start()


if __name__ == '__main__':
    main()
Condition版本生产者和消费者模式

lock版生产者和消费者模式可以正常运行,但是存在一个缺点,在消费者中,总是通过while True循环并且上锁判断钱够不够,这很消耗cpu资源。threading.Condition可以在没有数据的情况下处于阻塞等待状态。一旦有合适的数据,还可以使用notify相关函数来通知其他处于等待状态的线程。这样可以减少一些不必要的操作,提高程序的性能。

  • acquire:上锁
  • release:解锁
  • wait:将当前线程处于等待状态,并且会释放锁,可以被其他线程使用notify和notify_all函数唤醒。被唤醒后继续等待上锁,上锁后继续执行下面的代码块
  • notify:通知某个正在等待的线程,默认是第一个等待的线程
  • notify_all:通知所有正在等待的线程。notify和notify_all不会释放锁,并且会在release之前调用
import threading
import random
import time

gMoney=0
gTime=0
gCondtion=threading.Condition()

class Producer(threading.Thread):
    def run(self) -> None:  #箭头是提示作用,代表返回得内容是none
        global gMoney
        global gTime
        while True:
            gCondtion.acquire()
            if gTime>=10:
                gCondtion.release()
                break
            money=random.randint(0,100)
            gMoney+=money
            gTime+=1
            print('%s生产了%d元'%(threading.current_thread().name,gMoney))
            gCondtion.notify_all()
            gCondtion.release()
            time.sleep(1)

class Consummer(threading.Thread):
    def run(self) -> None:
        global gMoney
        while True:
            gCondtion.acquire()
            money=random.randint(0,100)
            while gMoney<money:
                if gTime>=10:
                    print("%s想消费%d元钱,但是余额只有%d元,并且生产者已经不再生产了。"%(threading.current_thread().name,money,gMoney))
                    gCondtion.release()
                    return
                print("%s想消费%d元钱,但是余额只有%d元,消费失败"%(threading.current_thread().name,money,gMoney))
                gCondtion.wait()
            gMoney-=money
            print("%s消费了%d元钱,剩余%d元钱"%(threading.current_thread().name,money,gMoney))
            gCondtion.release()
            time.sleep(1)

def main():
    for i in range(10):
        th=Producer(name='生产者%d号'%i)
        th.start()
    for i in range(10):
        th=Consummer(name='消费者%d号'%i)
        th.start()
if __name__ == '__main__':
    main()
Queue线程安全队列

[线程安全]

参考百度百科https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8/9747724?fr=aladdin

在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列 中,那么Python内置了一个线程安全的模块叫做queue模块。Python中的queue模块中提供了同 步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列 LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能 够在多线程中直接使用。可以使用队列来实现线程间的同步。相关的函数如下: 初始化Queue(maxsize):创建一个先进先出的队列。

  1. qsize():返回队列的大小。
  2. empty():判断队列是否为空。
  3. full():判断队列是否满了。
  4. get():从队列中取最后一个数据。
  5. put():将一个数据放到队列中。
from queue import Queue

q = Queue(4)
q.put(1)
q.put(2)
print(q.qsize())
print(q.empty())
print(q.full())
print(q.get())
表情包之同步爬虫完成

传统方式:

# coding=gbk

import requests
import os
from lxml import etree
from urllib import request
import re


def parse_page(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44'
    }
    response = requests.get(url, headers=headers)
    # print(response.text)
    text = response.text
    html = etree.HTML(text)
    # imgs = html.xpath("//div[@class='page-content text-center']//img[@class!='gif']")
    imgs = html.xpath("//*[@id='pic-detail']/div/div[2]/div[2]/ul/li/div/div//img")
    for img in imgs:
        # print(etree.tostring(img))
        img_url = img.get('data-original')
        alt = img.get('alt')
        alt = re.sub(r'[\?\..。!!()]', '', alt)
        suffix = os.path.splitext(img_url)[1]
        filename = alt + suffix
        print(filename)
        # print(suffix)
        request.urlretrieve(img_url, 'images/' + filename)


def main():
    for x in range(2):
        url = 'https://www.doutula.com/photo/list/?page=%d' % x
        parse_page(url)
        print('Downloading')


if __name__ == '__main__':
    main()
    print('Done')

多线程下载王者荣耀高清壁纸

网址:https://pvp.qq.com/web201605/wallpaper.shtml

# coding=gbk
import requests
from urllib import parse, request
import os

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36 Edg/85.0.564.70',
    'Referer': "https://pvp.qq.com/web201605/wallpaper.shtml"

}


#
def exctract_images(data):
    images_urls = []
    for i in range(1, 9):
        images_url = parse.unquote(data['sProdImgNo_%d' % i]).replace('200', '0')
        images_urls.append(images_url)
    return images_urls


def main():
    # 获取页面
    page_url = "https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=20&totalpage=0&page=0&iOrder=0&iSortNumClose=1&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&_=1602240319647"
    resp = requests.get(page_url, headers=headers)
    # print(resp.text)
    # print(type(resp.json()))  字典类型
    # print(resp.json())
    result = resp.json()
    datas = result['List']
    # 获取每张图片的url
    for data in datas:
        image_urls = exctract_images(data)
        name = parse.unquote(data['sProdName'])
        dirpath = os.path.join('images', name)
        # images/小乔
        os.mkdir(dirpath)
        for index,image_url in enumerate(image_urls):
            request.urlretrieve(image_url, os.path.join(dirpath, '%d.jpg' % (index + 1)))
            print('%sDone'%(image_url))


if __name__ == '__main__':
    main()

多线程下载:

# coding=gbk
import requests
from urllib import parse, request
import os, threading, queue

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36 Edg/85.0.564.70',
    'Referer': "https://pvp.qq.com/web201605/wallpaper.shtml"

}


# 生产url
class Producer(threading.Thread):

    def __init__(self, page_queue, image_queue, *args, **kwargs):
        super(Producer, self).__init__(*args,**kwargs)
        self.page_queue = page_queue
        self.image_queue = image_queue

    def run(self) -> None:
        while not self.page_queue.empty():
            page_url = self.page_queue.get()
            resp = requests.get(page_url, headers=headers)

            result = resp.json()
            datas = result['List']
            # 获取每张图片的url
            for data in datas:
                image_urls = exctract_images(data)
                name = parse.unquote(data['sProdName']).replace('1:1', "").strip()
                dirpath = os.path.join('images', name)
                # images/小乔
                if not os.path.exists(dirpath):
                    os.mkdir(dirpath)
                for index,image_url in enumerate(image_urls):
                    self.image_queue.put({'image_url': image_url, 'image_path': os.path.join(dirpath,'%d.jpg'%(index+1))})


class Consumer(threading.Thread):
    def __init__(self, image_queue, *args, **kwargs):
        super(Consumer, self).__init__(*args,**kwargs)
        self.image_queue = image_queue

    def run(self) -> None:
        while True:
            image_obj = self.image_queue.get()
            image_url = image_obj.get('image_url')
            image_path = image_obj.get('image_path')
            request.urlretrieve(image_url, image_path)
            print(image_path+'下载完成!')


#
def exctract_images(data):
    images_urls = []
    for i in range(1, 9):
        images_url = parse.unquote(data['sProdImgNo_%d' % i]).replace('200', '0')
        images_urls.append(images_url)
    return images_urls


def main():
    page_queue = queue.Queue(23)
    image_queue = queue.Queue(1000)
    # 获取页面
    for x in range(0, 23):
        page_url = "https://apps.game.qq.com/cgi-bin/ams/module/ishow/V1.0/query/workList_inc.cgi?activityId=2735&sVerifyCode=ABCD&sDataType=JSON&iListNum=20&totalpage=0&page=[page]&iOrder=0&iSortNumClose=1&_everyRead=true&iTypeId=2&iFlowId=267733&iActId=2735&iModuleId=2735&" \
                   "_=1602240319647".format(page=x)
        page_queue.put(page_url)

        # 创建线程
        for x in range(3):
            th = Producer(page_queue, image_queue, name='生产者%d号' % x)
            th.start()

        for x in range(5):
            th = Consumer(image_queue,name='消费者%d号' % x)
            th.start()


if __name__ == '__main__':
    main()

什么是GIL:

Python自带的解释器是CPython。CPython解释器的多线程实际上是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时刻只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个东西叫做GIL(Global Intepreter Lock),叫做全局解释器锁。这个解释器锁是有必要的。因为CPython解释器的内存管理不是线程安全的。当然除了CPython解释器,还有其他的解释器,有些解释器是没有GIL锁的,见下面:

  1. Jython:用Java实现的Python解释器。不存在GIL锁。更多详情请见:https://zh.wikipedia.org/wiki/Jython
  2. IronPython:用.net实现的Python解释器。不存在GIL锁。更多详情请见:https://zh.wikipedia.org/wiki/IronPython
  3. PyPy:用Python实现的Python解释器。存在GIL锁。更多详情请见:https://zh.wikipedia.org/wiki/PyPy
    GIL虽然是一个假的多线程。但是在处理一些IO操作(比如文件读写和网络请求)还是可以在很大程度上提高效率的。在IO操作上建议使用多线程提高效率。在一些CPU计算操作上不建议使用多线程,而建议使用多进程。

有了GIL,为什么还需要Lock:

GIL只是保证全局同一时刻只有一个线程在执行,但是他并不能保证执行代码的原子性。也就是说一个操作可能会被分成几个部分完成,这样就会导致数据有问题。所以需要使用Lock来保证操作的原子性。

动态网页爬虫

什么是动态网页爬虫和AJAX技术:

  1. 动态网页,是网站在不重新加载的情况下,通过ajax技术动态更新网站中的局部数据。比如拉勾网的职位页面,在换页的过程中,url是没有发生改变的,但是职位数据动态的更改了。
  2. AJAX(Asynchronouse JavaScript And XML)异步JavaScript和XML。前端与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用Ajax)如果需要更新内容,必须重载整个网页页面。因为传统的在传输数据格式方面,使用的是XML语法。因此叫做AJAX,其实现在数据交互基本上都是使用JSON。使用AJAX加载的数据,即使使用了JS,将数据渲染到了浏览器中,在右键->查看网页源代码还是不能看到通过ajax加载的数据,只能看到使用这个url加载的html代码。

动态网页爬虫的解决方案:

  1. 直接分析ajax调用的接口。然后通过代码请求这个接口。
  2. 使用Selenium+chromedriver模拟浏览器行为获取数据。
方式优点缺点
分析接口直接请求到数据,不需要做一些解析工作,性能高。分析接口比较复杂,特别是一些通过js哄笑的接口,要有一定的js功底,容易被发现是爬虫
selenium直接模拟浏览器行为,浏览器能够请求的,使用selenium也能请求到,爬虫更稳定。代码量多,性能低。

selenium和chromedriver:

使用selenium关闭浏览器:

  1. driver.close():关闭当前的页面。
  2. driver.quit():关闭整个浏览器。

selenium定位元素:

  1. find_element_by_id:根据id来查找某个元素。
  2. find_element_by_class_name:根据类名查找元素。
  3. find_element_by_name:根据name属性的值来查找元素。
  4. find_element_by_tag_name:根据标签名来查找元素。
  5. find_element_by_xpath:根据xpath语法来获取元素。
  6. find_element_by_css_selector:根据css选择器选择元素。

要注意,find_element是获取第一个满足条件的元素。find_elements是获取所有满足条件的元素。

selenium表单操作:

  1. webelement.send_keys:给输入框填充内容。
  2. webelement.click:点击。
  3. 操作select标签:需要首先用from selenium.webdriver.support.ui import Select来包装一下选中的对象,才能进行select选择:
    • select_by_index:按索引进行选择。
    • select_by_value:按值进行选择。
    • select_by_visible_text:按照可见文本进行选择。

selenium行为链:

有时候在页面中的操作可能要有很多步,那么这时候可以使用鼠标行为链类selenium.webdriver.common.action_chains.ActionChains来完成。比如现在要将鼠标移动到某个元素上并执行点击事件。那么示例代码如下:

inputTag = driver.find_element_by_id('kw')
submitTag = driver.find_element_by_id('su')
actions = ActionChains(driver)
actions.move_to_element(inputTag)
actions.send_keys_to_element(inputTag,'python')
actions.move_to_element(submitTag)
actions.click(submitTag)
actions.perform()

还有更多的鼠标相关的操作。
click_and_hold(element):点击但不松开鼠标。
context_click(element):右键点击。
double_click(element):双击。

更多方法请参考:http://selenium-python.readthedocs.io/api.html

为什么需要行为链条?
因为有些网站可能会在浏览器端做一些验证行为是否符合人类的行为来做反爬虫。这时候我们就可以使用行为链来模拟人的操作。行为链有更多的复杂操作,比如双击,右键等,在自动化测试中非常有用。

操作cookie:

  1. 获取所有的cookie:

    for cookie in driver.get_cookies():
        print(cookie)
    
  2. 根据cookie的key获取value:

    value = driver.get_cookie(key)
    
  3. 删除所有的cookie:

    driver.delete_all_cookies()
    
  4. 删除某个cookie:

    driver.delete_cookie(key)
    
  5. 添加cookie:

    driver.add_cookie({“name”:”username”,”value”:”abc”})
    

隐式等待和显式等待:

  1. 隐式等待:指定一个时间,在这个时间内一直会处于等待状态。隐式等待需要使用driver.implicitly_wait

  2. 显式等待:指定在某个时间内,如果某个条件满足了,那么就不会再等待,如果在指定的时间内条件都不满足,那么就不会再等待了。显式等待用的方法是from selenium.webdriver.support.ui import WebDriverWait。示例代码如下:

    driver.get("https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc")
    WebDriverWait(driver,100).until(
        EC.text_to_be_present_in_element_value((By.ID,"fromStationText"),"长沙")
    )
    WebDriverWait(driver,100).until(
        EC.text_to_be_present_in_element_value((By.ID,"toStationText"),"北京")
    )
    btn = driver.find_element_by_id("query_ticket")
    btn.click()
    

打开新窗口和切换页面:

  1. selenium中没有专门的打开新窗口的方法,是通过window.execute_script()来执行js脚本的形式来打开新窗口的。

    window.execute_script("window.open('https://www.douban.com/')")
    
  2. 打开新的窗口后driver当前的页面依然还是之前的,如果想要获取新的窗口的源代码,那么就必须先切换过去。示例代码如下:

    window.switch_to.window(driver.window_handlers[1])
    

设置代理:

设置代理通过ChromeOptions来设置,示例代码如下:

options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=http://110.52.235.176:9999")
driver = webdriver.Chrome(executable_path="D:\ProgramApp\chromedriver\chromedriver73.exe",chrome_options=options)

driver.get("http://httpbin.org/ip")

补充:

  1. get_property:获取html标签中官方写好的属性。
  2. get_attribute:获取html标签中官方和非官方的属性。
  3. driver.save_screenshoot:获取当前页面的截图,有时候请求失败了,那么可以把当前网页的截图保存下来,方便后期进行分析。
12306_test

未完。。。 访问太多,被拒了。。

# coding=gbk
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

import csv

driver = webdriver.Chrome(executable_path='D:\Learn\chromedriver\chromedriver.exe')


class TrainSpider(object):
    # 类属性 使用时用self.访问
    login_url = "https://kyfw.12306.cn/otn/resources/login.html"
    person_url = "https://kyfw.12306.cn/otn/view/index.html"
    left_ticket = 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc'
    """
    _ init _方法, 也称为构造方法 ,创建对象时为对象初始化成员变量
    """

    def __init__(self, from_station, to_station, train_date, trains):
        """

        :param from_station: 起始站
        :param to_station: 目的地
        :param train_date: 出发日期
        9:商务座 M:一等座
        :param trains: 购买车次 {“G529”:[]}
        """
        # 抢票的参数设置参考页面,需要输入的车票信息
        self.from_station = from_station
        self.to_station = to_station
        self.train_date = train_date
        self.trains = trains
        # 若driver保存在类当中,类中的对象在内存销毁时也跟随销毁
        self.station_codes = {}
        self.init_station_code()

    def init_station_code(self):
        with open('stations.csv', 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for line in reader:
                name = line['name']
                code = line['code']
                # print('name:%s, code:%s' % (name, code))
                self.station_codes[name] = code
        print(self.station_codes)

    # selenium打开的浏览器不保存cookie信息
    def login(self):
        # 登录 访问登录url
        driver.get(self.login_url)
        # 跳转到个人中心
        WebDriverWait(driver, 1000).until(
            EC.url_contains(self.person_url)
        )
        print('Login Done')
        # 查询车票

    def search_left_ticket(self):
        # 跳转到查询车票页面
        driver.get(self.left_ticket)
        print('redirect successful')
        from_station_input = driver.find_element_by_id('fromStation')
        # 出发地
        from_station_code = self.station_codes[self.from_station]
        # 设置输入值
        driver.execute_script("arguments[0].value='%s'" % from_station_code, from_station_input)
        # 目的地
        to_station_input = driver.find_element_by_id('toStation')
        to_station_codes = self.station_codes[self.to_station]
        driver.execute_script("arguments[0].value='%s'" % to_station_codes, to_station_input)
        # 出发时间
        train_date_input = driver.find_element_by_id('train_date')
        driver.execute_script("arguments[0].value='%s'" % self.train_date, train_date_input)
        # train_date_input.send_keys(self.train_date)
        # 查询操作
        search_btn = driver.find_element_by_id('query_ticket')
        search_btn.click()

        # 解析车次
        WebDriverWait(driver, 1000).until(
            EC.presence_of_element_located((By.XPATH, "//tbody[@id='queryLeftTable']/tr"))
        )
        train_trs = driver.find_elements_by_xpath("//tbody[@id='queryLeftTable']/tr[not(@datatran)]")
        """
        解决代码重复:
                   number = infos[0]
            if number in self.trains:
                seat_type = self.trains[number]
                if seat_type == "O":
                    count = infos[9]
                    if count.isdigt() or count == "有":
                        order_btn = train_tr.find_elements_by_xpath(".//a[@class='btn72']")
                        order_btn.click()
                elif seat_type == "M":
                    count = infos[8]
                    if count.isdigt() or count == "有":
                        order_btn = train_tr.find_elements_by_xpath(".//a[@class='btn72']")
                        order_btn.click()
        """
        is_searched = False
        for train_tr in train_trs:
            # print(train_tr.text)
            infos = train_tr.text.replace("\n", " ").split(" ")
            # print(infos)
            number = infos[0]
            if number in self.trains:
                seat_type = self.trains[number]
                for seat_typ in seat_type:
                    if seat_typ == "O":
                        count = infos[9]
                        if count.isdigt() or count == "有":
                            is_searched = True
                            print('Done1')
                            break

                    elif seat_typ == "M":
                        count = infos[8]
                        if count.isdigt() or count == "有":
                            is_searched = True
                            print('Done2')
                            break

                if is_searched:
                    order_btn = train_tr.find_elements_by_xpath(".//a[@class='btn72']")
                    order_btn.click()
                    print('Done3')
                    break  # 退出最外层循环,查找到合适车次直接退出,不在解析后续车次信息

    # 使用run方法,将抢票流程封装。 类似汽车启动 只插钥匙,不考虑是先启动发动机还是其他
    def run(self):  # 看作指挥官,控制流程
        # 1. 登录
        self.login()
        # 2.跳转查询页面
        self.search_left_ticket()


def main():
    # main函数运行完成,里面的临时变量均被销毁
    spider = TrainSpider('北京', '长沙', '2020-10-10', {"G529": ["O", "M"]})
    spider.run()


if __name__ == '__main__':
    main()
有道翻译破解(半成品 。。后续更新)
# coding=gbk
import requests


def main():
    url = 'http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36 Edg/86.0.622.38',
        'Referer': "http://fanyi.youdao.com/",
        "Cooike": 'P_INFO=sami41209; OUTFOX_SEARCH_USER_ID=1068192493@10.108.160.105; OUTFOX_SEARCH_USER_ID_NCOO=985141423.2222542; JSESSIONID=abcLrhDMpX1O2bmVJJqux; _ntes_nnid=f2c10c9bce5d541b96ab34168bfa5717,1602300559389; ___rl__test__cookies=1602300619617',
        "Host": "fanyi.youdao.com",
        "Origin": "http: // fanyi.youdao.com"

    }
    data = {
        'i': 'hello',
        'from': 'AUTO',
        'to': 'AUTO',
        'smartresult': 'dict',
        'client': 'fanyideskweb',
        'salt': '16023006196268',
        'sign': '4c8bc1df05051395b17f137e740f0713',
        'lts': '1602300619626',
        'bv': '50bb7d203385bb098ccf4c3e9502c021',
        'doctype': 'json',
        'version': "2.1",
        'keyfrom': "fanyi.web",
        'action': 'FY_BY_REALTlME'
    }
    resp = requests.post(url, headers=headers, data=data)
    print(resp.text)


if __name__ == '__main__':
    main()

Scrapy框架:

写一个爬虫,需要做很多的事情。比如:发送网络请求、数据解析、数据存储、反反爬虫机制(更换ip代理、设置请求头等)、异步请求等。这些工作如果每次都要自己从零开始写的话,比较浪费时间。因此Scrapy把一些基础的东西封装好了,在他上面写爬虫可以变的更加的高效(爬取效率和开发效率)。因此真正在公司里,一些上了量的爬虫,都是使用Scrapy框架来解决。

安装Scrapy框架:

  1. pip install scrapy。
  2. 可能会出现问题:
    • 在ubuntu下要先使用以下命令安装依赖包:sudo apt-get install python3-dev build-essential python3-pip libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev,安装完成后再安装scrapy
    • 在windows下安装可能会提示No module named win32api,这时候先使用命令:pip install pypiwin32,安装完成后再安装scrapy
    • 在windows下安装Scrapy可能会提示twisted安装失败,那么可以到这个页面下载twisted文件:https://www.lfd.uci.edu/~gohlke/pythonlibs/,下载的时候要根据自己的Python版本来选择不同的文件。下载完成后,通过pip install xxx.whl

Scrapy框架架构:

  1. Scrapy Engine(引擎):Scrapy框架的核心部分。负责在Spider和ItemPipeline、Downloader、Scheduler中间通信、传递数据等。
  2. Spider(爬虫):发送需要爬取的链接给引擎,最后引擎把其他模块请求回来的数据再发送给爬虫,爬虫就去解析想要的数据。这个部分是我们开发者自己写的,因为要爬取哪些链接,页面中的哪些数据是需要的,都是由程序员自己决定。
  3. Scheduler(调度器):负责接收引擎发送过来的请求,并按照一定的方式进行排列和整理,负责调度请求的顺序等。
  4. Downloader(下载器):负责接收引擎传过来的下载请求,然后去网络上下载对应的数据再交还给引擎。
  5. Item Pipeline(管道):负责将Spider(爬虫)传递过来的数据进行保存。具体保存在哪里,应该看开发者自己的需求。
  6. Downloader Middlewares(下载中间件):可以扩展下载器和引擎之间通信功能的中间件。
  7. Spider Middlewares(Spider中间件):可以扩展引擎和爬虫之间通信功能的中间件。
  8. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MlSge1iy-1602319620301)(C:\Users\Samantha\AppData\Roaming\Typora\typora-user-images\image-20201010140546178.png)]

创建Scrapy项目:

  1. 创建项目:scrapy startproject [项目名称].

  2. 创建爬虫:cd到项目中->scrapy genspider [爬虫名称] [域名].

    C:\Users\Samantha\Desktop\PythonStudy\spider\scrapy_test\gushiwen>scrapy genspider gsww g ushiwen.org

项目文件作用:

  1. settings.py:用来配置爬虫的。
  2. middlewares.py:用来定义中间件。
  3. items.py:用来提前定义好需要下载的数据字段。
  4. pipelines.py:用来保存数据。
  5. scrapy.cfg:用来配置项目的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nua2mrCL-1602319684517)(C:\Users\Samantha\AppData\Roaming\Typora\typora-user-images\image-20201010164438416.png)]
gsww.py

import scrapy
from ..items import GushiwenItem


# 在类中写爬虫代码
class GswwSpider(scrapy.Spider):
    name = 'gsww'
    allowed_domains = ['gushiwen.org']
    start_urls = ['https://www.gushiwen.cn/default_1.aspx']  # 可以指定多个

    def myprint(self, value):
        print('*' * 30)
        print(value)
        print('*' * 30)

    def parse(self, response):
        # 抓取网页源代码
        # print(response.text)
        # response集成了方法
        # self.myprint(type(response))  # <class 'scrapy.http.response.html.HtmlResponse'>
        gsw_divs = response.xpath("//div[@class='left']/div[@class='sons']")
        # print(type(gsw_divs))
        for gsw_div in gsw_divs:
            # response.xpath提取出来的都是selector对象
            # self.myprint(type(gsw_div))   # <class 'scrapy.selector.unified.Selector'>
            title = gsw_div.xpath('.//b/text()').get()  # getall()查找全部,返回一个列表
            # print(type(title))
            # self.myprint(title)
            source = gsw_div.xpath(".//p[@class='source']/a/text()").getall()  # 朝代和作者
            if source:
                dynasty = source[0]
                author = source[1]
            else:
                continue
            # self.myprint(source)
            # self.myprint(len(source))
            content_list = gsw_div.xpath(".//div[@class='contson']//text()").getall()
            content = "".join(content_list).strip()
            # self.myprint(content)
            item = GushiwenItem(title=title, dynasty=dynasty, author=author, content=content)
            # 将查询的数据 存储起来
            yield item  # 将需要的东西发送给管道

        next_href = response.xpath("//a[@id='amore']/@href").get()
        if next_href:
            # print(next_href) /default_3.aspx
            next_url = response.urljoin(next_href)
            # self.myprint(next_url)  https://www.gushiwen.cn/default_3.aspx

            # 获取下一个url后需要发送给调度器
            request = scrapy.Request(next_url, dont_filter=True)
            yield request

items.py

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


# 在类中定义字段
class GushiwenItem(scrapy.Item):
    title= scrapy.Field()
    name = scrapy.Field()
    dynasty = scrapy.Field()
    author = scrapy.Field()
    content = scrapy.Field()

pipeitems.py

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html


# useful for handling different item types with a single interface
from itemadapter import ItemAdapter
import json


class GushiwenPipeline:
    def open_spider(self, spider):
        self.fp = open("古诗文.txt", 'w', encoding='utf-8')

    def process_item(self, item, spider):
        # print('*' * 30)
        # print(item)  # 先要变成json对象,将其转化成字典类型,才可被接收
        # print('*' * 30)
        self.fp.write(json.dumps(dict(item),ensure_ascii=False)+"\n")  # 写完一行后自动换行
        return item  # setting可以配置多个管道,若存在多个,return可让程序处理后面的pipeline

    def close_spilder(self, spider):
        self.fp.close()



settings.py

# Scrapy settings for gushiwen project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     https://docs.scrapy.org/en/latest/topics/settings.html
#     https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
#     https://docs.scrapy.org/en/latest/topics/spider-middleware.html

BOT_NAME = 'gushiwen'

SPIDER_MODULES = ['gushiwen.spiders']
NEWSPIDER_MODULE = 'gushiwen.spiders'

# Crawl responsibly by identifying yourself (and your website) on the user-agent
# USER_AGENT = 'gushiwen (+http://www.yourdomain.com)'

# Obey robots.txt rules
ROBOTSTXT_OBEY = False  # 即使网站不允许,我还是要逆天而行

# Configure maximum concurrent requests performed by Scrapy (default: 16)
# CONCURRENT_REQUESTS = 32

# Configure a delay for requests for the same website (default: 0)
# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
# DOWNLOAD_DELAY = 3
# The download delay setting will honor only one of:
# CONCURRENT_REQUESTS_PER_DOMAIN = 16
# CONCURRENT_REQUESTS_PER_IP = 16

# Disable cookies (enabled by default)
# COOKIES_ENABLED = False

# Disable Telnet Console (enabled by default)
# TELNETCONSOLE_ENABLED = False

# Override the default request headers:
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36 Edg/86.0.622.38',
    'Rerfer':'https://so.gushiwen.cn/shiwenv_b7820a12ebaa.aspx',
}

# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
# SPIDER_MIDDLEWARES = {
#    'gushiwen.middlewares.GushiwenSpiderMiddleware': 543,
# }

# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
# DOWNLOADER_MIDDLEWARES = {
#    'gushiwen.middlewares.GushiwenDownloaderMiddleware': 543,
# }

# Enable or disable extensions
# See https://docs.scrapy.org/en/latest/topics/extensions.html
# EXTENSIONS = {
#    'scrapy.extensions.telnet.TelnetConsole': None,
# }

# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {  # 使用管道使,先设置 值越低,优先级越高
    'gushiwen.pipelines.GushiwenPipeline': 300,
}

# Enable and configure the AutoThrottle extension (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/autothrottle.html
# AUTOTHROTTLE_ENABLED = True
# The initial download delay
# AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
# AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
# AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
# AUTOTHROTTLE_DEBUG = False

# Enable and configure HTTP caching (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
# HTTPCACHE_ENABLED = True
# HTTPCACHE_EXPIRATION_SECS = 0
# HTTPCACHE_DIR = 'httpcache'
# HTTPCACHE_IGNORE_HTTP_CODES = []
# HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

CrawlSpider爬虫:

  1. 作用:可以定义规则,让Scrapy自动的去爬取我们想要的链接。而不必跟Spider类一样,手动的yield Request。
  2. 创建:scrapy genspider -t crawl [爬虫名] [域名]
  3. 提取的两个类:
    • LinkExtrator:用来定义需要爬取的url规则。
    • Rule:用来定义这个url爬取后的处理方式,比如是否需要跟进,是否需要执行回调函数等。

Scrapy Shell:

在命令行中,进入到项目所在的路径。然后:
scrapy shell 链接
在这个里面,可以先去写提取的规则,没有问题后,就可以把代码拷贝到项目中。方便写代码。

使用twisted异步保存mysql数据:

  1. 使用twisted.enterprise.adbapi来创建一个连接对象:

    def __init__(self,mysql_config):
        self.dbpool = adbapi.ConnectionPool(
            mysql_config['DRIVER'],
            host=mysql_config['HOST'],
            port=mysql_config['PORT'],
            user=mysql_config['USER'],
            password=mysql_config['PASSWORD'],
            db=mysql_config['DATABASE'],
            charset='utf8'
        )
    
    @classmethod
    def from_crawler(cls,crawler):
        # 只要重写了from_crawler方法,那么以后创建对象的时候,就会调用这个方法来获取pipline对象
        mysql_config = crawler.settings['MYSQL_CONFIG']
        return cls(mysql_config)
    
  2. 在插入数据的函数中,使用runInteraction来运行真正执行sql语句的函数。示例代码如下:

    def process_item(self, item, spider):
        # runInteraction中除了传运行sql的函数,还可以传递参数给回调函数使用
        result = self.dbpool.runInteraction(self.insert_item,item)
        # 如果出现了错误,会执行self.insert_error函数
        result.addErrback(self.insert_error)
        return item
    
    def insert_item(self,cursor,item):
        sql = "insert into article(id,title,author,pub_time,content,origin) values(null,%s,%s,%s,%s,%s)"
        args = (item['title'],item['author'],item['pub_time'],item['content'],item['origin'])
        cursor.execute(sql,args)
    
    def insert_error(self,failure):
        print("="*30)
        print(failure)
        print("="*30)
    

Scrapy下载图片:

  1. 解析图片的链接。

  2. 定义一个item,上面有两个字段,一个是image_urls,一个是images。其中image_urls是用来存储图片的链接,由开发者把数据爬取下来后添加的。

  3. 使用scrapy.pipelines.images.ImagesPipeline来作为数据保存的pipeline。

  4. 在settings.py中设置IMAGES_SOTRE来定义图片下载的路径。

  5. 如果想要有更复杂的图片保存的路径需求,可以重写ImagePipeline的file_path方法,这个方法用来返回每个图片的保存路径。

  6. file_path方法没有item对象,所以我们还需要重写get_media_requests方法,来把item绑定到request上。示例代码如下:

    class ImagedownloadPipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
        media_requests = super(ImagedownloadPipeline, self).get_media_requests(item,info)
        for media_request in media_requests:
            media_request.item = item
        return media_requests
    

    def file_path(self, request, response=None, info=None):
    origin_path = super(ImagedownloadPipeline, self).file_path(request,response,info)
    title = request.item[‘title’]
    title = re.sub(r’[\/😗?"<>|]’,"",title)
    save_path = os.path.join(settings.IMAGES_STORE,title)
    if not os.path.exists(save_path):
    os.mkdir(save_path)
    image_name = origin_path.replace(“full/”,"")
    return os.path.join(save_path,image_name)

    
    
  7. 在创建文件夹的时候,要注意一些特殊字符是不允许作为文件夹的名字而存在的,那么我们就可以通过正则表达式来删掉。r'[\\/:\*\?"<>\|]'

下载器中间件:

下载器中间件是引擎和下载器之间通信的中间件。在这个中间件中我们可以设置代理、更换请求头等来达到反反爬虫的目的。要写下载器中间件,可以在下载器中实现两个方法。一个是process_request(self,request,spider),这个方法是在请求发送之前会执行,还有一个是process_response(self,request,response,spider),这个方法是数据下载到引擎之前执行。

  1. process_request(self,request,spider)方法:
    这个方法是下载器在发送请求之前会执行的。一般可以在这个里面设置随机代理ip等。

    1. 参数:
      • request:发送请求的request对象。
      • spider:发送请求的spider对象。
    2. 返回值:
      • 返回None:如果返回None,Scrapy将继续处理该request,执行其他中间件中的相应方法,直到合适的下载器处理函数被调用。
      • 返回Response对象:Scrapy将不会调用任何其他的process_request方法,将直接返回这个response对象。已经激活的中间件的process_response()方法则会在每个response返回时被调用。
      • 返回Request对象:不再使用之前的request对象去下载数据,而是根据现在返回的request对象返回数据。
      • 如果这个方法中抛出了异常,则会调用process_exception方法。
  2. process_response(self,request,response,spider)方法:
    这个是下载器下载的数据到引擎中间会执行的方法。

    1. 参数:
      • request:request对象。
      • response:被处理的response对象。
      • spider:spider对象。
    2. 返回值:
      • 返回Response对象:会将这个新的response对象传给其他中间件,最终传给爬虫。
      • 返回Request对象:下载器链被切断,返回的request会重新被下载器调度下载。
      • 如果抛出一个异常,那么调用request的errback方法,如果没有指定这个方法,那么会抛出一个异常。

Scrapy中设置代理:

  1. 设置普通代理:

    class IPProxyDownloadMiddleware(object):
        PROXIES = [
         "5.196.189.50:8080",
        ]
        def process_request(self,request,spider):
            proxy = random.choice(self.PROXIES)
            print('被选中的代理:%s' % proxy)
            request.meta['proxy'] = "http://" + proxy
    
  2. 设置独享代理:

    class IPProxyDownloadMiddleware(object):
        def process_request(self,request,spider):
            proxy = '121.199.6.124:16816'
            user_password = "970138074:rcdj35xx"
            request.meta['proxy'] = proxy
            # bytes
            b64_user_password = base64.b64encode(user_password.encode('utf-8'))
            request.headers['Proxy-Authorization'] = 'Basic ' + b64_user_password.decode('utf-8')
    
  3. 代理服务商:

    • 芝麻代理:http://http.zhimaruanjian.com/
    • 太阳代理:http://http.taiyangruanjian.com/
    • 快代理:http://www.kuaidaili.com/
    • 讯代理:http://www.xdaili.cn/
    • 蚂蚁代理:http://www.mayidaili.com/
    • 极光代理:http://www.jiguangdaili.com/

分布式爬虫:

redis配置:
  1. 在ubuntu上安装redis:sudo apt install redis-server

  2. 连接reids服务器:redis-cli -h [ip地址] -p [端口号]

  3. 在其他电脑上连接本机的redis服务器:在/etc/redis/redis.conf中,修改bind,把redis服务器的ip地址加进去。示例如下:

    bind 192.168.175.129 127.0.0.1
    
  4. vim:有可能没有。那么通过sudo apt install vim就可以安装了。

  5. 虚拟机安装:vmware+ubuntu16.04.iso来安装。安装的时候,设置root用户的密码,用useradd命令来创建一个普通用户。后期方便通过xshell来连接。ubuntu不允许外面直接用root用户链接,那么我们可以先用普通用户连接,然后再切换到root用户。

  6. 修改代码:

    
    

爬虫部署:

  1. 在服务器上安装scrapyd:pip3 install scrapyd

  2. /usr/local/lib/python3.5/dist-packages/scrapyd下拷贝出default_scrapyd.conf放到/etc/scrapyd/scrapyd.conf

  3. 修改/etc/scrapyd/scrapyd.conf中的bind_address为自己的IP地址。

  4. 重新安装twisted

    pip uninstall twisted
    pip install twisted==18.9.0
    

    如果这一步不做,后期会出现intxxx的错误。

  5. 在开发机上(自己的window电脑上)安装pip install scrapyd-client

  6. 修改python/Script/scrapyd-deployscrapyd-deploy.py

  7. 在项目中,找到scrapy.cfg,然后配置如下:

    [settings]
    default = lianjia.settings
    
    [deploy]
    # 下面这个url要取消注释
    url = http://服务器的IP地址:6800/
    project = lianjia
    
  8. 在项目所在的路径执行命令生成版本号并上传爬虫代码:scrapyd-deploy。如果一次性想要把代码上传到多个服务器,那么可以修改scrapy.cfg为如下:

    [settings]
    default = lianjia.settings
    
    [deploy:服务器1]
    # 下面这个url要取消注释
    url = http://服务器1的IP地址:6800/
    project = lianjia
    
    [deploy:服务器2]
    # 下面这个url要取消注释
    url = http://服务器2的IP地址:6800/
    project = lianjia
    

    然后使用scrapyd-deploy -a就可以全部上传了。

  9. curl for windows下载地址:https://curl.haxx.se/windows/,解压后双击打开bin/curl.exe即可在cmd中使用了。

  10. 在cmd中使用命令运行爬虫:

    curl http://服务器IP地址:6800/schedule.json -d project=lianjia -d spider=house
    
  11. 如果后期修改了爬虫的代码,那么需要重新部署,然后服务器的scrapyd服务重新启动一下。

  12. 更多的API介绍:https://scrapyd.readthedocs.io/en/stable/api.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值