- 简介
- 获取
- 方法一:selenium webdriver.get(url)
- 方法二:requests
- 方法三:urllib
- 方法四:手动
- 解析
- 什么是xpath
- 什么是正则表达式
- 什么是selector
- 方法一:selenium find_elements()
- 方法二:lxml.etree
- 方法三:BeautifulSoup
- 方法四:HTMLParser
- 方法五:re(正则表达式)
- 方法N:手搓
- 附:环境搭建
- Windows: cmd/Powershell
- Linux/Mac: bash/zsh
简介
爬虫程序主要内容就两部分。
1,html获取器
2,html解析器
入门足够了。管理url、伪装用户行为、运行javascript等属于高级操作,不算入门,俺也没学过。
最后附上环境搭建部分。
以下各小节均以从ICML2021会议官网上爬取paper列表为例。
获取
获取网页信息有很多种方法,在此我记录一下我遇到过的几种,并分析它们的用途特点。
方法一:selenium webdriver.get(url)
【推荐】
from selenium import webdriver
url="https://icml.cc/Conferences/2021/Schedule?type=Poster"
driver = webdriver.Firefox()
driver.get(url)
# 到这里已经算是获取到了网页信息,因为selenium库中的各种API支持对网页进行各种查询、操作
# 如果想进一步获得全部html代码
from selenium.webdriver.common.by import By
element = driver.find_element(By.XPATH,'/html')
html = element.get_attribute('outerHTML')
selenium的安装配置较为麻烦,它需要单独下载各个浏览器对应的driver(Chrome、Firefox、IE等很多浏览器都支持),还要配置系统环境变量。
但它的效果也是最好的。有些网页会动态生成内容,这时就只能伪装成用户,使用浏览器运行javascripts。当然js一般是自动运行的。不使用浏览器或js运行环境的方法,一般只能获得网页初始的静态内容。
方法二:requests
import requests #导入requests包
url = "https://icml.cc/Conferences/2021/Schedule?type=Poster"
html = requests.get(url) #Get方式获取网页数据
print(html.text)
以上是GET方法,POST方法我暂时还不懂。
方法三:urllib
from urllib import request
url = "https://icml.cc/Conferences/2021/Schedule?type=Poster"
html = request.urlopen(url).read()
html.decode()
方法四:手动
打开浏览器,输入目标url,Ctrl+S保存页面到.html,再在程序中读取文件。
with open('xxx.html','r') as f:
html = f.read()
print(html)
解析
解析最重要的任务在于,找到想找的东西,不错找,不漏找。
什么是xpath
【推荐】
教程:w3school xpath
xpath大概长这样:/html/body/div[@class='container']
xpath甚至有内建函数,功能十分齐全。
注意,xpath中的索引从1开始。
什么是正则表达式
大概长这样:\d{1,3}(.\d{1,3}){3},这是表达IPv4地址的正则表达式(不过只限制了数字位数,没有限制三位数≤255)。
什么是selector
类似于xpath的一种html路径表达法,但感觉没有xpath支持性广。例如Firefox就复制不了selector。
方法一:selenium find_elements()
from selenium import webdriver
url="https://icml.cc/Conferences/2021/Schedule?type=Poster"
driver = webdriver.Firefox()
driver.get(url)
# 以上和html获取小节内容相同
from selenium.webdriver.common.by import By
xpath = "/html/body/div[@class='container pull-left']/div[@id='wholepage']/main[@id='main']/div[@class='container']/div[@class='row']/div[@class='col-xs-12 col-sm-9']/div[position()>3]/div"
driver.find_elements(By.XPATH,xpath)
# 这会返回一个列表,列表中的元素全部都是WebElement(selenium定义的变量类型)。
如使用find_elements则返回所有符合的元素,如使用find_element则返回第一个命中的元素。
但这种方法有一定问题。
1,浏览器放置时间久一点,selenium的API就会失去连接,而selenium的API只能对活动中的浏览器进行操控。
2,selenium没有提供针对html代码的解析程序。
方法二:lxml.etree
【推荐】
from lxml import etree
# html是html格式的字符串
Selector = etree.HTML(html)
xpath = "/html/body/div[@class='container pull-left']/div[@id='wholepage']/main[@id='main']/div[@class='container']/div[@class='row']/div[@class='col-xs-12 col-sm-9']/div[position()>3]/div"
Selector.xpath(xpath)
具体的用法我还没有仔细研究,但大体来说lxml是支持xpath的。
lxml本身也是BeautifulSoup的依赖库之一,其功能可以弥补selenium的缺点。
方法三:BeautifulSoup
from bs4 import BeautifulSoup
# 假设html为html格式的字符串
soup = BeautifulSoup(strhtml.text,'lxml') # 需要预先安装lxml
data = soup.select('#main>div>div.mtop.firstMod.clearfix>div.centerBox>ul.newsList>li>a') # 似乎不是xpath
BeautifulSoup和XPath的原理不一样,BeautifulSoup是基于DOM的,会载入整个文档,解析整个DOM树,因此时间和内存开销都会大很多。而lxml只会局部遍历。
就我个人的应用需求来说,BeautifulSoup不如selenium+lxml好用。
方法四:HTMLParser
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):
"""
recognize start tag, like <div>
:param tag:
:param attrs:
:return:
"""
print("Encountered a start tag:", tag)
def handle_endtag(self, tag):
"""
recognize end tag, like </div>
:param tag:
:return:
"""
print("Encountered an end tag :", tag)
def handle_data(self, data):
"""
recognize data, html content string
:param data:
:return:
"""
print("Encountered some data :", data)
def handle_startendtag(self, tag, attrs):
"""
recognize tag that without endtag, like <img />
:param tag:
:param attrs:
:return:
"""
print("Encountered startendtag :", tag)
def handle_comment(self,data):
"""
:param data:
:return:
"""
print("Encountered comment :", data)
parser = MyHTMLParser()
parser.feed('<html><head><title>Test</title></head>'
'<body><h1>Parse me!</h1><img src = "" />'
'<!-- comment --></body></html>')
(以上这段代码摘自Python爬虫常用之HtmlParser)
HTMLParser是python自带的功能,但设计起来非常麻烦,需要继承HTMLParser类,进行新类的定义,并且五种方法重载也十分不人性化。
然而网上查到的方法很多都是这种,难道它们都是用爬虫在互相爬取然后复制么。
方法五:re(正则表达式)
import re
# html是html格式的字符串
pattern = r'<a href=[^>]*>(.*)</a>'
result = re.findall(pattern, html)
不多解释,把
方法N:手搓
之前走过一些弯路,当时只知道BeautifulSoup,不知道selenium和xpath,就照自己的想法创了一种语法,很像xpath。虽然现在看来没用了,但还是放出来供大家学习讨论。
from bs4 import BeautifulSoup
def ruleParse(rule):
# 将字符串形式的rule解析成[{'name':'', 'attr': {} }, ... ]格式的内容
# examples:
# rule = "body.div(container pull-left).div(wholepage).main#main.div(container).div(row).div$2.div$3:-1"
# rule = "body.div{class:[container, pull-left]}.div{class:wholepage}.main{id:main}.div(container).div(row).div{range:2}.div{range:(3,-1)}"
# rule = "body.div$3.div$0.main.div$0.div$0.div$2.div$3:-1"
rule_ = []
i=0
mode = 'word'
string = ""
splitters = "@#$(){}[]:."
quoters = '\'"'
quoter='"'
spaces = " \t\n\r\b"
while i<len(rule):
char = rule[i]
i+=1
if mode=='word':
if char in splitters:
string = string.strip()
if string!="":
rule_.append([string,'word'])
string = ""
rule_.append([char,'splitter'])
elif char in quoters:
string = string.strip()
if string!="":
rule_.append([string,'word'])
string = ""
quoter = char
mode='str'
elif char in spaces:
string = string.strip()
if string!="":
rule_.append([string,'word'])
string = ""
elif char=="\\" and i!=len(rule):
char = rule[i]
i+=1
string+=char
else:
string+=char
else: # 'str' (quoted)
if rule[i] == quoter:
rule_.append([string,"str"])
string=""
mode='word'
else:
string+=char
if mode!='word':
raise "Syntax Error: unquoted string."
else:
string = string.strip()
if string!="":
rule_.append([string,'word'])
string = ""
rule__ = []
stack=[]
mode='name' # name class id range attr
name=None
class_=None
id_=None
range_=None
attr={}
def record():
nonlocal name,class_,id_,range_,attr,stack,rule__
if name==None:
raise ValueError("Empty name")
if class_!=None:
attr['class']=class_
if id_!=None:
attr['id']=id_
if range_!=None:
if len(range_)==0:
raise SyntaxError("Empty range")
i=0
while i<len(range_):
item=range_[i]
if item==':':
if i==0 or range_[i-1]==':':
range_.insert(i,None)
i+=1
i+=1
if range_[-1]==':':
range_.append(None)
range__=[]
for item in range_:
if item==':':
continue
elif item==None:
range__.append(None)
else:
range__.append(int(item))
attr['$range']=range__
rule__.append({
'name':name,
'attr':attr
})
name=None
class_=None
id_=None
range_=None
attr={}
for content,typ in rule_:
if mode=='name':
if typ=='word':
if name==None:
name=content
else:
raise ValueError("Duplicated name")
else: # typ == 'splitter'
if content=='.':
record()
elif content=='(':
mode='class'
stack=[]
elif content=='#':
mode='id'
elif content=='$':
mode='range'
elif content=='{':
mode='attr'
stack=[]
else:
raise ValueError(f'Invalid splitter "{content}"')
elif mode=='class':
if typ=='splitter':
if content==')':
mode='name'
class_=stack.copy()
stack=[]
else:
raise ValueError(f'Invalid splitter "{content}"')
else: #typ=='word'
stack.append(content)
elif mode=='id':
if typ=='splitter':
raise SyntaxError(f'Unexpected splitter "{content}"')
else: #typ=='word'
if id_!=None:
raise SyntaxError('Duplicated ID')
else:
id_=content
mode='name'
elif mode=='range':
if typ=='word':
stack.append(content)
else: # typ == 'splitter'
if content=='.':
range_=stack.copy()
stack=[]
record()
mode='name'
elif content in ',:':
stack.append(':')
elif mode=='attr':
if typ=='splitter':
if content=="{":
pass
elif content==':':
pass
else:
raise ValueError(f'Invalid splitter "{content}"')
else:
raise ValueError(f'Invalid mode "{mode}"')
if mode=='range':
range_=stack.copy()
stack=[]
if mode in ['range','name']:
record()
else:
raise SyntaxError("Unquoted content")
return rule__
def getElements(source,rule: str):
# 将受支持source转换成[BeutifulSouop]
if type(source)==str:
soups=[BeautifulSoup(source,features="lxml")]
elif isinstance(source, Iterable):
soups=list(source)
elif type(source)==BeautifulSoup:
soups=[source]
else:
raise TypeError("Invalid type for source.")
parsedRule = ruleParse(rule)
unalignedResultCnt=0
for i in range(len(parsedRule)):
item=parsedRule[i]
name=item['name']
attr=item['attr']
def lambda_(tag):
nonlocal name, attr
if not tag.name==name:
return False
for key,value in attr.items():
if key=='$range':
continue
if not tag.has_attr(key):
return False
if key=='class':
for class_ in value:
if not class_ in tag[key]:
return False
else:
if not tag[key]==value:
return False
return True
results=[]
resultCnt=0
resultLen=None
for soup in soups:
result=soup.find_all(lambda_)
resultCnt+=len(result)
if resultLen==None:
resultLen=len(result)
else:
if resultLen!=len(result):
unalignedResultCnt+=1
results.append(result)
if resultCnt==0:
return []
if '$range' in attr.keys():
range_ = attr['$range']
if len(range_)==1:
index=range_[0]
for i in range(len(results)):
results[i]=[results[i][index]]
elif len(range_)==2:
start, end=range_
for i in range(len(results)):
results[i]=results[i][start:end]
elif len(range_)==3:
start,end,skip=range_
for i in range(len(results)):
results[i]=results[i][start:end:skip]
else:
raise ValueError(f'Invalid range {range_}')
else:
pass
soups=[]
for result in results:
soups+=result
if unalignedResultCnt!=0:
print(f"Warning: unaligned result number ({unalignedResultCnt})")
return soups
# html是html格式的字符串
rule="body.div(container pull-left).div(wholepage).main#main.div(container).div(row).div$2.div$3:-1"
soups=getElements(html,rule)
效果还可以。但我刚写完,就在网上看到了xpath
更多文章请关注《万象专栏》
转载请注明出处:https://www.wanxiangsucai.com/read/cv59596