├── data ├── documents │ └── .gitignore ├── models │ └── .gitignore ├── results │ └── .gitignore └── stop_words │ └── .gitignore ├── .gitignore ├── README.md ├── keywords_tfidf.py ├── corpus.py ├── keywords_lda.py └── LICENSE /data/documents/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /data/models/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /data/results/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /data/stop_words/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Keyword Generator 2 | ================= 3 | 4 | The Keyword Generator, created in collaboration with KB Researcher-in-residence Pim Huijnen, is a command-line tool that offers two methods to extract relevant keywords from a collection of sample texts provided by the user: 5 | 6 | 1) `keywords_tfidf.py`, extracting keywords based on tf-idf scores. Options: 7 | 8 | - -k : number of keywords to be generated (default 10) 9 | - -d : document length (the documents provided by the user will be split into parts containing the specified number of words; by default the documents will not be split.) 10 | 11 | 2) `keywords_lda.py`, extracting keywords based on either [Gensim](https://radimrehurek.com/gensim/)'s or [Mallet](http://mallet.cs.umass.edu)'s implementation of LDA topic modeling. Options: 12 | 13 | - -t : number of topics (default 10) 14 | - -w : number of words per topic (default 10) 15 | - -k : number of keywords (default 10) 16 | - -d : document length (the documents provided by the user will be split into parts containing the specified number of words; by default the documents will not be split.) 17 | - m : mallet path (full path to the [Mallet](http://mallet.cs.umass.edu) executable; if not provided, [Gensim](https://radimrehurek.com/gensim/)'s LDA implementation will be used to generate topics.) 18 | 19 | Documents are to be placed in the `data/documents` folder, stop word lists in the `data/stop_words` folder. The keyword lists and any topics and topic distributions generated will be saved in the `data/results` folder. 20 | 21 | The Keyword Generator currently uses Python 2.7, and [Gensim](https://radimrehurek.com/gensim/) and [Mallet](http://mallet.cs.umass.edu) need to be installed locally. 22 | 23 | Some examples of commands: 24 | ``` 25 | $ ./keywords_tfidf.py 26 | $ ./keywords_tfidf.py -k 20 -d 100 27 | $ ./keywords_lda.py -k 10 -d 100 -t 5 -w 20 28 | $ ./keywords_lda.py -k 10 -d 100 -t 5 -w 20 -m /opt/mallet-2.0.7/bin/mallet 29 | ``` 30 | -------------------------------------------------------------------------------- /keywords_tfidf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # 5 | # Keyword Generator 6 | # 7 | # Copyright (C) 2015 Juliette Lonij, Koninklijke Bibliotheek - 8 | # National Library of the Netherlands 9 | # 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU Lesser General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU Lesser General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU Lesser General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | import argparse 25 | import codecs 26 | import corpus as cp 27 | import csv 28 | import gensim 29 | import math 30 | import operator 31 | import os 32 | import sys 33 | import time 34 | 35 | 36 | # Generate keywords 37 | def generate_keywords(tfidf_scores, num_keywords): 38 | print('Generating keywords...') 39 | keywords = {} 40 | 41 | # Sum of scores for token in all documents 42 | for doc in tfidf_scores: 43 | for t in doc: 44 | key = t[0] 45 | score = t[1] 46 | if key in keywords: 47 | keywords[key] += score 48 | else: 49 | keywords[key] = score 50 | 51 | # Sort keywords by highest score 52 | sorted_keywords = sorted(keywords.items(), key=operator.itemgetter(1), 53 | reverse=True) 54 | 55 | return sorted_keywords[:num_keywords] 56 | 57 | 58 | def print_keywords(keywords, dictionary): 59 | print('Keywords generated:') 60 | for i, k in enumerate(keywords): 61 | print('(%i) %s [%s]' % (i + 1, dictionary.get(k[0]), k[1])) 62 | 63 | 64 | def save_keywords(keywords, dictionary): 65 | timestamp = int(time.time()) 66 | with open('data' + os.sep + 'results' + os.sep + str(timestamp) + 67 | '_keywords' + '.csv', 'wb') as f: 68 | csv_writer = csv.writer(f, delimiter='\t') 69 | for k in keywords: 70 | csv_writer.writerow([dictionary.get(k[0]).encode('utf-8'), 71 | str(k[1])]) 72 | 73 | 74 | if __name__ == '__main__': 75 | if sys.stdout.encoding != 'UTF-8': 76 | sys.stdout = codecs.getwriter('utf-8')(sys.stdout, 'strict') 77 | 78 | parser = argparse.ArgumentParser() 79 | parser.add_argument('-k', required=False, type=int, default=10, 80 | help='number of keywords') 81 | parser.add_argument('-d', required=False, type=int, default=0, 82 | help='document length') 83 | args = parser.parse_args() 84 | 85 | num_keywords, doc_length = vars(args)['k'], vars(args)['d'] 86 | 87 | doc_folder = 'data' + os.sep + 'documents' 88 | stop_folder = 'data' + os.sep + 'stop_words' 89 | 90 | corpus, dictionary = cp.MyCorpus(doc_folder, stop_folder, doc_length).load() 91 | tfidf = gensim.models.TfidfModel(corpus) 92 | tfidf_scores = tfidf[corpus] 93 | keywords = generate_keywords(tfidf_scores, num_keywords) 94 | 95 | print_keywords(keywords, dictionary) 96 | save_keywords(keywords, dictionary) 97 | -------------------------------------------------------------------------------- /corpus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # 5 | # Keyword Generator 6 | # 7 | # Copyright (C) 2015 Juliette Lonij, Koninklijke Bibliotheek - 8 | # National Library of the Netherlands 9 | # 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU Lesser General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU Lesser General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU Lesser General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | import gensim 25 | import os 26 | import warnings 27 | 28 | 29 | # Pre-process input documents 30 | def get_documents(path, doc_length): 31 | print('Processing documents ...') 32 | docs = [] 33 | for filename in os.listdir(path): 34 | if filename.endswith('.txt'): 35 | with open(path + '/' + filename) as f: 36 | s = decode(f.read()) 37 | if not s: 38 | print('Could not decode file contents, skipping.') 39 | continue 40 | if doc_length > 0: 41 | for piece in splitter(doc_length, s): 42 | docs.append(piece) 43 | else: 44 | docs.append(s) 45 | num_docs = len(docs) 46 | print('Number of documents: %i' % num_docs) 47 | return docs 48 | 49 | 50 | # Split large documents into smaller parts 51 | def splitter(n, s): 52 | pieces = s.split() 53 | return (' '.join(pieces[i:i+n]) for i in xrange(0, len(pieces), n)) 54 | 55 | 56 | # Set stop word list 57 | def get_stop_words(path): 58 | print('Getting stop words ...') 59 | stop_words = [] 60 | for filename in os.listdir(path): 61 | if filename.endswith('.txt'): 62 | with open(path + os.sep + filename) as f: 63 | s = decode(f.read()) 64 | if not s: 65 | print('Could not decode file contents, skipping.') 66 | continue 67 | stop_words += [sw.lower() for sw in s.split() if sw] 68 | num_words = len(stop_words) 69 | print('Number of stop words: %i' % num_words) 70 | return stop_words 71 | 72 | 73 | # Try to decode various possible encodings 74 | def decode(s): 75 | encodings = ['utf-8', 'iso-8859-1'] 76 | decoded = None 77 | for e in encodings: 78 | try: 79 | decoded = s.decode(e) 80 | break 81 | except UnicodeDecodeError: 82 | continue 83 | return decoded 84 | 85 | 86 | # Read documents, tokenize 87 | def iter_docs(doc_list, stop_list): 88 | for doc in doc_list: 89 | yield (x for x in gensim.utils.tokenize(doc, lowercase=True, deacc=True, 90 | errors='replace') if x not in stop_list) 91 | 92 | 93 | # Corpus class 94 | class MyCorpus(object): 95 | warnings.filterwarnings('ignore') 96 | model_folder = 'data/models' 97 | 98 | def __init__(self, doc_dir, stop_dir, doc_length): 99 | self.doc_list = get_documents(doc_dir, doc_length) 100 | self.stop_list = get_stop_words(stop_dir) 101 | 102 | print('Generating dictionary ...') 103 | self.dictionary = gensim.corpora.Dictionary(iter_docs(self.doc_list, 104 | self.stop_list)) 105 | no_below = 1 if len(self.doc_list) <= 10 else 2 106 | no_above = 1 if len(self.doc_list) <= 10 else 0.95 107 | self.dictionary.filter_extremes(no_below=no_below, no_above=no_above, 108 | keep_n=100000) 109 | num_tokens = len(self.dictionary.items()) 110 | print('Number of unique tokens in dictionary: %s' % num_tokens) 111 | self.dictionary.save(os.path.join(self.model_folder, 'kwg.dict')) 112 | 113 | print('Generating corpus ...') 114 | gensim.corpora.MmCorpus.serialize(os.path.join(self.model_folder, 115 | 'kwg.mm'), self) 116 | 117 | def __iter__(self): 118 | for tokens in iter_docs(self.doc_list, self.stop_list): 119 | yield self.dictionary.doc2bow(tokens) 120 | 121 | def load(self): 122 | dictionary = gensim.corpora.Dictionary.load(os.path.join(self.model_folder, 123 | 'kwg.dict')) 124 | corpus = gensim.corpora.MmCorpus(os.path.join(self.model_folder, 125 | 'kwg.mm')) 126 | return corpus, dictionary 127 | -------------------------------------------------------------------------------- /keywords_lda.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # 5 | # Keyword Generator 6 | # 7 | # Copyright (C) 2015 Juliette Lonij, Koninklijke Bibliotheek - 8 | # National Library of the Netherlands 9 | # 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU Lesser General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU Lesser General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU Lesser General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | import argparse 25 | import codecs 26 | import corpus as cp 27 | import csv 28 | import gensim 29 | import math 30 | import operator 31 | import os 32 | import sys 33 | import time 34 | 35 | from builtins import input 36 | 37 | 38 | # Exclude topics 39 | def exclude_topics(topics): 40 | print_topics(topics) 41 | inp = input('Enter topics to exclude, separated by commas: ') 42 | if inp == '': 43 | return topics 44 | excl_topics = [int(i.strip()) for i in inp.split(',')] 45 | excl_topics.sort(reverse=True) 46 | for i in excl_topics: 47 | del topics[i-1] 48 | return topics 49 | 50 | 51 | # Print topics 52 | def print_topics(topics): 53 | print('Topics generated:') 54 | for i, topic in enumerate(topics): 55 | print('(' + str(i + 1) + ') ' + ', '.join([t[1] for t in topic])) 56 | 57 | 58 | # Generate keywords 59 | def generate_keywords(corpus, dictionary, topics, num_keywords): 60 | print('Generating keywords...') 61 | keywords = {} 62 | 63 | # Sum of probabilities for token in all topics 64 | for topic in topics: 65 | for t in topic: 66 | token = t[1] 67 | pr = t[0] 68 | if token in keywords: 69 | keywords[token] += pr 70 | else: 71 | keywords[token] = pr 72 | 73 | # Probability for each token multiplied by token frequency 74 | matrix = gensim.matutils.corpus2csc(corpus) 75 | for token, pr in keywords.items(): 76 | for d in dictionary.items(): 77 | if d[1] == token: 78 | token_index = d[0] 79 | break 80 | token_row = matrix.getrow(token_index) 81 | token_freq = token_row.sum(1).item() 82 | keywords[token] = pr * math.log(token_freq) 83 | 84 | # Sort keywords by highest score 85 | sorted_keywords = sorted(keywords.items(), key=operator.itemgetter(1), 86 | reverse=True) 87 | 88 | return sorted_keywords[:num_keywords] 89 | 90 | 91 | def print_keywords(keywords): 92 | print('Keywords generated:') 93 | for i, k in enumerate(keywords): 94 | print('(%i) %s [%s]' % (i + 1, k[0], k[1])) 95 | 96 | 97 | def save_keywords(keywords): 98 | timestamp = int(time.time()) 99 | with open('data' + os.sep + 'results' + os.sep + str(timestamp) + 100 | '_keywords' + '.csv', 'wb') as f: 101 | csv_writer = csv.writer(f, delimiter='\t') 102 | for k in keywords: 103 | csv_writer.writerow([k[0].encode('utf-8'), str(k[1])]) 104 | 105 | 106 | def save_topics(topics): 107 | timestamp = int(time.time()) 108 | with open('data' + os.sep + 'results' + os.sep + str(timestamp) + 109 | '_topics' + '.csv', 'wb') as f: 110 | csv_writer = csv.writer(f, delimiter='\t') 111 | for topic in topics: 112 | csv_writer.writerow([t[1].encode('utf-8') for t in topic]) 113 | csv_writer.writerow([str(t[0]) for t in topic]) 114 | 115 | 116 | def save_distributions(distributions): 117 | timestamp = int(time.time()) 118 | with open('data' + os.sep + 'results' + os.sep + str(timestamp) + 119 | '_distributions' + '.csv', 'wb') as f: 120 | csv_writer = csv.writer(f, delimiter='\t') 121 | csv_writer.writerow(['Document'] + ['Topic ' + str(i + 1) for i in 122 | range(len(distributions[0]))]) 123 | for i, dist in enumerate(distributions): 124 | csv_writer.writerow([str(i)] + ['{0:.5f}'.format(t[1]) for t in 125 | dist]) 126 | 127 | 128 | if __name__ == '__main__': 129 | if sys.stdout.encoding != 'UTF-8': 130 | sys.stdout = codecs.getwriter('utf-8')(sys.stdout, 'strict') 131 | 132 | parser = argparse.ArgumentParser() 133 | parser.add_argument('-t', required=False, type=int, default=10, 134 | help='number of topics') 135 | parser.add_argument('-w', required=False, type=int, default=10, 136 | help='number of words per topic') 137 | parser.add_argument('-k', required=False, type=int, default=10, 138 | help='number of keywords') 139 | parser.add_argument('-d', required=False, type=int, default=10, 140 | help='document length') 141 | parser.add_argument('-m', required=False, type=str, help='Mallet path') 142 | args = parser.parse_args() 143 | 144 | num_keywords, doc_length = vars(args)['k'], vars(args)['d'] 145 | num_topics, num_words = vars(args)['t'], vars(args)['w'] 146 | mallet_path = vars(args)['m'] 147 | 148 | doc_folder = 'data' + os.sep + 'documents' 149 | stop_folder = 'data' + os.sep + 'stop_words' 150 | 151 | corpus, dictionary = cp.MyCorpus(doc_folder, stop_folder, doc_length).load() 152 | 153 | if mallet_path: 154 | print('Generating model with Mallet LDA ...') 155 | lda = gensim.models.wrappers.LdaMallet(mallet_path, corpus=corpus, 156 | id2word=dictionary, num_topics=num_topics) 157 | topics = lda.show_topics(num_topics=num_topics, num_words=num_words, 158 | formatted=False) 159 | distributions = [dist for dist in lda.load_document_topics()] 160 | else: 161 | print('Generating model with Gensim LDA ...') 162 | lda = gensim.models.LdaModel(corpus, id2word=dictionary, 163 | num_topics=num_topics, alpha='auto', chunksize=1, eval_every=1) 164 | gensim_topics = [t[1] for t in lda.show_topics(num_topics=num_topics, 165 | num_words=num_words, formatted=False)] 166 | topics = [[(i[1], i[0]) for i in t] for t in gensim_topics] 167 | distributions = [] 168 | matrix = gensim.matutils.corpus2csc(corpus) 169 | for i in range(matrix.get_shape()[1]): 170 | bow = gensim.matutils.scipy2sparse(matrix.getcol(i).transpose()) 171 | distributions.append(lda.get_document_topics(bow, 0)) 172 | 173 | topics = exclude_topics(topics) 174 | keywords = generate_keywords(corpus, dictionary, topics, num_keywords) 175 | 176 | print_keywords(keywords) 177 | save_keywords(keywords) 178 | save_topics(topics) 179 | save_distributions(distributions) 180 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | --------------------------------------------------------------------------------