python爬虫入门

第一章:网络请求

1.1 虚拟环境

1.1.1 为什么需要虚拟环境

直接通过pip install xx安装的第三方包会存入系统级Python环境,若不同项目依赖不同版本的包(如Django 1.10和Django 0.9),会因版本不兼容导致项目无法正常运行。虚拟环境可解决多版本包共存的问题。

1.1.2 虚拟环境原理

虚拟环境如同独立的"抽屉",每个抽屉内安装的包相互隔离,可给不同项目指定对应的虚拟环境,保证项目依赖的包版本独立且不冲突。

1.1.3 安装virtualenv

通过pip/pip3安装:

bash 复制代码
pip install virtualenv
# 或针对Python3
pip3 install virtualenv

1.1.4 创建虚拟环境

bash 复制代码
virtualenv [虚拟环境名称]
  • 解释器版本由系统环境变量中Python2/Scripts和Python3/Scripts的顺序决定。

  • 手动指定解释器版本:

    bash 复制代码
    virtualenv -p C:\Python36\python.exe [虚拟环境名称]

1.1.5 进入/退出虚拟环境

系统 进入虚拟环境 退出虚拟环境
Windows 进入虚拟环境Scripts文件夹,执行activate deactivate
*nix source /path/to/virtualenv/bin/activate deactivate

1.1.6 virtualenvwrapper(简化虚拟环境管理)

安装
  • *nix:pip install virtualenvwrapper
  • Windows:pip install virtualenvwrapper-win
基本使用
功能 命令
创建虚拟环境 mkvirtualenv my_env
切换虚拟环境 workon my_env
退出虚拟环境 deactivate
删除虚拟环境 rmvirtualenv my_env
列出所有虚拟环境 lsvirtualenv
进入虚拟环境目录 cdvirtualenv
指定Python版本创建 mkvirtualenv --python==C:\Python36\python.exe hy_env
修改默认路径

在系统环境变量中添加WORKON_HOME,设置自定义存储路径。

1.2 爬虫前奏

1.2.1 爬虫实际例子

搜索引擎(百度、谷歌)、伯乐在线、惠惠购物助手、数据分析工具、抢票软件等。

1.2.2 网络爬虫定义

  • 通俗理解:模拟人类请求网站的程序,自动请求网页、抓取数据并提取有价值信息。
  • 专业定义:通过程序自动化获取互联网网页内容的技术。

1.2.3 通用爬虫和聚焦爬虫

  • 通用爬虫:搜索引擎核心组件,下载网页形成互联网内容镜像备份,无针对性。
  • 聚焦爬虫:面向特定需求,抓取时筛选内容,仅获取与需求相关的网页信息。

1.2.4 为什么用Python写爬虫

语言 劣势
PHP 多线程/异步支持差,并发处理能力弱
Java 代码量大、笨重,重构成本高
C/C++ 学习和开发成本高,开发效率低
Python 语法简洁、开发效率高,爬虫模块/框架丰富(Scrapy、Scrapy-redis)

1.2.5 爬虫准备工具

  • Python3.6开发环境
  • Pycharm 2017 professional版
  • 虚拟环境(virtualenv/virtualenvwrapper)
  • HTTP协议知识和Chrome抓包工具

1.3 HTTP协议和Chrome浏览器

1.3.1 HTTP和HTTPS协议

  • HTTP:超文本传输协议,端口80,明文传输。
  • HTTPS:HTTP的加密版本(SSL层),端口443,安全传输。

1.3.2 浏览器发送HTTP请求的过程

  1. 输入URL并回车,浏览器发送Request请求获取目标HTML文件;
  2. 服务器返回Response文件,浏览器解析HTML,发现依赖的图片/CSS/JS等文件;
  3. 浏览器自动发送Request请求获取这些依赖文件;
  4. 所有文件下载完成后,网页完整渲染。

1.3.3 URL详解

URL格式:scheme://host:port/path/?query-string=xxx#anchor

组成部分 说明
scheme 访问协议(http/https/ftp等)
host 主机名/域名(如www.baidu.com
port 端口号(默认80)
path 查找路径(如/Trending/now)
query-string 查询字符串(如wd=python)
anchor 锚点(前端页面定位用,后台不处理)
  • 特殊字符(中文等)会被浏览器编码为%+十六进制码值

1.3.4 常用请求方法

  • GET:仅从服务器获取数据,不修改服务器资源(如查询数据)。
  • POST:向服务器发送数据,修改服务器资源(如登录、上传文件)。
  • 注:部分网站会反爬虫,可能混用GET/POST。

1.3.5 请求头常见参数

参数 作用
User-Agent 标识请求来源(伪装浏览器避免被识别为爬虫)
Referer 标识请求来源URL(反爬虫常用)
Cookie 维持会话状态(登录验证必备)

1.3.6 常见响应状态码

状态码 含义
200 请求成功
301 永久重定向
302 临时重定向
400 请求URL错误
403 服务器拒绝访问
500 服务器内部错误

1.3.7 Chrome抓包工具

右键网页 → 检查 → 开发者工具,可查看网络请求、请求参数、响应数据等。

1.4 urllib库

urllib是Python内置网络请求库,可模拟浏览器发送请求、获取响应。

1.4.1 urlopen函数

基础用法:

python 复制代码
from urllib import request
# 发送GET请求
resp = request.urlopen('http://www.baidu.com')
# 读取响应内容(字节流)
print(resp.read())
  • 参数:url(请求地址)、data(POST请求数据,传入则为POST请求)。
  • 返回值:HTTPResponse对象,支持read()/readline()/getcode()等方法。

1.4.2 urlretrieve函数

下载文件到本地:

python 复制代码
from urllib import request
# 下载百度首页到本地baidu.html
request.urlretrieve('http://www.baidu.com/', 'baidu.html')

1.4.3 urlencode/parse_qs函数

  • urlencode:将字典转为URL编码字符串:

    python 复制代码
    from urllib import parse
    data = {'name':'爬虫基础','greet':'hello world','age':100}
    qs = parse.urlencode(data)
    print(qs)  # 输出:name=%E7%88%AC%E8%99%AB%E5%9F%BA%E7%A1%80&greet=hello+world&age=100
  • parse_qs:解码URL编码字符串:

    python 复制代码
    from urllib import parse
    qs = "name=%E7%88%AC%E8%99%AB%E5%9F%BA%E7%A1%80&greet=hello+world&age=100"
    print(parse.parse_qs(qs))  # 输出:{'name': ['爬虫基础'], 'greet': ['hello world'], 'age': ['100']}

1.4.4 urlparse/urlsplit函数

分割URL组成部分:

python 复制代码
from urllib import parse
url = 'http://www.baidu.com/s?username=zhiliao'
result = parse.urlsplit(url)
# 或 urlparse(多一个params属性,用于解析URL中;后的参数)
print('scheme:', result.scheme)  # http
print('netloc:', result.netloc)  # www.baidu.com
print('path:', result.path)      # /s
print('query:', result.query)    # username=zhiliao

1.4.5 Request类(添加请求头)

python 复制代码
from urllib import request
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
}
# 构造带请求头的请求
req = request.Request("http://www.baidu.com/", headers=headers)
resp = request.urlopen(req)
print(resp.read())

1.4.6 ProxyHandler(代理设置)

避免IP被封禁:

python 复制代码
from urllib import request
# 构造代理处理器
handler = request.ProxyHandler({"http":"218.66.161.88:31769"})
opener = request.build_opener(handler)
req = request.Request("http://httpbin.org/ip")
resp = opener.open(req)
print(resp.read())

常用免费代理平台:西刺免费代理IP、快代理、代理云。

1.4.7 Cookie处理

Cookie原理

HTTP无状态,Cookie用于标识用户身份,第一次登录后服务器返回Cookie,后续请求自动携带。

手动携带Cookie请求
python 复制代码
from urllib import request
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
    'Cookie': 'xxx'  # 从浏览器复制的Cookie
}
url = 'http://www.renren.com/880151247/profile'
req = request.Request(url, headers=headers)
resp = request.urlopen(req)
with open('renren.html','w') as fp:
    fp.write(resp.read().decode('utf-8'))
自动管理Cookie(CookieJar)
python 复制代码
from urllib import request, parse
from http.cookiejar import CookieJar

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

def get_opener():
    cookiejar = CookieJar()
    handler = request.HTTPCookieProcessor(cookiejar)
    opener = request.build_opener(handler)
    return opener

def login_renren(opener):
    data = {"email": "970138074@qq.com", "password": "pythonspider"}
    data = parse.urlencode(data).encode('utf-8')
    login_url = "http://www.renren.com/PLogin.do"
    req = request.Request(login_url, headers=headers, data=data)
    opener.open(req)

def visit_profile(opener):
    url = 'http://www.renren.com/880151247/profile'
    req = request.Request(url,headers=headers)
    resp = opener.open(req)
    with open('renren.html','w') as fp:
        fp.write(resp.read().decode('utf-8'))

if __name__ == '__main__':
    opener = get_opener()
    login_renren(opener)
    visit_profile(opener)
Cookie保存/加载
  • 保存Cookie到本地:

    python 复制代码
    from urllib import request
    from http.cookiejar import MozillaCookieJar
    cookiejar = MozillaCookieJar("cookie.txt")
    handler = request.HTTPCookieProcessor(cookiejar)
    opener = request.build_opener(handler)
    req = request.Request('http://httpbin.org/cookies', headers=headers)
    resp = opener.open(req)
    cookiejar.save(ignore_discard=True, ignore_expires=True)
  • 从本地加载Cookie:

    python 复制代码
    from urllib import request
    from http.cookiejar import MozillaCookieJar
    cookiejar = MozillaCookieJar("cookie.txt")
    cookiejar.load(ignore_expires=True, ignore_discard=True)
    handler = request.HTTPCookieProcessor(cookiejar)
    opener = request.build_opener(handler)
    req = request.Request('http://httpbin.org/cookies', headers=headers)
    resp = opener.open(req)
    print(resp.read())

1.5 requests库

requests是第三方库,比urllib更简洁易用,主打"HTTP for Humans"。

1.5.1 安装

bash 复制代码
pip install requests

文档:http://docs.python-requests.org/zh_CN/latest/

1.5.2 发送GET请求

python 复制代码
import requests
# 基础GET请求
response = requests.get("http://www.baidu.com/")

# 带查询参数和请求头
kw = {'wd':'中国'}
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"}
response = requests.get("http://www.baidu.com/s", params=kw, headers=headers)

# 响应属性
print(response.text)       # Unicode格式响应内容
print(response.content)    # 字节流响应内容
print(response.url)        # 完整URL
print(response.encoding)   # 响应编码
print(response.status_code)# 响应状态码

1.5.3 发送POST请求

python 复制代码
import requests
url = "https://www.lagou.com/jobs/positionAjax.json?city=%E6%B7%B1%E5%9C%B3&needAddtionalResult=false&isSchoolJob=0"
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
    'Referer': 'https://www.lagou.com/jobs/list_python?labelWords=&fromSearch=true&suginput='
}
data = {
    'first': 'true',
    'pn': 1,
    'kd': 'python'
}
# POST请求(data直接传字典,无需urlencode)
resp = requests.post(url, headers=headers, data=data)
print(resp.json())  # 直接解析JSON响应

1.5.4 使用代理

python 复制代码
import requests
url = "http://httpbin.org/get"
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
}
proxy = {
    'http': '171.14.209.180:27829'
}
resp = requests.get(url, headers=headers, proxies=proxy)

1.5.5 Cookie和Session

  • 获取响应Cookie:

    python 复制代码
    import requests
    resp = requests.get('http://www.baidu.com/')
    print(resp.cookies)          # Cookie对象
    print(resp.cookies.get_dict())  # 转为字典
  • Session共享Cookie(模拟登录):

    python 复制代码
    import requests
    url = "http://www.renren.com/PLogin.do"
    data = {"email":"970138074@qq.com",'password':"pythonspider"}
    headers = {
        'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"
    }
    # 创建Session对象,自动维护Cookie
    session = requests.session()
    session.post(url, data=data, headers=headers)
    # 访问需要登录的页面
    resp = session.get('http://www.renren.com/880151247/profile')
    print(resp.text)

1.5.6 处理不信任的SSL证书

python 复制代码
import requests
# verify=False 跳过SSL证书验证
resp = requests.get('http://www.12306.cn/mormhweb/', verify=False)
print(resp.content.decode('utf-8'))

第二章:数据提取

2.1 XPath语法与lxml库

2.1.1 什么是XPath

XPath(XML Path Language)是一门在XML和HTML文档中查找信息的语言,可遍历文档中的元素和属性,是爬虫提取页面数据的核心工具之一。

2.1.2 XPath开发工具

  • Chrome插件:XPath Helper
  • Firefox插件:Try XPath

2.1.3 XPath核心语法

1. 选取节点
表达式 描述 示例 结果
nodename 选取此节点的所有子节点 bookstore 选取bookstore下所有子节点
/ 根节点选取(最前面)或子节点选取 /bookstore 选取根元素下所有bookstore节点
// 全局选取(任意位置) //book 选取所有book节点
@ 选取节点属性 //book[@price] 选取所有含price属性的book节点
. 当前节点 ./a 选取当前节点下的a标签
2. 谓语(筛选特定节点)

谓语嵌在方括号中,用于精准匹配节点:

路径表达式 描述
/bookstore/book[1] 选取bookstore下第一个book子元素
/bookstore/book[last()] 选取bookstore下最后一个book元素
/bookstore/book[position()❤️] 选取bookstore下前两个book元素
//book[@price=10] 选取price属性等于10的所有book元素
3. 通配符
通配符 描述 示例 结果
* 匹配任意节点 /bookstore/* 选取bookstore下所有子元素
@* 匹配任意属性 //book[@*] 选取所有带属性的book元素
4. 选取多个路径

使用|运算符同时选取多个路径:

xpath 复制代码
//bookstore/book | //book/title
# 选取所有book元素及book下的所有title元素

2.1.4 lxml库(高性能解析器)

lxml是基于C实现的HTML/XML解析库,支持XPath语法,能自动修复不规范的HTML代码。

1. 安装
bash 复制代码
pip install lxml

官方文档:http://lxml.de/index.html

2. 基本使用
(1)解析字符串(自动补全标签)
python 复制代码
from lxml import etree 

text = '''
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a> # 缺少闭合标签
     </ul>
 </div>
'''

# 解析为HTML文档
html = etree.HTML(text) 
# 序列化HTML(自动补全li、html、body标签)
result = etree.tostring(html) 
print(result.decode('utf-8'))
(2)从文件读取HTML
python 复制代码
from lxml import etree

# 读取外部文件(hello.html)
html = etree.parse('hello.html')
# 格式化输出
result = etree.tostring(html, pretty_print=True)
print(result.decode('utf-8'))
3. 结合XPath提取数据

基于hello.html(包含5个li标签)的示例:

python 复制代码
from lxml import etree

html = etree.parse('hello.html')

# 1. 获取所有li标签
li_list = html.xpath('//li')
print(li_list)  # 返回元素对象列表

# 2. 获取所有li的class属性值
li_classes = html.xpath('//li/@class')
print(li_classes)  # ['item-0', 'item-1', 'item-inactive', 'item-1', 'item-0']

# 3. 获取li下href为指定值的a标签
target_a = html.xpath('//li/a[@href="link2.html"]')
print(target_a)

# 4. 获取li下所有span标签(//匹配子孙节点,/仅匹配子节点)
span_list = html.xpath('//li//span')
print(span_list)

# 5. 获取最后一个li的a标签href属性
last_href = html.xpath('//li[last()]/a/@href')
print(last_href)  # ['link5.html']

# 6. 获取倒数第二个li的a标签文本(两种方式)
# 方式1:通过元素对象的text属性
second_last_text1 = html.xpath('//li[last()-1]/a')[0].text
# 方式2:直接通过XPath提取文本
second_last_text2 = html.xpath('//li[last()-1]/a/text()')[0]
print(second_last_text1, second_last_text2)  # fourth item fourth item
4. 实战:爬取电影天堂数据
python 复制代码
import requests
from lxml import etree

BASE_DOMAIN = 'http://www.dytt8.net'
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
    'Referer': 'http://www.dytt8.net/html/gndy/dyzz/list_23_2.html'
}

def spider():
    """爬取列表页,获取详情页链接"""
    url = 'http://www.dytt8.net/html/gndy/dyzz/list_23_1.html'
    resp = requests.get(url, headers=HEADERS)
    text = resp.content.decode('gbk')  # 电影天堂编码为gbk
    tree = etree.HTML(text)
    all_a = tree.xpath("//div[@class='co_content8']//a")
    for a in all_a:
        title = a.xpath("text()")[0]
        href = a.xpath("@href")[0]
        if href.startswith('/'):
            detail_url = BASE_DOMAIN + href
            crawl_detail(detail_url)
            break  # 仅爬取第一条示例

def crawl_detail(url):
    """解析详情页,提取电影信息"""
    resp = requests.get(url, headers=HEADERS)
    text = resp.content.decode('gbk')
    tree = etree.HTML(text)
    # 提取发布时间
    create_time = tree.xpath("//div[@class='co_content8']/ul/text()")[0].strip()
    # 提取海报和截图
    imgs = tree.xpath("//div[@id='Zoom']//img/@src")
    cover = imgs[0] if imgs else ''  # 海报
    screenshoot = imgs[1] if len(imgs)>=2 else ''  # 截图
    # 提取详情信息(年代、评分、主演等)
    infos = tree.xpath("//div[@id='Zoom']//text()")
    for index, info in enumerate(infos):
        if info.startswith("◎年  代"):
            year = info.replace("◎年  代", "").strip()
            print(f"年代:{year}")
        if info.startswith("◎豆瓣评分"):
            douban_rating = info.replace("◎豆瓣评分", "").strip()
            print(f"豆瓣评分:{douban_rating}")
        if info.startswith("◎主  演"):
            actors = [info.replace("◎主  演", "").strip()]
            # 遍历后续文本,直到遇到下一个◎开头的标签
            for x in range(index+1, len(infos)):
                actor = infos[x].strip()
                if actor.startswith("◎"):
                    break
                if actor:
                    actors.append(actor)
            print(f"主演:{','.join(actors)}")

if __name__ == '__main__':
    spider()

2.1.5 注意事项

Chrome 62版本存在302重定向时无法记录FormData的bug,可升级至金丝雀版本(https://www.google.com/chrome/browser/canary.html)解决。

2.2 BeautifulSoup4库

2.2.1 简介

BeautifulSoup4(简称bs4)是HTML/XML解析库,基于DOM树解析,API人性化,支持CSS选择器,缺点是性能低于lxml。

2.2.2 安装与文档

2.2.3 解析工具对比

解析工具 解析速度 使用难度
BeautifulSoup4 最慢 最简单
lxml 简单
正则表达式 最快 最难

2.2.4 基本使用

python 复制代码
from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" 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 href="http://example.com/elsie" class="sister" id="link1"><!-- 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>
"""

# 创建BeautifulSoup对象(指定lxml解析器)
soup = BeautifulSoup(html, "lxml")
# 格式化输出HTML
print(soup.prettify())

2.2.5 四大核心对象

1. Tag(标签对象)

对应HTML中的标签,可通过soup.标签名获取第一个匹配的标签:

python 复制代码
# 获取标签
title_tag = soup.title  # <title>The Dormouse's story</title>
a_tag = soup.a  # 第一个a标签

# 标签属性
print(title_tag.name)  # title(标签名)
print(soup.p.attrs)  # {'class': ['title'], 'name': 'dromouse'}(所有属性)
print(soup.p['class'])  # ['title'](获取指定属性)
soup.p['class'] = 'new-title'  # 修改属性

获取标签内的文本内容:

python 复制代码
p_text = soup.p.string  # The Dormouse's story
print(type(p_text))  # <class 'bs4.element.NavigableString'>
3. BeautifulSoup(文档对象)

代表整个HTML文档,可视为特殊的Tag,name为[document]

python 复制代码
print(soup.name)  # [document]
4. Comment(注释对象)

特殊的NavigableString,对应HTML注释内容:

python 复制代码
markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup, 'lxml')
comment = soup.b.string
print(type(comment))  # <class 'bs4.element.Comment'>
print(comment)  # Hey, buddy. Want to buy a used parser?

2.2.6 遍历文档树

1. contents/children(子节点)
python 复制代码
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<p class="title"><b>The Dormouse's story</b></p>
"""
soup = BeautifulSoup(html_doc, 'lxml')
head_tag = soup.head

# contents:返回子节点列表
print(head_tag.contents)  # [<title>The Dormouse's story</title>]

# children:返回子节点迭代器
for child in head_tag.children:
    print(child)  # <title>The Dormouse's story</title>
2. strings/stripped_strings(文本遍历)
python 复制代码
# strings:获取所有文本(含空白)
for string in soup.strings:
    print(repr(string))

# stripped_strings:去除空白文本
for string in soup.stripped_strings:
    print(repr(string))

2.2.7 搜索文档树

1. find/find_all(核心方法)
  • find:返回第一个匹配的标签
  • find_all:返回所有匹配的标签列表
python 复制代码
# 按标签+属性查找
a_list = soup.find_all("a", attrs={"id": "link2"})
# 简写(直接传属性名)
a_list = soup.find_all("a", id="link2")
2. select(CSS选择器)

支持CSS语法查找标签,返回列表:

python 复制代码
# 1. 按标签名
print(soup.select('a'))

# 2. 按类名(.类名)
print(soup.select('.sister'))

# 3. 按ID(#ID名)
print(soup.select('#link1'))

# 4. 组合查找
print(soup.select("p #link1"))  # p标签下的#link1
print(soup.select("head > title"))  # head的直接子标签title

# 5. 按属性
print(soup.select('a[href="http://example.com/elsie"]'))

# 6. 提取文本(get_text())
for title in soup.select('title'):
    print(title.get_text())

2.3 正则表达式和re模块

2.3.1 什么是正则表达式

正则表达式是一套字符匹配规则,用于从字符串中精准匹配/提取符合规则的数据。

2.3.2 常用匹配规则

1. 基础匹配符
符号 描述 示例 匹配结果
普通字符 匹配自身 re.match('he', 'hello') 'he'
. 匹配任意字符(不含换行) re.match('.', 'ab') 'a'
\d 匹配数字(0-9) re.match('\d', '123') '1'
\D 匹配非数字 re.match('\D', 'a') 'a'
\s 匹配空白符(\n/\t/\r/空格) re.match('\s', '\t') '\t'
\w 匹配字母/数字/下划线 re.match('\w', '_') '_'
\W 匹配非\w字符 re.match('\W', '+') '+'
[] 匹配括号内任意字符 re.match('[\d\-]+', '0731-88888888') '0731-88888888'
2. 数量匹配符
符号 描述 示例 匹配结果
* 匹配0或多个 re.match('\d*', '0731') '0731'
+ 匹配1或多个 re.match('\w+', 'abc') 'abc'
? 匹配0或1个 re.match('\d?', '123') '1'
{m} 匹配m个 re.match('\d{2}', '123') '12'
{m,n} 匹配m-n个 re.match('\d{1,2}', '123') '12'
3. 边界匹配符
符号 描述 示例 匹配结果
^ 以指定字符开头 re.match('^h', 'hello') 'h'
$ 以指定字符结尾 re.search('\w+@163\.com$', 'xxx@163.com') 'xxx@163.com'
匹配多个表达式 `re.search('hello
4. 贪婪/非贪婪模式
  • 贪婪模式(默认):匹配尽可能多的字符

    python 复制代码
    re.match('\d+', '0123456').group()  # '0123456'
  • 非贪婪模式(加?):匹配尽可能少的字符

    python 复制代码
    re.match('\d+?', '0123456').group()  # '0'

2.3.3 经典匹配案例

python 复制代码
import re

# 1. 验证手机号(1开头,第二位3/4/5/8/7,后9位数字)
text = "18570631587"
ret = re.match('1[34587]\d{9}', text)
print(ret.group())  # 18570631587

# 2. 验证邮箱
text = "hynever@163.com"
ret = re.match('\w+@\w+\.[a-zA-Z\.]+', text)
print(ret.group())  # hynever@163.com

# 3. 验证URL
text = "http://www.baidu.com/"
ret = re.match('(http|https|ftp)://[^\s]+', text)
print(ret.group())  # http://www.baidu.com/

# 4. 验证身份证(18位,最后一位可为数字/x/X)
text = "3113111890812323X"
ret = re.match('\d{17}[\dxX]', text)
print(ret.group())  # 3113111890812323X

# 5. 匹配0-100数字
text = '99'
ret = re.match('[1-9]?\d$|100$', text)
print(ret.group())  # 99

2.3.4 转义字符与原生字符串

  • 转义字符:特殊符号(如$/\)需用\转义
  • 原生字符串:加r前缀,避免多次转义(推荐)
python 复制代码
# 匹配$符号
text = "apple price is $99"
ret = re.search('\$(\d+)', text)
print(ret.group())  # $99

# 匹配\c(原生字符串)
text = "apple \c"
ret = re.search(r'\\c', text)
print(ret.group())  # \c

2.3.5 re模块核心函数

1. match(从开头匹配)
python 复制代码
text = 'hello'
ret = re.match('h', text)
print(ret.group())  # h

# 匹配换行(flag=re.DOTALL)
text = "abc\nabc"
ret = re.match('abc.*abc', text, re.DOTALL)
print(ret.group())  # abc\nabc
2. search(全局匹配第一个)
python 复制代码
text = 'apple price $99 orange price $88'
ret = re.search('\d+', text)
print(ret.group())  # 99

# 分组提取(()分组)
text = "apple price $99,orange price $10"
ret = re.search(r".*(\$\d+).*(\$\d+)", text)
print(ret.group())    # 整体:apple price $99,orange price $10
print(ret.group(1))   # 第一个分组:$99
print(ret.group(2))   # 第二个分组:$10
print(ret.groups())   # 所有分组:('$99', '$10')
3. findall(匹配所有结果)
python 复制代码
text = 'apple price $99 orange price $88'
ret = re.findall('\d+', text)
print(ret)  # ['99', '88']
4. sub(替换匹配内容)
python 复制代码
text = 'apple price $99 orange price $88'
ret = re.sub('\d+', '0', text)
print(ret)  # apple price $0 orange price $0

# 实战:清理HTML标签
html = "<p>1、精通HTML5、CSS3、JavaScript</p>"
ret = re.sub('</?[a-zA-Z0-9]+>', "", html)
print(ret)  # 1、精通HTML5、CSS3、JavaScript
5. split(按规则分割字符串)
python 复制代码
text = "hello world ni hao"
ret = re.split('\W', text)
print(ret)  # ['hello', 'world', 'ni', 'hao']
6. compile(预编译正则)

提升重复匹配效率,支持注释(re.VERBOSE):

python 复制代码
text = "the number is 20.50"
# 预编译(带注释)
pattern = re.compile(r"""
    \d+  # 整数部分
    \.?  # 小数点(可选)
    \d*  # 小数部分(可选)
""", re.VERBOSE)
ret = re.search(pattern, text)
print(ret.group())  # 20.50

第三章:数据存储

3.1 JSON文件处理

3.1.1 什么是JSON

JSON(JavaScript Object Notation)是轻量级数据交换格式,基于ECMAScript子集,独立于编程语言,以简洁的文本格式存储/表示数据,兼具人类可读性和机器解析效率,是爬虫数据存储的常用格式。

核心特点:本质是字符串,支持字典(对象)、列表(数组)、数值、布尔、null、字符串(必须双引号)等类型,多数据用逗号分隔。

3.1.2 字典/列表转JSON字符串

使用json.dumps()方法,ensure_ascii=False可避免中文转义:

python 复制代码
import json

# 待转换数据
books = [
    {
        'title': '钢铁是怎样练成的',
        'price': 9.8
    },
    {
        'title': '红楼梦',
        'price': 9.9
    }
]

# 转换为JSON字符串(保留中文)
json_str = json.dumps(books, ensure_ascii=False)
print(json_str)  # 输出:[{"title": "钢铁是怎样练成的", "price": 9.8}, {"title": "红楼梦", "price": 9.9}]
print(type(json_str))  # <class 'str'>

3.1.3 直接将JSON写入文件

使用json.dump()方法,传入文件指针直接写入:

python 复制代码
import json

books = [
    {'title': '钢铁是怎样练成的', 'price': 9.8},
    {'title': '红楼梦', 'price': 9.9}
]

# 写入文件(默认utf-8编码)
with open('books.json', 'w', encoding='utf-8') as fp:
    json.dump(books, fp, ensure_ascii=False)

3.1.4 JSON字符串转Python对象

使用json.loads()方法解析JSON字符串:

python 复制代码
import json

# JSON字符串(注意双引号)
json_str = '[{"title": "钢铁是怎样练成的", "price": 9.8}, {"title": "红楼梦", "price": 9.9}]'

# 转换为Python列表
books = json.loads(json_str, encoding='utf-8')
print(type(books))  # <class 'list'>
print(books[0]['title'])  # 钢铁是怎样练成的

3.1.5 从文件读取JSON

使用json.load()方法直接读取文件中的JSON数据:

python 复制代码
import json

with open('books.json', 'r', encoding='utf-8') as fp:
    # 直接加载为Python对象
    books = json.load(fp)
    print(books)  # [{'title': '钢铁是怎样练成的', 'price': 9.8}, {'title': '红楼梦', 'price': 9.9}]

3.2 CSV文件处理

CSV(逗号分隔值)是纯文本表格格式,适用于简单结构化数据存储,Python内置csv模块支持读写。

3.2.1 读取CSV文件

方式1:基础读取(按索引取值)
python 复制代码
import csv

# 读取stock.csv文件
with open('stock.csv', 'r', encoding='utf-8') as fp:
    reader = csv.reader(fp)
    # 获取表头(第一行)
    titles = next(reader)
    print("表头:", titles)
    # 遍历数据行
    for row in reader:
        print(row)  # 每行是列表,按索引取值如row[0]
方式2:DictReader(按标题取值)

更易用,直接通过列名获取数据:

python 复制代码
import csv

with open('stock.csv', 'r', encoding='utf-8') as fp:
    # 创建字典读取器
    reader = csv.DictReader(fp)
    # 按列名取值
    for row in reader:
        print(row['turnoverVol'])  # 直接通过标题获取对应值

3.2.2 写入CSV文件

方式1:基础写入(列表格式)
python 复制代码
import csv

# 表头和数据
headers = ['name', 'age', 'classroom']
values = [
    ('zhiliao', 18, '111'),
    ('wena', 20, '222'),
    ('bbc', 21, '111')
]

# 写入文件(newline=''避免空行)
with open('test.csv', 'w', newline='', encoding='utf-8') as fp:
    writer = csv.writer(fp)
    writer.writerow(headers)  # 写入表头(单行)
    writer.writerows(values)  # 写入多行数据
方式2:DictWriter(字典格式)

按列名匹配写入,更贴合业务数据结构:

python 复制代码
import csv

headers = ['name', 'age', 'classroom']
values = [
    {"name": 'wenn', "age": 20, "classroom": '222'},
    {"name": 'abc', "age": 30, "classroom": '333'}
]

with open('test.csv', 'w', newline='', encoding='utf-8') as fp:
    writer = csv.DictWriter(fp, headers)
    writer.writeheader()  # 写入表头(注意:原代码笔误为csv.writeheader())
    writer.writerow({'name': 'zhiliao', "age": 18, "classroom": '111'})  # 写入单行
    writer.writerows(values)  # 写入多行

3.3 Excel文件处理

Python处理Excel依赖xlrd(读)和xlwt(写)库,适用于需要兼容Excel格式的场景。

3.3.1 安装依赖

bash 复制代码
pip install xlrd==1.2.0  # 高版本不支持xlsx,指定1.2.0兼容
pip install xlwt

3.3.2 读取Excel文件

1. 打开工作簿 & 获取Sheet
python 复制代码
import xlrd

# 打开Excel文件
workbook = xlrd.open_workbook("成绩表.xlsx")

# 获取所有Sheet名称
sheet_names = workbook.sheet_names()
print("所有Sheet:", sheet_names)

# 获取Sheet的3种方式
sheet1 = workbook.sheet_by_index(0)  # 按索引(从0开始)
sheet2 = workbook.sheet_by_name("1班成绩")  # 按名称
sheets = workbook.sheets()  # 获取所有Sheet对象

# 获取Sheet的行数/列数
nrows = sheet1.nrows  # 行数
ncols = sheet1.ncols  # 列数
print(f"行数:{nrows},列数:{ncols}")
2. 获取Cell(单元格)数据
python 复制代码
import xlrd

workbook = xlrd.open_workbook("成绩表.xlsx")
sheet = workbook.sheet_by_index(0)

# 方式1:获取指定单元格对象
cell = sheet.cell(0, 0)  # 第0行第0列
print("单元格值:", cell.value)
print("数据类型:", cell.ctype)  # 1=文本,2=数值,3=日期等

# 方式2:直接获取单元格值
value = sheet.cell_value(1, 2)  # 第1行第2列的值
print("单元格值(直接获取):", value)

# 方式3:获取整行/整列数据
row_values = sheet.row_values(0)  # 第0行所有值
col_values = sheet.col_values(1)  # 第1列所有值
print("第0行数据:", row_values)
print("第1列数据:", col_values)

# 方式4:切片获取行/列
row_slice = sheet.row_slice(0, 1, 3)  # 第0行,第1-2列(左闭右开)
col_slice = sheet.col_slice(0, 1, 3)  # 第0列,第1-2行

3.3.3 写入Excel文件

1. 新建Excel并写入
python 复制代码
import xlwt
import random

# 创建工作簿(指定编码)
workbook = xlwt.Workbook(encoding='utf-8')
# 添加Sheet(指定名称)
sheet = workbook.add_sheet("成绩表")

# 写入表头
fields = ['数学', '英语', '语文']
for index, field in enumerate(fields):
    sheet.write(0, index, field)  # 行、列、值

# 写入随机成绩(1-9行)
for row in range(1, 10):
    for col in range(3):
        grade = random.randint(0, 100)
        sheet.write(row, col, grade)

# 保存文件(仅支持xls格式)
workbook.save("成绩表_新.xls")
2. 追加数据到现有Excel
python 复制代码
import xlrd
import xlwt

# 读取原有Excel
workbook = xlrd.open_workbook("成绩表.xlsx")
rsheet = workbook.sheet_by_index(0)

# 1. 追加"总分"列
rsheet.put_cell(0, 4, xlrd.XL_CELL_TEXT, "总分", None)  # 表头
for row in range(1, rsheet.nrows):
    # 计算1-3列(数学/英语/语文)的总分
    total = sum(rsheet.row_values(row, 1, 4))
    rsheet.put_cell(row, 4, xlrd.XL_CELL_NUMBER, total, None)

# 2. 追加"平均分"行
total_rows = rsheet.nrows
for col in range(1, rsheet.ncols):
    # 计算每列平均分
    grades = rsheet.col_values(col, 1, total_rows)
    avg = sum(grades) / len(grades)
    rsheet.put_cell(total_rows, col, xlrd.XL_CELL_NUMBER, avg, None)

# 3. 写入新Excel文件
wwb = xlwt.Workbook(encoding="utf-8")
wsheet = wwb.add_sheet("1班学生成绩")
# 复制所有数据
for row in range(rsheet.nrows + 1):  # 包含平均分行
    for col in range(rsheet.ncols):
        wsheet.write(row, col, rsheet.cell_value(row, col) if row < rsheet.nrows else rsheet.cell_value(total_rows, col))

wwb.save("成绩表_追加.xls")

3.4 MySQL数据库

MySQL是关系型数据库,适用于结构化数据的持久化存储,Python通过pymysql驱动操作。

3.4.1 环境准备

  1. 安装MySQL:官网下载安装包(https://dev.mysql.com/downloads/windows/installer/5.7.html),需安装.NET Framework和Microsoft Visual C++依赖;

  2. 安装客户端(可选):Navicat(图形化操作工具);

  3. 安装Python驱动:

    bash 复制代码
    pip install pymysql

3.4.2 数据库连接

python 复制代码
import pymysql

# 建立连接
db = pymysql.connect(
    host="127.0.0.1",  # 主机地址(本地为127.0.0.1)
    user='root',        # 用户名
    password='root',    # 密码
    database='pymysql_test',  # 数据库名
    port=3306,          # 端口(默认3306)
    charset='utf8'      # 编码
)

# 创建游标(执行SQL的工具)
cursor = db.cursor()
# 测试连接
cursor.execute("select 1")
data = cursor.fetchone()
print("连接成功:", data)  # 输出(1,)

# 关闭连接
db.close()

3.4.3 插入数据

1. 直接插入
python 复制代码
import pymysql

db = pymysql.connect(host="127.0.0.1", user='root', password='root', database='pymysql_test', port=3306)
cursor = db.cursor()

# 插入SQL
sql = """
insert into user(
    id,username,gender,age,password
) 
values(null,'abc',1,18,'111111');
"""

try:
    cursor.execute(sql)
    db.commit()  # 提交事务(必须)
    print("插入成功")
except Exception as e:
    db.rollback()  # 失败回滚
    print("插入失败:", e)
finally:
    db.close()
2. 参数化插入(防SQL注入)
python 复制代码
import pymysql

db = pymysql.connect(host="127.0.0.1", user='root', password='root', database='pymysql_test', port=3306)
cursor = db.cursor()

# 带占位符的SQL
sql = """
insert into user(
    id,username,gender,age,password
) 
values(null,%s,%s,%s,%s);
"""

try:
    # 传入参数(元组格式)
    cursor.execute(sql, ('spider', 1, 20, '222222'))
    db.commit()
    print("参数化插入成功")
except Exception as e:
    db.rollback()
    print("插入失败:", e)
finally:
    db.close()

3.4.4 查询数据

python 复制代码
import pymysql

db = pymysql.connect(host="127.0.0.1", user='root', password='root', database='pymysql_test', port=3306)
cursor = db.cursor()

# 查询SQL
sql = "select * from user"
cursor.execute(sql)

# 方式1:逐行读取(fetchone)
print("=== fetchone ===")
while True:
    result = cursor.fetchone()
    if not result:
        break
    print(result)

# 方式2:读取所有(fetchall)
cursor.execute(sql)  # 重新执行SQL(游标已移动)
results = cursor.fetchall()
print("\n=== fetchall ===")
for result in results:
    print(result)

# 方式3:读取指定条数(fetchmany)
cursor.execute(sql)
results = cursor.fetchmany(2)  # 读取2条
print("\n=== fetchmany(2) ===")
for result in results:
    print(result)

db.close()

3.4.5 更新/删除数据

1. 更新数据
python 复制代码
import pymysql

db = pymysql.connect(host="127.0.0.1", user='root', password='root', database='pymysql_test', port=3306)
cursor = db.cursor()

sql = "update user set username='aaa' where id=1"
try:
    cursor.execute(sql)
    db.commit()
    print("更新成功")
except Exception as e:
    db.rollback()
    print("更新失败:", e)
finally:
    db.close()
2. 删除数据
python 复制代码
import pymysql

db = pymysql.connect(host="127.0.0.1", user='root', password='root', database='pymysql_test', port=3306)
cursor = db.cursor()

sql = "delete from user where id=1"
try:
    cursor.execute(sql)
    db.commit()
    print("删除成功")
except Exception as e:
    db.rollback()
    print("删除失败:", e)
finally:
    db.close()

3.5 MongoDB数据库

MongoDB是NoSQL数据库(文档型),适用于非结构化/海量数据存储,爬虫场景下灵活性更高。

3.5.1 环境准备

  1. 安装MongoDB:官网下载社区版(https://www.mongodb.com/download-center/community),安装时可勾选Compass(图形化工具);

  2. 配置运行环境:

    • 创建数据目录(如D:\mongodb\data\db)和日志目录(D:\mongodb\data\log);
    • 将MongoDB的bin目录加入系统环境变量;
    • 启动命令:mongod --dbpath D:\mongodb\data\db
  3. 制作Windows服务(可选,避免每次手动启动):

    bash 复制代码
    # 1. 创建配置文件mongod.cfg
    # logpath=D:\mongodb\data\log\mongod.log
    # dbpath=D:\mongodb\data\db
    
    # 2. 安装服务
    mongod --config "D:\mongodb\mongod.cfg" --install
    
    # 3. 启动/停止服务
    net start mongodb
    net stop mongodb
    
    # 4. 移除服务
    "D:\mongodb\bin\mongod.exe" --remove
  4. 安装Python驱动:

    bash 复制代码
    pip install pymongo

3.5.2 核心概念

SQL术语 MongoDB术语 说明
database database 数据库
table collection 集合(表)
row document 文档(行)
column field 字段(列)
primary key _id 主键(MongoDB自动生成)

3.5.3 Python连接MongoDB

python 复制代码
import pymongo

# 建立连接(本地默认端口27017)
client = pymongo.MongoClient('127.0.0.1', port=27017)

# 获取数据库(不存在则自动创建)
db = client.zhihu  # 等价于 client['zhihu']

# 获取集合(表,不存在则自动创建)
collection = db.qa  # 等价于 db['qa']

print("连接成功")

3.5.4 基本操作

1. 插入数据
python 复制代码
import pymongo

client = pymongo.MongoClient('127.0.0.1', 27017)
db = client.zhihu
collection = db.qa

# 插入单条
collection.insert_one({
    "username": "abc",
    "password": "hello"
})

# 插入多条
collection.insert_many([
    {"username": "abc", "password": "111111"},
    {"username": "bbb", "password": "222222"}
])

print("插入完成")
2. 查询数据
python 复制代码
import pymongo

client = pymongo.MongoClient('127.0.0.1', 27017)
db = client.zhihu
collection = db.qa

# 查询单条(默认第一条)
result = collection.find_one()
print("默认第一条:", result)

# 条件查询单条
result = collection.find_one({"username": "abc"})
print("条件查询:", result)

# 查询所有(返回游标,可遍历)
results = collection.find({"username": "abc"})
print("\n所有符合条件的数据:")
for res in results:
    print(res)
3. 更新数据
python 复制代码
import pymongo

client = pymongo.MongoClient('127.0.0.1', 27017)
db = client.zhihu
collection = db.qa

# 更新单条
collection.update_one(
    {"username": "abc"},  # 查询条件
    {"$set": {"username": "aaa"}}  # 更新操作
)

# 更新多条
collection.update_many(
    {"username": "abc"},
    {"$set": {"username": "aaa"}}
)

print("更新完成")
4. 删除数据
python 复制代码
import pymongo

client = pymongo.MongoClient('127.0.0.1', 27017)
db = client.zhihu
collection = db.qa

# 删除单条
collection.delete_one({"username": "abc"})

# 删除多条
collection.delete_many({"username": "abc"})

print("删除完成")

第四章:爬虫进阶

4.1 多线程爬虫

多线程爬虫核心是利用线程并行执行耗时操作(如IO密集型的图片/数据下载),提升爬虫效率,Python主要通过threading模块和queue线程安全队列实现。

核心知识点

  1. 多线程基础

    • threading.Thread类:创建线程,通过target指定执行函数或继承Thread重写run方法。
    • 线程安全问题:多线程共享全局变量会导致数据错乱,需用threading.Lock加锁解决。
    • 进阶同步:threading.Condition替代Lock,通过wait()/notify()减少无效锁竞争,提升性能。
  2. 线程安全队列(queue.Queue)

    内置锁机制,无需手动加锁,适合生产者-消费者模式:

    • 生产者线程:解析网页、提取待下载资源(如图片URL),存入队列。
    • 消费者线程:从队列取出资源,执行下载/保存操作。
  3. 多线程爬虫实战(以表情包下载为例)

    python 复制代码
    import threading
    import requests
    from lxml import etree
    from urllib import request
    import os
    import re
    from queue import Queue
    
    class Producer(threading.Thread):
        headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/62.0.3202.94 Safari/537.36'}
        def __init__(self, page_queue, img_queue, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.page_queue = page_queue
            self.img_queue = img_queue
    
        def run(self):
            while True:
                if self.page_queue.empty(): break
                url = self.page_queue.get()
                self.parse_page(url)
    
        def parse_page(self, url):
            response = requests.get(url, headers=self.headers)
            html = etree.HTML(response.text)
            imgs = html.xpath("//div[@class='page-content text-center']//a//img")
            for img in imgs:
                if img.get('class') == 'gif': continue
                img_url = img.xpath(".//@data-original")[0]
                suffix = os.path.splitext(img_url)[1]
                alt = re.sub(r'[,。??,/\\·]','', img.xpath(".//@alt")[0])
                self.img_queue.put((img_url, alt + suffix))
    
    class Consumer(threading.Thread):
        def __init__(self, page_queue, img_queue, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.page_queue = page_queue
            self.img_queue = img_queue
    
        def run(self):
            while True:
                if self.img_queue.empty() and self.page_queue.empty(): return
                img_url, filename = self.img_queue.get(block=True)
                request.urlretrieve(img_url, f'images/{filename}')
                print(f'{filename} 下载完成!')
    
    if __name__ == '__main__':
        page_queue = Queue(100)
        img_queue = Queue(500)
        # 填充页码队列
        for i in range(1, 101):
            page_queue.put(f"http://www.doutula.com/photo/list/?page={i}")
        # 启动5个生产者、5个消费者
        for _ in range(5):
            Producer(page_queue, img_queue).start()
        for _ in range(5):
            Consumer(page_queue, img_queue).start()
  4. GIL全局解释器锁

    CPython的GIL导致多线程无法利用多核CPU,仅适合IO密集型任务(如网络请求);CPU密集型任务建议用多进程。

4.2 动态网页爬虫

动态网页通过AJAX异步加载数据,无法直接从静态HTML中提取,核心解决方案有两种:

2.1 分析AJAX接口

  • 优点:代码量少、性能高,直接请求接口获取JSON/XML数据。
  • 缺点:需分析接口参数(如加密、签名),易被反爬识别。

2.2 Selenium+ChromeDriver模拟浏览器

Selenium模拟人类操作浏览器,能获取JS渲染后的完整页面,适合复杂动态页面。

  1. 环境准备

    • 安装:pip install selenium,下载对应Chrome版本的ChromeDriver并配置环境变量。

    • 基础使用:

      python 复制代码
      from selenium import webdriver
      # 指定ChromeDriver路径
      driver = webdriver.Chrome(executable_path=r'D:\chromedriver.exe')
      driver.get("https://www.baidu.com")
      print(driver.page_source)  # 获取渲染后的页面源码
      driver.quit()  # 关闭浏览器
  2. 核心操作

    • 定位元素:find_element_by_id/name/xpath/css_selector等,find_elements获取所有匹配元素。
    • 操作表单:输入框send_keys()、复选框click()、下拉框Select类。
    • 行为链:ActionChains模拟鼠标连续操作(移动、点击、输入)。
    • 页面等待:
      • 隐式等待:driver.implicitly_wait(10)(全局等待元素加载)。
      • 显式等待:WebDriverWait配合expected_conditions,等待指定条件满足。
    • 切换页面/代理:switch_to.window()切换标签页;ChromeOptions配置代理IP。

4.3 图形验证码识别

图形验证码是爬虫常见阻碍,核心通过OCR(光学字符识别)技术识别,主流工具为Tesseract。

3.1 环境准备

  • 安装Tesseract-OCR(配置环境变量TESSDATA_PREFIX指向训练数据目录)。
  • 安装依赖:pip install pytesseract pillow

3.2 基础使用

python 复制代码
import pytesseract
from PIL import Image

# 指定Tesseract路径(Windows需配置)
pytesseract.pytesseract.tesseract_cmd = r'D:\TesseractOCR\tesseract.exe'

# 打开图片并识别
image = Image.open("captcha.png")
text = pytesseract.image_to_string(image, lang='eng')  # lang指定语言(eng/chi_sim)
print("识别结果:", text)

3.3 优化

  • 预处理图片(灰度、二值化、降噪)提升识别率。
  • 复杂验证码(如滑块、点选)需结合OpenCV或第三方打码平台。

4.4 字体反爬

字体反爬通过自定义字体文件(如TTF)映射字符,网页显示的是字体编码而非真实文字,核心解决思路是形状匹配

4.1 反爬原理

  • 网页通过@font-face加载自定义字体,文字内容以&#xXXXX;(字体编码)形式存在。
  • 字体文件中cmap表存储"编码→字体名称"映射,glyf表存储字体形状绘制规则。

4.2 解决方案

  1. 提取网页中的自定义字体(base64解码或下载TTF文件)。
  2. 用FontCreator工具打开TTF,分析"字体形状→真实文字"的映射关系。
  3. 爬取时,将网页中的字体编码转换为字体形状,再匹配真实文字。

第五章:Scrapy框架

Scrapy是一款专为爬虫设计的高效、可扩展的Python框架,封装了网络请求、数据解析、异步处理、反反爬等核心能力,是企业级爬虫开发的首选工具。

5.1 框架架构

5.1.1 核心模块及功能

Scrapy的核心是引擎(Engine),负责各模块间的通信和数据流转,整体架构包含7个核心组件:

模块 核心功能
Scrapy Engine(引擎) 框架核心,负责调度、传递数据和协调所有模块
Spider(爬虫) 开发者自定义,指定爬取链接、解析页面数据(核心逻辑层)
Scheduler(调度器) 接收引擎的请求,排序/去重后调度请求顺序
Downloader(下载器) 下载网页数据,返回给引擎
Item Pipeline(管道) 处理爬虫解析后的数据(存储到文件/数据库)
Downloader Middlewares(下载中间件) 扩展引擎↔下载器的通信(设置代理、请求头)
Spider Middlewares(爬虫中间件) 扩展引擎↔爬虫的通信(过滤请求/响应)

5.1.2 核心流程

  1. 引擎向爬虫获取初始爬取URL(start_urls),交给调度器;
  2. 调度器将URL排序后,通过引擎交给下载器;
  3. 下载器下载页面数据,经下载中间件处理后返回引擎;
  4. 引擎将下载的响应(Response)交给爬虫解析;
  5. 爬虫解析出数据(Item)或新的URL:
    • 数据(Item)→ 引擎 → 管道(Pipeline)存储;
    • 新URL → 引擎 → 调度器,重复上述流程;
  6. 所有请求完成后,引擎关闭管道,爬虫结束。

5.2 快速入门

5.2.1 环境安装

通用安装
bash 复制代码
pip install scrapy
系统特殊配置
  • Ubuntu :先安装依赖再装Scrapy

    bash 复制代码
    sudo apt-get install python3-dev build-essential python3-pip libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev
    pip install scrapy
  • Windows :解决win32api缺失问题

    bash 复制代码
    pip install pypiwin32

5.2.2 项目创建与目录结构

1. 创建项目
bash 复制代码
# 进入目标目录后执行
scrapy startproject 项目名称  # 例:scrapy startproject qsbk_project
2. 核心目录结构
复制代码
qsbk_project/
├── qsbk_project/          # 项目核心目录
│   ├── __init__.py
│   ├── items.py           # 数据模型(定义爬取的字段)
│   ├── middlewares.py     # 中间件(下载/爬虫中间件)
│   ├── pipelines.py       # 数据管道(存储数据)
│   ├── settings.py        # 全局配置(请求头、并发、代理等)
│   └── spiders/           # 爬虫脚本目录(核心)
└── scrapy.cfg             # 项目部署配置

5.2.3 第一个爬虫:爬取糗事百科

1. 创建爬虫
bash 复制代码
# 进入项目目录后执行
scrapy genspider qsbk "qiushibaike.com"  # 爬虫名:qsbk,限定域名:qiushibaike.com
2. 核心配置(settings.py
python 复制代码
# 关闭爬虫协议(必改)
ROBOTSTXT_OBEY = False

# 添加请求头(模拟浏览器)
DEFAULT_REQUEST_HEADERS = {
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36',
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
}
3. 定义数据模型(items.py
python 复制代码
import scrapy

class QsbkItem(scrapy.Item):
    # 定义要爬取的字段
    author = scrapy.Field()  # 作者
    content = scrapy.Field()  # 段子内容
4. 编写爬虫逻辑(spiders/qsbk.py)
python 复制代码
import scrapy
from qsbk_project.items import QsbkItem

class QsbkSpider(scrapy.Spider):
    name = 'qsbk'  # 爬虫名(唯一)
    allowed_domains = ['qiushibaike.com']  # 允许爬取的域名
    start_urls = ['https://www.qiushibaike.com/text/']  # 初始URL

    def parse(self, response):
        """解析页面数据(核心方法)"""
        # 提取所有段子容器
        outer_box = response.xpath("//div[@id='content-left']/div")
        for box in outer_box:
            # 提取作者(去除空白字符)
            author = box.xpath(".//div[contains(@class,'author')]//h2/text()").extract_first().strip()
            # 提取段子内容
            content = box.xpath(".//div[@class='content']/span/text()").extract_first().strip()
            
            # 封装到Item对象
            item = QsbkItem()
            item['author'] = author
            item['content'] = content
            yield item  # 提交给管道
5. 编写数据管道(pipelines.py
python 复制代码
import json

class QsbkProjectPipeline:
    def __init__(self):
        # 初始化:打开文件
        self.fp = open('qsbk.json', 'w', encoding='utf-8')
        self.items = []

    def process_item(self, item, spider):
        """处理每个Item(必实现)"""
        self.items.append(dict(item))  # 转换为字典存入列表
        return item  # 必须返回Item(供后续管道处理)

    def close_spider(self, spider):
        """爬虫结束时执行"""
        json.dump(self.items, self.fp, ensure_ascii=False)  # 写入JSON文件
        self.fp.close()
6. 启用管道(settings.py
python 复制代码
ITEM_PIPELINES = {
   'qsbk_project.pipelines.QsbkProjectPipeline': 300,  # 数字越小,优先级越高
}
7. 运行爬虫
  • 命令行运行:

    bash 复制代码
    scrapy crawl qsbk  # qsbk为爬虫名
  • 脚本运行(创建start.py):

    python 复制代码
    from scrapy import cmdline
    cmdline.execute("scrapy crawl qsbk".split())

5.3 CrawlSpider

5.3.1 核心作用

继承自Scrapy.Spider,新增自动提取满足规则的URL能力,无需手动生成Request,适合批量爬取分页/列表类页面。

5.3.2 创建CrawlSpider

bash 复制代码
scrapy genspider -c crawl 爬虫名 域名  # 例:scrapy genspider -c crawl applet "wxapp-union.com"

5.3.3 核心组件

1. LinkExtractors(链接提取器)

自动提取满足规则的URL,核心参数:

python 复制代码
from scrapy.linkextractors import LinkExtractor

# 示例:提取所有分页链接(匹配/page/数字/的URL)
link_extractor = LinkExtractor(
    allow=r'/page/\d+/',  # 允许的URL正则
    deny_domains=['xxx.com'],  # 禁止的域名
    restrict_xpaths=['//div[@class="pager"]']  # 限定提取区域(XPath)
)
2. Rule(规则类)

定义URL的爬取规则,核心参数:

python 复制代码
from scrapy.spiders import Rule

# 示例规则
rules = [
    # 提取分页链接,跟进(继续爬取),不指定回调
    Rule(link_extractor, follow=True),
    # 提取详情页链接,指定回调函数解析,不跟进
    Rule(LinkExtractor(allow=r'/article/\d+/'), callback='parse_detail', follow=False)
]

5.3.4 示例:小程序社区爬虫

python 复制代码
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from wxapp_project.items import WxappItem

class WxappSpider(CrawlSpider):
    name = 'wxapp'
    allowed_domains = ['wxapp-union.com']
    start_urls = ['https://www.wxapp-union.com/']

    # 定义链接提取规则
    link_extractor = LinkExtractor(allow=r'/article/\d+/.html')
    rules = [
        Rule(link_extractor, callback='parse_detail', follow=False)
    ]

    def parse_detail(self, response):
        """解析详情页"""
        title = response.xpath("//h1[@class='ph']/text()").extract_first()
        author = response.xpath("//p[@class='authors']/a/text()").extract_first()
        content = response.xpath("//div[@class='content']//text()").extract()
        content = ''.join(content).strip()
        
        item = WxappItem()
        item['title'] = title
        item['author'] = author
        item['content'] = content
        yield item

5.4 Scrapy Shell

5.4.1 核心作用

轻量级交互式环境,无需运行完整爬虫,快速验证XPath/CSS选择器、调试解析规则。

5.4.2 使用方法

bash 复制代码
# 进入项目目录,执行以下命令(指定要调试的URL)
scrapy shell https://www.qiushibaike.com/text/

5.4.3 常用命令/属性

命令/属性 作用
response.xpath('//div[@class="content"]') 执行XPath解析
response.css('.content') 执行CSS选择器解析
extract() 提取所有结果(列表)
extract_first() 提取第一个结果(避免索引越界)
view(response) 在浏览器中打开响应页面(调试用)

5.5 Request和Response对象

5.5.1 Request对象

手动发送请求时使用,核心参数:

python 复制代码
from scrapy import Request

# 示例:发送GET请求
yield Request(
    url='https://www.xxx.com/page/2',  # 请求URL
    callback=self.parse_page,  # 回调函数
    meta={'page': 2},  # 传递数据(跨请求共享)
    dont_filter=True  # 禁止调度器过滤重复URL
)

# 示例:发送POST请求(模拟登录)
from scrapy import FormRequest

yield FormRequest(
    url='https://www.xxx.com/login',
    formdata={'username': 'xxx', 'password': 'xxx'},  # POST参数
    callback=self.after_login
)

5.5.2 Response对象

下载器返回的响应对象,核心属性:

属性 作用
response.text 响应文本(Unicode格式)
response.body 响应字节流(二进制)
response.xpath() XPath解析方法
response.css() CSS选择器解析方法
response.meta 获取Request传递的meta数据
response.status 响应状态码(200/404等)

5.5.3 模拟登录示例(豆瓣)

python 复制代码
class DoubanSpider(scrapy.Spider):
    name = 'douban'
    allowed_domains = ['douban.com']
    start_urls = ['https://www.douban.com/']

    def start_requests(self):
        """重写初始请求,发送登录POST"""
        login_url = 'https://accounts.douban.com/login'
        yield FormRequest(
            url=login_url,
            formdata={
                'form_email': '你的账号',
                'form_password': '你的密码',
                'source': 'index_nav'
            },
            callback=self.parse_login
        )

    def parse_login(self, response):
        """登录后解析"""
        # 验证是否登录成功
        if '我的豆瓣' in response.text:
            self.logger.info('登录成功!')
            # 继续爬取需要登录的页面
            yield Request(url='https://www.douban.com/mine', callback=self.parse_mine)
        else:
            self.logger.error('登录失败!')

5.6 下载文件和图片

Scrapy内置FilesPipeline/ImagesPipeline,支持异步下载、去重、格式转换,效率远高于手动下载。

5.6.1 下载图片(ImagesPipeline)

1. 定义Item(items.py
python 复制代码
import scrapy

class CarImageItem(scrapy.Item):
    image_urls = scrapy.Field()  # 图片URL列表(必须)
    images = scrapy.Field()  # 下载后的信息(自动填充)
2. 配置settings.py
python 复制代码
# 图片存储路径(绝对/相对路径)
IMAGES_STORE = './car_images'

# 启用图片管道
ITEM_PIPELINES = {
    'scrapy.pipelines.images.ImagesPipeline': 1,
}

# 可选:图片最小尺寸(过滤过小图片)
IMAGES_MIN_HEIGHT = 100
IMAGES_MIN_WIDTH = 100
3. 爬虫逻辑(爬取汽车之家图片)
python 复制代码
import scrapy
from car_project.items import CarImageItem

class CarSpider(scrapy.Spider):
    name = 'car'
    allowed_domains = ['autohome.com.cn']
    start_urls = ['https://car.autohome.com.cn/pic/series/65.html']

    def parse(self, response):
        # 提取图片URL
        image_urls = response.xpath("//div[@class='uibox-con']//img/@src").extract()
        # 补全URL(部分图片URL为相对路径)
        image_urls = [url if url.startswith('http') else 'https:' + url for url in image_urls]
        
        item = CarImageItem()
        item['image_urls'] = image_urls  # 必须是列表
        yield item

5.7 下载中间件

5.7.1 核心作用

拦截引擎与下载器之间的请求/响应,实现随机请求头、IP代理、反反爬等功能,核心方法:

  • process_request(request, spider):请求发送前执行;
  • process_response(request, response, spider):响应返回前执行。

5.7.2 实战1:随机请求头中间件

python 复制代码
# middlewares.py
import random

class RandomUserAgentMiddleware:
    # 自定义请求头列表
    USER_AGENTS = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Firefox/121.0',
        'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari/605.1.15'
    ]

    def process_request(self, request, spider):
        """请求发送前,随机设置User-Agent"""
        user_agent = random.choice(self.USER_AGENTS)
        request.headers['User-Agent'] = user_agent
        return None  # 返回None:继续处理请求

5.7.3 实战2:IP代理池中间件

python 复制代码
# middlewares.py
import random
import base64

class IPProxyMiddleware:
    # 开放代理池
    PROXIES = [
        '110.243.32.154:9999',
        '123.56.78.90:8080',
        '180.101.59.19:8080'
    ]

    def process_request(self, request, spider):
        """设置IP代理"""
        # 方式1:开放代理
        proxy = random.choice(self.PROXIES)
        request.meta['proxy'] = f'http://{proxy}'

        # 方式2:独享代理(带账号密码)
        # proxy = '121.199.6.124:16816'
        # user_pwd = '账号:密码'
        # b64_pwd = base64.b64encode(user_pwd.encode('utf-8')).decode('utf-8')
        # request.meta['proxy'] = f'http://{proxy}'
        # request.headers['Proxy-Authorization'] = f'Basic {b64_pwd}'
        return None

5.7.4 启用中间件(settings.py

python 复制代码
DOWNLOADER_MIDDLEWARES = {
   'car_project.middlewares.RandomUserAgentMiddleware': 543,
   'car_project.middlewares.IPProxyMiddleware': 544,  # 数字越小,优先级越高
}

5.8 Settings配置信息

5.8.1 核心配置项(常用)

配置项 作用 默认值
ROBOTSTXT_OBEY 是否遵守robots协议 True
CONCURRENT_REQUESTS 下载器并发请求数 16
DOWNLOAD_DELAY 下载延迟(秒),防反爬 0
DEFAULT_REQUEST_HEADERS 默认请求头 -
ITEM_PIPELINES 启用的管道及优先级 {}
DOWNLOADER_MIDDLEWARES 启用的下载中间件 {}
LOG_LEVEL 日志级别(DEBUG/INFO/WARNING) DEBUG
COOKIES_ENABLED 是否启用Cookie(关闭防追踪) True
DEPTH_LIMIT 最大爬取深度(0=无限制) 0
DOWNLOAD_TIMEOUT 下载超时时间(秒) 180

5.8.2 优化配置示例(防反爬)

python 复制代码
# 关闭robots协议
ROBOTSTXT_OBEY = False

# 降低并发,增加延迟
CONCURRENT_REQUESTS = 8
DOWNLOAD_DELAY = 2  # 每次请求间隔2秒

# 关闭Cookie
COOKIES_ENABLED = False

# 日志级别(上线后改为INFO,减少输出)
LOG_LEVEL = 'INFO'

# 请求头
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}

5.9 Scrapy爬虫实战

5.9.1 实战场景汇总

实战案例 核心技术 应用场景
糗事百科爬虫 Spider、XPath解析、JSON存储 入门级文本爬取
小程序社区爬虫 CrawlSpider、LinkExtractors 批量分页爬取
豆瓣模拟登录 FormRequest、POST请求 需登录的站点爬取
汽车之家图片爬取 ImagesPipeline、异步下载 文件/图片下载
BOSS直聘爬虫 IP代理中间件、随机请求头 高反爬站点爬取
简书整站爬虫 MySQL管道、Selenium集成 数据入库、动态页面爬取

5.9.2 实战核心要点

  1. 反反爬:随机请求头 + IP代理 + 下载延迟 + 关闭Cookie;
  2. 数据存储:JSON/CSV(简单)、MySQL/MongoDB(海量数据);
  3. 动态页面:集成Selenium/Playwright处理JS渲染;
  4. 效率优化:合理设置并发数、使用异步下载、增量爬取。

第六章:Scrapy-Redis分布式组件

Scrapy-Redis是Scrapy框架的分布式扩展组件,核心基于Redis数据库实现爬虫的分布式部署,解决多机协同爬取时的任务调度、数据去重、数据统一存储问题,大幅提升爬虫的爬取效率。

6.1 Redis数据库介绍

6.1.1 核心特性

Redis是一款内存型NoSQL数据库(支持磁盘持久化),相比传统数据库/Memcached,具备以下优势:

  • 数据存储在内存中,读写速度极快(单机QPS可达10万+);
  • 支持丰富的数据结构:字符串(String)、列表(List)、集合(Set)、哈希(Hash)、有序集合(Sorted Set);
  • 支持分布式、事务、发布/订阅、过期策略等特性;
  • 可持久化数据(定时同步到磁盘),避免数据丢失。

6.1.2 典型使用场景

场景 说明
登录会话存储 替代Memcached,数据可持久化
排行榜/计数器 点赞数、阅读量、主播排名等实时统计
消息队列 Celery、Scrapy-Redis等框架的中间人
在线人数统计 实时获取系统在线用户数
热点数据缓存 论坛板块、热门文章等低频变更数据缓存
分布式锁/去重 分布式爬虫的任务去重、资源竞争控制

6.1.3 环境安装与启动

1. Ubuntu系统
bash 复制代码
# 安装
sudo apt-get install redis-server

# 启动/停止/重启
sudo service redis-server start
sudo service redis-server stop
sudo service redis-server restart

# 查看运行状态
ps aux | grep redis
2. Windows系统
3. 允许远程访问

修改Redis配置文件(Ubuntu:/etc/redis/redis.conf;Windows:redis.windows.conf):

ini 复制代码
# 将bind 127.0.0.1改为0.0.0.0(允许所有IP访问)
bind 0.0.0.0

# 关闭保护模式(可选,生产环境需配置密码)
protected-mode no

修改后重启Redis服务。

6.1.4 核心数据结构操作(redis-cli)

1. 字符串(String):最基础类型
bash 复制代码
# 设置值(永久有效)
set username xiaotuo

# 设置值并指定过期时间(10秒后过期)
setex username 10 xiaotuo
# 或 set username xiaotuo EX 10

# 获取值
get username

# 删除值
del username

# 查看过期时间(-1=永久,-2=已过期)
ttl username

# 查看所有key
keys *
2. 列表(List):有序可重复,适合做队列
bash 复制代码
# 左插入(表头)
lpush languages python java php

# 右插入(表尾)
rpush languages c++

# 查看列表(0=-1表示所有元素)
lrange languages 0 -1

# 左弹出(移除并返回表头元素)
lpop languages

# 右弹出(移除并返回表尾元素)
rpop languages

# 获取列表长度
llen languages

# 删除指定元素(count=0删除所有匹配项)
lrem languages 0 php
3. 集合(Set):无序不可重复,适合去重/交集
bash 复制代码
# 添加元素
sadd team xiaotuo datuo mingming

# 查看所有元素
smembers team

# 删除元素
srem team mingming

# 获取集合大小
scard team

# 交集(共同元素)
sinter team1 team2

# 并集(所有元素)
sunion team1 team2

# 差集(team1有但team2没有)
sdiff team1 team2
4. 哈希(Hash):键值对集合,适合存储对象
bash 复制代码
# 设置字段值
hset website baidu baidu.com
hset website taobao taobao.com

# 获取字段值
hget website baidu

# 获取所有字段和值
hgetall website

# 获取所有字段
hkeys website

# 获取所有值
hvals website

# 删除字段
hdel website taobao

# 判断字段是否存在
hexists website baidu
5. 事务:批量执行命令(原子性)
bash 复制代码
# 开启事务
multi

# 执行多个命令(暂存,未提交)
set a 1
set b 2

# 提交事务(执行所有命令)
exec

# 取消事务(放弃所有命令)
discard
6. 发布/订阅:消息通信
bash 复制代码
# 终端1:订阅频道
subscribe news

# 终端2:发布消息到频道
publish news "Scrapy-Redis分布式爬虫教程"

6.2 Scrapy-Redis组件介绍

6.2.1 核心解决的问题

Scrapy本身不支持分布式,多机运行时会出现重复爬取、任务冲突、数据分散等问题,Scrapy-Redis通过Redis解决:

  1. 统一任务调度:所有爬虫节点从Redis获取待爬取的URL,避免重复任务;
  2. 分布式去重:所有爬虫节点共享Redis的去重指纹库,防止重复爬取;
  3. 统一数据存储:爬取的Item统一存入Redis,便于后续集中处理。

6.2.2 架构对比(Scrapy vs Scrapy-Redis)

模块 Scrapy(单机) Scrapy-Redis(分布式)
任务调度(Scheduler) 本地内存队列 Redis队列(所有节点共享)
去重(DupeFilter) 本地内存去重 Redis集合去重(共享指纹)
数据存储(Pipeline) 本地文件/数据库 Redis统一存储(可选)
启动方式 本地start_urls Redis推送初始URL(lpush)

以上两个图片对比我们可以发现。Item Pipeline在接收到数据后发送给了Redis、Scheduler调度器调度数据也是从Redis中来的、并且其实数据去重也是在Redis中做的。

6.2.3 安装

bash 复制代码
pip install scrapy-redis

6.2.4 核心配置(settings.py

python 复制代码
# ========== Scrapy-Redis 核心配置 ==========
# 1. 替换调度器:使用Redis存储待爬取的请求
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 2. 替换去重器:使用Redis的集合存储去重指纹
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 3. 启用Redis管道:将Item存入Redis(可选,也可自定义管道存入MySQL/MongoDB)
ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 300,
    # 自定义管道(如存入MySQL)可叠加
    # 'sfw_project.pipelines.SfwPipeline': 400,
}

# 4. 持久化调度器队列:Redis中的队列不会清空,支持暂停/恢复爬虫
SCHEDULER_PERSIST = True

# 5. Redis连接配置(分布式爬虫所有节点需指向同一Redis服务器)
REDIS_HOST = '192.168.1.100'  # Redis服务器IP(非本地需填真实IP)
REDIS_PORT = 6379  # Redis端口(默认6379)
# REDIS_PASSWORD = '123456'  # 如有密码需配置

# ========== 基础防反爬配置 ==========
ROBOTSTXT_OBEY = False
CONCURRENT_REQUESTS = 16
DOWNLOAD_DELAY = 1
DEFAULT_REQUEST_HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36',
}

6.3 搜房网分布式爬虫实战

6.3.1 需求说明

爬取搜房网(https://www1.fang.com)指定城市的小区信息(名称、价格、地址),通过多台机器分布式爬取,提升效率。

6.3.2 项目创建与改造

1. 创建Scrapy项目
bash 复制代码
scrapy startproject sfw_project
cd sfw_project
scrapy genspider -c crawl sfw "fang.com"  # 创建CrawlSpider爬虫
2. 改造爬虫(spiders/sfw.py)

将普通CrawlSpider改为RedisCrawlSpider,适配分布式:

python 复制代码
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import RedisCrawlSpider, Rule  # 替换为RedisCrawlSpider
from sfw_project.items import SfwItem

class SfwSpider(RedisCrawlSpider):  # 继承RedisCrawlSpider
    name = 'sfw'
    allowed_domains = ['fang.com']
    # 移除start_urls,改为redis_key(从Redis获取初始URL)
    redis_key = 'sfw:start_urls'  # 格式:项目名:标识

    # 定义链接提取规则
    # 规则1:城市列表→小区列表(跟进,不回调)
    list_link = LinkExtractor(allow=r'/newhouse/[\w]+/fangjia/\d+.htm')
    # 规则2:小区列表→小区详情(回调解析,不跟进)
    detail_link = LinkExtractor(allow=r'/newhouse/[\w]+/house/\d+.htm')

    rules = [
        Rule(list_link, follow=True),
        Rule(detail_link, callback='parse_detail', follow=False)
    ]

    def parse_detail(self, response):
        """解析小区详情页"""
        # 提取小区名称
        name = response.xpath("//h1[@class='detail-title']/text()").extract_first().strip()
        # 提取小区价格
        price = response.xpath("//span[@class='price']/text()").extract_first() or '暂无价格'
        # 提取小区地址
        address = response.xpath("//div[@class='detail-address']/span/text()").extract_first().strip()

        # 封装Item
        item = SfwItem()
        item['name'] = name
        item['price'] = price
        item['address'] = address

        yield item
3. 定义Item(items.py
python 复制代码
import scrapy

class SfwItem(scrapy.Item):
    # 小区名称
    name = scrapy.Field()
    # 小区价格
    price = scrapy.Field()
    # 小区地址
    address = scrapy.Field()
4. 自定义管道(可选,存入MySQL)

如果需要将数据存入MySQL(而非仅Redis),修改pipelines.py

python 复制代码
import pymysql

class SfwPipeline:
    def __init__(self):
        # 连接MySQL(分布式所有节点需指向同一数据库)
        self.conn = pymysql.connect(
            host='192.168.1.100',
            user='root',
            password='123456',
            database='sfw_db',
            port=3306,
            charset='utf8'
        )
        self.cursor = self.conn.cursor()

    def process_item(self, item, spider):
        """插入数据"""
        sql = """
        INSERT INTO community (name, price, address)
        VALUES (%s, %s, %s)
        ON DUPLICATE KEY UPDATE price=%s  # 去重更新
        """
        try:
            self.cursor.execute(sql, (item['name'], item['price'], item['address'], item['price']))
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            spider.logger.error(f"插入失败:{e}")
        return item

    def close_spider(self, spider):
        """关闭连接"""
        self.cursor.close()
        self.conn.close()

启用自定义管道(settings.py):

python 复制代码
ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 300,  # 先存入Redis
    'sfw_project.pipelines.SfwPipeline': 400,     # 再存入MySQL
}

6.3.3 分布式部署与运行

前提条件
  1. 所有爬虫节点(多台机器)需安装:scrapyscrapy-redispymysql(如需MySQL);
  2. 所有节点能访问同一Redis服务器和MySQL服务器(如有);
  3. 所有节点的爬虫代码保持一致。
运行步骤
步骤1:启动Redis服务器
bash 复制代码
# 在Redis服务器(如192.168.1.100)上启动
redis-server
步骤2:在所有爬虫节点启动爬虫
bash 复制代码
# 节点1(如192.168.1.101)
cd /path/to/sfw_project
scrapy runspider sfw.py

# 节点2(如192.168.1.102)
cd /path/to/sfw_project
scrapy runspider sfw.py

# 节点3(如192.168.1.103)
cd /path/to/sfw_project
scrapy runspider sfw.py

此时爬虫会等待Redis推送初始URL,不会立即爬取。

步骤3:推送初始URL到Redis
bash 复制代码
# 连接Redis服务器(任意节点均可)
redis-cli -h 192.168.1.100 -p 6379

# 推送初始URL(对应爬虫的redis_key=sfw:start_urls)
lpush sfw:start_urls https://www1.fang.com/newhouse/

推送后,所有爬虫节点会立即从Redis获取任务,开始分布式爬取。

6.3.4 监控与调试

  1. 查看Redis中的待爬取队列

    bash 复制代码
    redis-cli -h 192.168.1.100
    llen scrapy:queue:sfw  # 查看待爬取请求数
    smembers scrapy:dupefilter:sfw  # 查看去重指纹(数量大时慎用)
    lrange sfw:items 0 -1  # 查看存入Redis的Item
  2. 暂停/恢复爬虫

    • 暂停:直接关闭爬虫节点,Redis中的队列不会清空;
    • 恢复:重新启动爬虫节点,会从Redis继续获取未完成的任务。
  3. 扩展节点:新增机器只需同步代码,启动爬虫即可自动加入分布式集群。

相关推荐
1941s1 小时前
Google Agent Development Kit (ADK) 指南 第二章:环境搭建与快速开始
人工智能·python·adk·google agent
天下无贼2 小时前
【Python】2026版——FastAPI 框架快速搭建后端服务
后端·python·aigc
小蚂蚁i2 小时前
LangChain 完全学习手册:看完就能上手
后端·python·ai编程
Aawy1202 小时前
Python生成器(Generator)与Yield关键字:惰性求值之美
jvm·数据库·python
沐硕3 小时前
《基于改进协同过滤与多目标优化的健康饮食推荐系统设计与实现》
java·python·算法·fastapi·多目标优化·饮食推荐·改进协同过滤
乱世军军3 小时前
把 Python 3.13 降级到 3.11
开发语言·python
Y5neKO3 小时前
某国赛CTF逆向题目Writeup:re1
python·逆向·ctf
带娃的IT创业者4 小时前
WeClaw 架构演进史:从 0 到 1 构建跨平台 AI 助手的完整历程
人工智能·python·websocket·架构·fastapi·架构设计·实时通信
Storynone4 小时前
【Day28】LeetCode:509. 斐波那契数,70. 爬楼梯,746. 使用最小花费爬楼梯
python·算法·leetcode