上次写了一篇《爬取 CT 云影像的 DICOM 原始文件》,本以为是个小众需求,毕竟相关教程很少,但没想到很快就收到了些回复,其中就有人问我怎么爬另一个网站。
粗略一看该网站界面比上次的好些,至少布局是对齐了,它的域名是 medicalimagecloud.com,能搜到叫海纳医信。
不管怎样,这种小网站一般都没什么反爬措施,弄起来都挺简单的——我一开始也是这么想,但后来发现这回的网站还要真费点功夫。
本文记录下爬取该网站的思路,具体的代码见 cloud-dicom-downloader。
第一次进去就立刻点开 F12,在网络一栏里找可能是影像文件的请求,这个网站倒是没整 WebSocket 之类的东西,很容易找出关键请求:
响应体保存到文件,打开看了下不是 DICOM 的文件:
但是一眼瞅到个ftypjp2
,说明下载的是 JPEG 2000 格式的图片。再回去把网页上的按钮都摸一遍,发现菜单里能选格式:
看到熟悉的 Dicom 字样,我以为这次的爬虫就已经搞定了,只要写个脚本批量下载即可,于是便安心地睡去。直到第二天阅片软件打不开时我才发现下载的并不是 DICOM 文件。
这个下载下来的文件内容比较稀疏,大部分是 0x00,隔一段有一些其它的值,很明显没有压缩,看上去页不像是加密后的数据,同时它无法解码为字符串,大概率还是二进制的格式。另外文件的大小也引起了我的注意,都是 524288 字节,这个数正好等于 512 x 512 x 2,而片子的尺寸也是 512 像素的正方形,有没有可能它是图片的像素数据呢?
为了验证,直接读取该文件作为像素数据画成灰度图,结果正好就是 CT 影像,只不过暗了些,但这可能跟查看器有关,但它一定是图片内容没得跑了。
我们知道 DICOM 文件是由主体的图像加上附加信息(标签)组成的,从响应头中也能找到一个X-ImageFrame
包含了额外的信息,这说明该网站把 DICOM 文件给拆开了,分别发送标签和图像,于是一种可行的方案就是下载完它们之后重新合成。
做逆向工程,包括爬虫,一定要对数据有敏感性。文件大小、头尾的内容、分布特征都是重要的线索,通过它们有时能快速判断对方的设计方案,这也是很需要积累的地方。
当然如果能直接下载原始的文件是最好不过的,所以我还是又去网页里找了找,在源码里发现一个似乎是下载的 API:
可惜参数storageNode
和请求体studyDirectory
无法确定,试了下不填它们结果返回 500。在/ImageViewer/GetPatientStudies
请求的响应中还发现storageNode
都是null
,我觉得这条路是走不通了。
另外 DICOM 标准包括了一套通信机制,但那是给医院内部用的,比如 CT 机扫完使用C-Store
命令上传结果,然后让科室里的电脑能查询到。这套系统是否包含认证功能,以及在外部能否访问也都不知道,我也没发现该网站有这样的 API。
所以最终还是决定使用爬取后重组的方案。
那就开爬,首先是过认证。模拟登录是爬虫的基础,不多讲,这个网站的跳转还挺多:
首先入口是一个https://<xxx>.medicalimagecloud.com:<port?>/t/<32-chars-hex>
格式的 URL,外加一个密码。由于不是我的报告,我也不知道它们是怎么来的。直接访问该页面,收到一个跳转和一个 Cookie。
发送 POST 到跳转的登录页,请求体为id=<UUID>&Password=<密码>
,application/x-www-form-urlencoded
格式。UUID 就是页面 URL 最后的部分,收到的响应为报告页面。
从报告页中搜索查看影像
链接元素,其href
即为查看器页面的链接。
请求查看器链接,返回一个跳转页并设置了一些 Cookies,在该页面中可以找到个window.location.href= 'xxxxx'
代码,看来是用 JS 来做自动跳转的。
继续跟过去,又 TM 是一个跳转页,还是同样的配方,在页面代码里找到var TARGET_PATH = "xxxxx"
,xxxxx
即为最终的查看器页面。
最后在查看器页面的源码里能够找到一些关键信息,都在一个<script>
块里,其中STUDY_ID
、ACCESSION_NUMBER
、STUDY_EXAM_UID
、LOAD_IMAGE_CACHE_KEY
后面要用到。
回到 F12 控制台的网络部分,把所有 JSON 响应都看一遍就能知道:
/ImageViewer/GetImageSet
能够获取 DICOM 的基本信息,包含所有序列和图像的 ID,请求参数都在刚才的页面上拿到了。
/ImageViewer/GetImageDicomTags
可以拿到 DICOM 的标签,这个请求是在图像上右键 -> 点Dicom 信息
后法送。
/imageservice/api/image/<格式>/<序列 ID>/<图像 ID>/0/<清晰度>
用于下载图像,请求必须携带ck=
参数,值就是上面的LOAD_IMAGE_CACHE_KEY
。
/ImageViewer/renewcacauth
每分钟要请求一次,刷新 Cookie,太久不刷新的话会 403。
还有一个/ImageViewer/renewuseronline
看它不停发送,但不知道有啥用就不管了。
下载的话就是先拿基本信息,然后遍历所有序列(在它的响应里是displaySets
),每个序列再挨个下载所有图像和标签,最后合成。
DICOM 是一个历史悠久的格式,各个语言都有库可以用,生成 DCM 文件不是什么问题,库的用法见源码吧。
但这个网站有一个问题,就是获取不到标签的类型。
上面提到的/ImageViewer/GetImageDicomTags
拿到的数据只有tag
、name
和value
三个字段,缺乏VR
,对于标准中已有定义的标签倒是好说,但还有一些私有的标签,比如 CT 机的电压等等,我这只能把它们视为字符串了。这个问题应该影响不大,因为非标准标签本来用到的就少。
最后还有个担心的问题图像会不会给有损压缩了,毕竟界面上只有高低质量的切换功能,没有“无损”或是“原图”等词语。这个可以从 F12 工具里网络请求的 Stack Trace 面板跳转到发请求的代码:
可以看出 URL Path 的最后一个部分是p
或i
,而p
由i
计算得来,i
在上面的setResolutionLevel
中作为唯一参数,故它就是 Resolution Level,然后在另一处代码能找到这一行字:
此处有“lossless”这个词,说明还下载到的图像无论 J2K 还是像素格式,都是无损的原片。