python爬虫


一些爬虫的经验

F12与查看源代码的区别

有些情况下,使用谷歌浏览器开发者工具并不能准确地获得元素的XPATH表达式,因为此源码被谷歌浏览器做了优化

比如

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <thead>
      <th>姓名</th>
      <th>学号</th>
    </thead>
    <tr>
      <td>keyenzhou</td>
      <td>1</td>
    </tr>
    <tr>
      <td>zqa</td>
      <td>2</td>
    </tr>
  </body>
</html>

对于此代码:

谷歌开发者工具展示的源码为:

真正的源码为:

而我们用爬虫请求通常获取的是第二种源码,这样就会使原本的XPATH表达式失效。

这两个地方的HTML代码可能是不一样的,而且在现代化的网站中,这两个地方的 HTML大概率是不一样的。当我们使用 requests 或者 Scrapy 时,拿到的是第一种情况的源代码,这才是网页真正的源代码。而在开发者工具里面的 HTML 代码,是经过 Chrome 浏览器修饰甚至大幅度增删后的 HTML 代码。当网站有异步加载时,JavaScript 可以轻易在这里增加、删除非常多的内容。即使网站没有异步加载,如果网站原始的 HTML 代码编写不够规范,或者存在一些错漏,那么 Chrome 浏览器会自动纠错和调整。

参考资料

通用爬虫结构

my_spider/
├── main.py                # 主程序入口,启动爬虫
├── config.py              # 存放配置信息,如爬取目标、请求头等
├── requirements.txt       # 项目依赖的 Python 包列表
├── utils/
│   ├── __init__.py
│   ├── helpers.py         # 存放一些通用的辅助函数
│   └── constants.py       # 存放一些常量
├── data/
│   ├── __init__.py
│   └── output/
│       ├── __init__.py
│       └── results.csv    # 存放爬取结果
├── logs/
│   ├── __init__.py
│   └── spider.log         # 存放日志文件
├── spiders/
│   ├── __init__.py
│   ├── base_spider.py     # 爬虫基类,定义通用的爬取逻辑
│   ├── site1_spider.py    # 具体的爬虫实现,针对不同网站的爬取逻辑
│   └── site2_spider.py
└── tests/
    ├── __init__.py
    └── test_spiders.py     # 测试爬虫的代码

常用依赖库

PyMySQL~=1.1.0
requests~=2.31.0
beautifulsoup4~=4.12.2
selenium~=4.16.0
python-dateutil~=2.8.2

常用的request配置

web_config = {
    'headers': {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/120.0.0.0 Safari/537.36'
    },
    'proxies': {
        'http': 'http://127.0.0.1:7890',
        'https': 'http://127.0.0.1:7890'
    },
    'params': {
        'wd': 'ip'
    },
}

select与find

select 与 select_one

顾名思义 select即为选择全部,select_one即为选择一个

参数应该为CSS选择器

常见的选择器:

  • 通过标签
# 选择所有的段落标签
paragraphs = soup.select('p')
  • 通过类
# 选择具有 "example" 类的所有元素
elements_with_class = soup.select('.example')
  • 通过ID
# 选择具有 "unique-id" ID的元素
element_with_id = soup.select('#unique-id')
  • 通过组合
# 选择所有类为 "example" 的段落标签
paragraphs_with_class = soup.select('p.example')
  • 通过属性
# 选择具有特定属性的元素
elements_with_attribute = soup.select('[attribute_name="value"]')

需要注意的是:

选择器的选择目标并不唯一,同样的类选择器可能选到不同的目标,select_one会先匹配最近的,select会匹配所有的组成一个列表

find与find_all

Beautiful Soup库的find方法是用于查找文档中第一个符合条件的元素。该方法的语法为:

find(name, attrs, recursive, text, **kwargs)
  • name: 标签名或标签名列表,用于匹配相应的标签。
  • attrs: 字典,用于匹配标签的属性。
  • recursive: 布尔值,表示是否在子孙节点中查找,默认为True。
  • text: 字符串或正则表达式,用于匹配标签的文本内容。
  • **kwargs: 其他属性条件,可以用关键字参数传递
content_div = soup.find('div', id='content')

找到第一个id为’content’的元素,find_all与上面的select同理。

值得注意的是

根据HTML规范,每个页面上的元素都应该具有唯一的id属性值。这就意味着,相同页面上的两个元素不应该有相同的id值。

保持id的唯一性是一种良好的HTML编码实践,有助于提高页面的可维护性和可读性。

通过id查找的元素一般是唯一的。同理ID选择器也是如此。

找到元素如何获取文本

# 获取第一个段落标签的文本内容
first_paragraph_text = paragraphs[0].text

# 获取具有 "unique-id" ID的元素的属性值
id_value = element_with_id[0]['id']

text和string获取元素文本内容的区别

text属性:

  • text属性用于获取当前元素下的所有文本内容,包括其子孙元素的文本。
  • 如果当前元素包含子元素,text属性会返回一个合并了所有文本内容的字符串。
  • text属性返回的是一个字符串类型。
from bs4 import BeautifulSoup

html = "<p>This is <b>bold</b> text.</p>"
soup = BeautifulSoup(html, 'html.parser')
p_tag = soup.find('p')

print(p_tag.text)
# 输出:This is bold text.

string属性:

  • string属性用于获取当前元素的文本内容。
  • 如果元素本身包含文本内容,string属性将返回该文本内容。但如果元素包含子元素,string属性将返回None
from bs4 import BeautifulSoup

html = "<p>This is <b>bold</b> text.</p>"
soup = BeautifulSoup(html, 'html.parser')
p_tag = soup.find('p')

print(p_tag.string)
# 输出:None,因为<p>有多个子节点,不是字符串类型

所以我一般直接用text属性

get方法

使用[]获取文本时,如果[]里面的属性在标签中未找到,会直接报错,所以为了更加贴合实际开发的需求,没找到的使用None来代替,我一般使用get方法,这样如果不存在该属性名,则会返回指定值,不会报错导致退出程序。

get(attribute_name, default=None)
  • attribute_name: 要获取的属性名。
  • default: 如果指定的属性不存在,返回的默认值,默认为None
from bs4 import BeautifulSoup

html = '<a href="https://www.example.com" target="_blank">Example</a>'
soup = BeautifulSoup(html, 'html.parser')

a_tag = soup.find('a')

# 使用get方法获取href属性值,如果属性不存在则返回默认值
href_value = a_tag.get('href', 'No link found')
print(href_value)  # 输出:https://www.example.com

# 使用get方法获取target属性值,如果属性不存在则返回默认值
target_value = a_tag.get('target', 'No target found')
print(target_value)  # 输出:_blank

# 使用get方法获取不存在的属性,返回默认值
nonexistent_value = a_tag.get('nonexistent', 'Default value')
print(nonexistent_value)  # 输出:Default value

在上述示例中,a_tag是一个<a>标签元素,通过get方法获取了其hreftarget属性的值。如果属性不存在,则返回了指定的默认值。

get方法在处理 HTML 属性时很方便,特别是当你希望在属性不存在时提供一个默认值的情况。

爬虫协议

robots.txt 文件是网站用来向网络爬虫提供指导的文本文件,告诉爬虫哪些部分可以被爬取,哪些部分不应该被爬取。你可以通过在网站的根目录下添加 /robots.txt 来查看这个文件。

你可以通过在浏览器中输入网站的基本地址,然后加上 /robots.txt 来查看。例如:

https://www.example.com/robots.txt

在浏览器中访问这个 URL,你应该能够看到网站的 robots.txt 文件内容。这个文件可能包含一系列的规则,指定哪些爬虫是允许或被禁止的,以及哪些页面或目录是被允许或被禁止的。

SSL连接

你长时间爬取可能会收到如下报错

urllib3.exceptions.SSLError: [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1000)

我的猜想是由于长时间高频率的爬取,我的代理服务器短暂停了网络服务,拒绝了我的访问。

解决办法

使程序休眠即可

time.sleep(60)

selenium

在使用request时,经常会发现有些页面爬取不是很完整,因为有些页面是动态加载的,需要网页去执行JavaScript代码来生成页面,比如用react写的网页,这时request就不好用了。

我一般使用selenium这个工具来解决此类问题

  1. 安装Selenium: 首先确保您已经安装了Selenium。您可以使用以下命令通过pip安装:

    pip install selenium
  2. 安装浏览器驱动程序: Selenium需要浏览器驱动程序来控制浏览器。您需要下载适用于您使用的浏览器的驱动程序,并确保将其添加到系统路径中。例如,对于Chrome浏览器,您可以从ChromeDriver下载驱动程序。

  3. 使用Selenium打开浏览器: 编写Python脚本,使用Selenium打开浏览器并访问目标网页。

    from selenium import webdriver
    
    # 选择浏览器驱动程序路径并初始化浏览器
    driver = webdriver.Chrome(executable_path='/path/to/chromedriver')
    
    # 打开网页
    driver.get('https://example.com')
    
    # 获取网页源代码
    page_source = driver.page_source
    
    # 打印或进一步处理页面源代码
    print(page_source)
    
    # 关闭浏览器
    driver.quit()

    请将/path/to/chromedriver替换为您实际的ChromeDriver路径。

    我直接用webdriver.Chrome()也是可以的,应该是默认有驱动

等待元素加载

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 创建一个 WebDriver 实例(这里使用 Chrome)
driver = webdriver.Chrome()

# 设置超时时间为10秒
timeout = 10

try:
    # 使用 get 方法加载页面
    driver.get("https://example.com")

    # 使用 WebDriverWait 来等待页面加载完成
    element_present = EC.presence_of_element_located((By.ID, 'your_element_id'))
    WebDriverWait(driver, timeout).until(element_present)

    # 如果页面加载成功,继续进行其他操作

except TimeoutException:
    # 如果超时,处理异常,可以打印错误信息或执行其他操作
    print("页面加载超时")

finally:
    # 关闭浏览器窗口
    driver.quit()

在上面的代码中,selenium将会等待id等于’your_element_id’的元素加载出来,如果在timeout时间内没有加载出来,会抛出TimeoutException错误

一般来说。等待某个元素加载方式可能会比较省时间,但是依旧存在加载不完的情况

更加通用的办法是:

time.sleep(60)

加载完成后使用page_source = driver.page_source获取网页源代码放入beautifulsoup解析即可

获取API

captured_requests = driver.execute_script("return window.performance.getEntriesByType('resource')")

具体来说,getEntriesByType 用于按类型检索性能条目。以下是一些常见的使用情况:

  1. 获取导航性能信息:

    var navigationEntries = window.performance.getEntriesByType('navigation');

    这将返回包含有关页面导航性能的条目的数组,例如页面加载时间、重定向次数等。

  2. 获取资源性能信息:

    var resourceEntries = window.performance.getEntriesByType('resource');

    这将返回包含有关每个资源加载性能的条目的数组,例如图片、脚本、样式表等的加载时间、大小等信息。

  3. 获取标记和测量信息:

    var markEntries = window.performance.getEntriesByType('mark');
    var measureEntries = window.performance.getEntriesByType('measure');

    这将返回包含有关通过 performance.markperformance.measure 设置的标记和测量性能信息的数组。

    寻找XHR

    for request in captured_requests:
        url = request['name']
        method = request['initiatorType']
        if method == 'xmlhttprequest':
            if url.startswith(f'https://store.steampowered.com/appreviews/489830?cursor='):
                response = requests.get(url)
                json_data = response.json()
                soup = BeautifulSoup(json_data['html'], 'lxml')
                contents = soup.find_all('div', class_='content')
                for content in contents:
                    print(content.text.strip())

执行js代码

控制页面往下滑动

有些页面时常需要往下滑动触发加载请求

driver.execute_script("window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'});")

注意,有时滑动并不会滑到底,只会滑倒最下面未加载的页面

所以需要配合time.sleep()使用

我一般是:

driver.execute_script("window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'});")
time.sleep(5)
driver.execute_script("window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'});")
time.sleep(5)
driver.execute_script("window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'});")
time.sleep(5)

查找元素

selenium当然也能查找元素

link_element = driver.find_element(By.CSS_SELECTOR, '#view_product_page_btn > span')

执行按键操作

以下是我用过的一个处理方式

if driver.current_url == f'https://store.steampowered.com/agecheck/app/{AppID}/':
    WebDriverWait(driver, 60).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, '#view_product_page_btn > span')))
    WebDriverWait(driver, 60).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, '#ageYear')))
    select_element = driver.find_element(By.CSS_SELECTOR, '#ageYear')
    select = Select(select_element)
    select.select_by_value('2000')
    link_element = driver.find_element(By.CSS_SELECTOR, '#view_product_page_btn > span')
    link_element.click()

代码的意思是:

等待页面的两个对应元素加载完成,使用Select选择改元素,这个元素是一个下拉框,通过value选择年份,再按下确认按钮即可完成网页按下下拉框选择时间然后点击确认的操作。

driver.find_element(By.CSS_SELECTOR, '#ageYear') 使用 CSS 选择器定位了一个 id 为 ‘ageYear’ 的元素。接下来,Select(select_element) 将该元素转换为 Select 对象,使您能够执行与下拉列表相关的操作。

例如,您可以使用 Select 对象选择下拉列表中的选项,例如:

from selenium.webdriver.support.ui import Select

# 定位下拉列表元素
select_element = driver.find_element(By.CSS_SELECTOR, '#ageYear')

# 转换为 Select 对象
select = Select(select_element)

# 通过值选择选项
select.select_by_value('1990')

上述代码将选择下拉列表中值为 ‘1990’ 的选项。可以根据需要执行其他操作,例如通过索引、文本等选择选项,或者获取当前选中的选项等。

日期处理

通用格式

爬虫过程中可能遇到很多种奇怪的日期格式,这时就需要转换成通用的格式

from datetime import datetime
from dateutil import parser

def parse_date(date_string):
    try:
        # 尝试使用 dateutil.parser.parse 解析
        parsed_date = parser.parse(date_string)
        return parsed_date.strftime("%Y-%m-%d")
    except ValueError:
        try:
            # 尝试使用 datetime.strptime 解析中文日期
            chinese_date_format = "%Y 年 %m 月 %d 日"
            parsed_date = datetime.strptime(date_string, chinese_date_format)
            return parsed_date.strftime("%Y-%m-%d")
        except ValueError:
            return None

# 测试不同格式的日期字符串
date1 = '2022 年 2 月 25 日'
date2 = 'Apr 2019'
date3 = '2022-02-25'

parsed_date1 = parse_date(date1)
parsed_date2 = parse_date(date2)
parsed_date3 = parse_date(date3)

print(parsed_date1)
print(parsed_date2)
print(parsed_date3)

日期字符串转datetime对象

from datetime import datetime

# 输入字符串
date_string = '2023-12-12'

# 使用strptime方法将字符串转换为datetime对象
date_object = datetime.strptime(date_string, '%Y-%m-%d')

# 输出结果
print(date_object)

间隔为7的日期列表

start_date = datetime.strptime(url_config['start_date'], '%Y-%m-%d')
end_date = datetime.strptime(url_config['end_date'], '%Y-%m-%d')
interval_days = 7

date_list = []

current_date = start_date
while current_date.date() <= end_date.date():
    date_list.append(current_date.date())
    current_date += timedelta(days=interval_days)

log

使用logging模块处理日志

import logging
import traceback

# 配置日志
logging.basicConfig(filename='error.log', level=logging.ERROR)

try:
    # 你的代码,可能会引发异常
    result = 1 / 0
except Exception as e:
    # 捕获异常并将 traceback 写入日志
    logging.error("An error occurred: %s", str(e), exc_info=(type(e), e, e.__traceback__))

API

我一般是通过观察XHR请求获得处理方法。

requests库版本太高的error

Python遇到的坑–ValueError: check_hostname requires server_hostname

报错:

import requests
res = requests.get(url="https://blog.csdn.net/liboshi123/", verify=False)

运行上面的代码的时候,发现报了下面的错误:

  raise ValueError("check_hostname requires server_hostname")
ValueError: check_hostname requires server_hostname

报错的原因:

这个其实跟选用的python版本的关系不大,主要原因是因为每次使用 pip install 命令下载插件的时候,下载的都是最新的版本,比如下载requests插件,它会自动的将依赖的urllib3这个插件也安装,然后依赖的插件版本太高,就导致了这个报错的问题。

所以说,一般遇到这种莫名其妙的问题的时候,可以先去看一下是不是插件的问题导致的,解决措施就是 将urllib3插件的版本降低就可以,当然,直接在安装requests插件的时候,选择用低版本也可以解决这个问题。比如用下面的命令指定版本进行安装:

pip install requests==2.20
或者使用下面的命令降低版本:
pip install urllib3==1.25.8

这种类似的问题,在使用一些框架的时候经常会遇到,比如有的小伙伴在学习django,然后照着别人博客写的文章操作,最后报错,很有可能就是插件的版本导致的。

另外,在线安装插件时,如果插件下载过慢,或者报错的话,可以在插件的命令后面加上 -i 指定插件安装的源。

pip install 插件名称  -i http://mirrors.aliyun.com/pypi/simple

有时候报插件找不到的话,就换一个源试试。

不想每次都指定源进行安装的话 ,那就在用户名下文件夹下建一个pip的文件夹,然后新建pip.ini的配置文件,写入下面的内容就行(具体的源可以自己选择):{创建这个配置文件的存放位置有很多种方式都可以,感兴趣的可以自己去试试,比如pip所在目录下,或者%APPDATA%目录下去新建文件夹。}

[global]
index-url = http://mirrors.aliyun.com/pypi/simple
[install]
trusted-host=mirrors.aliyun.com

另外,有些插件通过上面的在线方式就是容易出现报错的,可以尝试用离线安装的方式去安装插件,去网上下载whl格式的文件进行安装,比如,可以在下面的链接下下载:

whl格式插件:

https://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml

pip install xxx.whl

官网下载插件:

https://pypi.org/

解压后,在目录执行:python setup.py install

一些数据库处理经验

用到的库

import pymysql as mysql

常用配置

db_config = {
    'host': 'localhost',
    'user': 'root',
    'password': '523902',
    'database': 'steam'
}

执行创建数据库sql脚本

# 创建数据库表
def create_schema(db: mysql.connections.Connection) -> None:
    """
    创建数据库的函数

    Args:
        db (pymysql.connections.Connection): pymysql的连接

    Returns:
        None: 这个函数没有返回值

    Raises:
        ValueError: 如果参数不符合预期,可能会引发 ValueError 异常。

    Example:
        >>> create_schema(db)
        None
    """
    with open(r"D:\pycharm_pro\database-lab\Steam_create.sql", mode="r", encoding="utf-8") as file:
        create_database_sql = file.read()
    cursor = db.cursor()
    try:
        sql_commands = create_database_sql.split(';')
        for command in sql_commands:
            command = command.strip()
            if command:
                cursor.execute(command + ';')
                db.commit()
    except Exception as e:
        print(f"Error message: {e}")
        db.rollback()
    finally:
        cursor.close()

数据库插入操作

因为插入操作经常涉及获取上一次插入的自增主键的值,所以使用一个可选参数通过传递一个类的方式来获得值。

# 辅助类,用于传入引用参数
from datetime import datetime

import pymysql as mysql
from dateutil import parser


class Wrapper:
    def __init__(self, value):
        self.value = value


# 插入数据库
def insert_sql(db: mysql.connections.Connection, sql: str, params: tuple, lastrowid=Wrapper(-1)) -> None:
    """
    插入数据库的函数

    Args:
        db (pymysql.connections.Connection): pymysql的连接
        sql(str): sql语句
        params(tuple): sql语句中占位符的参数
        lastrowid(Wrapper): 返回最后一次插入时的自增主键的值

    Returns:
        None: 这个函数没有返回值

    Raises:
        ValueError: 如果参数不符合预期,可能会引发 ValueError 异常。

    Example:
        >>> insert_sql(db,sql,params,lastrowid)
        None
    """
    cursor = db.cursor()
    try:
        cursor.execute(sql, params)
        lastrowid.value = cursor.lastrowid
        db.commit()
    except Exception as e:
        print(f'Error: {e}')
        print(sql)
        db.rollback()
    finally:
        cursor.close()

数据库导入导出

使用mysqldump导出数据库

mysqldump -u root -p123456 -h localhost your_database > your_dump.sql

使用mysql导入数据库

mysql -u root -p123456 -h localhost --default-character-set=utf8 your_database < your_dump.sql

值得注意的是使用mysql导入数据库时,一定要指定与原来导出.sql文件相同的字符集。

不然会出现如下问题:无法识别.sql文件中部分的字符


文章作者: Keyen Zhou
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Keyen Zhou !
  目录