导言
在过去,南伊利诺伊大学爱德华斯维尔(SIUE)的Lovejoy图书馆(SIUE)的期刊和杂志包订阅的更新是零敲碎打的,每一项决定都单独引用与购买有关的指标,而与其他指标无关。考虑的两个主要因素是价格比上一年上涨百分比和每次使用成本。如果价格上涨的百分比可以保持在4%-5%以下,并且每篇期刊文章的每次使用成本保持在诸如Illiad这样的送货服务的订购率以下,那么日刊或日刊包装就会被更新。在这一过程中,更新价格和统计分析由电子资源图书馆员提供给个别学科图书馆员,他们就续借问题作出最后决定。在此系统中,对期刊订阅进行跨学科分析只是为了确保资源的支付不超过一次。没有更多的颗粒分析,尤其是基于期刊主题内容的分析。我们的项目起源于2019年春天,目的是为我们在SIUE Lovejoy图书馆订阅的期刊生成主题标题数据。该项目的预期成果是详细分析和教育图书馆联络员和校园教员了解该期刊在其学科中的订阅情况,包括使用数据、每次使用的成本以及潜在的可供选择的开放教育资源(OER)或开放存取(OA)资源或成本较低的可比产品替代品。主要作者ShellyMcDavid曾在她以前的图书馆中看到过类似的模式,并希望将其应用于LIS对SIUE校园学科的藏书开发和年度组合评审。
背景
在SIUE的LIS中,我们并没有确定我们所订阅的电子资源的主题标题,也没有进行任何类型的学科特定期刊使用分析。这些都是这个项目的目标,着眼于这样的分析,作为年度项目评审的基础,提交给校园教职员工和管理人员。2020年秋季,Lis Lovejoy图书馆藏书分析与战略委员会成立,致力于实现这些目标。
主题方法
本项目的主要问题是在不同的图书馆目录中以有效、系统的方式收集一组ISSN编号的特定元数据。解决方案是创建一个Python(v3.7.1)脚本,它向目录搜索引擎发出一个HTTPS请求,以获取ISSN编号,并解析HTML搜索结果以收集该ISSN的相关元数据。该脚本设计为从命令提示符中使用,并允许通过传入CSV文件进行单个ISSN搜索或批量搜索。所需的位置参数之一是目录名称:WorldCat、Carli_I-Share或Mobius。
目录参数
脚本的一个特性是允许灵活地选择要对其进行搜索的目录,因为一次只能搜索一个目录。由于每个库Web目录都以不同的方式以不同的HTML结构和CSS样式格式化,因此创建了Python字典数据结构,因此无论选择哪个目录,脚本都可以以系统的方式操作。为每个目录定义了此字典,并包含以下参数:
-
-
- Base ear-目录搜索URL的开头部分,从https://开始,在第一个/例如“www.worldcat.org”之前结束。
- 搜索url-这是目录搜索URL的其余部分,没有base_url,这是用一个“{0}”参数化的,ISSN用于搜索例如,“/search?QT=WorldCat_org_all&q={0}”。
- 搜索标题-用于包含搜索结果页上ISSN标题的父HTML元素的CSS选择器。
- 记录-HTML元素的CSS选择器,该元素包含目录项页上的记录元数据。
- 标题-用于父HTML锚元素的CSS选择器,该元素包含目录项页上的项标题。
- 科目-HTML表元素,其中的文本以“Topics”或“Subject”开头,在BIB_Record的目录项的页面中。
-
编写的脚本支持WorldCat、Mobius Library Catalog和Carli的VUFind Library Catalog,但也可以通过为特定库目录定义上述参数来搜索更多的目录。这些参数确实需要对HTML元素和CSS样式的工作和功能有更广泛的了解。
加工过程
无论脚本是以单或批处理模式运行,搜索ISSN的过程都是相同的,涉及单个ISSN的两个URL请求:
- 搜索ISSN的目录并获取目录项的URL。
- 如果目录项URL有效,则在目录项的页面中搜索相关的元数据。
- 将元数据和搜索条件输出或写入磁盘。
该脚本并不适用于每个ISSN,并且将为上述步骤中涉及的任何失败提供信息性错误。
将URL请求发送到Web服务器时的一个复杂问题是,请求之间必须存在时间延迟,否则服务器将假设它正在被垃圾处理,并拒绝请求。因此,每个URL请求的前一个时间延迟为半秒。
输出量
如果脚本在单ISSN模式下运行,输出将发送到屏幕。此输出包括搜索的ISSN、目录项URL、项标题和项目记录中的主题列表。
在批处理ISSN模式下,在当前工作目录中创建一个CSV输出文件,将所选目录名称附加到文件名中,即“Batch_Output_worldcat.csv”。此输出文件将包含上述输出,但对于输入文件中列出的所有ISSN。一些ISSNs在搜索时会产生错误,对于这些错误,URL、标题或主题可能是空白的。在批处理模式下,脚本还将显示一个进度条,以通知用户完成大约的时间。
外部图书馆
脚本使用以下外部Python库:
-
-
- Urllib.request.urlopen--这是URL请求的生成方式。
- Bs4.BeautifulSoup--这是一个HTML解析器,它对来自URL请求的结果进行操作。
- Tqdm.tqdm--这是在批处理模式下运行时的进度条。
-
源代码-Subtscper.py
from urllib.request import urlopen
from bs4 import BeautifulSoup
from tqdm import tqdm
import re, argparse, sys, csv, time
catalogs = {
#'catalog name' : {
# 'base_url' : beginning part of URL from 'http://' to before first '/',
# 'search_url' : URL for online catalog search without base URL including '/';
# make sure that '{0}' is in the proper place for the query of ISSN,
# 'search_title' : CSS selector for parent element of anchor
# containing the journal title on search results in HTML,
# 'bib_record' : CSS selector for record metadata on catalog item's HTML page,
# 'bib_title' : CSS selector for parent element of anchor containing the journal title,
# 'bib_subjects' : HTML selector for specific table element where text begins with
# "Topics", "Subject" in catalog item's HTML page in context of bib_record
'worldcat' : {
'base_url' : "https://www.worldcat.org",
'search_url' : "/search?qt=worldcat_org_all&q={0}",
'search_title' : ".result.details .name",
'bib_record' : "div#bibdata",
'bib_title' : "div#bibdata h1.title",
'bib_subjects' : "th"
},
'carli_i-share' : {
'base_url' : "https://vufind.carli.illinois.edu",
'search_url' : "/all/vf-sie/Search/Home?lookfor={0}&type=isn&start_over=0&submit=Find&search=new",
'search_title' : ".result .resultitem",
'bib_record' : ".record table.citation",
'bib_title' : ".record h1",
'bib_subjects' : "th"
},
'mobius' : {
'base_url' : 'https://searchmobius.org',
'search_url' : "/iii/encore/search/C__S{0}%20__Orightresult__U?lang=eng&suite=cobalt",
'search_title' : ".dpBibTitle .title",
'bib_record' : "table#bibInfoDetails",
'bib_title' : "div#bibTitle",
'bib_subjects' : "td"
}
}
# Obtain the right parameters for specific catalog systems
# Input: catalog name: 'worldcat', 'carli i-share', 'mobius'
# Output: dictionary of catalog parameters
def get_catalog_params(catalog_key):
try:
return catalogs[catalog_key]
except:
print('Error - unknown catalog %s' % catalog_key)
# Search catalog for item by ISSN
# Input: ISSN, catalog parameters
# Output: full URL for catalog item
def search_catalog (issn, p = catalogs['carli_i-share']):
title_url = None
# catalog url for searching by issn
url = p['base_url'] + p['search_url'].format(issn)
u = urlopen (url)
try:
html = u.read().decode('utf-8')
finally:
u.close()
try:
soup = BeautifulSoup (html, features="html.parser")
title = soup.select(p['search_title'])[0]
title_url = title.find("a")['href']
except:
print('Error - unable to search catalog by ISSN')
return title_url
return p['base_url'] + title_url
# Scrape catalog item URL for metadata
# Input: full URL, catalog parameters
# Output: dictionary of catalog item metadata,
# including title and subjects
def scrape_catalog_item(url, p = catalogs['carli_i-share']):
result = {'title':None, 'subjects':None}
u = urlopen (url)
try:
html = u.read().decode('utf-8')
finally:
u.close()
try:
soup = BeautifulSoup (html, features="html.parser")
# title
try:
title = soup.select_one(p['bib_title']).contents[0].strip()
# save title to result dictionary
result["title"] = title
except:
print('Error - unable to scrape title from url')
# subjects
try:
record = soup.select_one(p['bib_record'])
subject = record.find_all(p['bib_subjects'], string=re.compile("(Subjects*|Topics*)"))[0]
subject_header_row = subject.parent
subject_anchors = subject_header_row.find_all("a")
subjects = []
for anchor in subject_anchors:
subjects.append(anchor.string.strip())
# save subjects to result dictionary
result["subjects"] = subjects
except:
print('Error - unable to scrape subjects from url')
except:
print('Error - unable to scrape url')
return result
# Search for catalog item and process metadata from item's HTML page
# Input: ISSN, catalog paramters
# Output: dictionary of values: issn, catalog url, title, subjects
def get_issn_data(issn, p = catalogs['carli_i-share']):
results = {'issn':issn, 'url':None, 'title':None, 'subjects':None}
time.sleep(time_delay)
url = search_catalog(issn, params)
results['url'] = url
if url: # only parse metadata for valid URL
time.sleep(time_delay)
item_data = scrape_catalog_item(url, params)
results['title'] = item_data['title']
if item_data['subjects'] is not None:
results['subjects'] = ','.join(item_data['subjects']).replace(', -', ' - ')
return results
# main loop to parse all journals
time_delay = 0.5 # time delay in seconds to prevent Denial of Service (DoS)
try:
# setup arguments for command line
args = sys.argv[1:]
parser = argparse.ArgumentParser(description='Scrape out metadata from online catalogs for an ISSN')
parser.add_argument('catalog', type=str, choices=('worldcat', 'carli_i-share', 'mobius'), help='Catalog name')
parser.add_argument('-b', '--batch', nargs=1, metavar=('Input CSV'), help='Run in batch mode - processing multiple ISSNs')
parser.add_argument('-s', '--single', nargs=1, metavar=('ISSN'), help='Run for single ISSN')
args = parser.parse_args()
params = get_catalog_params(args.catalog) # catalog parameters
# single ISSN
if args.single is not None:
issn = args.single[0]
r = get_issn_data(issn, params)
print('ISSN: {0}\r\nURL: {1}\r\nTitle: {2}\r\nSubjects: {3}'.format(r['issn'], r['url'], r['title'], r['subjects']))
# multiple ISSNs
elif args.batch is not None:
input_filename = args.batch[0]
output_filename = 'batch_output_{0}.csv'.format(args.catalog) # put name of catalog at end of output file
with open(input_filename, mode='r') as csv_input, open(output_filename, mode='w', newline='', encoding='utf-8') as csv_output:
read_in = csv.reader(csv_input, delimiter=',')
write_out = csv.writer(csv_output, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
write_out.writerow(['ISSN', 'URL', 'Title', 'Subjects']) # write out headers to output file
total_rows = sum(1 for row in read_in) # read all rows to get total
csv_input.seek(0) # move back to beginning of file
read_in = csv.reader(csv_input, delimiter=',') # reload csv reader object
for row in tqdm(read_in, total=total_rows): # tqdm is progress bar
# each row is an ISSN
issn = row[0]
r = get_issn_data(issn, params)
write_out.writerow([r['issn'], r['url'], r['title'], r['subjects']])
except Exception as e:
print(e)
局限性
虽然该项目有许多限制,但一个明显的限制是,主题标题通常由人生成和分配,这等同于人为错误的可能性。我们希望通过生成和比较三个来源的数据来规范我们的主题标题数据。然而,VuFind目录已经退役,需要我们更新新的Carli i共享目录的主题刮板脚本,这是我们目前还没有做的事情。
影响从图书馆目录中抓取主题标题的另一个因素是,许多服务器,比如OCLC的WorldCat服务器,只能在24小时内从相同的IP地址点击999次。这是服务器的常见做法。因此,当需要擦拭几千个主题标题时,这项工作必须经过一系列天的分析。
未来工作
在我们完成这个项目时,SIUE的LIS还没有将这个项目的目标应用到实际行动中的结构。然而,在2020年夏天,一位新院长接管了LIS的领导权,并于秋季成立了集合分析和战略委员会。该委员会的一个既定年度目标是创建“一种数据驱动的方法来收集开发,分析年度使用数据、每次使用数据的成本,以及在适用的情况下,日记账影响因素数据。”此外,“图书馆提出了其对潜在削减的客观看法,并与系系教员就与课程和研究议程有关的部门需要进行了对话。”
但是,我们今后在分析日志中的主题标题方面的工作将不依赖于本文中描述的定制开发的PythonWeb刮板代码。此后,OCLC开发了一个名为FAST API的实验性API(ApplicationProgramming Interface)。“FAST是主题标题数据主题术语分面应用的缩略语”(FastAPI[更新2021])。[]现在,我们将利用这个API生成主题标题,以帮助我们按主题收集开发的主题进行期刊使用分析,并为校园部门创建年度项目组合评审。
一个可能的未来工作的一个具体领域是分析我们的大型期刊包从主要出版商。这一抱负的目标是受NABE和Fowler在“离开‘’大人物‘…’中的想法的启发。五年后“(NABE和Fowler 2015)。[]了解这些大型数据库的主题组成将是评估我们是否可以用更有针对性的选择取代这些昂贵的订阅的关键工具。这肯定有助于实现使日记账支出的投资回报率最大化和建立部门投资组合审查的目标。
结论
这个项目在两个主要方面都很有价值,这两种方式都为SIUE的LIS提供了条件,使其能够从我们的期刊和数据库订阅中获得最大的收益。第一,除了每次使用的成本之外,按主题标题分析期刊收藏的想法本身就预见到了收集分析和战略委员会所从事的工作。这将允许对我们的更新进行更细致的分析,并且是为大学系创建基于学科的组合评审的一个基本概念。其次,研究三个图书馆目录的体系结构,编写Python脚本,测试和纠正错误,有助于了解学术期刊主题标题元数据的质量和范围。当我们现在使用和评估来自OCLC的API在完成与Python脚本相同的任务时,这些知识将是非常宝贵的。然而,我们希望本文中描述的脚本和过程可以为其他人提供一条低成本的途径来分析他们自己的期刊收藏,即使只是作为一个实验,或者作为走向更健壮的期刊使用和主题分析的第一步。
还是不难吧