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
四种对象
- BeautifulSoup 对象
- Tag 标签对象
- NavigableString 对象,标签中的文字
- 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)
------- 查找所有符合条件的元素
-
name 可设为集合, 如
{"h1", "h2", "h3"}
-
attrs 可包含多个值
{'class': {'red', 'blue'}}
- recursive 是布尔值
- text 匹配指定文字内容
- 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 ''>
-
name 可设为集合, 如
-
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 | 重定向
对于服务器重定向,可以通过 requests
或 urllib
处理,但是 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