Python 爬虫相关工具汇总

时间:2025-03-14 18:20:48

1 | 连接

1-1 | urllib

标准库

from urllib.request import urlopen
# 打开网页
html = urlopen('/pages/')
print(html.read())

网站自动重定向

Python 3 的 urllib 会自动处理网页被重定向

1-2 | requests

第三方库,pip install requests

网站自动重定向

需要手动设置 allow_redirects=True

r = requests.get('', allow_redirects=True)

1-3 | requests-html

requests 库作者编写,适用 Python 3.6 及以上版本,其能获取页面并处理元素

2 | 解析

2-1 | BeautifulSoup

第三方库,需安装 pip install beautifulsoup4

内置解析器:

请求网页并解析后,返回一个对象,可通过属性形式获取值

from urllib.request import urlopen
from bs4 import BeautifulSoup
# 请求网页
html = urlopen('/pages/')
# 解析网页, 第二个参数可换成其他解析器
bs = BeautifulSoup(html.read(), '')
bs.body.h1
四种对象
  1. BeautifulSoup 对象
  2. Tag 标签对象
  3. NavigableString 对象,标签中的文字
  4. Comment 对象,HTML 中的注释代码
关系查找

BeautifulSoup 默认查找后代标签,即父标签下的所有级别标签

# 只查子标签
bs.find('table', {'id':'giftList'}).children
# 兄弟标签
# 便于处理表格
bs.find('table', {'id':'giftList'}).next_sibling
bs.find('table', {'id':'giftList'}).next_siblings
bs.find('table', {'id':'giftList'}).previous_sibling
bs.find('table', {'id':'giftList'}).previous_siblings
# 父标签
bs.find('table', {'id':'giftList'}).parent
bs.find('table', {'id':'giftList'}).parents
# 后代标签
>>> bs1.descendants
<generator object Tag.descendants at 0x0000028EB3021230>
⚡常用方法
  • find_all(name, attrs, recursive, text, limit)

    ------- 查找所有符合条件的元素

    1. name 可设为集合, 如 {"h1", "h2", "h3"}
    2. attrs 可包含多个值 {'class': {'red', 'blue'}}
    3. recursive 是布尔值
    4. text 匹配指定文字内容
    5. limit 指定数量限制
    bs.findAll('span', {'class':'green'})  # <span class="green"></span>
    bs.find_all(['h1','h2','h3','h4','h5','h6'])
    bs.find_all('span', {'class':{'green', 'red'}})
    bs.find_all(class_='green')
    bs.find_all(text='the prince')
    
    >>> a = bs1.findAll("span", {"class": "green"})[0]
    >>> type(a)
    <class ''>
    
  • find(tag, attributes, recursive, text, keywords)

    查找单个标签

    # 相当于 find_all(*args, limit=1, **kwargs)
    # find(tag, attributes, recursive, text, keywords)
    bs.find(id='title')
    
  • get_text()

    返回无标签文字内容

    span = bs1.findAll("span", {"class": "green"})[0]
    >>> span.get_text()
    'Anna\nPavlovna Scherer'
    
  • .attrs 获取一个标签对象的所有属性

正则表达式

???? 如果你有一个问题需要正则表达式解决,那么问题就是两个了????

BeautifulSoup 支持正则匹配,通过正则对象实现:

import re
bs.find_all("img", {"src": re.complie("\.\.\/img\/gifts\/img.*\.jpg")})

# ../img/gifts/
# ../img/gifts/
# ../img/gifts/
Lambda 表达式

Beautiful 支持匿名函数,如获取只有两个属性的标签

>>> bs1.findAll(lambda tag: len(tag.attrs) == 2)

3 | 解析器

3-1 |

标准库,用于解析包含 HTML 的字符串

3-2 | lxml

第三方库,需安装 pip3 install lxml

优点:解析“杂乱”或包含错误语法的 HTML 代码的性能更好。可容忍并修正一些问题,如未闭合的标签、未正确嵌套的标签,以及缺失的 <head> 标签或 <body>标签

3-3 | html5lib

具有容错性的解析器,可容忍语法更糟糕的 HTML,但速度较慢

4 | 框架

4-1 | Scrapy

第三方库,需安装 pip install Scrapy

4-1-1 | 简易爬虫

1.创建项目

指令会自动创建项目文件

$ scrapy startproject <proj_name>

2.定义 Spider 类

在自动生成的 spider 文件夹下创建文件,并定义

import scrapy

class ArticleSpider(scrapy.Spider):
	name='article'
	def start_requests(self):
		urls = [
            '/wiki/Python_'
            '%28programming_language%29',
            '/wiki/Functional_programming',
            '/wiki/Monty_Python']
		return [scrapy.Request(url=url, callback=self.parse) for url in urls]
	
	def parse(self, response):
        url = response.url
        title = response.css('h1::text').extract_first()

3.执行

$ scrapy runspider 
4-1-2 | CrawlSpider
from scrapy.contrib.linkextractors import LinkExtractor
from scrapy.contrib.spiders import CrawlSpider, Rule

class ArticleSpider(CrawlSpider):
    name = 'articles'
    allowed_domains = ['']
    start_urls = ['/wiki/Benevolent_dictator_for_life']
	rules = [
        Rule(LinkExtractor(allow=r'.*'), callback='parse_items', follow=True)
    ]
    
	def parse_items(self, response):
        url = response.url
        title = response.css('h1::text').extract_first()
        ...
  • Rule 类可用于定义规则过滤不需要的链接
  • LinkExtractor 可根据规则提取链接,入参除了 allow 还有 deny
4-1-3 | Item

在自动生成的 文件中定义从一个 URL 中要爬取的事物

import scrapy

class Article(scrapy.Item):
    url = scrapy.Field()
    title = scrapy.Field()
    text = scrapy.Field()
    lastUpdated = scrapy.Field()

结合类 Crawler 使用:

from scrapy.contrib.linkextractors import LinkExtractor
from scrapy.contrib.spiders import CrawlSpider, Rule
from wikiSpider.items import Article
class ArticleSpider(CrawlSpider):
    name = 'articleItems'
    ...
    
def parse_items(self, response):
    article = Article()
    article['url'] = response.url
    article['title'] = ...

输出 Item:

$ scrapy runspider  -o  -t csv
$ scrapy runspider  -o  -t json
$ scrapy runspider  -o  -t xml
4-1-4 | Pipeline

虽然 Scrapy 是单线程的,但可异步发出和处理多个请求,适合数据处理需要大量时间时,或计算密集型的场景

1. 设置

在自动生成的 文件中

ITEM_PIPELINES = {
	# 300 表示当存在多个数据处理类时,运行管线组件的顺序, 建议 1~1000
	'': 300,
}

2.返回 Item

parse_items 方法返回一个item,负责提取原始数据,尽可能少做数据处理,然后传递给管线组件

3.定义处理逻辑

在自动生成的 文件中定义:

from wikiSpider.items import Article
from string import whitespace

class WikispiderPipeline(object):
    def process_item(self, article, spider):
        article['text'] = [line for line in article['text']
        return article
4-1-5 | Scrapy日志

先在 中定义日志默认级别:

LOG_LEVEL = 'ERROR'
# 只有 CRITICAL 和 ERROR 日志会显示

指定日志文件:执行爬虫时,可指定记录日志的文件,避免输出到终端:

$ scrapy crawl articles -s LOG_FILE=wiki.log

5 | 工具

5-1 | PySocks

一个简单的 Python 代理服务器通信模块,可隐藏 IP,也可配合 Tor 使用

import socks
import socket
from urllib.request import urlopen

socks.set_default_proxy(socks.SOCKS5, "localhost", 9150)
socket.socket = socks.socksocket
print(urlopen('').read())

5-2 | Selenium

第三方库,通过 WebDriver 对象对网页操作。WebDriver 类似一个浏览器,但可以像 BeautifulSoup 一样查找页面元素,与页面上的元素交互(如施放动作),常见任务之一是等待元素出现、消失

5-2-1 | Driver

无头浏览器 PhantomJS 使用前需要下载 Download PhantomJS

driver = webdriver.PhantomJS(executable_path='')
# 会唤起浏览器程序
browser = webdriver.Firefox('<path to Firefox webdriver>')
browser = webdriver.Chrome('<path to Chrome webdriver>')
browser = webdriver.Safari('<path to Safari webdriver>')
browser = webdriver.edge('<path to Internet Explorer webdriver>')

⚠️ 高版本 Selenium 已放弃 PhantomJS

from selenium import webdriver
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--headless")
browser = webdriver.Chrome(options=chrome_options)
5-2-2 | 重定向

对于服务器重定向,可以通过 requestsurllib 处理,但是 JS 代码执行的重定向需要持续检测是否已完成跳转

from selenium import webdriver
import time
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import StaleElementReferenceException

def waitForLoad(driver):
    # 1.随便找一个元素
    elem = driver.findElement(By.tagName("html"))
    count = 0
    while True:
        count += 1
        if count > 20:
            print('Timing out after 10 seconds and returning')
			return
		time.sleep(.5)
		try:
            # 2.标签不存在时,说明网站依井跳转
			elem == driver.findElement(By.tagName("html"))
		except StaleElementReferenceException:
			return

driver = webdriver.PhantomJS(executable_path='<Path to Phantom JS>')
driver.get('/pages/javascript/')
waitForLoad(driver)
print(driver.page_source)
5-2-3 | 隐藏 IP

无需安装 Pysocks,使用参数 service_args 即可

from selenium import webdriver
service_args = [ '--proxy=localhost:9150', '--proxy-type=socks5', ]
driver = webdriver.PhantomJS(executable_path='<path to PhantomJS>', service_args=service_args)
driver.get('')
print(driver.page_source)
driver.close()
5-2-4 | 等待元素出现

????示例:等待页面加载 3 秒后获取内容

from selenium import webdriver
import time
driver = webdriver.PhantomJS(executable_path='<PhantomJS Path Here>')
driver.get('/pages/javascript/')
time.sleep(3)
print(driver.find_element_by_id('content').text)
driver.close()

????示例:持续检测页面上某元素是否存在,隐式等待

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

driver = webdriver.PhantomJS(executable_path='')
driver.get('/pages/javascript/')
try:
	element = WebDriverWait(driver, 10).until(
        # 期望条件:元素出现
		EC.presence_of_element_located((By.ID, 'loadedButton'))
    )
finally:
	print(driver.find_element_by_id('content').text)
	driver.close()

5-3 | Tesseract

一个 OCR 库,目前由 Google赞助。Tesseract 是目前公认最优秀、最精确的开源OCR 系统

有 Linux 系统上的程序,也有 Python 第三方库 pytesserac

????识别图片验证码

import pytesseract
print(pytesseract.image_to_string(Image.open('files/')))

5-4 | _thread

非常底层的标准库,可开多线程进行爬取,提升效率

# 1.定义爬虫的函数
def crawl(thread_name: str, link: str):
    pass
# 2.开启线程
_thread.start_new_thread(crawl, ('Thread 1', '/link1',))
_thread.start_new_thread(crawl, ('Thread 2', '/link1'))
5-4-P | 竞争条件

问题

Race Condition,多个线程可能同时开始执行代码,导致一个页面被重复爬取

解决

  • list

    建一个空列表存储已爬过的页面链接,但不能避免竞争条件,且移除首个元素性能不佳

  • Queue

    以线程安全的方式传送静态数据的,从队列中检索出来之后,数据应该只存在于检索它的线程中

5-5 | threading

基于 _thread 的高级标准库,vs _thread 使用更方便:

  • 某些接口改了更好记的名称:get_ident 改为 currentThread

  • 可创建线程的局部数据 ()

    import threading
    def crawler(url):
    	data = threading.local()
    	data.visited = []
    # 抓取网站
    threading.Thread(target=crawler, args=('http://...')).start()
    

5-6 | multiprocessing

from multiprocessing import Process

# 1.定义爬取逻辑
def crawl(path):
	pass

# 2.定义进程
processes = []
processes.append(Process(target=crawl, args=('/aaa',)))
processes.append(Process(target=crawl, args=('/bbb',)))

# 3.开始进程
for p in processes:
	p.start()
    
# 可选:如果有代码需要进程结束再执行需要调用 join(), 否则 print() 会直接执行
for p in processes:
	p.join()
print('example text..')  

进程之间的通讯问题

创建两个“公共”队列,再额外定义一个委托器 delegator 负责检查页面是否已被爬取过

def delegator(taskQueue, urlsQueue):
    visited = ['/aaa', '/bbb']
    taskQueue.put('/aaa')
    taskQueue.put('/bbb')
	while 1:
		# 检查链接队列中是否存在新链接
		if not urlsQueue.empty():
			# 检查是否存在未访问过的页面
			links = [link for link in urlsQueue.get() if link not in visited]
			for link in links:
				# 将未访问页面添加到任务队列
				taskQueue.put(link)


def crawl(taskQueue, urlsQueue):
	while 1:
		while taskQueue.empty():
			time.sleep(1)	# 如果任务队列为空,休息 1 秒
        # 取出一个需要处理的页面    
		path = taskQueue.get()
        ...  # 爬取逻辑
        # 发现新页面,放入链接队列
        urlsQueue.put(links)
        
        
processes = []
taskQueue = Queue()
urlsQueue = Queue()
processes.append(Process(target=delegator, args=(taskQueue, urlsQueue,)))
processes.append(Process(target=crawl, args=(taskQueue, urlsQueue,)))
processes.append(Process(target=crawl, args=(taskQueue, urlsQueue,)))
for p in processes:
	p.start()

6 | 其他

6-1 | 目标值所在标签被多层嵌套

最终代码有效但极为复杂:

s.find_all('table')[4].find_all('tr')[2].find('td').find_all('div')[1].find

如何解决

  • 寻找“打印此页”的链接,或者看看网站有没有 HTML 样式更友好的移动版,需设置请求头
  • 寻找隐藏在 JavaScript 文件里的信息
  • 网页标题也许可以从网页的 URL链接里获取
  • 查找其他数据源

设计爬虫时注重异常捕捉同时兼顾易读性,获取的链接去重避免重复爬取,下载文件注意安全性

6-2 | 网站分类

  • 浅网 surface web:搜索引擎能搜出的页面

  • 深网 deep web:搜索引擎无法搜出的页面

  • 暗网 dark web:只有通过特定客户端或身份验证才能访问的网站,通常进行违法行为

Json 格式相比XML:XML 格式由于采用开合标签的形式,所以字符更多

6-3 | 盗链

Hotlink,将对方网站的资源链接嵌入我方网站,通常网站会有防盗链机制

6-4 | 页面编码

查看页面的 <meta charset="utf-8" /> 标签以确定页面使用的字符集,有部分网站仍在使用 ISO 系列字符集

content = "Non utf-8 string"
content = bytes(content, 'UTF-8')
content = content.decode('UTF-8')

6-5 | 从 CSV 获取数据

思路:将 .csv 文件中数据转为 StringIO 对象,让 Python 将其视为文件处理,无需下载源文件

from urllib.request import urlopen
from io import StringIO
import csv
data = urlopen('/files/'
              ).read().decode('ascii', 'ignore')
dataFile = StringIO(data)
csv_reader = csv.reader(dataFile)

6-6 | 从 PDF 获取数据

PDFMiner3K

from urllib.request import urlopen
from pdfminer.pdfinterp import PDFResourceManager, process_pdf
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from io import StringIO
from io import open

def readPDF(pdfFile):
	rsrcmgr = PDFResourceManager()
	retstr = StringIO()
	laparams = LAParams()
	device = TextConverter(rsrcmgr, retstr, laparams=laparams)
	process_pdf(rsrcmgr, device, pdfFile)
    device.close()
    content = retstr.getvalue()
    retstr.close()
    return content

6-7 | 提交文件

import requests
files = {'uploadFile': open('files/', 'rb')}
r = requests.post('/pages/', files=files)
print(r.text)

6-8 | Cookie 跟踪

import requests

# 1.获取 cookie
params = {'username': 'Ryan', 'password': 'password'}
r = requests.post('/pages/cookies/', params)

# 2.请求时使用 cookie
r = requests.get('/pages/cookies/', cookies=r.cookies)

6-9 | Session 跟踪

Session 对象可持续跟踪会话信息,,包括 cookie、header,HTTP 协议的信息,应对网站暗自修改cookie 的情况

import requests
# 实例化 Session
session = requests.Session()
params = {'username': 'username', 'password': 'password'}
s = session.post('/pages/cookies/', params)
s = session.get('/pages/cookies/')

6-10 | HTTP Basic Access Authentication

import requests
from requests.auth import AuthBase
from requests.auth import HTTPBasicAuth
# 定义用户名、密码
auth = HTTPBasicAuth('ryan', 'password')
r = requests.post(
    url='/pages/auth/', 
    auth=auth
)

6-11 |

1994 年搜索引擎技术刚刚兴起时出现,目的防止搜索引擎搜出相对隐私的页面内容使用,也称为机器人排除标准(Robots Exclusion Standard)

⚠️注意

  • 文件的语法没有标准格式
  • 文件并不是一个强制性约束

语法:

  • 注释用 # 号,换行符结尾
  • 如果一条规则后有一个与之矛盾的规则,则按后一条规则执行

????示例:除 Google 机器人不能访问 /private 外禁止其他所有机器人

#Welcome to my  file!
User-agent: *
Disallow: *

User-agent: Googlebot
Allow: *
Disallow: /private

6-12 | Tor

The Onion Router,洋葱路由器,一种 IP 地址匿名手段。

工作原理:由志愿者服务器构成一个网络,通过不同服务器构成的多个层(就像洋葱)把客户端包在最里面。数据进入该网络前会被加密,因此任何服务器都不能偷取通信数据。

虽然每个服务器的入站和出站通信都可查到,但是要想查出通信的真正起点终点必须知道整个通信链路上所有服务器的入站和出站通信细节,而这基本上是不可能的

缺点:网速可能很慢,因为请求需要多次传递

6-13 | 请求头

抓取时需伪装称浏览器,浏览器常用的 7 个 Request Header

名称
1 Host
2 Connection keep-alive
3 Accept text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
4 User-Agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5)
AppleWebKit/537.36(KHTML, like Gecko) Chrome/39.0.2171.95
Safari/537.36
5 Referrer
6 Accept-Encoding gzip,deflate,sdch
7 Accept-Language en-US,en;q=0.8

⚠️ urllib 会自动将 User-Agent 设为 Python-urllib/3.4

6-14 | 蜜罐

用隐含字段阻止网页抓取的方式主要有两种:

  • <form>标签中添加一个隐藏字段,值为服务端生成的随机值,提交时没有该字段会被敌方服务器识破
  • 蜜罐:honey pot,敌方服务器并不会使用提交数据中的所有隐藏字段,如果提交了这些服务器实际不用的隐藏字段,会被敌方识破

????检查<form>所在的页面,是否存在遗漏、弄错一些服务器预先设定好的隐含字段

????示例:1 个隐藏链接 + 2 个隐藏标签

<style>
.customHidden {
	position:absolute;
	right:50000px;
}
</style>
<body>
	<h2>A bot-proof form</h2>
	<a href="http://..." style="display:none;">Go here!</a>
	<a href="">Click me!</a>
	<form>
		<input type="hidden" name="phone" value="valueShouldNotBeModified"/><p/>
		<input type="text" name="email" class="customHidden"value="intentionallyBlank"/><p/>
        <input type="text" name="firstName"/><p/>
        <input type="text" name="lastName"/><p/>
        <input type="submit" value="Submit"/><p/>
	</form>
</body>

如何破招:Selenium 可以识别元素是否可见 fields = driver.find_elements_by_tag_name('input').is_displayed()

END