目标
多线程数据抓取-58同城转转网的二手产品
实作
1. 建立一个项目
新建一个项目58tongcheng1
2. 观察页面特征
不同页面的不同规则问题
分页问题
3. 设计工作流程
首先,在主列表页爬取所有商品的URL,存储在mongodb
中,在数据库中建立对应的URL_list
(爬虫1)
然后,详情页具体产品的信息,存储在数据库item_info
中 (爬虫2)
爬虫2应把 爬虫1抓取的并已存在数据库中的URL取出来,依次读取详情页,获得所要信息,再把这些信息存储在item_info
这个表单中
4. 创建channel_extract.py
获取每个分类产品的链接
from bs4 import BeautifulSoup
import requests
start_url = 'http://bj.58.com/sale.shtml'
url_host = 'http://bj.58.com'
def get_channel_urls(url):
wb_data = requests.get(start_url)
soup = BeautifulSoup(wb_data.text, 'lxml')
links = soup.select('ul.ym-submnu > li > b > a') # 寻找该标签时比较麻烦,因为它是hover显示
print(links)
for link in links:
page_url = url_host + str(link.get('href'))
print(page_url)
get_channel_urls(start_url) # 通过它打印出所有的URL
把所有的URL集中起来建立一个新的长字符串
channel_list = '''
http://bj.58.com/shouji/
http://bj.58.com/tongxunyw/
http://bj.58.com/danche/
http://bj.58.com/diandongche/
http://bj.58.com/fzixingche/
http://bj.58.com/sanlunche/
http://bj.58.com/peijianzhuangbei/
5. 创建page_parsing.py
获取产品详情
from bs4 import BeautifulSoup
import requests
import time
import pymongo
client = pymongo.MongoClient('localhost', 27017)
chengxu = client['chengxu']
url_list = chengxu['url_list3']
item_info = url_list['item_info3']
# spider 1 爬取首页中显示的类目中,一个类目下的所有商品的链接
def get_links_from(channel,pages,who_sells=0): # who_sells = 0表示个人,1表示商家
#http://bj.58.com/shouji/1/pn2/
list_view = '{}{}/pn{}/'.format(channel,str(who_sells),str(pages)) # 找网页规律的时候,刚刷新和点击后的相同页面的网址会有变化,但页面相同,它们是等价的,所以找页面规律时要多点击或刷新来找
wb_data = requests.get(list_view)
time.sleep(1)
soup = BeautifulSoup(wb_data.text, 'lxml')
if soup.find('td','t'): # 一个类目的页码是有限的,通过寻找td.t来判断系统是否爬过头了
for link in soup.select('td.t a.t'): # 这里的td.t a.t 是点击某个分类后的新网页的每个具体商品的链接的selector
#for link in soup.select( ('td.t a.t') if not soup.find_all('zhiding', 'huishou') else None ): #修改失败,计划排除被抓取的几排广告
# 注意!!!上面代码后面,若是('td.t >a.t')即无法显示结果,必须空格!这样才对('td.t > a.t')
item_link = link.get('href').split('?')[0] # 这里的0是对切片后的字符串形成的列表list进行筛选,选第一段,即0(for in 就是对列表的)
url_list.insert_one({'url': item_link }) # insert是数据库函数,注意区分
print(item_link)
else:
pass
#get_links_from('http://bj.58.com/shuma/', 2)
# spider 2 爬详情页的数据
def get_item_info(url):
wb_data = requests.get(url)
soup = BeautifulSoup(wb_data.text, 'lxml')
no_longer_exist = '商品已下架' in soup.find('div', "button_li").get_text() # 从下方 AAA 处移过来的代码,理解时先忽略它。
# find()里面的代码实际是完整的div="button_li",而且要保证该段代码在正常网页和已下架网页中都存在,否则正常网页报错。
if no_longer_exist:
pass
else:
title = soup.title.text
price = soup.select('span.price_now i')[0].text
# 后面必须加[0].text,因为数据库要是str才能存进去,soup.select返回的对象是list,就算list里面只有一个元素,也不能用.text方法,所以才选择用[0],把元素从list调出来,再进行.text方法
area = soup.select('.palce_li i')[0].text if soup.find_all('i') else None
item_info.insert_one({'title':title, 'price':price, 'area':area })
print({'title': title, 'price': price, 'area':area})
#get_item_info('http://zhuanzhuan.58.com/detail/919823388320399372z.shtml')
#======= AAA 爬取的商品链接中有失效的,剔除它(商品已交易则该网址会失效),测试完该段代码备注掉==========#
# url = 'http://zhuanzhuan.58.com/detail/922439089107222541z.shtml' # 网址上的商品已下架
# wb_data = requests.get(url)
# soup = BeautifulSoup(wb_data.text, 'lxml')
#print(soup.prettify())
# 上面的步骤查询了失效网址的结构。
#no_longer_exist = '商品已下架' in soup.find('span', "soldout_btn").get_text() # 搬到上方get_item_info
#print (no_longer_exist) # 查看no_longer_exist是True False。上面的find里代码必须是完整的<xxx>内容,形成一个list,否则系统报错属性错误或者无法迭代
注意事项均备注在代码中。。。
6. 多进程数据抓取
建立主程序 main.py
from multiprocessing import Pool #
from channel_extract import channel_list
from page_parsing import get_links_from
def get_all_links_from(channel):
for num in range(1,51):
get_links_from(channel,num)
if __name__=='__main__': # 一种类似作文开头的感谢领导的套话格式,防止上下程序串混乱了,没特别的意思
pool = Pool() # 创建进程池
pool.map(get_all_links_from, channel_list.split())
# map函数的特点是把括号内的后一个参数放到前一个参数(函数)里去依次执行。约定俗成map第一个参数为不带 () 的函数。
# channel_list 是引用过来的,我们之前定义过它是一个长字符串,将它分成一段段,split()函数会将一个字符串自动变成分割好的一个大list
监控程序 counts.py
import time
from page_parsing import url_list # url_list 是数据库的第一张表的名称
while True:
print(url_list.find().count())
# find()展示url_list中所有的元素,count()计数,这两种函数是数据库函数,不能用于字典和列表
time.sleep(5)
# 该段程序用来监控用,当它和主程序一起开的时候,它可以计算数量进程,方便管理
7. 运行
打开终端,开启3个窗口,切换到程序文件夹中,第一个窗口输入mongod
,输入mongo
,好了,mongo已开启
第二个窗口输入 python3 counts.py
第三个窗口输入python3 main.py
好了,开始抓取数据了,成功
8. 断点续传
from page_parsing import get_links_from, get_item_info, url_list, item_info # 该条为更改的,下面代码全部是新建的
# 断点续传
db_urls = [ item['url'] for item in url_list.find() ] # 用列表解析式装入所要爬取的链接
index_urls = [ item['url'] for item in item_info.find() ] # 所引出详情信息数据库中所有的现存的 url 字段
x = set(db_urls) # 转换成集合的数据结构
y = set(index_urls)
rest_of_urls = x - y
设计思路:
- 分两个数据库,第一个用于只用于存放抓取下来的
url
(ulr_list)
;第二个则储存 url 对应的物品详情信息(item_info)
- 在抓取过程中在第二个数据库中写入数据的同时,新增一个字段(key)
'index_url'
即该详情对应的链接 - 若抓取中断,在第二个存放详情页信息的数据库中的 url 字段应该是第一个数据库中 url 集合的子集
- 两个集合的 url 相减得出圣贤应该抓取的 url 还有哪些
备注
(1) find()
的参数依次为(标签名,标签属性),返回一个标签(可多重嵌套)或None;
(2)find_all()
的参数依次为(标签名,标签属性),返回一个标签列表或者空列表
(3) python的find()
是字符串对象的方法,用于查找子字符串,返回第一个字串出现的位置或-1(字串不存在);mongodb的find()
是列表对象的方法,接收字典参数,键值对为所要查找条目键值对,用于查找条目,返回True
(4) mongodb
的查询方法find()
与find_one()
find()
方法成功找到符合条件的记录则返回一个生成器(实质是停留在符合条件记录的集合的第一条记录位置的cursor),用list方法转化为列表后,如果该存在符合条件的记录,则生成一个列表,否则生成一个空列表。
find_one
({查询键值对},{显示字段:0表示不显示or1表示显示,其余默认不显示,'_id'默认显示})返回查询到的第一条。