Scrapy入门
最近因为工作上有数据采集相关内容,特地研究了一下Scrapy相关使用。总结了一下Scrapy的一些入门和实践经验。
考虑到想在武汉上车,因此就去采集了贝壳找房上的一些数据。具体的字段可以存到数据库或者文件中。这里是我定义的一些字段。
最终采集完数据之后,一般会对数据进行一些分析。如果会sql的直接写语句处理就好了,更友好的方式是用可视化展示出来,这样更直观。因此这里采用了pandas来处理数据,用pyecharts展示数据。这里是一个简单的效果图。
Scrapy安装
对于使用过Python的同学而言,这些内容可以忽略。
pip install scrapy
使用scrapy创建工程,这个脚本很方便,可以一键生成模板。
scrapy genspider [你的工程名称]
最终的工程结构是这样的:
Scrapy使用
生成的文件都有自己的作用。items.py
中定义数据字段。
import scrapy
class BeikeWuhanItem(scrapy.Item):
name = scrapy.Field()
lp_type = scrapy.Field()
image = scrapy.Field()
block = scrapy.Field()
address = scrapy.Field()
room_type = scrapy.Field()
spec = scrapy.Field()
ava_price = scrapy.Field()
total_range = scrapy.Field()
tags = scrapy.Field()
detail_url = scrapy.Field()
create_time = scrapy.Field()
可以发现这里的字段和在数据库中定义的一模一样。
settings.py
中定义一些设置属性。具体如何使用可以点击注释中的链接详细研究,scrapy的文档写的很全,对新手很友好。
# 下载间隔 10秒一次 太频繁了网站会给你封掉 要么用代理池
DOWNLOAD_DELAY = 10
# 后面的数字代表顺序
DOWNLOADER_MIDDLEWARES = {
'BeikeSpider.middlewares.IPProxyMiddleware': 100,
'BeikeSpider.middlewares.BeikespiderDownloaderMiddleware': 543,
}
pipelines.py
中定义对采集到的数据的处理。
class BeikespiderPipeline:
def __init__(self):
self.connection = pymysql.connect(
host=MYSQL_HOST,
user=MYSQL_USER,
password=MYSQL_PASS,
db=MYSQL_DB,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
)
def process_item(self, item, spider):
if isinstance(item, BeikeWuhanItem):
self.insert(item)
return item
def insert(self, item):
cursor = self.connection.cursor()
keys = item.keys()
values = tuple(item.values())
fields = ",".join(keys)
temp = ",".join(["%s"] * len(keys))
sql = "INSERT INTO wh_loupan (%s) VALUES (%s)" % (fields, temp)
cursor.execute(sql, values)
self.connection.commit()
这里的逻辑是将采集到的数据写入mysql,当然也可以写文件到excel也是没问题的。需要注意的是,这个pipeline写完了一定要在settings中配置一下。
最后就是自己的爬取逻辑了,beike_wuhan.py
这个文件不会被自动生成出来,需要自己定义。这里是爬虫的入口。
start_requests
方法是父类中的定义,这个方法中定义爬虫的名称,以及从哪个url出发。最后通过yield关键字将这个请求交给Scrapy的下载器处理,其中还有一个callback函数,表示处理完请求后要做什么操作。
parse
函数也是父类的,通过参数response可以看出在这里做返回数据的解析。
在解析页面数据的时候使用xpath是比较快捷的。不过对于眼神不好的还是考虑别的选择器,因为层级多了容易看花。至于如何使用xpath这里不细说,不过浏览器中打开调试器能直接选择元素复制其xpath,非常好用。提取出元素信息将其值填充到BeikeWuhanItem
中,这个过程还是比较繁琐的,需要点耐心。item构建完依然通过yield出去。这里的item就传递到了pipline中了,可以直接被process_item
函数处理。
因为爬取的不仅仅只有这一页数据,还要继续接着爬。这里涉及到了翻页动作。贝壳页面中虽然有下一页按钮,但是这个按钮的渲染是通过JS动态生成的,直接通过请求地址是不会返回这个按钮的元素的。这点值得注意一下,最简答的办法是查看网页源代码,如果能找到这个元素说明通过xpath可以取到,否则怎么都取不到。因此这里不能去通过判断是不是有下一页按钮去做翻页,而是得通过具体的总数去判断到底要不要继续爬。观察到如果到了最后一页,页面元素上的总条数就变成了0,这时候就不用继续翻页了。翻页的url也是通过yield传递给下载器,类似for循环,一直去下载解析。 这里也发现了贝壳的bug,显示有80页,其实到了40页就没数据了。导致我开始计算总页数的时候以为总页数就那么多,实际上的bug在有的页显示10条,有的页显示20条,不讲武德。
p_room_type = re.compile('[0-9]+')
p_room_spec = re.compile('[\s\S]*㎡$')
root_url = 'https://wh.fang.ke.com/'
default_schema = 'https:'
class BeikeWuhanSpider(Spider):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.curr_page = 1
self.url_template = "https://wh.fang.ke.com/loupan/nhs1pg%s"
name = "beike_wuhan"
allowed_domains = ["wh.fang.ke.com"]
def start_requests(self):
# 武汉楼盘的首页
baseurl = "https://wh.fang.ke.com/loupan/nhs1pg1"
UserAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36 Edg/92.0.902.62',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36']
UserAgent = random.choice(UserAgents)
headers = {'User-Agent': UserAgent}
yield Request(
url=baseurl,
headers=headers,
callback=self.parse)
def parse(self, response):
# next_page = response.xpath('/html/body/div[7]/div[2]/')
# if next_page is not None:
# next_page = response.urljoin(next_page)
# yield Request(next_page, callback=self.parse)
# 每页的数量还不是固定的 有的是10个每页 有的是20个每页
content = response.xpath('/html/body/div[6]/ul[2]/li')
for item in content:
image = item.xpath('./a/img/@src')[0].extract()
name = item.xpath('./div[1]/div[1]/a/@title')[0].extract()
detail_url = item.xpath('./div[1]/div[1]/a/@href')[0].extract()
lp_type = item.xpath('./div[1]/div[1]/span[2]/text()').extract_first()
address = item.xpath('./div[1]/a[1]/text()')[1].extract()
block = ''
if address:
block = address.split('/')[0]
room_type_texts = item.xpath('./div[1]/a[2]/span/text()').extract()
room_types = []
room_spec = ''
for room_type_text in room_type_texts:
if re.match(p_room_type, room_type_text):
room_types.append(room_type_text)
if re.match(p_room_spec, room_type_text):
room_spec = room_type_text.split(' ')[1]
room_type = '/'.join(room_types)
t = item.xpath('./div[1]/div[3]/span/text()').getall()
tags = ','.join(t)
ava_price = item.xpath('./div[1]/div[4]/div[1]/span[1]/text()').extract_first()
total = item.xpath('./div[1]/div[4]/div[2]/text()').extract_first()
if not total:
total = ''
wuhan_item = BeikeWuhanItem()
wuhan_item['name'] = name
wuhan_item['lp_type'] = lp_type
wuhan_item['image'] = default_schema + image
wuhan_item['block'] = block
wuhan_item['address'] = address
wuhan_item['room_type'] = room_type
wuhan_item['spec'] = room_spec
wuhan_item['ava_price'] = ava_price
wuhan_item['total_range'] = total
wuhan_item['tags'] = tags
wuhan_item['detail_url'] = root_url + detail_url
wuhan_item['create_time'] = datetime.datetime.now()
yield wuhan_item
# 楼盘总数 如果为0了 那么说明没有下一页了
count = response.xpath('/html/body/div[6]/div[2]/span[2]/text()').get()
print('===================================================================', count)
if int(count) > 0:
self.curr_page += 1
else:
self.crawler.engine.close_spider(self, '任务完成')
next_page = self.url_template % self.curr_page
yield Request(next_page, callback=self.parse)
贝壳对频繁请求做了限制,因此最好使用代理池,否则很容易被禁IP,得苦哈哈去点击“我不是人机”的识别码才能解除限制。 到这里,基本上整个采集的环节都描述完了。接下来就是对数据进行处理和展示。
pandas数据处理
pandas也能读取数据库中的记录,几行代码就搞定。
import pandas as pd
from sqlalchemy import create_engine
import matplotlib.pyplot as plt
from pyecharts.charts import Bar
plt.rcParams['font.sans-serif'] = 'SimHei'
engine = create_engine('mysql+pymysql://root:123456@localhost:3306/beike_spider')
sql = ''' select * from wh_loupan; '''
# 将查到的数据 load到pd
df = pd.read_sql_query(sql, engine)
接着对数据进行过滤,因为我们采集的楼盘有很多类型,如写字楼,底商等等。目前我只关心住宅,因此通过这个操作实现过滤。
# 过滤掉其他的类型 只拿住宅做分析 同时只保留了4个列
houses = df.loc[df["lp_type"] == "住宅", ['id', 'name', 'ava_price', 'block']]
不过话说回来,干嘛不用sql直接写过滤条件呢?当然可以,但这里是为了展示一下pandas的使用。
接着对住宅的区域进行分组,看看这些区的住宅楼盘的分布情况。
# 通过区来做分组 这里返回的是一个series类型 索引为区,值为区下楼盘的数量 因此需要将对应的值取出来
group_num = houses.groupby("block").size()
index = []
val = []
for i in group_num.items():
index.append(i[0])
val.append(i[1])
bar = Bar()
bar.add_xaxis(index)
bar.add_yaxis("楼盘数", val)
# render 会生成本地 HTML 文件,默认会在当前目录生成 render.html 文件
# 也可以传入路径参数,如 bar.render("mycharts.html")
bar.render("./figures/楼盘分布.html")
最后生成图表,直观的呈现出来。
完整代码请参考 github.com/Mr-Vincent/…
总结
在很早以前都是使用request库直接请求网页,需要处理连接,解析,重试之类的业务无关的脏活累活。代码写的又多又丑,目前使用了scrapy后发现瞬间清爽很多,效率提升了不少。 在习惯了Java这种强类型的语言后,再去写Python这种弱类型的有时候会很抓狂,比如做除法运算,虽然你知道你的除数是数字,但是解释器告诉你这是字符类型,不给你除,还得手动转一下类型,太气人了。还有在使用类方法的时候,根本不知道怎么去传参,各种花里胡哨的参数列表,对于强类型语言使用者来说,编程思维转变过来还是需要点时间。
更多不精彩内容,关注公众号也不一定获取到