О сортировке

enthusiastic emoticon

Многие из вас этого не знают, но у нас в стране принято, что диссертанта мучают всяческими придирками. Одна из этих придирок — это форматирование списка литературы.

Мало того, что он должен быть оформлен по ГОСТ, или по крайней мере, настолько похоже, чтобы диссертационный совет не заметил разницы1 – в моем случае, кстати, не заметил, хотя про то, насколько у нас путаный ГОСТ, я тогда не знал, и разница была значительна. Но список еще должен быть поделен на секции.

При этом, литературе на русском языке отдается почетное место — она не только складывается в отдельную кучку, но и подразделяется, как минимум, на монографии и научные статьи. Пожелания к подразделению могут всячески варьироваться. Литература же на иностранных языках небрежно сваливается в одну кучу без разбору.

Я не знаю где еще именно так испокон веку положено, но мне чуется, что только и исключительно у нас. В моем случае это выглядело особенно смешно, поскольку монографий на русском в моем списке раз-два и обчелся, статей тоже кот наплакал, прорва газетных статей, которым положена отдельная кучка, и аналогичная прорва буржуйских источников, которых не больше чем русскоязычных только и исключительно за счет того, что последние разбавлены газетными, которых без мелочи двести штук.

BibTeX, который со значительными трудностями2 удалось обучить ГОСТу лишь сравнительно недавно, лет пять тому назад, про все это не знает. Собственно, он еще много про что не знает, например про то, что в гуманитарных дисциплинах ставить ссылки на литературу номерами в квадратных скобках не принято, а принято их целиком в сноску совать, но так не только у нас, так что с этим я справился сравнительно быстро.

А вот с порубанием списка литературы на секции было очень тяжело, потому что делить его на секции именно так — принято только у нас. Все макропакеты, которые позволяют устраивать такие секции, не рассчитаны на то, что вам потребуется еще и ссылки в сноски затолкать, и безнадежно конфликтуют с тем, что это умеет делать.

Ко всему прочему, BibTeX еще и не знает что такое utf-8, отчего безбожно корежит текст когда пытается произвести сокращение инициалов, а заодно еще и путается с сортировкой. Самое обидное, что уже лет пять никто не берется его переписать — всех отпугивает этот его ублюдочный скриптовый язык, с которым надо сохранить совместимость. Единственные кому без этого никак вообще — японцы — написали jBibTeX, который не удалось даже раздобыть, чего уж там прочитать к нему инструкцию… но даже если бы удалось, наверное оказалось, бы что он знает только Shift-JIS. Я дня два ломал голову как все-таки запинать его, чтобы он не выпендривался и сортировал все как положено, деля на кучки. Получилось наконец.

Уже произведенный на свет бибтехом .bbl-файл, содержащий уже отформатированные и готовые к втыканию на место ссылки засасывается специальным скриптом. Скрипт ничего не знает про латех, кроме того, что библиография начинается с заголовка, кончается на концовку, и состоит из параграфов, в которых в первой строчке где-то есть уникальный ключ цитаты. Он также ничего не знает про бибтех — он только знает что его файлы также состоят из параграфов, в которых некоторые строчки имя автора, а некоторые — название, а начинаются они на строчку в которой есть ключ. А некоторые содержат строчку, которая указывает, в какую кучку литературы этот ключик должен попасть.

После чего скрипт тупо засасывает и .bbl и .bib, на основании последнего раскладывает литературу по кучкам, и сортирует ключики по именам авторов и названиям до посинения.

А потом записывает .bbl обратно в отсортированном порядке со вставленными на стратегические места псевдозаголовками кучек.

И этот скрипт будет моим подарком брату на день рожденья, потому что ему тоже защищаться…

#!/usr/bin/python
# -*- coding: utf-8 -*-

import os, getopt, sys, re, codecs

# Секции идут именно в этом порядке...
SECTIONORDER = ['norm','mono','arti','sprv','news','hudl','site','other']

# И имеют заголовки именно с таким текстом...
SECTIONS = {"norm":  u"Нормативные акты и документы", 
            "mono":  u"Монографии на русском языке",
            "arti":  u"Научные публикации", 
            "sprv":  u"Справочная литература",
            "news":  u"Сообщения в средствах массовой информации",
            "hudl":  u"Художественная литература", 
            "site":  u'Интернет"=ресурсы',
            "other": u"Литература на иностранных языках"}

# И оформляются вот так. \item[] вставляет строчку без номера, все 
# остальное - шрифт. На место $ будет вставлен текст заголовка.
STYLE = "\\item[]{\\large\\bf $}\n\n"

# Вот этот способ оформления требует добавки макроса в преамбулу, 
# но теоретически он прогрессивнее.
# STYLE =  "\\begin{extrabibtext}\n\\textbf{$}\\\\[2pt]\n\\end{extrabibtext}\n\n"

# \newenvironment{extrabibtext}{%
#  \normalsize
#  \addvspace{1.25\baselineskip}%
#  \parindent=\normalparindent
#  \parshape=0
#  \item[]%
#}{%
#  \par
#  \vspace{.75\baselineskip}
#}

# Ну, кодировку мы менять не будем, но вообще должен работать с любой...
ENCODING = 'utf-8'

def usage():
    print """
  Запускать после того как BibTeX создаст .bbl по стилю GOST и до
  дальнейшей обработки. Теоретически, будет работать и с другими стилями,
  которые дают .bbl где каждая запись начинается с \\bibitem и все записи
  разделены пустыми строками.

  Потребляет имя проекта, ищет его .bbl и .aux. .bbl изменяет. Внимание!
  Все очень примитивно, формат BibTeX на самом деле не понимается.
  Рассчитано на файлы сохраненные из JabRef, где каждое поле начинается 
  с новой строки и записи разделены пустыми строками.
  """
    print "  Требует дополнительного поля в записи - 'section'"
    for section in SECTIONORDER:
        print "   "+section,"-",SECTIONS[section]
    print """
  В таком порядке они и будут отсортированы, внутри секции - по именам 
  авторов, затем по названиям. Все ресурсы на иностранных языках, т.е. 
  те, у которых это поле отсутствует, идут скопом после них, по крайней
  мере, у нас на соцфаке. Сортировка пока очень тупая. Куда попадет буква
  'Ё' - хер знает, но боюсь что не после 'е'.
  """
    
def sortbyvalue(d):
    """ Returns the keys of dictionary d sorted by their values """
    items=d.items()
    backitems=[ [v[1],v[0]] for v in items]
    backitems.sort()
    return [ backitems[i][1] for i in range(0,len(backitems))]

def sortbib(database):

    splitkeys = {}
    
    for sectiontype in SECTIONORDER:
        splitkeys[sectiontype] = {} 
            
    for key, record in database.iteritems():
        sortline = ""
        if record.has_key('author'):
            sortline +=record['author']
        else:
            # А у кого авторов нет, те идут последними в списке.
            sortline += "0".zfill(80).replace("0",u"\uffff")
        if record.has_key('title'):
            sortline +=record['title']
            # А у кого заголовка нет, тот глюк.
        if record.has_key('section'):
            for sectiontype in SECTIONORDER:
                if record['section'] == sectiontype:
                    splitkeys[sectiontype][key]=sortline
        else:
            splitkeys['other'][key]=sortline
    
    sortedkeys = {}
    
    for section, dic in splitkeys.iteritems():
        sortedkeys[section]=sortbyvalue(dic)

    return sortedkeys

def parseaux(rawname,auxdata):
    bibre = re.compile(r'\\bibdata\{(.+)\}')
    bibfilename = None
    for line in auxdata:
        if line.startswith('\\bibdata'):
            bibrematch = bibre.match(line)
            if bibrematch != None:
                bibfilename = bibrematch.group(1)
    if bibfilename == None:
        print "Не могу найти ссылки на файл с библиографией в .aux"
        sys.exit(4)
    else:
        dir, file = os.path.split(rawname)
        if dir != "":
            dir += "/" 
        return os.path.normpath(dir+bibfilename) 

def readchunk(bbldata,counter):
    chunk = []
    while counter < len(bbldata)-1:
        counter += 1
        chunk.append(bbldata[counter])
        if bbldata[counter].strip() == "":
            break
    return chunk, counter

def cleanline(text,rectype):
    text = text.strip()
    text = text.strip(",")
    cleanlist = ["{", "}", '\\']
    for char in cleanlist:
        text = text.replace(char,"")
    if rectype == 'author':
        text = text.split(' and ')[0]
        loc = text.rfind(', ')
        if loc == -1:
            textparts = text.split('. ')
            if type(textparts) is list:
                text = ""
                text += textparts[-1]
                for part in textparts[0:-1]:
                    text += part
            else:
                textparts = ""
            
            if text[0].upper() != text[0]:
                text = text.split(' ')[1]
    # де Сервантес Сааведра" стал просто Сервантесом и хер с ним.
    
    cleanlist = [" ", ",", ".", "-", "<", ">", ":", "'","&","/"]
    for char in cleanlist:
        text = text.replace(char,"")
    # Это тоже неправильно, но сойдет. Я не нанимался полный 
    # парсер бибтеховых имен писать.    
    
    return text.lower()

def bibparse(bibdata):
    matches = { 'author'  : re.compile(r' *author *= *\{(.+)'),
                'title'   : re.compile(r' *title *= *\{(.+)'),
                'section' : re.compile(r' *section *= *\{(.+)}'),
              }
    keyre = re.compile(r'.*@.+\{(.+),')
    counter = -1
    itemdic = {}
    while counter < len(bibdata)-1:
        item, counter = readchunk(bibdata,counter)
        if item[0].strip().startswith('@'):
            if not item[0].strip().startswith('@comment'):
                keyrematch = keyre.match(item[0])
                if keyrematch != None:
                    key = keyrematch.group(1)
                    data = {}
                    for line in item:
                        for name, pattern in matches.iteritems():
                            matchdata = pattern.match(line)
                            if matchdata != None:
                                text = matchdata.group(1)
                                text = cleanline(text,name)
                                data[name] = text
                    itemdic[key]=data
    return itemdic
                    
                

def bblparse(bbldata):
    # begin.
    bibitemre = re.compile(r'\\bibitem\{(.+)\}')
    counter = -1
    itemdic = {}
    header, counter = readchunk(bbldata,counter)
    footer = []
    while counter < len(bbldata)-1:
        item, counter = readchunk(bbldata,counter)
        if item[0].startswith('\\bibitem'):
            bibmatch = bibitemre.match(item[0])
            if bibmatch != None:
                itemdic[bibmatch.group(1)] = item
        else:
            footer = item
    return header, footer, itemdic

def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:], "", [""])
    except getopt.GetoptError:
        usage()
        sys.exit(2)
    if len(args)!=1:
        usage()
        sys.exit(1)
    if not args[0].upper().endswith('.BBL'):
        rawname = args[0]
    else:
        rawname = args[0][:-4]
    try:
        bblfile = codecs.open(rawname+'.bbl','r',ENCODING)
        auxfile = codecs.open(rawname+'.aux','r',ENCODING)
    except:
        print "Что-то я файлов найти не могу"
        sys.exit(3)
    auxdata = auxfile.readlines()
    auxfile.close()
    bibname = parseaux(rawname,auxdata)
    bbldata = bblfile.readlines()
    bblfile.close()

    try:
        bibfile = codecs.open(bibname+'.bib','r',ENCODING)
    except:
        print bibname+'.bib',"был упомянут, но не открылся."
        sys.exit(5)
    
    bibdata = bibfile.readlines()
    bibfile.close()   
    dbitems = bibparse(bibdata)
    print len(dbitems), "ссылок найдено в .bib"
    
    bibheader, bibfooter, bibitems = bblparse(bbldata)
    print len(bibitems), "ссылок найдено в .bbl"

    sortedkeys = sortbib(dbitems)

    filteredkeys = {}
    
    for section in SECTIONORDER:
        filteredsection = []
        for key in sortedkeys[section]:
            if bibitems.has_key(key):
                filteredsection.append(key)
        filteredkeys[section] = filteredsection 

    # Усе, поехали записывать все обратно.

    bblfile = codecs.open(rawname+'.bbl','w',ENCODING,ENCODING)
    
    for line in bibheader:
        bblfile.write(line)
    
    for section in SECTIONORDER:
        if len(filteredkeys[section]) != 0:
            bblfile.write(STYLE.replace('$',SECTIONS[section]))
            for key in filteredkeys[section]:
                for line in bibitems[key]:
                    bblfile.write(line)
  
    for line in bibfooter:
        bblfile.write(line)

    bblfile.close()    

if __name__ == "__main__":
    main()



  1. Насколько я понимаю, в ВАК уже давно не обращают внимания даже на ГОСТ, но любой диссертационный совет очень дорожит своей репутацией, и старается не допускать, чтобы его диссертантов оттуда заворачивали. ↩︎

  2. Потому что он программируется на еще одном, собственном языке, который невероятно ублюдочен. ↩︎