最近,实验室的项目需要用到医药卫生知识服务系统的药品数据,查看发现该网站上的数据不少,手动采集数据比较麻烦,考虑使用网络爬虫,交由我来进行分析和爬取。
在我爬取药品数据的时候,发现该系统的数据是通过 AJAX 动态加载的,直接无脑复制 URL 使用 request.get 爬取的简单方式是行不通的。于是我爬取完之后写下这篇博客作为一个记录,也方便后来者进行学习。
医药卫生知识服务系统的网址是
我们点击右上方的 -> ,进入药品目录页
可以看到这些药品数据都是我们需要的,网站进行了分页展示,每一页展示十条
点击第一条药品 ,就会在新的标签页中可以看到药品的详细数据,这是药品的详情页。
总的任务就是把所有药品数据都爬取下来,每一个药品数据都用单独一个文件进行保存。
请大家在这一步记住目录页和详情页是怎么找到的,之后的步骤中会直接提及这两个概念。
每一个药品都用了单独的标签页进行展示,那么每一个药品都会对应一个链接来获取数据,因此我们找到这些 URL 的规律就可以爬取了。
我们点开第一个药品 和第二个药品 的详情页,在浏览器的地址栏中观察两个药品的URL:
我们发现,URL仅在 id 部分发生了变化,其余完全相同。这意味着 id 即为每个药品数据的标识,我们只需要找到每个药品的 id ,对 URL 进行拼接就可以对应获取到单个药品数据了。
因此,我们只需要拿到所有药品的 id 信息,然后遍历每一条 id ,就能依次拿到所有药品的数据了。那么药品的 id 信息又从哪拿呢,我们可以通过药品目录页进行爬取。
接下来是具体的找 id 的步骤,这里我使用的是谷歌浏览器。注意,找数据是编写爬虫最关键的部分,这非常重要。
- 第一步,用谷歌浏览器进入药品目录页
- 第二步,按 键,打开浏览器的控制台,转到 部分。(可以看到此时 Network 部分什么数据都没有)
- 第三步,键盘按 F5 进行页面刷新,部分会加载该页面的全部数据
- 第四步,在控制台左侧的 窗口搜索第一条药品的名字
可以看到,搜出了两个对应链接,我们分别点击链接,右侧的 会具体展示其数据。
通过 response 的对比发现,queryDetail.do 是单个药品的数据信息,而 searchList.do 是这一页十条药品的数据汇集信息。很明显,我们需要的是所有药品的 id,所以我们应当去分析 searchList.do 。
- 第五步,点击 searchList.do,在右侧中查看其
可以看到,该链接使用的是 get 请求方法,URL中包含了两个重要参数: 和 。参数的暗示就很明显了,这是第一页,这一页包含了十条数据。
- 第六步,我们新开一个标签页,把这个 URL 复制进去,可以正常看到十条药品的信息。如果把 改为,就可以看到第二页的药品信息。
当然,这样乱糟糟的页面是十分难看的,于是我装了一个浏览器插件:JSONVue,可以对 JSON 数据进行有效排列,展示会更加直观好看。(要装这个插件的自行百度就可以)。
这一步是为了熟悉对应数据的 JSON 结构,方便之后的编码工作进行数据的提取。如果使用熟练的话则可以跳过此步。
我们已经找到了药品 id 对应的链接,并且查看了对应数据的 JSON 格式,下面对药品 ID 进行爬取。
简单分为以下几个函数进行编写:
- 资源获取函数
- 数据提取函数
- 资源保存函数
- 主函数
1.资源获取
这里使用 request.get()方法进行资源获取,有两个注意事项:
- 准备多个用户代理 UA 来随机选取
- 每爬取一次页面沉睡一些时间
这两点的作用都是模拟用户的操作,更好地避免爬取过快导致爬虫被识别。同时,使用 try-except 结构也能更好地捕捉异常情况。
Tips:如果报异常的话,可以考虑 time.sleep() 多沉睡一点时间,例如 5 秒
getTotal()函数有一个参数,就是需要资源获取的 URL
getTotal() 函数结束后会返回一个 json 文件,这个就是十条药品数据信息汇总的 json 文件。这里我给出第一条做个大概的示意
2.数据提取
数据提取时这里有一个需要注意的点,不是所有的数据都是药品数据。例如在药品目录页的第三页,第22条是鼻出血、第23条是病毒性肺炎,它们都不是我们所要的药品信息,对于这些非药品可以直接忽略。
于是,我去仔细对比了药品与非药品的 JSON 数据。通过对比发现,药品的数据中会有 这个属性,而非药品是没有的。因此,我选择使用 属性来鉴别药品。
我们使用 findTotal()函数来进行数据处理,该函数有两个参数,一个是待处理的 json 文件,一个是用于保存药品 id 信息的列表
3.资源保存
所有的药品 id 信息提取完成之后,我们将其保存到文件中。为了方便查找,这里我将其保存在代码的同级目录下
saveTotal()函数进行文件的保存,该函数有一个参数,就是保存药品 id 信息的列表
4.主函数
目录页有三种方式,分别展示十条、二十条、三十条。
主函数中通过循环控制 、 两个参数即可生成 URL。这里我选择每次取三十条
5.总体代码
总体代码如下:
程序运行大概需要几分钟的时间,爬取完成的程序输出:
保存到的 药品ID.csv 文件如下图所示:
有了药品 id 之后,我们就可以爬取每个药品具体的数据了。
还是以第一个药品 为例,我们去寻找它的数据,操作方法与本博客的第一节 完全一模一样。
具体流程:在 的详情页,点击 F12,找到 ,刷新加载数据后,搜索关键字 “氨己烯酸”。依次对比后发现 的 response 才与详情页的数据相匹配。
查看 的 部分,发现其使用 的是 POST 请求方法,URL 链接是 https://med.ckcest.cn/queryDetails.do
于是我们查看 部分,该部分写明了该链接要传递的参数是 id 和 nameEn
我们直接将参数拼接成一个新的 URL :,在浏览器中开一个新窗口输入这个URL。可以看到,我们已经成功找到了该药品详情页的数据
药品的 ID 我们刚才已经爬取过了,可以再次编写爬虫爬取每个药品的详情数据了。
这里大概分为五个函数:
- 加载资源ID
- 获取数据
- 数据提取
- 保存信息
- 主函数
1.加载资源ID
药品ID.csv 是上一个爬虫程序爬取的关于所有药品 id 信息的文件,就放在同级目录下。
readerID()函数有一个参数,传入一个空列表,将 id 读取之后存入列表中
2.获取数据
写法与之前几乎没有区别,只是改成了 post 方法,需要定义 data 来传递参数。
getJson()函数有一个参数,接受传入的药品 id ,作为 post 的参数。
3.数据提取
数据提取这部分比较麻烦,因为它存在两个问题。
第一是每个药品数据属性不统一的问题。每条数据从 “keys” 属性大概分为头部和尾部两部分。在数据的头部,每一个药品从 titleZh 到 speciesZh 一共有固定的 14 个中文属性;但是在数据的尾部,有具体值的属性大概在5——10个不等。例如一共有 8 个有具体值的属性,而 却有 10 个属性有值。
以下是的详细数据:
针对这种情况,我的做法是,依次从数据头部取出 14 个属性的英文名,然后按英文名对比是否有值。因为 JSON 数据是键值对的形式,很好地支持了这种对比。
例如,titleZh 是“名称” 属性,我把最后两个字母 Zh 去掉,剩下 title,查找 title 键是否有值。如果该属性比对有值,则记录;如果结果是 None,则证明该属性没有具体值,直接跳过。
第二个问题是某些属性值存在 这样的 html 标签,以及 换行符。
关于 标签,我考虑使用正则表达式进行去除,将该标签全部替换为空值。
至于 换行符,使用自带的 replace 函数将其替换为空值即可。不过这里有个小细节,这种连在一起的换行符,必须要连写在一起才能生效
最后我使用了两个列表,第一个列表存入中文属性名,第二个列表存入对应属性值,最后把两个列表都统一存入一个新列表中,进行数据传递。
findJson()函数有两个参数,第一个参数是待处理的 Json 文件,第二个参数是传递数据的统一列表。
4.保存信息
写法与之前基本一样,文件名由具体的药品名称来命名,保存在同级目录下
这里我选择的是第一列写入属性名,第二列写入属性值。(当然也可以按照自己的需求来进行更改)
saveMedicine()函数有一个参数,即 findJson 函数处理好的数据列表
效果大概是这个样子
5.主函数
主函数的写法与之前也基本一样。因为每个药品要生成一个文件,所以把保存数据写在了循环里面
6.总体代码
总体代码如下:
因为每一页都要进行文件写入,所以程序运行时间会比较长,大概需要十分钟。爬取完成的程序输出:
最终写入的文件如下:
要写好一个爬虫进行数据的爬取,还是挺劳心费神。
必须得对网页的数据加载方式有一个清晰的认知,对于JSON数据的传递链接有合理的分析。抓取到数据之后,要想方法筛选提炼数据,还要按自己所需要的格式进行清洗。
我在提取药品详细数据时,所使用的键值对的对比方法,也不见得是很好的,它只是我个人想到的一个可以完成当前任务的方法。大家也可以再想想别的办法来处理数据,很可能就比我这个更好。