首页 > 编程语言 > 详细

Python网络爬虫(三)

时间:2021-04-27 19:48:05      阅读:41      评论:0      收藏:0      [点我收藏+]

网络爬虫之实战

正则表达式(Re)库入门

正则表达式的概念

正则表达式(Regular Expression):用来简洁表达一组字符串的方式。
正则表达式的优点:简洁!正则表达式表达的是一组字符串的特征。

‘P‘
‘PY‘
‘PYYY‘
...
#正则表达式为:PY+,表示P后面跟着若干个Y

#若一个字符串以‘PY‘开头而总共不超过10个字符串且后续不能再出现‘P‘或者‘Y‘,那么:
#正则表达式为:PY[^PY]{0,10}

我们可以认为正则表达式是通用的表达框架,它是一种针对字符串表达的”简洁“和“特征”的功能。正则表达式用于判断某个字符串是否属于一组字符串。
正则表达式的用途:

  1. 表达文本的特征(例如病毒或入侵等)
  2. 同时查找或替换一组字符串
  3. 匹配字符串的全部或部分
    正则表达式的使用:
  4. 编译:将符合正则表达式语法的字符串转换为正则表达式的特征。
regx = ‘PY[^PY]{0,10}‘	#编译前的正则表达式只是一个字符串
p = re.compile(regx)	#编译后才成为了一个真正的正则表达式

正则表达式的语法

正则表达式是由字符和操作符构成的。
常用的操作符:

操作符 说明 实例
. 表示任意单个字符
[] 字符集,对单个字符给出取值范围 a[b,c]表示ab或ac,[a-z]表示从a到z的单个字符
[^] 非字符集,对单个字符给出排除范围 [^abc]表示除了a,b和c以外的单个字符
* 表示前一个字符出现0次或若干次 abc*表示ab, abc, abcc, abccc等等
+ 表示前一个字符出现1次或若干次 abc+表示abc, abcc, abccc等等
? 表示前一个字符出现0次或1次 a[b]?表示a, ab等等
| 左右表达式任意一个 abc|def 表示abc和def任选一个
{m} 表示扩展前一个字符m次 ab{2}c表示abbc
{m, n} 表示扩展前一个字符m到n次 ab{1, 2}c表示abc,abbc
^ 没加“[]”表示匹配字符串的开头 ^abc表示abc在一个字符串的开头
$ 表示匹配字符串的末尾 abc$表示abc在一个字符串的末尾
() 分组标记,内部只能使用 | 符号 (abc|def)表示abc或def
\d 表示数字,等价于[0-9]
\w 表示字母或数字或下划线等价于[a-zA-Z0-9_]

以下给出若干实例:

>>>P(Y|YT|YTH|YTHO)?N 
"PN", "PYN", "PYTN", "PYTHN", "PYTHON"
>>>PN+
"PN", "PNN", "PNNN", "PNNNN"...
>>>PY[TH]ON
"PYTON", "PYHON"
>>>PY[^TH]?ON
"PYON", "PYAON", "PYBON"...
>>>PY{:3}N
"PN", "PYN", "PYYN", "PYYYN"

#一些经典的正则表达式:
>>>^[A-Za-z]+$	#表示26个字母组成的字符串
>>>^[A-za-z0-9]+$	#表示以英文字符或者数字组成的字符串的正则表达式
>>>^-?\d$	#表示整数形式(含负数)的正则表达式
>>>^[0-9]*[1-9][0-9]*$	#表示正整数形式的正则表达式
>>>[1-9]\d{5}	#表示国内邮政编码的正则表达式
>>>[\u4e00-\u9fa5]	#表示中文字符的正则表达式
>>>\d{3}-\d{8}|\d{4}-\d{7}	#表示的是国内固定电话的正则表达式
>>>([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5].){3}([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5].)	#表示的是IP地址的正则表达式

Re库的基本使用

主要用于字符串匹配。

正则表达式的表示类型

raw string类型(原生字符串类型):
原生字符串类型是指不包括转义符的字符串。只需要在字符串前面加一个r即可,例如:r"text"。
同样也可以使用字符串类型来表达,但是必须在“\”时需要增加额外的“\”.
因此,当正则表达式中含有转义符的时候,应该使用raw string来表达。

常用的re库功能函数

函数 功能
re.search() 在一个字符串中搜索匹配正则表达式的第一个位置,并返回一个match对象
re.match() 从一个字符串的开始位置起匹配正则表达式,返回一个match对象
re.findall() 搜索字符串,以列表类型返回全部能匹配的子串
re.split() 将一个字符串按照正则表达式的结果进行分割,返回列表类型
re.findall() 搜索字符串返回一个匹配结果的迭代类型,每个元素是一个match对象
re.sub 在一个字符串中,替换所有匹配的正则表达式子串,返回替换后的字符串

re.search(pattern, string, flags=0)

在一个字符串中匹配第一个正则表达式的位置,返回一个match对象。
pattern:正则表达式字符串或原生字符串表示。
string:待匹配字符串
flags:正则表达式的控制标记。主要有以下三个。

常用标记 说明
re.I (re.IGNORANCECASE) 忽略正则表达式的大小写区分,如[A-Z]也能表示小写的[a-z]
re.M (re.MULTILINE) 作用于^,能使得正则表达式对字符串每一行都进行匹配
re.S (re.DOTALL) 正则表达式中的 . 默认匹配除了换行符以外的所有字符
使用这一标记符后它也可以匹配换行符了
import re
match = re.research(r‘[1-9]\d{5}‘, ‘BIT 100081‘)
if match:
	print(match.group(0))

re.match(pattern, string, flags=0)

从一个字符串的开始位置起匹配正则表达式,返回一个match对象。
它的参数同re.research是一样的。

re.match(r‘[1-9]\d{5}‘, ‘BIT 100081‘)
match.group(0)
>>>Error	#这里没有匹配的结果,匹配时必须加入if match:
re.match(r‘[1-9]\d{5}‘, ‘100081 BIT‘)
	if match:
		match.group(0)
>>>‘100081‘

re.findall(pattern, string, flags=0)

搜索字符串,返回一个全部匹配结果的列表类型。
参数同前两个。

ls = re.findall(r‘[1-9]\d{5}‘, ‘BIT100081 TSU100084‘)
print(ls)
>>>[‘100081‘, ‘100084‘]

re.split(pattern, string, maxsplit=0, flags=0)

表示把目标字符串按照指定的模式分割出来。
参数中maxsplit表示最大分割数,若超过,则把剩下的所有的内容作为列表的最后一个参数返回。

re.split(r‘[1-9]\d{5}‘, ‘BIT10081 TSU10084‘)
>>>[‘BIT‘, ‘ TSU‘, ‘‘]
re.split(r‘[1-9]\d{5}‘, ‘BIT10081 TSU10084‘, naxsplit=1)
>>>[‘BIT‘, ‘ TSU10084‘]

re.finditer(pattern, string, flags=0)

搜索目标字符串返回一个迭代类型,迭代类型的每一个元素是match对象。

for m in re.finditer(r‘[1-9]\d{5}‘, ‘BIT100081 TSU100084‘):
	if m:
		print(m.group(0))
>>>10081
   10084

re.sub(pattern, rpel, string, count=0, flags=0)

搜索目标字符串,将匹配的部分替换为另一个字符串,返回匹配后的字符串。
参数中,count表示最大替换次数,rpel表示替换的字符串。

>>> import re
>>> re.sub(r‘[1-9]\d{5}‘, "zipcode:******","BIT100081 TSU100084")
‘BITzipcode:****** TSUzipcode:******‘
>>> re.sub(r‘[1-9]\d{5}‘, "zipcode:******","BIT100081 TSU100084",count=1)
‘BITzipcode:****** TSU100084‘

re库的两种使用方法

  1. 函数式用法:将re库作为一个函数来使用。
re.search(r‘[1-9]\d{5}‘, ‘BIT100081 TSU100084‘)
  1. 面向对象式用法:将正则表达式作为一个对象,编译后多次使用。
pat = re.compile(r‘[1-9]\d{5}‘)
pat.research("BIT100084")

这样多次调用只需一次编译,可以加快运行速度。
同样它有六种方法,同re库的使用是一样的,只是无需正则表达式中pattern的参数。

正则表达式中的match对象

  1. match对象的属性
属性 说明
match.string 匹配的目标字符串
match.re 匹配的模板
match.pos 匹配在目标字符串的起始位置
match.endpos 匹配在目标字符串的结束位置
  1. match对象的方法
方法 说明
match.group(0) 匹配的结果,有多个时只返回第一个
match.start() 匹配结果在目标字符串的开始位置
match.end() 匹配结果在目标字符串的结束位置
match.span() 返回一个元组类型,(.start(), .end())
>>>import re
>>> m = re.search(r‘[1-9]\d{5}‘, "BIT100081 TSU100084")
>>> m.string
‘BIT100081 TSU100084‘
>>> m.re
re.compile(‘[1-9]\\d{5}‘)
>>> m.pos
0
>>> m.endpos
19
>>> m.group(0)	#只显示第一个搜寻结果,如果要显示多个,应该使用finditer()遍历
‘100081‘
>>> m.start()
3
>>> m.end()
9
>>> m.span()
(3, 9)

re库的贪婪匹配和最小匹配

re库默认采用贪婪匹配:即,在一个给定字符串中,返回最长的哪个匹配的子串。
如果希望返回的是最小匹配,那么要对原本的操作符进行一定的扩展。

操作符 说明
*? 前一个字符0次或无限次循环,取最小匹配
+? 前一个字符1次或无限次循环,取最小匹配
?? 前一个字符出现0或1次,取最小匹配(0次)
{m,n}? 前一个字符出现m到n(含n)次,取最小匹配
>>>m = re.search(r‘PY.*N‘, "PYANBNCNDN")
>>>m.group(0)
"PYANBNCNDN"

>>>m = re.search(r‘PY.*?N‘, "PYANBNCNDN")
>>>m.group(0)
"PYAN"

实例2:淘宝商品比价定向爬虫

import requests
import re
cookie = #自己登陆后header里的cookie,涉及隐私信息不宜公开
cookie = cookie.encode("utf-8") 	
#有时会报编码错误,因为淘宝的cookie里有各种稀奇古怪的字符,包括中文,默认的latin-1有时无法解码
hd = {
    "user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36",
    "cookie":cookie
    }
def getHTMLText(url):
    try:
        r = requests.get(url, headers=hd)
        r.raise_for_status()        
        r.encodiding = r.apparent_encoding
        return r.text
    except:
        print("爬取错误!")
        return ""

def parsePage(itl, html):
    try:
        plt = re.findall(r‘\"view_price\"\:\"[\d\.]*\"‘, html)
        tlt = re.findall(r‘\"raw_title\":\".*?\"‘, html)
        llt = re.findall(r‘\"item_loc\":\".*?\"‘, html)
        slt = re.findall(r‘\"view_sales\":\".*?\"‘, html)
        nlt = re.findall(r‘\"nick\":\".*?\"‘, html)
        for i in range(len(plt)):
            p = eval(plt[i].split(":")[1])
            t = eval(tlt[i].split(":")[1])
            l = eval(llt[i].split(":")[1])
            s = eval(slt[i].split(":")[1])
            n = eval(nlt[i].split(":")[1])
            itl.append([t, p, l, s, n])
    except:
        print("")
    
def printGoodsList(itl):
    module = "{0:{5}^30}\t\t{1:^10}\t\t{2:^10}\t\t{3:^10}\t\t{4:^10}"
    print(module.format("商品名称", "商品价格", "店铺所在地", "商品销量", "店铺名称", chr(12288)))
    for i in itl:
        print(module.format(i[0], i[1], i[2], i[3], i[4], chr(12288)))

def main():
    goods = "书包"
    depth = 2
    start_url = "https://s.taobao.com/search?q=" + goods
    infolist = []
    for i in range(depth):
        try:
            url = start_url + "&s=" + str(44*i)
            html= requests.get(url, headers=hd)
            print(html.text)
            parsePage(infolist, html.text)
        except:
            continue
    printGoodsList(infolist)

main()            

这个例子是个实用性很强的例子,我想在日常网购中还是能发挥很大作用的。
要点还是在于对正则表达式的理解和掌握,这里很显然就必须用到一个最小匹配,一旦默认使用 了贪婪匹配整个爬取就会完全错误了!
如果有一定的web开发基础,我想会更容易使用和理解这样的爬虫。另外,如果能把结果输出为一个CSV文件,用Excel进一步处理那么效果应该会很不错。

实例3:股票数据定向爬虫

输入:获取所有上交所、深交所股票交易信息
输出:保存到文件中
技术路线:requests——bs4——re

选取网页的原则:应该选取那些将股票数据写在HTML页面中,不能由js代码生成的。同时,最好没有robots协议限制
选取方法:浏览器F12查看
选取心态:不必纠结于一个单一的网站。

import requests
import re
from bs4 import BeautifulSoup
import traceback

def getHTMLText(url):
    try:
        r = requests.get(url)
        r.raise_for_status()
        r.encoding = r.apparent_encoding
        return r.text
    except:
        print("爬取错误")
        return ""

def getFundList(lst, furl):
    html = getHTMLText(furl)
    soup = BeautifulSoup(html, "html.parser")
    a = soup.find_all("tr")
    for i in a:
        try:
            id = i.attrs["id"]
            lst.append(re.findall(r"[tr]\d{6}", id)[0])
        except:
            continue

def getFundInfo(lst, fiurl, fpath):
    for fund in lst:
        url = fiurl + fund[1:] + ".html"
        html = getHTMLText(url)
        try:
            if html == "":
                continue
            infoDict = {}
            soup = BeautifulSoup(html, ‘html.parser‘)
            fundInfo = soup.find(‘div‘, attrs={‘class‘: "merchandiseDetail"})
            name = fundInfo.find_all(attrs={‘class‘: "fundDetail-tit"})[0]
            infoDict.update({‘基金名称‘: name.text.split()[0]})  #这里对所有文本分割取第一个就是基金名字
            keyList = fundInfo.find_all("dt")
            valList = fundInfo.find_all("dd")
            for i in range(len(keyList)):
                key = keyList[i].text
                val = valList[i].text
                infoDict[key] = val

            with open(fpath, ‘a‘,encoding="utf-8")as f:
                f.write(str(infoDict)+‘\n‘)
        except:
            traceback.print_exc()    #获取错误信息
            continue

def main():
    fund_url = "https://fund.eastmoney.com/fund.html#os_0;isall_0;ft_;pt_1"
    fundinfo_url = "https://fund.eastmoney.com/"
    fpath = "D://fundinfo.txt"
    flist = []
    getFundList(flist, fund_url)
    getFundInfo(flist, fundinfo_url, fpath)

main()       

网址都做了变更,现在要找一个静态网页确实不太容易了。
因此我感觉目前学的部分的爬虫还是由很大局限性的,数据量不上去就没办法更好地进行数据的分析吧。仅仅是简单的爬取的话,那么与直接在相关网页中搜索没有多大的区别。


以上内容来自北京理工大学嵩天老师及其团队的《Python网络爬虫与信息提取》课程第三周的内容。
非常感谢嵩天老师及其团队给我们带来这样优质的课程。

Python网络爬虫(三)

原文:https://www.cnblogs.com/modered-study/p/14709309.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!